From 5a58788235d23d2707bb570f867a10de212401e6 Mon Sep 17 00:00:00 2001 From: takimata Date: Fri, 25 Jun 2021 11:43:55 +0200 Subject: [PATCH 1/9] 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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