ThePredator ThePredator - 1 month ago 4
Python Question

Highlight a label in a legend, matplotlib

As of now I am using Matplotlib to generate plots.

The legend on the plot can be tweaked using some parameters (as mentioned in this guide). But I would like to have something specific in the legend, as attached in this image below. enter image description here

I would like to highlight one of the labels in the legend like shown (as of now done using MS paint).

If there are other ways of highlighting a specific label, that would also suffice.

Answer

The answer by FLab is actually quite reasonable given how painful it can be to backtrace the coordinates of the plotted items. However, the demands of publication-grade figures are quite often unreasonable, and seeing matplotlib challenged by MS Paint is a enough good motivation for answering this.

Lets consider this example from the matplotlib gallery as a starting point:

N = 100
x = np.arange(N)
fig = plt.figure()
ax = fig.add_subplot(111)
xx = x - (N/2.0)
plt.plot(xx, (xx*xx)-1225, label='$y=x^2$')
plt.plot(xx, 25*xx, label='$y=25x$')
plt.plot(xx, -25*xx, label='$y=-25x$')
legend = plt.legend()
plt.show()

Once an image has been drawn, we can backtrace the elements in the legend instance to find out their coordinates. There are two difficulties associated with this:

  1. The coordinates we'll get through the get_window_extent method are in pixels, not "data" coordinates, so we'll need to use a transform function. A great overview of the transforms is given here.
  2. Finding a proper boundary is tricky. The legend instance above has two useful attributes, legend.legendHandles and legend.texts - two lists with a list of line artists and text labels respectively. One would need to get a bounding box for both elements, while keeping in mind that the implementation might not be perfect and is backend-specific (c.f. this SO question). This is a proper way to do this, but it's not the one in this answer, because...

.. because luckily in your case the legend items seem to be uniformly separated, so we could just get the legend box, split it into a number of rectangles equal to the number of rows in your legend, and draw one of the rectangles on-screen. Below we'll define two functions, one to get the data coordinates of the legend box, and another one to split them into chunks and draw a rectangle according to an index:

from matplotlib.patches import Rectangle

def get_legend_box_coord(ax, legend):
    """ Returns coordinates of the legend box """
    disp2data = ax.transData.inverted().transform
    box = legend.legendPatch
    # taken from here:
    # http://stackoverflow.com/a/28728709/4118756
    box_pixcoords = box.get_window_extent(ax)
    box_xycoords = [disp2data(box_pixcoords.p0), disp2data(box_pixcoords.p1)]
    box_xx, box_yy = np.array(box_xycoords).T
    return box_xx, box_yy

def draw_sublegend_box(ax, legend, idx):
    nitems = len(legend.legendHandles)
    xx, yy = get_legend_box_coord(ax, legend)
    # assuming equal spacing between legend items:
    y_divisors = np.linspace(*yy, num=nitems+1)
    height = y_divisors[idx]-y_divisors[idx+1]
    width = np.diff(xx)
    lower_left_xy = [xx[0], y_divisors[idx+1]]

    legend_box = Rectangle(
            xy = lower_left_xy,
            width = width,
            height = height,
            fill = False,
            zorder = 10)
    ax.add_patch(legend_box)

Now, calling draw_sublegend_box(ax, legend, 1) produces the following plot: sublegendbox

Note that annotating the legend in such is way is only possible once the figure has been drawn.

Comments