Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interaction between xtick label size and height changes FacetGrid ticks #2293

Open
billbrod opened this issue Sep 22, 2020 · 9 comments
Open

Comments

@billbrod
Copy link

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
Copy link
Author

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?

@mwaskom
Copy link
Owner

mwaskom commented Sep 23, 2020

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
Copy link
Owner

mwaskom commented Sep 23, 2020

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
Copy link
Owner

mwaskom commented Sep 23, 2020

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

@billbrod
Copy link
Author

billbrod commented Sep 23, 2020

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

@mwaskom
Copy link
Owner

mwaskom commented May 24, 2021

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 mwaskom closed this as completed May 24, 2021
@donok1
Copy link

donok1 commented Jun 24, 2021

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

@mwaskom mwaskom reopened this Jun 24, 2021
@mwaskom
Copy link
Owner

mwaskom commented Jun 24, 2021

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?

@donok1
Copy link

donok1 commented Jun 24, 2021

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants