Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit b7ccfb6

Browse files
committed
FIX/API: fig.canvas.draw always updates internal state
Previously the non-interactive backends, other than Agg, did not define `draw` methods and fell back to the base no-op version. However, we have been documenting that the correct way to update the various internal state we keep (run tight/constrained layouts, auto limits, text size/position, ...), this is now a bug due to our suggested usage drifting. closes #18407
1 parent 6d629a1 commit b7ccfb6

File tree

6 files changed

+62
-6
lines changed

6 files changed

+62
-6
lines changed

lib/matplotlib/backend_bases.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1584,6 +1584,12 @@ def _draw(renderer): raise Done(renderer)
15841584
figure.canvas = orig_canvas
15851585

15861586

1587+
def _no_output_draw(figure):
1588+
renderer = _get_renderer(figure)
1589+
with renderer._draw_disabled():
1590+
figure.draw(renderer)
1591+
1592+
15871593
def _is_non_interactive_terminal_ipython(ip):
15881594
"""
15891595
Return whether we are in a a terminal IPython, but non interactive.
@@ -1621,7 +1627,9 @@ def _check_savefig_extra_args(func=None, extra_kwargs=()):
16211627
@functools.wraps(func)
16221628
def wrapper(*args, **kwargs):
16231629
name = 'savefig' # Reasonable default guess.
1624-
public_api = re.compile(r'^savefig|print_[A-Za-z0-9]+$')
1630+
public_api = re.compile(
1631+
r'^savefig|print_[A-Za-z0-9]+|_no_output_draw$'
1632+
)
16251633
seen_print_figure = False
16261634
for frame, line in traceback.walk_stack(None):
16271635
if frame is None:
@@ -1632,8 +1640,9 @@ def wrapper(*args, **kwargs):
16321640
frame.f_globals.get('__name__', '')):
16331641
if public_api.match(frame.f_code.co_name):
16341642
name = frame.f_code.co_name
1635-
if name == 'print_figure':
1643+
if name in ('print_figure', '_no_output_draw'):
16361644
seen_print_figure = True
1645+
16371646
else:
16381647
break
16391648

lib/matplotlib/backends/backend_pdf.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
from matplotlib._pylab_helpers import Gcf
2929
from matplotlib.backend_bases import (
3030
_Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase,
31-
GraphicsContextBase, RendererBase)
31+
GraphicsContextBase, RendererBase, _no_output_draw)
3232
from matplotlib.backends.backend_mixed import MixedModeRenderer
3333
from matplotlib.figure import Figure
3434
from matplotlib.font_manager import findfont, get_font
@@ -2730,6 +2730,9 @@ def print_pdf(self, filename, *,
27302730
else: # we opened the file above; now finish it off
27312731
file.close()
27322732

2733+
def draw(self):
2734+
_no_output_draw(self.figure)
2735+
return super().draw()
27332736

27342737
FigureManagerPdf = FigureManagerBase
27352738

lib/matplotlib/backends/backend_pgf.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
from matplotlib import _api, cbook, font_manager as fm
2020
from matplotlib.backend_bases import (
2121
_Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase,
22-
GraphicsContextBase, RendererBase)
22+
GraphicsContextBase, RendererBase, _no_output_draw
23+
)
2324
from matplotlib.backends.backend_mixed import MixedModeRenderer
2425
from matplotlib.backends.backend_pdf import (
2526
_create_pdf_info_dict, _datetime_to_pdf)
@@ -906,6 +907,10 @@ def print_png(self, fname_or_fh, *args, **kwargs):
906907
def get_renderer(self):
907908
return RendererPgf(self.figure, None)
908909

910+
def draw(self):
911+
_no_output_draw(self.figure)
912+
return super().draw()
913+
909914

910915
FigureManagerPgf = FigureManagerBase
911916

lib/matplotlib/backends/backend_ps.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from matplotlib.afm import AFM
2424
from matplotlib.backend_bases import (
2525
_Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase,
26-
GraphicsContextBase, RendererBase)
26+
GraphicsContextBase, RendererBase, _no_output_draw)
2727
from matplotlib.cbook import is_writable_file_like, file_requires_unicode
2828
from matplotlib.font_manager import get_font
2929
from matplotlib.ft2font import LOAD_NO_HINTING, LOAD_NO_SCALE
@@ -1129,6 +1129,9 @@ def _print_figure_tex(
11291129

11301130
_move_path_to_path_or_stream(tmpfile, outfile)
11311131

1132+
def draw(self):
1133+
_no_output_draw(self.figure)
1134+
return super().draw()
11321135

11331136
def convert_psfrags(tmpfile, psfrags, font_preamble, custom_preamble,
11341137
paper_width, paper_height, orientation):

lib/matplotlib/backends/backend_svg.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from matplotlib import _api, cbook
1818
from matplotlib.backend_bases import (
1919
_Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase,
20-
RendererBase)
20+
RendererBase, _no_output_draw)
2121
from matplotlib.backends.backend_mixed import MixedModeRenderer
2222
from matplotlib.colors import rgb2hex
2323
from matplotlib.dates import UTC
@@ -1363,6 +1363,9 @@ def _print_svg(self, filename, fh, *, dpi=None, bbox_inches_restore=None,
13631363
def get_default_filetype(self):
13641364
return 'svg'
13651365

1366+
def draw(self):
1367+
_no_output_draw(self.figure)
1368+
return super().draw()
13661369

13671370
FigureManagerSVG = FigureManagerBase
13681371

lib/matplotlib/tests/test_backend_bases.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,3 +187,36 @@ def test_toolbar_zoompan():
187187
assert ax.get_navigate_mode() == "ZOOM"
188188
ax.figure.canvas.manager.toolmanager.trigger_tool('pan')
189189
assert ax.get_navigate_mode() == "PAN"
190+
191+
192+
@pytest.mark.parametrize("backend", ['svg', 'pgf', 'ps', 'pdf'])
193+
def test_draw(backend):
194+
from matplotlib.figure import Figure
195+
from matplotlib.backends.backend_agg import FigureCanvas
196+
test_backend = pytest.importorskip(
197+
f'matplotlib.backends.backend_{backend}'
198+
)
199+
TestCanvas = test_backend.FigureCanvas
200+
fig_test = Figure(constrained_layout=True)
201+
TestCanvas(fig_test)
202+
axes_test = fig_test.subplots(2, 2)
203+
204+
# defaults to FigureCanvasBase
205+
fig_agg = Figure(constrained_layout=True)
206+
# put a backends.backend_agg.FigureCanvas on it
207+
FigureCanvas(fig_agg)
208+
axes_agg = fig_agg.subplots(2, 2)
209+
210+
init_pos = [ax.get_position() for ax in axes_test.ravel()]
211+
212+
fig_test.canvas.draw()
213+
fig_agg.canvas.draw()
214+
215+
layed_out_pos_test = [ax.get_position() for ax in axes_test.ravel()]
216+
layed_out_pos_agg = [ax.get_position() for ax in axes_agg.ravel()]
217+
218+
for init, placed in zip(init_pos, layed_out_pos_test):
219+
assert not np.allclose(init, placed, atol=0.005)
220+
221+
for ref, test in zip(layed_out_pos_agg, layed_out_pos_test):
222+
np.testing.assert_allclose(ref, test, atol=0.005)

0 commit comments

Comments
 (0)