proplot-dev / proplot

🎨 A succinct matplotlib wrapper for making beautiful, publication-quality graphics
https://proplot.readthedocs.io
MIT License
1.11k stars 102 forks source link

wrong internal state with title setting and unable to make new figures #348

Closed syrte closed 1 year ago

syrte commented 2 years ago

Description

When I was adjusting the axes titles of a figure, something seemed to break the internal state of proplot. The package does not work anymore, see the error message below. I have no choice but restart the whole python session.

Steps to reproduce

I tried the following code

import proplot as pplt

fig, axs = pplt.subplots(None, 1, 4)
axs[0].format(
    title='title',
    titleloc='c', titlesize=10, titleweight='normal', titlecolor='gray10'
)

It complains "gray10" is not a good color as follows

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
/tmp/ipykernel_125973/2407293224.py in <module>
      2 
      3 fig, axs = pplt.subplots(None, 1, 4)
----> 4 axs[0].format(
      5     title='title',
      6     titleloc='c', titlesize=10, titleweight='normal', titlecolor='gray10'

~/local/conda/envs/nemo/lib/python3.8/site-packages/proplot/axes/cartesian.py in format(self, aspect, xloc, yloc, xspineloc, yspineloc, xoffsetloc, yoffsetloc, xwraprange, ywraprange, xreverse, yreverse, xlim, ylim, xmin, ymin, xmax, ymax, xscale, yscale, xbounds, ybounds, xmargin, ymargin, xrotation, yrotation, xformatter, yformatter, xticklabels, yticklabels, xticks, yticks, xlocator, ylocator, xminorticks, yminorticks, xminorlocator, yminorlocator, xcolor, ycolor, xlinewidth, ylinewidth, xtickloc, ytickloc, fixticks, xtickdir, ytickdir, xtickminor, ytickminor, xtickrange, ytickrange, xtickcolor, ytickcolor, xticklen, yticklen, xticklenratio, yticklenratio, xtickwidth, ytickwidth, xtickwidthratio, ytickwidthratio, xticklabelloc, yticklabelloc, xticklabeldir, yticklabeldir, xticklabelpad, yticklabelpad, xticklabelcolor, yticklabelcolor, xticklabelsize, yticklabelsize, xticklabelweight, yticklabelweight, xlabel, ylabel, xlabelloc, ylabelloc, xlabelpad, ylabelpad, xlabelcolor, ylabelcolor, xlabelsize, ylabelsize, xlabelweight, ylabelweight, xgrid, ygrid, xgridminor, ygridminor, xgridcolor, ygridcolor, xlabel_kw, ylabel_kw, xscale_kw, yscale_kw, xlocator_kw, ylocator_kw, xformatter_kw, yformatter_kw, xminorlocator_kw, yminorlocator_kw, **kwargs)
    987         """
    988         rc_kw, rc_mode = _pop_rc(kwargs)
--> 989         with rc.context(rc_kw, mode=rc_mode):
    990             # No mutable default args
    991             xlabel_kw = xlabel_kw or {}

~/local/conda/envs/nemo/lib/python3.8/site-packages/proplot/config.py in __enter__(self)
    791         rc_old = context.rc_old  # used to re-apply settings without copying whole dict
    792         for key, value in kwargs.items():
--> 793             kw_proplot, kw_matplotlib = self._get_item_dicts(key, value)
    794             for rc_dict, kw_new in zip(
    795                 (rc_proplot, rc_matplotlib),

~/local/conda/envs/nemo/lib/python3.8/site-packages/proplot/config.py in _get_item_dicts(self, key, value, skip_cycle)
    922         # Get validated key, value, and child keys
    923         key, value = self._validate_key(key, value)
--> 924         value = self._validate_value(key, value)
    925         keys = (key,) + rcsetup._rc_children.get(key, ())  # settings to change
    926         contains = lambda *args: any(arg in keys for arg in args)  # noqa: E731

~/local/conda/envs/nemo/lib/python3.8/site-packages/proplot/config.py in _validate_value(key, value)
    884             value = validate_matplotlib[key](value)
    885         elif key in validate_proplot:
--> 886             value = validate_proplot[key](value)
    887         return value
    888 

~/local/conda/envs/nemo/lib/python3.8/site-packages/proplot/internals/rcsetup.py in _validate_color(value, alternative)
    264             or not REGEX_NAMED_COLOR.match(value)
    265         ):
--> 266             raise ValueError(f'{value!r} is not a valid color arg.') from None
    267         return value
    268     except Exception as error:

ValueError: 'gray10' is not a valid color arg.

The problem is that I can not make any new figures since then. When I start a new figure in a jupyter cell

fig, axs = pplt.subplots(None, 1, 4)

I get the following error

Error in callback <function install_repl_displayhook.<locals>.post_execute at 0x7f705b74adc0> (for post_execute):
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
~/local/conda/envs/nemo/lib/python3.8/site-packages/matplotlib/pyplot.py in post_execute()
    137             def post_execute():
    138                 if matplotlib.is_interactive():
--> 139                     draw_all()
    140 
    141             # IPython >= 2

~/local/conda/envs/nemo/lib/python3.8/site-packages/matplotlib/_pylab_helpers.py in draw_all(cls, force)
    135         for manager in cls.get_all_fig_managers():
    136             if force or manager.canvas.figure.stale:
--> 137                 manager.canvas.draw_idle()
    138 
    139 

~/local/conda/envs/nemo/lib/python3.8/site-packages/matplotlib/backend_bases.py in draw_idle(self, *args, **kwargs)
   2053         if not self._is_idle_drawing:
   2054             with self._idle_draw_cntx():
-> 2055                 self.draw(*args, **kwargs)
   2056 
   2057     def get_width_height(self):

~/local/conda/envs/nemo/lib/python3.8/site-packages/proplot/figure.py in _canvas_preprocess(self, *args, **kwargs)
    461         ctx3 = rc.context(fig._render_context)  # draw with figure-specific setting
    462         with ctx1, ctx2, ctx3:
--> 463             fig.auto_layout()
    464             return func(self, *args, **kwargs)
    465 

~/local/conda/envs/nemo/lib/python3.8/site-packages/proplot/figure.py in auto_layout(self, renderer, aspect, tight, resize)
   1474         _align_content()
   1475         if tight:
-> 1476             gs._auto_layout_tight(renderer)
   1477         _align_content()
   1478 

~/local/conda/envs/nemo/lib/python3.8/site-packages/proplot/gridspec.py in _auto_layout_tight(self, renderer)
    818         pad = self._outerpad
    819         obox = fig.bbox_inches  # original bbox
--> 820         bbox = fig.get_tightbbox(renderer)
    821 
    822         # Calculate new figure margins

~/local/conda/envs/nemo/lib/python3.8/site-packages/matplotlib/figure.py in get_tightbbox(self, renderer, bbox_extra_artists)
   1637 
   1638         for a in artists:
-> 1639             bbox = a.get_tightbbox(renderer)
   1640             if bbox is not None and (bbox.width != 0 or bbox.height != 0):
   1641                 bb.append(bbox)

~/local/conda/envs/nemo/lib/python3.8/site-packages/proplot/axes/cartesian.py in get_tightbbox(self, renderer, *args, **kwargs)
   1310         self._apply_axis_sharing()
   1311         self._update_rotation('x')
-> 1312         return super().get_tightbbox(renderer, *args, **kwargs)
   1313 
   1314 

~/local/conda/envs/nemo/lib/python3.8/site-packages/proplot/axes/base.py in get_tightbbox(self, renderer, *args, **kwargs)
   2610         if self._inset_parent is not None and self._inset_zoom:
   2611             self.indicate_inset_zoom()
-> 2612         self._tight_bbox = super().get_tightbbox(renderer, *args, **kwargs)
   2613         return self._tight_bbox
   2614 

~/local/conda/envs/nemo/lib/python3.8/site-packages/matplotlib/axes/_base.py in get_tightbbox(self, renderer, call_axes_locator, bbox_extra_artists, for_layout_only)
   4444                 if bb_yaxis:
   4445                     bb.append(bb_yaxis)
-> 4446         self._update_title_position(renderer)
   4447         axbbox = self.get_window_extent(renderer)
   4448         bb.append(axbbox)

~/local/conda/envs/nemo/lib/python3.8/site-packages/proplot/axes/base.py in _update_title_position(self, renderer)
   2388         # Sync the title position with the a-b-c label position
   2389         aobj = self._title_dict['abc']
-> 2390         tobj = self._title_dict[self._abc_loc]
   2391         aobj.set_transform(tobj.get_transform())
   2392         aobj.set_position(tobj.get_position())

KeyError: None

---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
~/local/conda/envs/nemo/lib/python3.8/site-packages/IPython/core/formatters.py in __call__(self, obj)
    339                 pass
    340             else:
--> 341                 return printer(obj)
    342             # Finally look for special method names
    343             method = get_real_method(obj, self.print_method)

~/local/conda/envs/nemo/lib/python3.8/site-packages/IPython/core/pylabtools.py in retina_figure(fig, base64, **kwargs)
    166         base64 argument
    167     """
--> 168     pngdata = print_figure(fig, fmt="retina", base64=False, **kwargs)
    169     # Make sure that retina_figure acts just like print_figure and returns
    170     # None when the figure is empty.

~/local/conda/envs/nemo/lib/python3.8/site-packages/IPython/core/pylabtools.py in print_figure(fig, fmt, bbox_inches, base64, **kwargs)
    149         FigureCanvasBase(fig)
    150 
--> 151     fig.canvas.print_figure(bytes_io, **kw)
    152     data = bytes_io.getvalue()
    153     if fmt == 'svg':

~/local/conda/envs/nemo/lib/python3.8/site-packages/proplot/figure.py in _canvas_preprocess(self, *args, **kwargs)
    461         ctx3 = rc.context(fig._render_context)  # draw with figure-specific setting
    462         with ctx1, ctx2, ctx3:
--> 463             fig.auto_layout()
    464             return func(self, *args, **kwargs)
    465 

~/local/conda/envs/nemo/lib/python3.8/site-packages/proplot/figure.py in auto_layout(self, renderer, aspect, tight, resize)
   1474         _align_content()
   1475         if tight:
-> 1476             gs._auto_layout_tight(renderer)
   1477         _align_content()
   1478 

~/local/conda/envs/nemo/lib/python3.8/site-packages/proplot/gridspec.py in _auto_layout_tight(self, renderer)
    818         pad = self._outerpad
    819         obox = fig.bbox_inches  # original bbox
--> 820         bbox = fig.get_tightbbox(renderer)
    821 
    822         # Calculate new figure margins

~/local/conda/envs/nemo/lib/python3.8/site-packages/matplotlib/figure.py in get_tightbbox(self, renderer, bbox_extra_artists)
   1637 
   1638         for a in artists:
-> 1639             bbox = a.get_tightbbox(renderer)
   1640             if bbox is not None and (bbox.width != 0 or bbox.height != 0):
   1641                 bb.append(bbox)

~/local/conda/envs/nemo/lib/python3.8/site-packages/proplot/axes/cartesian.py in get_tightbbox(self, renderer, *args, **kwargs)
   1310         self._apply_axis_sharing()
   1311         self._update_rotation('x')
-> 1312         return super().get_tightbbox(renderer, *args, **kwargs)
   1313 
   1314 

~/local/conda/envs/nemo/lib/python3.8/site-packages/proplot/axes/base.py in get_tightbbox(self, renderer, *args, **kwargs)
   2610         if self._inset_parent is not None and self._inset_zoom:
   2611             self.indicate_inset_zoom()
-> 2612         self._tight_bbox = super().get_tightbbox(renderer, *args, **kwargs)
   2613         return self._tight_bbox
   2614 

~/local/conda/envs/nemo/lib/python3.8/site-packages/matplotlib/axes/_base.py in get_tightbbox(self, renderer, call_axes_locator, bbox_extra_artists, for_layout_only)
   4444                 if bb_yaxis:
   4445                     bb.append(bb_yaxis)
-> 4446         self._update_title_position(renderer)
   4447         axbbox = self.get_window_extent(renderer)
   4448         bb.append(axbbox)

~/local/conda/envs/nemo/lib/python3.8/site-packages/proplot/axes/base.py in _update_title_position(self, renderer)
   2388         # Sync the title position with the a-b-c label position
   2389         aobj = self._title_dict['abc']
-> 2390         tobj = self._title_dict[self._abc_loc]
   2391         aobj.set_transform(tobj.get_transform())
   2392         aobj.set_position(tobj.get_position())

KeyError: None

Figure(nrows=1, ncols=4, refwidth=2.5)

Proplot version

Paste the results of import matplotlib; print(matplotlib.__version__); import proplot; print(proplot.version)here. 3.4.3 0.9.5.post284

lukelbd commented 2 years ago

I think I see the problem -- the rc state is not reset by Configurator.__exit__ during the error because the error is triggered inside Configurator.__enter__ rather than inside the with...as block. This leaves rc in a "user-modification" context mode rather than an "initialization" mode, so the initial values for ax._title_loc and ax._abc_loc are unset.

I think the solution is to validate rc values inside of pplt.rc.context rather than inside of pplt.Configurator.__enter__ and simplify the content in __enter__, and/or use a try...except block in __enter__ that calls __exit__ if necessary.

lukelbd commented 1 year ago

This was a very simple fix -- should have done it much sooner. Fixed by c567329 with a simple try-except clause.