Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Difficulty combining and repositioning the legends of two charts in matplotlib and pandas

I am trying to plot two charts onto one figure, with both charts coming from the same dataframe, but one represented as a stacked bar chart and the other a simple line plot.

When I create the plot using the following code:

combined.iloc[:, 1:10].plot(kind='bar', stacked=True, figsize=(20,10))
combined.iloc[:, 0].plot(kind='line', secondary_y=True, use_index=False, linestyle='-', marker='o')
plt.legend(loc='upper left', fancybox=True, framealpha=1, shadow=True, borderpad=1)
plt.show()

With the combined data frame looking like this:

source data frame

I get the following image:

stacked bar chart with line number of CVEs overlaid

I am trying to combine both legends into one, and position the legend in the upper left hand corner so all the chart is visible.

Can someone explain why plt.legend() only seems to be editing the line chart corresponding to the combined.iloc[:, 0] slice of my combined dataframe? If anyone can see a quick and easy way to combine and reposition the legends please let me know! I'd be most grateful.

like image 415
Dan Harrison Avatar asked Feb 22 '20 12:02

Dan Harrison


1 Answers

Passing True for the argument secondary_y means that the plot will be created on a separate axes instance with twin x-axis, since this creates a different axes instance the solution is generally to create the legend manually, as in the answers to the question linked by @ImportanceOfBeingErnest. If you don't want to create the legend directly you can get around this issue by calling plt.legend() between calls to pandas.DataFrame.plot and storing the result. You can then recover the handles and labels from the two axes instances. The following code is a complete example of this

import pandas as pd
import matplotlib.pyplot as plt
import numpy as np

df = pd.DataFrame({'x' : np.random.random(25), 
                   'y' : np.random.random(25)*5, 
                   'z' : np.random.random(25)*2.5})

df.iloc[:, 1:10].plot(kind='bar', stacked=True)
leg = plt.legend()
df.iloc[:, 0].plot(kind='line', y='x', secondary_y=True)
leg2 = plt.legend()
plt.legend(leg.get_patches()+leg2.get_lines(), 
           [text.get_text() for text in leg.get_texts()+leg2.get_texts()], 
           loc='upper left', fancybox=True, framealpha=1, shadow=True, borderpad=1)
leg.remove()
plt.show()

This will produce

enter image description here

and should be fairly easy to modify to suit your specific use case.

Alternatively, you can use matplotlib.pyplot.figlegend(), but you will need to pass legend = False in all calls to pandas.DataFrame.plot(), i.e.

import pandas as pd
import matplotlib.pyplot as plt
import numpy as np

df = pd.DataFrame({'x' : np.random.random(25), 
                   'y' : np.random.random(25)*5, 
                   'z' : np.random.random(25)*2.5})

df.iloc[:, 1:10].plot(kind='bar', stacked=True, legend=False)
df.iloc[:, 0].plot(kind='line', y='x', secondary_y=True, legend=False)

plt.figlegend(loc='upper left', fancybox=True, framealpha=1, shadow=True, borderpad=1)
plt.show()

This will however default to positioning the legend outside the axes, but you can override the automatic positioning via the bbox_to_anchor argument in calling plt.figlegend().

like image 194
William Miller Avatar answered Oct 18 '22 00:10

William Miller