Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to add bar and line under the same label in a legend?

I made a plot with 2 Y axes, where one has a bar chart and the other one lines and I would like to add a common legend for both plots.

If I add the following:

axis1.legend([(bar1, line1), (bar2, line2)], ['Solution 1', 'Solution 2'], 
             loc='upper left', numpoints=1)

I almost get what I want, except for the markers / artists (not sure which term is suitable here), which are overlapping for both solutions as seen here:

Overlapping Artists

Is it possible to have the bar artist first and the line artist second, side by side, and then the label?

I suppose this has to do with the legend handler, according to the official Legend guide the tuple handler "simply plots the handles on top of one another for each item in the given tuple". Could anyone help me in writing a new custom handler which plots the handles side by side?

Edit: This question seems quite similar to this.

like image 285
JoKo Avatar asked Oct 31 '22 09:10

JoKo


1 Answers

As you have quoted from the paragraph on legend handlers in the legend guide, the handles of the items in each tuple contained in your list of tuples are being plotted on top of one another. So it would seem that the solution should be to take out the handles from the tuples: changing this [(bar1, line1), (bar2, line2)] to this [bar1, bar2, line1, line2]. But then you would face the issue that you need to have a corresponding number of labels, because with ['Solution 1', 'Solution 2'] the legend will only show the bar1, bar2 keys.

To avoid this issue, you could use the legend_handler_HandlerTuple class presented in the last example shown in the aforementioned paragraph on legend handlers. Your code for the legend would then look something like this (note that I rename bar1, bar2 to bars1, bars2 in all the following code snippets; the legend parameters are set so that the format is like in the image shown further below):

import matplotlib.pyplot as plt
from matplotlib.legend_handler import HandlerTuple
...

# Import data, draw plots
...
axis1.legend([(bar1, line1), (bar2, line2)], ['Solution 1', 'Solution 2'],
             loc='upper left', edgecolor='black', borderpad=0.7, handlelength=7,
             handletextpad=1.5, handler_map={tuple: HandlerTuple(ndivide=None, pad=1.5)})

This does the job but there is a more elegant alternative to this. Let me illustrate with a complete example based on the image you have provided and on this example for the code for the bars. From the code snippet you have shared, I imagine that your code is structured somewhat like this:

import numpy as np                 # v 1.19.2
import matplotlib.pyplot as plt    # v 3.3.2

# Compute variables to plot
var_bars1 = np.arange(1, 8)
var_bars2 = var_bars1/2
var_line1 = 2+var_bars1**1.3
var_line2 = 5+var_bars2**1.6
x = np.arange(len(var_bars1))

# Create grouped bar chart
bar_width = 0.3
fig, axis1 = plt.subplots(figsize=(9, 6))
# notice that the bar plot functions return a BarContainer object which contains
# the colored rectangles (stored as matplotlib.patches.Rectangle objects)
bars1 = axis1.bar(x-bar_width/2, var_bars1, bar_width, label='Solution 1')
bars2 = axis1.bar(x+bar_width/2, var_bars2, bar_width, label='Solution 2')
axis1.set_xlabel('x both', labelpad=15, fontsize=12)
axis1.set_ylabel('y bars', labelpad=15, fontsize=12)

# Create line chart over bar chart using same x axis
axis2 = axis1.twinx()
# notice that the plot functions return a list containing the single line (with
# markers), the comma after the variable name unpacks the list
line1, = axis2.plot(x, var_line1, 'kx', linestyle='-', ms=8, label='Solution 1')
line2, = axis2.plot(x, var_line2, 'ko', linestyle='-', ms=8, label='Solution 2')
axis2.set_ylabel('y lines', labelpad=15, fontsize=12)

Here is how to create the legend without using the HandlerTuple class:

# Create custom legend using handles extracted from plots and manually
# inserting the corresponding labels, leaving strings empty for the first
# column of legend handles (aka legend keys) and adding columnspacing=0 to
# remove the extra whitespace created by the empty strings
axis1.legend((bars1, bars2, line1, line2), ('', '', 'Solution 1', 'Solution 2'),
             loc='upper left', ncol=2, handlelength=3, edgecolor='black',
             borderpad=0.7, handletextpad=1.5, columnspacing=0)

The parameters for the legend are detailed in the documentation here. Before showing what the output looks like, it is worth noting that this code can be simplified in a way that automates the legend creation. It can be rewritten in a way to avoid having to store the output of each plotting function as a new variable by making use of the get_legend_handles_labels function, like this:

fig, axis1 = plt.subplots(figsize=(9, 6))

# Create grouped bar chart
axis1.bar(x-bar_width/2, var_bars1, bar_width, label='Solution 1')
axis1.bar(x+bar_width/2, var_bars2, bar_width, label='Solution 2')
axis1.set_xlabel('x both', labelpad=15, fontsize=12)
axis1.set_ylabel('y bars', labelpad=15, fontsize=12)

# Create line chart over bar chart using same x axis
axis2 = axis1.twinx()
axis2.plot(x, var_line1, 'kx', linestyle='-', ms=8, label='Solution 1')
axis2.plot(x, var_line2, 'ko', linestyle='-', ms=8, label='Solution 2')
axis2.set_ylabel('y lines', labelpad=15, fontsize=12)

handles1, labels1 = axis1.get_legend_handles_labels()
handles2, labels2 = axis2.get_legend_handles_labels()

# Create custom legend by unpacking tuples containing handles and using 
# only one set of unpacked labels along with set of unpacked empty strings
# (using None instead of empty strings gives the same result)
axis1.legend((*handles1, *handles2), (*len(labels1)*[''], *labels2),
             loc='upper left', ncol=2, handlelength=3, edgecolor='black',
             borderpad=0.7, handletextpad=1.5, columnspacing=0)

plt.show()

legend_labels

On a final note, it can be useful to know that seeing as there are two axes in this figure, it is technically possible to draw a single legend by overlapping the legends of both axes like in the following example, though this obviously requires a lot of manual tweaking to format the handles and labels as wanted.

# Create legend by overlapping the legends from both axes by adjusting the 
# bbox location and size, and by removing labels of the first legend
leg1 = axis1.legend(loc='upper left', bbox_to_anchor=(0., 0.5, 0.25, 0.5),
                    edgecolor='black', mode="expand")
for text in leg1.texts:
    text.set_visible(False)
axis2.legend(bbox_to_anchor=(0.05, 0.5, 0.2, 0.5), frameon=False)

Further legend functionalities can be accessed through the legend methods listed in the documentation on the legend class.

like image 149
Patrick FitzGerald Avatar answered Nov 09 '22 18:11

Patrick FitzGerald