5

The issue

I have a plot with 2 y-axes, each corresponding to a set of lines. The solid lines correspond to the left y-axis, and the dashed lines correspond to the right y-axis. I also have a legend and I want it to use only the solid lines as the keys since the dashed lines have the same labels, dependent on their color.

The issue is when I plot a legend for the solid lines, followed by the code for the dashed lines, the grid lines show through the legend. I need to have the grid lines specified for both axes since they won't show up otherwise, and if I move the legend to the dashed lines, it uses the dashed lines as keys instead. I don't want to change my plotting order either.

The code and plot

#Plot
x= np.arange(0,3)
fig,ax = plt.subplots(figsize=(6,6))
#DOD
dod1 = ax.plot(x, ctrl_dod,  color='r',       label='CTRL'  )
dod2 = ax.plot(x, mfkc_dod,  color='#e68a00', label='MFKC'  )
dod3 = ax.plot(x, gses_dod,  color='green',   label='GSES'  )
dod4 = ax.plot(x, gses3_dod, color='blue',    label='GSES-3')
dod5 = ax.plot(x, gses4_dod, color='purple',  label='GSES-4')
dod6 = ax.plot(x, mera_dod,  color='brown',   label='MERRA2')
ax.xaxis.grid(True)
ax.set_ylim([0.02,0.044])
ax.set_yticks(np.arange(0.02,0.045,0.004))
ax.set_xlabel('Month')
ax.set_ylabel('Dust Optical Depth (550 nm)')
ax.set_title('Global Mean DOD and DCM')
legend = ax.legend()
legend.get_frame().set_facecolor('white')

#DCM
ax2  = ax.twinx()
dcm1 = ax2.plot(x, ctrl_dcm*1e6,  color='r',       linestyle='--', label='CTRL'  )
dcm2 = ax2.plot(x, mfkc_dcm*1e6,  color='#e68a00', linestyle='--', label='MFKC'  )
dcm3 = ax2.plot(x, gses_dcm*1e6,  color='green',   linestyle='--', label='GSES'  )
dcm4 = ax2.plot(x, gses3_dcm*1e6, color='blue',    linestyle='--', label='GSES-3')
dcm5 = ax2.plot(x, gses4_dcm*1e6, color='purple',  linestyle='--', label='GSES-4')
dcm6 = ax2.plot(x, mera_dcm*1e6,  color='brown',   linestyle='--', label='MERRA2')
ax2.xaxis.grid(True)
ax2.yaxis.grid(True)
ax2.set_xlabel('Month')
ax2.set_ylabel('Dust Column Mass (mg m-2)')

#Limits
axes = plt.gca()
axes.set_xlim([-0.25,2.25])

#Labels
axes.set_xticks(x)
axes.set_xticklabels(['June','July','August'])

#Save
pylab.savefig('dod+dcm.png')

enter image description here

The question

How can I

a) have the legend keys use solid lines and

b) have the background for the legend opaque white?

Cebbie
  • 1,741
  • 6
  • 23
  • 37
  • 1
    "have the legend keys use solid lines" - The legend keys in the image provided are already solid lines, aren't they? – Vinícius Figueiredo Jul 26 '17 at 20:00
  • I do not understand "I need to have the grid lines specified for both axes since they won't show up otherwise". Using only ax.grid() and not show a grid for ax2 should directly solve the issue. If not, we need a [mcve] of the issue. – ImportanceOfBeingErnest Jul 26 '17 at 20:01
  • Importance- Okay, that actually works for this particular example because the axes match. Yesterday though, I had different grid lines for the left and right axes, which is why I needed the yaxis.grid(True) set for ax and ax2. It would still be helpful maybe to someone else to have an answer if the grids don't match. – Cebbie Jul 26 '17 at 20:08
  • You need to make the patch of the top axes transparent. – Mad Physicist Jul 26 '17 at 20:10
  • I see, my answer below would be independ of grids being present or not. So it will work in both cases. – ImportanceOfBeingErnest Jul 26 '17 at 20:15
  • Vinicius- Yes, because I set that legend for the first axis, but that's also why the grid lines show through. I specified "have the legend keys use solid lines" because I need the legend to come after ax2 so the grid lines don't show, but I don't want the legend to use ax2's dashed lines as the keys. – Cebbie Jul 26 '17 at 20:19

2 Answers2

5

You may create the legend for the second axes, but use the handles from the first axes.

h, l = ax.get_legend_handles_labels()
legend = ax2.legend(h,l, facecolor="white")
ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712
  • I get an error for that second line- "TypeError: __init__() got an unexpected keyword argument 'facecolor'". But if I take out that facecolor bit and add "legend.get_frame().set_facecolor('white')" to a new line, it works! Modify your answer and I'll accept it. – Cebbie Jul 26 '17 at 20:16
  • [`facecolor`](https://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.legend) is a documented argument of legend. If you are using an old version of matplotlib, you may of course use a different way of setting the facecolor. – ImportanceOfBeingErnest Jul 26 '17 at 20:18
  • That's entirely possible since I'm using a network-wide installation of Python, and I don't know what version it is. I'll accept your answer since the handles bit was what I really needed. – Cebbie Jul 26 '17 at 20:21
  • This does not work if I want a legend for ax and a second legend for ax2. When creating one of the two with this method the second disappears. – Bastian Feb 17 '21 at 14:42
  • ... but in that case the method provided by @ImportanceOfBeingErnest over at https://github.com/matplotlib/matplotlib/issues/3706#issuecomment-378407795 helps – Bastian Feb 17 '21 at 19:23
0

I came here, because I had the same problem but wanted two separate legends, one for each of the twin axes.

The accepted answer provided by @ImportanceOfBeingErnest then no longer works because it only permits one legend being attached to ax2.

In this case there is an alternative solution also provided by @ImportanceOfBeingErnest over at https://github.com/matplotlib/matplotlib/issues/3706#issuecomment-378407795.

I cast them both into one single function that I usually use in this case and thought it would be useful to provide it here:

def legend_to_ax( ax, ax_placein=None, method=2, **kwargs ):
    """ Wrapper around ax.legend(**kwargs) which permits to have the legend
    placed in the axis ax_placein.
    This is useful when drawing legends for multiple axes, e.g.produced with
    ax2 = ax1.twinx(), in order to have all legends on top of all axes.
    In this case provide the uppermost axis, e.g. ax2 here, as ax_placein.
    Args:
     - ax         : axis for which the legend is created
     - ax_placein : axis which the legend should be placed in (optional)
     - method     : method by which to handle the placement of the legend in
                    ax_placein.
                    method=1 ... based on 
                                 https://stackoverflow.com/a/45336414/7042795
                                 This method fails when multiple legends should
                                 be added to ax_placein.
                    method=2 ... based on
                                 https://github.com/matplotlib/matplotlib/issues/3706#issuecomment-378407795
                                 This adds the legend as an artist to ax_placein
                                 making it no longer appear via ax.get_legend().
                                 Instead parse ax_placein.artists()
     - **kwargs   : kwargs of ax.legend()
    Returns:
     - leg        : legend handle
    """

    if ax_placein is None:
        leg = ax.legend(**kwargs)
    
    elif method==1:
        # based on https://stackoverflow.com/a/45336414/7042795
        h, l = ax.get_legend_handles_labels()
        if ax_placein is not None:
            ax = ax_placein
        leg =  ax.legend( h, l, **kwargs)
    
    elif method==2:
        # based on https://github.com/matplotlib/matplotlib/issues/3706#issuecomment-378407795
        leg = ax.legend(**kwargs)
        if ax_placein is not None:
            leg.remove()
            ax_placein.add_artist(leg)
    
    return leg

So, with this function you can simply do

legend = legend_to_ax( ax, ax_placein=ax2, method=1, facecolor="white")

or

legend = legend_to_ax( ax, ax_placein=ax2, method=2, facecolor="white")

In case you do want two legends this should work:

legend1 = legend_to_ax( ax,  ax_placein=ax2, method=2, facecolor="white", loc="upper left")
legend2 = legend_to_ax( ax2, ax_placein=ax2, method=2, facecolor="white", loc="upper right")

I hope this is an appropriate place to share this information. Credits are due to @ImportanceOfBeingErnest though. I only merged their solution into this function.

Bastian
  • 901
  • 7
  • 23