Skip to content

Commit d95337f

Browse files
committed
Qt bugfixes, nbAgg improvements, related pre-processor improvements
1 parent 46607fa commit d95337f

File tree

3 files changed

+112
-70
lines changed

3 files changed

+112
-70
lines changed

proplot/axes.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1190,8 +1190,9 @@ def draw(self, renderer=None, *args, **kwargs):
11901190
def get_size_inches(self):
11911191
"""Returns the width and the height of the axes in inches."""
11921192
width, height = self.figure.get_size_inches()
1193-
width = width * abs(self.get_position().width)
1194-
height = height * abs(self.get_position().height)
1193+
bbox = self.get_position()
1194+
width = width * abs(bbox.width)
1195+
height = height * abs(bbox.height)
11951196
return width, height
11961197

11971198
def get_tightbbox(self, renderer, *args, **kwargs):

proplot/rctools.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -953,8 +953,14 @@ def ipython_matplotlib(backend=None, fmt=None):
953953
Parameters
954954
----------
955955
backend : str, optional
956-
The backend name. Use ``'auto'`` to apply ``%matplotlib inline`` for
957-
notebooks and ``%matplotlib qt`` for all other sessions.
956+
The backend name. The default is ``'auto'``, which applies
957+
``%matplotlib inline`` for notebooks and ``%matplotlib qt`` for
958+
all other sessions.
959+
960+
Note that when using the qt backend on macOS, you may want to prevent
961+
"tabbed" figure windows by navigating to Settings...Dock and changing
962+
"Prefer tabs when opening documents" to "Manually" (see \
963+
`Issue #13164 <https://github.com/matplotlib/matplotlib/issues/13164>`__).
958964
fmt : str or list of str, optional
959965
The inline backend file format(s). Valid formats include ``'jpg'``,
960966
``'png'``, ``'svg'``, ``'pdf'``, and ``'retina'``. This is ignored
@@ -996,8 +1002,9 @@ def ipython_matplotlib(backend=None, fmt=None):
9961002
ipython.magic(f'config InlineBackend.figure_formats = {fmt!r}')
9971003
ipython.magic('config InlineBackend.rc = {}') # no notebook overrides
9981004
ipython.magic('config InlineBackend.close_figures = True') # memory issues
999-
ipython.magic( # use proplot tight layout
1000-
'config InlineBackend.print_figure_kwargs = {"bbox_inches":None}')
1005+
ipython.magic( # use ProPlot tight layout instead
1006+
'config InlineBackend.print_figure_kwargs = {"bbox_inches":None}'
1007+
)
10011008

10021009

10031010
def ipython_autoreload(autoreload=None):

proplot/subplots.py

Lines changed: 98 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -456,8 +456,15 @@ def update(self, **kwargs):
456456
self.set_height_ratios(hratios)
457457

458458
# Validate args
459-
kwargs.pop('ncols', None)
460-
kwargs.pop('nrows', None)
459+
nrows = kwargs.pop('nrows', None)
460+
ncols = kwargs.pop('ncols', None)
461+
nrows_current, ncols_current = self.get_active_geometry()
462+
if (nrows is not None and nrows != nrows_current) or (
463+
ncols is not None and ncols != ncols_current):
464+
raise ValueError(
465+
f'Input geometry {(nrows, ncols)} does not match '
466+
f'current geometry {(nrows_current, ncols_current)}.'
467+
)
461468
self.left = kwargs.pop('left', None)
462469
self.right = kwargs.pop('right', None)
463470
self.bottom = kwargs.pop('bottom', None)
@@ -476,8 +483,8 @@ def update(self, **kwargs):
476483

477484
def _canvas_preprocess(canvas, method):
478485
"""Return a pre-processer that can be used to override instance-level
479-
canvas draw() and print_figure() methods. This applies tight layout and
480-
aspect ratio-conserving adjustments and aligns labels. Required so that
486+
canvas draw_idle() and print_figure() methods. This applies tight layout
487+
and aspect ratio-conserving adjustments and aligns labels. Required so that
481488
the canvas methods instantiate renderers with the correct dimensions.
482489
Note that MacOSX currently `cannot be resized \
483490
<https://github.com/matplotlib/matplotlib/issues/15131>`__."""
@@ -491,28 +498,30 @@ def _canvas_preprocess(canvas, method):
491498
# override bbox and bbox_inches as *properties*, but these are really
492499
# complicated, dangerous, and result in unnecessary extra draws.
493500
def _preprocess(self, *args, **kwargs):
494-
if method == 'draw_idle' and self._is_idle_drawing:
495-
return # copied from source code
496501
fig = self.figure # update even if not stale! needed after saves
497-
if fig.stale and method == 'print_figure':
498-
# Needed for displaying already-drawn inline figures, for
499-
# some reason tight layout algorithm gets it wrong otherwise.
500-
# Concerned that draw_idle() might wait until after
501-
# print_figure() is done, so we use draw().
502+
if method == 'print_figure':
503+
# When re-generating inline figures, the tight layout algorithm
504+
# can get figure size *or* spacing wrong unless we force additional
505+
# draw! Seems to have no adverse effects when calling savefig.
502506
self.draw()
503-
renderer = fig._get_renderer() # any renderer will do for now
504-
for ax in fig._iter_axes():
505-
ax._draw_auto_legends_colorbars() # may insert panels
506-
if rc['backend'] != 'nbAgg':
507-
fig._adjust_aspect() # resizes figure
508-
if fig._auto_tight_layout:
509-
fig._align_axislabels(False) # get proper label offset only
510-
fig._align_labels(renderer) # position labels and suptitle
511-
fig._adjust_tight_layout(renderer)
512-
fig._align_axislabels(True) # slide spanning labels across
513-
fig._align_labels(renderer) # update figure-relative coordinates!
514-
res = getattr(type(self), method)(self, *args, **kwargs)
515-
return res
507+
if fig._is_preprocessing:
508+
return
509+
with fig._context_preprocessing():
510+
renderer = fig._get_renderer() # any renderer will do for now
511+
for ax in fig._iter_axes():
512+
ax._draw_auto_legends_colorbars() # may insert panels
513+
resize = rc['backend'] != 'nbAgg'
514+
if resize:
515+
fig._adjust_aspect() # resizes figure
516+
if fig._auto_tight:
517+
fig._adjust_tight_layout(renderer, resize=resize)
518+
fig._align_axislabels(True)
519+
fig._align_labels(renderer)
520+
fallback = _notNone(
521+
fig._fallback_to_cm, rc['mathtext.fallback_to_cm']
522+
)
523+
with rc.context({'mathtext.fallback_to_cm': fallback}):
524+
return getattr(type(self), method)(self, *args, **kwargs)
516525
return _preprocess.__get__(canvas) # ...I don't get it either
517526

518527

@@ -714,6 +723,7 @@ def _subplots_geometry(**kwargs):
714723
class _hidelabels(object):
715724
"""Hide objects temporarily so they are ignored by the tight bounding box
716725
algorithm."""
726+
# NOTE: This will be removed when labels are implemented with AxesStack!
717727
def __init__(self, *args):
718728
self._labels = args
719729

@@ -811,7 +821,7 @@ def __init__(self,
811821
self._axpad = units(_notNone(axpad, rc['subplots.axpad']))
812822
self._panelpad = units(_notNone(panelpad, rc['subplots.panelpad']))
813823
self._auto_format = autoformat
814-
self._auto_tight_layout = _notNone(tight, rc['tight'])
824+
self._auto_tight = _notNone(tight, rc['tight'])
815825
self._include_panels = includepanels
816826
self._fallback_to_cm = fallback_to_cm
817827
self._ref_num = ref
@@ -996,44 +1006,51 @@ def _adjust_aspect(self):
9961006
return
9971007
ax = self._axes_main[self._ref_num - 1]
9981008
mode = ax.get_aspect()
999-
aspect = None
1000-
if mode == 'equal':
1001-
xscale, yscale = ax.get_xscale(), ax.get_yscale()
1002-
if xscale == 'linear' and yscale == 'linear':
1003-
aspect = 1.0 / ax.get_data_ratio()
1004-
elif xscale == 'log' and yscale == 'log':
1005-
aspect = 1.0 / ax.get_data_ratio_log()
1006-
else:
1007-
pass # matplotlib issues warning, forces aspect == 'auto'
1008-
# Apply aspect
1009-
if aspect is not None:
1010-
aspect = round(aspect * 1e10) * 1e-10
1011-
subplots_kw = self._subplots_kw
1012-
aspect_prev = round(subplots_kw['aspect'] * 1e10) * 1e-10
1013-
if aspect != aspect_prev:
1014-
subplots_kw['aspect'] = aspect
1015-
figsize, gridspec_kw, _ = _subplots_geometry(**subplots_kw)
1016-
self.set_size_inches(figsize, manual=False)
1017-
self._gridspec_main.update(**gridspec_kw)
1018-
1019-
def _adjust_tight_layout(self, renderer):
1009+
if mode != 'equal':
1010+
return
1011+
1012+
# Compare to current aspect
1013+
subplots_kw = self._subplots_kw
1014+
xscale, yscale = ax.get_xscale(), ax.get_yscale()
1015+
if xscale == 'linear' and yscale == 'linear':
1016+
aspect = 1.0 / ax.get_data_ratio()
1017+
elif xscale == 'log' and yscale == 'log':
1018+
aspect = 1.0 / ax.get_data_ratio_log()
1019+
else:
1020+
pass # matplotlib issues warning, forces aspect == 'auto'
1021+
aspect = round(aspect * 1e10) * 1e-10
1022+
aspect_prev = round(subplots_kw['aspect'] * 1e10) * 1e-10
1023+
if aspect == aspect_prev:
1024+
return
1025+
1026+
# Apply new aspect
1027+
subplots_kw['aspect'] = aspect
1028+
figsize, gridspec_kw, _ = _subplots_geometry(**subplots_kw)
1029+
self.set_size_inches(figsize, auto=True)
1030+
self._gridspec_main.update(**gridspec_kw)
1031+
1032+
def _adjust_tight_layout(self, renderer, resize=True):
10201033
"""Applies tight layout scaling that permits flexible figure
10211034
dimensions and preserves panel widths and subplot aspect ratios.
10221035
The `renderer` should be a `~matplotlib.backend_bases.RendererBase`
10231036
instance."""
10241037
# Initial stuff
10251038
axs = self._iter_axes()
1026-
obox = self.bbox_inches # original bbox
1027-
bbox = self.get_tightbbox(renderer)
1028-
gridspec = self._gridspec_main
10291039
subplots_kw = self._subplots_kw
10301040
subplots_orig_kw = self._subplots_orig_kw # tight layout overrides
10311041
if not axs or not subplots_kw or not subplots_orig_kw:
10321042
return
10331043

1044+
# Temporarily disable spanning labels and get correct
1045+
# positions for labels and suptitle
1046+
self._align_axislabels(False)
1047+
self._align_labels(renderer)
1048+
10341049
# Tight box *around* figure
10351050
# Get bounds from old bounding box
10361051
pad = self._pad
1052+
obox = self.bbox_inches # original bbox
1053+
bbox = self.get_tightbbox(renderer)
10371054
left = bbox.xmin
10381055
bottom = bbox.ymin
10391056
right = obox.xmax - bbox.xmax
@@ -1051,6 +1068,7 @@ def _adjust_tight_layout(self, renderer):
10511068
# Get arrays storing gridspec spacing args
10521069
axpad = self._axpad
10531070
panelpad = self._panelpad
1071+
gridspec = self._gridspec_main
10541072
nrows, ncols = gridspec.get_active_geometry()
10551073
wspace, hspace = subplots_kw['wspace'], subplots_kw['hspace']
10561074
wspace_orig = subplots_orig_kw['wspace']
@@ -1125,13 +1143,20 @@ def _adjust_tight_layout(self, renderer):
11251143
jspace[i] = space
11261144
spaces.append(jspace)
11271145

1128-
# Apply new spaces
1146+
# Update geometry solver kwargs
11291147
subplots_kw.update({
11301148
'wspace': spaces[0], 'hspace': spaces[1],
11311149
})
1150+
if not resize:
1151+
width, height = self.get_size_inches()
1152+
subplots_kw = subplots_kw.copy()
1153+
subplots_kw.update(width=width, height=height)
1154+
1155+
# Apply new spacing
11321156
figsize, gridspec_kw, _ = _subplots_geometry(**subplots_kw)
1157+
if resize:
1158+
self.set_size_inches(figsize, auto=True)
11331159
self._gridspec_main.update(**gridspec_kw)
1134-
self.set_size_inches(figsize, manual=False)
11351160

11361161
def _align_axislabels(self, b=True):
11371162
"""Align spanning *x* and *y* axis labels in the perpendicular
@@ -1411,7 +1436,7 @@ def _insert_row_column(
14111436

14121437
# Update figure
14131438
figsize, gridspec_kw, _ = _subplots_geometry(**subplots_kw)
1414-
self.set_size_inches(figsize, manual=False)
1439+
self.set_size_inches(figsize, auto=True)
14151440
if exists:
14161441
gridspec = self._gridspec_main
14171442
gridspec.update(**gridspec_kw)
@@ -1626,16 +1651,17 @@ def set_canvas(self, canvas):
16261651
# `~matplotlib.backend_bases.FigureCanvasBase.print_figure`
16271652
# methods. The latter is called by save() and by the inline backend.
16281653
# See `_canvas_preprocess` for details."""
1629-
# NOTE: Use draw_idle() rather than draw() becuase latter is not
1630-
# always called! For example, MacOSX uses _draw() and nbAgg does
1631-
# not call draw() *or* _draw()! Not sure how it works actually.
1632-
# Should be same because we piggyback draw() which *itself* defers
1633-
# the event. Just make sure to check _is_idle_drawing!
1634-
canvas.draw_idle = _canvas_preprocess(canvas, 'draw_idle')
1654+
# NOTE: Cannot use draw_idle() because it causes *major* complications
1655+
# for qt5 backend. Even though usage is less consistent we *must*
1656+
# use draw() and _draw().
1657+
if hasattr(canvas, '_draw'):
1658+
canvas._draw = _canvas_preprocess(canvas, '_draw')
1659+
else:
1660+
canvas.draw = _canvas_preprocess(canvas, 'draw')
16351661
canvas.print_figure = _canvas_preprocess(canvas, 'print_figure')
16361662
super().set_canvas(canvas)
16371663

1638-
def set_size_inches(self, w, h=None, forward=True, manual=True):
1664+
def set_size_inches(self, w, h=None, forward=True, auto=False):
16391665
# Set the figure size and, if this is being called manually or from
16401666
# an interactive backend, override the geometry tracker so users can
16411667
# use interactive backends. See #76. Undocumented because this is
@@ -1656,11 +1682,19 @@ def set_size_inches(self, w, h=None, forward=True, manual=True):
16561682
width_true, height_true = self.get_size_inches()
16571683
width_trunc = int(self.bbox.width) / self.dpi
16581684
height_trunc = int(self.bbox.height) / self.dpi
1659-
if (manual # have actually seen (width_true, heigh_trunc)!
1660-
and width not in (width_true, width_trunc)
1661-
and height not in (height_true, height_trunc)):
1662-
self._subplots_kw.update(width=width, height=height)
1663-
super().set_size_inches(width, height, forward=forward)
1685+
if auto:
1686+
with self._context_resizing():
1687+
super().set_size_inches(width, height, forward=forward)
1688+
else:
1689+
if ( # can have internal resizing not associated with any draws
1690+
(width not in (width_true, width_trunc)
1691+
or height not in (height_true, height_trunc))
1692+
and not self._is_resizing
1693+
and not self.canvas._is_idle_drawing # standard
1694+
and not getattr(self.canvas, '_draw_pending', None) # pyqt5
1695+
):
1696+
self._subplots_kw.update(width=width, height=height)
1697+
super().set_size_inches(width, height, forward=forward)
16641698

16651699
def _iter_axes(self):
16661700
"""Iterates over all axes and panels in the figure belonging to the

0 commit comments

Comments
 (0)