Search code examples
pythonmatplotliblinetransform

How to determine the size of the longest ticklabels to use in the startposition of a line


In this graph I want a horizontal line between the horizontal detailbars and the totalbar.

The line needs to start at the same position as the first character of the yticklabel of the longest label.

In the example below, I've tried different values myself and optically determined that the used value was ok in this case, but if the longest label was not 'second', but for example 'twentyfirst', then the startposition of the black line would start even more on the left.

How can this be determined or calculated automatically?

I've tried searching for solutions, but this seems a very specific subject.

import matplotlib.pyplot as plt

# Gegevens voor de balken
bartitles = ["First", "Second", "Third"]
barvalues = [4, 3, 5]
total_value = sum(barvalues)

# Figure and axes
fig, ax = plt.subplots()

# Plot bars
y_pos = list(range(1,4))
ax.barh(y_pos, barvalues, align='center', color='darkgrey')


# Totalbar
y_pos_total = 0  # Position of the totalbar
ax.barh(y_pos_total, total_value, align='center', color='blue')
bartitles.append("Total")
y_pos.append(y_pos_total)

# plot ticks and titles
ax.set_yticks(y_pos)
ax.set_yticklabels(bartitles)

# Line which starts outside the horizontal barchart
start_position = -0.115  # Value after optical trying different values to get the start equal to the 'S' of 'Second'
# How can this start_position be calculated when the barnames have other lenghts?

end_position = 1 # with transformed coordinates, 1 is to the end of the drawing canvas
y_coordinate_line = 0.5 
trans = ax.get_yaxis_transform()
ax.plot([start_position, end_position], [y_coordinate_line, y_coordinate_line], color="black", transform=trans, clip_on=False)
plt.show()

enter image description here It is the black line just above the blue bar.


Solution

  • Following this answer to get the bounding boxes, you can convert them to the data coordinates using this answer and then find the leftmost value as your start_position.

    ytickboxes = [l.get_window_extent() for l in ax.get_yticklabels()]
    ytickboxesdatacoords = [l.transformed(ax.transAxes.inverted()) for l in ytickboxes]
    start_position = min([l.x0 for l in ytickboxesdatacoords])
    

    The figure needs to be drawn before the coordinates can be found, so using fig.draw_without_rendering() does that. Since you are also using fig.tight_layout() you will need to call that just after the drawing, since the coordinates change after calling tight_layout().

    In your code, with a longer label, we get:

    import matplotlib.pyplot as plt
    
    bartitles = ["First", "0123456789", "Third"]
    barvalues = [4, 3, 5]
    total_value = sum(barvalues)
    
    # Figure and axes
    fig, ax = plt.subplots()
    
    # Plot bars
    y_pos = list(range(1,4))
    ax.barh(y_pos, barvalues, align='center', color='darkgrey')
    
    
    # Totalbar
    y_pos_total = 0  # Position of the totalbar
    ax.barh(y_pos_total, total_value, align='center', color='blue')
    bartitles.append("Total")
    y_pos.append(y_pos_total)
    
    # plot ticks and titles
    ax.set_yticks(y_pos)
    ax.set_yticklabels(bartitles)
    
    fig.draw_without_rendering()
    fig.tight_layout()
    
    ytickboxes = [l.get_window_extent() for l in ax.get_yticklabels()]
    ytickboxesdatacoords = [l.transformed(ax.transAxes.inverted()) for l in ytickboxes]
    start_position = min([l.x0 for l in ytickboxesdatacoords])
    
    end_position = 1
    y_coordinate_line = 0.5 
    trans = ax.get_yaxis_transform()
    ax.plot([start_position, end_position], [y_coordinate_line, y_coordinate_line], color="black", transform=trans, clip_on=False)
    plt.show()