Add the `annotate_bars` function.

Add a new helper function to automatically place text annotations near
the edges of bars in a bar plot.
master
Damien Goutte-Gattat 2 months ago
parent 5b62023202
commit 0ac012c1e1
  1. 102
      incenp/plotting/util.py

@ -17,6 +17,8 @@
"""Miscellaneous utility functions for plotting tasks."""
from matplotlib.transforms import Bbox
def xdistr(values, width, offset=0, even_max=10, center=False, min_sep=-0.05):
"""Distribute coordinates around an axis.
@ -88,3 +90,103 @@ def get_stars(pvalue):
return '*'
else:
return 'ns'
def annotate_bars(
ax, bars, annotations=None, fmt=None, space=0, text_kw={}, text_kw_reversed={}
):
"""Add text annotations to a set of bars.
This function draws text annotations near the end of previously drawn bars.
The text labels are automatically positioned to take into account the available
space between the bars and the edge of the plot.
:param ax: The plot to draw on
:param bars: The bars to annotate
:param annotations: A list containing the text annotations to draw; if None,
the annotations will be inferred from the bars' values
:param fmt: A format string to apply to the annotations when drawing them
:param space: An extra space to insert between the edge of the bars and the
text labels, in plot units (default to zero)
:param text_kw: Extra text parameters
:param text_kw_reversed: Extra text parameters for labels that end up being
placed inside a bar
"""
renderer = ax.figure.canvas.get_renderer()
trans = ax.transData.inverted()
is_vertical = bars.orientation == 'vertical'
if is_vertical:
ymin, ymax = ax.get_ylim()
is_inverted = ymin > ymax
else:
xmin, xmax = ax.get_xlim()
is_inverted = xmin > xmax
if annotations is None:
if is_vertical:
annotations = [bar.get_height() for bar in bars]
else:
annotations = [bar.get_width() for bar in bars]
if fmt is not None:
annotations = [fmt.format(a) for a in annotations]
for i, annotation in enumerate(annotations):
bar = bars[i]
if is_vertical:
x = bar.get_x() + bar.get_width() / 2
y = bar.get_height()
valign = 'bottom'
halign = 'center'
else:
x = bar.get_width()
y = bar.get_y() + bar.get_height() / 2
valign = 'center'
halign = 'left'
text = ax.text(x, y, annotation, va=valign, ha=halign, **text_kw)
box = Bbox(trans.transform(text.get_window_extent(renderer)))
reverse = False
if is_vertical:
if is_inverted:
if box.y0 - box.height + space < ymin:
# We have enough space below the bars, shift the labels down
ny = box.y0 - box.height + space
else:
# Keep the labels above the bottom of the bars, shift them up a bit
ny = box.y0 - space
reverse = True
else:
if box.y0 + box.height + space > ymax:
# Not enough space above the bars, shift the labels down
# We need to apply a correction to factor in the font depth
ny = box.y0 - box.height - (2 / 12 * box.height) - space
reverse = True
else:
# Keep the labels above the top of the bars, shift them up a bit
ny = box.y0 + space
text.set_y(ny)
else:
# Horizontal bars
if is_inverted:
if box.x0 - box.width + space < xmin:
# We have enough space on the left of the bars,
# shift the labels to the left
nx = box.x0 - box.width + space
else:
# Keep the labels inside the bars, shift them a bit to the right
nx = box.x0 - space
reverse = True
else:
if box.x0 + box.width + space > xmax:
# Not enough space to the right of the bars,
# shift the labels inside
nx = box.x0 - box.width - space
reverse = True
else:
# Keep the labels on the right of the bars, shift them a bit further
nx = box.x0 + space
text.set_x(nx)
if reverse and len(text_kw_reversed) > 0:
text.set(**text_kw_reversed)

Loading…
Cancel
Save