2

I would like to know why depending on how you call plt.plot() on an ax this ax may or may not be able to be modified downstream. Is this a bug in matplotlib or am I misunderstanding something? An example which illustrates the issue is shown below.

I am attempting to modify the legend location downstream of a plotting function call, similar to as discussed here. For whatever reason this seams to be dependent on how I call ax.plot. Here are two examples illustrating the issue

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

def myplot(df, ax, loop_cols):
    if loop_cols:
        for col in df.columns:
            ax.plot(df.loc[:, col])
    else:
        ax.plot(df)
    ax.legend(df.columns)
    return ax

This just amounts to calling ax.plot() repeatedly on the pd.Series, or calling it once on the pd.DataFrame. However depending on how this is called, it results in the inability to later modify the legend, as shown below.

df = pd.DataFrame(np.random.randn(100, 3)).cumsum()
fig = plt.figure()
ax = fig.gca()
myplot(df, ax, loop_cols=True)
ax.legend(loc='center left', bbox_to_anchor=(1, 0.5))
plt.show()

Chart legend properly set to right side

enter image description here

fig = plt.figure()
ax = fig.gca()
myplot(df, ax, loop_cols=False)
ax.legend(loc='center left', bbox_to_anchor=(1, 0.5))
plt.show()

Chart legend not set to appropriate location

enter image description here

This is with matplotlib version 2.1.0

mgilbert
  • 3,495
  • 4
  • 22
  • 39
  • Is there any reason why you aren't using pandas `DataFrame.plot()`? – DavidG Apr 17 '18 at 15:21
  • For this toy example that may be the best option, but the module I'm using this in attempts to use `ax.plot()` to be consistent throughout, since I seem to recall various differences in behaviour related to formatting for `DataFrame.plot` vs `ax.plot` – mgilbert Apr 17 '18 at 15:40
  • Fair enough. What version of matplotlib are you using? No legend appears for me when `loop_cols=False` using `matplotlib 2.1.2` – DavidG Apr 17 '18 at 15:42
  • Edited the answer to include the version – mgilbert Apr 17 '18 at 15:47

2 Answers2

0

I'm not why this behaviour is actually happening.... but you are calling ax.legend twice, once in the function, and once outside it. Altering your function such that the call to ax.legend() inside the function contains all the information solves the problem. This includes passing the legend handles and labels. In the example below I used ax.lines to get the Line2D objects, however if your plots are more complicated you may have to get the list from the call to plot using lines = ax.plot()

If the properties of the legend change then you could modify the function to accept paramters that are passed to ax.legend.

def myplot(df, ax, loop_cols):
    if loop_cols:
        for col in df.columns:
            ax.plot(df.loc[:, col])    
    else:
        ax.plot(df)
    #ax.legend(df.columns)  # modify this line
    ax.legend(ax.lines, df.columns, loc='center left', bbox_to_anchor=(1, 0.5))
    return ax

fig, (ax,ax2) = plt.subplots(1,2,figsize=(6,4))

df = pd.DataFrame(np.random.randn(100, 3)).cumsum()

myplot(df, ax, loop_cols=True)
ax.set_title("loop_cols=True")
#ax.legend(loc='center left', bbox_to_anchor=(1, 0.5))  # No need for this

myplot(df, ax2, loop_cols=False)
ax2.set_title("loop_cols=False")
#ax2.legend(loc='center left', bbox_to_anchor=(1, 0.5))  # No need for this

plt.subplots_adjust(left=0.08,right=0.88,wspace=0.55)
plt.show()

enter image description here

DavidG
  • 24,279
  • 14
  • 89
  • 82
  • I appreciate the work around, but my main issue is that I would like to write the function in a way such that a user can overwrite the default behaviour if they want. While for this case I could expose this as an extra function parameter, tweaking the various properties of an `ax` is quite a large API and thus I would like to be able to return an `ax` that the user can modify. The true goal was somewhat ambiguous in my question, I have edited it to reflect this. – mgilbert Apr 18 '18 at 15:19
0

Legends are not being modified in the above examples, new legends are being created in both cases. The issue relates to how legend() behaves differently depending on if there are labels on the matplotlib.lines.Line2Ds. The relevant section from the docs is

1. Automatic detection of elements to be shown in the legend

The elements to be added to the legend are automatically determined, when you do not pass in any extra arguments.

In this case, the labels are taken from the artist. You can specify them either at artist creation or by calling the :meth:~.Artist.set_label method on the artist::

line, = ax.plot([1, 2, 3], label='Inline label')
ax.legend()

or::

line.set_label('Label via method')
line, = ax.plot([1, 2, 3])
ax.legend()

Specific lines can be excluded from the automatic legend element selection by defining a label starting with an underscore. This is default for all artists, so calling Axes.legend without any arguments and without setting the labels manually will result in no legend being drawn.

2. Labeling existing plot elements

To make a legend for lines which already exist on the axes (via plot for instance), simply call this function with an iterable of strings, one for each legend item. For example::

ax.plot([1, 2, 3])
ax.legend(['A simple line'])

Note: This way of using is discouraged, because the relation between plot elements and labels is only implicit by their order and can easily be mixed up.

In the first case, no labels are set

import pandas as pd
import matplotlib.pyplot as plt


df = pd.DataFrame({'a': [1, 5, 3], 'b': [1, 3, -4]})
fig, axes = plt.subplots(1)
lines = axes.plot(df)
print(lines[0].get_label())
print(lines[1].get_label())

_line0
_line1

So calling legend() the first time with labels falls under case 2.. When legend is called the second time without labels it falls under case 1. As you can see, the Legend instances are different, and the second one rightly complains there are No handles with labels found to put in legend.

l1 = axes.legend(['a', 'b'])
print(repr(l1))
<matplotlib.legend.Legend object at 0x7f05638b7748>

l2 = axes.legend(loc='upper left')
No handles with labels found to put in legend.
print(repr(l2))
<matplotlib.legend.Legend object at 0x7f05638004e0>

In the second case, the lines are properly labelled and therefore the second call to legend() properly infers the labels.

s1 = pd.Series([1, 2, 3], name='a')
s2 = pd.Series([1, 5, 2], name='b')
fig, axes = plt.subplots(1)
line1 = axes.plot(s1)
line2 = axes.plot(s2)
print(line1[0].get_label())
print(line2[0].get_label())

a
b
mgilbert
  • 3,495
  • 4
  • 22
  • 39