From 5821a244edd1d645d4c3d10740521e9cd0d12508 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 28 Aug 2021 13:35:31 -0400 Subject: [PATCH 01/61] API: rename draw_no_output to draw_without_rendering closes #20001 --- ...put.rst => fig_draw_without_rendering.rst} | 14 ++++---- lib/matplotlib/backend_bases.py | 2 +- lib/matplotlib/backends/backend_pdf.py | 2 +- lib/matplotlib/backends/backend_pgf.py | 2 +- lib/matplotlib/backends/backend_ps.py | 2 +- lib/matplotlib/backends/backend_svg.py | 2 +- lib/matplotlib/figure.py | 2 +- lib/matplotlib/tests/test_axes.py | 2 +- lib/matplotlib/tests/test_colorbar.py | 6 ++-- .../tests/test_constrainedlayout.py | 32 +++++++++---------- lib/matplotlib/tests/test_dates.py | 4 +-- lib/matplotlib/tests/test_figure.py | 8 ++--- lib/matplotlib/tests/test_mathtext.py | 4 +-- lib/matplotlib/tests/test_units.py | 12 +++---- 14 files changed, 47 insertions(+), 47 deletions(-) rename doc/users/next_whats_new/{fig_draw_no_output.rst => fig_draw_without_rendering.rst} (66%) diff --git a/doc/users/next_whats_new/fig_draw_no_output.rst b/doc/users/next_whats_new/fig_draw_without_rendering.rst similarity index 66% rename from doc/users/next_whats_new/fig_draw_no_output.rst rename to doc/users/next_whats_new/fig_draw_without_rendering.rst index 293c6590b8c9..ed4360be7260 100644 --- a/doc/users/next_whats_new/fig_draw_no_output.rst +++ b/doc/users/next_whats_new/fig_draw_without_rendering.rst @@ -1,10 +1,10 @@ -Figure now has draw_no_output method ------------------------------------- +Figure now has draw_without_rendering method +-------------------------------------------- -Rarely, the user will want to trigger a draw without making output to -either the screen or a file. This is useful for determining the final +Rarely, the user will want to trigger a draw without making output to +either the screen or a file. This is useful for determining the final position of artists on the figure that require a draw, like text artists. This could be accomplished via ``fig.canvas.draw()`` but has side effects, -sometimes requires an open file, and is documented on an object most users -do not need to access. The `.Figure.draw_no_output` is provided to trigger -a draw without pushing to the final output, and with fewer side effects. \ No newline at end of file +sometimes requires an open file, and is documented on an object most users +do not need to access. The `.Figure.draw_without_rendering` is provided to trigger +a draw without pushing to the final output, and with fewer side effects. diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 21ff162062d0..150b8e7f497b 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -1570,7 +1570,7 @@ def _draw(renderer): raise Done(renderer) def _no_output_draw(figure): # _no_output_draw was promoted to the figure level, but # keep this here in case someone was calling it... - figure.draw_no_output() + figure.draw_without_rendering() def _is_non_interactive_terminal_ipython(ip): diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index 9980d49af85b..9ca791db0c5a 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -2793,7 +2793,7 @@ def print_pdf(self, filename, *, file.close() def draw(self): - self.figure.draw_no_output() + self.figure.draw_without_rendering() return super().draw() diff --git a/lib/matplotlib/backends/backend_pgf.py b/lib/matplotlib/backends/backend_pgf.py index 3f1cb7b172eb..749da559662c 100644 --- a/lib/matplotlib/backends/backend_pgf.py +++ b/lib/matplotlib/backends/backend_pgf.py @@ -883,7 +883,7 @@ def get_renderer(self): return RendererPgf(self.figure, None) def draw(self): - self.figure.draw_no_output() + self.figure.draw_without_rendering() return super().draw() diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index f13e114a815b..93d0705ae363 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -1119,7 +1119,7 @@ def _print_figure_tex( _move_path_to_path_or_stream(tmpfile, outfile) def draw(self): - self.figure.draw_no_output() + self.figure.draw_without_rendering() return super().draw() diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index 904cca7bf313..e4de85905ca7 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -1343,7 +1343,7 @@ def get_default_filetype(self): return 'svg' def draw(self): - self.figure.draw_no_output() + self.figure.draw_without_rendering() return super().draw() diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index abe8ec694922..dbd879521f25 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -2808,7 +2808,7 @@ def draw(self, renderer): self.canvas.draw_event(renderer) - def draw_no_output(self): + def draw_without_rendering(self): """ Draw the figure with no output. Useful to get the final size of artists that require a draw before their size is known (e.g. text). diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 509910bdeec1..32b0c202478b 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -4793,7 +4793,7 @@ def test_reset_ticks(fig_test, fig_ref): labelsize=14, labelcolor='C1', labelrotation=45, grid_color='C2', grid_alpha=0.8, grid_linewidth=3, grid_linestyle='--') - fig.draw_no_output() + fig.draw_without_rendering() # After we've changed any setting on ticks, reset_ticks will mean # re-creating them from scratch. This *should* appear the same as not diff --git a/lib/matplotlib/tests/test_colorbar.py b/lib/matplotlib/tests/test_colorbar.py index 055c4acb7642..5f19a0aaf20a 100644 --- a/lib/matplotlib/tests/test_colorbar.py +++ b/lib/matplotlib/tests/test_colorbar.py @@ -613,7 +613,7 @@ def test_mappable_2d_alpha(): # the original alpha array assert cb.alpha is None assert pc.get_alpha() is x - fig.draw_no_output() + fig.draw_without_rendering() def test_colorbar_label(): @@ -766,7 +766,7 @@ def test_inset_colorbar_layout(): cax = ax.inset_axes([1.02, 0.1, 0.03, 0.8]) cb = fig.colorbar(pc, cax=cax) - fig.draw_no_output() + fig.draw_without_rendering() # make sure this is in the figure. In the colorbar swapping # it was being dropped from the list of children... np.testing.assert_allclose(cb.ax.get_position().bounds, @@ -806,7 +806,7 @@ def test_aspects(): pc = ax[mm, nn].pcolormesh(np.arange(100).reshape(10, 10)) cb[nn][mm] = fig.colorbar(pc, ax=ax[mm, nn], orientation=orient, aspect=aspect, extend=extend) - fig.draw_no_output() + fig.draw_without_rendering() # check the extends are right ratio: np.testing.assert_almost_equal(cb[0][1].ax.get_position().height, cb[0][0].ax.get_position().height * 0.9, diff --git a/lib/matplotlib/tests/test_constrainedlayout.py b/lib/matplotlib/tests/test_constrainedlayout.py index 007fac6ec1f9..a8222a73d5ee 100644 --- a/lib/matplotlib/tests/test_constrainedlayout.py +++ b/lib/matplotlib/tests/test_constrainedlayout.py @@ -128,7 +128,7 @@ def test_constrained_layout7(): for gs in gsl: fig.add_subplot(gs) # need to trigger a draw to get warning - fig.draw_no_output() + fig.draw_without_rendering() @image_comparison(['constrained_layout8.png']) @@ -309,7 +309,7 @@ def test_constrained_layout18(): ax2 = ax.twinx() example_plot(ax) example_plot(ax2, fontsize=24) - fig.draw_no_output() + fig.draw_without_rendering() assert all(ax.get_position().extents == ax2.get_position().extents) @@ -321,7 +321,7 @@ def test_constrained_layout19(): example_plot(ax2, fontsize=24) ax2.set_title('') ax.set_title('') - fig.draw_no_output() + fig.draw_without_rendering() assert all(ax.get_position().extents == ax2.get_position().extents) @@ -341,11 +341,11 @@ def test_constrained_layout21(): fig, ax = plt.subplots(constrained_layout=True) fig.suptitle("Suptitle0") - fig.draw_no_output() + fig.draw_without_rendering() extents0 = np.copy(ax.get_position().extents) fig.suptitle("Suptitle1") - fig.draw_no_output() + fig.draw_without_rendering() extents1 = np.copy(ax.get_position().extents) np.testing.assert_allclose(extents0, extents1) @@ -355,11 +355,11 @@ def test_constrained_layout22(): """#11035: suptitle should not be include in CL if manually positioned""" fig, ax = plt.subplots(constrained_layout=True) - fig.draw_no_output() + fig.draw_without_rendering() extents0 = np.copy(ax.get_position().extents) fig.suptitle("Suptitle", y=0.5) - fig.draw_no_output() + fig.draw_without_rendering() extents1 = np.copy(ax.get_position().extents) np.testing.assert_allclose(extents0, extents1) @@ -407,7 +407,7 @@ def test_hidden_axes(): # (as does a gridspec slot that is empty) fig, axs = plt.subplots(2, 2, constrained_layout=True) axs[0, 1].set_visible(False) - fig.draw_no_output() + fig.draw_without_rendering() extents1 = np.copy(axs[0, 0].get_position().extents) np.testing.assert_allclose( @@ -433,7 +433,7 @@ def test_colorbar_align(): fig.set_constrained_layout_pads(w_pad=4 / 72, h_pad=4 / 72, hspace=0.1, wspace=0.1) - fig.draw_no_output() + fig.draw_without_rendering() if location in ['left', 'right']: np.testing.assert_allclose(cbs[0].ax.get_position().x0, cbs[2].ax.get_position().x0) @@ -475,7 +475,7 @@ def test_colorbars_no_overlapH(): def test_manually_set_position(): fig, axs = plt.subplots(1, 2, constrained_layout=True) axs[0].set_position([0.2, 0.2, 0.3, 0.3]) - fig.draw_no_output() + fig.draw_without_rendering() pp = axs[0].get_position() np.testing.assert_allclose(pp, [[0.2, 0.2], [0.5, 0.5]]) @@ -483,7 +483,7 @@ def test_manually_set_position(): axs[0].set_position([0.2, 0.2, 0.3, 0.3]) pc = axs[0].pcolormesh(np.random.rand(20, 20)) fig.colorbar(pc, ax=axs[0]) - fig.draw_no_output() + fig.draw_without_rendering() pp = axs[0].get_position() np.testing.assert_allclose(pp, [[0.2, 0.2], [0.44, 0.5]]) @@ -528,7 +528,7 @@ def test_align_labels(): fig.align_ylabels(axs=(ax3, ax1, ax2)) - fig.draw_no_output() + fig.draw_without_rendering() after_align = [ax1.yaxis.label.get_window_extent(), ax2.yaxis.label.get_window_extent(), ax3.yaxis.label.get_window_extent()] @@ -541,22 +541,22 @@ def test_align_labels(): def test_suplabels(): fig, ax = plt.subplots(constrained_layout=True) - fig.draw_no_output() + fig.draw_without_rendering() pos0 = ax.get_tightbbox(fig.canvas.get_renderer()) fig.supxlabel('Boo') fig.supylabel('Booy') - fig.draw_no_output() + fig.draw_without_rendering() pos = ax.get_tightbbox(fig.canvas.get_renderer()) assert pos.y0 > pos0.y0 + 10.0 assert pos.x0 > pos0.x0 + 10.0 fig, ax = plt.subplots(constrained_layout=True) - fig.draw_no_output() + fig.draw_without_rendering() pos0 = ax.get_tightbbox(fig.canvas.get_renderer()) # check that specifying x (y) doesn't ruin the layout fig.supxlabel('Boo', x=0.5) fig.supylabel('Boo', y=0.5) - fig.draw_no_output() + fig.draw_without_rendering() pos = ax.get_tightbbox(fig.canvas.get_renderer()) assert pos.y0 > pos0.y0 + 10.0 assert pos.x0 > pos0.x0 + 10.0 diff --git a/lib/matplotlib/tests/test_dates.py b/lib/matplotlib/tests/test_dates.py index c440003f49c9..3a38516c7272 100644 --- a/lib/matplotlib/tests/test_dates.py +++ b/lib/matplotlib/tests/test_dates.py @@ -76,7 +76,7 @@ def test_date_empty(): # http://sourceforge.net/tracker/?func=detail&aid=2850075&group_id=80706&atid=560720 fig, ax = plt.subplots() ax.xaxis_date() - fig.draw_no_output() + fig.draw_without_rendering() np.testing.assert_allclose(ax.get_xlim(), [mdates.date2num(np.datetime64('2000-01-01')), mdates.date2num(np.datetime64('2010-01-01'))]) @@ -85,7 +85,7 @@ def test_date_empty(): mdates.set_epoch('0000-12-31') fig, ax = plt.subplots() ax.xaxis_date() - fig.draw_no_output() + fig.draw_without_rendering() np.testing.assert_allclose(ax.get_xlim(), [mdates.date2num(np.datetime64('2000-01-01')), mdates.date2num(np.datetime64('2010-01-01'))]) diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index 7a222f20a058..317528879304 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -412,11 +412,11 @@ def test_autofmt_xdate(which): @mpl.style.context('default') def test_change_dpi(): fig = plt.figure(figsize=(4, 4)) - fig.draw_no_output() + fig.draw_without_rendering() assert fig.canvas.renderer.height == 400 assert fig.canvas.renderer.width == 400 fig.dpi = 50 - fig.draw_no_output() + fig.draw_without_rendering() assert fig.canvas.renderer.height == 200 assert fig.canvas.renderer.width == 200 @@ -1082,10 +1082,10 @@ def test_subfigure_ticks(): ax3 = subfig_bl.add_subplot(gs[0, 3:14], sharey=ax1) fig.set_dpi(120) - fig.draw_no_output() + fig.draw_without_rendering() ticks120 = ax2.get_xticks() fig.set_dpi(300) - fig.draw_no_output() + fig.draw_without_rendering() ticks300 = ax2.get_xticks() np.testing.assert_allclose(ticks120, ticks300) diff --git a/lib/matplotlib/tests/test_mathtext.py b/lib/matplotlib/tests/test_mathtext.py index 768db940c756..0055d54a03a8 100644 --- a/lib/matplotlib/tests/test_mathtext.py +++ b/lib/matplotlib/tests/test_mathtext.py @@ -402,7 +402,7 @@ def test_default_math_fontfamily(): prop2 = text2.get_fontproperties() assert prop2.get_math_fontfamily() == 'cm' - fig.draw_no_output() + fig.draw_without_rendering() def test_argument_order(): @@ -427,7 +427,7 @@ def test_argument_order(): prop4 = text4.get_fontproperties() assert prop4.get_math_fontfamily() == 'dejavusans' - fig.draw_no_output() + fig.draw_without_rendering() def test_mathtext_cmr10_minus_sign(): diff --git a/lib/matplotlib/tests/test_units.py b/lib/matplotlib/tests/test_units.py index c7a06e0e32ac..a6f6b44c9707 100644 --- a/lib/matplotlib/tests/test_units.py +++ b/lib/matplotlib/tests/test_units.py @@ -226,17 +226,17 @@ def test_empty_default_limits(quantity_converter): munits.registry[Quantity] = quantity_converter fig, ax1 = plt.subplots() ax1.xaxis.update_units(Quantity([10], "miles")) - fig.draw_no_output() + fig.draw_without_rendering() assert ax1.get_xlim() == (0, 100) ax1.yaxis.update_units(Quantity([10], "miles")) - fig.draw_no_output() + fig.draw_without_rendering() assert ax1.get_ylim() == (0, 100) fig, ax = plt.subplots() ax.axhline(30) ax.plot(Quantity(np.arange(0, 3), "miles"), Quantity(np.arange(0, 6, 2), "feet")) - fig.draw_no_output() + fig.draw_without_rendering() assert ax.get_xlim() == (0, 2) assert ax.get_ylim() == (0, 30) @@ -244,20 +244,20 @@ def test_empty_default_limits(quantity_converter): ax.axvline(30) ax.plot(Quantity(np.arange(0, 3), "miles"), Quantity(np.arange(0, 6, 2), "feet")) - fig.draw_no_output() + fig.draw_without_rendering() assert ax.get_xlim() == (0, 30) assert ax.get_ylim() == (0, 4) fig, ax = plt.subplots() ax.xaxis.update_units(Quantity([10], "miles")) ax.axhline(30) - fig.draw_no_output() + fig.draw_without_rendering() assert ax.get_xlim() == (0, 100) assert ax.get_ylim() == (28.5, 31.5) fig, ax = plt.subplots() ax.yaxis.update_units(Quantity([10], "miles")) ax.axvline(30) - fig.draw_no_output() + fig.draw_without_rendering() assert ax.get_ylim() == (0, 100) assert ax.get_xlim() == (28.5, 31.5) From 1a9f0a2bad6e47ec8388f62fe9d202a25c648a4a Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 28 Aug 2021 14:13:01 -0400 Subject: [PATCH 02/61] DOC: re-word the whats new entry --- .../fig_draw_without_rendering.rst | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/doc/users/next_whats_new/fig_draw_without_rendering.rst b/doc/users/next_whats_new/fig_draw_without_rendering.rst index ed4360be7260..03db87ac444b 100644 --- a/doc/users/next_whats_new/fig_draw_without_rendering.rst +++ b/doc/users/next_whats_new/fig_draw_without_rendering.rst @@ -1,10 +1,12 @@ -Figure now has draw_without_rendering method --------------------------------------------- +Figure now has ``draw_without_rendering`` method +------------------------------------------------ -Rarely, the user will want to trigger a draw without making output to -either the screen or a file. This is useful for determining the final -position of artists on the figure that require a draw, like text artists. -This could be accomplished via ``fig.canvas.draw()`` but has side effects, -sometimes requires an open file, and is documented on an object most users -do not need to access. The `.Figure.draw_without_rendering` is provided to trigger -a draw without pushing to the final output, and with fewer side effects. +Rarely, the user will want to trigger a draw without rendering to either the +screen or a file. This is useful for determining the final position of artists +on the figure that require a draw, like text artists, or resolve deferred +computation like automatic data limits. This can be done by +``fig.canvas.draw()``, which forces a full draw and rendering, however this has +side effects, sometimes requires an open file, and is doing more work than is +needed. The `.Figure.draw_without_rendering` method is provided to run the +code in Matplotlib that updates values that are computed at draw-time and get +accurate dimensions of the Artists more efficiently. From 5a58788235d23d2707bb570f867a10de212401e6 Mon Sep 17 00:00:00 2001 From: takimata Date: Fri, 25 Jun 2021 11:43:55 +0200 Subject: [PATCH 03/61] Support sketch_params in pgf backend Fixes #20516 PGF's `random steps` decoration seems to be the most similar, but does not exactly match the behaviour described in matplotlib's docs. Therefore I repurposed the `randomness` argument as a seed to give control on how the line looks afterwards. --- lib/matplotlib/artist.py | 11 +++++++++++ lib/matplotlib/backends/backend_pgf.py | 15 +++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index 152c0ea33ff5..f7a3189cc6ee 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -677,6 +677,17 @@ def set_sketch_params(self, scale=None, length=None, randomness=None): """ Set the sketch parameters. + Requires the following preamble when using the PGF backend: + + \\usepackage{pgf} + \\usepgfmodule{decorations} + \\usepgflibrary{decorations.pathmorphing} + + This also applies to PGF backend + PDF output, where this must be added + to `pgf.preamble` manually. The PGF backend uses the `randomness` + argument as a seed and not as described below. Pass the same seed to + obtain the same random shape. + Parameters ---------- scale : float, optional diff --git a/lib/matplotlib/backends/backend_pgf.py b/lib/matplotlib/backends/backend_pgf.py index 3f1cb7b172eb..859d818d07c6 100644 --- a/lib/matplotlib/backends/backend_pgf.py +++ b/lib/matplotlib/backends/backend_pgf.py @@ -600,6 +600,21 @@ def _print_pgf_path(self, gc, path, transform, rgbFace=None): r"{\pgfqpoint{%fin}{%fin}}" % coords) + # apply pgf decorators + sketch_params = gc.get_sketch_params() if gc else None + if sketch_params is not None: + # Only "length" directly maps to "segment length" in PGF's API + # The others are combined in "amplitude" -> Use "randomness" as + # PRNG seed to allow the user to force the same shape on multiple + # sketched lines + scale, length, randomness = sketch_params + if scale is not None: + writeln(self.fh, r"\pgfkeys{/pgf/decoration/.cd, " + f"segment length = {(length * f):f}in, " + f"amplitude = {(scale * f):f}in}}") + writeln(self.fh, f"\\pgfmathsetseed{{{int(randomness)}}}") + writeln(self.fh, r"\pgfdecoratecurrentpath{random steps}") + def _pgf_path_draw(self, stroke=True, fill=False): actions = [] if stroke: From f67e6048adf870d18bdf901794d5963d62ac1c6b Mon Sep 17 00:00:00 2001 From: takimata Date: Fri, 25 Jun 2021 11:53:47 +0200 Subject: [PATCH 04/61] Fix docstring --- lib/matplotlib/artist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index f7a3189cc6ee..de5051847c5d 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -684,7 +684,7 @@ def set_sketch_params(self, scale=None, length=None, randomness=None): \\usepgflibrary{decorations.pathmorphing} This also applies to PGF backend + PDF output, where this must be added - to `pgf.preamble` manually. The PGF backend uses the `randomness` + to *pgf.preamble* manually. The PGF backend uses the *randomness* argument as a seed and not as described below. Pass the same seed to obtain the same random shape. From 92e9fb659f1a6e85f51678b1237cc447426d929f Mon Sep 17 00:00:00 2001 From: takimata Date: Wed, 30 Jun 2021 14:06:33 +0200 Subject: [PATCH 05/61] Add test --- lib/matplotlib/tests/test_backend_pgf.py | 35 ++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/lib/matplotlib/tests/test_backend_pgf.py b/lib/matplotlib/tests/test_backend_pgf.py index a463c96e61fc..11a5fb52fef3 100644 --- a/lib/matplotlib/tests/test_backend_pgf.py +++ b/lib/matplotlib/tests/test_backend_pgf.py @@ -337,3 +337,38 @@ def test_minus_signs_with_tex(fig_test, fig_ref, texsystem): mpl.rcParams["pgf.texsystem"] = texsystem fig_test.text(.5, .5, "$-1$") fig_ref.text(.5, .5, "$\N{MINUS SIGN}1$") + + +@pytest.mark.backend("pgf") +def test_sketch_params(): + fig, ax = plt.subplots(figsize=[3, 3]) + ax.set_xticks([]) + ax.set_yticks([]) + ax.set_frame_on(False) + handle = ax.plot([0, 1])[0] + handle.set_sketch_params(scale=5, length=30, randomness=42) + + with BytesIO() as fd: + fig.savefig(fd, format='pgf') + buf = fd.getvalue().decode() + + baseline = r"""\begin{pgfscope}% +\pgfpathrectangle{\pgfqpoint{0.375000in}{0.300000in}}""" \ + r"""{\pgfqpoint{2.325000in}{2.400000in}}% +\pgfusepath{clip}% +\pgfsetrectcap% +\pgfsetroundjoin% +\pgfsetlinewidth{1.003750pt}% +\definecolor{currentstroke}{rgb}{0.000000,0.000000,1.000000}% +\pgfsetstrokecolor{currentstroke}% +\pgfsetdash{}{0pt}% +\pgfpathmoveto{\pgfqpoint{0.375000in}{0.300000in}}% +\pgfpathlineto{\pgfqpoint{2.700000in}{2.700000in}}% +\pgfkeys{/pgf/decoration/.cd, """ \ + r"""segment length = 0.300000in, amplitude = 0.050000in}% +\pgfmathsetseed{42}% +\pgfdecoratecurrentpath{random steps}% +\pgfusepath{stroke}% +\end{pgfscope}%""" + # check that \pgfkeys{/pgf/decoration/.cd, ...} is in path definition + assert baseline in buf From c2ef020c9e6ba957397a5bcf15dda5e142f347c2 Mon Sep 17 00:00:00 2001 From: takimata Date: Thu, 1 Jul 2021 12:44:58 +0200 Subject: [PATCH 06/61] Clarify stuff --- lib/matplotlib/artist.py | 1 - lib/matplotlib/backends/backend_pgf.py | 11 +++++++---- lib/matplotlib/tests/test_backend_pgf.py | 22 ++++++---------------- 3 files changed, 13 insertions(+), 21 deletions(-) diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index de5051847c5d..5b48920403df 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -679,7 +679,6 @@ def set_sketch_params(self, scale=None, length=None, randomness=None): Requires the following preamble when using the PGF backend: - \\usepackage{pgf} \\usepgfmodule{decorations} \\usepgflibrary{decorations.pathmorphing} diff --git a/lib/matplotlib/backends/backend_pgf.py b/lib/matplotlib/backends/backend_pgf.py index 859d818d07c6..43971796ae87 100644 --- a/lib/matplotlib/backends/backend_pgf.py +++ b/lib/matplotlib/backends/backend_pgf.py @@ -603,10 +603,13 @@ def _print_pgf_path(self, gc, path, transform, rgbFace=None): # apply pgf decorators sketch_params = gc.get_sketch_params() if gc else None if sketch_params is not None: - # Only "length" directly maps to "segment length" in PGF's API - # The others are combined in "amplitude" -> Use "randomness" as - # PRNG seed to allow the user to force the same shape on multiple - # sketched lines + # Only "length" directly maps to "segment length" in PGF's API. + # PGF uses "amplitude" to pass the combined deviation in both x- + # and y-direction, while matplotlib only varies the length of the + # wiggle along the line ("randomness" and "length" parameters) + # and has a separate "scale" argument for the amplitude. + # -> Use "randomness" as PRNG seed to allow the user to force the + # same shape on multiple sketched lines scale, length, randomness = sketch_params if scale is not None: writeln(self.fh, r"\pgfkeys{/pgf/decoration/.cd, " diff --git a/lib/matplotlib/tests/test_backend_pgf.py b/lib/matplotlib/tests/test_backend_pgf.py index 11a5fb52fef3..b656744b8f87 100644 --- a/lib/matplotlib/tests/test_backend_pgf.py +++ b/lib/matplotlib/tests/test_backend_pgf.py @@ -341,34 +341,24 @@ def test_minus_signs_with_tex(fig_test, fig_ref, texsystem): @pytest.mark.backend("pgf") def test_sketch_params(): - fig, ax = plt.subplots(figsize=[3, 3]) + fig, ax = plt.subplots(figsize=(3, 3)) ax.set_xticks([]) ax.set_yticks([]) ax.set_frame_on(False) - handle = ax.plot([0, 1])[0] + handle, = ax.plot([0, 1]) handle.set_sketch_params(scale=5, length=30, randomness=42) with BytesIO() as fd: fig.savefig(fd, format='pgf') buf = fd.getvalue().decode() - baseline = r"""\begin{pgfscope}% -\pgfpathrectangle{\pgfqpoint{0.375000in}{0.300000in}}""" \ - r"""{\pgfqpoint{2.325000in}{2.400000in}}% -\pgfusepath{clip}% -\pgfsetrectcap% -\pgfsetroundjoin% -\pgfsetlinewidth{1.003750pt}% -\definecolor{currentstroke}{rgb}{0.000000,0.000000,1.000000}% -\pgfsetstrokecolor{currentstroke}% -\pgfsetdash{}{0pt}% -\pgfpathmoveto{\pgfqpoint{0.375000in}{0.300000in}}% + baseline = r"""\pgfpathmoveto{\pgfqpoint{0.375000in}{0.300000in}}% \pgfpathlineto{\pgfqpoint{2.700000in}{2.700000in}}% \pgfkeys{/pgf/decoration/.cd, """ \ r"""segment length = 0.300000in, amplitude = 0.050000in}% \pgfmathsetseed{42}% \pgfdecoratecurrentpath{random steps}% -\pgfusepath{stroke}% -\end{pgfscope}%""" - # check that \pgfkeys{/pgf/decoration/.cd, ...} is in path definition +\pgfusepath{stroke}%""" + # \pgfdecoratecurrentpath must be after the path definition and before the + # path is used (\pgfusepath) assert baseline in buf From 9d73118106a22eea330638e9a9bcf342d585c717 Mon Sep 17 00:00:00 2001 From: takimata Date: Fri, 23 Jul 2021 12:52:40 +0200 Subject: [PATCH 07/61] Fix rendering of code block --- lib/matplotlib/artist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index 5b48920403df..bc5f5147c15c 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -677,7 +677,7 @@ def set_sketch_params(self, scale=None, length=None, randomness=None): """ Set the sketch parameters. - Requires the following preamble when using the PGF backend: + Requires the following preamble when using the PGF backend:: \\usepgfmodule{decorations} \\usepgflibrary{decorations.pathmorphing} From b94b282e9ec79d93a15644d9986b1f925fc7dd95 Mon Sep 17 00:00:00 2001 From: takimata Date: Fri, 23 Jul 2021 16:02:39 +0200 Subject: [PATCH 08/61] Automatically load PGF components --- lib/matplotlib/artist.py | 13 +++---------- lib/matplotlib/backends/backend_pgf.py | 3 +++ lib/matplotlib/tests/test_backend_pgf.py | 2 ++ 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index bc5f5147c15c..185ec79ca8d8 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -677,16 +677,6 @@ def set_sketch_params(self, scale=None, length=None, randomness=None): """ Set the sketch parameters. - Requires the following preamble when using the PGF backend:: - - \\usepgfmodule{decorations} - \\usepgflibrary{decorations.pathmorphing} - - This also applies to PGF backend + PDF output, where this must be added - to *pgf.preamble* manually. The PGF backend uses the *randomness* - argument as a seed and not as described below. Pass the same seed to - obtain the same random shape. - Parameters ---------- scale : float, optional @@ -700,6 +690,9 @@ def set_sketch_params(self, scale=None, length=None, randomness=None): The scale factor by which the length is shrunken or expanded (default 16.0) + The PGF backend uses this argument as an RNG seed and not as + described above. Using the same seed yields the same random shape. + .. ACCEPTS: (scale: float, length: float, randomness: float) """ if scale is None: diff --git a/lib/matplotlib/backends/backend_pgf.py b/lib/matplotlib/backends/backend_pgf.py index 43971796ae87..861e3cee2c90 100644 --- a/lib/matplotlib/backends/backend_pgf.py +++ b/lib/matplotlib/backends/backend_pgf.py @@ -612,6 +612,9 @@ def _print_pgf_path(self, gc, path, transform, rgbFace=None): # same shape on multiple sketched lines scale, length, randomness = sketch_params if scale is not None: + # PGF guarantees that repeated loading is a no-op + writeln(self.fh, r"\usepgfmodule{decorations}") + writeln(self.fh, r"\usepgflibrary{decorations.pathmorphing}") writeln(self.fh, r"\pgfkeys{/pgf/decoration/.cd, " f"segment length = {(length * f):f}in, " f"amplitude = {(scale * f):f}in}}") diff --git a/lib/matplotlib/tests/test_backend_pgf.py b/lib/matplotlib/tests/test_backend_pgf.py index b656744b8f87..f46d0114e28d 100644 --- a/lib/matplotlib/tests/test_backend_pgf.py +++ b/lib/matplotlib/tests/test_backend_pgf.py @@ -354,6 +354,8 @@ def test_sketch_params(): baseline = r"""\pgfpathmoveto{\pgfqpoint{0.375000in}{0.300000in}}% \pgfpathlineto{\pgfqpoint{2.700000in}{2.700000in}}% +\usepgfmodule{decorations}% +\usepgflibrary{decorations.pathmorphing}% \pgfkeys{/pgf/decoration/.cd, """ \ r"""segment length = 0.300000in, amplitude = 0.050000in}% \pgfmathsetseed{42}% From 18c06c3685bccc57c00bccf162b30082afa67eb7 Mon Sep 17 00:00:00 2001 From: takimata Date: Sun, 22 Aug 2021 21:21:09 +0200 Subject: [PATCH 09/61] Scale params to match existing appearance --- lib/matplotlib/backends/backend_pgf.py | 7 +++++-- lib/matplotlib/tests/test_backend_pgf.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/backends/backend_pgf.py b/lib/matplotlib/backends/backend_pgf.py index 861e3cee2c90..5914b375b732 100644 --- a/lib/matplotlib/backends/backend_pgf.py +++ b/lib/matplotlib/backends/backend_pgf.py @@ -611,13 +611,16 @@ def _print_pgf_path(self, gc, path, transform, rgbFace=None): # -> Use "randomness" as PRNG seed to allow the user to force the # same shape on multiple sketched lines scale, length, randomness = sketch_params + # make PGF output visually similar to matplotlib's sketched lines + adjustment_a = 0.5 + adjustment_b = 2 if scale is not None: # PGF guarantees that repeated loading is a no-op writeln(self.fh, r"\usepgfmodule{decorations}") writeln(self.fh, r"\usepgflibrary{decorations.pathmorphing}") writeln(self.fh, r"\pgfkeys{/pgf/decoration/.cd, " - f"segment length = {(length * f):f}in, " - f"amplitude = {(scale * f):f}in}}") + f"segment length = {(length * f * adjustment_a):f}in, " + f"amplitude = {(scale * f * adjustment_b):f}in}}") writeln(self.fh, f"\\pgfmathsetseed{{{int(randomness)}}}") writeln(self.fh, r"\pgfdecoratecurrentpath{random steps}") diff --git a/lib/matplotlib/tests/test_backend_pgf.py b/lib/matplotlib/tests/test_backend_pgf.py index f46d0114e28d..9b5b0b28ee3f 100644 --- a/lib/matplotlib/tests/test_backend_pgf.py +++ b/lib/matplotlib/tests/test_backend_pgf.py @@ -357,7 +357,7 @@ def test_sketch_params(): \usepgfmodule{decorations}% \usepgflibrary{decorations.pathmorphing}% \pgfkeys{/pgf/decoration/.cd, """ \ - r"""segment length = 0.300000in, amplitude = 0.050000in}% + r"""segment length = 0.150000in, amplitude = 0.100000in}% \pgfmathsetseed{42}% \pgfdecoratecurrentpath{random steps}% \pgfusepath{stroke}%""" From a9377cdd2bbc76cde85242b39a735bc0f8d3dd9c Mon Sep 17 00:00:00 2001 From: takimata <37397269+takimata@users.noreply.github.com> Date: Tue, 24 Aug 2021 11:34:40 +0200 Subject: [PATCH 10/61] Use descriptive names Co-authored-by: Elliott Sales de Andrade --- lib/matplotlib/backends/backend_pgf.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/backends/backend_pgf.py b/lib/matplotlib/backends/backend_pgf.py index 5914b375b732..4d247f3ba06c 100644 --- a/lib/matplotlib/backends/backend_pgf.py +++ b/lib/matplotlib/backends/backend_pgf.py @@ -611,16 +611,16 @@ def _print_pgf_path(self, gc, path, transform, rgbFace=None): # -> Use "randomness" as PRNG seed to allow the user to force the # same shape on multiple sketched lines scale, length, randomness = sketch_params - # make PGF output visually similar to matplotlib's sketched lines - adjustment_a = 0.5 - adjustment_b = 2 if scale is not None: + # make PGF output visually similar to matplotlib's sketched lines + length *= 0.5 + scale *= 2 # PGF guarantees that repeated loading is a no-op writeln(self.fh, r"\usepgfmodule{decorations}") writeln(self.fh, r"\usepgflibrary{decorations.pathmorphing}") writeln(self.fh, r"\pgfkeys{/pgf/decoration/.cd, " - f"segment length = {(length * f * adjustment_a):f}in, " - f"amplitude = {(scale * f * adjustment_b):f}in}}") + f"segment length = {(length * f):f}in, " + f"amplitude = {(scale * f):f}in}}") writeln(self.fh, f"\\pgfmathsetseed{{{int(randomness)}}}") writeln(self.fh, r"\pgfdecoratecurrentpath{random steps}") From c3503a2c0c9f5f99a9d5c1819efa88b09bc1f7dd Mon Sep 17 00:00:00 2001 From: takimata Date: Wed, 25 Aug 2021 12:17:30 +0200 Subject: [PATCH 11/61] Fix comment length --- lib/matplotlib/backends/backend_pgf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/backends/backend_pgf.py b/lib/matplotlib/backends/backend_pgf.py index 4d247f3ba06c..16ad89182c22 100644 --- a/lib/matplotlib/backends/backend_pgf.py +++ b/lib/matplotlib/backends/backend_pgf.py @@ -612,7 +612,7 @@ def _print_pgf_path(self, gc, path, transform, rgbFace=None): # same shape on multiple sketched lines scale, length, randomness = sketch_params if scale is not None: - # make PGF output visually similar to matplotlib's sketched lines + # make matplotlib and PGF rendering visually similar length *= 0.5 scale *= 2 # PGF guarantees that repeated loading is a no-op From 124ba6e5443e33b8620151d5d7b09d39a09210ba Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 30 Aug 2021 22:04:55 -0400 Subject: [PATCH 12/61] DOC: re-word whats new Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> --- .../fig_draw_without_rendering.rst | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/doc/users/next_whats_new/fig_draw_without_rendering.rst b/doc/users/next_whats_new/fig_draw_without_rendering.rst index 03db87ac444b..8b9e3147bf07 100644 --- a/doc/users/next_whats_new/fig_draw_without_rendering.rst +++ b/doc/users/next_whats_new/fig_draw_without_rendering.rst @@ -1,12 +1,12 @@ Figure now has ``draw_without_rendering`` method ------------------------------------------------ -Rarely, the user will want to trigger a draw without rendering to either the -screen or a file. This is useful for determining the final position of artists -on the figure that require a draw, like text artists, or resolve deferred -computation like automatic data limits. This can be done by -``fig.canvas.draw()``, which forces a full draw and rendering, however this has -side effects, sometimes requires an open file, and is doing more work than is -needed. The `.Figure.draw_without_rendering` method is provided to run the -code in Matplotlib that updates values that are computed at draw-time and get -accurate dimensions of the Artists more efficiently. +Some aspects of a figure are only determined at draw-time, such as the exact +position of text artists or deferred computation like automatic data limits. +If you need these values, you can use ``figure.canvas.draw()`` to force a full +draw. However, this has side effects, sometimes requires an open file, and is +doing more work than is needed. + +The new `.Figure.draw_without_rendering` method runs all the updates that +``draw()`` does, but skips rendering the figure. It's thus more efficient if you +need the updated values to configure further aspects of the figure. From f023115fdf49f1ecfdb406def0f23462ad096669 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 1 Sep 2021 12:11:29 +0200 Subject: [PATCH 13/61] Small cleanups to math_to_image. - parse() already normalizes prop=None to prop=FontProperties(). - We don't need to explicitly attach a canvas to a plain Figure() before calling savefig() anymore; FigureCanvasBase is always attached and handles the dispatching to the correct concrete canvas class. --- lib/matplotlib/mathtext.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/matplotlib/mathtext.py b/lib/matplotlib/mathtext.py index ce6de4cdee56..0ce21c5ba200 100644 --- a/lib/matplotlib/mathtext.py +++ b/lib/matplotlib/mathtext.py @@ -588,18 +588,12 @@ def math_to_image(s, filename_or_obj, prop=None, dpi=None, format=None): format is determined as for `.Figure.savefig`. """ from matplotlib import figure - # backend_agg supports all of the core output formats - from matplotlib.backends import backend_agg - - if prop is None: - prop = FontProperties() parser = MathTextParser('path') width, height, depth, _, _ = parser.parse(s, dpi=72, prop=prop) fig = figure.Figure(figsize=(width / 72.0, height / 72.0)) fig.text(0, depth/height, s, fontproperties=prop) - backend_agg.FigureCanvasAgg(fig) fig.savefig(filename_or_obj, dpi=dpi, format=format) return depth From 486ad067bb6aa0bda883dc4984223b7bef2f8207 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 1 Sep 2021 13:10:48 -0400 Subject: [PATCH 14/61] BUG: Fix f_back is None handling --- lib/matplotlib/backends/backend_qt.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/backends/backend_qt.py b/lib/matplotlib/backends/backend_qt.py index 5b74176befff..e8a137d6a565 100644 --- a/lib/matplotlib/backends/backend_qt.py +++ b/lib/matplotlib/backends/backend_qt.py @@ -333,7 +333,8 @@ def keyReleaseEvent(self, event): def resizeEvent(self, event): frame = sys._getframe() - if frame.f_code is frame.f_back.f_code: # Prevent PyQt6 recursion. + # Prevent PyQt6 recursion, but sometimes frame.f_back is None + if frame.f_code is getattr(frame.f_back, 'f_code', None): return w = event.size().width() * self.device_pixel_ratio h = event.size().height() * self.device_pixel_ratio From 0b724bf3ea06dee2f73d487aa09406a77e117b2d Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Sat, 31 Jul 2021 01:33:37 -0400 Subject: [PATCH 15/61] Add a GTK4 backend. --- lib/matplotlib/__init__.py | 4 +- lib/matplotlib/backend_bases.py | 5 +- lib/matplotlib/backends/backend_gtk4.py | 832 ++++++++++++++++++ lib/matplotlib/backends/backend_gtk4agg.py | 80 ++ lib/matplotlib/backends/backend_gtk4cairo.py | 35 + lib/matplotlib/cbook/__init__.py | 13 +- lib/matplotlib/mpl-data/matplotlibrc | 4 +- lib/matplotlib/pyplot.py | 4 +- lib/matplotlib/rcsetup.py | 2 +- .../tests/test_backends_interactive.py | 12 +- mplsetup.cfg.template | 4 +- 11 files changed, 978 insertions(+), 17 deletions(-) create mode 100644 lib/matplotlib/backends/backend_gtk4.py create mode 100644 lib/matplotlib/backends/backend_gtk4agg.py create mode 100644 lib/matplotlib/backends/backend_gtk4cairo.py diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 0361a37aed48..ac84e82c30f6 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -1098,8 +1098,8 @@ def use(backend, *, force=True): backend names, which are case-insensitive: - interactive backends: - GTK3Agg, GTK3Cairo, MacOSX, nbAgg, QtAgg, QtCairo, - TkAgg, TkCairo, WebAgg, WX, WXAgg, WXCairo, Qt5Agg, Qt5Cairo + GTK3Agg, GTK3Cairo, GTK4Agg, GTK4Cairo, MacOSX, nbAgg, QtAgg, + QtCairo, TkAgg, TkCairo, WebAgg, WX, WXAgg, WXCairo, Qt5Agg, Qt5Cairo - non-interactive backends: agg, cairo, pdf, pgf, ps, svg, template diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 84f644e57dbd..fda7bd1c9613 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -101,6 +101,7 @@ def _safe_pyplot_import(): backend_mapping = { 'qt': 'qtagg', 'gtk3': 'gtk3agg', + 'gtk4': 'gtk4agg', 'wx': 'wxagg', 'tk': 'tkagg', 'macosx': 'macosx', @@ -1656,7 +1657,7 @@ class FigureCanvasBase: A high-level figure instance. """ - # Set to one of {"qt", "gtk3", "wx", "tk", "macosx"} if an + # Set to one of {"qt", "gtk3", "gtk4", "wx", "tk", "macosx"} if an # interactive framework is required, or None otherwise. required_interactive_framework = None @@ -1732,7 +1733,7 @@ def _fix_ipython_backend2gui(cls): # don't break on our side. return rif = getattr(cls, "required_interactive_framework", None) - backend2gui_rif = {"qt": "qt", "gtk3": "gtk3", + backend2gui_rif = {"qt": "qt", "gtk3": "gtk3", "gtk4": "gtk4", "wx": "wx", "macosx": "osx"}.get(rif) if backend2gui_rif: if _is_non_interactive_terminal_ipython(ip): diff --git a/lib/matplotlib/backends/backend_gtk4.py b/lib/matplotlib/backends/backend_gtk4.py new file mode 100644 index 000000000000..f2e270da76b4 --- /dev/null +++ b/lib/matplotlib/backends/backend_gtk4.py @@ -0,0 +1,832 @@ +import functools +import io +import logging +import os +from pathlib import Path +import sys + +import matplotlib as mpl +from matplotlib import _api, backend_tools, cbook +from matplotlib._pylab_helpers import Gcf +from matplotlib.backend_bases import ( + _Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2, + TimerBase, ToolContainerBase) +from matplotlib.backend_tools import Cursors +from matplotlib.figure import Figure +from matplotlib.widgets import SubplotTool + +try: + import gi +except ImportError as err: + raise ImportError("The GTK4 backends require PyGObject") from err + +try: + # :raises ValueError: If module/version is already loaded, already + # required, or unavailable. + gi.require_version("Gtk", "4.0") +except ValueError as e: + # in this case we want to re-raise as ImportError so the + # auto-backend selection logic correctly skips. + raise ImportError from e + +from gi.repository import Gio, GLib, GObject, Gtk, Gdk, GdkPixbuf + + +_log = logging.getLogger(__name__) + +backend_version = "%s.%s.%s" % ( + Gtk.get_major_version(), Gtk.get_minor_version(), Gtk.get_micro_version()) + +# Placeholder +_application = None + + +def _shutdown_application(app): + # The application might prematurely shut down if Ctrl-C'd out of IPython, + # so close all windows. + for win in app.get_windows(): + win.destroy() + # The PyGObject wrapper incorrectly thinks that None is not allowed, or we + # would call this: + # Gio.Application.set_default(None) + # Instead, we set this property and ignore default applications with it: + app._created_by_matplotlib = True + global _application + _application = None + + +def _create_application(): + global _application + + if _application is None: + app = Gio.Application.get_default() + if app is None or getattr(app, '_created_by_matplotlib'): + # display_is_valid returns False only if on Linux and neither X11 + # nor Wayland display can be opened. + if not mpl._c_internal_utils.display_is_valid(): + raise RuntimeError('Invalid DISPLAY variable') + _application = Gtk.Application.new('org.matplotlib.Matplotlib3', + Gio.ApplicationFlags.NON_UNIQUE) + # The activate signal must be connected, but we don't care for + # handling it, since we don't do any remote processing. + _application.connect('activate', lambda *args, **kwargs: None) + _application.connect('shutdown', _shutdown_application) + _application.register() + cbook._setup_new_guiapp() + else: + _application = app + + +def _mpl_to_gtk_cursor(mpl_cursor): + return _api.check_getitem({ + Cursors.MOVE: "move", + Cursors.HAND: "pointer", + Cursors.POINTER: "default", + Cursors.SELECT_REGION: "crosshair", + Cursors.WAIT: "wait", + Cursors.RESIZE_HORIZONTAL: "ew-resize", + Cursors.RESIZE_VERTICAL: "ns-resize", + }, cursor=mpl_cursor) + + +class TimerGTK4(TimerBase): + """Subclass of `.TimerBase` using GTK4 timer events.""" + + def __init__(self, *args, **kwargs): + self._timer = None + super().__init__(*args, **kwargs) + + def _timer_start(self): + # Need to stop it, otherwise we potentially leak a timer id that will + # never be stopped. + self._timer_stop() + self._timer = GLib.timeout_add(self._interval, self._on_timer) + + def _timer_stop(self): + if self._timer is not None: + GLib.source_remove(self._timer) + self._timer = None + + def _timer_set_interval(self): + # Only stop and restart it if the timer has already been started + if self._timer is not None: + self._timer_stop() + self._timer_start() + + def _on_timer(self): + super()._on_timer() + + # Gtk timeout_add() requires that the callback returns True if it + # is to be called again. + if self.callbacks and not self._single: + return True + else: + self._timer = None + return False + + +class FigureCanvasGTK4(Gtk.DrawingArea, FigureCanvasBase): + required_interactive_framework = "gtk4" + _timer_cls = TimerGTK4 + + def __init__(self, figure=None): + FigureCanvasBase.__init__(self, figure) + GObject.GObject.__init__(self) + self.set_hexpand(True) + self.set_vexpand(True) + + self._idle_draw_id = 0 + self._lastCursor = None + self._rubberband_rect = None + + self.set_draw_func(self._draw_func) + self.connect('resize', self.resize_event) + + click = Gtk.GestureClick() + click.set_button(0) # All buttons. + click.connect('pressed', self.button_press_event) + click.connect('released', self.button_release_event) + self.add_controller(click) + + key = Gtk.EventControllerKey() + key.connect('key-pressed', self.key_press_event) + key.connect('key-released', self.key_release_event) + self.add_controller(key) + + motion = Gtk.EventControllerMotion() + motion.connect('motion', self.motion_notify_event) + motion.connect('enter', self.enter_notify_event) + motion.connect('leave', self.leave_notify_event) + self.add_controller(motion) + + scroll = Gtk.EventControllerScroll.new( + Gtk.EventControllerScrollFlags.VERTICAL) + scroll.connect('scroll', self.scroll_event) + self.add_controller(scroll) + + self.set_focusable(True) + + css = Gtk.CssProvider() + css.load_from_data(b".matplotlib-canvas { background-color: white; }") + style_ctx = self.get_style_context() + style_ctx.add_provider(css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) + style_ctx.add_class("matplotlib-canvas") + + def pick(self, mouseevent): + # GtkWidget defines pick in GTK4, so we need to override here to work + # with the base implementation we want. + FigureCanvasBase.pick(self, mouseevent) + + def destroy(self): + self.close_event() + + def set_cursor(self, cursor): + # docstring inherited + self.set_cursor_from_name(_mpl_to_gtk_cursor(cursor)) + + def scroll_event(self, controller, dx, dy): + FigureCanvasBase.scroll_event(self, 0, 0, dy) + return True + + def button_press_event(self, controller, n_press, x, y): + # flipy so y=0 is bottom of canvas + y = self.get_allocation().height - y + FigureCanvasBase.button_press_event(self, x, y, + controller.get_current_button()) + self.grab_focus() + + def button_release_event(self, controller, n_press, x, y): + # flipy so y=0 is bottom of canvas + y = self.get_allocation().height - y + FigureCanvasBase.button_release_event(self, x, y, + controller.get_current_button()) + + def key_press_event(self, controller, keyval, keycode, state): + key = self._get_key(keyval, keycode, state) + FigureCanvasBase.key_press_event(self, key) + return True + + def key_release_event(self, controller, keyval, keycode, state): + key = self._get_key(keyval, keycode, state) + FigureCanvasBase.key_release_event(self, key) + return True + + def motion_notify_event(self, controller, x, y): + # flipy so y=0 is bottom of canvas + y = self.get_allocation().height - y + FigureCanvasBase.motion_notify_event(self, x, y) + + def leave_notify_event(self, controller): + FigureCanvasBase.leave_notify_event(self) + + def enter_notify_event(self, controller, x, y): + # flipy so y=0 is bottom of canvas + y = self.get_allocation().height - y + FigureCanvasBase.enter_notify_event(self, xy=(x, y)) + + def resize_event(self, area, width, height): + dpi = self.figure.dpi + self.figure.set_size_inches(width / dpi, height / dpi, forward=False) + FigureCanvasBase.resize_event(self) + self.draw_idle() + + def _get_key(self, keyval, keycode, state): + unikey = chr(Gdk.keyval_to_unicode(keyval)) + key = cbook._unikey_or_keysym_to_mplkey( + unikey, + Gdk.keyval_name(keyval)) + modifiers = [ + (Gdk.ModifierType.CONTROL_MASK, 'ctrl'), + (Gdk.ModifierType.ALT_MASK, 'alt'), + (Gdk.ModifierType.SHIFT_MASK, 'shift'), + (Gdk.ModifierType.SUPER_MASK, 'super'), + ] + for key_mask, prefix in modifiers: + if state & key_mask: + if not (prefix == 'shift' and unikey.isprintable()): + key = f'{prefix}+{key}' + return key + + def _draw_rubberband(self, rect): + self._rubberband_rect = rect + # TODO: Only update the rubberband area. + self.queue_draw() + + def _draw_func(self, drawing_area, ctx, width, height): + self.on_draw_event(self, ctx) + self._post_draw(self, ctx) + + def _post_draw(self, widget, ctx): + if self._rubberband_rect is None: + return + + x0, y0, w, h = self._rubberband_rect + x1 = x0 + w + y1 = y0 + h + + # Draw the lines from x0, y0 towards x1, y1 so that the + # dashes don't "jump" when moving the zoom box. + ctx.move_to(x0, y0) + ctx.line_to(x0, y1) + ctx.move_to(x0, y0) + ctx.line_to(x1, y0) + ctx.move_to(x0, y1) + ctx.line_to(x1, y1) + ctx.move_to(x1, y0) + ctx.line_to(x1, y1) + + ctx.set_antialias(1) + ctx.set_line_width(1) + ctx.set_dash((3, 3), 0) + ctx.set_source_rgb(0, 0, 0) + ctx.stroke_preserve() + + ctx.set_dash((3, 3), 3) + ctx.set_source_rgb(1, 1, 1) + ctx.stroke() + + def on_draw_event(self, widget, ctx): + # to be overwritten by GTK4Agg or GTK4Cairo + pass + + def draw(self): + # docstring inherited + if self.is_drawable(): + self.queue_draw() + + def draw_idle(self): + # docstring inherited + if self._idle_draw_id != 0: + return + def idle_draw(*args): + try: + self.draw() + finally: + self._idle_draw_id = 0 + return False + self._idle_draw_id = GLib.idle_add(idle_draw) + + def flush_events(self): + # docstring inherited + context = GLib.MainContext.default() + while context.pending(): + context.iteration(True) + + +class FigureManagerGTK4(FigureManagerBase): + """ + Attributes + ---------- + canvas : `FigureCanvas` + The FigureCanvas instance + num : int or str + The Figure number + toolbar : Gtk.Box + The Gtk.Box + vbox : Gtk.VBox + The Gtk.VBox containing the canvas and toolbar + window : Gtk.Window + The Gtk.Window + + """ + def __init__(self, canvas, num): + _create_application() + self.window = Gtk.Window() + _application.add_window(self.window) + super().__init__(canvas, num) + + try: + self.window.set_icon_from_file(window_icon) + except Exception: + # Some versions of gtk throw a glib.GError but not all, so I am not + # sure how to catch it. I am unhappy doing a blanket catch here, + # but am not sure what a better way is - JDH + _log.info('Could not load matplotlib icon: %s', sys.exc_info()[1]) + + self.vbox = Gtk.Box() + self.vbox.set_property("orientation", Gtk.Orientation.VERTICAL) + self.window.set_child(self.vbox) + + self.vbox.prepend(self.canvas) + # calculate size for window + w = int(self.canvas.figure.bbox.width) + h = int(self.canvas.figure.bbox.height) + + self.toolbar = self._get_toolbar() + + if self.toolmanager: + backend_tools.add_tools_to_manager(self.toolmanager) + if self.toolbar: + backend_tools.add_tools_to_container(self.toolbar) + + if self.toolbar is not None: + sw = Gtk.ScrolledWindow(vscrollbar_policy=Gtk.PolicyType.NEVER) + sw.set_child(self.toolbar) + self.vbox.append(sw) + min_size, nat_size = self.toolbar.get_preferred_size() + h += nat_size.height + + self.window.set_default_size(w, h) + + self._destroying = False + self.window.connect("destroy", lambda *args: Gcf.destroy(self)) + self.window.connect("close-request", lambda *args: Gcf.destroy(self)) + if mpl.is_interactive(): + self.window.show() + self.canvas.draw_idle() + + self.canvas.grab_focus() + + def destroy(self, *args): + if self._destroying: + # Otherwise, this can be called twice when the user presses 'q', + # which calls Gcf.destroy(self), then this destroy(), then triggers + # Gcf.destroy(self) once again via + # `connect("destroy", lambda *args: Gcf.destroy(self))`. + return + self._destroying = True + self.window.destroy() + self.canvas.destroy() + + def show(self): + # show the figure window + self.window.show() + self.canvas.draw() + if mpl.rcParams['figure.raise_window']: + if self.window.get_surface(): + self.window.present() + else: + # If this is called by a callback early during init, + # self.window (a GtkWindow) may not have an associated + # low-level GdkSurface (self.window.get_surface()) yet, and + # present() would crash. + _api.warn_external("Cannot raise window yet to be setup") + + def full_screen_toggle(self): + if not self.window.is_fullscreen(): + self.window.fullscreen() + else: + self.window.unfullscreen() + + def _get_toolbar(self): + # must be inited after the window, drawingArea and figure + # attrs are set + if mpl.rcParams['toolbar'] == 'toolbar2': + toolbar = NavigationToolbar2GTK4(self.canvas, self.window) + elif mpl.rcParams['toolbar'] == 'toolmanager': + toolbar = ToolbarGTK4(self.toolmanager) + else: + toolbar = None + return toolbar + + def get_window_title(self): + return self.window.get_title() + + def set_window_title(self, title): + self.window.set_title(title) + + def resize(self, width, height): + """Set the canvas size in pixels.""" + if self.toolbar: + toolbar_size = self.toolbar.size_request() + height += toolbar_size.height + canvas_size = self.canvas.get_allocation() + if canvas_size.width == canvas_size.height == 1: + # A canvas size of (1, 1) cannot exist in most cases, because + # window decorations would prevent such a small window. This call + # must be before the window has been mapped and widgets have been + # sized, so just change the window's starting size. + self.window.set_default_size(width, height) + else: + self.window.resize(width, height) + + +class NavigationToolbar2GTK4(NavigationToolbar2, Gtk.Box): + def __init__(self, canvas, window): + self.win = window + Gtk.Box.__init__(self) + + self.add_css_class('toolbar') + + self._gtk_ids = {} + for text, tooltip_text, image_file, callback in self.toolitems: + if text is None: + self.append(Gtk.Separator()) + continue + image = Gtk.Image.new_from_gicon( + Gio.Icon.new_for_string( + str(cbook._get_data_path('images', + f'{image_file}-symbolic.svg')))) + self._gtk_ids[text] = button = ( + Gtk.ToggleButton() if callback in ['zoom', 'pan'] else + Gtk.Button()) + button.set_child(image) + button.add_css_class('flat') + button.add_css_class('image-button') + # Save the handler id, so that we can block it as needed. + button._signal_handler = button.connect( + 'clicked', getattr(self, callback)) + button.set_tooltip_text(tooltip_text) + self.append(button) + + # This filler item ensures the toolbar is always at least two text + # lines high. Otherwise the canvas gets redrawn as the mouse hovers + # over images because those use two-line messages which resize the + # toolbar. + label = Gtk.Label() + label.set_markup( + '\N{NO-BREAK SPACE}\n\N{NO-BREAK SPACE}') + label.set_hexpand(True) # Push real message to the right. + self.append(label) + + self.message = Gtk.Label() + self.append(self.message) + + NavigationToolbar2.__init__(self, canvas) + + def set_message(self, s): + escaped = GLib.markup_escape_text(s) + self.message.set_markup(f'{escaped}') + + def draw_rubberband(self, event, x0, y0, x1, y1): + height = self.canvas.figure.bbox.height + y1 = height - y1 + y0 = height - y0 + rect = [int(val) for val in (x0, y0, x1 - x0, y1 - y0)] + self.canvas._draw_rubberband(rect) + + def remove_rubberband(self): + self.canvas._draw_rubberband(None) + + def _update_buttons_checked(self): + for name, active in [("Pan", "PAN"), ("Zoom", "ZOOM")]: + button = self._gtk_ids.get(name) + if button: + with button.handler_block(button._signal_handler): + button.set_active(self.mode.name == active) + + def pan(self, *args): + super().pan(*args) + self._update_buttons_checked() + + def zoom(self, *args): + super().zoom(*args) + self._update_buttons_checked() + + def save_figure(self, *args): + dialog = Gtk.FileChooserNative( + title='Save the figure', + transient_for=self.canvas.get_root(), + action=Gtk.FileChooserAction.SAVE, + modal=True) + self._save_dialog = dialog # Must keep a reference. + + ff = Gtk.FileFilter() + ff.set_name('All files') + ff.add_pattern('*') + dialog.add_filter(ff) + dialog.set_filter(ff) + + formats = [] + default_format = None + for i, (name, fmts) in enumerate( + self.canvas.get_supported_filetypes_grouped().items()): + ff = Gtk.FileFilter() + ff.set_name(name) + for fmt in fmts: + ff.add_pattern(f'*.{fmt}') + dialog.add_filter(ff) + formats.append(name) + if self.canvas.get_default_filetype() in fmts: + default_format = i + # Setting the choice doesn't always work, so make sure the default + # format is first. + formats = [formats[default_format], *formats[:default_format], + *formats[default_format+1:]] + dialog.add_choice('format', 'File format', formats, formats) + dialog.set_choice('format', formats[default_format]) + + dialog.set_current_folder(Gio.File.new_for_path( + os.path.expanduser(mpl.rcParams['savefig.directory']))) + dialog.set_current_name(self.canvas.get_default_filename()) + + @functools.partial(dialog.connect, 'response') + def on_response(dialog, response): + file = dialog.get_file() + fmt = dialog.get_choice('format') + fmt = self.canvas.get_supported_filetypes_grouped()[fmt][0] + dialog.destroy() + self._save_dialog = None + if response != Gtk.ResponseType.ACCEPT: + return + # Save dir for next time, unless empty str (which means use cwd). + if mpl.rcParams['savefig.directory']: + parent = file.get_parent() + mpl.rcParams['savefig.directory'] = parent.get_path() + try: + self.canvas.figure.savefig(file.get_path(), format=fmt) + except Exception as e: + msg = Gtk.MessageDialog( + transient_for=self.canvas.get_root(), + message_type=Gtk.MessageType.ERROR, + buttons=Gtk.ButtonsType.OK, modal=True, + text=str(e)) + msg.show() + + dialog.show() + + def set_history_buttons(self): + can_backward = self._nav_stack._pos > 0 + can_forward = self._nav_stack._pos < len(self._nav_stack._elements) - 1 + if 'Back' in self._gtk_ids: + self._gtk_ids['Back'].set_sensitive(can_backward) + if 'Forward' in self._gtk_ids: + self._gtk_ids['Forward'].set_sensitive(can_forward) + + +class ToolbarGTK4(ToolContainerBase, Gtk.Box): + _icon_extension = '-symbolic.svg' + + def __init__(self, toolmanager): + ToolContainerBase.__init__(self, toolmanager) + Gtk.Box.__init__(self) + self.set_property('orientation', Gtk.Orientation.HORIZONTAL) + + # Tool items are created later, but must appear before the message. + self._tool_box = Gtk.Box() + self.append(self._tool_box) + self._groups = {} + self._toolitems = {} + + # This filler item ensures the toolbar is always at least two text + # lines high. Otherwise the canvas gets redrawn as the mouse hovers + # over images because those use two-line messages which resize the + # toolbar. + label = Gtk.Label() + label.set_markup( + '\N{NO-BREAK SPACE}\n\N{NO-BREAK SPACE}') + label.set_hexpand(True) # Push real message to the right. + self.append(label) + + self._message = Gtk.Label() + self.append(self._message) + + def add_toolitem(self, name, group, position, image_file, description, + toggle): + if toggle: + button = Gtk.ToggleButton() + else: + button = Gtk.Button() + button.set_label(name) + button.add_css_class('flat') + + if image_file is not None: + image = Gtk.Image.new_from_gicon( + Gio.Icon.new_for_string(image_file)) + button.set_child(image) + button.add_css_class('image-button') + + if position is None: + position = -1 + + self._add_button(button, group, position) + signal = button.connect('clicked', self._call_tool, name) + button.set_tooltip_text(description) + self._toolitems.setdefault(name, []) + self._toolitems[name].append((button, signal)) + + def _find_child_at_position(self, group, position): + children = [None] + child = self._groups[group].get_first_child() + while child is not None: + children.append(child) + child = child.get_next_sibling() + return children[position] + + def _add_button(self, button, group, position): + if group not in self._groups: + if self._groups: + self._add_separator() + group_box = Gtk.Box() + self._tool_box.append(group_box) + self._groups[group] = group_box + self._groups[group].insert_child_after( + button, self._find_child_at_position(group, position)) + + def _call_tool(self, btn, name): + self.trigger_tool(name) + + def toggle_toolitem(self, name, toggled): + if name not in self._toolitems: + return + for toolitem, signal in self._toolitems[name]: + toolitem.handler_block(signal) + toolitem.set_active(toggled) + toolitem.handler_unblock(signal) + + def remove_toolitem(self, name): + if name not in self._toolitems: + self.toolmanager.message_event(f'{name} not in toolbar', self) + return + + for group in self._groups: + for toolitem, _signal in self._toolitems[name]: + if toolitem in self._groups[group]: + self._groups[group].remove(toolitem) + del self._toolitems[name] + + def _add_separator(self): + sep = Gtk.Separator() + sep.set_property("orientation", Gtk.Orientation.VERTICAL) + self._tool_box.append(sep) + + def set_message(self, s): + self._message.set_label(s) + + +class RubberbandGTK4(backend_tools.RubberbandBase): + def draw_rubberband(self, x0, y0, x1, y1): + NavigationToolbar2GTK4.draw_rubberband( + self._make_classic_style_pseudo_toolbar(), None, x0, y0, x1, y1) + + def remove_rubberband(self): + NavigationToolbar2GTK4.remove_rubberband( + self._make_classic_style_pseudo_toolbar()) + + +class SaveFigureGTK4(backend_tools.SaveFigureBase): + def trigger(self, *args, **kwargs): + + class PseudoToolbar: + canvas = self.figure.canvas + + return NavigationToolbar2GTK4.save_figure(PseudoToolbar()) + + +class ConfigureSubplotsGTK4(backend_tools.ConfigureSubplotsBase, Gtk.Window): + def _get_canvas(self, fig): + return self.canvas.__class__(fig) + + def trigger(self, *args): + NavigationToolbar2GTK4.configure_subplots( + self._make_classic_style_pseudo_toolbar(), None) + + +class HelpGTK4(backend_tools.ToolHelpBase): + def _normalize_shortcut(self, key): + """ + Convert Matplotlib key presses to GTK+ accelerator identifiers. + + Related to `FigureCanvasGTK4._get_key`. + """ + special = { + 'backspace': 'BackSpace', + 'pagedown': 'Page_Down', + 'pageup': 'Page_Up', + 'scroll_lock': 'Scroll_Lock', + } + + parts = key.split('+') + mods = ['<' + mod + '>' for mod in parts[:-1]] + key = parts[-1] + + if key in special: + key = special[key] + elif len(key) > 1: + key = key.capitalize() + elif key.isupper(): + mods += [''] + + return ''.join(mods) + key + + def _is_valid_shortcut(self, key): + """ + Check for a valid shortcut to be displayed. + + - GTK will never send 'cmd+' (see `FigureCanvasGTK4._get_key`). + - The shortcut window only shows keyboard shortcuts, not mouse buttons. + """ + return 'cmd+' not in key and not key.startswith('MouseButton.') + + def trigger(self, *args): + section = Gtk.ShortcutsSection() + + for name, tool in sorted(self.toolmanager.tools.items()): + if not tool.description: + continue + + # Putting everything in a separate group allows GTK to + # automatically split them into separate columns/pages, which is + # useful because we have lots of shortcuts, some with many keys + # that are very wide. + group = Gtk.ShortcutsGroup() + section.append(group) + # A hack to remove the title since we have no group naming. + child = group.get_first_child() + while child is not None: + child.set_visible(False) + child = child.get_next_sibling() + + shortcut = Gtk.ShortcutsShortcut( + accelerator=' '.join( + self._normalize_shortcut(key) + for key in self.toolmanager.get_tool_keymap(name) + if self._is_valid_shortcut(key)), + title=tool.name, + subtitle=tool.description) + group.append(shortcut) + + window = Gtk.ShortcutsWindow( + title='Help', + modal=True, + transient_for=self._figure.canvas.get_root()) + window.set_child(section) + + window.show() + + +class ToolCopyToClipboardGTK4(backend_tools.ToolCopyToClipboardBase): + def trigger(self, *args, **kwargs): + with io.BytesIO() as f: + self.canvas.print_rgba(f) + w, h = self.canvas.get_width_height() + pb = GdkPixbuf.Pixbuf.new_from_data(f.getbuffer(), + GdkPixbuf.Colorspace.RGB, True, + 8, w, h, w*4) + clipboard = self.canvas.get_clipboard() + clipboard.set(pb) + + +# Define the file to use as the GTk icon +if sys.platform == 'win32': + icon_filename = 'matplotlib.png' +else: + icon_filename = 'matplotlib.svg' +window_icon = str(cbook._get_data_path('images', icon_filename)) + + +backend_tools.ToolSaveFigure = SaveFigureGTK4 +backend_tools.ToolConfigureSubplots = ConfigureSubplotsGTK4 +backend_tools.ToolRubberband = RubberbandGTK4 +backend_tools.ToolHelp = HelpGTK4 +backend_tools.ToolCopyToClipboard = ToolCopyToClipboardGTK4 + +Toolbar = ToolbarGTK4 + + +@_Backend.export +class _BackendGTK4(_Backend): + FigureCanvas = FigureCanvasGTK4 + FigureManager = FigureManagerGTK4 + + @staticmethod + def mainloop(): + global _application + if _application is None: + return + + try: + _application.run() # Quits when all added windows close. + finally: + # Running after quit is undefined, so create a new one next time. + _application = None diff --git a/lib/matplotlib/backends/backend_gtk4agg.py b/lib/matplotlib/backends/backend_gtk4agg.py new file mode 100644 index 000000000000..b3439dc109cd --- /dev/null +++ b/lib/matplotlib/backends/backend_gtk4agg.py @@ -0,0 +1,80 @@ +import numpy as np + +from .. import cbook +try: + from . import backend_cairo +except ImportError as e: + raise ImportError('backend Gtk4Agg requires cairo') from e +from . import backend_agg, backend_gtk4 +from .backend_cairo import cairo +from .backend_gtk4 import Gtk, _BackendGTK4 +from matplotlib import transforms + + +class FigureCanvasGTK4Agg(backend_gtk4.FigureCanvasGTK4, + backend_agg.FigureCanvasAgg): + def __init__(self, figure): + backend_gtk4.FigureCanvasGTK4.__init__(self, figure) + self._bbox_queue = [] + + def on_draw_event(self, widget, ctx): + allocation = self.get_allocation() + w, h = allocation.width, allocation.height + + if not len(self._bbox_queue): + Gtk.render_background( + self.get_style_context(), ctx, + allocation.x, allocation.y, + allocation.width, allocation.height) + bbox_queue = [transforms.Bbox([[0, 0], [w, h]])] + else: + bbox_queue = self._bbox_queue + + ctx = backend_cairo._to_context(ctx) + + for bbox in bbox_queue: + x = int(bbox.x0) + y = h - int(bbox.y1) + width = int(bbox.x1) - int(bbox.x0) + height = int(bbox.y1) - int(bbox.y0) + + buf = cbook._unmultiplied_rgba8888_to_premultiplied_argb32( + np.asarray(self.copy_from_bbox(bbox))) + image = cairo.ImageSurface.create_for_data( + buf.ravel().data, cairo.FORMAT_ARGB32, width, height) + ctx.set_source_surface(image, x, y) + ctx.paint() + + if len(self._bbox_queue): + self._bbox_queue = [] + + return False + + def blit(self, bbox=None): + # If bbox is None, blit the entire canvas to gtk. Otherwise + # blit only the area defined by the bbox. + if bbox is None: + bbox = self.figure.bbox + + allocation = self.get_allocation() + x = int(bbox.x0) + y = allocation.height - int(bbox.y1) + width = int(bbox.x1) - int(bbox.x0) + height = int(bbox.y1) - int(bbox.y0) + + self._bbox_queue.append(bbox) + self.queue_draw_area(x, y, width, height) + + def draw(self): + backend_agg.FigureCanvasAgg.draw(self) + super().draw() + + +class FigureManagerGTK4Agg(backend_gtk4.FigureManagerGTK4): + pass + + +@_BackendGTK4.export +class _BackendGTK4Agg(_BackendGTK4): + FigureCanvas = FigureCanvasGTK4Agg + FigureManager = FigureManagerGTK4Agg diff --git a/lib/matplotlib/backends/backend_gtk4cairo.py b/lib/matplotlib/backends/backend_gtk4cairo.py new file mode 100644 index 000000000000..391a1a372856 --- /dev/null +++ b/lib/matplotlib/backends/backend_gtk4cairo.py @@ -0,0 +1,35 @@ +from contextlib import nullcontext + +from . import backend_cairo, backend_gtk4 +from .backend_gtk4 import Gtk, _BackendGTK4 + + +class RendererGTK4Cairo(backend_cairo.RendererCairo): + def set_context(self, ctx): + self.gc.ctx = backend_cairo._to_context(ctx) + + +class FigureCanvasGTK4Cairo(backend_gtk4.FigureCanvasGTK4, + backend_cairo.FigureCanvasCairo): + + def __init__(self, figure): + super().__init__(figure) + self._renderer = RendererGTK4Cairo(self.figure.dpi) + + def on_draw_event(self, widget, ctx): + with (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar + else nullcontext()): + self._renderer.set_context(ctx) + allocation = self.get_allocation() + Gtk.render_background( + self.get_style_context(), ctx, + allocation.x, allocation.y, + allocation.width, allocation.height) + self._renderer.set_width_height( + allocation.width, allocation.height) + self.figure.draw(self._renderer) + + +@_BackendGTK4.export +class _BackendGTK4Cairo(_BackendGTK4): + FigureCanvas = FigureCanvasGTK4Cairo diff --git a/lib/matplotlib/cbook/__init__.py b/lib/matplotlib/cbook/__init__.py index 109b9ea69cc9..6d181c43107d 100644 --- a/lib/matplotlib/cbook/__init__.py +++ b/lib/matplotlib/cbook/__init__.py @@ -50,8 +50,8 @@ def _get_running_interactive_framework(): Returns ------- Optional[str] - One of the following values: "qt", "gtk3", "wx", "tk", "macosx", - "headless", ``None``. + One of the following values: "qt", "gtk3", "gtk4", "wx", "tk", + "macosx", "headless", ``None``. """ # Use ``sys.modules.get(name)`` rather than ``name in sys.modules`` as # entries can also have been explicitly set to None. @@ -64,8 +64,13 @@ def _get_running_interactive_framework(): if QtWidgets and QtWidgets.QApplication.instance(): return "qt" Gtk = sys.modules.get("gi.repository.Gtk") - if Gtk and Gtk.main_level(): - return "gtk3" + if Gtk: + if Gtk.MAJOR_VERSION == 4: + from gi.repository import GLib + if GLib.main_depth(): + return "gtk4" + if Gtk.MAJOR_VERSION == 3 and Gtk.main_level(): + return "gtk3" wx = sys.modules.get("wx") if wx and wx.GetApp(): return "wx" diff --git a/lib/matplotlib/mpl-data/matplotlibrc b/lib/matplotlib/mpl-data/matplotlibrc index 19e89e3cdd5e..106d881ce88c 100644 --- a/lib/matplotlib/mpl-data/matplotlibrc +++ b/lib/matplotlib/mpl-data/matplotlibrc @@ -71,9 +71,9 @@ ## *************************************************************************** ## The default backend. If you omit this parameter, the first working ## backend from the following list is used: -## MacOSX QtAgg Gtk3Agg TkAgg WxAgg Agg +## MacOSX QtAgg Gtk4Agg Gtk3Agg TkAgg WxAgg Agg ## Other choices include: -## QtCairo GTK3Cairo TkCairo WxCairo Cairo +## QtCairo GTK4Cairo GTK3Cairo TkCairo WxCairo Cairo ## Qt5Agg Qt5Cairo Wx # deprecated. ## PS PDF SVG Template ## You can also deploy your own backend outside of Matplotlib by referring to diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 201255da848c..b222466dda45 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -212,6 +212,7 @@ def switch_backend(newbackend): current_framework = cbook._get_running_interactive_framework() mapping = {'qt': 'qtagg', 'gtk3': 'gtk3agg', + 'gtk4': 'gtk4agg', 'wx': 'wxagg', 'tk': 'tkagg', 'macosx': 'macosx', @@ -222,7 +223,8 @@ def switch_backend(newbackend): candidates = [best_guess] else: candidates = [] - candidates += ["macosx", "qtagg", "gtk3agg", "tkagg", "wxagg"] + candidates += [ + "macosx", "qtagg", "gtk4agg", "gtk3agg", "tkagg", "wxagg"] # Don't try to fallback on the cairo-based backends as they each have # an additional dependency (pycairo) over the agg-based backend, and diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index 2c3c88e2fa66..a8a54c10dac6 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -35,7 +35,7 @@ # The capitalized forms are needed for ipython at present; this may # change for later versions. interactive_bk = [ - 'GTK3Agg', 'GTK3Cairo', + 'GTK3Agg', 'GTK3Cairo', 'GTK4Agg', 'GTK4Cairo', 'MacOSX', 'nbAgg', 'QtAgg', 'QtCairo', 'Qt5Agg', 'Qt5Cairo', diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index a1f27fea577a..bb17e5fdaf82 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -29,8 +29,8 @@ def _get_testable_interactive_backends(): *[([qt_api, "cairocffi"], {"MPLBACKEND": "qtcairo", "QT_API": qt_api}) for qt_api in ["PyQt6", "PySide6", "PyQt5", "PySide2"]], - (["cairo", "gi"], {"MPLBACKEND": "gtk3agg"}), - (["cairo", "gi"], {"MPLBACKEND": "gtk3cairo"}), + *[(["cairo", "gi"], {"MPLBACKEND": f"gtk{version}{renderer}"}) + for version in [3, 4] for renderer in ["agg", "cairo"]], (["tkinter"], {"MPLBACKEND": "tkagg"}), (["wx"], {"MPLBACKEND": "wx"}), (["wx"], {"MPLBACKEND": "wxagg"}), @@ -45,6 +45,12 @@ def _get_testable_interactive_backends(): reason = "{} cannot be imported".format(", ".join(missing)) elif env["MPLBACKEND"] == 'macosx' and os.environ.get('TF_BUILD'): reason = "macosx backend fails on Azure" + elif env["MPLBACKEND"].startswith('gtk'): + import gi + version = env["MPLBACKEND"][3] + repo = gi.Repository.get_default() + if f'{version}.0' not in repo.enumerate_versions('Gtk'): + reason = "no usable GTK bindings" marks = [] if reason: marks.append(pytest.mark.skip( @@ -87,7 +93,7 @@ def _test_interactive_impl(): assert_equal = TestCase().assertEqual assert_raises = TestCase().assertRaises - if backend.endswith("agg") and not backend.startswith(("gtk3", "web")): + if backend.endswith("agg") and not backend.startswith(("gtk", "web")): # Force interactive framework setup. plt.figure() diff --git a/mplsetup.cfg.template b/mplsetup.cfg.template index 2fd28a6e4d67..6c54a23fdccb 100644 --- a/mplsetup.cfg.template +++ b/mplsetup.cfg.template @@ -28,8 +28,8 @@ [rc_options] # User-configurable options # -# Default backend, one of: Agg, Cairo, GTK3Agg, GTK3Cairo, MacOSX, Pdf, Ps, -# QtAgg, QtCairo, SVG, TkAgg, WX, WXAgg. +# Default backend, one of: Agg, Cairo, GTK3Agg, GTK3Cairo, GTK4Agg, GTK4Cairo, +# MacOSX, Pdf, Ps, QtAgg, QtCairo, SVG, TkAgg, WX, WXAgg. # # The Agg, Ps, Pdf and SVG backends do not require external dependencies. Do # not choose MacOSX if you have disabled the relevant extension modules. The From 139a4dba1258a7db18ddd35aad4baf1b871f8b1e Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 3 Aug 2021 20:22:28 -0400 Subject: [PATCH 16/61] Move common GTK Backend code into a separate file. --- lib/matplotlib/backends/_backend_gtk.py | 76 +++++++++++++++++++++++++ lib/matplotlib/backends/backend_gtk3.py | 64 ++------------------- lib/matplotlib/backends/backend_gtk4.py | 63 ++------------------ 3 files changed, 88 insertions(+), 115 deletions(-) create mode 100644 lib/matplotlib/backends/_backend_gtk.py diff --git a/lib/matplotlib/backends/_backend_gtk.py b/lib/matplotlib/backends/_backend_gtk.py new file mode 100644 index 000000000000..1f7ca5107594 --- /dev/null +++ b/lib/matplotlib/backends/_backend_gtk.py @@ -0,0 +1,76 @@ +""" +Common code for GTK3 and GTK4 backends. +""" + +import logging + +import matplotlib as mpl +from matplotlib import cbook +from matplotlib.backend_bases import ( + _Backend, +) + +# The GTK3/GTK4 backends will have already called `gi.require_version` to set +# the desired GTK. +from gi.repository import Gio, Gtk + + +_log = logging.getLogger(__name__) + +backend_version = "%s.%s.%s" % ( + Gtk.get_major_version(), Gtk.get_minor_version(), Gtk.get_micro_version()) + +# Placeholder +_application = None + + +def _shutdown_application(app): + # The application might prematurely shut down if Ctrl-C'd out of IPython, + # so close all windows. + for win in app.get_windows(): + win.destroy() + # The PyGObject wrapper incorrectly thinks that None is not allowed, or we + # would call this: + # Gio.Application.set_default(None) + # Instead, we set this property and ignore default applications with it: + app._created_by_matplotlib = True + global _application + _application = None + + +def _create_application(): + global _application + + if _application is None: + app = Gio.Application.get_default() + if app is None or getattr(app, '_created_by_matplotlib'): + # display_is_valid returns False only if on Linux and neither X11 + # nor Wayland display can be opened. + if not mpl._c_internal_utils.display_is_valid(): + raise RuntimeError('Invalid DISPLAY variable') + _application = Gtk.Application.new('org.matplotlib.Matplotlib3', + Gio.ApplicationFlags.NON_UNIQUE) + # The activate signal must be connected, but we don't care for + # handling it, since we don't do any remote processing. + _application.connect('activate', lambda *args, **kwargs: None) + _application.connect('shutdown', _shutdown_application) + _application.register() + cbook._setup_new_guiapp() + else: + _application = app + + return _application + + +class _BackendGTK(_Backend): + @staticmethod + def mainloop(): + global _application + if _application is None: + return + + try: + _application.run() # Quits when all added windows close. + finally: + # Running after quit is undefined, so create a new one next time. + _application = None diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 46b48c2200aa..c6ed6ec86e0f 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -29,13 +29,13 @@ raise ImportError from e from gi.repository import Gio, GLib, GObject, Gtk, Gdk +from ._backend_gtk import ( + _create_application, _shutdown_application, + backend_version, _BackendGTK) _log = logging.getLogger(__name__) -backend_version = "%s.%s.%s" % ( - Gtk.get_major_version(), Gtk.get_minor_version(), Gtk.get_micro_version()) - @_api.caching_module_getattr # module-level deprecations class __getattr__: @@ -56,46 +56,6 @@ def cursord(self): return {} -# Placeholder -_application = None - - -def _shutdown_application(app): - # The application might prematurely shut down if Ctrl-C'd out of IPython, - # so close all windows. - for win in app.get_windows(): - win.destroy() - # The PyGObject wrapper incorrectly thinks that None is not allowed, or we - # would call this: - # Gio.Application.set_default(None) - # Instead, we set this property and ignore default applications with it: - app._created_by_matplotlib = True - global _application - _application = None - - -def _create_application(): - global _application - - if _application is None: - app = Gio.Application.get_default() - if app is None or getattr(app, '_created_by_matplotlib'): - # display_is_valid returns False only if on Linux and neither X11 - # nor Wayland display can be opened. - if not mpl._c_internal_utils.display_is_valid(): - raise RuntimeError('Invalid DISPLAY variable') - _application = Gtk.Application.new('org.matplotlib.Matplotlib3', - Gio.ApplicationFlags.NON_UNIQUE) - # The activate signal must be connected, but we don't care for - # handling it, since we don't do any remote processing. - _application.connect('activate', lambda *args, **kwargs: None) - _application.connect('shutdown', _shutdown_application) - _application.register() - cbook._setup_new_guiapp() - else: - _application = app - - @functools.lru_cache() def _mpl_to_gtk_cursor(mpl_cursor): name = _api.check_getitem({ @@ -373,9 +333,9 @@ class FigureManagerGTK3(FigureManagerBase): """ def __init__(self, canvas, num): - _create_application() + app = _create_application() self.window = Gtk.Window() - _application.add_window(self.window) + app.add_window(self.window) super().__init__(canvas, num) self.window.set_wmclass("matplotlib", "Matplotlib") @@ -868,18 +828,6 @@ def error_msg_gtk(msg, parent=None): @_Backend.export -class _BackendGTK3(_Backend): +class _BackendGTK3(_BackendGTK): FigureCanvas = FigureCanvasGTK3 FigureManager = FigureManagerGTK3 - - @staticmethod - def mainloop(): - global _application - if _application is None: - return - - try: - _application.run() # Quits when all added windows close. - finally: - # Running after quit is undefined, so create a new one next time. - _application = None diff --git a/lib/matplotlib/backends/backend_gtk4.py b/lib/matplotlib/backends/backend_gtk4.py index f2e270da76b4..3858b9f5b02c 100644 --- a/lib/matplotlib/backends/backend_gtk4.py +++ b/lib/matplotlib/backends/backend_gtk4.py @@ -30,52 +30,13 @@ raise ImportError from e from gi.repository import Gio, GLib, GObject, Gtk, Gdk, GdkPixbuf +from ._backend_gtk import ( + _create_application, _shutdown_application, + backend_version, _BackendGTK) _log = logging.getLogger(__name__) -backend_version = "%s.%s.%s" % ( - Gtk.get_major_version(), Gtk.get_minor_version(), Gtk.get_micro_version()) - -# Placeholder -_application = None - - -def _shutdown_application(app): - # The application might prematurely shut down if Ctrl-C'd out of IPython, - # so close all windows. - for win in app.get_windows(): - win.destroy() - # The PyGObject wrapper incorrectly thinks that None is not allowed, or we - # would call this: - # Gio.Application.set_default(None) - # Instead, we set this property and ignore default applications with it: - app._created_by_matplotlib = True - global _application - _application = None - - -def _create_application(): - global _application - - if _application is None: - app = Gio.Application.get_default() - if app is None or getattr(app, '_created_by_matplotlib'): - # display_is_valid returns False only if on Linux and neither X11 - # nor Wayland display can be opened. - if not mpl._c_internal_utils.display_is_valid(): - raise RuntimeError('Invalid DISPLAY variable') - _application = Gtk.Application.new('org.matplotlib.Matplotlib3', - Gio.ApplicationFlags.NON_UNIQUE) - # The activate signal must be connected, but we don't care for - # handling it, since we don't do any remote processing. - _application.connect('activate', lambda *args, **kwargs: None) - _application.connect('shutdown', _shutdown_application) - _application.register() - cbook._setup_new_guiapp() - else: - _application = app - def _mpl_to_gtk_cursor(mpl_cursor): return _api.check_getitem({ @@ -330,9 +291,9 @@ class FigureManagerGTK4(FigureManagerBase): """ def __init__(self, canvas, num): - _create_application() + app = _create_application() self.window = Gtk.Window() - _application.add_window(self.window) + app.add_window(self.window) super().__init__(canvas, num) try: @@ -815,18 +776,6 @@ def trigger(self, *args, **kwargs): @_Backend.export -class _BackendGTK4(_Backend): +class _BackendGTK4(_BackendGTK): FigureCanvas = FigureCanvasGTK4 FigureManager = FigureManagerGTK4 - - @staticmethod - def mainloop(): - global _application - if _application is None: - return - - try: - _application.run() # Quits when all added windows close. - finally: - # Running after quit is undefined, so create a new one next time. - _application = None From e135227863fe6336c0fc44bf94145991051977ae Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 3 Aug 2021 20:33:16 -0400 Subject: [PATCH 17/61] Combine common GTK timer code. --- lib/matplotlib/backends/_backend_gtk.py | 40 +++++++++++++++++++++++-- lib/matplotlib/backends/backend_gtk3.py | 40 ++----------------------- lib/matplotlib/backends/backend_gtk4.py | 40 ++----------------------- 3 files changed, 44 insertions(+), 76 deletions(-) diff --git a/lib/matplotlib/backends/_backend_gtk.py b/lib/matplotlib/backends/_backend_gtk.py index 1f7ca5107594..60efc2f65c11 100644 --- a/lib/matplotlib/backends/_backend_gtk.py +++ b/lib/matplotlib/backends/_backend_gtk.py @@ -7,12 +7,12 @@ import matplotlib as mpl from matplotlib import cbook from matplotlib.backend_bases import ( - _Backend, + _Backend, TimerBase, ) # The GTK3/GTK4 backends will have already called `gi.require_version` to set # the desired GTK. -from gi.repository import Gio, Gtk +from gi.repository import Gio, GLib, Gtk _log = logging.getLogger(__name__) @@ -62,6 +62,42 @@ def _create_application(): return _application +class TimerGTK(TimerBase): + """Subclass of `.TimerBase` using GTK timer events.""" + + def __init__(self, *args, **kwargs): + self._timer = None + super().__init__(*args, **kwargs) + + def _timer_start(self): + # Need to stop it, otherwise we potentially leak a timer id that will + # never be stopped. + self._timer_stop() + self._timer = GLib.timeout_add(self._interval, self._on_timer) + + def _timer_stop(self): + if self._timer is not None: + GLib.source_remove(self._timer) + self._timer = None + + def _timer_set_interval(self): + # Only stop and restart it if the timer has already been started. + if self._timer is not None: + self._timer_stop() + self._timer_start() + + def _on_timer(self): + super()._on_timer() + + # Gtk timeout_add() requires that the callback returns True if it + # is to be called again. + if self.callbacks and not self._single: + return True + else: + self._timer = None + return False + + class _BackendGTK(_Backend): @staticmethod def mainloop(): diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index c6ed6ec86e0f..38a20d570fde 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -31,7 +31,9 @@ from gi.repository import Gio, GLib, GObject, Gtk, Gdk from ._backend_gtk import ( _create_application, _shutdown_application, - backend_version, _BackendGTK) + backend_version, _BackendGTK, + TimerGTK as TimerGTK3, +) _log = logging.getLogger(__name__) @@ -70,42 +72,6 @@ def _mpl_to_gtk_cursor(mpl_cursor): return Gdk.Cursor.new_from_name(Gdk.Display.get_default(), name) -class TimerGTK3(TimerBase): - """Subclass of `.TimerBase` using GTK3 timer events.""" - - def __init__(self, *args, **kwargs): - self._timer = None - super().__init__(*args, **kwargs) - - def _timer_start(self): - # Need to stop it, otherwise we potentially leak a timer id that will - # never be stopped. - self._timer_stop() - self._timer = GLib.timeout_add(self._interval, self._on_timer) - - def _timer_stop(self): - if self._timer is not None: - GLib.source_remove(self._timer) - self._timer = None - - def _timer_set_interval(self): - # Only stop and restart it if the timer has already been started - if self._timer is not None: - self._timer_stop() - self._timer_start() - - def _on_timer(self): - super()._on_timer() - - # Gtk timeout_add() requires that the callback returns True if it - # is to be called again. - if self.callbacks and not self._single: - return True - else: - self._timer = None - return False - - class FigureCanvasGTK3(Gtk.DrawingArea, FigureCanvasBase): required_interactive_framework = "gtk3" _timer_cls = TimerGTK3 diff --git a/lib/matplotlib/backends/backend_gtk4.py b/lib/matplotlib/backends/backend_gtk4.py index 3858b9f5b02c..e88661a57f12 100644 --- a/lib/matplotlib/backends/backend_gtk4.py +++ b/lib/matplotlib/backends/backend_gtk4.py @@ -32,7 +32,9 @@ from gi.repository import Gio, GLib, GObject, Gtk, Gdk, GdkPixbuf from ._backend_gtk import ( _create_application, _shutdown_application, - backend_version, _BackendGTK) + backend_version, _BackendGTK, + TimerGTK as TimerGTK4, +) _log = logging.getLogger(__name__) @@ -50,42 +52,6 @@ def _mpl_to_gtk_cursor(mpl_cursor): }, cursor=mpl_cursor) -class TimerGTK4(TimerBase): - """Subclass of `.TimerBase` using GTK4 timer events.""" - - def __init__(self, *args, **kwargs): - self._timer = None - super().__init__(*args, **kwargs) - - def _timer_start(self): - # Need to stop it, otherwise we potentially leak a timer id that will - # never be stopped. - self._timer_stop() - self._timer = GLib.timeout_add(self._interval, self._on_timer) - - def _timer_stop(self): - if self._timer is not None: - GLib.source_remove(self._timer) - self._timer = None - - def _timer_set_interval(self): - # Only stop and restart it if the timer has already been started - if self._timer is not None: - self._timer_stop() - self._timer_start() - - def _on_timer(self): - super()._on_timer() - - # Gtk timeout_add() requires that the callback returns True if it - # is to be called again. - if self.callbacks and not self._single: - return True - else: - self._timer = None - return False - - class FigureCanvasGTK4(Gtk.DrawingArea, FigureCanvasBase): required_interactive_framework = "gtk4" _timer_cls = TimerGTK4 From 1d6fd65c07f9437e3f2550e9b4774195171a0a1b Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 4 Aug 2021 20:17:22 -0400 Subject: [PATCH 18/61] Combine common GTK toolbar code. --- lib/matplotlib/backends/_backend_gtk.py | 66 +++++++++++++++- lib/matplotlib/backends/backend_gtk3.py | 100 +++++------------------- lib/matplotlib/backends/backend_gtk4.py | 64 ++------------- 3 files changed, 90 insertions(+), 140 deletions(-) diff --git a/lib/matplotlib/backends/_backend_gtk.py b/lib/matplotlib/backends/_backend_gtk.py index 60efc2f65c11..47cde9f77cd8 100644 --- a/lib/matplotlib/backends/_backend_gtk.py +++ b/lib/matplotlib/backends/_backend_gtk.py @@ -5,9 +5,9 @@ import logging import matplotlib as mpl -from matplotlib import cbook +from matplotlib import backend_tools, cbook from matplotlib.backend_bases import ( - _Backend, TimerBase, + _Backend, NavigationToolbar2, TimerBase, ) # The GTK3/GTK4 backends will have already called `gi.require_version` to set @@ -98,6 +98,68 @@ def _on_timer(self): return False +class _NavigationToolbar2GTK(NavigationToolbar2): + # Must be implemented in GTK3/GTK4 backends: + # * __init__ + # * save_figure + + def set_message(self, s): + escaped = GLib.markup_escape_text(s) + self.message.set_markup(f'{escaped}') + + def draw_rubberband(self, event, x0, y0, x1, y1): + height = self.canvas.figure.bbox.height + y1 = height - y1 + y0 = height - y0 + rect = [int(val) for val in (x0, y0, x1 - x0, y1 - y0)] + self.canvas._draw_rubberband(rect) + + def remove_rubberband(self): + self.canvas._draw_rubberband(None) + + def _update_buttons_checked(self): + for name, active in [("Pan", "PAN"), ("Zoom", "ZOOM")]: + button = self._gtk_ids.get(name) + if button: + with button.handler_block(button._signal_handler): + button.set_active(self.mode.name == active) + + def pan(self, *args): + super().pan(*args) + self._update_buttons_checked() + + def zoom(self, *args): + super().zoom(*args) + self._update_buttons_checked() + + def set_history_buttons(self): + can_backward = self._nav_stack._pos > 0 + can_forward = self._nav_stack._pos < len(self._nav_stack._elements) - 1 + if 'Back' in self._gtk_ids: + self._gtk_ids['Back'].set_sensitive(can_backward) + if 'Forward' in self._gtk_ids: + self._gtk_ids['Forward'].set_sensitive(can_forward) + + +class RubberbandGTK(backend_tools.RubberbandBase): + def draw_rubberband(self, x0, y0, x1, y1): + _NavigationToolbar2GTK.draw_rubberband( + self._make_classic_style_pseudo_toolbar(), None, x0, y0, x1, y1) + + def remove_rubberband(self): + _NavigationToolbar2GTK.remove_rubberband( + self._make_classic_style_pseudo_toolbar()) + + +class ConfigureSubplotsGTK(backend_tools.ConfigureSubplotsBase, Gtk.Window): + def _get_canvas(self, fig): + return self.canvas.__class__(fig) + + def trigger(self, *args): + _NavigationToolbar2GTK.configure_subplots( + self._make_classic_style_pseudo_toolbar(), None) + + class _BackendGTK(_Backend): @staticmethod def mainloop(): diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 38a20d570fde..95892999021f 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -31,8 +31,10 @@ from gi.repository import Gio, GLib, GObject, Gtk, Gdk from ._backend_gtk import ( _create_application, _shutdown_application, - backend_version, _BackendGTK, + backend_version, _BackendGTK, _NavigationToolbar2GTK, TimerGTK as TimerGTK3, + ConfigureSubplotsGTK as ConfigureSubplotsGTK3, + RubberbandGTK as RubberbandGTK3, ) @@ -291,7 +293,7 @@ class FigureManagerGTK3(FigureManagerBase): num : int or str The Figure number toolbar : Gtk.Toolbar - The Gtk.Toolbar + The toolbar vbox : Gtk.VBox The Gtk.VBox containing the canvas and toolbar window : Gtk.Window @@ -418,7 +420,7 @@ def resize(self, width, height): self.window.resize(width, height) -class NavigationToolbar2GTK3(NavigationToolbar2, Gtk.Toolbar): +class NavigationToolbar2GTK3(_NavigationToolbar2GTK, Gtk.Toolbar): def __init__(self, canvas, window): self.win = window GObject.GObject.__init__(self) @@ -435,21 +437,16 @@ def __init__(self, canvas, window): str(cbook._get_data_path('images', f'{image_file}-symbolic.svg'))), Gtk.IconSize.LARGE_TOOLBAR) - self._gtk_ids[text] = tbutton = ( + self._gtk_ids[text] = button = ( Gtk.ToggleToolButton() if callback in ['zoom', 'pan'] else Gtk.ToolButton()) - tbutton.set_label(text) - tbutton.set_icon_widget(image) - self.insert(tbutton, -1) + button.set_label(text) + button.set_icon_widget(image) # Save the handler id, so that we can block it as needed. - tbutton._signal_handler = tbutton.connect( + button._signal_handler = button.connect( 'clicked', getattr(self, callback)) - tbutton.set_tooltip_text(tooltip_text) - - toolitem = Gtk.SeparatorToolItem() - self.insert(toolitem, -1) - toolitem.set_draw(False) - toolitem.set_expand(True) + button.set_tooltip_text(tooltip_text) + self.insert(button, -1) # This filler item ensures the toolbar is always at least two text # lines high. Otherwise the canvas gets redrawn as the mouse hovers @@ -460,6 +457,7 @@ def __init__(self, canvas, window): label = Gtk.Label() label.set_markup( '\N{NO-BREAK SPACE}\n\N{NO-BREAK SPACE}') + toolitem.set_expand(True) # Push real message to the right. toolitem.add(label) toolitem = Gtk.ToolItem() @@ -471,35 +469,6 @@ def __init__(self, canvas, window): NavigationToolbar2.__init__(self, canvas) - def set_message(self, s): - escaped = GLib.markup_escape_text(s) - self.message.set_markup(f'{escaped}') - - def draw_rubberband(self, event, x0, y0, x1, y1): - height = self.canvas.figure.bbox.height - y1 = height - y1 - y0 = height - y0 - rect = [int(val) for val in (x0, y0, x1 - x0, y1 - y0)] - self.canvas._draw_rubberband(rect) - - def remove_rubberband(self): - self.canvas._draw_rubberband(None) - - def _update_buttons_checked(self): - for name, active in [("Pan", "PAN"), ("Zoom", "ZOOM")]: - button = self._gtk_ids.get(name) - if button: - with button.handler_block(button._signal_handler): - button.set_active(self.mode.name == active) - - def pan(self, *args): - super().pan(*args) - self._update_buttons_checked() - - def zoom(self, *args): - super().zoom(*args) - self._update_buttons_checked() - def save_figure(self, *args): dialog = Gtk.FileChooserDialog( title="Save the figure", @@ -544,14 +513,6 @@ def on_notify_filter(*args): except Exception as e: error_msg_gtk(str(e), parent=self) - def set_history_buttons(self): - can_backward = self._nav_stack._pos > 0 - can_forward = self._nav_stack._pos < len(self._nav_stack._elements) - 1 - if 'Back' in self._gtk_ids: - self._gtk_ids['Back'].set_sensitive(can_backward) - if 'Forward' in self._gtk_ids: - self._gtk_ids['Forward'].set_sensitive(can_forward) - class ToolbarGTK3(ToolContainerBase, Gtk.Box): _icon_extension = '-symbolic.svg' @@ -569,26 +530,26 @@ def __init__(self, toolmanager): def add_toolitem(self, name, group, position, image_file, description, toggle): if toggle: - tbutton = Gtk.ToggleToolButton() + button = Gtk.ToggleToolButton() else: - tbutton = Gtk.ToolButton() - tbutton.set_label(name) + button = Gtk.ToolButton() + button.set_label(name) if image_file is not None: image = Gtk.Image.new_from_gicon( Gio.Icon.new_for_string(image_file), Gtk.IconSize.LARGE_TOOLBAR) - tbutton.set_icon_widget(image) + button.set_icon_widget(image) if position is None: position = -1 - self._add_button(tbutton, group, position) - signal = tbutton.connect('clicked', self._call_tool, name) - tbutton.set_tooltip_text(description) - tbutton.show_all() + self._add_button(button, group, position) + signal = button.connect('clicked', self._call_tool, name) + button.set_tooltip_text(description) + button.show_all() self._toolitems.setdefault(name, []) - self._toolitems[name].append((tbutton, signal)) + self._toolitems[name].append((button, signal)) def _add_button(self, button, group, position): if group not in self._groups: @@ -633,16 +594,6 @@ def set_message(self, s): self._message.set_label(s) -class RubberbandGTK3(backend_tools.RubberbandBase): - def draw_rubberband(self, x0, y0, x1, y1): - NavigationToolbar2GTK3.draw_rubberband( - self._make_classic_style_pseudo_toolbar(), None, x0, y0, x1, y1) - - def remove_rubberband(self): - NavigationToolbar2GTK3.remove_rubberband( - self._make_classic_style_pseudo_toolbar()) - - class SaveFigureGTK3(backend_tools.SaveFigureBase): def trigger(self, *args, **kwargs): @@ -659,15 +610,6 @@ def set_cursor(self, cursor): self._make_classic_style_pseudo_toolbar(), cursor) -class ConfigureSubplotsGTK3(backend_tools.ConfigureSubplotsBase, Gtk.Window): - def _get_canvas(self, fig): - return self.canvas.__class__(fig) - - def trigger(self, *args): - NavigationToolbar2GTK3.configure_subplots( - self._make_classic_style_pseudo_toolbar(), None) - - class HelpGTK3(backend_tools.ToolHelpBase): def _normalize_shortcut(self, key): """ diff --git a/lib/matplotlib/backends/backend_gtk4.py b/lib/matplotlib/backends/backend_gtk4.py index e88661a57f12..a7aa6a1c1757 100644 --- a/lib/matplotlib/backends/backend_gtk4.py +++ b/lib/matplotlib/backends/backend_gtk4.py @@ -32,8 +32,10 @@ from gi.repository import Gio, GLib, GObject, Gtk, Gdk, GdkPixbuf from ._backend_gtk import ( _create_application, _shutdown_application, - backend_version, _BackendGTK, + backend_version, _BackendGTK, _NavigationToolbar2GTK, TimerGTK as TimerGTK4, + ConfigureSubplotsGTK as ConfigureSubplotsGTK4, + RubberbandGTK as RubberbandGTK4, ) @@ -249,7 +251,7 @@ class FigureManagerGTK4(FigureManagerBase): num : int or str The Figure number toolbar : Gtk.Box - The Gtk.Box + The toolbar vbox : Gtk.VBox The Gtk.VBox containing the canvas and toolbar window : Gtk.Window @@ -368,7 +370,7 @@ def resize(self, width, height): self.window.resize(width, height) -class NavigationToolbar2GTK4(NavigationToolbar2, Gtk.Box): +class NavigationToolbar2GTK4(_NavigationToolbar2GTK, Gtk.Box): def __init__(self, canvas, window): self.win = window Gtk.Box.__init__(self) @@ -411,35 +413,6 @@ def __init__(self, canvas, window): NavigationToolbar2.__init__(self, canvas) - def set_message(self, s): - escaped = GLib.markup_escape_text(s) - self.message.set_markup(f'{escaped}') - - def draw_rubberband(self, event, x0, y0, x1, y1): - height = self.canvas.figure.bbox.height - y1 = height - y1 - y0 = height - y0 - rect = [int(val) for val in (x0, y0, x1 - x0, y1 - y0)] - self.canvas._draw_rubberband(rect) - - def remove_rubberband(self): - self.canvas._draw_rubberband(None) - - def _update_buttons_checked(self): - for name, active in [("Pan", "PAN"), ("Zoom", "ZOOM")]: - button = self._gtk_ids.get(name) - if button: - with button.handler_block(button._signal_handler): - button.set_active(self.mode.name == active) - - def pan(self, *args): - super().pan(*args) - self._update_buttons_checked() - - def zoom(self, *args): - super().zoom(*args) - self._update_buttons_checked() - def save_figure(self, *args): dialog = Gtk.FileChooserNative( title='Save the figure', @@ -502,14 +475,6 @@ def on_response(dialog, response): dialog.show() - def set_history_buttons(self): - can_backward = self._nav_stack._pos > 0 - can_forward = self._nav_stack._pos < len(self._nav_stack._elements) - 1 - if 'Back' in self._gtk_ids: - self._gtk_ids['Back'].set_sensitive(can_backward) - if 'Forward' in self._gtk_ids: - self._gtk_ids['Forward'].set_sensitive(can_forward) - class ToolbarGTK4(ToolContainerBase, Gtk.Box): _icon_extension = '-symbolic.svg' @@ -611,16 +576,6 @@ def set_message(self, s): self._message.set_label(s) -class RubberbandGTK4(backend_tools.RubberbandBase): - def draw_rubberband(self, x0, y0, x1, y1): - NavigationToolbar2GTK4.draw_rubberband( - self._make_classic_style_pseudo_toolbar(), None, x0, y0, x1, y1) - - def remove_rubberband(self): - NavigationToolbar2GTK4.remove_rubberband( - self._make_classic_style_pseudo_toolbar()) - - class SaveFigureGTK4(backend_tools.SaveFigureBase): def trigger(self, *args, **kwargs): @@ -630,15 +585,6 @@ class PseudoToolbar: return NavigationToolbar2GTK4.save_figure(PseudoToolbar()) -class ConfigureSubplotsGTK4(backend_tools.ConfigureSubplotsBase, Gtk.Window): - def _get_canvas(self, fig): - return self.canvas.__class__(fig) - - def trigger(self, *args): - NavigationToolbar2GTK4.configure_subplots( - self._make_classic_style_pseudo_toolbar(), None) - - class HelpGTK4(backend_tools.ToolHelpBase): def _normalize_shortcut(self, key): """ From 68b4ee3f190b8ce5b6523c04232a6d4d9eebc39d Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 5 Aug 2021 19:40:19 -0400 Subject: [PATCH 19/61] Clean up calls to Gtk.Window.set_icon_from_file. According to the docs, this function was added in GTK 2.2, so probably was only needed when we supported GTK2. It also no longer exists in GTK4. --- lib/matplotlib/backends/backend_gtk3.py | 8 +------- lib/matplotlib/backends/backend_gtk4.py | 12 ------------ 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 95892999021f..7701d7742bf0 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -307,13 +307,7 @@ def __init__(self, canvas, num): super().__init__(canvas, num) self.window.set_wmclass("matplotlib", "Matplotlib") - try: - self.window.set_icon_from_file(window_icon) - except Exception: - # Some versions of gtk throw a glib.GError but not all, so I am not - # sure how to catch it. I am unhappy doing a blanket catch here, - # but am not sure what a better way is - JDH - _log.info('Could not load matplotlib icon: %s', sys.exc_info()[1]) + self.window.set_icon_from_file(window_icon) self.vbox = Gtk.Box() self.vbox.set_property("orientation", Gtk.Orientation.VERTICAL) diff --git a/lib/matplotlib/backends/backend_gtk4.py b/lib/matplotlib/backends/backend_gtk4.py index a7aa6a1c1757..0babb1ff4974 100644 --- a/lib/matplotlib/backends/backend_gtk4.py +++ b/lib/matplotlib/backends/backend_gtk4.py @@ -1,6 +1,5 @@ import functools import io -import logging import os from pathlib import Path import sys @@ -39,9 +38,6 @@ ) -_log = logging.getLogger(__name__) - - def _mpl_to_gtk_cursor(mpl_cursor): return _api.check_getitem({ Cursors.MOVE: "move", @@ -264,14 +260,6 @@ def __init__(self, canvas, num): app.add_window(self.window) super().__init__(canvas, num) - try: - self.window.set_icon_from_file(window_icon) - except Exception: - # Some versions of gtk throw a glib.GError but not all, so I am not - # sure how to catch it. I am unhappy doing a blanket catch here, - # but am not sure what a better way is - JDH - _log.info('Could not load matplotlib icon: %s', sys.exc_info()[1]) - self.vbox = Gtk.Box() self.vbox.set_property("orientation", Gtk.Orientation.VERTICAL) self.window.set_child(self.vbox) From 7b63dee50c4e503f0e4e77b288de251d509b4c2a Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 5 Aug 2021 22:48:19 -0400 Subject: [PATCH 20/61] Add GTK4 to docs. Since these examples don't need to support the very oldest GTK3, I took the opportunity to rewrite them using the recommended Application-styled model. --- .flake8 | 8 +- doc/api/backend_gtk4_api.rst | 16 ++++ doc/api/index_backend_api.rst | 1 + doc/devel/dependencies.rst | 4 +- .../embedding_in_gtk3_panzoom_sgskip.py | 2 +- .../embedding_in_gtk3_sgskip.py | 2 +- .../embedding_in_gtk4_panzoom_sgskip.py | 51 +++++++++++ .../embedding_in_gtk4_sgskip.py | 45 +++++++++ ...t_sgskip.py => gtk3_spreadsheet_sgskip.py} | 6 +- .../gtk4_spreadsheet_sgskip.py | 91 +++++++++++++++++++ ...tk_sgskip.py => pylab_with_gtk3_sgskip.py} | 6 +- .../user_interfaces/pylab_with_gtk4_sgskip.py | 51 +++++++++++ tutorials/introductory/sample_plots.py | 1 + tutorials/introductory/usage.py | 14 ++- 14 files changed, 282 insertions(+), 16 deletions(-) create mode 100644 doc/api/backend_gtk4_api.rst create mode 100644 examples/user_interfaces/embedding_in_gtk4_panzoom_sgskip.py create mode 100644 examples/user_interfaces/embedding_in_gtk4_sgskip.py rename examples/user_interfaces/{gtk_spreadsheet_sgskip.py => gtk3_spreadsheet_sgskip.py} (97%) create mode 100644 examples/user_interfaces/gtk4_spreadsheet_sgskip.py rename examples/user_interfaces/{pylab_with_gtk_sgskip.py => pylab_with_gtk3_sgskip.py} (96%) create mode 100644 examples/user_interfaces/pylab_with_gtk4_sgskip.py diff --git a/.flake8 b/.flake8 index 06ad576c1b19..82b495f4cfe3 100644 --- a/.flake8 +++ b/.flake8 @@ -123,8 +123,12 @@ per-file-ignores = examples/ticks_and_spines/date_concise_formatter.py: E402 examples/user_interfaces/embedding_in_gtk3_panzoom_sgskip.py: E402 examples/user_interfaces/embedding_in_gtk3_sgskip.py: E402 - examples/user_interfaces/gtk_spreadsheet_sgskip.py: E402 + examples/user_interfaces/embedding_in_gtk4_panzoom_sgskip.py: E402 + examples/user_interfaces/embedding_in_gtk4_sgskip.py: E402 + examples/user_interfaces/gtk3_spreadsheet_sgskip.py: E402 + examples/user_interfaces/gtk4_spreadsheet_sgskip.py: E402 examples/user_interfaces/mpl_with_glade3_sgskip.py: E402 - examples/user_interfaces/pylab_with_gtk_sgskip.py: E402 + examples/user_interfaces/pylab_with_gtk3_sgskip.py: E402 + examples/user_interfaces/pylab_with_gtk4_sgskip.py: E402 examples/user_interfaces/toolmanager_sgskip.py: E402 examples/userdemo/pgf_preamble_sgskip.py: E402 diff --git a/doc/api/backend_gtk4_api.rst b/doc/api/backend_gtk4_api.rst new file mode 100644 index 000000000000..c2bc05d6f36c --- /dev/null +++ b/doc/api/backend_gtk4_api.rst @@ -0,0 +1,16 @@ +**NOTE** These backends are not documented here, to avoid adding a dependency +to building the docs. + +.. redirect-from:: /api/backend_gtk4agg_api +.. redirect-from:: /api/backend_gtk4cairo_api + + +:mod:`matplotlib.backends.backend_gtk4agg` +========================================== + +.. module:: matplotlib.backends.backend_gtk4agg + +:mod:`matplotlib.backends.backend_gtk4cairo` +============================================ + +.. module:: matplotlib.backends.backend_gtk4cairo diff --git a/doc/api/index_backend_api.rst b/doc/api/index_backend_api.rst index ad2febf8dc38..25c06820c9da 100644 --- a/doc/api/index_backend_api.rst +++ b/doc/api/index_backend_api.rst @@ -10,6 +10,7 @@ backend_agg_api.rst backend_cairo_api.rst backend_gtk3_api.rst + backend_gtk4_api.rst backend_nbagg_api.rst backend_pdf_api.rst backend_pgf_api.rst diff --git a/doc/devel/dependencies.rst b/doc/devel/dependencies.rst index 5502664f4b35..1007210b8ba9 100644 --- a/doc/devel/dependencies.rst +++ b/doc/devel/dependencies.rst @@ -42,9 +42,9 @@ and the capabilities they provide. * Tk_ (>= 8.4, != 8.6.0 or 8.6.1) [#]_: for the Tk-based backends. * PyQt6_ (>= 6.1), PySide6_, PyQt5_, or PySide2_: for the Qt-based backends. -* PyGObject_: for the GTK3-based backends [#]_. +* PyGObject_: for the GTK-based backends [#]_. * wxPython_ (>= 4) [#]_: for the wx-based backends. -* pycairo_ (>= 1.11.0) or cairocffi_ (>= 0.8): for the GTK3 and/or cairo-based +* pycairo_ (>= 1.11.0) or cairocffi_ (>= 0.8): for the GTK and/or cairo-based backends. * Tornado_: for the WebAgg backend. diff --git a/examples/user_interfaces/embedding_in_gtk3_panzoom_sgskip.py b/examples/user_interfaces/embedding_in_gtk3_panzoom_sgskip.py index 2f0833f09511..95d8df21a3a2 100644 --- a/examples/user_interfaces/embedding_in_gtk3_panzoom_sgskip.py +++ b/examples/user_interfaces/embedding_in_gtk3_panzoom_sgskip.py @@ -20,7 +20,7 @@ win = Gtk.Window() win.connect("delete-event", Gtk.main_quit) win.set_default_size(400, 300) -win.set_title("Embedding in GTK") +win.set_title("Embedding in GTK3") fig = Figure(figsize=(5, 4), dpi=100) ax = fig.add_subplot(1, 1, 1) diff --git a/examples/user_interfaces/embedding_in_gtk3_sgskip.py b/examples/user_interfaces/embedding_in_gtk3_sgskip.py index f5872304964d..b672ba8d9ff0 100644 --- a/examples/user_interfaces/embedding_in_gtk3_sgskip.py +++ b/examples/user_interfaces/embedding_in_gtk3_sgskip.py @@ -19,7 +19,7 @@ win = Gtk.Window() win.connect("delete-event", Gtk.main_quit) win.set_default_size(400, 300) -win.set_title("Embedding in GTK") +win.set_title("Embedding in GTK3") fig = Figure(figsize=(5, 4), dpi=100) ax = fig.add_subplot() diff --git a/examples/user_interfaces/embedding_in_gtk4_panzoom_sgskip.py b/examples/user_interfaces/embedding_in_gtk4_panzoom_sgskip.py new file mode 100644 index 000000000000..685a278fc7ad --- /dev/null +++ b/examples/user_interfaces/embedding_in_gtk4_panzoom_sgskip.py @@ -0,0 +1,51 @@ +""" +=========================================== +Embedding in GTK4 with a navigation toolbar +=========================================== + +Demonstrate NavigationToolbar with GTK4 accessed via pygobject. +""" + +import gi +gi.require_version('Gtk', '4.0') +from gi.repository import Gtk + +from matplotlib.backends.backend_gtk4 import ( + NavigationToolbar2GTK4 as NavigationToolbar) +from matplotlib.backends.backend_gtk4agg import ( + FigureCanvasGTK4Agg as FigureCanvas) +from matplotlib.figure import Figure +import numpy as np + + +def on_activate(app): + win = Gtk.ApplicationWindow(application=app) + win.set_default_size(400, 300) + win.set_title("Embedding in GTK4") + + fig = Figure(figsize=(5, 4), dpi=100) + ax = fig.add_subplot(1, 1, 1) + t = np.arange(0.0, 3.0, 0.01) + s = np.sin(2*np.pi*t) + ax.plot(t, s) + + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + win.set_child(vbox) + + # Add canvas to vbox + canvas = FigureCanvas(fig) # a Gtk.DrawingArea + canvas.set_hexpand(True) + canvas.set_vexpand(True) + vbox.append(canvas) + + # Create toolbar + toolbar = NavigationToolbar(canvas, win) + vbox.append(toolbar) + + win.show() + + +app = Gtk.Application( + application_id='org.matplotlib.examples.EmbeddingInGTK4PanZoom') +app.connect('activate', on_activate) +app.run(None) diff --git a/examples/user_interfaces/embedding_in_gtk4_sgskip.py b/examples/user_interfaces/embedding_in_gtk4_sgskip.py new file mode 100644 index 000000000000..c92e139de25f --- /dev/null +++ b/examples/user_interfaces/embedding_in_gtk4_sgskip.py @@ -0,0 +1,45 @@ +""" +================= +Embedding in GTK4 +================= + +Demonstrate adding a FigureCanvasGTK4Agg widget to a Gtk.ScrolledWindow using +GTK4 accessed via pygobject. +""" + +import gi +gi.require_version('Gtk', '4.0') +from gi.repository import Gtk + +from matplotlib.backends.backend_gtk4agg import ( + FigureCanvasGTK4Agg as FigureCanvas) +from matplotlib.figure import Figure +import numpy as np + + +def on_activate(app): + win = Gtk.ApplicationWindow(application=app) + win.set_default_size(400, 300) + win.set_title("Embedding in GTK4") + + fig = Figure(figsize=(5, 4), dpi=100) + ax = fig.add_subplot() + t = np.arange(0.0, 3.0, 0.01) + s = np.sin(2*np.pi*t) + ax.plot(t, s) + + # A scrolled margin goes outside the scrollbars and viewport. + sw = Gtk.ScrolledWindow(margin_top=10, margin_bottom=10, + margin_start=10, margin_end=10) + win.set_child(sw) + + canvas = FigureCanvas(fig) # a Gtk.DrawingArea + canvas.set_size_request(800, 600) + sw.set_child(canvas) + + win.show() + + +app = Gtk.Application(application_id='org.matplotlib.examples.EmbeddingInGTK4') +app.connect('activate', on_activate) +app.run(None) diff --git a/examples/user_interfaces/gtk_spreadsheet_sgskip.py b/examples/user_interfaces/gtk3_spreadsheet_sgskip.py similarity index 97% rename from examples/user_interfaces/gtk_spreadsheet_sgskip.py rename to examples/user_interfaces/gtk3_spreadsheet_sgskip.py index 1f0f6e702240..925ea33faa48 100644 --- a/examples/user_interfaces/gtk_spreadsheet_sgskip.py +++ b/examples/user_interfaces/gtk3_spreadsheet_sgskip.py @@ -1,7 +1,7 @@ """ -=============== -GTK Spreadsheet -=============== +================ +GTK3 Spreadsheet +================ Example of embedding Matplotlib in an application and interacting with a treeview to store data. Double click on an entry to update plot data. diff --git a/examples/user_interfaces/gtk4_spreadsheet_sgskip.py b/examples/user_interfaces/gtk4_spreadsheet_sgskip.py new file mode 100644 index 000000000000..047ae4cf974e --- /dev/null +++ b/examples/user_interfaces/gtk4_spreadsheet_sgskip.py @@ -0,0 +1,91 @@ +""" +================ +GTK4 Spreadsheet +================ + +Example of embedding Matplotlib in an application and interacting with a +treeview to store data. Double click on an entry to update plot data. +""" + +import gi +gi.require_version('Gtk', '4.0') +gi.require_version('Gdk', '4.0') +from gi.repository import Gtk + +from matplotlib.backends.backend_gtk4agg import FigureCanvas # or gtk4cairo. + +from numpy.random import random +from matplotlib.figure import Figure + + +class DataManager(Gtk.ApplicationWindow): + num_rows, num_cols = 20, 10 + + data = random((num_rows, num_cols)) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.set_default_size(600, 600) + + self.set_title('GtkListStore demo') + + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, + spacing=8) + self.set_child(vbox) + + label = Gtk.Label(label='Double click a row to plot the data') + vbox.append(label) + + sw = Gtk.ScrolledWindow() + sw.set_has_frame(True) + sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) + sw.set_hexpand(True) + sw.set_vexpand(True) + vbox.append(sw) + + model = self.create_model() + self.treeview = Gtk.TreeView(model=model) + self.treeview.connect('row-activated', self.plot_row) + sw.set_child(self.treeview) + + # Matplotlib stuff + fig = Figure(figsize=(6, 4), constrained_layout=True) + + self.canvas = FigureCanvas(fig) # a Gtk.DrawingArea + self.canvas.set_hexpand(True) + self.canvas.set_vexpand(True) + vbox.append(self.canvas) + ax = fig.add_subplot() + self.line, = ax.plot(self.data[0, :], 'go') # plot the first row + + self.add_columns() + + def plot_row(self, treeview, path, view_column): + ind, = path # get the index into data + points = self.data[ind, :] + self.line.set_ydata(points) + self.canvas.draw() + + def add_columns(self): + for i in range(self.num_cols): + column = Gtk.TreeViewColumn(str(i), Gtk.CellRendererText(), text=i) + self.treeview.append_column(column) + + def create_model(self): + types = [float] * self.num_cols + store = Gtk.ListStore(*types) + for row in self.data: + # Gtk.ListStore.append is broken in PyGObject, so insert manually. + it = store.insert(-1) + store.set(it, {i: val for i, val in enumerate(row)}) + return store + + +def on_activate(app): + manager = DataManager(application=app) + manager.show() + + +app = Gtk.Application(application_id='org.matplotlib.examples.GTK4Spreadsheet') +app.connect('activate', on_activate) +app.run() diff --git a/examples/user_interfaces/pylab_with_gtk_sgskip.py b/examples/user_interfaces/pylab_with_gtk3_sgskip.py similarity index 96% rename from examples/user_interfaces/pylab_with_gtk_sgskip.py rename to examples/user_interfaces/pylab_with_gtk3_sgskip.py index 277f7de2a9eb..4d943032df5a 100644 --- a/examples/user_interfaces/pylab_with_gtk_sgskip.py +++ b/examples/user_interfaces/pylab_with_gtk3_sgskip.py @@ -1,7 +1,7 @@ """ -=============== -pyplot with GTK -=============== +================ +pyplot with GTK3 +================ An example of how to use pyplot to manage your figure windows, but modify the GUI by accessing the underlying GTK widgets. diff --git a/examples/user_interfaces/pylab_with_gtk4_sgskip.py b/examples/user_interfaces/pylab_with_gtk4_sgskip.py new file mode 100644 index 000000000000..6e0cebcce23c --- /dev/null +++ b/examples/user_interfaces/pylab_with_gtk4_sgskip.py @@ -0,0 +1,51 @@ +""" +================ +pyplot with GTK4 +================ + +An example of how to use pyplot to manage your figure windows, but modify the +GUI by accessing the underlying GTK widgets. +""" + +import matplotlib +matplotlib.use('GTK4Agg') # or 'GTK4Cairo' +import matplotlib.pyplot as plt + +import gi +gi.require_version('Gtk', '4.0') +from gi.repository import Gtk + + +fig, ax = plt.subplots() +ax.plot([1, 2, 3], 'ro-', label='easy as 1 2 3') +ax.plot([1, 4, 9], 'gs--', label='easy as 1 2 3 squared') +ax.legend() + +manager = fig.canvas.manager +# you can access the window or vbox attributes this way +toolbar = manager.toolbar +vbox = manager.vbox + +# now let's add a button to the toolbar +button = Gtk.Button(label='Click me') +button.connect('clicked', lambda button: print('hi mom')) +button.set_tooltip_text('Click me for fun and profit') +toolbar.append(button) + +# now let's add a widget to the vbox +label = Gtk.Label() +label.set_markup('Drag mouse over axes for position') +vbox.insert_child_after(label, fig.canvas) + + +def update(event): + if event.xdata is None: + label.set_markup('Drag mouse over axes for position') + else: + label.set_markup( + f'x,y=({event.xdata}, {event.ydata})') + + +fig.canvas.mpl_connect('motion_notify_event', update) + +plt.show() diff --git a/tutorials/introductory/sample_plots.py b/tutorials/introductory/sample_plots.py index 91ae19eb0015..003bc70661ff 100644 --- a/tutorials/introductory/sample_plots.py +++ b/tutorials/introductory/sample_plots.py @@ -338,6 +338,7 @@ For examples of how to embed Matplotlib in different toolkits, see: + * :doc:`/gallery/user_interfaces/embedding_in_gtk4_sgskip` * :doc:`/gallery/user_interfaces/embedding_in_gtk3_sgskip` * :doc:`/gallery/user_interfaces/embedding_in_wx2_sgskip` * :doc:`/gallery/user_interfaces/mpl_with_glade3_sgskip` diff --git a/tutorials/introductory/usage.py b/tutorials/introductory/usage.py index 08b4d6ad00a0..17e623399b65 100644 --- a/tutorials/introductory/usage.py +++ b/tutorials/introductory/usage.py @@ -300,9 +300,10 @@ def my_plotter(ax, data1, data2, param_dict): # Without a backend explicitly set, Matplotlib automatically detects a usable # backend based on what is available on your system and on whether a GUI event # loop is already running. The first usable backend in the following list is -# selected: MacOSX, Qt5Agg, Gtk3Agg, TkAgg, WxAgg, Agg. The last, Agg, is a -# non-interactive backend that can only write to files. It is used on Linux, -# if Matplotlib cannot connect to either an X display or a Wayland display. +# selected: MacOSX, QtAgg, GTK4Agg, Gtk3Agg, TkAgg, WxAgg, Agg. The last, Agg, +# is a non-interactive backend that can only write to files. It is used on +# Linux, if Matplotlib cannot connect to either an X display or a Wayland +# display. # # Here is a detailed description of the configuration methods: # @@ -370,7 +371,7 @@ def my_plotter(ax, data1, data2, param_dict): # from the canvas (the place where the drawing goes). The canonical # renderer for user interfaces is ``Agg`` which uses the `Anti-Grain # Geometry`_ C++ library to make a raster (pixel) image of the figure; it -# is used by the ``QtAgg``, ``GTK3Agg``, ``wxAgg``, ``TkAgg``, and +# is used by the ``QtAgg``, ``GTK4Agg``, ``GTK3Agg``, ``wxAgg``, ``TkAgg``, and # ``macosx`` backends. An alternative renderer is based on the Cairo library, # used by ``QtCairo``, etc. # @@ -419,6 +420,9 @@ def my_plotter(ax, data1, data2, param_dict): # GTK3Agg Agg rendering to a GTK_ 3.x canvas (requires PyGObject_, # and pycairo_ or cairocffi_). This backend can be activated in # IPython with ``%matplotlib gtk3``. +# GTK4Agg Agg rendering to a GTK_ 4.x canvas (requires PyGObject_, +# and pycairo_ or cairocffi_). This backend can be activated in +# IPython with ``%matplotlib gtk4``. # macosx Agg rendering into a Cocoa canvas in OSX. This backend can be # activated in IPython with ``%matplotlib osx``. # TkAgg Agg rendering to a Tk_ canvas (requires TkInter_). This @@ -430,6 +434,8 @@ def my_plotter(ax, data1, data2, param_dict): # figure. # GTK3Cairo Cairo rendering to a GTK_ 3.x canvas (requires PyGObject_, # and pycairo_ or cairocffi_). +# GTK4Cairo Cairo rendering to a GTK_ 4.x canvas (requires PyGObject_, +# and pycairo_ or cairocffi_). # wxAgg Agg rendering to a wxWidgets_ canvas (requires wxPython_ 4). # This backend can be activated in IPython with ``%matplotlib wx``. # ========= ================================================================ From d3804c2872b91c757950566e7a79516decc2bc7a Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 19 Aug 2021 19:08:17 -0400 Subject: [PATCH 21/61] Fix Ctrl+C out of IPython on GTK4. The `Gtk.Window.destroy` doesn't work, but `.close` correctly triggers our cleanup. --- lib/matplotlib/backends/_backend_gtk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/backends/_backend_gtk.py b/lib/matplotlib/backends/_backend_gtk.py index 47cde9f77cd8..f652815f5120 100644 --- a/lib/matplotlib/backends/_backend_gtk.py +++ b/lib/matplotlib/backends/_backend_gtk.py @@ -28,7 +28,7 @@ def _shutdown_application(app): # The application might prematurely shut down if Ctrl-C'd out of IPython, # so close all windows. for win in app.get_windows(): - win.destroy() + win.close() # The PyGObject wrapper incorrectly thinks that None is not allowed, or we # would call this: # Gio.Application.set_default(None) From 3eeae5b1f7c1c308d747ed0da277631ee17d30b6 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 1 Sep 2021 22:26:33 -0400 Subject: [PATCH 22/61] Build wheels for Apple Silicon. --- .github/workflows/cibuildwheel.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/cibuildwheel.yml b/.github/workflows/cibuildwheel.yml index 9114df686a96..47455e74fac0 100644 --- a/.github/workflows/cibuildwheel.yml +++ b/.github/workflows/cibuildwheel.yml @@ -15,6 +15,7 @@ jobs: env: min-numpy-version: "1.17.3" min-numpy-hash: "b6/d6/be8f975f5322336f62371c9abeb936d592c98c047ad63035f1b38ae08efe" + CIBW_ARCHS_MACOS: "x86_64 universal2 arm64" strategy: matrix: os: [ubuntu-18.04, windows-latest, macos-latest] From 640f96d76bce1a41351911551ade5131c34f2f30 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 2 Sep 2021 11:07:10 +0200 Subject: [PATCH 23/61] Cleanup some dviread docstrings. --- lib/matplotlib/dviread.py | 66 ++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 36 deletions(-) diff --git a/lib/matplotlib/dviread.py b/lib/matplotlib/dviread.py index 117c60aa786d..3207a01de8be 100644 --- a/lib/matplotlib/dviread.py +++ b/lib/matplotlib/dviread.py @@ -84,8 +84,6 @@ def _arg(nbytes, signed, dvi, _): def _arg_slen(dvi, delta): """ - Signed, length *delta* - Read *delta* bytes, returning None if *delta* is zero, and the bytes interpreted as a signed integer otherwise. """ @@ -96,26 +94,20 @@ def _arg_slen(dvi, delta): def _arg_slen1(dvi, delta): """ - Signed, length *delta*+1 - Read *delta*+1 bytes, returning the bytes interpreted as signed. """ - return dvi._arg(delta+1, True) + return dvi._arg(delta + 1, True) def _arg_ulen1(dvi, delta): """ - Unsigned length *delta*+1 - Read *delta*+1 bytes, returning the bytes interpreted as unsigned. """ - return dvi._arg(delta+1, False) + return dvi._arg(delta + 1, False) def _arg_olen1(dvi, delta): """ - Optionally signed, length *delta*+1 - Read *delta*+1 bytes, returning the bytes interpreted as unsigned integer for 0<=*delta*<3 and signed if *delta*==3. """ @@ -139,30 +131,30 @@ def _dispatch(table, min, max=None, state=None, args=('raw',)): matches *state* if not None, reads arguments from the file according to *args*. - *table* - the dispatch table to be filled in - - *min* - minimum opcode for calling this function - - *max* - maximum opcode for calling this function, None if only *min* is allowed - - *state* - state of the Dvi object in which these opcodes are allowed - - *args* - sequence of argument specifications: - - ``'raw'``: opcode minus minimum - ``'u1'``: read one unsigned byte - ``'u4'``: read four bytes, treat as an unsigned number - ``'s4'``: read four bytes, treat as a signed number - ``'slen'``: read (opcode - minimum) bytes, treat as signed - ``'slen1'``: read (opcode - minimum + 1) bytes, treat as signed - ``'ulen1'``: read (opcode - minimum + 1) bytes, treat as unsigned - ``'olen1'``: read (opcode - minimum + 1) bytes, treat as unsigned - if under four bytes, signed if four bytes + Parameters + ---------- + table : dict[int, callable] + The dispatch table to be filled in. + + min, max : int + Range of opcodes that calls the registered function; *max* defaults to + *min*. + + state : _dvistate, optional + State of the Dvi object in which these opcodes are allowed. + + args : list[str], default: ['raw'] + Sequence of argument specifications: + + - 'raw': opcode minus minimum + - 'u1': read one unsigned byte + - 'u4': read four bytes, treat as an unsigned number + - 's4': read four bytes, treat as a signed number + - 'slen': read (opcode - minimum) bytes, treat as signed + - 'slen1': read (opcode - minimum + 1) bytes, treat as signed + - 'ulen1': read (opcode - minimum + 1) bytes, treat as unsigned + - 'olen1': read (opcode - minimum + 1) bytes, treat as unsigned + if under four bytes, signed if four bytes """ def decorate(method): get_args = [_arg_mapping[x] for x in args] @@ -185,6 +177,7 @@ def wrapper(self, byte): class Dvi: """ A reader for a dvi ("device-independent") file, as produced by TeX. + The current implementation can only iterate through pages in order, and does not even attempt to verify the postamble. @@ -956,8 +949,9 @@ def _parse_and_cache_line(self, line): def _parse_enc(path): r""" - Parses a \*.enc file referenced from a psfonts.map style file. - The format this class understands is a very limited subset of PostScript. + Parse a \*.enc file referenced from a psfonts.map style file. + + The format supported by this function is a tiny subset of PostScript. Parameters ---------- From 3d7c92cf4ae48d29a87bed5544c56fcf333d7e33 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 2 Sep 2021 11:20:20 +0200 Subject: [PATCH 24/61] Docstring cleanups. Mostly D401 (imperative mood) fixes, and a few other things. --- lib/matplotlib/backend_tools.py | 2 +- lib/matplotlib/backends/backend_wx.py | 4 ++-- lib/matplotlib/colors.py | 2 +- lib/matplotlib/lines.py | 4 ++-- lib/matplotlib/path.py | 2 +- lib/matplotlib/testing/widgets.py | 7 ++++--- lib/matplotlib/widgets.py | 2 +- lib/mpl_toolkits/axisartist/axisline_style.py | 8 ++------ 8 files changed, 14 insertions(+), 17 deletions(-) diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index cc81b1f9269b..390b90134aed 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -900,7 +900,7 @@ class ToolHelpBase(ToolBase): @staticmethod def format_shortcut(key_sequence): """ - Converts a shortcut string from the notation used in rc config to the + Convert a shortcut string from the notation used in rc config to the standard notation for displaying shortcuts, e.g. 'ctrl+a' -> 'Ctrl+A'. """ return (key_sequence if len(key_sequence) == 1 else diff --git a/lib/matplotlib/backends/backend_wx.py b/lib/matplotlib/backends/backend_wx.py index 16b152a51689..da78f69d9e8e 100644 --- a/lib/matplotlib/backends/backend_wx.py +++ b/lib/matplotlib/backends/backend_wx.py @@ -592,8 +592,8 @@ def _get_imagesave_wildcards(self): @_api.delete_parameter("3.4", "origin") def gui_repaint(self, drawDC=None, origin='WX'): """ - Performs update of the displayed image on the GUI canvas, using the - supplied wx.PaintDC device context. + Update the displayed image on the GUI canvas, using the supplied + wx.PaintDC device context. The 'WXAgg' backend sets origin accordingly. """ diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index f75e069d3083..91fe214f6412 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -2324,7 +2324,7 @@ def blend_soft_light(self, rgb, intensity): def blend_overlay(self, rgb, intensity): """ - Combines an rgb image with an intensity map using "overlay" blending. + Combine an rgb image with an intensity map using "overlay" blending. Parameters ---------- diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 8f4ddaa5c6ea..9a727f9a8107 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -601,7 +601,7 @@ def get_markevery(self): def set_picker(self, p): """ - Sets the event picker details for the line. + Set the event picker details for the line. Parameters ---------- @@ -688,7 +688,7 @@ def recache(self, always=False): def _transform_path(self, subslice=None): """ - Puts a TransformedPath instance at self._transformed_path; + Put a TransformedPath instance at self._transformed_path; all invalidation of the transform is then handled by the TransformedPath instance. """ diff --git a/lib/matplotlib/path.py b/lib/matplotlib/path.py index c09250123fb9..4280d55eeacd 100644 --- a/lib/matplotlib/path.py +++ b/lib/matplotlib/path.py @@ -162,7 +162,7 @@ def __init__(self, vertices, codes=None, _interpolation_steps=1, @classmethod def _fast_from_codes_and_verts(cls, verts, codes, internals_from=None): """ - Creates a Path instance without the expense of calling the constructor. + Create a Path instance without the expense of calling the constructor. Parameters ---------- diff --git a/lib/matplotlib/testing/widgets.py b/lib/matplotlib/testing/widgets.py index 49d5cb7175f9..3c3a4b6273bc 100644 --- a/lib/matplotlib/testing/widgets.py +++ b/lib/matplotlib/testing/widgets.py @@ -2,15 +2,16 @@ ======================== Widget testing utilities ======================== -Functions that are useful for testing widgets. -See also matplotlib.tests.test_widgets + +See also :mod:`matplotlib.tests.test_widgets`. """ + import matplotlib.pyplot as plt from unittest import mock def get_ax(): - """Creates plot and returns its axes""" + """Create a plot and return its axes.""" fig, ax = plt.subplots(1, 1) ax.plot([0, 200], [0, 200]) ax.set_aspect(1.0) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 155b3b3f7b15..220fb1d280cd 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -2710,7 +2710,7 @@ def onselect(eclick: MouseEvent, erelease: MouseEvent) - "move": Move the existing shape, default: no modifier. - "clear": Clear the current shape, default: "escape". - - "square": Makes the shape square, default: "shift". + - "square": Make the shape square, default: "shift". - "center": Make the initial point the center of the shape, default: "ctrl". diff --git a/lib/mpl_toolkits/axisartist/axisline_style.py b/lib/mpl_toolkits/axisartist/axisline_style.py index 80f3ce58eb48..db4b0c144c5e 100644 --- a/lib/mpl_toolkits/axisartist/axisline_style.py +++ b/lib/mpl_toolkits/axisartist/axisline_style.py @@ -9,9 +9,7 @@ class _FancyAxislineStyle: class SimpleArrow(FancyArrowPatch): - """ - The artist class that will be returned for SimpleArrow style. - """ + """The artist class that will be returned for SimpleArrow style.""" _ARROW_STYLE = "->" def __init__(self, axis_artist, line_path, transform, @@ -69,9 +67,7 @@ def draw(self, renderer): FancyArrowPatch.draw(self, renderer) class FilledArrow(SimpleArrow): - """ - The artist class that will be returned for SimpleArrow style. - """ + """The artist class that will be returned for SimpleArrow style.""" _ARROW_STYLE = "-|>" From a1c69dd1a1a1baf1376551a57bd5ffb1df687c23 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 2 Sep 2021 11:59:54 +0200 Subject: [PATCH 25/61] Rename symbol_name to glyph_name where appropriate. When referring to a font glyph, "glyph name" is standard terminology (e.g. FT_Get_Glyph_Name or Adobe docs); additionally there is a separate concept of "symbol_name" used by the mathtext parser which has a different meaning (`\foo` commands referring to TeX symbol names), so let's not confuse them. symbol_name is kept in the mathtext SimpleNamespaces for backcompat (changing the SimpleNamespace to a proper class with a property handling the deprecation is not worth the work). --- lib/matplotlib/_mathtext.py | 24 +++++++++++++----------- lib/matplotlib/backends/backend_pdf.py | 12 ++++++------ lib/matplotlib/backends/backend_ps.py | 4 ++-- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index bbb247ec02fe..73e6163944e1 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -264,7 +264,7 @@ def _get_info(self, fontname, font_class, sym, fontsize, dpi, math=True): if bunch is not None: return bunch - font, num, symbol_name, fontsize, slanted = \ + font, num, glyph_name, fontsize, slanted = \ self._get_glyph(fontname, font_class, sym, fontsize, math) font.set_size(fontsize, dpi) @@ -292,7 +292,8 @@ def _get_info(self, fontname, font_class, sym, fontsize, dpi, math=True): fontsize = fontsize, postscript_name = font.postscript_name, metrics = metrics, - symbol_name = symbol_name, + glyph_name = glyph_name, + symbol_name = glyph_name, # Backcompat alias. num = num, glyph = glyph, offset = offset @@ -358,7 +359,7 @@ def __init__(self, *args, **kwargs): _slanted_symbols = set(r"\int \oint".split()) def _get_glyph(self, fontname, font_class, sym, fontsize, math=True): - symbol_name = None + glyph_name = None font = None if fontname in self.fontmap and sym in latex_to_bakoma: basename, num = latex_to_bakoma[sym] @@ -373,13 +374,13 @@ def _get_glyph(self, fontname, font_class, sym, fontsize, math=True): if font is not None: gid = font.get_char_index(num) if gid != 0: - symbol_name = font.get_glyph_name(gid) + glyph_name = font.get_glyph_name(gid) - if symbol_name is None: + if glyph_name is None: return self._stix_fallback._get_glyph( fontname, font_class, sym, fontsize, math) - return font, num, symbol_name, fontsize, slanted + return font, num, glyph_name, fontsize, slanted # The Bakoma fonts contain many pre-sized alternatives for the # delimiters. The AutoSizedChar class will use these alternatives @@ -556,8 +557,8 @@ def _get_glyph(self, fontname, font_class, sym, fontsize, math=True): glyphindex = font.get_char_index(uniindex) slanted = False - symbol_name = font.get_glyph_name(glyphindex) - return font, uniindex, symbol_name, fontsize, slanted + glyph_name = font.get_glyph_name(glyphindex) + return font, uniindex, glyph_name, fontsize, slanted def get_sized_alternatives_for_symbol(self, fontname, sym): if self.cm_fallback: @@ -854,7 +855,7 @@ def _get_info(self, fontname, font_class, sym, fontsize, dpi, math=True): if found_symbol: try: - symbol_name = font.get_name_char(glyph) + glyph_name = font.get_name_char(glyph) except KeyError: _log.warning( "No glyph in standard Postscript font {!r} for {!r}" @@ -864,7 +865,7 @@ def _get_info(self, fontname, font_class, sym, fontsize, dpi, math=True): if not found_symbol: glyph = '?' num = ord(glyph) - symbol_name = font.get_name_char(glyph) + glyph_name = font.get_name_char(glyph) offset = 0 @@ -890,7 +891,8 @@ def _get_info(self, fontname, font_class, sym, fontsize, dpi, math=True): fontsize = fontsize, postscript_name = font.get_fontname(), metrics = metrics, - symbol_name = symbol_name, + glyph_name = glyph_name, + symbol_name = glyph_name, # Backcompat alias. num = num, glyph = glyph, offset = offset diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index 9ca791db0c5a..bd0c370f1cba 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -1063,12 +1063,12 @@ def createType1Descriptor(self, t1font, fontfile): return fontdescObject - def _get_xobject_symbol_name(self, filename, symbol_name): + def _get_xobject_glyph_name(self, filename, glyph_name): Fx = self.fontName(filename) return "-".join([ Fx.name.decode(), os.path.splitext(os.path.basename(filename))[0], - symbol_name]) + glyph_name]) _identityToUnicodeCMap = b"""/CIDInit /ProcSet findresource begin 12 dict begin @@ -1204,7 +1204,7 @@ def get_char_width(charcode): # Send the glyphs with ccode > 255 to the XObject dictionary, # and the others to the font itself if charname in multi_byte_chars: - name = self._get_xobject_symbol_name(filename, charname) + name = self._get_xobject_glyph_name(filename, charname) self.multi_byte_charprocs[name] = charprocObject else: charprocs[charname] = charprocObject @@ -1347,7 +1347,7 @@ def embedTTFType42(font, characters, descriptor): self.currentstream.write(stream) self.endStream() - name = self._get_xobject_symbol_name(filename, charname) + name = self._get_xobject_glyph_name(filename, charname) self.multi_byte_charprocs[name] = charprocObject # CIDToGIDMap stream @@ -2417,8 +2417,8 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): def _draw_xobject_glyph(self, font, fontsize, glyph_idx, x, y): """Draw a multibyte character from a Type 3 font as an XObject.""" - symbol_name = font.get_glyph_name(glyph_idx) - name = self.file._get_xobject_symbol_name(font.fname, symbol_name) + glyph_name = font.get_glyph_name(glyph_idx) + name = self.file._get_xobject_glyph_name(font.fname, glyph_name) self.file.output( Op.gsave, 0.001 * fontsize, 0, 0, 0.001 * fontsize, x, y, Op.concat_matrix, diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index 93d0705ae363..c44f89c638d9 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -701,12 +701,12 @@ def draw_mathtext(self, gc, x, y, s, prop, angle): lastfont = font.postscript_name, fontsize self._pswriter.write( f"/{font.postscript_name} {fontsize} selectfont\n") - symbol_name = ( + glyph_name = ( font.get_name_char(chr(num)) if isinstance(font, AFM) else font.get_glyph_name(font.get_char_index(num))) self._pswriter.write( f"{ox:f} {oy:f} moveto\n" - f"/{symbol_name} glyphshow\n") + f"/{glyph_name} glyphshow\n") for ox, oy, w, h in rects: self._pswriter.write(f"{ox} {oy} {w} {h} rectfill\n") self._pswriter.write("grestore\n") From 75710ed10e64cb9bb3c4c9f00e06749078ea52b6 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 2 Sep 2021 16:39:57 +0200 Subject: [PATCH 26/61] Clarify support for 2D coordinate inputs to streamplot. --- lib/matplotlib/streamplot.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/streamplot.py b/lib/matplotlib/streamplot.py index 6de221476490..d6bceb6c9915 100644 --- a/lib/matplotlib/streamplot.py +++ b/lib/matplotlib/streamplot.py @@ -26,7 +26,9 @@ def streamplot(axes, x, y, u, v, density=1, linewidth=None, color=None, Parameters ---------- x, y : 1D/2D arrays - Evenly spaced strictly increasing arrays to make a grid. + Evenly spaced strictly increasing arrays to make a grid. If 2D, all + rows of *x* must be equal and all columns of *y* must be equal; i.e., + they must be as if generated by ``np.meshgrid(x_1d, y_1d)``. u, v : 2D arrays *x* and *y*-velocities. The number of rows and columns must match the length of *y* and *x*, respectively. From f23fd1f0c39a3e3d135d858ec41f4c69fbc28d64 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Fri, 3 Sep 2021 08:58:28 +0200 Subject: [PATCH 27/61] Make HandlerLine2D{,Compound} inherit constructors from HandlerNpoints. The constructores are entirely identical (including wrt. defaults and docstrings). --- lib/matplotlib/legend_handler.py | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/lib/matplotlib/legend_handler.py b/lib/matplotlib/legend_handler.py index b289e26cc1f5..d4876ddc5c6b 100644 --- a/lib/matplotlib/legend_handler.py +++ b/lib/matplotlib/legend_handler.py @@ -214,19 +214,6 @@ class HandlerLine2DCompound(HandlerNpoints): a line-only with a marker-only artist. May be deprecated in the future. """ - def __init__(self, marker_pad=0.3, numpoints=None, **kwargs): - """ - Parameters - ---------- - marker_pad : float - Padding between points in legend entry. - numpoints : int - Number of points to show in legend entry. - **kwargs - Keyword arguments forwarded to `.HandlerNpoints`. - """ - super().__init__(marker_pad=marker_pad, numpoints=numpoints, **kwargs) - def create_artists(self, legend, orig_handle, xdescent, ydescent, width, height, fontsize, trans): @@ -286,20 +273,6 @@ class HandlerLine2D(HandlerNpoints): artist for the line and another for the marker(s). """ - def __init__(self, marker_pad=0.3, numpoints=None, **kw): - """ - Parameters - ---------- - marker_pad : float - Padding between points in legend entry. - numpoints : int - Number of points to show in legend entry. - **kwargs - Keyword arguments forwarded to `.HandlerNpoints`. - """ - HandlerNpoints.__init__(self, marker_pad=marker_pad, - numpoints=numpoints, **kw) - def create_artists(self, legend, orig_handle, xdescent, ydescent, width, height, fontsize, trans): From f41edb62de216d76121be1be1828e1752fdc5c59 Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Sat, 4 Sep 2021 10:41:03 +0200 Subject: [PATCH 28/61] FIX: colorbar with boundary norm, proportional, extend --- lib/matplotlib/colorbar.py | 5 ++-- .../test_colorbar/proportional_colorbars.png | Bin 0 -> 11329 bytes lib/matplotlib/tests/test_colorbar.py | 27 ++++++++++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 lib/matplotlib/tests/baseline_images/test_colorbar/proportional_colorbars.png diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index 9d40ac8e5e9c..d826649af167 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -1198,8 +1198,9 @@ def _proportional_y(self): a proportional colorbar, plus extension lengths if required: """ if isinstance(self.norm, colors.BoundaryNorm): - y = (self._boundaries - self._boundaries[0]) - y = y / (self._boundaries[-1] - self._boundaries[0]) + y = (self._boundaries - self._boundaries[self._inside][0]) + y = y / (self._boundaries[self._inside][-1] - + self._boundaries[self._inside][0]) # need yscaled the same as the axes scale to get # the extend lengths. if self.spacing == 'uniform': diff --git a/lib/matplotlib/tests/baseline_images/test_colorbar/proportional_colorbars.png b/lib/matplotlib/tests/baseline_images/test_colorbar/proportional_colorbars.png new file mode 100644 index 0000000000000000000000000000000000000000..a1f9745230ba39cc362d144aa7994dc8a8a86edb GIT binary patch literal 11329 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 literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/test_colorbar.py b/lib/matplotlib/tests/test_colorbar.py index 5f19a0aaf20a..00a6f52698e2 100644 --- a/lib/matplotlib/tests/test_colorbar.py +++ b/lib/matplotlib/tests/test_colorbar.py @@ -827,3 +827,30 @@ def test_aspects(): np.testing.assert_almost_equal( cb[1][0].ax.get_position(original=False).height * 2, cb[1][2].ax.get_position(original=False).height, decimal=2) + + +@image_comparison(['proportional_colorbars.png'], remove_text=True, + style='mpl20') +def test_proportional_colorbars(): + + x = y = np.arange(-3.0, 3.01, 0.025) + X, Y = np.meshgrid(x, y) + Z1 = np.exp(-X**2 - Y**2) + Z2 = np.exp(-(X - 1)**2 - (Y - 1)**2) + Z = (Z1 - Z2) * 2 + + levels = [-1.25, -0.5, -0.125, 0.125, 0.5, 1.25] + cmap = mcolors.ListedColormap( + ['0.3', '0.5', 'white', 'lightblue', 'steelblue']) + cmap.set_under('darkred') + cmap.set_over('crimson') + norm = mcolors.BoundaryNorm(levels, cmap.N) + + extends = ['neither', 'both'] + spacings = ['uniform', 'proportional'] + fig, axs = plt.subplots(2, 2) + for i in range(2): + for j in range(2): + CS3 = axs[i, j].contourf(X, Y, Z, levels, cmap=cmap, norm=norm, + extend=extends[i]) + fig.colorbar(CS3, spacing=spacings[j], ax=axs[i, j]) From 6134061d3cc8ca0b2a3a8b26189c0aca2e7978e4 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sat, 4 Sep 2021 15:39:01 +0200 Subject: [PATCH 29/61] Remove unused icon_filename, window_icon globals. On Gtk4, these have never been part of a released version, and can be directly removed. --- lib/matplotlib/backends/backend_gtk4.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/lib/matplotlib/backends/backend_gtk4.py b/lib/matplotlib/backends/backend_gtk4.py index 0babb1ff4974..3ffff77792db 100644 --- a/lib/matplotlib/backends/backend_gtk4.py +++ b/lib/matplotlib/backends/backend_gtk4.py @@ -658,14 +658,6 @@ def trigger(self, *args, **kwargs): clipboard.set(pb) -# Define the file to use as the GTk icon -if sys.platform == 'win32': - icon_filename = 'matplotlib.png' -else: - icon_filename = 'matplotlib.svg' -window_icon = str(cbook._get_data_path('images', icon_filename)) - - backend_tools.ToolSaveFigure = SaveFigureGTK4 backend_tools.ToolConfigureSubplots = ConfigureSubplotsGTK4 backend_tools.ToolRubberband = RubberbandGTK4 From 4dd72b971c9e6f368e7a12f7da5141ce87bb89a8 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sat, 4 Sep 2021 15:46:35 +0200 Subject: [PATCH 30/61] Deprecate some backend_gtk3 helper globals. It is nicer to just define them at the point of use rather than ~400 lines later. --- .../deprecations/20995-AL.rst | 3 +++ lib/matplotlib/backends/backend_gtk3.py | 19 ++++++++++--------- 2 files changed, 13 insertions(+), 9 deletions(-) create mode 100644 doc/api/next_api_changes/deprecations/20995-AL.rst diff --git a/doc/api/next_api_changes/deprecations/20995-AL.rst b/doc/api/next_api_changes/deprecations/20995-AL.rst new file mode 100644 index 000000000000..bdccd8f5d333 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/20995-AL.rst @@ -0,0 +1,3 @@ +``backend_gtk3.icon_filename`` and ``backend_gtk3.window_icon`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +... are deprecated with no replacement. diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 7701d7742bf0..efbbbb387238 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -59,6 +59,13 @@ def cursord(self): except TypeError as exc: return {} + icon_filename = _api.deprecated("3.6", obj_type="")(property( + lambda self: + "matplotlib.png" if sys.platform == "win32" else "matplotlib.svg")) + window_icon = _api.deprecated("3.6", obj_type="")(property( + lambda self: + str(cbook._get_data_path("images", __getattr__("icon_filename"))))) + @functools.lru_cache() def _mpl_to_gtk_cursor(mpl_cursor): @@ -307,7 +314,9 @@ def __init__(self, canvas, num): super().__init__(canvas, num) self.window.set_wmclass("matplotlib", "Matplotlib") - self.window.set_icon_from_file(window_icon) + icon_ext = "png" if sys.platform == "win32" else "svg" + self.window.set_icon_from_file( + str(cbook._get_data_path(f"images/matplotlib.{icon_ext}"))) self.vbox = Gtk.Box() self.vbox.set_property("orientation", Gtk.Orientation.VERTICAL) @@ -698,14 +707,6 @@ def trigger(self, *args, **kwargs): clipboard.set_image(pb) -# Define the file to use as the GTk icon -if sys.platform == 'win32': - icon_filename = 'matplotlib.png' -else: - icon_filename = 'matplotlib.svg' -window_icon = str(cbook._get_data_path('images', icon_filename)) - - def error_msg_gtk(msg, parent=None): if parent is not None: # find the toplevel Gtk.Window parent = parent.get_toplevel() From 72c728cc899b8ffcf238ffce40b1118fc41b837e Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sat, 4 Sep 2021 16:10:31 +0200 Subject: [PATCH 31/61] Fix ToolManager + TextBox support. Try running e.g. examples/widgets/textbox.py together with `rcParams["toolbar"] = "toolmanager"`. Before this fix, clicking on the TextBox would result in "TypeError: push() takes 2 positional arguments but 3 were given". --- lib/matplotlib/tests/test_widgets.py | 6 +++++- lib/matplotlib/widgets.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index a43cfec6191f..f50402a20a15 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -619,7 +619,11 @@ def test_CheckButtons(): check.disconnect(cid) -def test_TextBox(): +@pytest.mark.parametrize("toolbar", ["none", "toolbar2", "toolmanager"]) +def test_TextBox(toolbar): + # Avoid "toolmanager is provisional" warning. + dict.__setitem__(plt.rcParams, "toolbar", toolbar) + from unittest.mock import Mock submit_event = Mock() text_change_event = Mock() diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 220fb1d280cd..a59b3b1b1677 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1287,7 +1287,7 @@ def begin_typing(self, x): # If using toolmanager, lock keypresses, and plan to release the # lock when typing stops. toolmanager.keypresslock(self) - stack.push(toolmanager.keypresslock.release, self) + stack.callback(toolmanager.keypresslock.release, self) else: # If not using toolmanager, disable all keypress-related rcParams. # Avoid spurious warnings if keymaps are getting deprecated. From 1586fb5716abf88ffab4542bb2ce8aff128d2306 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Mon, 6 Sep 2021 07:54:41 +0200 Subject: [PATCH 32/61] Remove now-unused rcParams _deprecated entries. These entries have already been removed (with the corresponding API change documentation). Also remove the now-unused _all_deprecated. --- lib/matplotlib/__init__.py | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index ac84e82c30f6..8a6f9f62c378 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -575,24 +575,13 @@ def gen_candidates(): # rcParams deprecated and automatically mapped to another key. # Values are tuples of (version, new_name, f_old2new, f_new2old). _deprecated_map = {} - # rcParams deprecated; some can manually be mapped to another key. # Values are tuples of (version, new_name_or_None). -_deprecated_ignore_map = { - 'mpl_toolkits.legacy_colorbar': ('3.4', None), -} - +_deprecated_ignore_map = {} # rcParams deprecated; can use None to suppress warnings; remain actually -# listed in the rcParams (not included in _all_deprecated). +# listed in the rcParams. # Values are tuples of (version,) -_deprecated_remain_as_none = { - 'animation.avconv_path': ('3.3',), - 'animation.avconv_args': ('3.3',), - 'animation.html_args': ('3.3',), -} - - -_all_deprecated = {*_deprecated_map, *_deprecated_ignore_map} +_deprecated_remain_as_none = {} @docstring.Substitution( From f7622b65033d981fa793ef42eba8083abe2422a4 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Mon, 6 Sep 2021 08:22:21 +0200 Subject: [PATCH 33/61] Test the rcParams deprecation machinery. --- lib/matplotlib/tests/test_rcparams.py | 36 +++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/lib/matplotlib/tests/test_rcparams.py b/lib/matplotlib/tests/test_rcparams.py index 17f63f737d84..610ff9b80b40 100644 --- a/lib/matplotlib/tests/test_rcparams.py +++ b/lib/matplotlib/tests/test_rcparams.py @@ -508,3 +508,39 @@ def test_backend_fallback_headful(tmpdir): # The actual backend will depend on what's installed, but at least tkagg is # present. assert backend.strip().lower() != "agg" + + +def test_deprecation(monkeypatch): + monkeypatch.setitem( + mpl._deprecated_map, "patch.linewidth", + ("0.0", "axes.linewidth", lambda old: 2 * old, lambda new: new / 2)) + with pytest.warns(_api.MatplotlibDeprecationWarning): + assert mpl.rcParams["patch.linewidth"] \ + == mpl.rcParams["axes.linewidth"] / 2 + with pytest.warns(_api.MatplotlibDeprecationWarning): + mpl.rcParams["patch.linewidth"] = 1 + assert mpl.rcParams["axes.linewidth"] == 2 + + monkeypatch.setitem( + mpl._deprecated_ignore_map, "patch.edgecolor", + ("0.0", "axes.edgecolor")) + with pytest.warns(_api.MatplotlibDeprecationWarning): + assert mpl.rcParams["patch.edgecolor"] \ + == mpl.rcParams["axes.edgecolor"] + with pytest.warns(_api.MatplotlibDeprecationWarning): + mpl.rcParams["patch.edgecolor"] = "#abcd" + assert mpl.rcParams["axes.edgecolor"] != "#abcd" + + monkeypatch.setitem( + mpl._deprecated_ignore_map, "patch.force_edgecolor", + ("0.0", None)) + with pytest.warns(_api.MatplotlibDeprecationWarning): + assert mpl.rcParams["patch.force_edgecolor"] is None + + monkeypatch.setitem( + mpl._deprecated_remain_as_none, "svg.hashsalt", + ("0.0",)) + with pytest.warns(_api.MatplotlibDeprecationWarning): + mpl.rcParams["svg.hashsalt"] = "foobar" + assert mpl.rcParams["svg.hashsalt"] == "foobar" # Doesn't warn. + mpl.rcParams["svg.hashsalt"] = None # Doesn't warn. From 162f68835f95bafcdaa9ad7338e77add599166db Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sun, 5 Sep 2021 21:24:07 +0200 Subject: [PATCH 34/61] Deemphasize mpl_toolkits in API docs. The docs should present mpl_toolkits as more or less regular modules, that just happen to live in a different toplevel package for historical reasons. So just concatenate them to the end of the main module list. (This is particularly in preparation of the new website redesign, where the separate "Extra Toolkits" grouping is really too conspicuous on the front page.) Remove the toolkits/index page (documenting mplot3d and axis_grid/axisartist ("ag/aa") together doesn't make much sense except for this historical grouping), merging the mplot3d intro blurb together with the one previously at mplot3d/index.rst; the ag/aa blurbs were just included from the ag/aa API docs themselves and therefore don't need to be kept. The mplot3d intro blurb itself can go to the top of the mplot3d API docs, rather than needing a separate page. --- doc/api/index.rst | 15 ---------- doc/api/toolkits/index.rst | 46 ------------------------------ doc/api/toolkits/mplot3d.rst | 32 ++++++++++++++++++--- doc/api/toolkits/mplot3d/index.rst | 28 ------------------ doc/devel/testing.rst | 2 +- doc/index.rst | 2 -- 6 files changed, 29 insertions(+), 96 deletions(-) delete mode 100644 doc/api/toolkits/index.rst delete mode 100644 doc/api/toolkits/mplot3d/index.rst diff --git a/doc/api/index.rst b/doc/api/index.rst index 9c33f38ad6d8..ed0e7f310275 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -101,21 +101,6 @@ Alphabetical list of modules: widgets_api.rst _api_api.rst _enums_api.rst - -Toolkits --------- - -:ref:`toolkits-index` are collections of application-specific functions that extend -Matplotlib. The following toolkits are included: - -.. toctree:: - :hidden: - - toolkits/index.rst - -.. toctree:: - :maxdepth: 1 - toolkits/mplot3d.rst toolkits/axes_grid1.rst toolkits/axisartist.rst diff --git a/doc/api/toolkits/index.rst b/doc/api/toolkits/index.rst deleted file mode 100644 index 59c01ab21a69..000000000000 --- a/doc/api/toolkits/index.rst +++ /dev/null @@ -1,46 +0,0 @@ -.. _toolkits-index: - -.. _toolkits: - -######## -Toolkits -######## - -Toolkits are collections of application-specific functions that extend -Matplotlib. - -.. _toolkit_mplot3d: - -mplot3d -======= - -:mod:`mpl_toolkits.mplot3d` provides some basic 3D -plotting (scatter, surf, line, mesh) tools. Not the fastest or most feature -complete 3D library out there, but it ships with Matplotlib and thus may be a -lighter weight solution for some use cases. Check out the -:doc:`mplot3d tutorial ` for more -information. - -.. figure:: ../../gallery/mplot3d/images/sphx_glr_contourf3d_2_001.png - :target: ../../gallery/mplot3d/contourf3d_2.html - :align: center - :scale: 50 - -.. toctree:: - :maxdepth: 2 - - mplot3d/index.rst - mplot3d/faq.rst - -Links ------ -* mpl3d API: :ref:`toolkit_mplot3d-api` - -.. include:: axes_grid1.rst - :start-line: 1 - -.. include:: axisartist.rst - :start-line: 1 - -.. include:: axes_grid.rst - :start-line: 1 diff --git a/doc/api/toolkits/mplot3d.rst b/doc/api/toolkits/mplot3d.rst index 97d3bf13246f..5b3cb52571bb 100644 --- a/doc/api/toolkits/mplot3d.rst +++ b/doc/api/toolkits/mplot3d.rst @@ -1,8 +1,32 @@ -.. _toolkit_mplot3d-api: +.. _toolkit_mplot3d-index: +.. currentmodule:: mpl_toolkits.mplot3d + +************************ +``mpl_toolkits.mplot3d`` +************************ + +The mplot3d toolkit adds simple 3D plotting capabilities (scatter, surface, +line, mesh, etc.) to Matplotlib by supplying an Axes object that can create +a 2D projection of a 3D scene. The resulting graph will have the same look +and feel as regular 2D plots. Not the fastest or most feature complete 3D +library out there, but it ships with Matplotlib and thus may be a lighter +weight solution for some use cases. + +See the :doc:`mplot3d tutorial ` for +more information. + +.. image:: /_static/demo_mplot3d.png + :align: center + +The interactive backends also provide the ability to rotate and zoom the 3D +scene. One can rotate the 3D scene by simply clicking-and-dragging the scene. +Zooming is done by right-clicking the scene and dragging the mouse up and down +(unlike 2D plots, the toolbar zoom button is not used). + +.. toctree:: + :maxdepth: 2 -*********** -mplot3d API -*********** + mplot3d/faq.rst .. note:: `.pyplot` cannot be used to add content to 3D plots, because its function diff --git a/doc/api/toolkits/mplot3d/index.rst b/doc/api/toolkits/mplot3d/index.rst deleted file mode 100644 index 8b153c06903f..000000000000 --- a/doc/api/toolkits/mplot3d/index.rst +++ /dev/null @@ -1,28 +0,0 @@ -.. _toolkit_mplot3d-index: -.. currentmodule:: mpl_toolkits.mplot3d - -******* -mplot3d -******* - -Matplotlib mplot3d toolkit -========================== -The mplot3d toolkit adds simple 3D plotting capabilities to matplotlib by -supplying an axes object that can create a 2D projection of a 3D scene. -The resulting graph will have the same look and feel as regular 2D plots. - -See the :doc:`mplot3d tutorial ` for -more information on how to use this toolkit. - -.. image:: /_static/demo_mplot3d.png - -The interactive backends also provide the ability to rotate and zoom -the 3D scene. One can rotate the 3D scene by simply clicking-and-dragging -the scene. Zooming is done by right-clicking the scene and dragging the -mouse up and down. Note that one does not use the zoom button like one -would use for regular 2D plots. - -.. toctree:: - :maxdepth: 2 - - faq.rst diff --git a/doc/devel/testing.rst b/doc/devel/testing.rst index aa189948003c..65898b95ee0c 100644 --- a/doc/devel/testing.rst +++ b/doc/devel/testing.rst @@ -244,7 +244,7 @@ The correct target folder can be found using:: python -c "import matplotlib.tests; print(matplotlib.tests.__file__.rsplit('/', 1)[0])" An analogous copying of :file:`lib/mpl_toolkits/tests/baseline_images` -is necessary for testing the :ref:`toolkits`. +is necessary for testing ``mpl_toolkits``. Run the tests ^^^^^^^^^^^^^ diff --git a/doc/index.rst b/doc/index.rst index 0350a1047a9f..3b18def1db3c 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -31,8 +31,6 @@ Reference - :doc:`Axes API ` for *most* plotting methods - :doc:`Figure API ` for figure-level methods -- :doc:`Extra Toolkits ` - How-tos ======= From 481da9b7715912d417641ab68b26eacb0509c859 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Tue, 7 Sep 2021 17:11:12 +0100 Subject: [PATCH 35/61] Separate tick and spine examples --- .flake8 | 2 +- doc/users/prev_whats_new/whats_new_0.99.rst | 2 +- examples/axisartist/demo_axisline_style.py | 4 ++-- examples/axisartist/demo_parasite_axes.py | 2 +- examples/axisartist/demo_parasite_axes2.py | 2 +- examples/axisartist/simple_axisartist1.py | 6 +++--- examples/spines/README.txt | 4 ++++ .../centered_spines_with_arrows.py | 0 .../multiple_yaxis_with_spines.py | 0 .../spine_placement_demo.py | 2 +- examples/{ticks_and_spines => spines}/spines.py | 0 .../{ticks_and_spines => spines}/spines_bounds.py | 0 .../{ticks_and_spines => spines}/spines_dropped.py | 0 examples/text_labels_and_annotations/date.py | 9 ++++----- examples/ticks/README.txt | 4 ++++ examples/{ticks_and_spines => ticks}/auto_ticks.py | 0 .../centered_ticklabels.py | 0 .../colorbar_tick_labelling_demo.py | 0 .../{ticks_and_spines => ticks}/custom_ticker1.py | 0 .../date_concise_formatter.py | 0 .../date_demo_convert.py | 0 .../{ticks_and_spines => ticks}/date_demo_rrule.py | 0 .../date_index_formatter2.py | 0 .../date_precision_and_epochs.py | 0 .../major_minor_demo.py | 0 .../{ticks_and_spines => ticks}/scalarformatter.py | 0 .../{ticks_and_spines => ticks}/tick-formatters.py | 0 .../{ticks_and_spines => ticks}/tick-locators.py | 0 .../tick_label_right.py | 0 .../tick_labels_from_values.py | 0 .../{ticks_and_spines => ticks}/tick_xlabel_top.py | 0 .../ticklabels_rotation.py | 0 examples/ticks_and_spines/README.txt | 4 ---- lib/matplotlib/dates.py | 14 +++++++------- lib/matplotlib/ticker.py | 6 +++--- 35 files changed, 32 insertions(+), 29 deletions(-) create mode 100644 examples/spines/README.txt rename examples/{ticks_and_spines => spines}/centered_spines_with_arrows.py (100%) rename examples/{ticks_and_spines => spines}/multiple_yaxis_with_spines.py (100%) rename examples/{ticks_and_spines => spines}/spine_placement_demo.py (97%) rename examples/{ticks_and_spines => spines}/spines.py (100%) rename examples/{ticks_and_spines => spines}/spines_bounds.py (100%) rename examples/{ticks_and_spines => spines}/spines_dropped.py (100%) create mode 100644 examples/ticks/README.txt rename examples/{ticks_and_spines => ticks}/auto_ticks.py (100%) rename examples/{ticks_and_spines => ticks}/centered_ticklabels.py (100%) rename examples/{ticks_and_spines => ticks}/colorbar_tick_labelling_demo.py (100%) rename examples/{ticks_and_spines => ticks}/custom_ticker1.py (100%) rename examples/{ticks_and_spines => ticks}/date_concise_formatter.py (100%) rename examples/{ticks_and_spines => ticks}/date_demo_convert.py (100%) rename examples/{ticks_and_spines => ticks}/date_demo_rrule.py (100%) rename examples/{ticks_and_spines => ticks}/date_index_formatter2.py (100%) rename examples/{ticks_and_spines => ticks}/date_precision_and_epochs.py (100%) rename examples/{ticks_and_spines => ticks}/major_minor_demo.py (100%) rename examples/{ticks_and_spines => ticks}/scalarformatter.py (100%) rename examples/{ticks_and_spines => ticks}/tick-formatters.py (100%) rename examples/{ticks_and_spines => ticks}/tick-locators.py (100%) rename examples/{ticks_and_spines => ticks}/tick_label_right.py (100%) rename examples/{ticks_and_spines => ticks}/tick_labels_from_values.py (100%) rename examples/{ticks_and_spines => ticks}/tick_xlabel_top.py (100%) rename examples/{ticks_and_spines => ticks}/ticklabels_rotation.py (100%) delete mode 100644 examples/ticks_and_spines/README.txt diff --git a/.flake8 b/.flake8 index 82b495f4cfe3..7094b6c49b5f 100644 --- a/.flake8 +++ b/.flake8 @@ -120,7 +120,7 @@ per-file-ignores = examples/style_sheets/plot_solarizedlight2.py: E501 examples/subplots_axes_and_figures/demo_constrained_layout.py: E402 examples/text_labels_and_annotations/custom_legends.py: E402 - examples/ticks_and_spines/date_concise_formatter.py: E402 + examples/ticks/date_concise_formatter.py: E402 examples/user_interfaces/embedding_in_gtk3_panzoom_sgskip.py: E402 examples/user_interfaces/embedding_in_gtk3_sgskip.py: E402 examples/user_interfaces/embedding_in_gtk4_panzoom_sgskip.py: E402 diff --git a/doc/users/prev_whats_new/whats_new_0.99.rst b/doc/users/prev_whats_new/whats_new_0.99.rst index 94abde7fe4da..c2d761a25031 100644 --- a/doc/users/prev_whats_new/whats_new_0.99.rst +++ b/doc/users/prev_whats_new/whats_new_0.99.rst @@ -112,7 +112,7 @@ that denote the data limits -- in various arbitrary locations. No longer are your axis lines constrained to be a simple rectangle around the figure -- you can turn on or off left, bottom, right and top, as well as "detach" the spine to offset it away from the data. See -:doc:`/gallery/ticks_and_spines/spine_placement_demo` and +:doc:`/gallery/spines/spine_placement_demo` and :class:`matplotlib.spines.Spine`. .. plot:: diff --git a/examples/axisartist/demo_axisline_style.py b/examples/axisartist/demo_axisline_style.py index 1427a90952a1..c7270941dadf 100644 --- a/examples/axisartist/demo_axisline_style.py +++ b/examples/axisartist/demo_axisline_style.py @@ -7,8 +7,8 @@ Note: The `mpl_toolkits.axisartist` axes classes may be confusing for new users. If the only aim is to obtain arrow heads at the ends of the axes, -rather check out the -:doc:`/gallery/ticks_and_spines/centered_spines_with_arrows` example. +rather check out the :doc:`/gallery/spines/centered_spines_with_arrows` +example. """ from mpl_toolkits.axisartist.axislines import AxesZero diff --git a/examples/axisartist/demo_parasite_axes.py b/examples/axisartist/demo_parasite_axes.py index ef7d5ca5268d..0b7858f645f3 100644 --- a/examples/axisartist/demo_parasite_axes.py +++ b/examples/axisartist/demo_parasite_axes.py @@ -10,7 +10,7 @@ `mpl_toolkits.axes_grid1.parasite_axes.ParasiteAxes`. An alternative approach using standard Matplotlib subplots is shown in the -:doc:`/gallery/ticks_and_spines/multiple_yaxis_with_spines` example. +:doc:`/gallery/spines/multiple_yaxis_with_spines` example. An alternative approach using :mod:`mpl_toolkits.axes_grid1` and :mod:`mpl_toolkits.axisartist` is found in the diff --git a/examples/axisartist/demo_parasite_axes2.py b/examples/axisartist/demo_parasite_axes2.py index 3c23e37b7ae7..651cdd032ae5 100644 --- a/examples/axisartist/demo_parasite_axes2.py +++ b/examples/axisartist/demo_parasite_axes2.py @@ -19,7 +19,7 @@ `~.mpl_toolkits.axes_grid1.parasite_axes.ParasiteAxes` is the :doc:`/gallery/axisartist/demo_parasite_axes` example. An alternative approach using the usual Matplotlib subplots is shown in -the :doc:`/gallery/ticks_and_spines/multiple_yaxis_with_spines` example. +the :doc:`/gallery/spines/multiple_yaxis_with_spines` example. """ from mpl_toolkits.axes_grid1 import host_subplot diff --git a/examples/axisartist/simple_axisartist1.py b/examples/axisartist/simple_axisartist1.py index dacbfd0721f8..95d710cbe0b1 100644 --- a/examples/axisartist/simple_axisartist1.py +++ b/examples/axisartist/simple_axisartist1.py @@ -6,9 +6,9 @@ This example showcases the use of :mod:`.axisartist` to draw spines at custom positions (here, at ``y = 0``). -Note, however, that it is simpler to achieve this effect -using standard `.Spine` methods, as demonstrated in -:doc:`/gallery/ticks_and_spines/centered_spines_with_arrows`. +Note, however, that it is simpler to achieve this effect using standard +`.Spine` methods, as demonstrated in +:doc:`/gallery/spines/centered_spines_with_arrows`. .. redirect-from:: /gallery/axisartist/simple_axisline2 """ diff --git a/examples/spines/README.txt b/examples/spines/README.txt new file mode 100644 index 000000000000..40bc3952eacd --- /dev/null +++ b/examples/spines/README.txt @@ -0,0 +1,4 @@ +.. _spines_examples: + +Spines +====== diff --git a/examples/ticks_and_spines/centered_spines_with_arrows.py b/examples/spines/centered_spines_with_arrows.py similarity index 100% rename from examples/ticks_and_spines/centered_spines_with_arrows.py rename to examples/spines/centered_spines_with_arrows.py diff --git a/examples/ticks_and_spines/multiple_yaxis_with_spines.py b/examples/spines/multiple_yaxis_with_spines.py similarity index 100% rename from examples/ticks_and_spines/multiple_yaxis_with_spines.py rename to examples/spines/multiple_yaxis_with_spines.py diff --git a/examples/ticks_and_spines/spine_placement_demo.py b/examples/spines/spine_placement_demo.py similarity index 97% rename from examples/ticks_and_spines/spine_placement_demo.py rename to examples/spines/spine_placement_demo.py index e567d8160d9c..d433236657f9 100644 --- a/examples/ticks_and_spines/spine_placement_demo.py +++ b/examples/spines/spine_placement_demo.py @@ -6,7 +6,7 @@ Adjusting the location and appearance of axis spines. Note: If you want to obtain arrow heads at the ends of the axes, also check -out the :doc:`/gallery/ticks_and_spines/centered_spines_with_arrows` example. +out the :doc:`/gallery/spines/centered_spines_with_arrows` example. """ import numpy as np import matplotlib.pyplot as plt diff --git a/examples/ticks_and_spines/spines.py b/examples/spines/spines.py similarity index 100% rename from examples/ticks_and_spines/spines.py rename to examples/spines/spines.py diff --git a/examples/ticks_and_spines/spines_bounds.py b/examples/spines/spines_bounds.py similarity index 100% rename from examples/ticks_and_spines/spines_bounds.py rename to examples/spines/spines_bounds.py diff --git a/examples/ticks_and_spines/spines_dropped.py b/examples/spines/spines_dropped.py similarity index 100% rename from examples/ticks_and_spines/spines_dropped.py rename to examples/spines/spines_dropped.py diff --git a/examples/text_labels_and_annotations/date.py b/examples/text_labels_and_annotations/date.py index c37a5fb61e31..f1701ad9bc3b 100644 --- a/examples/text_labels_and_annotations/date.py +++ b/examples/text_labels_and_annotations/date.py @@ -16,11 +16,10 @@ An alternative formatter is the `~.dates.ConciseDateFormatter`, used in the second ``Axes`` below (see -:doc:`/gallery/ticks_and_spines/date_concise_formatter`), which often -removes the need to rotate the tick labels. The last ``Axes`` -formats the dates manually, using `~.dates.DateFormatter` to -format the dates using the format strings documented at -`datetime.date.strftime`. +:doc:`/gallery/ticks/date_concise_formatter`), which often removes the need to +rotate the tick labels. The last ``Axes`` formats the dates manually, using +`~.dates.DateFormatter` to format the dates using the format strings documented +at `datetime.date.strftime`. """ import matplotlib.pyplot as plt diff --git a/examples/ticks/README.txt b/examples/ticks/README.txt new file mode 100644 index 000000000000..82441a0d9ee7 --- /dev/null +++ b/examples/ticks/README.txt @@ -0,0 +1,4 @@ +.. _ticks_examples: + +Ticks +===== diff --git a/examples/ticks_and_spines/auto_ticks.py b/examples/ticks/auto_ticks.py similarity index 100% rename from examples/ticks_and_spines/auto_ticks.py rename to examples/ticks/auto_ticks.py diff --git a/examples/ticks_and_spines/centered_ticklabels.py b/examples/ticks/centered_ticklabels.py similarity index 100% rename from examples/ticks_and_spines/centered_ticklabels.py rename to examples/ticks/centered_ticklabels.py diff --git a/examples/ticks_and_spines/colorbar_tick_labelling_demo.py b/examples/ticks/colorbar_tick_labelling_demo.py similarity index 100% rename from examples/ticks_and_spines/colorbar_tick_labelling_demo.py rename to examples/ticks/colorbar_tick_labelling_demo.py diff --git a/examples/ticks_and_spines/custom_ticker1.py b/examples/ticks/custom_ticker1.py similarity index 100% rename from examples/ticks_and_spines/custom_ticker1.py rename to examples/ticks/custom_ticker1.py diff --git a/examples/ticks_and_spines/date_concise_formatter.py b/examples/ticks/date_concise_formatter.py similarity index 100% rename from examples/ticks_and_spines/date_concise_formatter.py rename to examples/ticks/date_concise_formatter.py diff --git a/examples/ticks_and_spines/date_demo_convert.py b/examples/ticks/date_demo_convert.py similarity index 100% rename from examples/ticks_and_spines/date_demo_convert.py rename to examples/ticks/date_demo_convert.py diff --git a/examples/ticks_and_spines/date_demo_rrule.py b/examples/ticks/date_demo_rrule.py similarity index 100% rename from examples/ticks_and_spines/date_demo_rrule.py rename to examples/ticks/date_demo_rrule.py diff --git a/examples/ticks_and_spines/date_index_formatter2.py b/examples/ticks/date_index_formatter2.py similarity index 100% rename from examples/ticks_and_spines/date_index_formatter2.py rename to examples/ticks/date_index_formatter2.py diff --git a/examples/ticks_and_spines/date_precision_and_epochs.py b/examples/ticks/date_precision_and_epochs.py similarity index 100% rename from examples/ticks_and_spines/date_precision_and_epochs.py rename to examples/ticks/date_precision_and_epochs.py diff --git a/examples/ticks_and_spines/major_minor_demo.py b/examples/ticks/major_minor_demo.py similarity index 100% rename from examples/ticks_and_spines/major_minor_demo.py rename to examples/ticks/major_minor_demo.py diff --git a/examples/ticks_and_spines/scalarformatter.py b/examples/ticks/scalarformatter.py similarity index 100% rename from examples/ticks_and_spines/scalarformatter.py rename to examples/ticks/scalarformatter.py diff --git a/examples/ticks_and_spines/tick-formatters.py b/examples/ticks/tick-formatters.py similarity index 100% rename from examples/ticks_and_spines/tick-formatters.py rename to examples/ticks/tick-formatters.py diff --git a/examples/ticks_and_spines/tick-locators.py b/examples/ticks/tick-locators.py similarity index 100% rename from examples/ticks_and_spines/tick-locators.py rename to examples/ticks/tick-locators.py diff --git a/examples/ticks_and_spines/tick_label_right.py b/examples/ticks/tick_label_right.py similarity index 100% rename from examples/ticks_and_spines/tick_label_right.py rename to examples/ticks/tick_label_right.py diff --git a/examples/ticks_and_spines/tick_labels_from_values.py b/examples/ticks/tick_labels_from_values.py similarity index 100% rename from examples/ticks_and_spines/tick_labels_from_values.py rename to examples/ticks/tick_labels_from_values.py diff --git a/examples/ticks_and_spines/tick_xlabel_top.py b/examples/ticks/tick_xlabel_top.py similarity index 100% rename from examples/ticks_and_spines/tick_xlabel_top.py rename to examples/ticks/tick_xlabel_top.py diff --git a/examples/ticks_and_spines/ticklabels_rotation.py b/examples/ticks/ticklabels_rotation.py similarity index 100% rename from examples/ticks_and_spines/ticklabels_rotation.py rename to examples/ticks/ticklabels_rotation.py diff --git a/examples/ticks_and_spines/README.txt b/examples/ticks_and_spines/README.txt deleted file mode 100644 index e7869c5a08d1..000000000000 --- a/examples/ticks_and_spines/README.txt +++ /dev/null @@ -1,4 +0,0 @@ -.. _ticks_and_spines_examples: - -Ticks and spines -================ diff --git a/lib/matplotlib/dates.py b/lib/matplotlib/dates.py index 67aff6270815..7ec320ffab33 100644 --- a/lib/matplotlib/dates.py +++ b/lib/matplotlib/dates.py @@ -21,8 +21,8 @@ .. seealso:: - :doc:`/gallery/text_labels_and_annotations/date` - - :doc:`/gallery/ticks_and_spines/date_concise_formatter` - - :doc:`/gallery/ticks_and_spines/date_demo_convert` + - :doc:`/gallery/ticks/date_concise_formatter` + - :doc:`/gallery/ticks/date_demo_convert` .. _date-format: @@ -38,7 +38,7 @@ 20 microseconds for the rest of the allowable range of dates (year 0001 to 9999). The epoch can be changed at import time via `.dates.set_epoch` or :rc:`dates.epoch` to other dates if necessary; see -:doc:`/gallery/ticks_and_spines/date_precision_and_epochs` for a discussion. +:doc:`/gallery/ticks/date_precision_and_epochs` for a discussion. .. note:: @@ -144,7 +144,7 @@ * `RRuleLocator`: Locate using a `matplotlib.dates.rrulewrapper`. `.rrulewrapper` is a simple wrapper around dateutil_'s `dateutil.rrule` which allow almost arbitrary date tick specifications. See :doc:`rrule example - `. + `. * `AutoDateLocator`: On autoscale, this class picks the best `DateLocator` (e.g., `RRuleLocator`) to set the view limits and the tick locations. If @@ -271,7 +271,7 @@ def set_epoch(epoch): `~.dates.set_epoch` must be called before any dates are converted (i.e. near the import section) or a RuntimeError will be raised. - See also :doc:`/gallery/ticks_and_spines/date_precision_and_epochs`. + See also :doc:`/gallery/ticks/date_precision_and_epochs`. Parameters ---------- @@ -683,7 +683,7 @@ class ConciseDateFormatter(ticker.Formatter): Examples -------- - See :doc:`/gallery/ticks_and_spines/date_concise_formatter` + See :doc:`/gallery/ticks/date_concise_formatter` .. plot:: @@ -1659,7 +1659,7 @@ class MicrosecondLocator(DateLocator): If you really must use datetime.datetime() or similar and still need microsecond precision, change the time origin via `.dates.set_epoch` to something closer to the dates being plotted. - See :doc:`/gallery/ticks_and_spines/date_precision_and_epochs`. + See :doc:`/gallery/ticks/date_precision_and_epochs`. """ def __init__(self, interval=1, tz=None): diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index fc30977d9b39..7a0380453452 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -118,9 +118,9 @@ the input ``str``. For function input, a `.FuncFormatter` with the input function will be generated and used. -See :doc:`/gallery/ticks_and_spines/major_minor_demo` for an -example of setting major and minor ticks. See the :mod:`matplotlib.dates` -module for more information and examples of using date locators and formatters. +See :doc:`/gallery/ticks/major_minor_demo` for an example of setting major +and minor ticks. See the :mod:`matplotlib.dates` module for more information +and examples of using date locators and formatters. """ import itertools From 79a29b6bd99075cced8957059c2a24c30928bd5e Mon Sep 17 00:00:00 2001 From: David Stansby Date: Tue, 7 Sep 2021 17:41:38 +0100 Subject: [PATCH 36/61] Use numpydoc for GridSpecFromSubplotSpec.__init__ --- lib/matplotlib/gridspec.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/gridspec.py b/lib/matplotlib/gridspec.py index 752048d64a59..74b35d41797d 100644 --- a/lib/matplotlib/gridspec.py +++ b/lib/matplotlib/gridspec.py @@ -490,11 +490,19 @@ def __init__(self, nrows, ncols, wspace=None, hspace=None, height_ratios=None, width_ratios=None): """ - The number of rows and number of columns of the grid need to - be set. An instance of SubplotSpec is also needed to be set - from which the layout parameters will be inherited. The wspace - and hspace of the layout can be optionally specified or the - default values (from the figure or rcParams) will be used. + Parameters + ---------- + nrows, ncols : int + Number of rows and number of columns of the grid. + subplot_spec : SubplotSpec + Spec from which the layout parameters are inherited. + wspace, hspace : float, optional + See `GridSpec` for more details. If not specified default values + (from the figure or rcParams) are used. + height_ratios : array-like of length *nrows*, optional + See `GridSpecBase` for details. + width_ratios : array-like of length *ncols*, optional + See `GridSpecBase` for details. """ self._wspace = wspace self._hspace = hspace From 308d2ac9f9d7be07b37b01fd9206dffc1851bec0 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Fri, 7 Sep 2018 16:16:45 +0200 Subject: [PATCH 37/61] Avoid TransformedBbox where unneeded. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit transform_bbox is much faster when one does not need a TransformedBbox, i.e. something that keeps track of later changes in the transform: ``` In [1]: from pylab import * ...: from matplotlib.transforms import * ...: tr = plt.gca().transAxes ...: bb = Bbox([[0, 0], [1, 1]]) ...: %timeit TransformedBbox(bb, tr).x0 ...: %timeit tr.transform_bbox(bb).x0 ...: %timeit TransformedBbox(bb, tr).width ...: %timeit tr.transform_bbox(bb).width 11.8 µs ± 145 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each) 2.72 µs ± 31.5 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each) 11.9 µs ± 183 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each) 2.83 µs ± 16.6 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each) ``` --- lib/matplotlib/tight_bbox.py | 4 +--- lib/matplotlib/tight_layout.py | 5 ++--- lib/mpl_toolkits/axes_grid1/inset_locator.py | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/tight_bbox.py b/lib/matplotlib/tight_bbox.py index 5904ebc1fa1c..bd58833baf88 100644 --- a/lib/matplotlib/tight_bbox.py +++ b/lib/matplotlib/tight_bbox.py @@ -57,11 +57,9 @@ def restore_bbox(): tr = Affine2D().scale(fixed_dpi) dpi_scale = fixed_dpi / fig.dpi - _bbox = TransformedBbox(bbox_inches, tr) - fig.bbox_inches = Bbox.from_bounds(0, 0, bbox_inches.width, bbox_inches.height) - x0, y0 = _bbox.x0, _bbox.y0 + x0, y0 = tr.transform(bbox_inches.p0) w1, h1 = fig.bbox.width * dpi_scale, fig.bbox.height * dpi_scale fig.transFigure._boxout = Bbox.from_bounds(-x0, -y0, w1, h1) fig.transFigure.invalidate() diff --git a/lib/matplotlib/tight_layout.py b/lib/matplotlib/tight_layout.py index 809b970915a9..66283c7d7348 100644 --- a/lib/matplotlib/tight_layout.py +++ b/lib/matplotlib/tight_layout.py @@ -13,7 +13,7 @@ from matplotlib import _api, rcParams from matplotlib.font_manager import FontProperties -from matplotlib.transforms import TransformedBbox, Bbox +from matplotlib.transforms import Bbox def _auto_adjust_subplotpars( @@ -84,8 +84,7 @@ def _auto_adjust_subplotpars( bb += [ax.get_tightbbox(renderer)] tight_bbox_raw = Bbox.union(bb) - tight_bbox = TransformedBbox(tight_bbox_raw, - fig.transFigure.inverted()) + tight_bbox = fig.transFigure.inverted().transform_bbox(tight_bbox_raw) hspaces[rowspan, colspan.start] += ax_bbox.xmin - tight_bbox.xmin # l hspaces[rowspan, colspan.stop] += tight_bbox.xmax - ax_bbox.xmax # r diff --git a/lib/mpl_toolkits/axes_grid1/inset_locator.py b/lib/mpl_toolkits/axes_grid1/inset_locator.py index 5193cc540c31..fcdb32851b8c 100644 --- a/lib/mpl_toolkits/axes_grid1/inset_locator.py +++ b/lib/mpl_toolkits/axes_grid1/inset_locator.py @@ -126,7 +126,7 @@ def __init__(self, parent_axes, zoom, loc, bbox_transform=bbox_transform) def get_extent(self, renderer): - bb = TransformedBbox(self.axes.viewLim, self.parent_axes.transData) + bb = self.parent_axes.transData.transform_bbox(self.axes.viewLim) fontsize = renderer.points_to_pixels(self.prop.get_size_in_points()) pad = self.pad * fontsize return (abs(bb.width * self.zoom) + 2 * pad, From 961a92cc782c4f98d2afd2e6ab92398a4d4fa4cf Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 8 Sep 2021 11:12:50 +0200 Subject: [PATCH 38/61] Reword custom_ticker1 example. I intentionally left some "historical" flavor to the text. Remove the backreference to FuncFormatter, which has become a semi-internal API now that users should never have to directly instantiate them. Also remove the backreference to `subplots()`: although the example does use it, it's really not about showcasing it. --- examples/ticks/custom_ticker1.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/examples/ticks/custom_ticker1.py b/examples/ticks/custom_ticker1.py index ee088fa4d5af..f3e0ec4ef3bf 100644 --- a/examples/ticks/custom_ticker1.py +++ b/examples/ticks/custom_ticker1.py @@ -1,28 +1,27 @@ """ -============== -Custom Ticker1 -============== +============= +Custom Ticker +============= -The new ticker code was designed to explicitly support user customized -ticking. The documentation of :mod:`matplotlib.ticker` details this -process. That code defines a lot of preset tickers but was primarily -designed to be user extensible. +The :mod:`matplotlib.ticker` module defines many preset tickers, but was +primarily designed for extensibility, i.e., to support user customized ticking. -In this example a user defined function is used to format the ticks in +In this example, a user defined function is used to format the ticks in millions of dollars on the y axis. """ -import matplotlib.pyplot as plt -money = [1.5e5, 2.5e6, 5.5e6, 2.0e7] +import matplotlib.pyplot as plt def millions(x, pos): """The two arguments are the value and tick position.""" return '${:1.1f}M'.format(x*1e-6) + fig, ax = plt.subplots() -# Use automatic FuncFormatter creation +# set_major_formatter internally creates a FuncFormatter from the callable. ax.yaxis.set_major_formatter(millions) +money = [1.5e5, 2.5e6, 5.5e6, 2.0e7] ax.bar(['Bill', 'Fred', 'Mary', 'Sue'], money) plt.show() @@ -33,6 +32,4 @@ def millions(x, pos): # The use of the following functions, methods, classes and modules is shown # in this example: # -# - `matplotlib.pyplot.subplots` # - `matplotlib.axis.Axis.set_major_formatter` -# - `matplotlib.ticker.FuncFormatter` From 2d2cbe0bb6fcc1a2dc1ebd56bf3643bab9bc4fc4 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 14 Aug 2021 21:09:48 -0400 Subject: [PATCH 39/61] DOC: re-arrange user's guide and index --- doc/contents.rst | 2 -- doc/index.rst | 30 +++++++++++++++++++++++++----- doc/users/index.rst | 37 +++++++++++++++++++++++++++++++++---- 3 files changed, 58 insertions(+), 11 deletions(-) diff --git a/doc/contents.rst b/doc/contents.rst index 37fd17172ce2..f8bb820b8aa7 100644 --- a/doc/contents.rst +++ b/doc/contents.rst @@ -20,8 +20,6 @@ Contents tutorials/index.rst api/index.rst users/index.rst - devel/index.rst - Third-party packages .. only:: html diff --git a/doc/index.rst b/doc/index.rst index 3b18def1db3c..c4b79b4c4925 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -4,7 +4,7 @@ .. module:: matplotlib -Matplotlib documentation +Matplotlib documentation ------------------------ Release: |release| @@ -14,19 +14,20 @@ and interactive visualizations in Python. Learn ===== - + - :doc:`Quick-start Guide ` - Basic :doc:`Plot Types ` and :doc:`Example Gallery ` - `Introductory Tutorials <../tutorials/index.html#introductory>`_ - :doc:`External Learning Resources ` - + + Reference ========= - :doc:`API Reference ` - - :doc:`pyplot API `: top-level interface to create - Figures (`.pyplot.figure`) and Subplots (`.pyplot.subplots`, + - :doc:`pyplot API `: top-level interface to create + Figures (`.pyplot.figure`) and Subplots (`.pyplot.subplots`, `.pyplot.subplot_mosaic`) - :doc:`Axes API ` for *most* plotting methods - :doc:`Figure API ` for figure-level methods @@ -41,4 +42,23 @@ How-tos Understand how Matplotlib works =============================== +- The :ref:`users-guide-explain` section of the :doc:`Users guide ` - Many of the :doc:`Tutorials ` have explanatory material + + +Third-party Packages +-------------------- + +There are many `Third-party packages +`_ built on top of and extending +Matplotlib. + + +Contributing +------------ + + +.. toctree:: + :maxdepth: 2 + + devel/index.rst diff --git a/doc/users/index.rst b/doc/users/index.rst index c4ec864df787..734607f25721 100644 --- a/doc/users/index.rst +++ b/doc/users/index.rst @@ -9,15 +9,44 @@ User's guide :Release: |version| :Date: |today| +Release Notes +------------- + .. toctree:: - :maxdepth: 2 + :maxdepth: 3 + + release_notes.rst + +.. _users-guide-explain: + +Explanations +------------ + +.. toctree:: + :maxdepth: 1 interactive.rst fonts.rst - release_notes.rst + + +FAQ and External Resources +-------------------------- + +.. toctree:: + :maxdepth: 2 + + ../faq/index.rst + ../resources/index.rst + + + +Back Matter +----------- + +.. toctree:: + :maxdepth: 1 + license.rst ../citing.rst - ../resources/index.rst - ../faq/index.rst credits.rst history.rst From 8d8674cda9dafcd8cc66cb00ebcb6aa68d570ca0 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 20 Aug 2021 13:35:24 -0400 Subject: [PATCH 40/61] DOC: add direct Matplotlib sponsorship to sponsor button --- .github/FUNDING.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 2bef7ab95a56..5c9afed3c02b 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,3 +1,3 @@ # These are supported funding model platforms -github: [numfocus] +github: [matplotlib, numfocus] custom: https://numfocus.org/donate-to-matplotlib From ccf62700a39c5742c1d48a85efc5a91dffff4b2c Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 20 Aug 2021 19:31:06 -0400 Subject: [PATCH 41/61] DOC: split the user-guide into parts This make the navigation on the left behave better --- doc/users/backmatter.rst | 11 +++++++++++ doc/users/explain.rst | 10 ++++++++++ doc/users/index.rst | 40 +++------------------------------------- doc/users/resource.rst | 8 ++++++++ 4 files changed, 32 insertions(+), 37 deletions(-) create mode 100644 doc/users/backmatter.rst create mode 100644 doc/users/explain.rst create mode 100644 doc/users/resource.rst diff --git a/doc/users/backmatter.rst b/doc/users/backmatter.rst new file mode 100644 index 000000000000..c95908c9314c --- /dev/null +++ b/doc/users/backmatter.rst @@ -0,0 +1,11 @@ + +Back Matter +----------- + +.. toctree:: + :maxdepth: 1 + + license.rst + ../citing.rst + credits.rst + history.rst diff --git a/doc/users/explain.rst b/doc/users/explain.rst new file mode 100644 index 000000000000..d9d7a8474dbb --- /dev/null +++ b/doc/users/explain.rst @@ -0,0 +1,10 @@ +.. _users-guide-explain: + +Explanations +------------ + +.. toctree:: + :maxdepth: 1 + + interactive.rst + fonts.rst diff --git a/doc/users/index.rst b/doc/users/index.rst index 734607f25721..cdc016921430 100644 --- a/doc/users/index.rst +++ b/doc/users/index.rst @@ -9,44 +9,10 @@ User's guide :Release: |version| :Date: |today| -Release Notes -------------- - .. toctree:: :maxdepth: 3 release_notes.rst - -.. _users-guide-explain: - -Explanations ------------- - -.. toctree:: - :maxdepth: 1 - - interactive.rst - fonts.rst - - -FAQ and External Resources --------------------------- - -.. toctree:: - :maxdepth: 2 - - ../faq/index.rst - ../resources/index.rst - - - -Back Matter ------------ - -.. toctree:: - :maxdepth: 1 - - license.rst - ../citing.rst - credits.rst - history.rst + explain.rst + resource.rst + backmatter.rst diff --git a/doc/users/resource.rst b/doc/users/resource.rst new file mode 100644 index 000000000000..9b68eb3547bc --- /dev/null +++ b/doc/users/resource.rst @@ -0,0 +1,8 @@ +FAQ and External Resources +-------------------------- + +.. toctree:: + :maxdepth: 2 + + ../faq/index.rst + ../resources/index.rst From 6339e69b5d20ed7b56b65d9f5ad1623d8c489ce5 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 21 Aug 2021 20:50:51 -0400 Subject: [PATCH 42/61] DOC: add search bar back to inner index --- doc/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/conf.py b/doc/conf.py index 00f7ed7c5ea2..f7c5bbee5171 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -341,6 +341,7 @@ def _check_dependencies(): # Custom sidebar templates, maps page names to templates. html_sidebars = { "index": [ + 'search-field.html', # 'sidebar_announcement.html', "sidebar_versions.html", "donate_sidebar.html", From 6598304635007c4fb1c161bf7561565c8502cdbf Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 21 Aug 2021 20:51:15 -0400 Subject: [PATCH 43/61] ENH: require sphinx-panels to build the docs --- doc/conf.py | 2 ++ requirements/doc/doc-requirements.txt | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index f7c5bbee5171..6eda40d8d3eb 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -71,12 +71,14 @@ 'sphinxext.skip_deprecated', 'sphinxext.redirect_from', 'sphinx_copybutton', + 'sphinx_panels', ] exclude_patterns = [ 'api/prev_api_changes/api_changes_*/*', ] +panels_add_bootstrap_css = False def _check_dependencies(): names = { diff --git a/requirements/doc/doc-requirements.txt b/requirements/doc/doc-requirements.txt index 2cfba0dbad07..2f69ed7d032f 100644 --- a/requirements/doc/doc-requirements.txt +++ b/requirements/doc/doc-requirements.txt @@ -14,10 +14,11 @@ ipywidgets numpydoc>=0.8 pydata-sphinx-theme>=0.5.0 sphinxcontrib-svg2pdfconverter>=1.1.0 -# sphinx-gallery>=0.7 -# b41e328 is PR 808 which adds the image_srcset directive. When this is +# sphinx-gallery>=0.7 +# b41e328 is PR 808 which adds the image_srcset directive. When this is # released with sphinx gallery, we can change to the last release w/o this feature: # sphinx-gallery>0.90 git+git://github.com/sphinx-gallery/sphinx-gallery@b41e328#egg=sphinx-gallery sphinx-copybutton +sphinx-panels scipy From 0e00c505ebb036b84d982181801264db60c3964e Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 21 Aug 2021 20:51:40 -0400 Subject: [PATCH 44/61] DOC: add release notes to top bar --- doc/contents.rst | 1 + doc/users/index.rst | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/contents.rst b/doc/contents.rst index f8bb820b8aa7..62c4d90828a5 100644 --- a/doc/contents.rst +++ b/doc/contents.rst @@ -20,6 +20,7 @@ Contents tutorials/index.rst api/index.rst users/index.rst + users/release_notes.rst .. only:: html diff --git a/doc/users/index.rst b/doc/users/index.rst index cdc016921430..840603ee24d9 100644 --- a/doc/users/index.rst +++ b/doc/users/index.rst @@ -12,7 +12,6 @@ User's guide .. toctree:: :maxdepth: 3 - release_notes.rst explain.rst resource.rst backmatter.rst From 6a91938d5765b6598a5464131ab35d7b8965142f Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 21 Aug 2021 20:53:20 -0400 Subject: [PATCH 45/61] DOC: move installation out of top bar and onto inner index --- doc/contents.rst | 1 - doc/index.rst | 55 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/doc/contents.rst b/doc/contents.rst index 62c4d90828a5..e9e27c81a9fc 100644 --- a/doc/contents.rst +++ b/doc/contents.rst @@ -14,7 +14,6 @@ Contents .. toctree:: :maxdepth: 2 - users/installing.rst plot_types/index.rst gallery/index.rst tutorials/index.rst diff --git a/doc/index.rst b/doc/index.rst index c4b79b4c4925..ee87ade82ac3 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -4,6 +4,13 @@ .. module:: matplotlib +.. toctree:: + :maxdepth: 2 + :hidden: + + users/installing.rst + + Matplotlib documentation ------------------------ @@ -12,6 +19,54 @@ Release: |release| Matplotlib is a comprehensive library for creating static, animated, and interactive visualizations in Python. +Installation +============ + +.. panels:: + :card: + install-card + :column: col-lg-6 col-md-6 col-sm-12 col-xs-12 p-3 + + Working with conda? + ^^^^^^^^^^^^^^^^^^^ + + Matplotlib is part of the `Anaconda `__ + distribution and can be installed with Anaconda or Miniconda: + + ++++++++++++++++++++++ + + .. code-block:: bash + + conda install matplotlib + + --- + + Prefer pip? + ^^^^^^^^^^^ + + Matplotlib can be installed via pip from `PyPI `__. + + ++++ + + .. code-block:: bash + + pip install matplotlib + + --- + :column: col-12 p-3 + + In-depth instructions? + ^^^^^^^^^^^^^^^^^^^^^^ + + Installing a specific version? Installing from source? Check the advanced + installation page. + + .. container:: custom-button + + :doc:`Installation Guide ` + + + + Learn ===== From a1c2d0d59e943280e5eb47625c27911b0ca76709 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 21 Aug 2021 21:06:00 -0400 Subject: [PATCH 46/61] DOC: try to make the left sidebar on inner index better --- doc/_static/mpl.css | 8 ++++++++ doc/_templates/cheatsheet_sidebar.html | 9 +++++++++ doc/_templates/donate_sidebar.html | 6 ++++-- doc/_templates/sidebar_versions.html | 9 --------- doc/conf.py | 1 + 5 files changed, 22 insertions(+), 11 deletions(-) create mode 100644 doc/_templates/cheatsheet_sidebar.html diff --git a/doc/_static/mpl.css b/doc/_static/mpl.css index 88c620d559e8..b5ac4b6ddd3a 100644 --- a/doc/_static/mpl.css +++ b/doc/_static/mpl.css @@ -212,3 +212,11 @@ table.property-table th, table.property-table td { padding: 4px 10px; } + +.donate-button { + margin: 1em 0; +} + +.sphinxsidebarwrapper { + margin: 0 1em; +} diff --git a/doc/_templates/cheatsheet_sidebar.html b/doc/_templates/cheatsheet_sidebar.html new file mode 100644 index 000000000000..615c2bc4cd04 --- /dev/null +++ b/doc/_templates/cheatsheet_sidebar.html @@ -0,0 +1,9 @@ + +
+

Matplotlib cheatsheets

+ + Matplotlib cheatsheets + +
diff --git a/doc/_templates/donate_sidebar.html b/doc/_templates/donate_sidebar.html index fc7310b70088..02d5ff6fc46c 100644 --- a/doc/_templates/donate_sidebar.html +++ b/doc/_templates/donate_sidebar.html @@ -1,6 +1,8 @@ - - -
-

Matplotlib cheatsheets

- - Matplotlib cheatsheets - -
diff --git a/doc/conf.py b/doc/conf.py index 6eda40d8d3eb..845c362be74e 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -346,6 +346,7 @@ def _check_dependencies(): 'search-field.html', # 'sidebar_announcement.html', "sidebar_versions.html", + "cheatsheet_sidebar.html", "donate_sidebar.html", ], # '**': ['localtoc.html', 'pagesource.html'] From 81002d031d48194f10b475e3d5079f912eb91e26 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 21 Aug 2021 21:06:18 -0400 Subject: [PATCH 47/61] DOC: put documentation types in a 2x2 grid --- doc/index.rst | 56 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/doc/index.rst b/doc/index.rst index ee87ade82ac3..ec55c673dd9a 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -65,40 +65,50 @@ Installation :doc:`Installation Guide ` +Learning Resources +================== -Learn -===== +.. panels:: + + Tutorials + ^^^^^^^^^ + + - :doc:`Quick-start Guide ` + - Basic :doc:`Plot Types ` + - `Introductory Tutorials <../tutorials/index.html#introductory>`_ + - :doc:`External Learning Resources ` + + --- + + How-tos + ^^^^^^^ + - :doc:`Example Gallery ` + - :doc:`Matplotlib FAQ ` -- :doc:`Quick-start Guide ` -- Basic :doc:`Plot Types ` and :doc:`Example Gallery ` -- `Introductory Tutorials <../tutorials/index.html#introductory>`_ -- :doc:`External Learning Resources ` + --- + Understand how Matplotlib works + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Reference -========= + - The :ref:`users-guide-explain` section of the :doc:`Users guide ` + - Many of the :doc:`Tutorials ` have explanatory material -- :doc:`API Reference ` + --- - - :doc:`pyplot API `: top-level interface to create - Figures (`.pyplot.figure`) and Subplots (`.pyplot.subplots`, - `.pyplot.subplot_mosaic`) - - :doc:`Axes API ` for *most* plotting methods - - :doc:`Figure API ` for figure-level methods + Reference + ^^^^^^^^^ -How-tos -======= + - :doc:`API Reference ` -- :doc:`Installation Guide ` -- :doc:`Contributing to Matplotlib ` -- :doc:`Matplotlib FAQ ` + - :doc:`pyplot API `: top-level interface to create + Figures (`.pyplot.figure`) and Subplots (`.pyplot.subplots`, + `.pyplot.subplot_mosaic`) + - :doc:`Axes API ` for *most* plotting methods + - :doc:`Figure API ` for figure-level methods -Understand how Matplotlib works -=============================== + - :doc:`Extra Toolkits ` -- The :ref:`users-guide-explain` section of the :doc:`Users guide ` -- Many of the :doc:`Tutorials ` have explanatory material Third-party Packages From 8eb01928d1d6795e8488080f818dab874a28879c Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 21 Aug 2021 21:24:18 -0400 Subject: [PATCH 48/61] DOC: rename user's guide -> user guide --- doc/users/index.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/users/index.rst b/doc/users/index.rst index 840603ee24d9..8b326d5dc0ab 100644 --- a/doc/users/index.rst +++ b/doc/users/index.rst @@ -1,8 +1,8 @@ .. _users-guide-index: -############ -User's guide -############ +########## +User guide +########## .. only:: html From 0d62f099f4dbe5ff684890ceff3d8aeb591a564b Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 21 Aug 2021 21:24:30 -0400 Subject: [PATCH 49/61] DOC: move detailed installation to user guide --- doc/index.rst | 6 ------ doc/users/index.rst | 1 + 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/doc/index.rst b/doc/index.rst index ec55c673dd9a..fda4e0444279 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -4,12 +4,6 @@ .. module:: matplotlib -.. toctree:: - :maxdepth: 2 - :hidden: - - users/installing.rst - Matplotlib documentation ------------------------ diff --git a/doc/users/index.rst b/doc/users/index.rst index 8b326d5dc0ab..53537f39ee7b 100644 --- a/doc/users/index.rst +++ b/doc/users/index.rst @@ -12,6 +12,7 @@ User guide .. toctree:: :maxdepth: 3 + installing.rst explain.rst resource.rst backmatter.rst From 3839c5bac2c40d3d47f1aa4f4256407333bfe271 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 23 Aug 2021 23:00:02 -0400 Subject: [PATCH 50/61] DOC: suggestions from review Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> --- doc/index.rst | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/doc/index.rst b/doc/index.rst index fda4e0444279..45014bde071b 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -20,13 +20,12 @@ Installation :card: + install-card :column: col-lg-6 col-md-6 col-sm-12 col-xs-12 p-3 - Working with conda? + Installing using conda ^^^^^^^^^^^^^^^^^^^ Matplotlib is part of the `Anaconda `__ distribution and can be installed with Anaconda or Miniconda: - ++++++++++++++++++++++ .. code-block:: bash @@ -34,7 +33,7 @@ Installation --- - Prefer pip? + Installing using pip ^^^^^^^^^^^ Matplotlib can be installed via pip from `PyPI `__. @@ -46,17 +45,8 @@ Installation pip install matplotlib --- - :column: col-12 p-3 - In-depth instructions? - ^^^^^^^^^^^^^^^^^^^^^^ - - Installing a specific version? Installing from source? Check the advanced - installation page. - - .. container:: custom-button - - :doc:`Installation Guide ` +Further details are availabe in the :doc:`Installation Guide `. Learning Resources @@ -105,7 +95,7 @@ Learning Resources -Third-party Packages +Third-party packages -------------------- There are many `Third-party packages From 1c77a1bd78cea2dd8fa55b382b0f318473664939 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 23 Aug 2021 23:23:07 -0400 Subject: [PATCH 51/61] STY: flake8 in conf.py --- doc/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/conf.py b/doc/conf.py index 845c362be74e..b4f8b7ef5806 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -80,6 +80,7 @@ panels_add_bootstrap_css = False + def _check_dependencies(): names = { "colorspacious": 'colorspacious', From f779ea63f0bb0373f6ecc909c935d086d517b3bc Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 23 Aug 2021 23:37:13 -0400 Subject: [PATCH 52/61] STY: fix section capitalization --- doc/index.rst | 2 +- doc/users/fonts.rst | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/index.rst b/doc/index.rst index 45014bde071b..7b3a0187d553 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -49,7 +49,7 @@ Installation Further details are availabe in the :doc:`Installation Guide `. -Learning Resources +Learning resources ================== diff --git a/doc/users/fonts.rst b/doc/users/fonts.rst index e385c98284c0..1b9861df7414 100644 --- a/doc/users/fonts.rst +++ b/doc/users/fonts.rst @@ -1,16 +1,16 @@ -Fonts in Matplotlib Text Engine +Fonts in Matplotlib text engine =============================== Matplotlib needs fonts to work with its text engine, some of which are shipped alongside the installation. However, users can configure the default fonts, or -even provide their own custom fonts! For more details, see :doc:`Customizing +even provide their own custom fonts! For more details, see :doc:`Customizing text properties `. However, Matplotlib also provides an option to offload text rendering to a TeX engine (``usetex=True``), see :doc:`Text rendering with LaTeX `. -Font Specifications +Font specifications ------------------- Fonts have a long and sometimes incompatible history in computing, leading to different platforms supporting different types of fonts. In practice, there are @@ -40,7 +40,7 @@ fonts', more about which is explained later in the guide): NOTE: Adobe will disable support for authoring with Type 1 fonts in January 2023. `Read more here. `_ -Special Mentions +Special mentions ^^^^^^^^^^^^^^^^ Other font specifications which Matplotlib supports: From 50d41c5ab66222f4b6d6da959ce29b0993180bb8 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 23 Aug 2021 23:38:15 -0400 Subject: [PATCH 53/61] DOC: continue to shuffle things around to placate the TOC --- doc/index.rst | 9 +++++---- doc/users/index.rst | 15 ++++++++++----- doc/users/resource.rst | 8 -------- 3 files changed, 15 insertions(+), 17 deletions(-) delete mode 100644 doc/users/resource.rst diff --git a/doc/index.rst b/doc/index.rst index 7b3a0187d553..5ffe6d688a7d 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -106,8 +106,9 @@ Matplotlib. Contributing ------------ +Matplotlib is a community project maitained for and by its users. There are many ways +you can help! -.. toctree:: - :maxdepth: 2 - - devel/index.rst +- Help other users `on discourse `__ +- report a bug or request a feature `on GitHub `__ +- or improve the :ref:`documentation and code ` diff --git a/doc/users/index.rst b/doc/users/index.rst index 53537f39ee7b..ca88135034cf 100644 --- a/doc/users/index.rst +++ b/doc/users/index.rst @@ -9,10 +9,15 @@ User guide :Release: |version| :Date: |today| + + + .. toctree:: - :maxdepth: 3 + :maxdepth: 3 - installing.rst - explain.rst - resource.rst - backmatter.rst + installing.rst + explain.rst + ../faq/index.rst + ../resources/index.rst + backmatter.rst + ../devel/index.rst diff --git a/doc/users/resource.rst b/doc/users/resource.rst deleted file mode 100644 index 9b68eb3547bc..000000000000 --- a/doc/users/resource.rst +++ /dev/null @@ -1,8 +0,0 @@ -FAQ and External Resources --------------------------- - -.. toctree:: - :maxdepth: 2 - - ../faq/index.rst - ../resources/index.rst From 769b3b7ddf6ad564f03f2466b92b01323c6c1451 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 23 Aug 2021 23:38:51 -0400 Subject: [PATCH 54/61] DOC: add link to contents to index page --- doc/contents.rst | 2 +- doc/index.rst | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/doc/contents.rst b/doc/contents.rst index e9e27c81a9fc..b7654ae7e77f 100644 --- a/doc/contents.rst +++ b/doc/contents.rst @@ -1,4 +1,4 @@ - +.. _complete_sitemap: Contents ======== diff --git a/doc/index.rst b/doc/index.rst index 5ffe6d688a7d..224eba9b892b 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -112,3 +112,9 @@ you can help! - Help other users `on discourse `__ - report a bug or request a feature `on GitHub `__ - or improve the :ref:`documentation and code ` + + +Site Map +-------- + +The :ref:`complete contents of the docs `. From 2d599a8ece58d5ed57dde4fbcb3d73fd35e37a58 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 23 Aug 2021 23:39:10 -0400 Subject: [PATCH 55/61] DOC: tweaks to content / layout of inner index --- doc/index.rst | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/doc/index.rst b/doc/index.rst index 224eba9b892b..4672a508a850 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -5,10 +5,8 @@ .. module:: matplotlib -Matplotlib documentation ------------------------- - -Release: |release| +Matplotlib |release| documentation +---------------------------------- Matplotlib is a comprehensive library for creating static, animated, and interactive visualizations in Python. @@ -38,13 +36,11 @@ Installation Matplotlib can be installed via pip from `PyPI `__. - ++++ .. code-block:: bash pip install matplotlib - --- Further details are availabe in the :doc:`Installation Guide `. @@ -85,11 +81,10 @@ Learning resources - :doc:`API Reference ` - - :doc:`pyplot API `: top-level interface to create - Figures (`.pyplot.figure`) and Subplots (`.pyplot.subplots`, - `.pyplot.subplot_mosaic`) - :doc:`Axes API ` for *most* plotting methods - :doc:`Figure API ` for figure-level methods + - Top-level interface to create Figures (`.pyplot.figure`) and Subplots + (`.pyplot.subplots`, `.pyplot.subplot_mosaic`) - :doc:`Extra Toolkits ` From 887be7b10fe85fb4e5fdc767fadd818c76f48486 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 23 Aug 2021 23:58:20 -0400 Subject: [PATCH 56/61] DOC: add note about wheels --- doc/index.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/index.rst b/doc/index.rst index 4672a508a850..3bfc6313eed4 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -34,7 +34,8 @@ Installation Installing using pip ^^^^^^^^^^^ - Matplotlib can be installed via pip from `PyPI `__. + Matplotlib provides wheels for Linux, OSX, and Windows which can be + installed via pip from `PyPI `__. .. code-block:: bash From 965ef4d865b09dc1defc88c419efa320844cf780 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Tue, 24 Aug 2021 11:55:28 -0400 Subject: [PATCH 57/61] DOC: hide the next/prev buttons --- doc/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index b4f8b7ef5806..7167515082a3 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -315,8 +315,8 @@ def _check_dependencies(): "url": "https://twitter.com/matplotlib/", "icon": "fab fa-twitter-square", }, - ], + "show_prev_next": False, } include_analytics = False if include_analytics: From 4a892b7a97a19b1fa340be5a2d0a81eb8e15e79f Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Tue, 24 Aug 2021 12:38:59 -0400 Subject: [PATCH 58/61] DOC: add (but do not enable) external link to third-party page --- doc/conf.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/conf.py b/doc/conf.py index 7167515082a3..8452de5986d7 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -294,6 +294,12 @@ def _check_dependencies(): html_theme_options = { "logo_link": "index", "collapse_navigation": True if CIRCLECI else False, + # "external_links": [ + # { + # "name": "Third party packages", + # "url": "https://matplotlib.org/mpl-third-party/" + # }, + # ], "icon_links": [ { "name": "gitter", From 6d6ce6b5ea212134d7288f14a03d2b966f37dee7 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 3 Sep 2021 20:04:13 -0400 Subject: [PATCH 59/61] DOC: re-arrange contents.rst and top navbar This hard codes the contents of the top bar which gets us out of the trap that we want to have a different top-level structure to our complete documentation than we want to have in the nav bar. --- doc/_templates/mpl_nav_bar.html | 24 ++++++++++++++++++++++++ doc/conf.py | 1 + doc/contents.rst | 7 +++---- doc/faq/index.rst | 6 +++--- doc/users/backmatter.rst | 4 ++-- doc/users/index.rst | 15 ++++++++------- examples/user_interfaces/test.bmp | Bin 0 -> 800054 bytes 7 files changed, 41 insertions(+), 16 deletions(-) create mode 100644 doc/_templates/mpl_nav_bar.html create mode 100644 examples/user_interfaces/test.bmp diff --git a/doc/_templates/mpl_nav_bar.html b/doc/_templates/mpl_nav_bar.html new file mode 100644 index 000000000000..d672730de313 --- /dev/null +++ b/doc/_templates/mpl_nav_bar.html @@ -0,0 +1,24 @@ + diff --git a/doc/conf.py b/doc/conf.py index 8452de5986d7..fc4cdcf09359 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -323,6 +323,7 @@ def _check_dependencies(): }, ], "show_prev_next": False, + "navbar_center": ["mpl_nav_bar.html"], } include_analytics = False if include_analytics: diff --git a/doc/contents.rst b/doc/contents.rst index b7654ae7e77f..f9d10936c7fc 100644 --- a/doc/contents.rst +++ b/doc/contents.rst @@ -14,11 +14,10 @@ Contents .. toctree:: :maxdepth: 2 - plot_types/index.rst - gallery/index.rst - tutorials/index.rst - api/index.rst + users/installing.rst users/index.rst + users/backmatter.rst + devel/index.rst users/release_notes.rst .. only:: html diff --git a/doc/faq/index.rst b/doc/faq/index.rst index def68fc84c71..d7840a626d1d 100644 --- a/doc/faq/index.rst +++ b/doc/faq/index.rst @@ -1,8 +1,8 @@ .. _faq-index: -################## -The Matplotlib FAQ -################## +###### +How-To +###### .. only:: html diff --git a/doc/users/backmatter.rst b/doc/users/backmatter.rst index c95908c9314c..8d9fc0407d6f 100644 --- a/doc/users/backmatter.rst +++ b/doc/users/backmatter.rst @@ -1,6 +1,6 @@ -Back Matter ------------ +Project Information +------------------- .. toctree:: :maxdepth: 1 diff --git a/doc/users/index.rst b/doc/users/index.rst index ca88135034cf..32d70d3537fe 100644 --- a/doc/users/index.rst +++ b/doc/users/index.rst @@ -1,8 +1,8 @@ .. _users-guide-index: -########## -User guide -########## +########### +Usage Guide +########### .. only:: html @@ -13,11 +13,12 @@ User guide .. toctree:: - :maxdepth: 3 + :maxdepth: 2 - installing.rst + ../plot_types/index.rst + ../tutorials/index.rst + ../gallery/index.rst explain.rst ../faq/index.rst + ../api/index.rst ../resources/index.rst - backmatter.rst - ../devel/index.rst diff --git a/examples/user_interfaces/test.bmp b/examples/user_interfaces/test.bmp new file mode 100644 index 0000000000000000000000000000000000000000..ae58370451a8e036ff214bef4c9cd1b17533aa7a GIT binary patch literal 800054 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 RfB*y_009U<00K1=`2TKqI1B&) literal 0 HcmV?d00001 From daffe4613bd477b3ceecb2d09ca0304c035ec327 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 3 Sep 2021 20:09:28 -0400 Subject: [PATCH 60/61] DOC: push up sphinx-pydata-theme dependency --- requirements/doc/doc-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/doc/doc-requirements.txt b/requirements/doc/doc-requirements.txt index 2f69ed7d032f..8be10d4d107c 100644 --- a/requirements/doc/doc-requirements.txt +++ b/requirements/doc/doc-requirements.txt @@ -12,7 +12,7 @@ colorspacious ipython ipywidgets numpydoc>=0.8 -pydata-sphinx-theme>=0.5.0 +pydata-sphinx-theme>=0.6.0 sphinxcontrib-svg2pdfconverter>=1.1.0 # sphinx-gallery>=0.7 # b41e328 is PR 808 which adds the image_srcset directive. When this is From 399fe226a27f05f2fbcc8e3ae46eae3650938492 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 20 Aug 2021 19:29:55 -0400 Subject: [PATCH 61/61] DOC: re-arrange the release notes toctrees This makes them play much nicer with the navigation on the left side. --- doc/users/release_notes.rst | 185 +----------------------------------- doc/users/relnotes/0.x.rst | 40 ++++++++ doc/users/relnotes/1.x.rst | 20 ++++ doc/users/relnotes/2.0.rst | 9 ++ doc/users/relnotes/2.1.rst | 10 ++ doc/users/relnotes/2.2.rst | 8 ++ doc/users/relnotes/3.0.rst | 10 ++ doc/users/relnotes/3.1.rst | 12 +++ doc/users/relnotes/3.2.rst | 11 +++ doc/users/relnotes/3.3.rst | 14 +++ doc/users/relnotes/3.4.rst | 11 +++ doc/users/relnotes/3.5.rst | 9 ++ 12 files changed, 157 insertions(+), 182 deletions(-) create mode 100644 doc/users/relnotes/0.x.rst create mode 100644 doc/users/relnotes/1.x.rst create mode 100644 doc/users/relnotes/2.0.rst create mode 100644 doc/users/relnotes/2.1.rst create mode 100644 doc/users/relnotes/2.2.rst create mode 100644 doc/users/relnotes/3.0.rst create mode 100644 doc/users/relnotes/3.1.rst create mode 100644 doc/users/relnotes/3.2.rst create mode 100644 doc/users/relnotes/3.3.rst create mode 100644 doc/users/relnotes/3.4.rst create mode 100644 doc/users/relnotes/3.5.rst diff --git a/doc/users/release_notes.rst b/doc/users/release_notes.rst index d8458e5ba1c2..af06f437eda2 100644 --- a/doc/users/release_notes.rst +++ b/doc/users/release_notes.rst @@ -6,188 +6,9 @@ Release notes ============= -.. include from another document so that it's easy to exclude this for releases -.. include:: release_notes_next.rst - -Version 3.4 -=========== -.. toctree:: - :maxdepth: 1 - - prev_whats_new/whats_new_3.4.0.rst - ../api/prev_api_changes/api_changes_3.4.2.rst - ../api/prev_api_changes/api_changes_3.4.0.rst - prev_whats_new/github_stats_3.4.1.rst - prev_whats_new/github_stats_3.4.0.rst - -Version 3.3 -=========== -.. toctree:: - :maxdepth: 1 - - prev_whats_new/whats_new_3.3.0.rst - ../api/prev_api_changes/api_changes_3.3.1.rst - ../api/prev_api_changes/api_changes_3.3.0.rst - prev_whats_new/github_stats_3.3.4.rst - prev_whats_new/github_stats_3.3.3.rst - prev_whats_new/github_stats_3.3.2.rst - prev_whats_new/github_stats_3.3.1.rst - prev_whats_new/github_stats_3.3.0.rst - -Version 3.2 -=========== -.. toctree:: - :maxdepth: 1 - - prev_whats_new/whats_new_3.2.0.rst - ../api/prev_api_changes/api_changes_3.2.0.rst - prev_whats_new/github_stats_3.2.2.rst - prev_whats_new/github_stats_3.2.1.rst - prev_whats_new/github_stats_3.2.0.rst - -Version 3.1 -=========== .. toctree:: :maxdepth: 1 + :glob: + :reversed: - prev_whats_new/whats_new_3.1.0.rst - ../api/prev_api_changes/api_changes_3.1.1.rst - ../api/prev_api_changes/api_changes_3.1.0.rst - prev_whats_new/github_stats_3.1.3.rst - prev_whats_new/github_stats_3.1.2.rst - prev_whats_new/github_stats_3.1.1.rst - prev_whats_new/github_stats_3.1.0.rst - -Version 3.0 -=========== -.. toctree:: - :maxdepth: 1 - - prev_whats_new/whats_new_3.0.rst - ../api/prev_api_changes/api_changes_3.0.1.rst - ../api/prev_api_changes/api_changes_3.0.0.rst - prev_whats_new/github_stats_3.0.3.rst - prev_whats_new/github_stats_3.0.2.rst - prev_whats_new/github_stats_3.0.1.rst - prev_whats_new/github_stats_3.0.0.rst - -Version 2.2 -=========== -.. toctree:: - :maxdepth: 1 - - prev_whats_new/whats_new_2.2.rst - ../api/prev_api_changes/api_changes_2.2.0.rst - -Version 2.1 -=========== -.. toctree:: - :maxdepth: 1 - - prev_whats_new/whats_new_2.1.0.rst - ../api/prev_api_changes/api_changes_2.1.2.rst - ../api/prev_api_changes/api_changes_2.1.1.rst - ../api/prev_api_changes/api_changes_2.1.0.rst - -Version 2.0 -=========== -.. toctree:: - :maxdepth: 1 - - prev_whats_new/whats_new_2.0.0.rst - ../api/prev_api_changes/api_changes_2.0.1.rst - ../api/prev_api_changes/api_changes_2.0.0.rst - -Version 1.5 -=========== -.. toctree:: - :maxdepth: 1 - - prev_whats_new/whats_new_1.5.rst - ../api/prev_api_changes/api_changes_1.5.3.rst - ../api/prev_api_changes/api_changes_1.5.2.rst - ../api/prev_api_changes/api_changes_1.5.0.rst - -Version 1.4 -=========== -.. toctree:: - :maxdepth: 1 - - prev_whats_new/whats_new_1.4.rst - ../api/prev_api_changes/api_changes_1.4.x.rst - -Version 1.3 -=========== -.. toctree:: - :maxdepth: 1 - - prev_whats_new/whats_new_1.3.rst - ../api/prev_api_changes/api_changes_1.3.x.rst - -Version 1.2 -=========== -.. toctree:: - :maxdepth: 1 - - prev_whats_new/whats_new_1.2.2.rst - prev_whats_new/whats_new_1.2.rst - ../api/prev_api_changes/api_changes_1.2.x.rst - -Version 1.1 -=========== -.. toctree:: - :maxdepth: 1 - - prev_whats_new/whats_new_1.1.rst - ../api/prev_api_changes/api_changes_1.1.x.rst - -Version 1.0 -=========== -.. toctree:: - :maxdepth: 1 - - prev_whats_new/whats_new_1.0.rst - -Version 0.x -=========== -.. toctree:: - :maxdepth: 1 - - prev_whats_new/changelog.rst - prev_whats_new/whats_new_0.99.rst - ../api/prev_api_changes/api_changes_0.99.x.rst - ../api/prev_api_changes/api_changes_0.99.rst - prev_whats_new/whats_new_0.98.4.rst - ../api/prev_api_changes/api_changes_0.98.x.rst - ../api/prev_api_changes/api_changes_0.98.1.rst - ../api/prev_api_changes/api_changes_0.98.0.rst - ../api/prev_api_changes/api_changes_0.91.2.rst - ../api/prev_api_changes/api_changes_0.91.0.rst - ../api/prev_api_changes/api_changes_0.90.1.rst - ../api/prev_api_changes/api_changes_0.90.0.rst - - ../api/prev_api_changes/api_changes_0.87.7.rst - ../api/prev_api_changes/api_changes_0.86.rst - ../api/prev_api_changes/api_changes_0.85.rst - ../api/prev_api_changes/api_changes_0.84.rst - ../api/prev_api_changes/api_changes_0.83.rst - ../api/prev_api_changes/api_changes_0.82.rst - ../api/prev_api_changes/api_changes_0.81.rst - ../api/prev_api_changes/api_changes_0.80.rst - - ../api/prev_api_changes/api_changes_0.73.rst - ../api/prev_api_changes/api_changes_0.72.rst - ../api/prev_api_changes/api_changes_0.71.rst - ../api/prev_api_changes/api_changes_0.70.rst - - ../api/prev_api_changes/api_changes_0.65.1.rst - ../api/prev_api_changes/api_changes_0.65.rst - ../api/prev_api_changes/api_changes_0.63.rst - ../api/prev_api_changes/api_changes_0.61.rst - ../api/prev_api_changes/api_changes_0.60.rst - - ../api/prev_api_changes/api_changes_0.54.3.rst - ../api/prev_api_changes/api_changes_0.54.rst - ../api/prev_api_changes/api_changes_0.50.rst - ../api/prev_api_changes/api_changes_0.42.rst - ../api/prev_api_changes/api_changes_0.40.rst + relnotes/* diff --git a/doc/users/relnotes/0.x.rst b/doc/users/relnotes/0.x.rst new file mode 100644 index 000000000000..baddb784b6f2 --- /dev/null +++ b/doc/users/relnotes/0.x.rst @@ -0,0 +1,40 @@ +Version 0.x +*********** + +.. toctree:: + :maxdepth: 1 + + ../prev_whats_new/changelog.rst + ../prev_whats_new/whats_new_0.99.rst + ../../api/prev_api_changes/api_changes_0.99.x.rst + ../../api/prev_api_changes/api_changes_0.99.rst + ../prev_whats_new/whats_new_0.98.4.rst + ../../api/prev_api_changes/api_changes_0.98.x.rst + ../../api/prev_api_changes/api_changes_0.98.1.rst + ../../api/prev_api_changes/api_changes_0.98.0.rst + ../../api/prev_api_changes/api_changes_0.91.2.rst + ../../api/prev_api_changes/api_changes_0.91.0.rst + ../../api/prev_api_changes/api_changes_0.90.1.rst + ../../api/prev_api_changes/api_changes_0.90.0.rst + ../../api/prev_api_changes/api_changes_0.87.7.rst + ../../api/prev_api_changes/api_changes_0.86.rst + ../../api/prev_api_changes/api_changes_0.85.rst + ../../api/prev_api_changes/api_changes_0.84.rst + ../../api/prev_api_changes/api_changes_0.83.rst + ../../api/prev_api_changes/api_changes_0.82.rst + ../../api/prev_api_changes/api_changes_0.81.rst + ../../api/prev_api_changes/api_changes_0.80.rst + ../../api/prev_api_changes/api_changes_0.73.rst + ../../api/prev_api_changes/api_changes_0.72.rst + ../../api/prev_api_changes/api_changes_0.71.rst + ../../api/prev_api_changes/api_changes_0.70.rst + ../../api/prev_api_changes/api_changes_0.65.1.rst + ../../api/prev_api_changes/api_changes_0.65.rst + ../../api/prev_api_changes/api_changes_0.63.rst + ../../api/prev_api_changes/api_changes_0.61.rst + ../../api/prev_api_changes/api_changes_0.60.rst + ../../api/prev_api_changes/api_changes_0.54.3.rst + ../../api/prev_api_changes/api_changes_0.54.rst + ../../api/prev_api_changes/api_changes_0.50.rst + ../../api/prev_api_changes/api_changes_0.42.rst + ../../api/prev_api_changes/api_changes_0.40.rst diff --git a/doc/users/relnotes/1.x.rst b/doc/users/relnotes/1.x.rst new file mode 100644 index 000000000000..fe4e273c6917 --- /dev/null +++ b/doc/users/relnotes/1.x.rst @@ -0,0 +1,20 @@ +Version 1.x +*********** + +.. toctree:: + :maxdepth: 1 + + ../prev_whats_new/whats_new_1.5.rst + ../../api/prev_api_changes/api_changes_1.5.3.rst + ../../api/prev_api_changes/api_changes_1.5.2.rst + ../prev_whats_new/whats_new_1.4.rst + ../../api/prev_api_changes/api_changes_1.4.x.rst + ../../api/prev_api_changes/api_changes_1.5.0.rst + ../prev_whats_new/whats_new_1.3.rst + ../../api/prev_api_changes/api_changes_1.3.x.rst + ../prev_whats_new/whats_new_1.2.2.rst + ../prev_whats_new/whats_new_1.2.rst + ../../api/prev_api_changes/api_changes_1.2.x.rst + ../prev_whats_new/whats_new_1.1.rst + ../../api/prev_api_changes/api_changes_1.1.x.rst + ../prev_whats_new/whats_new_1.0.rst diff --git a/doc/users/relnotes/2.0.rst b/doc/users/relnotes/2.0.rst new file mode 100644 index 000000000000..892c8af988f8 --- /dev/null +++ b/doc/users/relnotes/2.0.rst @@ -0,0 +1,9 @@ +Version 2.0 +*********** + +.. toctree:: + :maxdepth: 1 + + ../prev_whats_new/whats_new_2.0.0.rst + ../../api/prev_api_changes/api_changes_2.0.1.rst + ../../api/prev_api_changes/api_changes_2.0.0.rst diff --git a/doc/users/relnotes/2.1.rst b/doc/users/relnotes/2.1.rst new file mode 100644 index 000000000000..262c1b9bfc4f --- /dev/null +++ b/doc/users/relnotes/2.1.rst @@ -0,0 +1,10 @@ +Version 2.1 +*********** + +.. toctree:: + :maxdepth: 1 + + ../prev_whats_new/whats_new_2.1.0.rst + ../../api/prev_api_changes/api_changes_2.1.2.rst + ../../api/prev_api_changes/api_changes_2.1.1.rst + ../../api/prev_api_changes/api_changes_2.1.0.rst diff --git a/doc/users/relnotes/2.2.rst b/doc/users/relnotes/2.2.rst new file mode 100644 index 000000000000..4c23465627ff --- /dev/null +++ b/doc/users/relnotes/2.2.rst @@ -0,0 +1,8 @@ +Version 2.2 +*********** + +.. toctree:: + :maxdepth: 1 + + ../prev_whats_new/whats_new_2.2.rst + ../../api/prev_api_changes/api_changes_2.2.0.rst diff --git a/doc/users/relnotes/3.0.rst b/doc/users/relnotes/3.0.rst new file mode 100644 index 000000000000..1f8f4bb4679b --- /dev/null +++ b/doc/users/relnotes/3.0.rst @@ -0,0 +1,10 @@ +Version 3.0 +*********** + +.. toctree:: + :maxdepth: 1 + + ../prev_whats_new/whats_new_3.0.rst + ../../api/prev_api_changes/api_changes_3.0.1.rst + ../../api/prev_api_changes/api_changes_3.0.0.rst + ../prev_whats_new/github_stats_3.0.2.rst diff --git a/doc/users/relnotes/3.1.rst b/doc/users/relnotes/3.1.rst new file mode 100644 index 000000000000..3d4c88209e12 --- /dev/null +++ b/doc/users/relnotes/3.1.rst @@ -0,0 +1,12 @@ +Version 3.1 +*********** + +.. toctree:: + :maxdepth: 1 + + ../prev_whats_new/whats_new_3.1.0.rst + ../../api/prev_api_changes/api_changes_3.1.1.rst + ../../api/prev_api_changes/api_changes_3.1.0.rst + ../prev_whats_new/github_stats_3.1.2.rst + ../prev_whats_new/github_stats_3.1.1.rst + ../prev_whats_new/github_stats_3.1.0.rst diff --git a/doc/users/relnotes/3.2.rst b/doc/users/relnotes/3.2.rst new file mode 100644 index 000000000000..c7512026ea87 --- /dev/null +++ b/doc/users/relnotes/3.2.rst @@ -0,0 +1,11 @@ +Version 3.2 +*********** + +.. toctree:: + :maxdepth: 1 + + ../prev_whats_new/whats_new_3.2.0.rst + ../../api/prev_api_changes/api_changes_3.2.0.rst + ../prev_whats_new/github_stats_3.2.2.rst + ../prev_whats_new/github_stats_3.2.1.rst + ../prev_whats_new/github_stats_3.2.0.rst diff --git a/doc/users/relnotes/3.3.rst b/doc/users/relnotes/3.3.rst new file mode 100644 index 000000000000..f45253265379 --- /dev/null +++ b/doc/users/relnotes/3.3.rst @@ -0,0 +1,14 @@ +Version 3.3 +=========== + +.. toctree:: + :maxdepth: 1 + + ../prev_whats_new/whats_new_3.3.0.rst + ../../api/prev_api_changes/api_changes_3.3.1.rst + ../../api/prev_api_changes/api_changes_3.3.0.rst + ../prev_whats_new/github_stats_3.3.4.rst + ../prev_whats_new/github_stats_3.3.3.rst + ../prev_whats_new/github_stats_3.3.2.rst + ../prev_whats_new/github_stats_3.3.1.rst + ../prev_whats_new/github_stats_3.3.0.rst diff --git a/doc/users/relnotes/3.4.rst b/doc/users/relnotes/3.4.rst new file mode 100644 index 000000000000..4d7562be18cd --- /dev/null +++ b/doc/users/relnotes/3.4.rst @@ -0,0 +1,11 @@ +Version 3.4 +=========== + +.. toctree:: + :maxdepth: 1 + + ../prev_whats_new/whats_new_3.4.0.rst + ../../api/prev_api_changes/api_changes_3.4.2.rst + ../../api/prev_api_changes/api_changes_3.4.0.rst + ../prev_whats_new/github_stats_3.4.1.rst + ../prev_whats_new/github_stats_3.4.0.rst diff --git a/doc/users/relnotes/3.5.rst b/doc/users/relnotes/3.5.rst new file mode 100644 index 000000000000..44f49b923d49 --- /dev/null +++ b/doc/users/relnotes/3.5.rst @@ -0,0 +1,9 @@ +Version 3.5 +=========== + +.. toctree:: + :maxdepth: 1 + + ../next_whats_new + ../../api/next_api_changes + ../github_stats