seaborn
seaborn copied to clipboard
Interaction between xtick label size and height changes FacetGrid ticks
I've found what seems to be a weird bug where some interaction between the height
argument of FacetGrid and the rcParam xtick.labelsize
can result in xtick labels being visible on the upper facets of a multi-row FacetGrid, like so:
The upper facets should not have any xlabels on them, yet they do.
Code to reproduce this example:
- Generate data
N = 6
x = np.linspace(0, 10)
x = np.vstack(N*[x])
A = np.random.rand(N) * np.eye(N)
b = np.random.rand(N, 1)
y = A@x+b
i = np.array([i*np.ones_like(x[i]) for i in range(N)]).flatten()
data = {'x': x.flatten(), 'y': y.flatten(), 'i': i}
tmp = pd.DataFrame(data)
- Create plot above.
with sns.axes_style('white'), sns.plotting_context('notebook', rc={'xtick.labelsize': 10}):
g = sns.FacetGrid(tmp, col='i', col_wrap=3, height=2.2)
g.map(plt.plot, 'x', 'y')
Changing either height or the xtick.labelsize
can change the behavior:
- Create proper plot:
with sns.axes_style('white'), sns.plotting_context('notebook', rc={'xtick.labelsize': 10}):
g = sns.FacetGrid(tmp, col='i', col_wrap=3, height=2.5)
g.map(plt.plot, 'x', 'y')
- Create another proper plot:
with sns.axes_style('white'), sns.plotting_context('notebook', rc={'xtick.labelsize': 12}):
g = sns.FacetGrid(tmp, col='i', col_wrap=3, height=2.2)
g.map(plt.plot, 'x', 'y')
- Mess up a plot using textsize 12:
with sns.axes_style('white'), sns.plotting_context('notebook', rc={'xtick.labelsize': 12}):
g = sns.FacetGrid(tmp, col='i', col_wrap=3, height=2.6)
g.map(plt.plot, 'x', 'y')
Weirdly that last one looks different when I save it as a png or svg vs view it in a notebook (where 4 and 6 are visible on the upper facets, but not 10).
This is all on Ubuntu 18.04, with seaborn version 0.11.0 and matplotlib version 3.0.3 (svg backend)
Actually, going back through my example, it appears the above is not sufficient to reproduce it. You'd also need to run the following: plt.style.use({'figure.subplot.right': .96, 'figure.subplot.left': .075})
Which makes me think this issue might be too niche to be of real concern. But it is weird -- how does seaborn determine whether to show the xticks / xticklabels on the upper facets?
I think that if this is a bug (and it seems to be?) it's probably a bug at the matplotlib layer.
As far as I recall (the FacetGrid
code is mostly quite old) it's not doing anything that related to this. The ticklabels are hidden as part of matplotlib's sharex/sharey logic, and seaborn doesn't do anything explicitly beyond setting that when calling plt.subplots
.
With that said, I'm having trouble reproducing using straight matplotlib, e.g.
plt.style.use({'figure.subplot.right': .96, 'figure.subplot.left': .075})
with sns.axes_style('white'), sns.plotting_context('notebook', rc={'xtick.labelsize': 12}):
f, axs = plt.subplots(2, 3, sharex=True, sharey=True, figsize=(3 * 2.6, 2 * 2.6))
f.tight_layout()
for i, ax in enumerate(axs.flat):
data = tmp[tmp["i"] == i]
ax.plot(data["x"], data["y"])
ax.set_title(f"i = {i}")
plt.setp(axs[:, 0], ylabel="y")
plt.setp(axs[-1, :], xlabel="x")
f.tight_layout()
sns.despine()
but the problem seems very narrow so it make take some futzing to get the order of operations exact...
FWIW mapping a plotting function doesn't seem required:
plt.style.use({'figure.subplot.right': .96, 'figure.subplot.left': .075})
with sns.axes_style('white'), sns.plotting_context('notebook', rc={'xtick.labelsize': 12}):
g = sns.FacetGrid(tmp, col='i', col_wrap=3, height=2.6)
reproduces it (slightly differently)
Actually ... just noticed that your example uses col_wrap
which actually does do a little bit extra: https://github.com/mwaskom/seaborn/blob/master/seaborn/axisgrid.py#L464
It does seem to be something with col_wrap
, since the problem doesn't happen if you remove it:
N = 6
x = np.linspace(0, 10)
x = np.vstack(N*[x])
A = np.random.rand(N) * np.eye(N)
b = np.random.rand(N, 1)
y = A@x+b
i = np.array([i*np.ones_like(x[i]) for i in range(N)]).flatten()
data = {'x': x.flatten(), 'y': y.flatten(), 'i': i, 'j': i//2, 'k': np.round((i/2)+.1)-i//2}
tmp = pd.DataFrame(data)
plt.style.use({'figure.subplot.right': .96, 'figure.subplot.left': .075})
with sns.axes_style('white'), sns.plotting_context('notebook', rc={'xtick.labelsize': 12}):
g = sns.FacetGrid(tmp, col='j', row='k', height=2.6)
It looks like the main difference is that when col_wrap
is not None, sharex
and sharey
are set to the first axis, whereas when it's None, they're set to True
.
That said, I am unable reproduce the problem in a similar way to how col_wrap operates using pure matplotlib
plt.style.use({'figure.subplot.right': .96, 'figure.subplot.left': .075})
with sns.axes_style('white'), sns.plotting_context('notebook', rc={'xtick.labelsize': 12}):
f = plt.figure(figsize=(3*2.6, 2*2.6))
axs = np.empty(6, object)
axs[0] = f.add_subplot(2, 3, 1)
for i in range(1, 6):
axs[i] = f.add_subplot(2, 3, i+1, sharex=axs[0], sharey=axs[0])
axs = axs.reshape((2, 3))
f.tight_layout()
for i, ax in enumerate(axs.flat):
data = tmp[tmp["i"] == i]
ax.plot(data["x"], data["y"])
ax.set_title(f"i = {i}")
plt.setp(axs[:, 0], ylabel="y")
plt.setp(axs[-1, :], xlabel="x")
f.tight_layout()
for ax in axs[0, :]:
for label in ax.get_xticklabels():
label.set_visible(False)
ax.xaxis.offsetText.set_visible(False)
for ax in axs[:, 1]:
for label in ax.get_yticklabels():
label.set_visible(False)
ax.yaxis.offsetText.set_visible(False)
for ax in axs[:, 2]:
for label in ax.get_yticklabels():
label.set_visible(False)
ax.yaxis.offsetText.set_visible(False)
sns.despine()
Finally: I dropped some print statements in that if sharex
block you linked, and it appears like, at that point, not all xtick labels get returned by ax.get_xticklabels()
, and so the label.set_visible(False)
ignores the ones that remains visible (10 in the examples above). It's unclear to me why that would be the case though....
I'm finding that I can't reproduce this on current versions of things, so I am going to close as a weird upstream problem. Ping if you run into it again.
Hi @mwaskom,
I recently updated seaborn from v.010 and found this problem again, but strangely only when using sns context "poster"
.
Here the example bothering me:
import seaborn as sns
import pandas as pd
sns.set(style="whitegrid", palette="Pastel2", context="poster")
# initialise some data
data = {'X':[2019, 2020, 2021,2019, 2020, 2021,2019, 2020, 2021,2019, 2020, 2021,2019, 2020, 2021,2019, 2020, 2021,2019, 2020, 2021,2019, 2020, 2021],
'Y':['AAA', 'AAA', 'AAA', 'AAA','AAA', 'AAA', 'BBB', 'BBB','BBB','BBB','BBB','BBB','CCC','CCC','CCC','CCC','CCC','CCC','DDD','DDD','DDD','DDD','DDD','DDD'],
'H':['B2','B1','B2','B1','B2','B1','B2','B1','B2','B1','B2','B1','B2','B1','B2','B1','B2','B1','B2','B1','B2','B1','B2','B1'],
'Z':[20, 21, 19, 18,20, 21, 19, 18,20, 21, 19, 18, 18,20, 21, 19, 18, 18,20, 21, 19, 18,20,21]}
df = pd.DataFrame(data)
g = sns.FacetGrid(df, col='Y',
col_wrap=2,
)
g.map(sns.pointplot,
'X', 'Z', 'H',
order=[2019, 2020, 2021],
hue_order=['B1','B2'],
palette='Pastel2',
ci=None);
g.set(xlabel=None,ylabel=None);
The last xtick label, here "2021" is still visible on the upper graphs.
But for other context it disappears. Here with context = talk
:
This probably comes from some strange issue with the size formating, but I don't know matplotlib enought to work it out. Thanks for work on seaborn !
Versions used:
Python 3.9.5
sns.__version__ = 0.11.1
matplotlib.__version__ = 3.4.2
Thanks @donok1 that's reproducible in my dev environment too. I really have no clue what might be causing this, but here is an experiment that may shed some insight:
- Add the following line to the end of the script to show the full extent that the ticklabel objects are occupying:
plt.setp(g.axes[-1].get_xticklabels(), backgroundcolor=(1, 0, 0, .5))
Play around with font_scale
in sns.set
to get a gradual change in font size. The threshold for the problem is about font_scale=1.906
. And that is just about where the extent boxes start to overlap the actual text of their adjacent lablels. It's not perfect, so this may be a red herring. But it's better than being totally clueless?
Thanks @mwaskom for the great tip.
It also works when I tweak the size of the graphs with height
directly in FacetGrid. That will do well enough for now.
And for sure, any think you know is better than being clueless ! :)