seaborn icon indicating copy to clipboard operation
seaborn copied to clipboard

Interaction between xtick label size and height changes FacetGrid ticks

Open billbrod opened this issue 3 years ago • 9 comments

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:

test

The upper facets should not have any xlabels on them, yet they do.

Code to reproduce this example:

  1. 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)
  1. 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:

  1. 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')

test

  1. 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')

test

  1. 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')

test

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)

billbrod avatar Sep 22 '20 22:09 billbrod

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?

billbrod avatar Sep 22 '20 22:09 billbrod

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...

mwaskom avatar Sep 23 '20 00:09 mwaskom

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)

mwaskom avatar Sep 23 '20 00:09 mwaskom

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

mwaskom avatar Sep 23 '20 00:09 mwaskom

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)

test

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()

test

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....

billbrod avatar Sep 23 '20 15:09 billbrod

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.

mwaskom avatar May 24 '21 19:05 mwaskom

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. download

But for other context it disappears. Here with context = talk: download

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

donok1 avatar Jun 24 '21 10:06 donok1

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?

mwaskom avatar Jun 24 '21 11:06 mwaskom

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 ! :)

donok1 avatar Jun 24 '21 14:06 donok1