What do Line2D objects returned by seaborn.lineplot with hue represent?

Question:

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

# generate data
rng = np.random.default_rng(12)

x = np.linspace(0, np.pi, 50)
y1, y2 = np.sin(x[:25]), np.cos(x[25:])
cat = rng.choice(["a", "b", "c"], 50)

data = pd.DataFrame({"x": x , "y" : y1.tolist() + y2.tolist(), "cat": cat})

# generate figure
fig, ax = plt.subplots(figsize=(8, 5), ncols=2)
g0 = sns.lineplot(x="x", y="y", data=data, hue='cat', ls="--", ax=ax[0])
g1 = sns.lineplot(x="x", y="y", data=data, hue='cat', ls="--", ax=ax[1])

g1.get_lines()[0].set_color('black')

print(ax[0].get_lines())

for line in ax[0].get_lines():
    print(line.get_label())

plt.tight_layout()
plt.show()

This returns a list:

<a list of 6 Line2D objects>

of which the first three objects are the coloured dotted lines in the left subplot in the figure, confirmed by changing the color of one of these lines in the right subplot.

enter image description here

But I am unable to understand what the last three Line2D objects are in the list ax[0].get_lines().

If I try to access the labels of the Line2D objects using

[line.get_label() for line in ax[0].get_lines()]

it gives ['_child0', '_child1', '_child2', 'b', 'a', 'c'].

But the last three Line2D objects in the list don’t behave like an usual Line2D object since

  1. ax[0].get_lines()[-1].set_lw(0.2) did not change anything perceivable in the figure.
  2. I expected ax[0].get_lines()[-1].remove()
    would remove green colored legend line in the left subplot, but it had no effect.

So, what do the last three Line2D objects in the list ax[0].get_lines(), which do not have the string _child in their labels, represent?

Generated with matplotlib (v3.5.1) and seaborn (v0.11.2).

Asked By: medium-dimensional

||

Answers:

These are dummy lines used to create the legend. Seaborn’s legends can be quite complex, due to the many options. You might want to check out this github issue to get an idea about Seaborn trying to create more elaborate legends than what matplotlib currently allows.

If you change the properties of these dummy lines after the legend has been created, you won’t see an effect. However, creating the legend again afterwards will show the change.

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

# generate data
rng = np.random.default_rng(12)

x = np.linspace(0, np.pi, 50)
y1, y2 = np.sin(x[:25]), np.cos(x[25:])
x = np.round(x / 2, 1) * 2
cat = rng.choice(["a", "b", "c"], 50)

data = pd.DataFrame({"x": x, "y": y1.tolist() + y2.tolist(), "cat": cat})

# generate figure
fig, ax = plt.subplots(figsize=(8, 5), ncols=2)
g0 = sns.lineplot(x="x", y="y", data=data, hue='cat', ls="--", ax=ax[0])
g1 = sns.lineplot(x="x", y="y", data=data, hue='cat', ls="--", ax=ax[1])

g1.get_lines()[0].set_color('black')
g1.get_lines()[3].set_color('purple')
g1.get_lines()[3].set_linewidth(5)
g1.legend()  # create the legend again

plt.tight_layout()
plt.show()

the dummy line objects are used for the legend

PS: If you just want to change the legend symbols (called "handles" in matplotlib), you can change them directly, e.g.:

for h in g1.legend_.legendHandles:
     h.set_linestyle("--")

The reason that, in this example, the legend doesn’t automatically apply the linestyle, is that the ls= parameter gets sent directly to matplotlib for drawing the lines, but not to the code to create seaborn’s legend. In a future seaborn version, this will probably be tackled. As seaborn allows many combinations of options, it is non-trivial to get this working satisfactory for all those combinations. See also open issue 2861.

Answered By: JohanC
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.