seaborn icon indicating copy to clipboard operation
seaborn copied to clipboard

so.Est() + tick label formatter => 'PseudoAxis' object has no attribute '_view_interval'

Open stas-sl opened this issue 7 months ago • 8 comments

Hi, thanks for cool library, I especially enjoy using objects interface.

While using it I encountered a small issue when trying to customise tick labels using matplotlib formatter in combination with so.Est(). If I replace it with so.Agg() the issue disappears. I guess this has something to do with the fact that several variables use the same scale (y/ymin/ymax) and _view_interval is not initialised properly in that case?

import seaborn as sns
import seaborn.objects as so
import matplotlib as mpl

fmri = sns.load_dataset("fmri")
(
    so.Plot(fmri, x="timepoint", y="signal", color="event")
        .add(so.Band(), so.Est())
        .scale(y=so.Continuous().label(mpl.ticker.PercentFormatter()))  
)
File ~/.pyenv/versions/miniforge3/lib/python3.9/site-packages/matplotlib/ticker.py:233, in Formatter.format_ticks(self, values)
    231 """Return the tick labels for all the ticks at once."""
    232 self.set_locs(values)
--> 233 return [self(value, i) for i, value in enumerate(values)]

File ~/.pyenv/versions/miniforge3/lib/python3.9/site-packages/matplotlib/ticker.py:233, in <listcomp>(.0)
    231 """Return the tick labels for all the ticks at once."""
    232 self.set_locs(values)
--> 233 return [self(value, i) for i, value in enumerate(values)]

File ~/.pyenv/versions/miniforge3/lib/python3.9/site-packages/matplotlib/ticker.py:1525, in PercentFormatter.__call__(self, x, pos)
   1523 def __call__(self, x, pos=None):
   1524     """Format the tick as a percentage with the appropriate scaling."""
-> 1525     ax_min, ax_max = self.axis.get_view_interval()
   1526     display_range = abs(ax_max - ax_min)
   1527     return self.fix_minus(self.format_pct(x, display_range))

File ~/.pyenv/versions/miniforge3/lib/python3.9/site-packages/seaborn/_core/scales.py:921, in PseudoAxis.get_view_interval(self)
    920 def get_view_interval(self):
--> 921     return self._view_interval

AttributeError: 'PseudoAxis' object has no attribute '_view_interval'

Versions: seaborn: 0.13.0 matplotlib: 3.6.2

stas-sl avatar Dec 05 '23 22:12 stas-sl

Thanks for the reproducible example! I don't know exactly what is causing this but I think you're on the right track with your suggestion. Note that it is also possible to get percent formatting by directly using the seaborn API:

fmri = sns.load_dataset("fmri")
(
    so.Plot(fmri, x="timepoint", y="signal", color="event")
    .add(so.Band(), so.Est())
    .scale(y=so.Continuous().label(like="{x:.1%}"))
)

mwaskom avatar Dec 05 '23 23:12 mwaskom

A similar (but different) error is apparent when trying to use a LogLocator for ticking of bands added with so.Est:

import seaborn as sns
import seaborn.objects as so
import matplotlib as mpl

fmri = sns.load_dataset("fmri")
(
    so.Plot(fmri, x="timepoint", y="signal", color="event")
        .add(so.Band(), so.Est())
        .scale(y=so.Continuous().tick(mpl.ticker.LogLocator()))  
)

Which errors the following AttributeError (mpl 3.8.2, sns 0.13.0):

AttributeError                            Traceback (most recent call last)
File ~\miniconda3\envs\tst\Lib\site-packages\IPython\core\formatters.py:344, in BaseFormatter.__call__(self, obj)
    342     method = get_real_method(obj, self.print_method)
    343     if method is not None:
--> 344         return method()
    345     return None
    346 else:

File ~\miniconda3\envs\tst\Lib\site-packages\seaborn\_core\plot.py:387, in Plot._repr_png_(self)
    385 if Plot.config.display["format"] != "png":
    386     return None
--> 387 return self.plot()._repr_png_()

File ~\miniconda3\envs\tst\Lib\site-packages\seaborn\_core\plot.py:934, in Plot.plot(self, pyplot)
    930 """
    931 Compile the plot spec and return the Plotter object.
    932 """
    933 with theme_context(self._theme_with_defaults()):
--> 934     return self._plot(pyplot)

File ~\miniconda3\envs\tst\Lib\site-packages\seaborn\_core\plot.py:964, in Plot._plot(self, pyplot)
    962 # Process the data for each layer and add matplotlib artists
    963 for layer in layers:
--> 964     plotter._plot_layer(self, layer)
    966 # Add various figure decorations
    967 plotter._make_legend(self)

File ~\miniconda3\envs\tst\Lib\site-packages\seaborn\_core\plot.py:1505, in Plotter._plot_layer(self, p, layer)
   1503 # TODO is this the right place for this?
   1504 for view in self._subplots:
-> 1505     view["ax"].autoscale_view()
   1507 if layer["legend"]:
   1508     self._update_legend_contents(p, mark, data, scales, layer["label"])

File ~\miniconda3\envs\tst\Lib\site-packages\matplotlib\axes\_base.py:2939, in _AxesBase.autoscale_view(self, tight, scalex, scaley)
   2934     # End of definition of internal function 'handle_single_axis'.
   2936 handle_single_axis(
   2937     scalex, self._shared_axes["x"], 'x', self.xaxis, self._xmargin,
   2938     x_stickies, self.set_xbound)
-> 2939 handle_single_axis(
   2940     scaley, self._shared_axes["y"], 'y', self.yaxis, self._ymargin,
   2941     y_stickies, self.set_ybound)

File ~\miniconda3\envs\tst\Lib\site-packages\matplotlib\axes\_base.py:2896, in _AxesBase.autoscale_view.<locals>.handle_single_axis(scale, shared_axes, name, axis, margin, stickies, set_bound)
   2894 # If x0 and x1 are nonfinite, get default limits from the locator.
   2895 locator = axis.get_major_locator()
-> 2896 x0, x1 = locator.nonsingular(x0, x1)
   2897 # Find the minimum minpos for use in the margin calculation.
   2898 minimum_minpos = min(
   2899     getattr(ax.dataLim, f"minpos{name}") for ax in shared)

File ~\miniconda3\envs\tst\Lib\site-packages\matplotlib\ticker.py:2427, in LogLocator.nonsingular(self, vmin, vmax)
   2424     vmin, vmax = 1, 10
   2425 else:
   2426     # Consider shared axises
-> 2427     minpos = min(axis.get_minpos() for axis in self.axis._get_shared_axis())
   2428     if not np.isfinite(minpos):
   2429         minpos = 1e-300  # This should never take effect.

AttributeError: 'PseudoAxis' object has no attribute '_get_shared_axis'

MaozGelbart avatar Dec 13 '23 11:12 MaozGelbart

Probably the same underlying problem but @MaozGelbart I am curious what your usecase is for using a LogLocator on a linear-scale axis?

mwaskom avatar Dec 13 '23 12:12 mwaskom

Probably the same underlying problem but @MaozGelbart I am curious what your usecase is for using a LogLocator on a linear-scale axis?

No such use case, just for ease of reproduction. Can reproduce with this code as well:

import seaborn as sns
import seaborn.objects as so
import matplotlib as mpl

fmri = sns.load_dataset("fmri")
(
    so.Plot(fmri, x="timepoint", y="signal", color="event")
        .add(so.Band(), so.Est())
        .scale(y=so.Continuous(trans="symlog").tick(locator=LogLocator()))
)

MaozGelbart avatar Dec 13 '23 12:12 MaozGelbart

OK got it, thanks!

mwaskom avatar Dec 13 '23 12:12 mwaskom

(I guess, follow-up question, did you have a usecase where LogLocator was necessary to configure something that couldn't be configured through the other parameters to Continuous.tick? Or were you just poking at the issue?)

mwaskom avatar Dec 13 '23 12:12 mwaskom

(I guess, follow-up question, did you have a usecase where LogLocator was necessary to configure something that couldn't be configured through the other parameters to Continuous.tick? Or were you just poking at the issue?)

In the objects interface symlog scale uses a different locator than log scale, so sometimes in this scenario it makes sense to change the locator into a logarithmic one.

MaozGelbart avatar Dec 13 '23 14:12 MaozGelbart