From 42aaea3678f578888e73ebb0ae7e6f657f68171a Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 1 Oct 2026 20:08:57 +0200 Subject: [PATCH] Version 0.2.0 (#74) * Add a include_plotlyjs param to savefig (#69) * Update README.qmd (#70) * Update README.qmd * Update README.md * Added a matplotlib parser (#71) * Added a matplotlib parser * Extende matplotlib parsing coverage * Extend matplotlib parsing * formatting * Fix bugs * Xarray support (#72) * Added xarray support * small edits like adding titles and so on * Added da.xarray accessor * Added xarray to docs + bugfixes * formatting * Preparation for tikz support in plasma-plots (#73) * Preparation for tikz support in plasma-plots * Fix tests * Fix tests * Another fix * Fix docs build (#75) --- .github/workflows/ci_pipeline.yml | 14 +- .github/workflows/docs.yml | 8 +- .github/workflows/matplotlib-import.yml | 25 + README.md | 82 +- README.qmd | 64 +- .../figure-commonmark/cell-20-output-1.png | Bin 0 -> 68939 bytes .../fig-showcase-subplots-output-1.png | Bin 25103 -> 106705 bytes docs/matplotlib-import-support.md | 209 +++ docs/source/index.rst | 1 + pyproject.toml | 9 +- scripts/benchmark_matplotlib_import.py | 58 + .../backends/matplotlib/import_state.py | 342 +++++ .../backends/matplotlib/importer.py | 1078 +++++++++++++ .../backends/tikzfigure/__init__.py | 9 + src/maxplotlib/backends/tikzfigure/convert.py | 1331 +++++++++++++++++ src/maxplotlib/backends/tikzfigure/text.py | 183 +++ src/maxplotlib/canvas/canvas.py | 833 ++++++----- src/maxplotlib/subfigure/line_plot.py | 920 ++++++------ src/maxplotlib/tests/test_canvas.py | 57 +- src/maxplotlib/tests/test_flame_chart.py | 9 + src/maxplotlib/tests/test_gantt_chart.py | 9 + .../tests/test_matplotlib_import.py | 348 +++++ .../tests/test_matplotlib_import_extended.py | 612 ++++++++ .../tests/test_tikzfigure_backend.py | 259 ++++ src/maxplotlib/tests/test_xarray.py | 896 +++++++++++ src/maxplotlib/utils/xarray_support.py | 346 +++++ src/maxplotlib/xarray.py | 170 +++ tutorials/tutorial_02.ipynb | 4 +- tutorials/tutorial_07_tikz.ipynb | 250 ++-- .../tutorial_15_tikzfigure_subplots.ipynb | 53 +- tutorials/tutorial_17_xarray.ipynb | 600 ++++++++ 31 files changed, 7829 insertions(+), 950 deletions(-) create mode 100644 .github/workflows/matplotlib-import.yml create mode 100644 README_files/figure-commonmark/cell-20-output-1.png create mode 100644 docs/matplotlib-import-support.md create mode 100644 scripts/benchmark_matplotlib_import.py create mode 100644 src/maxplotlib/backends/matplotlib/import_state.py create mode 100644 src/maxplotlib/backends/matplotlib/importer.py create mode 100644 src/maxplotlib/backends/tikzfigure/__init__.py create mode 100644 src/maxplotlib/backends/tikzfigure/convert.py create mode 100644 src/maxplotlib/backends/tikzfigure/text.py create mode 100644 src/maxplotlib/tests/test_matplotlib_import.py create mode 100644 src/maxplotlib/tests/test_matplotlib_import_extended.py create mode 100644 src/maxplotlib/tests/test_tikzfigure_backend.py create mode 100644 src/maxplotlib/tests/test_xarray.py create mode 100644 src/maxplotlib/utils/xarray_support.py create mode 100644 src/maxplotlib/xarray.py create mode 100644 tutorials/tutorial_17_xarray.ipynb diff --git a/.github/workflows/ci_pipeline.yml b/.github/workflows/ci_pipeline.yml index eced47c..d3a6406 100644 --- a/.github/workflows/ci_pipeline.yml +++ b/.github/workflows/ci_pipeline.yml @@ -25,9 +25,19 @@ jobs: # with: # python-version: ${{ matrix.python-version }} + - name: Install LaTeX + run: | + # pdflatex with pgfplots: the tikzfigure backend, its tests and tutorials + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + texlive-latex-base texlive-latex-extra texlive-pictures \ + texlive-fonts-recommended lmodern + - name: Install python dependencies run: | python -m pip install --upgrade pip + # tikzfigure 0.4.0 is not on PyPI yet: install it from GitHub (drop once released) + pip install "tikzfigure[vis] @ git+https://github.com/max-models/tikzfigure@c290f43ac1deb4a4ed3b5b40919f10d3a6b53e8b" pip install ".[dev]" - name: Run tests @@ -37,7 +47,5 @@ jobs: - name: Test tutorials run: | - # tutorial_07_tikz.ipynb requires pdflatex — skip it in CI - jupyter nbconvert --to notebook --execute \ - $(ls tutorials/*.ipynb | grep -v tutorial_07_tikz) \ + jupyter nbconvert --to notebook --execute tutorials/*.ipynb \ --output-dir=/tmp --ExecutePreprocessor.timeout=300 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c59b8ea..8a9f6c1 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -25,14 +25,20 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Install pandoc + - name: Install pandoc and LaTeX run: | sudo apt-get update sudo apt-get install -y pandoc + # pdflatex with pgfplots: the tutorials compile tikzfigure figures + sudo apt-get install -y --no-install-recommends \ + texlive-latex-base texlive-latex-extra texlive-pictures \ + texlive-fonts-recommended lmodern - name: Install Python dependencies run: | python -m pip install --upgrade pip + # tikzfigure 0.4.0 is not on PyPI yet: install it from GitHub (drop once released) + pip install "tikzfigure[vis] @ git+https://github.com/max-models/tikzfigure@c290f43ac1deb4a4ed3b5b40919f10d3a6b53e8b" pip install ".[docs]" - name: Build Sphinx docs diff --git a/.github/workflows/matplotlib-import.yml b/.github/workflows/matplotlib-import.yml new file mode 100644 index 0000000..cde5a4c --- /dev/null +++ b/.github/workflows/matplotlib-import.yml @@ -0,0 +1,25 @@ +name: Matplotlib import compatibility + +on: + push: + branches: [main, devel] + pull_request: + +jobs: + import-fixtures: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + matplotlib: ['3.8.*', '3.9.*', '3.10.*', '3.11.*'] + env: + MPLBACKEND: Agg + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + # tikzfigure 0.4.0 is not on PyPI yet: install it from GitHub (drop once released) + - run: python -m pip install "tikzfigure[vis] @ git+https://github.com/max-models/tikzfigure@c290f43ac1deb4a4ed3b5b40919f10d3a6b53e8b" + - run: python -m pip install '.[test]' 'numpy<2' 'matplotlib==${{ matrix.matplotlib }}' + - run: python -m pytest src/maxplotlib/tests/test_matplotlib_import.py src/maxplotlib/tests/test_matplotlib_import_extended.py diff --git a/README.md b/README.md index cbf1bcc..11f4beb 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Maxlotlib +# Maxplotlib # Maxplotlib @@ -219,23 +219,29 @@ canvas.show(backend="tikzfigure") ![](README_files/figure-commonmark/cell-14-output-1.png) -### Horizontal Subplots with TikZ Backend +### Subplots and Meshes with the TikZ Backend -The tikzfigure backend supports creating side-by-side subplots (1×n -layouts): +The tikzfigure backend draws the canvas with Matplotlib and converts the +drawn figure into pgfplots axes, so every layout converts (rows, columns, +grids, twin axes), with LaTeX text, legends and colorbars. Lines, +markers, bars and text are pgfplots code; meshes and images are included +as images: ``` python x = np.linspace(0, 2 * np.pi, 200) -canvas, (ax1, ax2) = Canvas.subplots(ncols=2, width="10cm", ratio=0.3) +canvas, (ax1, ax2) = Canvas.subplots(ncols=2, width="12cm", ratio=0.45) -ax1.plot(x, np.sin(x), color="royalblue") -ax1.set_title("sin(x)") +ax1.plot(x, np.sin(x), color="royalblue", label="$\\sin x$") +ax1.plot(x, np.cos(x), color="tomato", label="$\\cos x$") +ax1.set_title("Lines") +ax1.set_legend(True) -ax2.plot(x, np.cos(x), color="tomato") -ax2.set_title("cos(x)") +xx, yy = np.meshgrid(x, x) +ax2.pcolormesh(xx, yy, np.sin(xx) * np.cos(yy), cmap="RdBu_r") +ax2.add_colorbar(label="$\\sin x \\cos y$") +ax2.set_title("A mesh") -canvas.suptitle("Trigonometric Functions") -canvas.show(backend="tikzfigure") # Generates LaTeX subfigures +canvas.show(backend="tikzfigure") # compiles with pdflatex ```
@@ -248,9 +254,11 @@ Figure 2
-**Note:** Only horizontal layouts (1×n) are currently supported with the -tikzfigure backend. Vertical/grid layouts will raise -`NotImplementedError`. See the tutorials for more examples. +`canvas.render(backend="tikzfigure").savefig("figure.tikz")` writes the +code for `\\input` in a LaTeX document, with the images next to it. Any +Matplotlib figure converts the same way with +`maxplotlib.backends.tikzfigure.figure_to_tikz(fig)`. See the tutorials +for more examples. ### Terminal Backend with plotext @@ -349,3 +357,49 @@ canvas.show() (
, array([[]], dtype=object)) + +### xarray data + +Plot labelled [xarray](https://docs.xarray.dev) data directly +(`pip install maxplotlibx[xarray]`). Axes come from the coordinates, +labels from the `long_name` and `units` attributes, and titles from the +coordinates you selected. `import maxplotlib.xarray` adds a `.maxplot` +accessor that mirrors xarray’s own `.plot` API and returns an ordinary +`Canvas`, so the backend is still chosen when rendering: + +``` python +import xarray as xr + +import maxplotlib.xarray # registers da.maxplot and ds.maxplot + +t = np.linspace(0, 1.5, 6) +xs = np.linspace(0, 2 * np.pi, 80) +ys = np.linspace(-1, 1, 50) +wave = xr.DataArray( + np.sin(xs - 2 * t[:, None, None]) * np.exp(-3 * ys[None, :, None] ** 2), + dims=("t", "y", "x"), + coords={"t": ("t", t, {"units": "s"}), "y": ys, "x": ("x", xs, {"units": "m"})}, + name="phi", + attrs={"long_name": "Potential", "units": "V"}, +) + +wave.maxplot.pcolormesh(col="t", col_wrap=3, canvas_kwargs={"width": "16cm", "ratio": 0.6}).show() +``` + +![](README_files/figure-commonmark/cell-20-output-1.png) + + (
, + array([[, + , + ], + [, + , + ]], + dtype=object)) + +The same works through Canvas methods, +e.g. `canvas.plot(da, hue="species")`, +`ax.pcolormesh(da, xcoord="R", ycoord="Z")` for curvilinear grids, or +`Canvas.facet(da, col="t")`. `ds.maxplot.scatter(x=..., y=..., hue=...)` +plots one Dataset variable against another. See the [xarray +tutorial](tutorials/tutorial_17_xarray.ipynb) for more. diff --git a/README.qmd b/README.qmd index a27e50c..13111d0 100644 --- a/README.qmd +++ b/README.qmd @@ -1,5 +1,5 @@ --- -title: Maxlotlib +title: Maxplotlib format: gfm fig-dpi: 150 --- @@ -194,9 +194,12 @@ Or plot with the TikZ backend: canvas.show(backend="tikzfigure") ``` -### Horizontal Subplots with TikZ Backend +### Subplots and Meshes with the TikZ Backend -The tikzfigure backend supports creating side-by-side subplots (1×n layouts): +The tikzfigure backend draws the canvas with Matplotlib and converts the drawn figure into +pgfplots axes, so every layout converts (rows, columns, grids, twin axes), with LaTeX text, +legends and colorbars. Lines, markers, bars and text are pgfplots code; meshes and images are +included as images: ```{python} #| label: fig-showcase-subplots @@ -204,19 +207,24 @@ The tikzfigure backend supports creating side-by-side subplots (1×n layouts): #| fig-height: 6 x = np.linspace(0, 2 * np.pi, 200) -canvas, (ax1, ax2) = Canvas.subplots(ncols=2, width="10cm", ratio=0.3) +canvas, (ax1, ax2) = Canvas.subplots(ncols=2, width="12cm", ratio=0.45) -ax1.plot(x, np.sin(x), color="royalblue") -ax1.set_title("sin(x)") +ax1.plot(x, np.sin(x), color="royalblue", label="$\\sin x$") +ax1.plot(x, np.cos(x), color="tomato", label="$\\cos x$") +ax1.set_title("Lines") +ax1.set_legend(True) -ax2.plot(x, np.cos(x), color="tomato") -ax2.set_title("cos(x)") +xx, yy = np.meshgrid(x, x) +ax2.pcolormesh(xx, yy, np.sin(xx) * np.cos(yy), cmap="RdBu_r") +ax2.add_colorbar(label="$\\sin x \\cos y$") +ax2.set_title("A mesh") -canvas.suptitle("Trigonometric Functions") -canvas.show(backend="tikzfigure") # Generates LaTeX subfigures +canvas.show(backend="tikzfigure") # compiles with pdflatex ``` -**Note:** Only horizontal layouts (1×n) are currently supported with the tikzfigure backend. Vertical/grid layouts will raise `NotImplementedError`. See the tutorials for more examples. +`canvas.render(backend="tikzfigure").savefig("figure.tikz")` writes the code for `\\input` in a +LaTeX document, with the images next to it. Any Matplotlib figure converts the same way with +`maxplotlib.backends.tikzfigure.figure_to_tikz(fig)`. See the tutorials for more examples. ### Terminal Backend with plotext @@ -276,3 +284,37 @@ Show all layers: ```{python} canvas.show() ``` + +### xarray data + +Plot labelled [xarray](https://docs.xarray.dev) data directly +(`pip install maxplotlibx[xarray]`). Axes come from the coordinates, labels +from the `long_name` and `units` attributes, and titles from the coordinates +you selected. `import maxplotlib.xarray` adds a `.maxplot` accessor that mirrors +xarray's own `.plot` API and returns an ordinary `Canvas`, so the backend is +still chosen when rendering: + +```{python} +import xarray as xr + +import maxplotlib.xarray # registers da.maxplot and ds.maxplot + +t = np.linspace(0, 1.5, 6) +xs = np.linspace(0, 2 * np.pi, 80) +ys = np.linspace(-1, 1, 50) +wave = xr.DataArray( + np.sin(xs - 2 * t[:, None, None]) * np.exp(-3 * ys[None, :, None] ** 2), + dims=("t", "y", "x"), + coords={"t": ("t", t, {"units": "s"}), "y": ys, "x": ("x", xs, {"units": "m"})}, + name="phi", + attrs={"long_name": "Potential", "units": "V"}, +) + +wave.maxplot.pcolormesh(col="t", col_wrap=3, canvas_kwargs={"width": "16cm", "ratio": 0.6}).show() +``` + +The same works through Canvas methods, e.g. `canvas.plot(da, hue="species")`, +`ax.pcolormesh(da, xcoord="R", ycoord="Z")` for curvilinear grids, or +`Canvas.facet(da, col="t")`. `ds.maxplot.scatter(x=..., y=..., hue=...)` plots +one Dataset variable against another. See the +[xarray tutorial](tutorials/tutorial_17_xarray.ipynb) for more. diff --git a/README_files/figure-commonmark/cell-20-output-1.png b/README_files/figure-commonmark/cell-20-output-1.png new file mode 100644 index 0000000000000000000000000000000000000000..74215c780cbc7e32c48c6d6510e1c01aca936eb0 GIT binary patch literal 68939 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 literal 0 HcmV?d00001 diff --git a/README_files/figure-commonmark/fig-showcase-subplots-output-1.png b/README_files/figure-commonmark/fig-showcase-subplots-output-1.png index e943f7616ad70de4cc0b5f178c75ac0e19917b67..e37a7ac39d357421139d6e8bdc6e31a316f92a33 100644 GIT binary patch literal 106705 zcmdqIgIMDxR7pzW zHO&8>S`Zs~jP~DCRb-@Kf&V?XtnmNl%UT6FIXS%mf#zmsXXobDR+5vGTUuJCaz&qZ z_h@Nv*Ip13V&dXHeAk>&b9HrHUS8hZ+zbs3?Yh+oe_R2^pbXc^!67~|F_Y6wE{#=h zFpipyiHQje)>K!&xPYUhqazTAwY4?TC)$W`K|LucDZMr~ViJT4zAN2Gnm#2a-@T
    (w9n7a-|pyh4Tvu^?h)$;VfA4ZB2~{j2{*BXxBAlI<7cT6@yC+hr zl=D)=o8RUG6q=Tj@_l+*SzSFx(6u}+4u3Czii+y1X0`9rV@FB~F>q4{Bce`BSgM zlZSzUfr&{@QPFn3Oa&PkS@aO|1!1k#-Q~f>#f8IrEiJO;zLJuX6=${jqnnW=u$PAi zGJ~v~-0t3h%ObiPjUz!M(Av)E@{?Gd3nfSCc3k!?VkxwcrD%#rQpO;ov4CLgj zrt`$lRno!0Bs;NO>#K9wQSCld@*xio@1y~uV7qYj7re^%bH(pzqBX9gLzn{Hpu)w< zXbLWT?E2w|;N{C-9bWD)iB^|t{GJ|_-s66MWK<#|qHUHpGO@|=@z;}kZ)tal{u}%`(V4HBYzsc^9UW_B7E|z< zPlvz1q25jVRcY6o1K(Cy{AjX$<=uhu!DVmZ;9_s_+|3^casKvJATU3ZCyLked>e?9 zhMJo9RP99PmXW-y?DLM0tgNi4DC`wCSJx0PMFoXzufE=1LwPkdqCABz--p1Upq(~u zZf-5M7j9AE;d#vtPEMz%r%h`fF9=<={xfqmV1XBRRoZX|H@A{zi1+pJQP2r7K z6$^|Q{wH9T@2~)aBsP296da89`)hSI)DO)ev~Pa?e6AFFtnI%9eCcvn3oI;rvxkZd zy}#MYt;ogpj{siVeLDk0*r@WqheOO3%by>Zd!RfPH+Q=;dt)M&7#SJ)llE7%9334U z?0tNMy;J%Y?Q%tY44c0>Z#9{kzMtXm6p?`U(W5wF(39 zy-}OxMAFi7CjtcC7G3HGiQ&}v)jw3P4pAJW`v0-9v5}4@qhVqBqYVuUFQbcyiV6z} z;rYq$bL&FZE-xq7=>hxXA)&8NbqSS|yS-Jz#KiRQ@VIs}Go$ma^Jmi4(>o#g2Pi@R zyH#*zW@cF#!-GW@4CU?X*GPHOf3pV%2L}cQ6wL$I0^Ih0%$$HFB>Pzyy_1L0Bm^i!r0h&w&=~l;h#2jHML{g|Nl1bN6R7hBQkMR zwRYTmd`B~d7THhe@PVN;^NX|zarwWD%MVw9v?pqB*ZEog*U2OzNxwSWyme9SZp4Y9s% zRQTl!BR>9A2A9S8`T08*7G$C1l$0*zT)biDyL3WUtixgQ4OXkxa~@-*DW z%$|(L=3zKLyDzkY5(1m>5BPeAZ+LVzRj&V_dAZ%(?zKYq=eEvuudU2F7v1kFy6 z$B{jI06_3w$Jy^SO?+xPIkn$dTN-Wj*t=Eyv~YUre0*H_ck@$|5!ceKVido}R`nnL zupo;;rb9;N7JAktb~ntevVpx&HeDgp$IKpQfoBui!VU|&e&#h-xlQFO^C>>y>TO|5 zk7`VUG0#|&Jq~k~xEbu$xa>vS{_Ltg3s*|sv!ndECo0Z^>Yk5JFS$3S0+OC6_;;p| zK~L8fD8%be+ba_HT*z)F{oNn@a5Hf%{Lk)%O$D<-lfP~I_wC+nxl!|U4Q+=A53wE! z_%iB><$f4j^IR7~ z#Al+L20wUwPEfo16av<$(j|ZKB2iXSvcG`GdZDPKr0LGY_5KG(j%KDx!17EdD+m0N z<4~h^vc&N+v^LegKv7?soI$E1HA*=N$IFRD?L?yHH~G`2A#Zz%l(Ve#NZkmR8h!#d+~FZzS%Sg6Hv6f7=7S>;)y; zve*3ue7(wr+C6{iNBy6{>|H_Wmc8^szZjAGtuj=?(vdLx-|~9yb_+dDZW*T-w_h;d_o&Q2Ib;q;w_#K=}bcQ zPRFLfJ7Of0*!=gt#=Z3c`Zo%EJ15~*T2>RF!Tb^ zvH`93*m}NjS8wWdbHW2l(v3Z=YArQcWl5NpgI2J;LyNmZ`X{lnAp7t}_nHiuX(3WS zN(=+nPt<1eQy3+7S0XKcI9)oc_8c_kE{C1vY0S=8u;_m~4N}a8j?pc%5`O>Waxl>z z^{al$N|AIS-BZB_kx#mQSsuOoVDq;M{H95mY|!0(So({&E&_Ey?`X>L+e$gMk7o8^ z{k_@|-H;cn$ZwyX9=o$k*R1-|J)54YdbbB*=b;s@O#GJSB?=m*%WfKB5~|$+w~DLT zB%ie0!?s;-zgYRFiVj|fhY9iDiSPMFaJJH@zpc+~|A7u2eGJ&kaN6H!0PS)?ttY=7 zd-i(%}~DT0vfS@jR$o4Z*~&J}OJ(2$UrqgiLRi>}U;>(a56|4qqou$-Kn z?}a+BH0mQZHV$4x`G08nGL4syd?=p?|DuRpEst|ev93aDWw35>IdWjzSSh_R1m1Xd zHR@6v#nXS5QoZ?GG|l%8RyW*p?Ho8W)Es)WMARN6ApSgjrl9woG(JK!9DRg1HAOhil=!QpVHq)!Z%7&BdWsmA6#-7e%ThtHb-ba!5wz0 zPqv;Y{0^s1N}tHUzs-J+Y@#&vbyz%+VR-4^Ti>U2fpw1r*UXzE5{2n|ejZ+CdUstV z0~Zw86Fs;Qo#Vz(nj?o$Qc+g*81L)0Pz`y=Xa7uXtJd9UjO~j{wpCUOUuQPF7L+yfoVs zHc&1f#l$=sHOA~L&r}yV_7Q6o;Uxd2PA+gZsy*?Pa@}d7unzYO`CSQv*V|E{ zfe%_zdc2rImSpWmF+Yh9IHya~&SK$lrvK8%7EXgQS+)zx=s=!x0@*?q^W(( zm#QE4gglI`(w4W%uB4ec%H$W_{LsR+{;3$-Pnx2kU`W=nWJtd5H@HHCxe1*ANe`0G z2MgsL05Lt3iW~}Uh#xVsU(P|0#gMd+9FW41Q2R1O2OEQlN?W6Yyo_?O-$7_bdqEDH zLkjTem;AU?i!d?-=9nW6+~3-|A9R-vaSTFW-qe4KRDiQ5+pl7F#km^c<(de0CV>Oz zqS}+Nbr2EEM9}?3qtpJRL5O2LAv9XloPTpVOT}wmW?xkt(&Oj%`iPPQ-ZI$>Dn%k{ zWf5F8ZtmuM!10_hw+6jNtw(_lFr@Ma|qb0Q?=T6A>X} zhdI@ebsFp^XQbujVf>4_W6jTfPaIZIe_p*6@%#60K%D@}v!>2h|3rc6?>CSHqHBQ# zZ0TwHQ_0?eKR~MS^nuP&e<&FQd!R_4H@`RE#(`If7-m{MSwe3WeSTn^1iQq2NSru9 zQ&JJ*&dTQRQ&5_Q&@A{MSm>eM8Eg05AhyShOV4SS2O1)1bha&z5!EK*&~jX1BUqXAPpfrJQkSz%Gz_~1*3gi*n$cDd|eE=P(XplH^jDD zy=1i^^A=0%0cDTcQAFZGI1J{UE_wPvA1A%LAsm=$(`se>HcXKfYJeo0w7Y-sj)HZonvW80=Z@um3A7%d@#6t zJefKA&;-r^^JLlT`G&KTG(sEZ4>ne^!(VCsMhJbUx7K3Fh={;E8<*xe4&D??=Iw^i zELztoOnqqfQb2iWxPci>ln(C1%g#oExAb%Vq)|EvR+t*ON{1lFojM^+fa=F(CXnGZ zS+&nk`s;|PzW(gstH50SL|qpJMu>g#=4D7<*ln!i$ z7+Cbs^Np5gx~Ef~5y+5PZfNw68=K^5`Ed{^B*x#yAH1o{mkXW^2QM&wv-3QRfJrPG zT_T z5IPVba9ZVdZbt0x?gGRIV6h8;HkIq?>FMq5%|aOq>s{FWsG_83CwB*Qd&Op+B5AFDS%m~^90a|i!482$g-Emu6kTUBWp+z~&Vp^G zMoyDGBr-z4{w5+g7N`B=QcBoqykj6t(hyZUs?m6XjI&km<@GTstAgTRs^Q@A+{SKD zZSJY+*C?m*Q8^}NW_f0&GkIpG$gw${)d;=-%S_)d-Jq+Sb-N`=gF7} zTrk9R*2xeK80!^O7z1*W0AP~Tiv{`O1C3Du2L}FXKg5CveE1zxn5M!*n}_jcZiiHx zqK$CqJpc;M=k^c&d_W}lJ`Xj+Za~C^loPQrZ2gslySs|v`d^3 zoCBbmGu#pniPoXF{I7UTdT-WL<}noJsJQVqIBu_0ipn&W;N;eJs2l<uGL`40cv~1(! z{(|+2p7hN;LlglHO{pv-cD#L{wgzwi z7=JLUw;0563*)%ILexuM`6x*v@L2MnYDqZrRGoP&`T6rUwckA<5s{=lCJxT|#f9z8 zvHe+ajz-E2S3l^sj!q{Lor%whT7Nj6P>EPL8|DG6s1f^1(>eX;Rgk;Goju|l@z>$g zw^@%9&O7a>%F^*>GNB=t>vx6upGQ%j!CXt|&Ww&2uRIk~cLGWvb{FZAr3_jLh`l@` z#7hvp;p=aTif+RyNtALvrRC=`?8y5CLa^Jme6H0QmchwJcwM(IZ`NFanH{r+XO-T; z7*Ia$J!{zPOB+SS0l&*{iXG}Y5&3>Dd#hXF&jDTj zzme+)RGZtYqo;>3zbG(H`GEm4RPCj38O*EsJjFD>Tox$f6n=jEQXu0GMkpPH3-(es zh4Gn^LXlQHk8rJjYgS4?IJ{~n=F!pCs#>zZCSIL)O}19`%p}n3;V!{{RS!^fXu3auh@rdhj>m zT9qDC4*WA@6Y?;Q#Z5$?NsxY#*y+7BU&($*Yzh!~PIGWnJG(=h*sH@_VSq^4;nOcZ1B+!6Ykn+Vs|40Q^t9 zNyCNyx%uAC4(;Y@wRu5rMC>}naTy^~kjaL?T(41hU9Pl-X~~5BU5)_0{f-cBsN$*m z@KxqwFw>x&qLZj;Tl{W=sk;0suAH-m*`p|p?S^)%(`~ul9eqK6Bg3Py>vlHWx3TWHjXMdxDcKF=wdqMT)6PbMW`f_rSWo2dA*}nnx%EZ`MN1$B_{@lF+x90lq z3EoL?h}?!3dM5akN^i8?nMo1xdA9MvvDBRxfmy{9BL|760~IqEeqN-6tbgwgWhKJ* zKgm*#gCaUdsqYh{Lg%!A+^%(gvQF1%=hrdsQ2}=gSQjY~?u|pX~p7 z=MUyQf{&`Vr&oJ7YQ1k)&@Ab_rkMa83pNCSbrEM31>}(Yjok9xl%my_Y$l{{jHw(C zeHe~|r>$35*gAO2Ju3j@wZV>Lv=Hfbn5$<2_9UU#ZJE45qDMl}Y8Qsr0Ov{KtF-VM@J z?4zp$?thq701AfnG6@dR(M)lD0RZr-TErh-ImG_2LU|&EYqh$3X>N9xG#MBibMWD!5A*>sQNpt*Ud6THVP~Uj2mF)^t>4^m@oB;^`fk7^L~)BAk_YI(lp1Q!A%aM_hFl_&U z;e%&7bUR6!vBY9SMKy0qewKhz0WBkhNE8SJ2D?0i z@=*x(p+Vgg^u@9`)_!@nEZ*8$twvlsmu0AD{;jQOD?Ot!V1l_-N`7qDHE8P9${$s2 z{H2oB10tNXMPRNZON@i4dpR!xXZ7z&MXr=Q|E9jSrha2Z=<(>Y?HH8jV}IZLk&cQR z`Yx%_q|sDK$O5HLAoG;JKyp5aOXSK zH14~%jwzkatAF@0chE(De({R!?N=X$itio1ypU`dmEW6t(e-vjCCaV$A!tTIn=CfW4+XZuhZHe8QwCh^^3*GJ3K@E(xWln6 zEByJyyGvA{rmc^dydj8%NeI2Z2!WoOTm`q`)f%TJRIqUJbsYy&-?~J|Br)QXO@ecb zl|1(6Op6$rL`&i&s4kkUqKUe((wRDAJ1G`dmwm16X%kn8OnQUgnUuUons9l&)vvF( zAg9vu4DvI;qaXu;N9hM+?FR9U(-zqj9tjNzUntBTzlWUAK*lmdz_fM0MnGpzyV%=5 zJ#oO0nGd&qYY|78N5cv?#wDMgR68FRMoQEiJ+9JfNc1d4)xL$eE+*Gy5%e`#f8onD z6Xkg~UCX4w!`f1rT5nLtXxlAv*pt%rH`6a33mbbKf$+W9eh~W7R3gG0lk)cU?#K1L z`nf%9Cu}aR)eWl$IBpCd*`^$s#KoH)`n?u(Z-aUKFxYTD5msPEfmYMyCso;}Vb z-zoacV~}$>rb?86Gts*g)#K%qWoL?BD@O-njvRvQU2_=*Dt6)vJ&i?ng*F&dvZwP# z1PA*r%IS<&DvmK=NK zKhL8|exwMj^|v|7oDWjKq`q;ghham=TNNFLQ!#>gS6z>dP&HH z>WHg?F8?|m=~v$Diw6P)+50}a2y;Q+Ka~B@6xV&Rk%$BdtKoC>YA{ zvyxYn_|0g$>T*SW!9L78B%j-; zy;pwxZgOp37+H6leElq1HuB5VL`C&Gi$8RnoOLB7jqWx{H?e?`R$p6Ndze>J@+_Cl z%xuFP-+ml)(nzk-$Vd|orp>WNt+Z;778Tf2)BdWS$)w)iW>5kSN^ zy^+X~s11eC-S*{G&&7sVm{^d>Xo5IFWv@!S?M<_~+4JR6T-#sr>$x9CyT7;Sq~@Ko zd23aV8)wJoT)XZq4Q8gUYV>W-4p?gG4TimmvxyMyc(Zo8Of=KCGYhV#&Sr*rR&L)+ z%q})F;T-myg;?Oe3xUxNx3bQLAR_z${lWz=2>koSsjab-LNC|Kv(he11+Sf~`~Ej7 zlZ=-n-)DE{+g9Iumi3!_Z6(zoFu8bNN_@3%)E#Q{%DpkuW9N;NigFCYAT=L_mli2; zEY_EvZczm>%k4`UGR9}8qj;+cC?ZI5+fxxQh#3|lxv{L?I2qW*Ut0vdef`{D7FFRn zgHbwJqo&EY_<^I$iW0vi8U-qM5R+7O*7|7dK(nns%-?GPA~8UjWsqk))8`pb8Y5Aa z9{Q)j@ZEAgBBf{}4QuKbHw0TBd6kKc9@@Xr6b2@F{G<8hXnw&2Z7o9S#ae`h`WdG7 zlF|v!>yZ7M-1V}u>Zbxb)nUiF9wwR-$d(sH+IOCd>hUPPY+JF&xzh}~ja3DF$s^iKRtoi<-4*k!MC{sS%uV&{dFqGt1En|n0Z6!ji zYAOXBT6ya$`hwkzRg)+IdR;IY%k$O8W8ubL|EZCj>%9iq^DnDEFO28MBfpW69S7I5 z27wE7$sq|O`bor#FQ9KZJ$gkOpP%&)g;zAw>xo=YKo5^$%RzZ1QQa!A?D z5)yc7*c;%$?=W(l15Lnnu6_o9Sbk1=MG~ch`n%1Dk^#!&=>B))6BQ2iwn6bOu)`(} z0lSEsWkz&vXc19E2AJgO4{3bu1=$B!K@uRs0oFR^b!U)4Rprq_zUNH)zGp@~uB@Za zQ(AVk8+`Zf{M+MLho%QNflX_=@z%%X5-wBH`JBYDqN&$!Q&ST!K5{XOO8jd$@>_kR zOv(Bw_F}~GqIiJiAU!ozL-+T~N8U9|a`KESy|%g%?zePwYiHkQ+drH^5?1d7el@cG zC>b33n-oQ5m!N$kUaR}06p?C5$VzUO3R_i|euIC;^^PcuAHZTq3xk2O)ARsUBKC$X zE-$m)kM&fUYF^A@0T#(=2o;0*uvZ@k^q7z+wNTj+tQ+k~r6sbN-NDb$L7wlGolvOI zs~ID+)453U|Mn*mQ|<5TXHz_8@uN1|d^U!6n~&=bL;=X2K2UJQEXo9^;YiSKX0emt zr7g6s)*`V;Fd}Os;=I>s9JH1}hujT{xVfzTM*XH9=RFrt9P!+%ZYQb`o<0ASh$(tv zWLUyNGu3}mBsSALnh;Q5adJ$&*g0E~1%PXP>l zN-pz%?Jhrl{J^6WoLAMjlpnf-&VOI9X6AW4Fpy)wnYiTFq@n)lN=la}OgaFyd3H(j z`R&aaX>s#Sg+BnbAkVyR?IC^tqWL%t}Ud)h&Za^|P`zmIATZvu=!ZCU&i1KjIp~ z!``<6ix0h(jgE>K*>eut3Z_|PJVQYB1|+^p#`bTz$*5GWM9}X&lxse;qg71eU(e5j z1hw^kB{`ibw-Rwe^Cr{T>{4rpqBt{cQK7*pVOEX=?+rbR(Gl9hss>MY40zN1`*ycbmHP>3|y0 zaHf_&^g#?Os5Z=XaKAKOVtXK5kBLF0_TfZX_oVh_IZx3Y<8+kv^J;=6(JQkx)&>cT zTu3AfK9VGojIz0LGZPbm@G+KQF*5zV z4Lte_ZdF~v+r+1s52BH~=4#^sV^h(n!$cn*wT_hKr&@rZG~zULBfoFXT9*-NE*u|a++T6kXZ{k$ zl3f*v08+Xsf<%~1p!!;7pl?$mz)2o*C?rMRgLbM=rb4Bl``Suu!X-U69XBp82h^nI z1hD9t%z{^oYkwtwELV?%uBJAa^%zw_EHlXrbA_;8iI5qarA49>NtY!UEj#W>FzNt^ zy>6GwLt=*IG4?%9ooQyM5%vI;?aIMx!`u#_JSs zr1JW%0y@;23O#@YGKn-X;w(G)v3EgABFzJ#c!%hTWOW-f`w#^rGkWRgoRD-Aybf}A zHNShuzK9YQGc84wt4W z>M^1H%~9Y~x4lvx2hTl@-Ri@YCl<;1$mh@o75L?a%r|mb8mD7%J!K38fS`KHf)Uvzw<&w_j9F+CX>_}zdh!6R%aSc^H>R4qj5>B1967NJb{`3 zAZKerRSgDl&8$jvi$!(v-^V0mh_UAj!9v%jpQYEl6rJ*%_4daM`zF6S^Cz|X{I3@7 z;(C{dPYsfL*|hX3d>e2vClBOWn?%(SpEq+p3Sg09!S&fRNZEW`zuq6@L|#RfY0a+@ z+coLEvchn=5a~9|${dTc) z-_l*7nT|9>wAzvQF7W@|Ug~}jd=zR_UO&K{nwFpfJVC=B`b;LxT8VNhzZCN>xWJWMmzDgzXItV+cfBj1t09#IM`!4L=# z-1laSqe4Ta!P#GnY7*{Fg$xTiJ0K?yRtaEZ;k{l8>xT()mdXl!wM^j<>O-?uPP&)L zbRXAHnv~zJh<&7Tv~cGvfuMOPM+REt05sW21#pfUp3|V0U!+G1I%LIU+e)zPBFpCU zk(HrL_;f=RE|5PTqC)+qPb!|cBWcL|5ZP!-j@Bx0O#XY8Nd;vph2)I%Xmi0D+&Ndq zJ^|!KPEd9GaC<`7jY=`YR}TasfZ~7AI8BxdGog^7i3;WV19l8a%?M=Ik)Fn# z1fHLmKs%z7FZr>gBnbkub>lMZc-)aWN;G+d2r;x$Tk|!=QD}hjuACSVn=Qdw3f8c=l$m~i)>^YwkZmLf%v*=frb2o zYdjF4+QCJx$rV1)f!6KinzPJLY8|WRS8HV%DIdMz?oLKxIw~?SW2r0O*7>`l6ted~ z|+R|w&&S6_i1 zZ~OLGcv)R`)n?h0?4we8*^?ziB|8j8EA)k)D-{ue)DKb{J9r5#genPs=Vkl)PK_y= zc=WAU>d0jp^1c9c$QA-+XF&~a#_bVf?2XeC8eBSF7sb|YOZM^qwz;fM29#sM6 zh$dpc@1o{C`!k&pn*9D_n3Zd>0KtH~)r+E^-DD?#6WeM}OffmEfTR$egT2+9(>d(B zX*_S}#nC6Q*4N1EZ;z@=%mfl1#`q0#H@A3m7W{$}n%vnrz-^*BK?E;)G`?z|_b0FWz#jvs%u`33r>d9g^lm5$^>&p5dg%JQ^#%&C<&!ad*WPe z^8MFHp6QW!u3-q5zvRK|VZ;(ms5LPPUgJI^ST$X-_uz#aa%~GOcK*!iE{GUH|z)PA3mnXOS0GAx?!~f8lW3Df9`Z15 z#cypr3V=3WCZ^Ga1zsAO{E`xSE-sLLby=D0hYw%M%X|HNAMd{b3EH$OYVV$ zBHK#m-p0`xh{nUr41Ryp;{Z2FQ6aEF2;3KWx!fsEae{kVyVZZlZnHgos) zp)&|@Y?-1fsLQG}CbSrq!?L+?L@KCTwi;L5aP9Tg@skUbU5vWobcx1H_tRv5vLrTW zzmBvPCfg{4>BWtdE~8>a56L=8;(zxsEdNo>tnQkO?@M0y}s9wVgxB7Cm4vV7CH?>#iDE??8V)(A!+4-+^_vwXwnDW!{~9H`{C= zoSfF?=n6g>jBZJfB4q>m3t08ruZ0XEbB93aUx*mj&GIk`vC+U&04wBtO z%aZ7K8J?VUIxnQA>=8_HI6EM~CXA0lBr5;j$(b2y$|DMy9(-1``uF&+p^`VitE4|h zMuaoH8=ORcEvCj1nDCX|d^~9U^9yOB_wCfY*d}N-&j}(1TE1z3R92YUGNSe^>IGQE zG9t_kjrrJg)z51yYfIhwb4ewY6l&j;)dgklGkCKGrX3*ZpqNYKG}V&NEjIA}2%Vuw zLPP*M4lD&|KuQj*yVjK+vwfPExa#)~(=r|5jv4kOO~gZG!D(jS?ww|~C1tS!Eal0U zVcG1@1@m6j$d9Gv%BfU*v=WQlaHq(8jc(;6bul677Rhx4&f$eK*l@3*p_2?P#|o80 zYeNoyycdi?K|+L!^k17iTaSAcE!rdZf`Ln%oPEOFK#FSora;js@I9enuWeAB5Zhb? zCXUN4rB0#lIH&5Mi1ad}(oBre;5@b*7nTW;J1j3{6#M;b6m8i+H#_uiLjqXiYTI7W zTVszS_E-`eaV$BMOrY(TUHV2kvU{897vs%*P2L>av31CRX0^}&tlz8Jcg5^y*TT^V zsLlQ8S$f_RHwVWzzbBDS+$BqR_X}d;9Co9?$VjXX^mNqW!k=6#DXDTU&VYNfJ{WCo zVNqRGwXm=d>a3%sIN-mN99hRI34Kd`zNDR_Y|bHMZo_j(OabY*O1SxUWd zH%67{3qmHb>Im9WBXpUJgv#i~72oT=E;!gfBo`_?XSDpHOgZ-E9t!Yod04O+2}TLA2i=Cyz=n>DMV5>?d6-$Z7kxkRLr2t~TmekVe0Ej= z_=D#|j3$4-@<;qYSx2{CjRE=71jlelD~R;H#^lG$j-pcrOAYXi6mr^hP#lAZ8>3HAI^^I zZNmc~MJn*o+VsSgjx?<1rPNVT@7kk(^U2{}X%NcXHeGLdY~G6gfGGahWx#S0+^KjH z#CIb~GyX@u%lEv|dXY-VEm~izsHW!lPn-L7Q+2gH2;Sm;L}g=SOEEbz^38F5uo{|` zk-+Npz!VxWWf}2sdzq4)9DD$5{X+r)He44q^zEW4m_7B_@Hp6~vP+;Z(Xuq61foEd z9|CbHmggS8w$v<{|M0IikRBX&ZmZ$ovcYF7zxXk@(M3)ZLJAJUz{tGzr^!{3#FFJO zAq-+l-@$_5&)R53i*LM2UEk_~6O{G!yZA>8;5q-G*kVIgaMWu>i63yDu=2-Ld)*pQ zr?`&@V28SG0E3;V^_bK;R0`-+O5W%hF=!f9ObCek^m)XpMS zpTQZ1%UDNBJql`Y8uRizP@0GP>8OSi$&-T0rM2G?$x&gpjINhR^*6??ZAHI_l0BoG zJ|Xqqs8^(HTp2vz)MVyY*(fagZ4!~pQfCqQX@ayd8(Y?K!h1ozJ|j(Z5~--gkn`d< zhJO&F#WGiSL5LWcTTI?KACG|4eg0X*8LVf@RxhkJ6T*L(Hl6ME%>6&lp87jk(Nmm=2EtZu!>vB=)@b zwCNO0X6o@d?I+y-7JQToTT?E4hqv;9wmLtv?aC)yc?Edxwp=p-$J^;t|JfhD`U=F# zOhN%2oZq=Czy2B@*YYCU{Ij$HH951Kq_IhBk zv~pawG8S(L+eZowqt7EdC#Nfn#upX-+?L@ak0k@Iy1as1y|8R_{i4#?>cHvmXuFr- z`%TmGAB3hw-Gk$W!|CKrX?!r!5F(dU;{&TJb)Y<~bkMKEA@&(JK(NQ*W-tNGX+UqY z`7VGFXO;B%HQDUdDhk;aJmh4?of8e{ymO?q1R1r@@mv6(YSN}`Dp*J;5EDj3ZD;kT zy7HBT8*O@CBIwh)(t&LI+hiA8ztf5L!vc<1YLk6uH=~-4W*9n!zqiFVFsw41yegqp zw>-<#nWvIEXgck0_%=R@Z!SU-26TJhgreN8|`AIN$QVdxVnSX2&VecTb3uaoIIDp>FBrw?WWpj zMC|H0qDDd5dED((vfA(6(4?*KpzFcRVH^_SR{$R#vDGqH{8ez?%0P-FScE!nhI!@ZXVAk%yY&pufdW$Uy){M#3}ClV>I zJmk`x4|B&_y#)1k(e4kSolT-^5ctK9mmcZ(@wAfhV4#ECaO@e`x>1qETPBUFqB3KF zJS3!m^}u30XF@IZL=p!38GiG31)Dr1OH;_9hN#hb6)(ngQ}YDz42hCcSO0duMZQKM z2r@K2Dj9xX=0&_)=@)_=`B*x$Q`i;At%bbAk?#!^H7LZN;_6T~FKP+p_s?@avs-8lJ9B(vHe z${IRt^k4vQZwD7!;U5BNT|Nxj44DKp{itl9Uo4`5(jR*jdsIQ0y;lDxr6Ed{jM$va zF>zqT`ZIBhXbIM=0`%_H+l_2QG6ab}@q5`{H1@f>V}++pnr!WI6n#}#-cM+48 zqR@(+2mDFd#uw{AU6}}kKG7!TmANo&Q0F%<#e=kt7nUx~5mQ~gBsV?ZlBpPjjLUs{ zh#ORZmEw#1j`Mn8F2QA|nSp3)SQv)St7)TwNMc}PEhS)16ouM>E-rvAn%0X|#YYuo zW!E{d-rJ9kBGCyv0*pLABO1wPa3O>nf$uVYNUT*vsS;^S&HUOJPGOAF&0w=b=w}S4 zGcre83ENRfGDg<787px{6MaUQkV2>3s*pM_eB1X3R4d-g2D~61(?n8lsX`efihFy zwpy+#iC6rA0{u{YG9qm2ELT_D+O0kD-CRl~3~X?7f?2-by@?s;9uBS{=bCI|<)~rh z*}wCkO+;Wm5t)wkLwXvNSDE5W%|=rgt*M>I=y&N;3JfbL|CAhd+=PIyxWurelbA2> z{E3F)5%GbUcStDRbJPH~t2zGhzI1)2HQqYw-QB@Z^=6JPz8|78c4M6CPtU+Y6Egb( z2?P{}ja9u=3N4|EY>?)1ucO)1KZP+i>D|IxU;YN8eec^1aHfEykuHzC^kw|~GdA7@ zRN~S5hGQi+1dL#W59h%OYFHq(O-D#sU$AxJPW}K<{>LB^ceTxh`(q0RT6Dh4CrLo! ztBI8&Kiu%RW?ftuZ1c$Ce<=3bOZ4mTg^JQoqwd{HxwxFK_5__A0GQ#3951O7-gOn` ze(^oU;WPjhBQhdlP3UWRIjdgldCubHM_6x3YtiaQBMn76raDt|K*9}Kq+5G|-`vWM zG5>vZ>^dn6`CqQ6Rw)deHean;_(YUQ<}=Bu{1s^pkC%|if+nM?gh^;_Y5y02_P8fp+GYdI{h*&j(6<{Yh{Ty^^5))EJ~mbA+*Zk!{^by5 z6J~X` zU9URj!h0@CGWz~^Z;EXTH*yl3B?g2({2>+K2uStM7^khbF?PxZowg@sm@AZP5%CR; zzhjzW`NF{Tb^6*DzRl;KP-i7D=nk~m2mtbdhg?Ky+NIb|WNJ#LUfUy1VXwr%w#B?l zL(z4^%hrYU7&CCS)a=f!u!k|Q%e}n3{5)4&TpU>WubWCs!N+LA56FoL2{(Jy|A(fx z42YtCzlVt>mWHL9rIub&kfl49r4gh{N(4j@l#ZplrMp2uKsuC?5QFYix)p(E?(gsa z?CW{4GoP6&&UMbYdQBPBJzu=2d(v9H?$auMISLbPa-9FVt`s#8Nh4&5frRn!tlv^m zQev3(oG-k^K@REvHx(Z&5nKYB!2)Fi&%~;If7A`c!(b*{UMS$(1EwSZ^Z$4>oiT#Y z%*)BT?~_h@(<9NVQu&Khca+)<0|gb9f?x_>CF`8*utr#XiG_hj6_KiZr0WcT7X0$6iEFP|dRy|3ao>gW0yaFwq({bPdYBxVa z8Ca=j8Dq|5O=U5uq2}wKZ>#u=3ECDVf^qQqImk`V6Z*I zD;!I>UU!}+`8a3sQ`tTrUvUSWGQzLgOJ)bW=Q9?+qGJ7v=KnVF}VC& z9Y;CL(<8(gA#libCdNRz9$NhDUGs*Cn)0jOHQCHOlM8d9gVTVsp=O7102=@Z{I1WQ zRTUMzI9XFzYSJ_`OpA}Nd%oIpB7}h#_dDR~fKKi$!VMV7```U-Ehs1`EM#p|1ccsg z9?JwLA-b8VXs5@zM^ul!cUpjbuUD_#6)2^PqU>%r3y!BGopM^1eisr?nf3@GL`BWc zPxjJHYlyAqEOi~r|;T6hjK$|Y#B)#r+>wA(xL7wfijGI zqBo6D$n;|+RBrj~xnyioFHiJ6qA0>(lZ{Kg|})uz69!u=F<<`O(Z)RNQT zBLc3E_YV8WVD@h7FiC24EM2S$Tc@P(@lNjg-vyn|4KLRW&MBtl;g}{^KfTA`TeYY? z2S|&Ulx5wftf&+w{PZ!a4ma%XsWWcLN3AwMH9*)=pW{#LL$^I94JAF5ZYjo%knzg-kp{qjrc-ReTtpMW!v;9jteG%IWH zQQW+)k&(n4d|Y$Q%|}$n?|L9}^Hm1=>tBCFNCrr&!VlV0G=yyiv-que=z=|gvg0FF zqV=GwpGX{0!3+~tCHR)0xUKq@y;L%X-wU?S>oUv~^!C*OfGh#1Bhxa)3o?{S7TMV| z+~YOX{TO$PK9D?aT7M`6k1H~A%`ED}?Q=yUO#Xow@*>49@>@*yo00^Nc&Q$lol=Y^ zMVp~|K+fQS;u&xz*ksAiGA0U%M}SN4481m_5JH8Vjt)`&PqW@UZQ=W6A29(ba&8Wr z34{^@juUJ<=(Dlv4KG~eG-t`2B6*G(V^X9(<&pE@8KTD7aQjqH%pg${Rr#mTk`jra zpt3MahGMY~3y1ZEbzF*wR)L*yJvtGq8W(4bm_ieD@5~_X*C1EA{ zQTSod16El@97k@bQhEM_lSbF!1He!BH$+H?5V|{fETNKV!x*;iiS<`{Bn;AXgNNrY8>sqc65}rB)j@ z(QsNd27bzrjFI&HXNK{FYBOFXV^qgfd_>!0m-d=gtG4z$`IcV{lvQK2&DJl>Kcg}> zJ}Jl(bXl7oR51a^H<-lYAI)NtibW8P?*Xcow{Chz=r8fXt#7?I`#lOnNpF!{RWATq z5YGBUh;ol3@S(hN@ZZcgkQ#t1zHsv8w3vJ6=OA>Z9nGYG3y2dc3zbf7?^FL{PU>OT z{QR&s2K*DgRlZ_o;~y?3T}cqghhd_YTEbWMS=_+91fY`iQ4jXiMGf# zJU$E-d)b?|yE2Nb;>Y=K-aM%HW)?vadHDWQ<RWXrJ83M-N&JI#V(> zU8{w0<#In{RYh5&d0|#K&Bh}oRb-5mmSUi8rekpbeIIzrqt^dm0PRKLfx*XM#FOQ> z-wk}@Q?9+(k71$!+^y#bWi-Y6>4R|xfMx+)vK1+i1Tal_Mq0$ca9zo6I;sq-=4n@RDE-oSu%m6 zEC{o|100V~4=?gzd^UZ``b5<@EV$mIurQzTCa2Cys`)41A5N8?=lMG9=YhPgw#7X8Z}b<)%c}KOI$`nb z7wuA9UB*`DAdilBnr0#)+>H@X$?g%kGO~}bM!r-{l>HuR+~4`8yz4~w?Wa)6549!F zpD0^8=mzG&iB!E=I`0EkZotLL5v0X2H2H{XW((dUhWrj}JPhL6HwRdzeWxRIH^E9F zCOYCdb z*##Wtdy|iHSjx3f#3?o<-JG5@J-Phz`l>s{DgKe}`))_-P+R(iw*e7%bK=Sibq^xQ zz|f9Pvt6)A^f;Gc@!hl z%Dyw?yY&Oni32?&V^Ic#taZa7Uz{I_HH45TaDiU`TS29hm`b+xk%3@e_ z0AMzl>-P#>FarTsy-ijjfpQri(NR&^s1CDHwp?8sN=-uBme%nqV(W5tYoSP?^4j<7rE#gZp*ep93TIu*VGp;j%1@d9u(1GZ{{!Y0>Gkoo9A6Agjio=6e}eb zg{7kFJzwB?$YLTe8|JRhhyM150Ynow8Llg!J_l;_QzgR&*jO!-Oz7Kt13)iS^ez#H zv-jOON}DsPo~iCZAQOBmbguk@4Vx>)?zgM#%O7Sz63DFzVJ!rd?kQ~JjqK;npDL1U zG%BpEOE6#5oGXflU@Ys|Md5&6j|H+#`_>rfebUR?Gp=vECI|nkwMCEfu&UKRzJwZW zm=d&l(LP!IceZjvsQ6+RgvlR_3pX#6_iJh`iLhKg z8Q@^^=YRS{@_cgNt>Znwh*{SJ-uvM%6vSx#V6a;*{B3ln4M5g#Lb+G`88zEY{n*bf z#BR-%mKW`*n9jt%0|r_xuwj$4>dO~`UAAJ#v^0|eQjaw>KS0ZKFT&RpIz#fE* zD`fIsjC>;&goBhh3qyG=npZ7R+>t~ttXiutSrId4z?s^mWq;L^{&k4YpvfouyfZBn&&?ss z%8Y#>B}K&L2fnC2Wy0Iu71-a_`}c^`Ki+R#5MkT|InVp&W$zIAut_CO=0E$DT3pR~ zdIroI3Y9LD6)-5HtCHJ{7xN0sNv)c=_-~ZMeeG96Fe;)bFrvq8o}o%#V00tXHliAe zGe%TB(i4=A6+63c6@uf50m4ui7?z0@h4vSn4CzC}uL*8*sCTHZ;G7C||$ zN(ssfDxEwNSS)$$n%^f-7tE~>Oj~`v!~SVU0Ztob(=t^Uk>-n87O|C#sh;)|pwelu zw$e89oYs0ae#s^Ft{-iO!$ApWUCB^gv2vxjEFddg=F#mYl(nG^@81Aa!I(@zYZ!%? z^gxn)`kpfl5*^gHfPjLi5l1cw%5Qw9c+!wJvw`>=Bpa zyoBkzX_bD4&;At-tXX!{yJcS;Qs{6QA-Xdjwp;{*BL;@KtdqI?%@HU{r^TvII3A`z6T7qrl(8QT&fHlG}sg9HrK=2FlVh>a?qM0{SA zkk=0SJOm$x0*y=I1YZIf;&L3Z^qv2f4M@iG$uf6*UVW8-U7hqYI=MOCQ7^(wlIWa9 zHax$q4+{$)LvjreX4-IB9{fCAy)I`9re!@jy!+PO$N*Zyw~hV~@|ievjiVwybgG-Q z!1sz$~2*1sAF+YG} z^|+UaoV)B_8M8bce|xzt_B|+-`4xG2DJOopn&L2AQ zD;BI7Xnm4}XV008exG)ZWsK>YRu;eg&cbX&i}EFSnl0iEe0OMD@uuS4M}$TmTXNkQ z6+WCMrxcndBL(AK0l4_AN~&2ljTntd@*yyVH1ZLzkQs#*8+KcKuriycVdpnAk&7k! z`O&k!v^>0B?+^~=r;&J}QA&qG--k6@I50hc8hVTvgKKMsptxK!-Ov%Mj{R5R-KYww zfXX)Jvszic?zSpveM*CFKwV2x>%e z%bN+N&vST_rx*}TX0)df21MwMLCZh7Z1k%lI>Qi{i=+)p?r&vIZbT~KKmFKH(Iki{ z??-RHP}dthKn7Fyxm5Q5G(&F-rx&a#k!g9$wga@iuPL{MpY*22h^`2T9x#|&k#fvRnU-l4N* zNPumRH=_Pho~a(-D0P#L!-+J05^BzpsDD30&YbAHT~96DNWU2p#ncG`lqc1m(dlim z9qwUV3IJ-TA^=sHV6V@Ou#ruD?NycW|P$?vI)_q zNJ-nIV8&r4WF}|MZYU(`{ium4%{#5}Mf z1M6MYbt$)B{oN13Mb6C|XHtni(F0(SCt(p#nNTkW+@gUQKbCKArN>$YmoV=Vu0P1D zuiHfDI6ihUS?sND``+ixlI}Q`&XvfMT5d7CC|#LjXhLrkE6X3Q7Jf4}VKS%@k<8J;t+8>n z;d5JgVIvCm$sl~TBzZ+l2acj(ugMo_-i+Juk~~o>qgSiaCM7d4IDwy0MM9$3?6vPy z^oxh`Usm52XM_%uWa>L&NpLHOFiBo1MNZ4b(AxMha)s_&e|}+4C>8N-= z)iF?dV_;yQWM|;6#BR00#$8kxQNp2#!d_i?7QmNtc>+~m&xujDDKHI`>%s52$o)i_ z6@6y2D-k{MotS2o2;v&GV?j9igG2wzc@JF{==2lE2sqIY+Nh36kI`1>hZ)!>O@{q) zJ!}nrK=zr;Q)&tC$ajIz_0ifroNxk1bfD{zhBlV<0F3v+(kJAZ52ie6?W{!6h-}rI zRN1%qKF}J9HFH0j9L9~o)m+-TarW2}f%-%hJ0R`oY`2sqBByO|^!%TD8n>Zc z2|~F2Qw{9wM_pDga_O5$aTw%Vy;&p4(s%-OT&2>0swDE;j~}=okg}TEt7F^wg#~&l zs`R8J)(1l)f1@tg(2uc)9u+Nb(N*I2ANwPK-@k?v(F4_P!Do_wp)ZU2s%MJR+OX)0 zqg6{sK&AK|NH)b7Nz!kQDKHhSLL z>}nJ4#4!0U5-~v5(~8%u!?7_8dui}2*Tb?VJY_t0@yCT5JD9YI^#P5)NTSHa=xttC zPaaURp}pZe6&cemSSg<0l~PW+2;m{$8ACGairVG~3@*sTv%r>fW(e^{cJ??@-9Fdn zeA@1Ox}RSurD*9M!Hg#%W#2DVS_pB#sObR|rDJhYF;^tM=-?2Q!u;GiLDGOIQ}PBI ztBSsG;C@uYeWO$E@D@WX8P5f~_vOH_7m7UkqG`9e zLASqy<*-V)0Eqytg)1-2&3xRDGWiu_ic#1c0#yj2FgcXJ`BSZ2-GhYHI3(=Bq2d&xy_ji2PrN)ccz+ zH-^wb_vZx!$Il%cUT%IOwzJ!;GN{1PV3hI#z0H??bp)WxX+m!5{>4we>Kl!9>ZPGi zSrcsql`+-(!ztH^cv1rW1yo$;u;+5kLu+jcJg&t14Y4$W;P1*yd`rTN*)>90O_Lm{ z8n*{Ok@{hh1s{sXC)inpTpX&(?v1X`fBdWt!eSCsj)Y4aJ%8J_vcbPZ=c?@{BtG|4 zHO`ZDo>-HD*+P3@wTpAQnDa)qwnN}6c~Yg!B=$)q_ADbCqju7ovL#lJZ!ntT>n8$h zEOo+Erj(t9u_FY^>?bQH52-BhPcf~gXU8#kTnu3Pl;4M_jbqQ62B~xcF^u}^i2~jO z?{J^7d#0&9(^Tb#rC2b|FJ5cc-od5_UFXU8q`cq3GzzBPFQix@_}IZ1jlo{3$hRSs zf%;K3YYR0B_+$%g^N%^`*`mm=)qSzySU>DynQGJ%*gJ-Zak3m_=~DXmpQ-EmD+`uO z!L%_%Iqk|zG8`Wdfw+R}gPU&b)q4FmhIegg0xOnQ{{@vGis%`((f#6t*Yuf}6)>a^ zWAbG&?Yb|}Gr)}VVT?zuBW^=yLoLr{ccu!a9{x%R|9zj6v#>E7`v+Tvfq_Bp?;iV` zH*apFbSVE^Izi0L@X+QyQqF0jfQhx5n%ZAc&(T<+o-DxiC^Pf=n*s80@dNVDKx77Y zX>+}?*fuIJ;a5ngGIS#Da3n#MO$B$xu^iOG>DM~k`5^^{V;sn;Zz_(L_2|~-FE$eT zvKKo*s;yhGx_!kusI^}ga?UD2^Se&`SKZ`yv;4Y2H~hArrIxONq28DElQJG&DJ*d` zPx3Gy_YV=I3^`D0NLsKz22(zjuqFTO{>|5quIVaM!59M~P~=+&;9e|izeDR~mH zI@_sYDeNU%2*KQYcXaUpZ+);Hj;46LGOP|BJ@FBOX{gW--Boo?@WuZm&=MLf(AbxvtoUf5RpycocNM z8w7Z1M{KsXwmw{a=+!9uieX;!aQ&h8Q>Wya0~0PT?(%UjV10VO3;06oW{Z@imdas> ztQEAJwGuNS^)wt?m6Og!`pHyKk0C(k0e9y@>th&|0;js~tl!Q@!m>LUjVQ=FHeY2ChV6zs!{E4F0H}`30kVyF3RPKI! zE670)m9qwXppEkYlO1g8-Q^U}EVLt;8|jL)z8&`hQz8Z_ zx7{D7fkRliMr89`F8oKTJNs%FHmvqbBo%ofl|^ddG>R3ZUf7;+3#^|joV;2X_$lAy zBli->b$RA22L$RUf9ym!ML{({+v-ZgwAo}fR_+|=AzQ9o^3xm^Q?T<`VXseX|#Z;?umxRk@J0 zX76$R6xV+MWN?4K>XsJ6o5F$u7KHP2hnc(#vGwIwYrC&L#mxiO`i#CO#%5-t5C0C{ zPvs;0n~z-?p!t;z!!pg~#dJ!PWzA$N&m^KKN9Phkxn#YHU;G{}r&P|*K1kG5rKssk z(V9`32o9H%f2z_b{)_kDM6~rRey-7UAEpp_oSGLvQt*@Chres^Cx> z!4KCZN1}C`;CbpSLdS6byD#XyGg&P^BjNF61&DfOg$onm8^0teD#^A>6h)OSnA76- zNmsSeuOf%oyC;aNiZ5(5tL(hc*gt|#tV5c0w4m?di3;%?WRYn6S?IT9(bt}K{cs3* zs%Z`+IGEvyjb#bGW0Owb{d~(dr@P^HHXCN#iAqWmZqY?E;}6DBFGi!2Cn@SV>}6>x zT+QLx31hEiB^8DteUO)TfB}ZpALK^!ar&(}^1;22B?JXo01hpqwN>uPi%=#`m$%ZI zzQt;eXL^EmeHl=X^(iCdm+$Wto|&w5F>}`w*T-w_!dw_XT~j0mrS4`KTY#&5!EvEA_T=$7pZ@$7(_v21N$`V2@GOW^_4x1{aP>isP=eEoWy~#RN;e$F?ah zEqkIkl=XetKU7ns49kd%6^F|4Y+=F&tQDx|?-+=BX<_ryN5O$oN|dPQ6mM7kS?z#y z$%JS(QGY0bgs^RhwM~KHKh~R6cFR(CXh?7_#z{s=saH1bbJ$oMwyq*B}0ua6*)G4e7QB^JiZx zfg+*xkuP7@EdiBH8-(;MEGap!kh`nu=*Ye&$c$sn?@LqTtQg}ja`GoD z9a&F;GF^fif9?=gg@SmkO}MOQl6kRXuOvw)c%t)!NwM6>ZAi_f9T}~2N!vWY%^cx2 z{aCc$-W&<@>m7+*tvRVq_hSX9gUr}l!{?Gx;_>#<*pvmEot0CBBK7tENyGcei=#Ox z$ha_aW4@liP|RGVf5c{I^q>B)P#T7O*ST!fi6`!Z#Ovr>Vrg)x5YQK|B|{U1u9G$r za*er4Vw4kj;l{}(V5`6EAnWylh$AI^wtPB~E>AWo@?auvEINeK#jmTOea-~h5cyX- zBJ@Y$*4Bd01?kDN*on#QJOg`9pNDsGIt{O3jKAL2EHNwE%Q}xKx2L&LM!27_?^KVCvAxOpbzfS7(^KBIf+4^GThEuVv;$> zoSY=zggo5}Ovo+_F3(Ue*QtrNhNfQhD}Mp;kd@I-Kohvf+U@a#^7mMfUH6K=7W~KV z>Vo3V!d=|bxN(O!L~{0Z|6Ovu&?R3)ODERbEv95oCe>{T$5#|JmE)y-wGE;* z@QoR*W0qWytd+Co;nrxXJO`jb1_I#};+)7zg@Tgun`_e^`~(yU8~ex_A;!{Ek~)M} zMv!y6r%APu<@>@%Eg{;Dkbt_sg(%!nX#qosqU%Jt3HX@MdLz-kpNw;%iWq>K6LY`+ zbw%HP1T5M&xw`RGi63TGPueBdy$+~5%mH*oys(y%pR*Tq8>`Xg^}fKUu?L9 z!^Nv5nZK6G^inaOtQ;|h!%GlDsac72wJAnG%}K=Hw;3WpK+fN1!=5-Y$i&okrAPU^ z_$@@@R-U4pH7}%^kVWO_-6)yD$t-SOZ;VvC-=WY zwG|5aHqip-%;E`!JtUfOQwtMwvj0A3#cRuO(Yg`2xVRWp=!yfoOyF=ypFOgMY`|e@ z`p1uaao3Fc`lAR!nsS|7M^|Tg2HVH9`-7W{ZSJCv9=&?;LdtUuBQEXZ`>Lw_pH@MK z2M0tJ`lhCm&%TxdVwGR}`^!CVJ_->s5agd+dAL+MsrNTCF9RV2#5>gZ)3z`)(TI}k z0}Th%bc-8*4vIUEbs9P(&^+)=u~g=rJg(abqrJ61j-&uZ^0^<0>{{>Xa=ybK!pVJN z;eTXt3GX-*pQ&R1Okz^`!EPNbNSmx#n#sY%#)>vx@GLIQ3Ip@7Hy@z)1{( z#gvnX2;*gB~~jNjngd;^fI$FbBv6%%-jGO4X;VWS5%!!ph%vGFQmK z?TMe4FSg9H3kxg)a|KAN*s9;~ z0(%KtfPm!c>Z+(1I-p2e7;|yq;}$gk<41O`f`4&&OI8fq8qW=j74l9HxCj80p1FkS z&d_XwT5UGaF&^qDjFmCU9I0kg972;)vRxN>XFB|575-#1<g!ld z7AyT3{J2Fv33=13$s=7?qK5#zC+0z}>6*=2m<+Q;w3KzmvErmVQIAnlX&WZTTjtFZ zSRFMFb*lUX?_#A%KCF#QvXG-5)7#`vdv-vrd;4&4H=1n3Q&OGym7G)44ndtZk zZ^{8`EPn<_JF5}Rby%dhNGmAaw71Y@Jm%NQLvJ}a<8PK*t`;0ozk zciZTW=e>EY-c?>fw-5QuCu-K;!X?Haylp!_bnl(LcGo<1pBb6>cw~K1q(p@%HRgZ* zkn=ZzZqaPrSzdb(UcZIjMJJHV?^}tdcP9q9F^J>`%cQX3sPf?Jf3r=)lGcY;vo^)V z*fR4!ai$1QG)<%E>Z?_tW{V?IrELtda}4)V6GNv^P*l!@*czUN@(v}PEy3) z#El_=p$lW#I2iy*PKfSShC>IVYErN=j(^CB=K7`a3y%H@sjW~*e0&-=eMdD)sq(S48Gfi+O5`{<0HMBJ~Ug>4r>JmP1>G`1Tk$=_f0QY4xbmp2}IS%v*z>xTQ&p)8Z;YkrfX?z5qH(ytPjq{{&^;rH5_=UN}8oLp~DPntYm) z)vy7kPLezqe&@Z=&h{?qcQ7h1P{LWPM?Megz>ESX?0x$L&an@5R>GG_f%2rsW^7Ld zo+9}0I7u3GdC9a-Y{0)-iJJkC$j^2~x?$(w)~F-#&(GELrLEn9AlZN`{yQlE8#w&1 zD#(Mn>g{(bcyq-w`S#&7$B75Gwz+m49%qT!yEh6t#1`svUDU1E$^l(k0U z^NkJTo)1QjZ^u0aHi7qTz~U2R+d)Zmo$M6k77ya-ROQ*K41MfC2(}}nv-AX$GmC^* zvaRbW;WA1sNeCT_5G22<&O8_$epdGfg|u$(IYt!rielIZFHPNs3WB-@WjPG zt8g@-9>EqxDZV88M5n))!F+lEuM;rB?pMNX*hniO{`Kb@AElqAKWV))JhQQq@8H+J z(toz$7k!%Y9Sg+Ih-3SwO-$y~Ib7)NIr;}dU3PbC=vP({7SpPw^po(^)Y)02-13hf zT(^&UZ3c@~V$-xA+kGN>`gD0Pij;w#UUo9-w+4Wu>Xn^36t8-aYt<|S(EsrF#Zw{( z(+cb6Fq%&oAHFDi3a4uunBmUl&S9X_j-Gu+TEo`JwhLzF0U7r9703K88qEs~%WO6n z3ZLxR%Y?5-Y-6! zOyr4rzX1*@bs%o|YpN(3s#X!9-C3wOqm!qls>Ji6%!>oD+v!xkj>hm3kQ7aQ_%1G+ z%*m7S>iQXcsW`F$6Oi${TJ|kw?k^Gijw6g32YQP$$hnvrG1FQ8FS{XuC zD-EPG&qsk%PydvOX6`mBce5y!+eGWgA*vgg4veaX0*mbPIM4l#cYvOk% z3*W~-@}~>-v^9^%Dn_yjwdHplEGLDb>k<9zDl)qS{l(?^QZ|JC4Yh{@bRuh&$sH|D znXs3fH2S+0dhNJ^C({o7-!Gd@4B7keRVhMWmE^O7e*x$EqG6ZiX$^3X=cEytgWP*8 zz}*3?>2T7&x3_Ya-wZrFJY;kpk3rZ6tb65N{`&Ukd^ZsYA7y3VS5`K!16=M>x`{B6 z>NN|!%L8M|CHk*RO~_f_#uew@QZQXHfu{ z_Q?GYyj3>>3i@;2ivd?Yet?Nwb zS5}zR)6LD!o}a5cX#bDY4Or; z26~p&-)Wti<%Iz4tn{DzzBF7VL5VT8r6-_&UmU0M1p-0$L?1ks_5XJlfb*HWtX3lB z^x=kkf`s1Hd_G~d}S^ef&1a-~jVaKynspVoAqLRS!KSOUv0B!sQ{^(B zksUV=4_qvR`wUrGq~Gn$MJEsMqENcuYTH*2=^7E15t#tX_02BNQZOiHC99;O581FX z9n{cZLaDT|DTv+a6qG4IGqB$uA0PL3407CS=Hn>p_jkWep~9Ev;=^m8p}E0$%qZz8 zf5RG&WJLHiR%mR?`T)M`r=d86f3N&sZ@8D94D~+TJ=f9ES!ZXEaLW>Lh)L1D=hE1I#g>5m# z$S(d;gwxUd_w>a4^fbnURoDY>kQN@Ad+XXSgkFBqHNba!{W{;G^7!}BUAMVTcQe)1 zMe*O82yYop``6J`w*gkU~Up1U4R2N9TI8&`wu1m!$|&rE1l1E$TXw5Hebj7hp98$MyIbbb zqesA+(8JNDi^}MWB>9t;%IKnO@=C({MLunWJ`ZvPGu(pmolqF1AGV+Pt`|d48I#zoY$Nyzqbi?4{NBbd>PZ&-Mig; zDL8A*LUv{Gg$a& zuQm~Dx39(_wqG1LbpkTQk1YvSQW!3BK7`nsc%?5wF7kG0jxA^F7|xX)v}t{DM;Gp7IFVq{5lDsw zux#}e%ORRh28C1jKBnD%lsN&-WeM9+3tGSehbAV7B+7vuj1ij!{yW)n|JF$d-ckXZ z0s-gE6xpEad}{`BL{UQn>9M3|!HEK}_SFv#Z~R>T?Z1%Ai(GnHdvMxEvc2PSIaDPg zu;jWI`_xO)s>;&$+w|kZ`1yjHzJ^5AmD@6k{58&yWoZ$km#&|O(z!Oc1U?gR#xxcN zpC7%;^rDh-#j6^jO4ub$S^Fq@YB`s#vXv{6oGGuTWGp&g$72u{IISp*1lsIh7NE

    wbP)`xOO8(i?I!5>P)yH8s5H{^jMT|3wc!Kfenf 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� zl?%v`L>@STC5&z*4h}-!>=9GF;deU#6a#9}aUp6bFh@V_i!nb8fQR)6r2Lp8F9Jrs zJVyZJlF#-a-u6%|xfulNZd|!n7t2EX^yB zRaZX*fIU88g0F@^_Dl^h_!sw%~=GX_!b=24!K{M)2AN*X)(|Dh7E%_{dm zPyrZHCuK;9!yyKP5?(rjsh7PctF*pT@XRAZf5d|spvLhjg*zjp9>=cdrmwK)#ecx$9>YDfSvfP9N`v5 zGsx!!Q?lGb|5oZ0eN>ET2@yY&12{u9;FV3PKHK^I*xh*p@tu(p1`S>h{<`%kxH*32 z4Z(+3#I?GM?FL1>W(8i*?X&Jtf&gupGYeS^p*m|e!DIi?ynS)Mmi4UN2}SiNdAbZA zpG;it+jh+U*S`lp^oNnZ%GJgud@idQR}c<*M{FqrUJ!44gq!eXZ0`WB5<~Y=hy(dg zYPH^IQMjDa2dFS7w-V--P@{Vr8FA+U9>6g4)u&o%(nZ$X z<^i;c@Kx@l+eVDZeA?8j8E88Vih&G|jKJQDZ|?qV`>FZ*^&U`GFE1{%wYA$X{t?59 z3cb@Eqt}kNssGV~K11t+&7T3`#`LP~1ax%Tc=_65{mFPYuMb|{h|7JN#Cr%?800J& z@_AY%F9i?49e#m^Iw`U$Rx?X?GPq^AYvj0@uX7^tmn@B-K!S#fAjQ(wpdcB3OVtl4 zMIK8>S2e8y(kRO3{!X7IvtXnFFF3RddMT3at-zcTba*i#xx`b^*%fIC8rK zPFk;Vl0#uh^Gd;aJ$_j+SK`|!*Y&XFjhuts2jA7r7uj5%eS7wM&xl%+AQ;^HJ^8cp z9R>Cd{szEOy>+|7ypOMH2Ie~Zz{iNv@u{hl_wh`Q!R<8__A~A(X{- z(lva&$@Z_gjz8!T*B@dLs%yi2W>fSiN>WYe%YV`w>>8iP62E+(!%8q=h3v)K5va{omsRiC^7`#tKPqh zNPK%NWO=+P;MIupvMY&j*uk~$sT4qpyaQfF&B7rb#Nvr1uqnb-*$9J(-8SKk>~oWc zsD~}r+-D)nMFg+d(}}?28(6#KwlQ?VV(5$KmYxG*xdzk&SS>%FpULUgs+*ug(es)8}b z<-l2e_U~ zB*jIo=c`k}_AlnT~k>jH~Kbjp#7Bgq8L}gOCa2OwQ+L)rIy3BJ1~X zeIEV_XuE`Yzb#x*>(ezJ%;C1o{2G-pdH!52c7VuQA!SItUEwj;rwJXm_uhSu?P-qr3zH_?VV;3^1km!8(Rm+n7>+pYKwFy&EoBE0F8Jew@>6I1K z=ZF0Kbd;nmIz~XGK1U)HGQQB?y{+x0uF!Z`FdAU_GqLE*X^p@nC3aOT((YALB82R- z1zix0Llkt?M%TJ7+89nk5aR&|9|XDhWQ?KOFehd4xQ=jn*Yyvz3+PWX?ncdb#5|D; z8$Zj7ZtQcG%mMyw^7rG5;m;CO0g9K(88Uwqz1rFnlmrw`jSp^;FzXrJqw2A-{%9p6 zMXvecf!#jiccF+)niRi>Uc8f}3P)xo8mg#Wwb1=ToVg8lzFH}-3E1B zfS%ea0PSj_<#p1ne$cJbe_E=Z=9Jp-4qFdEx?H_#)d%g5y$ASwXSx|PzFq4@kA@(( zZ@bWB-3!4MG(SMJOs=o9(1t-^m8{H`4aoUFj?b;R?>UaHDkgU@M7ng+HR(?f@Bl4jAXPR6dc zmI%K~+r&BlPwAQ=_S`BPv-TSnJ5d&TTb#H|3K){#y#z@eC$h5M6g2O4PW~w2 zMtp=-y5Bm);kNxJDPoHhqSRPkt(uY&5Yx_=16w4XXm~f?7|-XW&UrP$K{!&2)*n*_ z)w-%IkVkS1Tfmul+9bQnc&B!qzcm`UHa^*w0191YeuV%fd3DAlt>bE(XUBATq}9XQ zIl91l-RSDRS}j2Q`tJs2Nj}c-TpY3fuG=96F$TK=_Rnja#18c&neDO$Kvb{mj`@ZG zk8+Tb)w;0D3Qt~5si0wtP=>5hit(Mp=$nk7Ixbo8f=J{ZyTXKRoh|d|wRUZ1?IO9SmoD!&bozp>?y^&KgyRE6$*;|^tA zc@-GrHVPEuuK-yu!gRRYx<|NRc>!Ruz1O*agH9#~SrUH^gNjg1xxC6TbRP{447})K z>Hd?V$6Kk*FQVt~w=2e!n&+SkG|G6Y0kCBx#A=}%%rpqr8Xp8ZBrE%c8yGbhLu=`Z zQ`C&kEKE2e^`mQ8Op8Y{Z$=gMN*9{M#=&+U^~m*QA<)0xJ-tl2N?{xYQh<;T2(~Uw zq*)fCNV1b&j<){(uI_nWPlh^Gv6EF^AK#9p?Vo!&iK0BTghCayV?To589YQL&D>eO z@@-v}I|`>LA7-SeZb9H@1NCaP`$YnlAugRyuSaN}wahyJQlQhglgRt+6V#o$j3XPL zF>x)>x|RTk6!f})Dz!E$!}mWhyGSIf`CC_Iz}1|ypi1nMY;L~B|Ikofv#%F;y*AW- zht?m**AU^40RsWFtVgPNl;;!G%y-D7A3Leo^&oFCfbPe*3{4S%>~jI3$=+?vw-QtO zqmq$`55rJ<5$Q#oD*7UBOLoS-U`lPLY1}7h>@Szq3h@8t8vHf*|Jg&Fci1nNv6_RI zi|z+njC*~6prAQ>{8#jTHv)@M>=1`sjxsXOWI;*^hq4_U%!iul_Yx>&PrxTqz&5{V zDKjc?h!86cv2&E;J(H7J9r+Feo(tg5bZw+cB^TDEMP z$B7q09d5;wEV*mfRojLN83IV$qsb+V6Uq?9mNL%i38 zYI&{8A>P729br2iS%}1HHy&E--*IFD<{k}XInR*eV_5Y9D;CwP|L?>TAeYMG)pAij zVJ>2`or^(J`YU4fH2}qH;1+w zxqT%0BklU;YzbH51xqX-GZHNjE$yWog^2MZHKi`}+-EC;3Rzu2U*iOXn4Ba5x6ESH zFu$^~qIK?$44X*Sg5Wd0dCvA*pOu9BICyxBK;0)6SOJv~>%JwHKt38$Nxsn;=&;g1 z5A#-W!B5-cQXyvKvl)*+^9)yyz-lKFn4qh#7-YFy5!ih+klrW8Jb?3O_zTdHDwhr` za}^GX!nDZ;uq>0`et1IZ)WS7w-z`(^U-0LOn`^X0=`@Y8uh_gl_Yt*kVib8Rp56}8 zORcCGMVU<60!1uvtFp*IME<}FRE>DQF)3jwT@+#xh)G+}b=rTqiTcO}I60zfw$W!0 z^TRZ)_AlI#b1~!Ksh&3V+cBLFM)uSKrJt;tDI6)ig-Y!fE+=aT!4=Y%)XCnTPU%)b z=!F|anKihxZ4V&i%J%m0p7F)G-13w3O~Rh4?7G?Pp?t|s+_na{!=34Q{R>N0-;CTzp66#h=3s=j#KY~G~m3iuKUM{fu@Mf%D!UJEYw#eUxA zW4&777?qQV0Z_PyJf}Yka#e-k{NFM6k=ehYzu+fr5)q+X{nQ`J!|B8^bAIDdp-WGZ zWi5nyj-jy!XIqGQVVHm|dV+7ragBB@I3aGbS}A#W+vdv7)oa(TN@PDrIguP%;HT4K z55U(5aL>ZA?aLwi0QiFDrWX``-SK_p4v_FhwnjM~0=t#ayHQ2MGuCR$YY2c9HkzA2JQS`#0 zt^Vf;CUL`VgggZ~_Y><_UYz{_sg|WX>u4ssXL!n82)DasT!wh@v&bQ}BthIREPZj_ z`3Xjv(6Lm{sM~KXL@w$KEA$2V&e!4BQjetWSkEpRsoK>)#k{I)cfG4&T&(!5rHk}7 z31b25f#Y0>qq0I(sG`i>e1NaZo4dPkbFweUSojP31<&H=&DL*wb2}0sNO4C7D0K+Z z?h|7EZFl)15nTq@) zIe9hFPgZ?NY>!@Cr&$ZG?m#@Vn0^%9UrL`Y@-76 zqX;aCq^mNcYqt^PCUo&b2ya9*mbP4LN&e4tZy@jQM{m@uyy~;QRdAtDX$5VJfV1#& zeXe}(!M^pxfUD+e9?aA6Z!O3>Y-O{sB-?|I>|c zzlDs%?v5t>TI<`O9fS)wLvt@{@2`d(;7%xL`3XR<@u{wuznVu_M(q_BG4O^_>q zn8XY}Bb+EqQG1pztO4pnZ#;Q_E!*;oaNc5NLPLvNlbH*Dk$%)YXXCs=SIHeGirfTJ zt(36_a-s0qA)S##Mo6;3{OGTO1QE1pd@kF6*R|Cr)6%oZGW-c7ZCQ&5c8K4 zh-^V_;-_q-H4%yn_BF`-P||rP>xN#zy=w2c@8@v8*%AQYeG>c#W+1m+c#AbM%2oLV z5DIr0*pLy#h`99cq=2B(tl2v$6-;J;XczfeDPgo*4e_OImSc&=NyPlS{>3k%Exy`f3Kmw- zFa!t<{1S2%0&}N&F_!#Be3+em0w?@5r?ty>h&c;$l+}p^_C4d7z`|@1+4*4U_f&&} z!3^ljJ}^*uSHJ>mAad*1mW>>nh?8xGRYVHF3mc(J!*BkR^6%2DXOT^jW~uRp96Yw|rvv4oEBm`I%E`);4f zQN}`w&Z$(N0@LKeP58j1pPj*30a=U}S7B0pti-MGUA;WYZ2H^#>j;r$fFN*YP}5pG zv0%aG-pl%sRG%OCE5U}dI-9#sK(zqhYh6v#D(G@xaXaNb%*T%x*O=ORWYJSE^`yK9aQ zT8scglAV^*krHpI9T0MQ!X}D1k<;+gvy6pm<@KFlE%ihg7)??piqI3+_eHP`1zNHi zmU?y?5Q>})e(|89+yby@9hj54_-9Vbhqqbp-!x1{Fv_p&*{Ua5opEoU3R(P6th@=p zcSHNu)ws-V(9pF;8p~q@WLu-Dkm)<6P$gu81_YGIc)|9c)N|JbJk5l%LT}}c4PxmR zaBp=I2+;r7s2q%Z3gj?9HE5_b$R_gf5srbBQ%&zP<-p{`J5^8oGY=Hu%$2-+4J^=Z zKC0A*qRSmsXmVzzy`A06T3t_1&;JaMxQY>Pz3LW5RJQa&;Vl~V2lb}*d<74bo8ti{M{=(fxwRC(TQqCVMQyo9I;pl^$&=rbcp%ym<^eIU~TBXr?c|CxLY( zrKLHY0(tnyzLzz?P2@|kfY~_$f})@x;;$oJ^{4t#4qo@ zqlH!D@6=A$ror%~1`x~eecAn_^wYYN*~N-T37z1#m-dYXOP^>sxk-@{#XE9lB?^{A zD81jH6{=8?3YXyzPn@(iDdoxE!Cp$YE)LSl47j+$_n7^S##4j!0{*b2LNs2X5<}w< zPO@H*_HL+Q_u>s9W~7n_#uoQ$YVTj{`tt(llChPr%7hRcaqEw$tO+o|k^vMQ#?pW% zf;j^4vm5>El=P0>mxxG6c+Hz2h9CYpA6c_S%&mvkhY5OBfzlT1g5+-Vc|@bgcR0{L z2{ulVf6hkx($eli|JOV9s<|j);H?C{O_GuaRyb6&ky7WvT}VvHK82H)ikq0p9YLG^EZ&R?%^3VL%rxkMwwne zbvd!H+#^nXNYbr$z&?R?*kX!n8|WB}Xin|FO!=t~4`w zHY)VLNb9~{9Mb$+oiV>a+=>S8cKA8~YMRHNlhpIGz8qI2n3e$UxsGbCn#hKNcZ@vr z97Afn9}#@@aYNBjl%^bREX2aUGQ9i0jI2KW+W^s0tluMTIW#+AFMcT*6hdF4Py*mR z@#!Dj@|ZUH52wyKohnIMnJg>`4MHuL=Cc~QNSKnC!uj|qq(72AQb@uWfD2G6=QYae z9I8%eh5G$;y!xS#W#Ex|;YE@a`pNkdO(ZhQZgs#wMa?i%&KAg-Dazx=A8%(s_P!HH zje+W@VhE1(c-9*NCiEyEen3xabKe6P;J9mQII4Mu0LgMwL3yHkA0_bvr1{sDTuOzx zbB);_1L_Pj;a@Wynnl6wJ9i+bi?%=xRV&x_8x0)JE9+y2?=#QhqloFb18;bZHiU>J zBmu>(cWz7!uw%VJ2#tVNfwcZ6Am)!bNvh+oaGXD+=vF9Qa?^yIy_*SPZnwDHRYCXC z{j{HM&(u^$O;>~%L`VsNI2MrF!A9(I;zvZ9b-0>GI0s}0Nq54@b*%DL@DqMsCEms?_#Ib zq7`h;XztqdX~)W|_X=W~0mrADFK|Xd_&T_+q<@UuvT{|{!4VUa1a)E zC~+tc8hM*T(gF5gAxMWr+UmOMC(9#!&U;e&{ z$FOt(ki?^qcl7#J`jd~Ha#65p_O;IMgMYo3O0~LH`9X$hj(3DNy7sM{FRn7jkM@@`$YDYRr z#!GdLDeUIUKA<15?7)?dA(3=3SjW^)lI!b<0{mFtOt53PN=I?H-PkQwQO_`6qMwAn z?UkasXa>)EiFWzX((ejdve)C0fVNmUlmVEw6)s?;eo|29vm;LLW5*oKV8^cc+?~vl zd4KBf;z%?;`J{C&JH(9IJlMGuEf1>+IQH7>(EMLGhNJ?exKLatN_nBDAE^!Q{j4tg z$CwN^YX+pO2Yi7j3K$Iq1;ujkeN9P8iS{%TAD@)R#^C>m=}A&L6n7Hy{;ye|#tK;) zMK=>dQ%1i8dtJSd$?-Ith&Q9_Q|b39*MIWd*QG&0CfmXU5`oElz2GZQ)Jggh^Aliy z+lcHEMExhrZKdZL+r8*bQOFu+DF;zj)L|gWX9S+mtKB<~H-*nV5iz*jhXiShTbym< z$(~4TC8l+LS^Ol7W}+qx!Cmg>wrb@6?)wXkIGk_2LchUg`8aR87(pN!(cme+QRS}P zY1aISBzAsx3JVPEtaFMbN;BWGCu%M{$9m$Oy3ZTVg9kgKExTR}AXjiU`Olw@$C2sq zF|PY4Ww;x(_}TEXAXiANd#07xp~;sZq*57W_T{c+)`q>Z;PRbR#ig*{x)T7`8#{k^ zTd4*v8vg!8v&gS>XR9Kx&syvik-e!n8o~2hr4!g4rnDLX0mk5OD&gd+ zkFATs)1>%cq3a5GCoHfQ#2BLC^R5fz4NQar8@CJUPX8bpO(5ew?}ceTkl}&PKif?V zU<`!Iqk+*!GLzKB`GrV3-PwLv^dB1MGma9UhZ$;7+qGPDn;0fz*iKqqPTRI`n=fX_ zY(@eA#%BC$nlXh_x$RR_C1Eb6(1=lDCklKlWi~t4V6FIHw#eO1d7Uc>g$3D7*wKI3 ztuNs*_ILx?O)#C>im%N@&)K(y_Uk|^!wW+(Tz&5waljcd&Ra`SXV&{9+gvN%*erdr zsjnAE8F}#mMcM(yw=4{zO17rWaVANtI83Zm!)|4DjE+2SX|S)h)-b@(mg@fp!^`g1K%%G?BjS=dD{UV?aQZ(Xq0HUB&p*+3-j6gVoRk% zuulZ!!$SwOB@kVDn|~c-i=xjpSrskwi=g2;dPwD>S)5_VQ!)Uo zk!8KMMYo9!X#bCv1YWbisu$})f0>OEwSrBt^Z~2AK_*} z@TgCmi~nE{@6?X|yZhg3?-y3Ek1(Q^K<`V$XwH1XwnzIr2pRrU>ofPqiJ@_V6W__U zH;T4FQTh$Hlv;rMe(wvM z0jP(T-OS53o|{jmt5dL=uHs2|hvW1`5o7K@iDY^ruvU3)B9`fg&z-^;4(fVblwH72 ziY5dARm=%e2yt5qLNqMTx7p^`YGJTK)f6ysk4M&c!i`KBs~0kquk*i({5_EQD+R!+ zI5<(O%PZRj#TlDsy{HuY!_6-*txB(7S;t@+V9RXm_o{turcdzDv+s2oDd$GwmQt~` zUBBU?ukdD;#S|&?#9{}p-GGt7I?MAKD;;d^2Q2O^uc+f5`7T@o-0WERsbyE413y)R zjk`|D3eiG6SuF!+8@oTyQSowhk-tP|pH#C^J{ak2V(@*y$kG(ktMn^1ffb!4e1EF* zb2c#zm}Q4RvmGxr<~X0&nKK;5FsC$;kXr}iz)qazHd4*z{VxQNH*;;?`}+R_e~*+9 zwi!baRm-ogLTg&)`6a)if5a^QC!#DkDi6$-2>zS&-FE)X<}rE5xVL!bzt&mMkf+#5 zxzyLF;*ClJU}IJ{=}vX9<0Fn>y!chh88e~*s)zP6^Vm^%Ubv_6GXjb31<%hjr1%)4 zT8{D6k8StNILgpUG<|YI2uB#^?wGa(@43)pM>GH>opRlS_wRP5sK=w8 zj+XHk9(3Nk1@NU%f@eQ)vJ65-%tHs+ud*1`x)wDg<8hAA@502aJ$h(F&^jp6{6vHo zgqdgafQ4`*r*i&|n)-fnMAM7iTZr(O%9P<*Afu?L`bys2*^xIEvi5{6yWz-tmB^c^ z#o+8jFlgO_CSFi0-S5j@VUOzi2wxnlr6n&La4jp(ZGgyhkiDrzQ#R*7WDXOyx^866 zItb8anl&;z{5X4Q6|njB(U`D+b%R6UL=PoKC}uDLdUW@x5*buALf=}0+3SVxTz@)g z?kh!54dPcPr3Ew6j$Q6~FRcZ8Hd%nBK0t(Rjl`l|rP=b>@XQsgfTe2l?Cw zCg}Bk5JCrp#QXkMf&GeB5Bow+1{WI!A`-0`A7Srns(dFtFW z>=s-6A%D8m)HIS>hzi(ftQrrKwbur;4gl<*r+X_Q?+kEafR?N-oTc#{PXR}0=pDSS zT?t}fvW#QcEkiKEqnch3!-cOSCKt1y?Nq{{%dEr{uaxZW^Q!%Zs5*!itR~KyR39R{ zc;ww6al@ha&1DME_Z9B9g@M9fu^4037~!OmbS-&wxeDm>5pS7Jz9Y@m2V#c*EaMVXw}W^{$lky;R%R(ij~XLT1FTN^G0n$#0+wIbF}W z(0;zvCVu8fb`+mjV*EMXl$+KoXX| z`uk}wKqs6q=`k9d@DiI%dwj!O8T@t$b;Hjicon)Ycie@cxcJN4?GKjaUsP{yN+JR2 z7>YMbwxz`FD7}}z3WH7IxmGRT=~}|~5!_dmqvU2%pS1p{VM=Pn7o+bUz~8+uD)hP3 z8kd7g=RpKn#NfLlYBg+ccH@3ZTJJwQW(nN67H|2i;(4RmJ#FB9SpxZ|R|2Yl8o|;P zkYCGbFr{&Sq%LQchFkycymSwk=_9Lu_p6@<$k{^KjT#hmg@@SQ^k+oSqbVxj*M#Y; zn6$|NH$9EjAq?{<1u%8x%=uJiTiCKEP?$-D-KT(mqGA`c`Hsz*1V{IOOne7y{F-86 zEns$_0@*9Z(FFSJLH{43zA7xr?u!?Op>vo4>4u@DySuwfNbmXJvT(0_lwXt%w{kNkiQkANG0>Z3RfOGn9St=wI#PwPksDf9)Qc1`4XD%*D2yEFlU` zC?KAKC6b@{0n&~Zd5YmCNU-5WJ4ZaQzvbzxtIqj4uU!$CSB!PDcnu6$l4?jGzO^TC zY-(sl*xP+_at&fuk5|nRj}c{&s3rv<7527q%EZa`#MtORDHH**nAiarC4e}T&<{Vm zsrYKo7)cnYZW#xEp}Uv0~?Cu*UtsM^N0W)Ubq8Wnxx*YMJjhH=dfw+ zvOZ^7QPkSIhI1uy*-@Z<^)7JiM!z-!t@ zc4`8IV%^XPj@0wOqr-caZNx7=S5mGjK-aZMk>y+=$lKDLim_N_I7Rt=p7Q%Leai|i zam7$}oOkQaF8D3&0|U_@F^lz#s_9JDIR+K49VR-((1FIDYGtQ6<^8|C|N*;42Uug74x4Xx9=&p|I~e14zD5Vj;lA_{I`oD!e{fOB!d1;Q>+OaI6?+d1T||f zIIE+Bb|I4w%mPas)KHt(&U{q}NG~Djb33Ba30(Akdh!jZMSmJImyQ#w0xYK9XC8jo zE4b3}xd8z}c=moJ0?+)nU;2Odx5X|DaSOCLzVM|57}JUDoSd8j0%;l200tMZ#YRU* z2izf!kGcZSB7wT+xU{4t^ns)&(^dHHe?m-?#^=)!+-vSF0LuEeJr!5iDR;j3a)2Q; zT5GxGmwZ7*RaHu6rnKKV9W8BPM1+Eum&*gA>wozv@_EC{G&R-~)g{)q{=3DE>A@@? zS=aPpsZ(QBVnNk3EL$-!rGC@-WVx@xCsD@i4s3bQaw=l()!{gEyE)7Z=>i4as*qtC z4!V(k^HX$Ga}-Pz{BueS6w(IlaN{XL+Ybe_kr;je(LC`HCn1!PP2STMj2sWrTNiPK zuw9a=ZkLfPiZJQ$BzrE1LLA<#^GCX>X{}XtQ>}DzN_d6EzX=Lu&0{zrypO)eH z(f5$l7v$Qp)aL?C<2n+_+J2w{!J zAynX-@YxkeywLZR8O{rZ)$iLHwho4P+BMVwK_4krSM=ZRN7u|u%ypTEmzUSW;|#Eh zVblM34`6qrqM~wga^Ttpzy2r1_JbB*m^sU1%h%)Wf0QxqBiO9r@6}%J@9qMU$*skQ zRX2Nu3vd1oD_Pu8zLj><^@m^03GE=7n)r}0larG-U`~Ko7UI_asXi#ozw4p8LXgEl(()tS1&w5P@gY*;H(i8 zDZA4<_W$(Ee@{3V@pXU@pd!;DpxUKF1&W1@2-z&pcsNfon5TF@wQ4JOWYL*SA)AIdiSuOFPv zPn=!8E@cR-F32Y^ppWyvnaXs^=b9mU4l2`;0xB;F?SvS1ZrX8-Dj>G=>%X0-LVqB_ zG#gJUKed(TE$4t-`*C(Li1R!KY{g;2a*69-4xH{?BFBavSb+dk^ZcwpJ#v~17 zrI$}%OzHgA)W2|WrZo;>r-E{?l!`}oyqRRHcG8_-HMv%#QKo5&Arc~llgII}e=loX z7rC`Vy+XlEip3}Z9mr7yz(TPR@Ix-IJJU&1zyoke@t+7ACQO61qu5$*7Y`uou&7NKuG8+m`U(eGC`SP*~!a$$rSh8 z%qaRW1;u_j9DWQoD))uuV?$uiUeVL0Q^%+8_mUl#LwHdNj~h@GK*j<*L90Ohn!^#* zNkdkeiB#xVuXk3MV%sjNbsQ(+eO8OeXL$ZDmlUZd3cd)LRw#4}XOzg8fKh3bHu8VR z>?v-bewbH3{W<0p;$7wo$qqBc3978@A_@&O5cI}N{_PW-bxph zEud2o^z?A^>^TJo8wdnCCxLTw*A34}?;-*-BEt9;QC}?DAR;44Gt5nVM3X^}9_}>R zxGwyG>z^sZq>LLJUh75naCGVGOjXA*S)y8_;!3~7-9*8H$dSol2(YqxVkBYyYe!+Y z@*5{|jR$uAsev@lPqI*M!o-E>v7dZ;k$9!nEjc_6<5Zm)Gk>RS$?-qB;24R8WK_T% zns>nxAM*IvNA6+TfFF_?>$WLdtd2sgy)?{0Nwn0`VVa**PXhUt(9zcWrTT8R3`t_0 zVfL7Q74kB=n2GbKp@rYNSPdy>+OoOoWsmz85K{M-QGR3ZA3T+vzp|3=v6z}SB5IS{ zzbvvD!z-%*b>$N>-OX#}5oMGCVS{m4iz1lPUVjsHtSmQj-^(-q(Q9~Z_1V3ZLbZkH zmrwrFMd+tzRo^p?Z%!Y6n3J^>`RSIGe#I4$mu)J%MDg;L$DVL;W&7OFx;^{0L-daO z`VVW8!taNTia;PF1bVJ!)&D#FCq{s=*wj>fB3`qF2#%va?tQQ6&o-kk z@-4UVp^o5?Fz9o&6s9N1=ND$?ad&6ZU`F=V$RK(dv&1<&a}k&yC`i-FZ!7X63waHa zgWx}GWYO$IC_FImfg;D;34zI(vFB>XGfadokJHGYC&9Ew^LC7^BIL~~-W;t82}Uyd z9bn6dH7-h-^g)%VP=mTYzLbW%;hfe}{rg!M2)6v2=S{T!IKh{+T7t0(%wB~AkJlPe zug^6;rL7a{Zw)-uy^3C&C4|J^eii>+=CSzd$<>ck;=)_ybfG~)QjS4je#idJ081ubLEx5!id$t&Xs@&Z5@LbmUT>i`clK&QZtX|wh{fPencq| z*!0-dbCUr-%i@d(pGnFT8a-#c3I>~b^K7k=AG3#JccGdDN#JTBZ*+1P)!!yD%oDT@DG)9Sj7URb2-o;DDVQpq4tfvhvEo0poM$$B!Sc`~V`=>3Vnc-A{^w+Z(dZ zSFV38#Ct#K;!E|xdGx#&8fblApxr>qU&PG#UvKyyMkUZ6H@+WHaC1-G2+}}@-U4m* zec{vJ!i9x#T;G3#P_h!%;gf#iI2XHNL8fC}@)+tmc8LZ?#cX}{{f1YkgJHW;@pd>L zO0r6~&72C{RZhz{^L~e}I6gFM|9hE#HFp11T>c9b$&io1B_o6J&w>ad(@p#Fij?Yy zx;HtU2st+@Yv@FxfAM+>p$fpnY2HLE4NkM$kg?>zObxhhD8!_ zhp3g-=vCyv+P-i7!assjQb1g31#+yC>pCR}sd8!&3GKsyaswFr&v*NM&J>LuZQXjA zA;Wto73$x|pA)0nv&K+;`cyop?+a%`UDq%F{Or`o&addtV!v4F;2YlrWca3+Q+k8g zg6nGnd6tAOu5STcb$49u7_|jTwNfl@sQYe**$pm<-oH??VtmguE=SfCn&p-;tV_TH z(8D}Gi-IhnFMNF}tyb@D9=`l#4p@z|?a_z0F`0zFwt(kkH$^tDm3$t)FcK z{X6LkN?s5YkhnPGE5kIF>h5i)2F*J&24K8%T@oZv{Tg=^pnEF~>Dm6Zy71|Hp`Yl3D08ZYo}KUh*4X5}T( zC+nCJx?+y!1oALb8@{p_v@%cUM4u2t@LMrObQ}cs9`iq?C*xCi#N!pD+|bm_p)?D8|1~6;_#fkjxr-h=`fT+nWA+ zk+sh=m-eX(ul%Sa43vmtAnoS8@n*drP~WeH)vU_9y|OQ1{8r09)6Ku~%*B>pewriuno*B|^oJkM{@5>Q7u(C*Y?!60+gC8Cq9c@49C`UO2>$c+KYhQJk z0Ovuvn5XCRdJ5&)AH!c}_cd3`Z8t8P@4PF##@YPjoQcibRVv1YhOHF`bN zg-V7>V-s4zIVYPBMS;GmIROV!_W0WpN$|cQxHm2{tMB_AoCJ@0>EyTl!@(d!)!3ym z^YVUjt3dO!lBwar?e(v6z#GRcoo#j}x1f{eyNg!F^x4+Od@;rhc0b#tI?GfRP3Kp- zFS%zH7vH}2%`{j_GhRqjsfuG>*p59KxUAaFQ92rrKu>p&DVIUAxkWLEvpytarS-&b=W%wN|Q;7?#GXTo4tYtQsZBvsvSq}D`N6%9?){oF6eM^zn z>c%Udpi!yn{ByLVK<}BY3UaU^J%Yp2SnmtR^3(VuL){%+Zu8egM=YB>g-?-t2&K54 z5Bq#J-B^$Hq9|vf7h!_2yyZ2VnMlnc z;WR6pEkpDqXvExLMv3NWcGR_XeN(9C&e%aZC!DTToJy4Q$nsH>pp)g%?IUE*hnCrzQArKe2cbt4 zK%wAEL0=FLZ+l#u6Wr}JUf{EkGdVk<^(B2>ePxRKWiNZMhJtDLr@AvV{2)JCqe62>lWddSNiO z0mX&>zlNb?)yRC683S1%!q-|H@3PF)@N;EI+Ej;y8aCk8@7WOMF=vqM;aE*O9N7^C z-_rDnh&*}pOl0K#MGPbZdwAaIRc^$uFs9@%!Z-9JR0Y{28(2`1M^OPzSv)8M0ZVK) zDpbliK(twN*SjB^*Guav5$>E73G$cIpwQhdjw@k;CYP|1Kxxsj55Eo#VIuUB)|4Hq zq!nWcWMat<{;(>?8%BI@k6bRQXCSI9Sy(Dt_(CR`i$Y4%ZnlPRzJ_NO!ew#Q$Q02O zIcMn5>Df`D;JK!oS)fF=EjB_QGfaFM<;IgGzpcg5k42o+AT!6@s-_@n>&!Az&iIYm z*BUNL;L2P;qGZkRkD$;n=gs9(tvGTGNW>xRqyzB)jJBH1O##vEW9AMUR21NU;vb%9wfWN~y=$%3<5g%L+eL3b#U^-{!xrKaO2 zl_695JPVB8a45#51P7t5uMkKAYCxHhh*1y7CVEa&mNx64fP#ubPUjLvQKdLlR+1@A zM@>vU=JrYttzhq)O~i{fCOXV&-LT*6ye#xp39LFy2!jNsCY8_%A{M^`g-9guIcnnJ z({j3soUro4ac~*yGu|@f5T+7amZUz6u==?&$(E&QvE@(M2=+kh%F`HMxV3BzNg5Xq zq1?&eStAK~p7dMG~S8@U|k3V@@x z_*qYL0;LOZ=6!*pRJt>?sh41`3zPyRks_K<8#`kp&9KD@Ya&eGBAH$u2hU)L(Xgnb z15d@1#zE%(HB*o%@e2@X1M*!``bPS%t=&ntzD+CvnXFA&Z`I&knQy*j`)UjwxkQXT zPR@+xAZyjikRTm!j*F?thFd>{am&hJV~^Vh(hqB?9*xTC}Q6tOk^<~#5M=L z1*y?b(zpMN+a3+sOQ8N(kn!8*uU(nvEtPu?9Gm8U$3`vwR-;hL($9V<1|saSVc>5J69)D%zw3OK(j~|DAM1ZAbgGNd89C zqXJV0_Hg^@v?@3if?g2vqU-5UP$`jSeQ8gwAS9i*nI?=Vl86mw4~;~wN`-4U%6N>* ze8>hB3oXv`n!^XScNwQ$p%|&TlZDuS+qFt$ZvMuEv$axx_UMuPp zJ|i9~e=#a{7Nxaa=1cJRA}S= z=(r4pDYK_#(QgtfV7}|X9(yVBW+;g!^BlHGHcq6vVaX^=Esgeq|Oc4eO8LuWe^Hhfw^t=f;|S!4wioY2$JBExY&++Zq?IdHV(D(HCjwF-Qva99kX6YO(ZJ+DhWkPUcaVMTZcr14{5=Ya=yA}?Shthp*bMDQQ|Q2)YRA@l2?xs{Fh_s^ z>T5G3A8pPH5)C>=)=W0KPu?hUPIS+R9bd8*V|MUws@tPV#f;F~ibh!{Oz0`l#zM%! zj7_2`qj2#Q+(F{^{b0?vtXtSp3Nw^!Oz36XrOymwQoDy#v)DHK!<0Ci7+KAewrA;T zo@-uz@|#gCeP%#5=&3y1#ej45!c(f32rL|GK7$3um;0~TO{14tvB}CrT}#g9-q}+P zV-accOikF}(bZ>vmPaO{9iJ_5tfy1w+*ALD2bM%%OF8o|mnF z1mqSXU;x@00e6ZCI~Bp<;E=_+g2GZ1jSUuL;-rY!n4H9m`T($V)7^ZN7?Eb=bPT#? z%#fE>+~L5{=z#M$sew(2a};`>+%Czgy3JZE6DTy6ML3a}F!mFZZhYSi%m^#ivT7D6 zND!*?j?iiaZnnJ;?FgQJf0hc&)OO7x{MRTFx{)LD0uJ6vIA*7^Ad-Ag*&(}{B1sAc z2|D(A2uy{falMn*{#m2S8df+3CkiKDxL5X<4)lh|V8_@Oj!n;FJGj$W`{h4wNJmy5 z?iInvDW+-PApaZ7u5u~lUZE^s+m_`4zOQH zf8Nh9qA02G$dJb|QDeUD_?R)KkB*LO67JtnM7)iT!8Ll9uXN7PnpS%}2IjLa*G|I5 zmXYNJ6I6uA1r*Bses)FBv7>}7A8k;@=O036HQ{QHwcjym$BT`O!H9wgemMULQ0wh7 zuEVfzwVXTcqxlM)cDr=uujFEh%VO1d5qhvq?}-YA+z|B2M^>#fb_RFN5z=M7W=gkQ zDx2L4hd32-M>^*Sua@E>YvSe2h%~xj5Ix4*)L)(h_{mtjbbdr7XtVOd9xV#Y`cWa3 zeDO%D)euN$3`9az1;_LaD{XdCI5wVEi;!@3P8MKYELCv3L;l%yAwYK!>P!KASTb^Q zR51~8aa=;H&q34#7?_whZx$+aOHmXK3u&bSMQn$#NS++$&m%ll>S?y#){6+Qz5aQ& z>02-2<)|YB3c`e1S9TS<+1A$;EA6PfkhheOm50fd744O#v9lj}z2}0zDPyKGfsHYc zcCg2Z>?ONmG2@whCIOKNMA5@OIAW#iRVnmptMA2x%At|GHzJLbL6Mw47)OgpqPHj0 zRVSvpWGV)+=!bhv1qDnD9H9sff3ss=EKcMv=5SWVnoTW=p>8yu;kBYuwo&9Eg;JD zajMg^IvG1j)_;_-fR~e(u7FH(>r1MXY1Q?1uJb+ukWHr*_1>M#=Eo$Ysi~#dBl`VNU|APJaBhf0IC~qBnQ8pOxcuWsoTs}L z$HIrJIqmoPHCx!7$z&Ik>7okW-7d~J#HGNhYohX@j zI&O96MAjMyRs5_#UWAfPHFy}yIcrlAS|-zyEvD}DF`Q7#Q({Ig8RQ=}6>zSEb5vfz ztb%%W9KpWIG0dlrJ`q06mt;}bArM!)rb1@V6|I;b!}Z%Bo@bX?O{Z?dv8e?;co6o; zMkP}=feJnyN}f)$CNw5}>JfB%EbVoe5=+JQrjxyBf=s#-~jEaSag@rBFm^Pd3EjQSp z-OKfl`8XOH8+Qhwh>3}L@^Et26lz15FS+MS)_)%^b+{1ybV0x%{RoC)F zf?ffK-cY9j$h7EfGCdVLJER&{Cxr*`pagz}JXnZ)#hO&vmA!L$*?_VE|HWaE455U~ zIi9REBoQK)vT0Kyivs;R?kC5lH(F7m#ehjF$b7Oed@Gd5(U{fyV*;W?qi;&^eA|CF zg5x~~!f~{kHQ(pGdT9YzKCdU{{4DN621l?Z&q6Y6<1@gu@q)?Yjlag+lGPytH8jX zE(bnNy9&ROwl-27I)lBKgoKZ9>A+he9puJ_m0vVaUm^QjS_H~(5&^(IlSxIZr={I{P}W9ic`m+J*QoYwq%QHk|O-u zivF^oy}l8yHgZ-axulZtKUyHOecU+4ike0LU*SqMboR=0sFL6vDIr5|`8Io`GMxYx zi_XZ`sxa3D(4S>^~tpPw87TZN}!tt`Dcl2{!HQd|aIl;jO@Rm5J9 z(4vtku7z>AXBLgJYn?pn!7e3C-@C5tGXrUUA5|zGLVE+C0pzvvs_K|J~$z|S#)ZCP&D?b1#RY zV{MIoN^O{_^FEr(NUIfS^Y^=8`m>=lu`IlmU-J^T_((h&yyEZjClWK@Y8Id~KMb#` z!_HWCps1_(Nzzgz6gPgyVnqT<$xjWs(yOLyNUA76o<_1Y7-)-k&4wd0j&dgCPU9Q+>|) zVh#4Xs@>B`8UxP2uC<}8;ij;R5WZx>;p7M+mX5Q~Ag&?N(EnB9gJ+Jvd{y;pgDVj5 zGQFQKp6-^I)ibaNF)%O&26nf$W|j;o3Ur=s0mjqYlT~I3zV~-m05$wF_~h`gqOx-9 zNzDJ!9*{)d`s=ydc~EeCx^tpG29QqQ{>kit zt2oNGY_HvmG6bQ<5$*Yj0Lf$m_;dl5Wvzo#T=)E#dPm|xWv4PU9#_>rFnm;s_^2gXipNZrDxuZ&K$S=2P-6f z>Mo>eZ-7SadUPWDy`B{hbhz_+AHyn*=BQJfD-mL_dwe9V-6QQjqV);3aQE-{XbSNWqn z9E?L1vX79AA&cx(;kcvRyL1dBMTX;7g#T)mDDRCGEjyfr+IbhdIAY?H9F2l3{`7}@fA1aH*F(Xl*RU?6(D&NnDugOeT$4QE6O91op$lPu=QCF@DI62nVg z(IP1`MOz`2;)7Zr>R*q_CkbBLwJSG*v8wBvr61H+^&y@pI4EL3c9r+z5fvpTC^&rA zHvf5LV}l@=o0ZiM&|G~V7&y;Y2$wD(EeL6JoGKvUFfuSQid86iHTRKC;Ja0)_ugnS zV{Y!tVJskX8`oQDs;M;rP8WjCX*ejqX3isKXV$sadn3vv0;&F&uTk=(&vq=bgC4Al zMgp>-{JfVl2h!-Jjh*H$xZgqex35$R1r;P_f(5ISnhgIYJ3AYd_pdddkxPBUJWGY& z8T>}hOLz(jwh9UUsgzx7@DF+I5OCE#6I|eT*8Ma0Bvr<|mf`Kj8~v)!pYQ$@1*1nW z3JQ`;+x+NzKM*b;)foH~L}HBn)CC}2jjj9vM`5Sni)beaMTr^tw@N|+0s_3efd2c> zgTKIb@7uR;fomo8#ar0r@^{Fr-fmhe$^R~P#~#umTRrir)c9j`?IhxFyz7kn*$)|M zx}-UsTxHrq_c_f<{oIzv(YYihfQ(P zRA1gc%-C8m

    3_*tpFnEp2b<%6ryI=f~d%B^$Ks{4Wi?2kSb9=TB9YX%m?Xqpdy zDorl_a)9vd%Wqp`k!^ik71s87^F~oYp@Q20kC5=w7TClV1zpH(m8l1<=X9XI&&&Wd zOhiNk%PrX5-QDvMPznF{PyJQ^Snb?*6aH*1bsg^Y_wG2D{o(aX=s3Mo-Po`+t02Pj z&&;A$uu*`m(x+A_av)facCgD)WWDb-d#g!Ap4+_tVEe7FuBx_9j}Du}g>(#ueB7|I ztHmR|vMcGc^v?^80&}){c7Ju$Y>6`tg9%n148HN`uzK|GqN+R$jv=-Y z{Pa*5{AlVoG%x_VrDI_M9$Q>eP8}ySd_Pp>tE;Pd!E(4Luub%k=7YZcP*IqC89n4` z^vDy`TVXPLP{PsMQYc@p{8WFyM*gn85tJb>9JPp0jb8PWL%h}@a!nF_v_Ec4iB)EP zlgJ!v`#^G+Ru!v^Fj;8rEmWkGo?x;jkmU-m`A|CDQc>nB_qt?x6~IF`l9@4FQ0DkrN9(f$QA$WbA;pHIKH6MFU!N4{a!($rj7 zSlA0*-Pm}XQKki|2@T8N{sTaW=FH6P?2P)^{$4Bi18rqd4++@q*}dz z#F~-f3T14D#=5b5GE&}Ca2s8W!M>i4iOZ+HMl!+QPX zu%JyM5fbtS-t?ICVyr~+y3hHnDQlB0RW8x>RS@PRHkQjDF39&z#n6TW%2bL@f{hXwZyn^~GN)_XW0+(}O6nxUGV5YFs zWJbS4p3}E&M|&tYLR9mb|KwQXPaON#(;`ZT@wV)ue`e)dZ}n}f(!LGYmmj(OW@m=S zrFM4D^wu0&kqkWWG#}cF-JaC!o|m9VDRxKvhz-NwazbOpZZ9me zyons=TW6Mr)pA~%hyAytu(!miQz7S)6aS{5^}(VqlU384C2wNI{6R`DUzT@vc7}$^ z+S-0*DBauJlc5J{i76lcY8oK0FsSu#-~SN`R`@sLC{4X{^uXSA$1T0+B2IXQ`D@2D zcWG`$imI9Ne3Ow|`h>)nU~GR^Evn0}MAM;nmk}zV+Hlgwrqp0bb6d+`ojuOBH`jJD}Om@9fcU?63GWO^_g%z%_O{|^pLHowX`_R`a zHWmGJXrgEb78#6d?5SwDyzM)0E3F-IVqvW?HhVTg0+xz@dcby$vp5FwX6<9mnig{; zC;)l#Rm6$NwSPP$#O>P3s?dl<-}zPVZ0~xi+HRV~>=)AW@+R~0)Z87GI;t?NcGs^^ z�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 Coqm4+ diff --git a/docs/matplotlib-import-support.md b/docs/matplotlib-import-support.md new file mode 100644 index 0000000..5bef77d --- /dev/null +++ b/docs/matplotlib-import-support.md @@ -0,0 +1,209 @@ +# Matplotlib import support and roadmap + +`Canvas.from_matplotlib(source, strict=False, **canvas_kwargs)` accepts an +in-memory Matplotlib figure, axes, or rectangular axes array. This checklist +covers the intended import surface. It is a roadmap, not a promise that every +item already works on every rendering backend. + +Checked items are implemented within their stated scope. Unless explicitly +stated otherwise, fidelity checks below refer to the **Matplotlib backend**. +Complex built-in artists are retained as detached, editable native geometry; +these entries deliberately raise on backends that cannot represent them. +Portable entries continue to support the other renderers, with the limitations +listed below. Unchecked items are +pending or partial. Importing drawn geometry does not recover the original +samples, plotting call, callback, or statistical model. Matplotlib, Plotly, +TikZ, and plotext have different rendering capabilities; import success does +not guarantee that every backend can render the result identically. + +## Inputs and ownership + +- [x] Whole `Figure`, individual `Axes`, flat lists/arrays, and 2D lists/arrays. +- [x] Explicit 2D array order; infer ordinary grids from flat input. +- [x] Reject empty, ragged, non-axes, duplicate, and mixed-figure inputs. +- [x] Snapshot plot arrays and styles without reparenting source artists. +- [x] Canvas keyword overrides for figure size, DPI, and other canvas options. +- [x] Warnings for recognized unsupported content; strict mode raises. +- [x] Structured import report with artist identities, severity, and fallbacks. +- [ ] Complete detection of unsupported style properties in strict mode. +- [ ] Version compatibility matrix and fixtures across supported Matplotlib versions. +- [x] Optional serialized figure input with an explicit trust boundary. + +PNG/JPEG input and reconstruction of original data from PDF/SVG are separate +features, not part of this object importer. Arbitrary custom Python artists +need an adapter API or an explicit raster fallback; universal semantic +conversion is not possible. + +## Figure and layout + +- [x] Ordinary rectangular subplot grids and individual axes. +- [x] Figure dimensions and export DPI defaults. +- [x] Figure title and shared axis label text. +- [x] One `twinx` and one `twiny` per primary subplot; import only selected axes. +- [ ] Full twin-axis spine positioning, multiple twins, and cross-backend parity. + Matplotlib supports multiple twins and spine positions; backend parity is pending. +- [x] Shared-axis relationships and linked limits after import. +- [x] Preserve omitted/empty grid cells without drawing extra empty axes. +- [x] Spanning cells, subplot mosaics, nested GridSpec, and subfigures + (subfigure rectangles/decorations are flattened into the destination figure). +- [x] Grid width/height ratios, margins, spacing, constrained/tight layout + for ordinary/nested GridSpec; subfigures retain their snapshot rectangles. +- [x] Arbitrary axes rectangles, overlapping axes, inset axes, inset-zoom + connectors (`indicate_inset_zoom`/`indicate_inset`, Matplotlib 3.10+). The + connector spans two Axes and recomputes its geometry from live limits on + every draw, so it is rebuilt against the reconstructed parent/inset pair + rather than snapshotted. Matplotlib < 3.10's tuple-returning API still falls + back to native geometry. +- [x] Secondary axes with forward/inverse coordinate functions. +- [x] Figure backgrounds, frame styling, and figure-level text/patches/images. +- [x] Figure title/shared-label typography and placement. +- [x] Figure-level legends and shared colorbars. + +Irregular grids use one row of addressable Canvas slots and retain the source +axes rectangles when rendered with Matplotlib. A 2D input array defines its own +layout; slots occupied only by a twin remain empty rather than drawing extra axes. +Ordinary/nested GridSpec layout engines reflow on resize. Subfigure layout-engine +reflow and full cross-backend layout parity remain pending. + +## Lines, markers, and collections + +- [x] Numeric 2D lines, markers, NaN gaps, line/marker colors and widths. +- [x] Step draw styles in imported line entries (Matplotlib rendering). +- [x] Horizontal/vertical reference lines with fractional extents (Matplotlib). +- [x] Plain line collections, including `hlines`/`vlines`, as individual lines. +- [x] Standard scatter marker shapes, sizes, scalar colors, and per-point colors. +- [x] Scatter colormaps and normalization copied for Matplotlib rendering. +- [x] Preserve date/time, categorical, quantity/unit converters and formatters. +- [x] Custom dash sequences, cap/join styles, markevery, and gap colors. +- [x] Infinite `axline` semantics, event plots, stem containers. +- [x] Scalar-mapped line collections, offset collections, per-point transforms. +- [ ] Custom scatter paths and hollow markers on every backend. +- [ ] Match scatter area/size semantics and normalization on every backend. +- [x] Preserve ordering between artist types at equal z-order. + +## Bars, errors, fills, and statistical plots + +- [x] Vertical/horizontal bars, positions, dimensions, baselines, and styling. +- [x] Stacked/grouped bars as their resolved rectangles. +- [x] Histogram bars as geometry (original samples are unavailable). +- [x] Error-bar containers with data lines: symmetric/asymmetric x/y errors, + caps, colors, widths, and labels; avoid duplicated component artists. +- [x] Error-only plots and bar errors as line/cap geometry, without inventing + asymmetric centers that the source no longer retains. +- [x] Simple data-coordinate polygon collections, including `fill_between`, + `fill_betweenx`, and stackplot regions, as filled polygon geometry. +- [x] Data-coordinate polygon patches and stairs values/edges/baseline. +- [x] Error limits/arrows, subsampled errors, and independently styled components. +- [ ] Restore semantic fill boundaries/where masks when recoverable. + Drawn paths and retained container metadata are preserved; Matplotlib usually + does not retain the original where-mask or samples. +- [x] Spans in blended coordinates; general rectangles, circles, ellipses, wedges. +- [x] Compound polygons, holes, curved paths, arbitrary PathPatch geometry. +- [x] Box plots, violin plots, pie/donut charts, hist2d, hexbin. +- [x] Preserve statistical groupings when containers/metadata retain them: + `subplot.import_groups` and entry `source_container_ids` retain bar, errorbar, + and stem memberships, labels, orientation, and available data values. + +## Images, fields, and colorbars + +- [x] `imshow` arrays, extent, origin, colormap, normalization, interpolation + copied into entries (Matplotlib rendering). +- [ ] Match image extent/origin, RGB(A), masks, alpha, and interpolation on all backends. +- [ ] Nonlinear normalization, clim, under/over/bad colors on all backends. +- [x] `pcolor`, `pcolormesh`, nonuniform grids, QuadMesh, triangular meshes. +- [x] Contours, filled contours, levels, labels, and contour topology. +- [x] Quiver, barbs, streamplots, vector-field keys. +- [x] Axes colorbars tied to the correct image/scatter/mesh mappable. +- [x] Colorbar orientation, label, ticks, limits, extend, and normalization. +- [x] Multiple/shared colorbars and explicit colorbar axes. + +## Text and annotations + +- [x] Data-coordinate text, font family/size/weight/style, color, alignment, + rotation (backend support varies). +- [x] Data-coordinate annotations with optional arrows and independent copied + arrow properties; references to other patches are explicitly rejected. +- [x] Center title and x/y labels with basic typography and label padding. +- [x] Text boxes, multiline spacing, math/TeX fidelity, wrapping, clipping + through Matplotlib (TeX still requires the source environment’s TeX setup). +- [ ] Axes/figure fractions, point/pixel offsets, blended and callable coordinates. +- [ ] Annotation arrows with full backend parity and artist-relative coordinates. +- [x] Left/right titles, custom title/label positions, offset text. +- [x] AnnotationBbox, OffsetBox, tables, and other composite text artists. + +## Axes, ticks, grids, and legends + +- [x] Limits including reversed limits; default linear/log scales. +- [x] Explicit major tick locations/labels with FixedLocator. +- [x] Basic grid visibility, axes visibility, background, aspect, axisbelow. +- [x] Basic per-axes legend visibility and artist labels. +- [x] Non-default log bases; symlog, logit, asinh, function/custom scales. +- [x] Automatic/fixed minor ticks, locator/formatter configuration and units. +- [x] Tick placement, direction, length, width, color, rotation, and font styling. +- [x] Per-axis major/minor grid visibility and line styling. +- [x] Spine visibility, colors, widths, bounds, and positions. +- [x] Autoscale flags, sticky edges, margins, adjustable/anchor/box aspect. +- [x] Legend order, renamed labels, proxy handles, multiple legends, grouping. +- [x] Legend location/anchor, columns, title, typography, frame and spacing. + +## Transforms, metadata, and advanced axes + +- [x] General affine/nonlinear/blended transforms, coordinate rebinding. +- [x] Clip paths/boxes, path effects, rasterization, sketch settings, filters. +- [x] Hidden artists retained as hidden editable entries. +- [ ] Artist IDs, URLs, picking, metadata, and accessibility descriptions. +- [x] Polar, geographic/custom projections, axisartist and parasite axes via + native axes snapshots; custom projection classes must remain available. +- [x] 3D lines/scatter, surfaces, wireframes, collections, camera/projection. +- [ ] Animations, widgets, callbacks, and interactive state (separate adapters). + Static artist state is copied; arbitrary callback closures and GUI event loops + cannot be reconstructed from a figure’s drawn geometry. +- [x] Optional raster fallback for unsupported artists with explicit loss reporting. + +## Validation and next priorities + +- [x] Tests for all three input forms, source independence, strict/warning modes. +- [x] Matplotlib reconstruction tests for supported geometry and axis settings. +- [x] Plotly smoke/geometry tests for representative supported imports. +- [x] Image comparisons with tolerances, plus representative portable export + tests for Matplotlib PNG/SVG, Plotly HTML, plotext text, and TikZ source. +- [x] Large figures, empty/masked data, performance and memory benchmarks. + +New import controls: + +```python +canvas = Canvas.from_matplotlib(fig, strict=True) +report = canvas.import_report.to_dict() # identities, severity, fallback, backends +canvas = Canvas.from_matplotlib("figure.pickle", trusted=True) +canvas = Canvas.from_matplotlib(fig, fallback="raster") +``` + +`trusted=True` is required before any pickle is read. Pickles can execute code; +load only files whose producer you trust and use a matching Matplotlib version. +`fallback="native"` (default) retains built-in native geometry with rebound +transforms. `fallback="skip"` warns (or raises in strict mode) for those artists. +`fallback="raster"` snapshots the selected figure content and records the loss of +editable data and vector geometry; it supports Matplotlib and Plotly. Callback +functions and custom scales remain Python objects, not portable serialization. +Strict mode checks import losses, not universal rendering parity. + +Remaining priorities are cross-backend fidelity, subfigure reflow, portable +projection adapters, and live interaction adapters. Source metadata does not +usually retain original fill masks or box/violin samples; these are imported as +geometry without inventing lost statistical inputs. + +Reference APIs: [Matplotlib artists](https://matplotlib.org/stable/tutorials/artists.html), +[containers](https://matplotlib.org/stable/api/container_api.html), and +[annotations](https://matplotlib.org/stable/users/explain/text/annotations.html). + + +Compatibility fixtures live in `test_matplotlib_import.py` and +`test_matplotlib_import_extended.py`. The CI matrix covers Matplotlib 3.8, 3.9, +3.10 and 3.11 on Python 3.11. Snapshots use version-sensitive Matplotlib state; +there is no cross-version pickle compatibility promise. + +Run `python scripts/benchmark_matplotlib_import.py --points 100000 --panels 4` +for reproducible timing/allocation observations. A local Matplotlib 3.11.1 run +imported 400,000 points in 0.41 s with 8.75 MiB peak Python allocations and rendered +in 0.79 s. These are observations, not performance guarantees; native allocations +outside Python are excluded from the memory measurement. diff --git a/docs/source/index.rst b/docs/source/index.rst index e1bd3ab..82a2734 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -31,3 +31,4 @@ documentation for details. tutorials/tutorial_14_axis_and_layout_controls tutorials/tutorial_15_tikzfigure_subplots tutorials/tutorial_16_plotext_advanced + tutorials/tutorial_17_xarray diff --git a/pyproject.toml b/pyproject.toml index e7c49cf..5c3163a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "maxplotlibx" -version = "0.1.9" +version = "0.2.0" description = "A reproducible plotting module with various backends and export options." readme = "README.md" requires-python = ">=3.8" @@ -19,14 +19,19 @@ dependencies = [ "pint", "plotly", "plotext >= 6.0, < 7", - "tikzfigure[vis]>=0.3.0", + "tikzfigure[vis]>=0.4.0", ] [project.optional-dependencies] test = [ "pytest", "coverage", + "xarray", +] +xarray = [ + "xarray", ] docs = [ + "xarray", "myst-parser", "sphinx", "sphinx-rtd-theme", diff --git a/scripts/benchmark_matplotlib_import.py b/scripts/benchmark_matplotlib_import.py new file mode 100644 index 0000000..1121383 --- /dev/null +++ b/scripts/benchmark_matplotlib_import.py @@ -0,0 +1,58 @@ +"""Measure import time and peak Python allocation for reproducible fixtures. + +Run from an installed checkout: python scripts/benchmark_matplotlib_import.py +Timings are observations, not brittle pass/fail thresholds. Native renderer +allocations outside Python are not included in tracemalloc's peak measurement. +""" + +import argparse +import json +import time +import tracemalloc + +import matplotlib + +matplotlib.use("Agg") +import matplotlib.pyplot as plt +import numpy as np + +from maxplotlib import Canvas + + +def benchmark(points, panels): + fig, axes = plt.subplots(panels, 1, figsize=(8, max(3, panels * 2))) + x = np.linspace(0, 10, points) + for index, ax in enumerate(np.atleast_1d(axes)): + ax.plot(x, np.sin(x + index)) + ax.scatter([], []) + tracemalloc.start() + started = time.perf_counter() + canvas = Canvas.from_matplotlib(fig, strict=True) + imported = time.perf_counter() + _, peak = tracemalloc.get_traced_memory() + tracemalloc.stop() + result, _ = canvas.render() + result.canvas.draw() + finished = time.perf_counter() + stats = dict( + matplotlib=matplotlib.__version__, + points_per_panel=points, + panels=panels, + import_seconds=imported - started, + render_seconds=finished - imported, + import_peak_python_mib=peak / 1024**2, + entries=sum(len(plot.line_data) for _, _, plot in canvas.iter_subplots()), + ) + plt.close(fig) + plt.close(result) + return stats + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--points", type=int, default=100_000) + parser.add_argument("--panels", type=int, default=4) + arguments = parser.parse_args() + if arguments.points < 1 or arguments.panels < 1: + parser.error("points and panels must be positive") + print(json.dumps(benchmark(arguments.points, arguments.panels), indent=2)) diff --git a/src/maxplotlib/backends/matplotlib/import_state.py b/src/maxplotlib/backends/matplotlib/import_state.py new file mode 100644 index 0000000..286dedc --- /dev/null +++ b/src/maxplotlib/backends/matplotlib/import_state.py @@ -0,0 +1,342 @@ +"""Detached Matplotlib state used by the object importer. + +Transforms must be rebound, not frozen in source display coordinates. The +snapshot memo cuts ownership links before copying and replaces them with tokens; +each render binds those tokens to its new figure/axes. No source artists are +reparented, and repeated renders do not share mutable Matplotlib state. +""" + +import copy +from dataclasses import asdict, dataclass, field + +from matplotlib.artist import Artist +from matplotlib.cbook import CallbackRegistry +from matplotlib.collections import Collection +from matplotlib.image import AxesImage +from matplotlib.lines import Line2D +from matplotlib.patches import Patch +from matplotlib.table import Table +from matplotlib.text import Text +from matplotlib.transforms import Bbox, BboxTransformTo + + +@dataclass +class ImportDiagnostic: + artist_id: int + artist_type: str + label: str + severity: str + message: str + fallback: str | None = None + backends: tuple = ("matplotlib",) + + +@dataclass +class ImportReport: + """Import decisions, including losses and backend-specific representations.""" + + diagnostics: list = field(default_factory=list) + + def add( + self, + artist, + message, + *, + severity="info", + fallback=None, + backends=("matplotlib",), + ): + item = ImportDiagnostic( + id(artist), + type(artist).__name__, + str(getattr(artist, "get_label", lambda: "")()), + severity, + message, + fallback, + backends, + ) + self.diagnostics.append(item) + return item + + def to_dict(self): + return asdict(self) + + +def root_figure(figure): + while getattr(figure, "figure", figure) is not figure: + figure = figure.figure + return figure + + +def figure_bounds(ax, figure): + return tuple( + ax.get_position(original=True) + .transformed(ax.figure.transSubfigure) + .transformed(figure.transFigure.inverted()) + .bounds + ) + + +def _bindings(ax, fig): + bindings = { + "figure": fig, + "transFigure": fig.transFigure, + "transSubfigure": fig.transSubfigure, + "dpi_scale_trans": fig.dpi_scale_trans, + "fig_bbox": fig.bbox, + } + if ax is not None: + bindings.update( + axes=ax, + transData=ax.transData, + transAxes=ax.transAxes, + transScale=ax.transScale, + transLimits=ax.transLimits, + ax_bbox=ax.bbox, + xaxis=ax.xaxis, + yaxis=ax.yaxis, + xaxis_transform=ax.get_xaxis_transform(), + yaxis_transform=ax.get_yaxis_transform(), + ) + return bindings + + +class _BindingToken: + """Weak-referenceable marker for copied Transform parent links.""" + + +class ReboundSnapshot: + """Copy a payload without retaining its owning axes, figure or callbacks.""" + + def __init__(self, payload, ax=None, fig=None): + owner = fig if fig is not None else ax.figure + fig = root_figure(owner) + self.owner_bounds = None + self.tokens = {} + memo = {} + bindings = _bindings(ax, fig) + if owner is not fig: + self.owner_bounds = tuple( + owner.bbox.transformed(fig.transFigure.inverted()).bounds + ) + bindings.update(owner=owner, owner_transform=owner.transSubfigure) + for name, value in bindings.items(): + # Some canonical transforms are aliases of one another. + if id(value) not in memo: + self.tokens[name] = memo[id(value)] = _BindingToken() + artists = [] + if isinstance(payload, Artist): + artists = payload.findobj() + for artist in artists: + for name in ("_remove_method", "stale_callback"): + value = getattr(artist, name, None) + if value is not None: + memo[id(value)] = None + callbacks = getattr(artist, "_callbacks", None) + if callbacks is not None: + memo[id(callbacks)] = CallbackRegistry() + self.payload = copy.deepcopy(payload, memo) + + def clone(self, ax=None, fig=None): + fig = fig if fig is not None else ax.figure + bindings = _bindings(ax, fig) + if self.owner_bounds is not None: + bindings.update( + owner=fig, + owner_transform=BboxTransformTo(Bbox.from_bounds(*self.owner_bounds)) + + fig.transFigure, + ) + return copy.deepcopy( + self.payload, + {id(token): bindings[name] for name, token in self.tokens.items()}, + ) + + def draw(self, ax, **overrides): + artist = self.clone(ax) + artist.set(**overrides) + clipbox, clippath = artist.get_clip_box(), artist.get_clip_path() + # add_* establishes the new removal and stale callbacks. + if isinstance(artist, Collection): + ax.add_collection(artist, autolim=False) + elif isinstance(artist, Line2D): + ax.add_line(artist) + elif isinstance(artist, Patch): + ax.add_patch(artist) + elif isinstance(artist, AxesImage): + ax.add_image(artist) + elif isinstance(artist, Table): + ax.add_table(artist) + elif isinstance(artist, Text): + ax._add_text(artist) + else: + ax.add_artist(artist) + artist.set_clip_path(clippath) + artist.set_clip_box(clipbox) + return artist + + +def capture_axis_state(ax): + """Capture decorations which have no backend-neutral equivalent yet.""" + state = {"axes": {}, "spines": {}, "titles": {}} + for name in ("x", "y"): + axis = getattr(ax, name + "axis") + state["axes"][name] = dict( + scale=ReboundSnapshot(axis._scale, ax), + major_locator=ReboundSnapshot(axis.get_major_locator(), ax), + minor_locator=ReboundSnapshot(axis.get_minor_locator(), ax), + major_formatter=ReboundSnapshot(axis.get_major_formatter(), ax), + minor_formatter=ReboundSnapshot(axis.get_minor_formatter(), ax), + units=copy.deepcopy(axis.get_units()), + converter=copy.deepcopy( + axis.get_converter() + if hasattr(axis, "get_converter") + else axis.converter + ), + major_kw=_tick_params(axis, "major"), + minor_kw=_tick_params(axis, "minor"), + ticks={ + which: [ + _tick_style(tick) + for tick in getattr(axis, "get_" + which + "_ticks")() + ] + for which in ("major", "minor") + }, + label_position=axis.get_label_position(), + offset_style=_text_properties(axis.get_offset_text()), + ) + for name, spine in ax.spines.items(): + state["spines"][name] = dict( + visible=spine.get_visible(), + edgecolor=spine.get_edgecolor(), + linewidth=spine.get_linewidth(), + linestyle=spine.get_linestyle(), + bounds=spine.get_bounds(), + ) + if spine.spine_type in ("left", "right", "top", "bottom"): + state["spines"][name]["position"] = copy.deepcopy(spine.get_position()) + for loc, title in ( + ("left", ax._left_title), + ("center", ax.title), + ("right", ax._right_title), + ): + state["titles"][loc] = ( + title.get_text(), + _text_properties(title), + title.get_position(), + ) + state["autotitlepos"] = ax._autotitlepos + state["label_coords"] = { + name: ( + axis.label.get_position(), + ReboundSnapshot(axis.label.get_transform(), ax), + ) + for name, axis in (("x", ax.xaxis), ("y", ax.yaxis)) + if not axis._autolabelpos + } + return state + + +def _text_properties(text): + return dict( + fontproperties=copy.deepcopy(text.get_fontproperties()), + color=text.get_color(), + rotation=text.get_rotation(), + rotation_mode=text.get_rotation_mode(), + horizontalalignment=text.get_ha(), + verticalalignment=text.get_va(), + visible=text.get_visible(), + usetex=text.get_usetex(), + ) + + +def _tick_params(axis, which): + tick = getattr(axis, "get_" + which + "_ticks")(1)[0] + defaults = dict( + length=tick._size, + width=tick._width, + pad=tick._base_pad, + direction=tick._tickdir, + color=tick.tick1line.get_color(), + ) + defaults.update(copy.deepcopy(getattr(axis, "_" + which + "_tick_kw"))) + return defaults + + +def _tick_line_style(line): + return { + name: getattr(line, "get_" + name)() + for name in ( + "color", + "marker", + "markersize", + "markeredgewidth", + "markeredgecolor", + "visible", + "zorder", + ) + } + + +def _tick_style(tick): + return dict( + tick1line=_tick_line_style(tick.tick1line), + tick2line=_tick_line_style(tick.tick2line), + label1=_text_properties(tick.label1), + label2=_text_properties(tick.label2), + gridline=dict( + visible=tick.gridline.get_visible(), + color=tick.gridline.get_color(), + linewidth=tick.gridline.get_linewidth(), + linestyle=tick.gridline.get_linestyle(), + alpha=tick.gridline.get_alpha(), + ), + ) + + +def apply_axis_state(ax, state, *, units=True): + for name, props in state["spines"].items(): + props = copy.deepcopy(props) + bounds = props.pop("bounds") + ax.spines[name].set(**props) + if bounds is not None: + ax.spines[name].set_bounds(*bounds) + for name, settings in state["axes"].items(): + axis = getattr(ax, name + "axis") + # Scales are installed before locators/formatters: changing a scale + # installs defaults and would otherwise erase the imported tick setup. + getattr(ax, "set_" + name + "scale")(settings["scale"].clone(ax)) + if units: + converter = copy.deepcopy(settings["converter"]) + if hasattr(axis, "set_converter"): + if not getattr(axis, "_converter_is_explicit", False): + axis.set_converter(converter) + else: + axis.converter = converter + axis.set_units(copy.deepcopy(settings["units"])) + for kind in ("major", "minor"): + getattr(axis, "set_" + kind + "_locator")( + settings[kind + "_locator"].clone(ax) + ) + getattr(axis, "set_" + kind + "_formatter")( + settings[kind + "_formatter"].clone(ax) + ) + axis.set_tick_params(which=kind, **copy.deepcopy(settings[kind + "_kw"])) + ticks = getattr(axis, "get_" + kind + "_ticks")() + styles = settings["ticks"][kind] + for i, tick in enumerate(ticks): + if styles: + style = styles[min(i, len(styles) - 1)] + for part, props in style.items(): + getattr(tick, part).set(**props) + axis.set_label_position(settings["label_position"]) + axis.get_offset_text().set(**settings["offset_style"]) + for loc, (text, props, position) in ( + state["titles"].items() if hasattr(ax, "set_title") else () + ): + title = ax.set_title(text, loc=loc, **props) + title.set_position(position) + ax._autotitlepos = state["autotitlepos"] + for name, (position, transform) in state["label_coords"].items(): + getattr(ax, name + "axis").set_label_coords( + *position, transform=transform.clone(ax) + ) diff --git a/src/maxplotlib/backends/matplotlib/importer.py b/src/maxplotlib/backends/matplotlib/importer.py new file mode 100644 index 0000000..15036b2 --- /dev/null +++ b/src/maxplotlib/backends/matplotlib/importer.py @@ -0,0 +1,1078 @@ +"""Translate Matplotlib artists into backend-independent canvas entries.""" + +import copy +import os +import pickle +import warnings + +import numpy as np +from matplotlib.artist import Artist +from matplotlib.axes import Axes +from matplotlib.collections import LineCollection, PathCollection, PolyCollection +from matplotlib.container import BarContainer, ErrorbarContainer, StemContainer +from matplotlib.figure import Figure +from matplotlib.legend import Legend +from matplotlib.lines import AxLine +from matplotlib.markers import MarkerStyle +from matplotlib.patches import Polygon, StepPatch +from matplotlib.path import Path +from matplotlib.text import Annotation +from matplotlib.ticker import FixedLocator +from matplotlib.transforms import IdentityTransform + +try: + from matplotlib.inset import InsetIndicator +except ImportError: # Matplotlib < 3.10 returns a plain (Rectangle, patches) tuple. + InsetIndicator = None + +from .import_state import ( + ImportReport, + ReboundSnapshot, + capture_axis_state, + figure_bounds, + root_figure, +) + + +def import_matplotlib( + canvas_cls, + source, + *, + strict=False, + trusted=False, + fallback="native", + **canvas_kwargs, +): + if fallback not in ("native", "skip", "raster"): + raise ValueError("fallback must be 'native', 'skip', or 'raster'") + if isinstance(source, (str, bytes, os.PathLike)) or hasattr(source, "read"): + if not trusted: + raise ValueError( + "Serialized Matplotlib input requires trusted=True; " + "unpickling can execute arbitrary Python code" + ) + if isinstance(source, bytes): + source = pickle.loads(source) + elif hasattr(source, "read"): + source = pickle.load(source) + else: + with open(source, "rb") as stream: + source = pickle.load(stream) + report = ImportReport() + + def unsupported(message, artist=None): + artist = artist if artist is not None else unsupported.artist + report.add( + artist, + message, + severity="error" if strict else "warning", + fallback="skipped", + ) + if strict: + error = NotImplementedError(message) + error.import_report = report + raise error + warnings.warn(message, UserWarning, stacklevel=3) + + unsupported.artist = source + unsupported.report = report + unsupported.fallback = fallback + + whole_figure = isinstance(source, Figure) + explicit_shape = None + if whole_figure: + axes = list(source.axes) + elif isinstance(source, Axes): + axes = [source] + explicit_shape = (1, 1) + else: + try: + array = np.asarray(source, dtype=object) + except (TypeError, ValueError) as exc: + raise TypeError( + "Expected a Figure, Axes, or rectangular array of Axes" + ) from exc + if array.ndim not in (1, 2): + raise TypeError("Expected a Figure, Axes, or 1D/2D array of Axes") + axes = list(array.flat) + if array.ndim == 2: + explicit_shape = array.shape + if not axes: + raise ValueError("At least one Matplotlib Axes is required") + if not all(isinstance(ax, Axes) for ax in axes): + raise TypeError("Every array element must be a Matplotlib Axes") + if len({id(ax) for ax in axes}) != len(axes): + raise ValueError("Duplicate Axes are not supported") + figure = root_figure(source if whole_figure else axes[0].get_figure()) + if any(root_figure(ax.get_figure()) is not figure for ax in axes): + raise ValueError("All Axes must belong to the same Figure") + + if fallback == "raster": + return _raster_import( + canvas_cls, figure, axes, whole_figure, report, **canvas_kwargs + ) + + # Colorbar axes are decorations, not independent data subplots. + selected = [] + twins = [] + colorbars = [] + # The source figure's creation order identifies the primary axes even when + # the caller passes the twin first in an array. + for index, ax in sorted( + enumerate(axes), key=lambda item: figure.axes.index(item[1]) + ): + if getattr(ax, "_colorbar", None) is not None: + colorbars.append(ax._colorbar) + else: + parent = next( + ( + previous + for _, previous in selected + if ax in previous._twinned_axes.get_siblings(previous) + ), + None, + ) + if parent is None: + selected.append((index, ax)) + else: + direction = "x" if ax.get_shared_x_axes().joined(ax, parent) else "y" + twins.append((parent, ax, direction)) + selected.sort(key=lambda item: item[0]) + if not selected: + raise ValueError("No supported Axes to import") + + if explicit_shape is not None: + nrows, ncols = explicit_shape + positioned = [(i // ncols, i % ncols, ax) for i, ax in selected] + else: + specs = [ax.get_subplotspec() for _, ax in selected] + common_grid = specs[0].get_gridspec() if specs[0] is not None else None + simple_grid = common_grid is not None and all( + spec is not None + and spec.get_gridspec() is common_grid + and spec.rowspan.stop - spec.rowspan.start == 1 + and spec.colspan.stop - spec.colspan.start == 1 + for spec in specs + ) + if simple_grid and len({spec.num1 for spec in specs}) == len(specs): + nrows, ncols = common_grid.get_geometry() + positioned = [ + (spec.rowspan.start, spec.colspan.start, ax) + for spec, (_, ax) in zip(specs, selected) + ] + else: + # Irregular layouts use stable editable slots and retain their + # actual axes rectangles for the Matplotlib renderer. + nrows, ncols = 1, len(selected) + positioned = [(0, i, ax) for i, (_, ax) in enumerate(selected)] + + if "nrows" in canvas_kwargs or "ncols" in canvas_kwargs: + raise TypeError("nrows and ncols are inferred from the imported axes") + canvas_kwargs.setdefault("figsize", tuple(figure.get_size_inches())) + canvas_kwargs.setdefault("dpi", figure.dpi) + canvas = canvas_cls(nrows=nrows, ncols=ncols, **canvas_kwargs) + canvas.import_report = report + canvas._import_layout = {} + canvas._import_in_layout = {} + canvas._import_shared_axes = [] + canvas._import_extra_twins = [] + canvas._import_colorbars = [] + canvas._import_figure_artists = [] + source_targets = {} + source_slots = {} + for row, col, ax in positioned: + target = canvas.add_subplot(row=row, col=col) + source_targets[ax] = target + source_slots[ax] = (row, col) + if explicit_shape is None: + canvas._import_layout[(row, col)] = figure_bounds(ax, figure) + canvas._import_in_layout[(row, col)] = ax.get_in_layout() + _import_axes(ax, target, unsupported) + for parent, twin, direction in twins: + if parent is ax: + factory = canvas.twinx if direction == "x" else canvas.twiny + registry = ( + canvas._twinx_subplots + if direction == "x" + else canvas._twiny_subplots + ) + if (row, col) in registry: + from maxplotlib.subfigure.line_plot import LinePlot + + twin_target = LinePlot() + canvas._import_extra_twins.append( + ((row, col), direction, twin_target) + ) + else: + twin_target = factory(row=row, col=col) + source_targets[twin] = twin_target + _import_axes(twin, twin_target, unsupported) + if explicit_shape is None and all(ax.figure is figure for _, ax in selected): + specs = {source_slots[ax]: ax.get_subplotspec() for _, ax in selected} + canvas._import_layout_specs = ReboundSnapshot(specs, fig=figure) + canvas._import_layout_engine = copy.deepcopy(figure.get_layout_engine()) + if canvas._import_layout_engine is None: + canvas.subplots_adjust( + **{ + name: getattr(figure.subplotpars, name) + for name in ("left", "right", "bottom", "top", "wspace", "hspace") + } + ) + for name in ("x", "y"): + for index, (_, ax) in enumerate(selected): + parent = next( + ( + previous + for _, previous in selected[:index] + if getattr(ax, "get_shared_" + name + "_axes")().joined( + ax, previous + ) + ), + None, + ) + if parent is not None: + canvas._import_shared_axes.append( + (name, source_slots[parent], source_slots[ax]) + ) + for colorbar in colorbars: + owner = getattr(colorbar.mappable, "axes", None) + if owner is not None and owner not in source_targets: + unsupported("Colorbar mappable is outside the selected axes", colorbar.ax) + continue + canvas._import_colorbars.append( + _capture_colorbar(colorbar, source_targets.get(owner)) + ) + report.add(colorbar.ax, "Colorbar is linked to its imported mappable") + if whole_figure: + canvas._import_figure_patch = ReboundSnapshot(figure.patch, fig=figure) + canvas._import_figure_style = dict( + facecolor=figure.get_facecolor(), + edgecolor=figure.get_edgecolor(), + linewidth=figure.patch.get_linewidth(), + frameon=figure.get_frameon(), + ) + for getter, setter in ( + ("get_suptitle", "suptitle"), + ("get_supxlabel", "supxlabel"), + ("get_supylabel", "supylabel"), + ): + value = getattr(figure, getter, lambda: "")() + if value: + text = getattr(figure, "_" + setter) + getattr(canvas, setter)( + value, + x=text.get_position()[0], + y=text.get_position()[1], + **_text_style(text), + ) + titles = {figure._suptitle, figure._supxlabel, figure._supylabel} + for artist in ( + list(figure.legends) + + list(figure.artists) + + list(figure.lines) + + list(figure.patches) + + list(figure.images) + + [text for text in figure.texts if text not in titles] + ): + canvas._import_figure_artists.append(ReboundSnapshot(artist, fig=figure)) + report.add( + artist, "Figure decoration retained for Matplotlib", fallback="native" + ) + + def subfigure_decorations(owner): + for subfigure in owner.subfigs: + background = ReboundSnapshot(subfigure.patch, fig=subfigure) + background.payload.set_zorder( + min((ax.get_zorder() for ax in figure.axes), default=0) - 1 + ) + canvas._import_figure_artists.append(background) + for artist in ( + list(subfigure.texts) + + list(subfigure.legends) + + list(subfigure.artists) + + list(subfigure.lines) + + list(subfigure.patches) + + list(subfigure.images) + ): + canvas._import_figure_artists.append( + ReboundSnapshot(artist, fig=subfigure) + ) + subfigure_decorations(subfigure) + + subfigure_decorations(figure) + return canvas + + +def _style(artist): + return dict( + label=artist.get_label(), + alpha=copy.deepcopy(artist.get_alpha()), + zorder=artist.get_zorder(), + visible=artist.get_visible(), + gid=artist.get_gid(), + url=artist.get_url(), + rasterized=artist.get_rasterized(), + clip_on=artist.get_clip_on(), + snap=artist.get_snap(), + in_layout=artist.get_in_layout(), + ) + + +def _scatter_marker(path): + # Keep standard marker names portable to the other renderers. + for marker in ( + "o", + "s", + "^", + "v", + "<", + ">", + "D", + "d", + "*", + "+", + "x", + "p", + "h", + "H", + ".", + "P", + "X", + ): + style = MarkerStyle(marker) + candidate = style.get_path().transformed(style.get_transform()) + if np.array_equal(path.vertices, candidate.vertices) and np.array_equal( + path.codes, candidate.codes + ): + return marker + return copy.deepcopy(path) + + +def _line_style(line): + return dict( + color=line.get_color(), + linestyle=( + copy.deepcopy(line._unscaled_dash_pattern) + if line.is_dashed() + else line.get_linestyle() + ), + linewidth=line.get_linewidth(), + marker=None if line.get_marker() in ("None", "", " ") else line.get_marker(), + markersize=line.get_markersize(), + markerfacecolor=line.get_markerfacecolor(), + markeredgecolor=line.get_markeredgecolor(), + markeredgewidth=line.get_markeredgewidth(), + drawstyle=line.get_drawstyle(), + dash_capstyle=line.get_dash_capstyle(), + dash_joinstyle=line.get_dash_joinstyle(), + solid_capstyle=line.get_solid_capstyle(), + solid_joinstyle=line.get_solid_joinstyle(), + markevery=copy.deepcopy(line.get_markevery()), + gapcolor=line.get_gapcolor(), + antialiased=line.get_antialiased(), + markerfacecoloralt=line.get_markerfacecoloralt(), + fillstyle=line.get_fillstyle(), + **_style(line), + ) + + +def _text_style(text): + kwargs = dict( + color=text.get_color(), + fontsize=text.get_fontsize(), + fontweight=text.get_fontweight(), + fontstyle=text.get_fontstyle(), + fontfamily=list(text.get_fontfamily()), + rotation=text.get_rotation(), + ha=text.get_ha(), + va=text.get_va(), + usetex=text.get_usetex(), + rotation_mode=text.get_rotation_mode(), + linespacing=text._linespacing, + fontstretch=text.get_fontproperties().get_stretch(), + fontvariant=text.get_fontproperties().get_variant(), + parse_math=text.get_parse_math(), + wrap=text.get_wrap(), + ) + + box = text.get_bbox_patch() + if box is not None: + kwargs["bbox"] = dict( + boxstyle=copy.deepcopy(box.get_boxstyle()), + facecolor=box.get_facecolor(), + edgecolor=box.get_edgecolor(), + linewidth=box.get_linewidth(), + linestyle=box.get_linestyle(), + alpha=box.get_alpha(), + hatch=box.get_hatch(), + ) + return kwargs + + +def _import_errorbar(container, ax, target, unsupported): + """Return whether the container was consumed as one semantic entry.""" + line, caps, ranges = container.lines + # Without a data line the original center of asymmetric errors is lost. + # Leave these artists for the geometry import instead of inventing centers. + if line is None: + return False + if line.get_transform() != ax.transData or any( + bars.get_transform() != ax.transData for bars in ranges + ): + return False + components = [line, *caps, *ranges] + if any( + _needs_native_style(artist) or artist.get_visible() != line.get_visible() + for artist in components + ): + return False + if any(cap.get_marker() not in ("_", "|") for cap in caps): + return False + if caps and any( + any( + getattr(cap, "get_" + prop)() != getattr(caps[0], "get_" + prop)() + for prop in ("color", "markersize", "markeredgewidth", "alpha", "visible") + ) + for cap in caps[1:] + ): + return False + x, y = (np.array(v, copy=True) for v in line.get_data(orig=False)) + errors = {} + for axis, bars in zip( + [i for i, flag in enumerate((container.has_xerr, container.has_yerr)) if flag], + ranges, + ): + segments = bars.get_segments() + if len(segments) != len(x): + return False + bounds = np.full((len(x), 2), np.nan) + for i, segment in enumerate(segments): + if len(segment) == 2: + bounds[i] = segment[:, axis] + centers = x if axis == 0 else y + error = np.array([centers - bounds[:, 0], bounds[:, 1] - centers]) + if np.any(error < -1e-12): + unsupported( + "Error-bar centers are outside their ranges; importing geometry" + ) + return False + errors["xerr" if axis == 0 else "yerr"] = np.maximum(error, 0) + kwargs = _line_style(line) + kwargs["label"] = container.get_label() + if any( + not np.array_equal(bars.get_colors(), ranges[0].get_colors()) + or not np.array_equal(bars.get_linewidths(), ranges[0].get_linewidths()) + for bars in ranges[1:] + ): + return False + if ranges: + base_zorder = ranges[0].get_zorder() + delta = line.get_zorder() - base_zorder + if not np.isclose(abs(delta), 0.1) or any( + bar.get_zorder() != base_zorder for bar in ranges + ): + return False + kwargs["zorder"] = base_zorder + kwargs["barsabove"] = delta < 0 + colors = ranges[0].get_colors() + widths = ranges[0].get_linewidths() + if len(colors): + kwargs["ecolor"] = tuple(colors[0]) + if len(widths): + kwargs["elinewidth"] = float(widths[0]) + kwargs["capsize"] = caps[0].get_markersize() / 2 if caps else 0 + if caps: + kwargs["capthick"] = caps[0].get_markeredgewidth() + target.errorbar(x, y, **errors, **kwargs) + return True + + +def _collection_style(collection, index): + kwargs = _style(collection) + if index: + kwargs["label"] = "" + for key, values in ( + ("facecolor", collection.get_facecolors()), + ("edgecolor", collection.get_edgecolors()), + ("linewidth", collection.get_linewidths()), + ( + "linestyle", + getattr(collection, "_us_linestyles", collection.get_linestyles()), + ), + ("antialiased", collection._antialiaseds), + ): + kwargs[key] = ( + copy.deepcopy(values[index % len(values)]) if len(values) else "none" + ) + offset, dashes = kwargs["linestyle"] + kwargs["linestyle"] = (offset, tuple(dashes) if dashes is not None else None) + return kwargs + + +def _import_collection(collection, ax, target, unsupported): + """Import plain line/polygon collections; return False for scatter.""" + if not isinstance(collection, (LineCollection, PolyCollection)): + return False + if ( + collection.get_transform() != ax.transData + or np.any(collection.get_offsets()) + or collection.get_array() is not None + or np.size(collection.get_transforms()) != 0 + or np.ndim(collection.get_alpha()) != 0 + ): + _native(collection, ax, target, unsupported) + return True + if isinstance(collection, LineCollection): + for i, segment in enumerate(collection.get_segments()): + if not len(segment): + continue + kwargs = _collection_style(collection, i) + kwargs.pop("facecolor") + kwargs["color"] = kwargs.pop("edgecolor") + target.plot(segment[:, 0].copy(), segment[:, 1].copy(), **kwargs) + else: + for i, path in enumerate(collection.get_paths()): + if path.codes is not None and ( + np.count_nonzero(path.codes == Path.MOVETO) > 1 + or np.any( + ~np.isin(path.codes, [Path.MOVETO, Path.LINETO, Path.CLOSEPOLY]) + ) + ): + _native(collection, ax, target, unsupported) + return True + vertices = path.vertices + if path.codes is not None and path.codes[-1] == Path.CLOSEPOLY: + vertices = vertices[:-1] + target.fill( + vertices[:, 0].copy(), + vertices[:, 1].copy(), + hatch=collection.get_hatch(), + **_collection_style(collection, i), + ) + return True + + +def _import_axes(ax, target, unsupported): + if ax.name != "rectilinear" or type(ax).__module__.startswith("mpl_toolkits."): + target._import_projection = ReboundSnapshot(ax, fig=ax.figure) + target._import_projection_artist_ids = { + name: [id(artist) for artist in getattr(ax, name)] + for name in ( + "lines", + "collections", + "images", + "patches", + "texts", + "artists", + ) + } + unsupported.report.add( + ax, + "Projection, camera and native artists retained for " + "Matplotlib; edit through the rendered axes", + fallback="native", + ) + return + consumed = set() + target.import_groups = { + id(container): dict( + type=type(container).__name__, + label=container.get_label(), + artist_ids=[id(artist) for artist in container.get_children()], + orientation=getattr(container, "orientation", None), + datavalues=copy.deepcopy(getattr(container, "datavalues", None)), + ) + for container in ax.containers + } + # A bar container retains grouping and orientation, unlike raw rectangles. + for container in _tracked(ax.containers, ax, target, unsupported): + if isinstance(container, ErrorbarContainer): + if _import_errorbar(container, ax, target, unsupported): + consumed.update(container.get_children()) + continue + if isinstance(container, StemContainer): + unsupported.report.add( + container, "Stem components retained as geometry", fallback="geometry" + ) + continue + if not isinstance(container, BarContainer): + unsupported(f"Unsupported {type(container).__name__}; container skipped") + consumed.update(container.get_children()) + continue + for i, patch in enumerate(container.patches): + consumed.add(patch) + if _needs_native_style(patch): + _native(patch, ax, target, unsupported) + target.line_data[-1]["source_order"] = ax.get_children().index(patch) + continue + kwargs = _style(patch) + kwargs.update( + color=patch.get_facecolor(), + edgecolor=patch.get_edgecolor(), + linewidth=patch.get_linewidth(), + hatch=patch.get_hatch(), + linestyle=patch.get_linestyle(), + antialiased=patch.get_antialiased(), + fill=patch.get_fill(), + label=container.get_label() if i == 0 else "", + ) + if container.orientation == "horizontal": + target.barh( + [patch.get_y() + patch.get_height() / 2], + [patch.get_width()], + left=patch.get_x(), + height=patch.get_height(), + **kwargs, + ) + else: + target.bar( + [patch.get_x() + patch.get_width() / 2], + [patch.get_height()], + bottom=patch.get_y(), + width=patch.get_width(), + **kwargs, + ) + + target.line_data[-1]["source_order"] = ax.get_children().index(patch) + + for line in _tracked(ax.lines, ax, target, unsupported): + if line in consumed: + continue + if ( + isinstance(line, AxLine) + or _needs_native_style(line) + or getattr(line._marker, "_user_transform", None) is not None + ): + _native(line, ax, target, unsupported) + continue + x, y = line.get_data(orig=False) + if ( + line.get_transform() == ax.get_yaxis_transform() + and len(y) == 2 + and y[0] == y[1] + ): + target.axhline(y[0], xmin=x[0], xmax=x[1], **_line_style(line)) + continue + if ( + line.get_transform() == ax.get_xaxis_transform() + and len(x) == 2 + and x[0] == x[1] + ): + target.axvline(x[0], ymin=y[0], ymax=y[1], **_line_style(line)) + continue + if line.get_transform() != ax.transData: + _native(line, ax, target, unsupported) + continue + target.plot( + np.array(x, copy=True), + np.array(y, copy=True), + **_line_style(line), + ) + for collection in _tracked(ax.collections, ax, target, unsupported): + if collection in consumed: + continue + if _needs_native_style(collection): + _native(collection, ax, target, unsupported) + continue + if _import_collection(collection, ax, target, unsupported): + continue + if not isinstance(collection, PathCollection): + _native(collection, ax, target, unsupported) + continue + paths = collection.get_paths() + if ( + len(paths) != 1 + or collection.get_offset_transform() != ax.transData + or not isinstance(collection.get_transform(), IdentityTransform) + ): + _native(collection, ax, target, unsupported) + continue + offsets = np.ma.asarray(collection.get_offsets()).filled(np.nan) + kwargs = _style(collection) + kwargs.update( + s=collection.get_sizes().copy(), + marker=_scatter_marker(paths[0]), + edgecolors=collection.get_edgecolors().copy(), + linewidths=collection.get_linewidths().copy(), + ) + values = collection.get_array() + if values is not None: + kwargs.update( + c=values.copy(), + cmap=copy.copy(collection.get_cmap()), + norm=copy.deepcopy(collection.norm), + ) + else: + colors = collection.get_facecolors() + if len(colors) == 1: + kwargs["color"] = tuple(colors[0]) + elif len(colors): + kwargs["c"] = colors.copy() + else: + kwargs["facecolors"] = "none" + target.scatter(offsets[:, 0].copy(), offsets[:, 1].copy(), **kwargs) + for image in _tracked(ax.images, ax, target, unsupported): + if image.get_transform() != ax.transData or _needs_native_style(image): + _native(image, ax, target, unsupported) + continue + target.add_imshow( + image.get_array().copy(), + extent=tuple(image.get_extent()), + origin=image.origin, + cmap=copy.copy(image.get_cmap()), + norm=copy.deepcopy(image.norm), + interpolation=image.get_interpolation(), + interpolation_stage=getattr(image, "_interpolation_stage", "data"), + filternorm=image.get_filternorm(), + filterrad=image.get_filterrad(), + resample=image.get_resample(), + **_style(image), + ) + for text in _tracked(ax.texts, ax, target, unsupported): + if ( + text.get_bbox_patch() is not None + or _needs_native_style(text) + or text.get_wrap() + ): + _native(text, ax, target, unsupported) + continue + if isinstance(text, Annotation): + if text.xycoords != "data" or text.anncoords != "data": + _native(text, ax, target, unsupported) + continue + arrowprops = text.arrowprops + if arrowprops and any(key in arrowprops for key in ("patchA", "patchB")): + _native(text, ax, target, unsupported) + continue + target.annotate( + text.get_text(), + xy=tuple(text.xy), + xytext=tuple(text.get_position()), + arrowprops=copy.deepcopy(arrowprops), + annotation_clip=text.get_annotation_clip(), + **_text_style(text), + **_style(text), + ) + continue + if text.get_transform() != ax.transData: + _native(text, ax, target, unsupported) + continue + target.text( + *text.get_position(), + text.get_text(), + **_text_style(text), + **_style(text), + ) + for patch in _tracked(ax.patches, ax, target, unsupported): + if patch in consumed: + continue + if patch.get_data_transform() != ax.transData or _needs_native_style(patch): + _native(patch, ax, target, unsupported) + consumed.add(patch) + continue + kwargs = dict( + facecolor=patch.get_facecolor(), + edgecolor=patch.get_edgecolor(), + linewidth=patch.get_linewidth(), + linestyle=patch.get_linestyle(), + hatch=patch.get_hatch(), + fill=patch.get_fill(), + **_style(patch), + ) + if isinstance(patch, StepPatch): + data = patch.get_data() + target.stairs( + data.values.copy(), + data.edges.copy(), + baseline=copy.deepcopy(data.baseline), + orientation=patch.orientation, + **kwargs, + ) + consumed.add(patch) + elif isinstance(patch, Polygon): + xy = patch.get_xy() + target.fill( + xy[:, 0].copy(), xy[:, 1].copy(), closed=patch.get_closed(), **kwargs + ) + consumed.add(patch) + else: + _native(patch, ax, target, unsupported) + target._import_inset_indicators = {} + for artist in _tracked(list(ax.artists) + list(ax.tables), ax, target, unsupported): + if isinstance(artist, Legend): + continue + if ( + InsetIndicator is not None + and isinstance(artist, InsetIndicator) + and artist._inset_ax in ax.child_axes + ): + index = ax.child_axes.index(artist._inset_ax) + target._import_inset_indicators[index] = _capture_inset_indicator(artist) + unsupported.report.add( + artist, + "Inset indicator rebuilt from the live inset axes", + fallback="native", + ) + continue + _native(artist, ax, target, unsupported) + target._import_child_axes = [] + for child in ax.child_axes: + _import_child(child, ax, target, unsupported) + + title_kwargs = _text_style(ax.title) + title_kwargs["x"] = ax.title.get_position()[0] + title_kwargs["pad"] = ax.titleOffsetTrans.transform((0, 0))[1] * 72 / ax.figure.dpi + if not ax._autotitlepos: + title_kwargs["y"] = ax.title.get_position()[1] + target.set_title(ax.get_title(), **title_kwargs) + target.set_xlabel( + ax.get_xlabel(), labelpad=ax.xaxis.labelpad, **_text_style(ax.xaxis.label) + ) + target.set_ylabel( + ax.get_ylabel(), labelpad=ax.yaxis.labelpad, **_text_style(ax.yaxis.label) + ) + target.set_xlim(*ax.get_xlim()) + target.set_ylim(*ax.get_ylim()) + for name in ("x", "y"): + scale = getattr(ax, f"get_{name}scale")() + getattr(target, f"set_{name}scale")(scale) + axis = getattr(ax, f"{name}axis") + if isinstance(axis.get_major_locator(), FixedLocator): + getattr(target, f"set_{name}ticks")( + axis.get_majorticklocs().copy(), + labels=axis.get_major_formatter().format_ticks( + axis.get_majorticklocs() + ), + ) + target.set_grid( + any(line.get_visible() for line in ax.get_xgridlines() + ax.get_ygridlines()) + ) + target.set_legend(ax.get_legend() is not None and ax.get_legend().get_visible()) + target.set_facecolor(ax.get_facecolor()) + target.set_axisbelow(ax.get_axisbelow()) + target.set_aspect(ax.get_aspect()) + target.set_visible(ax.get_visible()) + if not ax.axison: + target.set_axis_off() + for name in ( + "adjustable", + "anchor", + "box_aspect", + "frame_on", + "alpha", + "zorder", + "rasterized", + "autoscalex_on", + "autoscaley_on", + ): + getattr(target, "set_" + name)(getattr(ax, "get_" + name)()) + target.set_xmargin(ax.margins()[0]) + target.set_ymargin(ax.margins()[1]) + unsupported.report.add( + ax, + "Axis scales, tick and spine styles, and legend layout " + "retained for Matplotlib", + fallback="native", + ) + target._import_grid = target._grid + target._import_scales = (target._xaxis_scale, target._yaxis_scale) + target._import_axis_state = capture_axis_state(ax) + target._import_legends = [ + ReboundSnapshot(legend, ax) + for legend in ax.get_children() + if isinstance(legend, Legend) + ] + target.line_data.sort(key=lambda entry: entry.get("source_order", 0)) + for entries in target.layered_line_data.values(): + entries.sort(key=lambda entry: entry.get("source_order", 0)) + + +def _needs_native_style(artist): + box = artist.get_clip_box() + custom_box = ( + box is not None + and artist.axes is not None + and not np.array_equal(box.bounds, artist.axes.bbox.bounds) + ) + return ( + custom_box + or artist.get_path_effects() + or artist.get_agg_filter() is not None + or artist.get_sketch_params() is not None + or artist.get_clip_path() is not None + ) + + +def _native(artist, ax, target, unsupported): + if unsupported.fallback == "skip" or not type(artist).__module__.startswith( + ("matplotlib.", "mpl_toolkits.") + ): + unsupported(f"Unsupported {type(artist).__name__}; artist skipped", artist) + return + snapshot = ReboundSnapshot(artist, ax) + target._add( + dict(plot_type="matplotlib_artist", snapshot=snapshot, layer=0, kwargs={}), 0 + ) + unsupported.report.add( + artist, + "Editable artist geometry retained for Matplotlib; " + "other backends require a raster import", + fallback="native", + ) + + +def _tracked(artists, ax, target, unsupported): + order = {id(artist): i for i, artist in enumerate(ax.get_children())} + for artist in artists: + unsupported.artist = artist + start = len(target.line_data) + yield artist + entries = target.line_data[start:] + children = getattr(artist, "get_children", lambda: [])() + index = order.get( + id(artist), + min( + (order.get(id(child), len(order)) for child in children), + default=len(order), + ), + ) + for entry in entries: + entry["source_artist_id"] = id(artist) + entry["source_container_ids"] = [ + identifier + for identifier, group in target.import_groups.items() + if identifier == id(artist) or id(artist) in group["artist_ids"] + ] + entry.setdefault("source_order", index) + if isinstance(artist, Artist): + entry["source_sticky_edges"] = ( + list(artist.sticky_edges.x), + list(artist.sticky_edges.y), + ) + entry["source_picker"] = artist.get_picker() + if entries: + unsupported.report.add( + artist, + "Imported " + str(len(entries)) + " plot entries", + fallback="geometry" if len(entries) > 1 else None, + ) + + +def _capture_colorbar(colorbar, target): + axis = ( + colorbar.ax.yaxis if colorbar.orientation == "vertical" else colorbar.ax.xaxis + ) + return dict( + target=target, + artist_id=id(colorbar.mappable), + standalone=( + dict( + norm=copy.deepcopy(colorbar.mappable.norm), + cmap=copy.copy(colorbar.mappable.get_cmap()), + ) + if target is None + else None + ), + position=tuple(colorbar.ax.get_position().bounds), + kwargs=dict( + orientation=colorbar.orientation, + extend=colorbar.extend, + extendfrac=colorbar.extendfrac, + extendrect=colorbar.extendrect, + spacing=colorbar.spacing, + drawedges=colorbar.drawedges, + boundaries=copy.deepcopy(colorbar.boundaries), + values=copy.deepcopy(colorbar.values), + ), + label=axis.label.get_text(), + label_style=_text_style(axis.label), + ticks=copy.deepcopy(colorbar.get_ticks()), + formatter=ReboundSnapshot(colorbar.formatter, colorbar.ax), + state=capture_axis_state(colorbar.ax), + ) + + +def _capture_inset_indicator(indicator): + """Style for ``indicate_inset_zoom``; connectors are recomputed on render. + + ``InsetIndicator`` spans two Axes and recomputes its rectangle/connector + geometry from live axes limits on every draw, so it cannot be captured as + detached geometry the way single-axes artists are. Recreating it with + ``Axes.indicate_inset_zoom`` on the reconstructed parent/inset pair keeps + that live behavior instead of freezing a stale snapshot. + """ + rectangle = indicator.rectangle + return dict( + facecolor=copy.deepcopy(rectangle.get_facecolor()), + edgecolor=copy.deepcopy(rectangle.get_edgecolor()), + linewidth=rectangle.get_linewidth(), + linestyle=rectangle.get_linestyle(), + alpha=indicator.get_alpha(), + zorder=indicator.get_zorder(), + visible=indicator.get_visible(), + ) + + +def _import_child(child, ax, target, unsupported): + from matplotlib.axes._secondary_axes import SecondaryAxis + + from maxplotlib.subfigure.line_plot import LinePlot + + if isinstance(child, SecondaryAxis): + target._import_child_axes.append( + dict( + kind="secondary", + orientation=child._orientation, + location=child._loc, + functions=child._functions, + locator=ReboundSnapshot(child.get_axes_locator(), ax), + state=capture_axis_state(child), + xlabel=child.get_xlabel(), + ylabel=child.get_ylabel(), + ) + ) + else: + subplot = LinePlot() + _import_axes(child, subplot, unsupported) + bounds = ax.transAxes.inverted().transform_bbox(child.bbox).bounds + target._import_child_axes.append( + dict( + kind="inset", + bounds=tuple(bounds), + subplot=subplot, + locator=ReboundSnapshot(child.get_axes_locator(), ax), + ) + ) + unsupported.report.add( + child, "Child axes retained for Matplotlib", fallback="native" + ) + + +def _raster_import(canvas_cls, figure, axes, whole_figure, report, **kwargs): + from matplotlib.backends.backend_agg import FigureCanvasAgg + + snapshot = copy.deepcopy(figure) + if not whole_figure: + indices = [figure.axes.index(ax) for ax in axes] + for i, ax in enumerate(list(snapshot.axes)): + if i not in indices: + ax.remove() + FigureCanvasAgg(snapshot).draw() + rgba = np.asarray(snapshot.canvas.buffer_rgba()).copy() + kwargs.setdefault("figsize", tuple(figure.get_size_inches())) + kwargs.setdefault("dpi", figure.dpi) + canvas = canvas_cls(**kwargs) + subplot = canvas.add_subplot(row=0, col=0) + subplot.add_imshow(rgba) + subplot.set_axis_off() + subplot.set_grid(False) + canvas.import_report = report + report.add( + figure, + "Raster snapshot: data, artist editing, vector paths and interactive " + "state are lost", + severity="warning", + fallback="raster", + backends=("matplotlib", "plotly"), + ) + return canvas diff --git a/src/maxplotlib/backends/tikzfigure/__init__.py b/src/maxplotlib/backends/tikzfigure/__init__.py new file mode 100644 index 0000000..5f2ddad --- /dev/null +++ b/src/maxplotlib/backends/tikzfigure/__init__.py @@ -0,0 +1,9 @@ +"""The tikzfigure backend: pgfplots figures from what Matplotlib draws. + +See :func:`figure_to_tikz`; ``Canvas.render(backend="tikzfigure")`` uses it. +""" + +from .convert import TikzConversionWarning, figure_to_tikz +from .text import latex + +__all__ = ["TikzConversionWarning", "figure_to_tikz", "latex"] diff --git a/src/maxplotlib/backends/tikzfigure/convert.py b/src/maxplotlib/backends/tikzfigure/convert.py new file mode 100644 index 0000000..d787777 --- /dev/null +++ b/src/maxplotlib/backends/tikzfigure/convert.py @@ -0,0 +1,1331 @@ +"""A drawn Matplotlib figure as a tikzfigure figure of pgfplots axes. + +Every axes becomes a pgfplots ``axis`` at the place and size it has in the +figure, with its limits, scales, labels, title, ticks, grid, spines and +legend. What is drawn in it is converted artist by artist, in Matplotlib's +drawing order: + +* vector graphics, in pgfplots code: lines and markers + (:class:`~matplotlib.lines.Line2D`), scatter plots, line and polygon + collections (``hlines``, ``fill_between``, error bars, ...), contour + lines, patches (bars, spans, polygons, arrows) and text and annotations; +* raster images, rendered by Matplotlib itself and placed with + ``\\addplot graphics``: meshes, images, filled contours, quivers, and + every artist that has no vector counterpart here or would be too large + for TeX (``max_markers``, ``max_items``, ``max_points``). + +Colorbars are axes of their own: their color strip is an image, their +ticks and label pgfplots text. Axes that pgfplots cannot represent (polar +or 3-D projections, symlog scales) become one image each, as do figure +legends. Text is converted with :func:`~maxplotlib.backends.tikzfigure.text.latex`. +""" + +from __future__ import annotations + +import io +import warnings + +import matplotlib.colors as mcolors +import matplotlib.ticker as mticker +import numpy as np +from matplotlib.collections import ( + Collection, + LineCollection, + PathCollection, + PolyCollection, +) +from matplotlib.contour import ContourSet +from matplotlib.image import AxesImage +from matplotlib.lines import Line2D +from matplotlib.markers import MarkerStyle +from matplotlib.patches import FancyArrowPatch, Patch +from matplotlib.path import Path +from matplotlib.text import Annotation, Text +from matplotlib.transforms import Bbox +from tikzfigure import TikzFigure +from tikzfigure.core.axis import Axis2D +from tikzfigure.core.plot import format_number + +from .text import is_multiline, latex + +__all__ = ["TikzConversionWarning", "figure_to_tikz"] + + +class TikzConversionWarning(UserWarning): + """A part of a Matplotlib figure that is drawn as an image or left out.""" + + +# Matplotlib marker -> (pgfplots mark when filled, when hollow, rotation, size factor) +_MARKS = { + "o": ("*", "o", 0, 0.5), + ".": ("*", "o", 0, 0.25), + ",": ("*", "o", 0, 0.1), + "s": ("square*", "square", 0, 0.5), + "^": ("triangle*", "triangle", 0, 0.5), + "v": ("triangle*", "triangle", 180, 0.5), + "<": ("triangle*", "triangle", 90, 0.5), + ">": ("triangle*", "triangle", 270, 0.5), + "D": ("diamond*", "diamond", 0, 0.5), + "d": ("diamond*", "diamond", 0, 0.45), + "p": ("pentagon*", "pentagon", 0, 0.5), + "h": ("pentagon*", "pentagon", 0, 0.5), + "H": ("pentagon*", "pentagon", 0, 0.5), + "*": ("star", "star", 0, 0.5), + "+": ("+", "+", 0, 0.5), + "P": ("+", "+", 0, 0.5), + "x": ("x", "x", 0, 0.5), + "X": ("x", "x", 0, 0.5), + "|": ("|", "|", 0, 0.5), + "_": ("-", "-", 0, 0.5), + "1": ("Mercedes star", "Mercedes star", 180, 0.5), + "2": ("Mercedes star", "Mercedes star", 0, 0.5), +} +_NO_MARKER = ("None", "none", "", " ", None) +_NO_LINE = ("None", "none", "", " ") +_HA = {"left": "west", "center": "", "right": "east"} +_VA = { + "top": "north", + "center": "", + "bottom": "south", + "baseline": "base", + "center_baseline": "mid", +} +# formatters whose labels pgfplots writes just as well by itself +_AUTO_FORMATTERS = ( + mticker.ScalarFormatter, + mticker.LogFormatterSciNotation, + mticker.LogFormatterMathtext, +) +_AUTO_LOCATORS = ( + mticker.AutoLocator, + mticker.MaxNLocator, + mticker.LogLocator, + mticker.AutoMinorLocator, +) + + +def figure_to_tikz( + figure, + *, + raster_dpi: float = 300, + max_markers: int = 2000, + max_items: int = 500, + max_points: int = 20000, + precision: int = 6, +) -> TikzFigure: + """Convert a Matplotlib figure into a :class:`~tikzfigure.TikzFigure`. + + The figure is drawn first (without showing it), so that its layout, + autoscaled limits, ticks and legend positions are final, and is left + as it was afterwards. + + Parameters + ---------- + figure : matplotlib.figure.Figure + The figure to convert. + raster_dpi : float, optional + Resolution of the parts drawn as images (meshes, images, ...). + Default: 300. + max_markers : int, optional + Scatter plots with more points are drawn as an image. Default: 2000. + max_items : int, optional + Collections with more differently styled items (e.g. a line + collection colored by value) are drawn as an image. Default: 500. + max_points : int, optional + Lines with more points, after Matplotlib's path simplification, are + drawn as an image. Default: 20000. + precision : int, optional + Significant digits of the written coordinates. Default: 6. + + Returns + ------- + tikzfigure.TikzFigure + The figure: ``.generate_tikz()`` for the code, ``.savefig("f.pdf")`` + to compile it, ``.savefig("f.tikz")`` to write the code and its + images. + """ + converter = _FigureConverter( + figure, + raster_dpi=raster_dpi, + max_markers=max_markers, + max_items=max_items, + max_points=max_points, + precision=precision, + ) + return converter.convert() + + +class _Colors: + """Colors used in the figure, defined once as ``\\definecolor``.""" + + def __init__(self): + self.names: dict[str, str] = {} + + def __call__(self, color): + """``(name, opacity)`` of a color; name ``None`` for a transparent one.""" + rgba = mcolors.to_rgba(color) + if rgba[3] <= 0: + return None, 0.0 + code = mcolors.to_hex(rgba, keep_alpha=False)[1:].upper() + name = self.names.setdefault(code, f"mpl{code}") + return name, float(rgba[3]) + + def definitions(self) -> str: + return "\n".join( + f"\\definecolor{{{name}}}{{HTML}}{{{code}}}" + for code, name in self.names.items() + ) + + +def _font(size) -> str: + size = float(size) + return f"\\fontsize{{{size:g}}}{{{1.2 * size:g}}}\\selectfont" + + +def _pt(value) -> str: + return f"{float(value):.4g}pt" + + +def _dash(pattern) -> str | None: + """A Matplotlib ``(offset, [on, off, ...])`` dash pattern in points as TikZ.""" + if pattern is None: + return None + offset, sequence = pattern + if not sequence: + return None + parts = [] + for index, length in enumerate(sequence): + parts.append(("on" if index % 2 == 0 else "off") + " " + _pt(length)) + option = "dash pattern=" + " ".join(parts) + if offset: + option += f", dash phase={_pt(offset)}" + return option + + +class _FigureConverter: + def __init__(self, figure, **options): + self.figure = figure + self.options = options + self.colors = _Colors() + self.tikz = TikzFigure() + self.width, self.height = figure.get_size_inches() + + # -- the figure ---------------------------------------------------------- + def convert(self) -> TikzFigure: + figure = self.figure + figure.draw_without_rendering() + # images are rendered from this figure; a layout engine would move the + # axes while other parts are hidden + engine = figure.get_layout_engine() + figure.set_layout_engine("none") + try: + for ax in figure.axes: + self._axes_tree(ax) + for text in figure.texts: + self._figure_text(text) + for legend in figure.legends: + if legend.get_visible(): + self._as_image(legend, "a figure legend") + finally: + figure._layout_engine = engine + self.tikz.add_package("lmodern") + self.tikz.add_package("amsmath") + definitions = self.colors.definitions() + if definitions: + self.tikz.add_raw(definitions) + return self.tikz + + def _axes_tree(self, ax): + if not ax.get_visible(): + return + if ax.name != "rectilinear" or not _supported_scales(ax): + what = ( + f"{ax.name} axes" + if ax.name != "rectilinear" + else f"{ax.get_xscale()}/{ax.get_yscale()} scaled axes" + ) + self._as_image(ax, what) + return + _AxesConverter(self, ax).convert() + for child in ax.child_axes: + self._axes_tree(child) + + def _figure_text(self, text): + if not text.get_visible() or not text.get_text().strip(): + return + if isinstance(text, Annotation): + text.update_positions(self.figure._get_renderer()) + x, y = _text_display_position(text) + if not self.figure.bbox.contains(x, y): + return + x_in, y_in = x / self.figure.dpi, y / self.figure.dpi + node = _text_node(text, self.colors, f"({x_in:.4f}in,{y_in:.4f}in)") + self.tikz.add_raw(node) + + # -- images of whole parts --------------------------------------------- + def _as_image(self, artist, what): + """Draw ``artist`` (axes or legend) with its decorations as one image.""" + warnings.warn( + f"{what} cannot be drawn with pgfplots; it is included as an image", + TikzConversionWarning, + stacklevel=4, + ) + renderer = self.figure._get_renderer() + bbox = artist.get_tightbbox(renderer) + if bbox is None or bbox.width <= 0 or bbox.height <= 0: + return + inches = bbox.transformed(self.figure.dpi_scale_trans.inverted()) + data = self.render_only([artist], inches) + axis = Axis2D( + xlim=(0, 1), + ylim=(0, 1), + grid=False, + width=f"{inches.width:.4f}in", + height=f"{inches.height:.4f}in", + options=[ + "hide axis", + "scale only axis", + "anchor=south west", + f"at={{({inches.x0:.4f}in,{inches.y0:.4f}in)}}", + ], + ) + axis.add_graphics(0, 1, 0, 1, data=data, plot_options=["forget plot"]) + self.tikz.axes.append(axis) + + def render_only(self, artists, inches: Bbox, keep_axes=None) -> bytes: + """A PNG of the figure region ``inches`` showing only ``artists``. + + ``keep_axes`` is the axes whose decorations stay hidden while its + artists in ``artists`` are drawn; every other axes is hidden. + """ + figure = self.figure + keep = set(map(id, artists)) + hidden = [] + + def hide(artist): + if id(artist) not in keep and artist.get_visible(): + artist.set_visible(False) + hidden.append(artist) + + # the axes containing a kept artist, and the axes containing those, + # stay visible (an artist of a hidden axes is not drawn), with their + # other children hidden + parents = {id(child): ax for ax in _all_axes(figure) for child in ax.child_axes} + owners = set() + for artist in artists: + owner = getattr(artist, "axes", None) + if owner is artist: + owner = parents.get(id(artist)) + while owner is not None: + owners.add(id(owner)) + owner = parents.get(id(owner)) + for ax in _all_axes(figure): + if id(ax) in keep: + continue + if id(ax) in owners: + for child in ax.get_children(): + if id(child) not in owners: + hide(child) + else: + hide(ax) + hide(figure.patch) + for text in figure.texts: + hide(text) + for legend in figure.legends: + hide(legend) + for artist in artists: # the artists themselves are drawn + if not artist.get_visible(): + artist.set_visible(True) + hidden.append(("shown", artist)) + buffer = io.BytesIO() + try: + figure.savefig( + buffer, + format="png", + dpi=self.options["raster_dpi"], + transparent=True, + bbox_inches=inches, + pad_inches=0, + ) + finally: + for artist in hidden: + if isinstance(artist, tuple): + artist[1].set_visible(False) + else: + artist.set_visible(True) + return buffer.getvalue() + + +def _all_axes(figure): + out = [] + + def walk(ax): + out.append(ax) + for child in ax.child_axes: + walk(child) + + for ax in figure.axes: + walk(ax) + return out + + +def _supported_scales(ax) -> bool: + return ax.get_xscale() in ("linear", "log") and ax.get_yscale() in ( + "linear", + "log", + ) + + +def _text_display_position(text): + return text.get_transform().transform(text.get_unitless_position()) + + +def _anchor(text) -> str: + vertical = _VA.get(text.get_va(), "") + horizontal = _HA.get(text.get_ha(), "") + if vertical == "base" and not horizontal: + return "base" + if vertical == "mid" and not horizontal: + return "mid" + anchor = " ".join(part for part in (vertical, horizontal) if part) + return anchor or "center" + + +def _text_node(text, colors, at: str) -> str: + """A ``\\node`` for a Matplotlib text, placed at the TikZ point ``at``.""" + options = [f"anchor={_anchor(text)}", "inner sep=0pt"] + rotation = text.get_rotation() + if rotation: + options.append(f"rotate={rotation:g}") + color, opacity = colors(text.get_color()) + if color is not None and color != "mpl000000": + options.append(f"text={color}") + alpha = text.get_alpha() + if alpha is not None and alpha < 1: + opacity *= alpha + if opacity < 1: + options.append(f"text opacity={opacity:.3g}") + options.append(f"font={{{_font(text.get_fontsize())}}}") + if is_multiline(text.get_text()): + alignment = getattr(text, "_multialignment", None) or text.get_ha() + options.append(f"align={alignment}") + box = text.get_bbox_patch() + if box is not None: + face, face_opacity = colors(box.get_facecolor()) + edge, _ = colors(box.get_edgecolor()) + options[1] = "inner sep=2pt" + if face is not None: + options.append(f"fill={face}") + if face_opacity < 1: + options.append(f"fill opacity={face_opacity:.3g}, text opacity=1") + if edge is not None and box.get_linewidth() > 0: + options.append(f"draw={edge}, line width={_pt(box.get_linewidth())}") + if "round" in type(box.get_boxstyle()).__name__.lower(): + options.append("rounded corners=2pt") + return f"\\node[{', '.join(options)}] at {at} {{{latex(text.get_text())}}};" + + +class _AxesConverter: + """One Matplotlib axes as one pgfplots axis.""" + + def __init__(self, parent: _FigureConverter, ax): + self.parent = parent + self.figure = parent.figure + self.ax = ax + self.colors = parent.colors + self.options = parent.options + self.colorbar = getattr(ax, "_colorbar", None) + self.legend_entries: list = [] + self.has_images = False + + # -- the axis ---------------------------------------------------------- + def convert(self): + ax = self.ax + position = ax.get_position() + width, height = self.parent.width, self.parent.height + x0, y0 = position.x0 * width, position.y0 * height + self.size = (position.width * width, position.height * height) + self.inches = Bbox.from_bounds(x0, y0, *self.size) + xlim, ylim = ax.get_xlim(), ax.get_ylim() + + options = [ + "scale only axis", + "anchor=south west", + f"at={{({x0:.4f}in,{y0:.4f}in)}}", + "clip mode=individual", + "unbounded coords=jump", + "every axis plot/.append style={line join=round}", + ] + if xlim[0] > xlim[1]: + options.append("x dir=reverse") + if ylim[0] > ylim[1]: + options.append("y dir=reverse") + if not ax.axison: + options.append("hide axis") + else: + options.extend(self._frame()) + for name in ("x", "y"): + options.extend(self._ticks(name)) + options.extend(self._labels()) + options.extend(self._background()) + + self.axis = Axis2D( + xlabel=latex(ax.get_xlabel()) if ax.xaxis.get_visible() else "", + ylabel=latex(ax.get_ylabel()) if ax.yaxis.get_visible() else "", + title=latex(self._title()), + xlim=(float(min(xlim)), float(max(xlim))), + ylim=(float(min(ylim)), float(max(ylim))), + xlog=ax.get_xscale() == "log", + ylog=ax.get_yscale() == "log", + grid=None, + width=f"{self.size[0]:.4f}in", + height=f"{self.size[1]:.4f}in", + options=options, + ) + self._legend_setup() + self._contents() + self._legend_entries() + if self.has_images: + self.axis.options.append("axis on top") + grid = self._grid() + if grid: + self.axis.options.extend(grid) + self.parent.tikz.axes.append(self.axis) + + def _title(self) -> str: + for location in ("center", "left", "right"): + title = self.ax.get_title(location) + if title: + return title + return "" + + def _frame(self) -> list[str]: + spines = self.ax.spines + visible = { + name: name in spines and spines[name].get_visible() + for name in ("left", "right", "top", "bottom") + } + if "outline" in spines and spines["outline"].get_visible(): # a colorbar + return [] + if all(visible.values()): + return [] + options = [] + x = ( + "box" + if visible["bottom"] and visible["top"] + else "bottom" if visible["bottom"] else "top" if visible["top"] else None + ) + y = ( + "box" + if visible["left"] and visible["right"] + else "left" if visible["left"] else "right" if visible["right"] else None + ) + options.append(f"axis x line*={x}" if x else "axis x line=none") + options.append(f"axis y line*={y}" if y else "axis y line=none") + return options + + def _ticks(self, name) -> list[str]: + ax = self.ax + axis = getattr(ax, f"{name}axis") + if not axis.get_visible(): + return [f"{name}tick=\\empty", f"{name}ticklabels={{}}"] + options = [] + locator, formatter = axis.get_major_locator(), axis.get_major_formatter() + lo, hi = sorted(getattr(ax, f"get_{name}lim")()) + # the ticks where Matplotlib has them; their labels written by pgfplots, + # unless Matplotlib's formatter writes something else than numbers + tolerance = 1e-9 * abs(hi - lo) + locs = [ + loc + for loc in axis.get_majorticklocs() + if lo - tolerance <= loc <= hi + tolerance + ] + if isinstance(locator, mticker.NullLocator) or not locs: + options.append(f"{name}tick=\\empty") + else: + options.append( + f"{name}tick={{{','.join(f'{float(loc):.10g}' for loc in locs)}}}" + ) + if not isinstance(formatter, _AUTO_FORMATTERS): + labels = formatter.format_ticks(locs) + options.append( + f"{name}ticklabels={{{','.join('{' + latex(lab) + '}' for lab in labels)}}}" + ) + ticks = axis.get_major_ticks() + if ticks: + tick = ticks[0] + first = tick.tick1line.get_visible() + second = tick.tick2line.get_visible() + side = "both" if first and second else "left" if first else "right" + if not first and not second: + options.append(f"major {name} tick style={{draw=none}}") + else: + options.append(f"{name}tick pos={side}") + label1, label2 = tick.label1.get_visible(), tick.label2.get_visible() + if not label1 and not label2: + options.append(f"{name}ticklabels={{}}") + options.append(f"scaled {name} ticks=false") + else: + options.append( + f"{name}ticklabel pos={'right' if label2 and not label1 else 'left'}" + ) + options.append( + f"{name} tick label style={{font={{{_font(tick.label1.get_fontsize())}}}}}" + ) + if name == "x": + direction = getattr(tick, "_tickdir", "out") + align = {"in": "inside", "out": "outside", "inout": "center"}.get( + direction, "outside" + ) + options.append(f"tick align={align}") + options.append(f"major tick length={_pt(tick._size)}") + if axis.get_label_position() in ("top", "right"): + options.append(f"{name}label near ticks") + if not any(option.startswith(f"{name}ticklabel pos") for option in options): + options.append(f"{name}ticklabel pos=right") + return options + + def _labels(self) -> list[str]: + options = [] + for key, label in ( + ("xlabel", self.ax.xaxis.label), + ("ylabel", self.ax.yaxis.label), + ("title", self.ax.title), + ): + style = [f"font={{{_font(label.get_fontsize())}}}"] + color, _ = self.colors(label.get_color()) + if color is not None and color != "mpl000000": + style.append(f"text={color}") + if is_multiline(label.get_text()): + style.append("align=center") + options.append(f"{key} style={{{', '.join(style)}}}") + return options + + def _background(self) -> list[str]: + if self.colorbar is not None: + return [] + patch = self.ax.patch + if not patch.get_visible(): + return [] + color, opacity = self.colors(patch.get_facecolor()) + if color is None or color == "mplFFFFFF": + return [] + fill = f"fill={color}" + ( + f", fill opacity={opacity:.3g}" if opacity < 1 else "" + ) + return [f"axis background/.style={{{fill}}}"] + + def _grid(self) -> list[str]: + options = [] + style = None + for name in ("x", "y"): + axis = getattr(self.ax, f"{name}axis") + ticks = axis.get_major_ticks() + if ticks and ticks[0].gridline.get_visible(): + options.append(f"{name}majorgrids") + style = style or ticks[0].gridline + if style is not None: + color, opacity = self.colors(style.get_color()) + alpha = style.get_alpha() + if alpha is not None: + opacity *= alpha + parts = [f"draw={color}", f"line width={_pt(style.get_linewidth())}"] + if opacity < 1: + parts.append(f"draw opacity={opacity:.3g}") + dash = _dash(style._dash_pattern) if style.is_dashed() else None + parts.append(dash or "solid") + options.append(f"major grid style={{{', '.join(parts)}}}") + return options + + # -- the legend -------------------------------------------------------- + def _legend_setup(self): + legend = self.ax.get_legend() + if legend is None or not legend.get_visible(): + return + handles = getattr(legend, "legend_handles", None) + if handles is None: # Matplotlib < 3.7 + handles = legend.legendHandles + self.legend_entries = [ + (handle, text.get_text()) + for handle, text in zip(handles, legend.get_texts()) + ] + bbox = legend.get_window_extent().transformed(self.ax.transAxes.inverted()) + center_x, center_y = (bbox.x0 + bbox.x1) / 2, (bbox.y0 + bbox.y1) / 2 + vertical = "north" if center_y > 0.5 else "south" + horizontal = "east" if center_x > 0.5 else "west" + at = ( + bbox.x1 if horizontal == "east" else bbox.x0, + bbox.y1 if vertical == "north" else bbox.y0, + ) + style = [] + frame = legend.get_frame() + if legend.get_frame_on() and frame.get_visible(): + face, face_opacity = self.colors(frame.get_facecolor()) + edge, _ = self.colors(frame.get_edgecolor()) + style.append(f"fill={face}" if face else "fill=none") + if face_opacity < 1: + style.append(f"fill opacity={face_opacity:.3g}, text opacity=1") + style.append(f"draw={edge}" if edge else "draw=none") + if "round" in type(frame.get_boxstyle()).__name__.lower(): + style.append("rounded corners=2pt") + else: + style.extend(["draw=none", "fill=none"]) + if legend.get_texts(): + style.append(f"font={{{_font(legend.get_texts()[0].get_fontsize())}}}") + style.append("cells={anchor=west}") + self.axis.set_legend( + at=at, + anchor=f"{vertical} {horizontal}", + columns=getattr(legend, "_ncols", 1) or None, + style=style, + ) + + def _legend_label(self, artist) -> str: + """Plots never make legend entries themselves; see :meth:`_legend_entries`.""" + return "" + + def _legend_entries(self): + """The legend as Matplotlib draws it: an image and a text per entry. + + The plots are left out of the legend (``forget plot``), so that the + entries keep the legend's order and style, also for artists drawn as + images. + """ + for handle, label in self.legend_entries: + options = self._legend_image(handle) + self.axis.add_raw( + f"\\addlegendimage{{{', '.join(options)}}}\n" + f"\\addlegendentry{{{latex(label)}}}" + ) + + def _legend_image(self, handle) -> list[str]: + if isinstance(handle, Line2D): + options = self._line_options(handle) + if options: + return options + elif isinstance(handle, Patch): + face = handle.get_facecolor() if handle.get_fill() else "none" + # the patch's colors include its alpha + options = self._area_options( + face, + handle.get_edgecolor(), + handle.get_linewidth(), + _dash(handle._dash_pattern), + ) + if options: + return options + elif isinstance(handle, PathCollection): + handle.update_scalarmappable() + faces, edges = handle.get_facecolors(), handle.get_edgecolors() + sizes = handle.get_sizes() + widths = np.atleast_1d(handle.get_linewidths()) + paths = handle.get_paths() + return ["only marks"] + self._mark( + _marker_name(paths[0]) if len(paths) else "o", + float(np.sqrt(sizes[0])) if len(sizes) else 6.0, + faces[0] if len(faces) else "none", + edges[0] if len(edges) and not isinstance(edges, str) else "none", + float(widths[0]) if len(widths) else 1.0, + ) + elif isinstance(handle, LineCollection): + colors = handle.get_colors() + widths = np.atleast_1d(handle.get_linewidths()) + styles = handle.get_linestyles() + stroke = self._stroke( + colors[0] if len(colors) else "black", + float(widths[0]) if len(widths) else 1.0, + _dash(styles[0]) if len(styles) else None, + ) + if stroke: + return stroke + ["mark=none"] + return ["empty legend"] + + # -- what is drawn ----------------------------------------------------- + def _contents(self): + ax = self.ax + children = list(getattr(ax, "_children", [])) + if not children: # an older Matplotlib + children = ( + ax.collections + + ax.patches + + ax.lines + + ax.texts + + ax.images + + ax.tables + ) + children = [child for child in children if child.get_visible()] + children.sort(key=lambda artist: artist.get_zorder()) # stable: drawing order + pending_images = [] + for artist in children: + emit = None if self.colorbar is not None else self._vector(artist) + if emit is None: + pending_images.append(artist) + continue + self._flush_images(pending_images) + pending_images = [] + emit() + self._flush_images(pending_images) + + def _flush_images(self, artists): + if not artists: + return + reasons = sorted( + { + type(artist).__name__ + for artist in artists + if not isinstance(artist, _ALWAYS_RASTER) + } + ) + if reasons and self.colorbar is None: + warnings.warn( + f"drawing {', '.join(reasons)} as an image in the pgfplots axis", + TikzConversionWarning, + stacklevel=6, + ) + data = self.parent.render_only(artists, self.inches) + xlim, ylim = self.ax.get_xlim(), self.ax.get_ylim() + self.axis.add_graphics( + float(min(xlim)), + float(max(xlim)), + float(min(ylim)), + float(max(ylim)), + data=data, + plot_options=["forget plot"], + ) + self.has_images = True + + def _vector(self, artist): + """A function adding ``artist`` to the axis as vector graphics, or None.""" + if isinstance(artist, Line2D): + return self._line(artist) + if isinstance(artist, ContourSet): + return self._contour_lines(artist) + if isinstance(artist, PathCollection): + return self._scatter(artist) + if isinstance(artist, LineCollection): + return self._line_collection(artist) + if type(artist) is PolyCollection or type(artist).__name__ in ( + "FillBetweenPolyCollection", + ): + return self._polygons(artist) + if isinstance(artist, Patch): + return self._patch(artist) + if isinstance(artist, Text): + return self._text(artist) + return None + + # coordinates + def _view(self): + """The axes box in display coordinates, grown by one box size on each side. + + Geometry is clipped to it: what lies further out is not shown, and its + coordinates could exceed the largest dimension TeX can hold. + """ + box = self.ax.bbox + return ( + box.x0 - box.width, + box.y0 - box.height, + box.x1 + box.width, + box.y1 + box.height, + ) + + def _inside(self, display): + x0, y0, x1, y1 = self._view() + display = np.asarray(display, dtype=float).reshape(-1, 2) + with np.errstate(invalid="ignore"): + return ( + (display[:, 0] >= x0) + & (display[:, 0] <= x1) + & (display[:, 1] >= y0) + & (display[:, 1] <= y1) + ) + + def _clip_line(self, display, simplify=False): + """A polyline in display coordinates clipped to :meth:`_view`; gaps as nan. + + With ``simplify``, it is also simplified as Matplotlib does when drawing. + """ + display = np.asarray(display, dtype=float).reshape(-1, 2) + if not simplify and self._inside(display).all(): + return display + finite = np.isfinite(display).all(axis=1) + if not finite.any(): + return np.empty((0, 2)) + path = Path(display).cleaned( + remove_nans=True, clip=self._view(), simplify=simplify + ) + return _with_gaps(path) + + def _clip_polygon(self, display): + """A polygon in display coordinates clipped to :meth:`_view` (empty if outside).""" + display = np.asarray(display, dtype=float).reshape(-1, 2) + display = display[np.isfinite(display).all(axis=1)] + if len(display) < 3 or self._inside(display).all(): + return display + closed = Path(np.vstack([display, display[:1]]), closed=True) + clipped = closed.clip_to_bbox(Bbox.from_extents(*self._view())) + polygons = clipped.to_polygons() + return polygons[0] if polygons else np.empty((0, 2)) + + def _to_data(self, display): + display = np.asarray(display, dtype=float).reshape(-1, 2) + with np.errstate(all="ignore"): + data = self.ax.transData.inverted().transform(display) + # display -> data leaves round-off (1e-16 for 0): snap it on linear axes + for index, name in enumerate(("x", "y")): + if getattr(self.ax, f"get_{name}scale")() == "linear": + lo, hi = getattr(self.ax, f"get_{name}lim")() + tiny = 1e-9 * abs(hi - lo) + with np.errstate(invalid="ignore"): + data[np.abs(data[:, index]) < tiny, index] = 0.0 + return data + + def _add(self, data, options, label="", cycle=False, clip=True): + data = np.asarray(data, dtype=float) + if not clip: + self.axis.add_raw(self._draw(data, options, cycle)) + return + self.axis.add_plot( + x=data[:, 0].tolist(), + y=data[:, 1].tolist(), + label=label, + options=list(options) + ["forget plot"], + cycle=cycle, + precision=self.options["precision"], + ) + + def _draw(self, data, options, cycle) -> str: + """A ``\\draw`` path in axis coordinates, which pgfplots does not clip.""" + precision = self.options["precision"] + keep = [ + option + for option in options + if not option.startswith( + ("mark", "only marks", "forget plot", "area legend") + ) + ] + parts = [] + connect = False + for x, y in data: + if not (np.isfinite(x) and np.isfinite(y)): + connect = False + continue + point = f"(axis cs:{format_number(float(x), precision)},{format_number(float(y), precision)})" + parts.append(("-- " if connect else "") + point) + connect = True + if cycle and parts: + parts.append("-- cycle") + return f"\\draw[{', '.join(keep)}] {' '.join(parts)};" + + def _stroke(self, color, linewidth, dash, alpha=None) -> list[str] | None: + name, opacity = self.colors(color) + if name is None or linewidth <= 0: + return None + if alpha is not None: + opacity *= alpha + options = [f"draw={name}", f"line width={_pt(linewidth)}"] + if opacity < 1: + options.append(f"draw opacity={opacity:.3g}") + options.append(dash or "solid") + return options + + def _fill(self, color, alpha=None) -> list[str] | None: + name, opacity = self.colors(color) + if name is None: + return None + if alpha is not None: + opacity *= alpha + options = [f"fill={name}"] + if opacity < 1: + options.append(f"fill opacity={opacity:.3g}") + return options + + def _mark(self, marker, size, face, edge, edge_width, alpha=None) -> list[str]: + """``mark=...`` options for a Matplotlib marker of ``size`` points.""" + if isinstance(marker, str) and marker in _MARKS: + filled_mark, hollow_mark, rotation, factor = _MARKS[marker] + else: + filled_mark, hollow_mark, rotation, factor = _MARKS["o"] + fill = self._fill(face, alpha) + mark = filled_mark if fill else hollow_mark + mark_options = ["solid"] + if rotation: + mark_options.append(f"rotate={rotation}") + mark_options.extend(fill or ["fill=none"]) + stroke = self._stroke(edge, edge_width, None, alpha) + if stroke: + mark_options.extend(option for option in stroke if option != "solid") + else: + mark_options.append("draw=none") + return [ + f"mark={mark}", + f"mark size={_pt(max(size * factor, 0.1))}", + f"mark options={{{', '.join(mark_options)}}}", + ] + + # artists + def _line(self, line): + path = line.get_path() + if not len(path.vertices): + return lambda: None + display = line.get_transform().transform(path.vertices) + marker = line.get_marker() + has_marker = marker not in _NO_MARKER and line.get_markersize() > 0 + if has_marker: # the markers stay where they are; far ones are left out + display = np.where(self._inside(display)[:, None], display, np.nan) + else: + display = self._clip_line(display, simplify=len(display) > 2000) + if len(display) > self.options["max_points"]: + return None + data = self._to_data(display) + options = self._line_options(line) + if options is None: + return lambda: None + label = self._legend_label(line) + clip = line.get_clip_on() + return lambda: self._add(data, options, label, clip=clip) + + def _line_options(self, line) -> list[str] | None: + """The style of a Line2D as plot options; None if it draws nothing.""" + marker = line.get_marker() + has_marker = marker not in _NO_MARKER and line.get_markersize() > 0 + has_line = line.get_linestyle() not in _NO_LINE and line.get_linewidth() > 0 + alpha = line.get_alpha() + options = [] + if has_line: + dash = _dash(line._dash_pattern) if line.is_dashed() else None + stroke = self._stroke(line.get_color(), line.get_linewidth(), dash, alpha) + if stroke is None: + has_line = False + else: + options.extend(stroke) + if not has_line: + if not has_marker: + return None + options.append("only marks") + if has_marker: + face = line.get_markerfacecolor() + if line.get_fillstyle() == "none": + face = "none" + options.extend( + self._mark( + marker, + line.get_markersize(), + face, + line.get_markeredgecolor(), + line.get_markeredgewidth(), + alpha, + ) + ) + else: + options.append("mark=none") + return options + + def _contour_lines(self, contours): + if contours.filled: + return None + paths = contours.get_paths() + transform = contours.get_transform() + colors = contours.get_edgecolor() + widths = np.atleast_1d(contours.get_linewidth()) + styles = contours.get_linestyle() + pieces = [] + total = 0 + for index, path in enumerate(paths): + lines = transform.transform_path(path).to_polygons(closed_only=False) + if not lines: + continue + joined = [] + for line in lines: + joined.append(self._clip_line(line)) + joined.append([[np.nan, np.nan]]) + display = np.concatenate(joined[:-1]) + total += len(display) + dash = _dash(styles[index % len(styles)]) if len(styles) else None + stroke = self._stroke( + colors[index % len(colors)], widths[index % len(widths)], dash + ) + if stroke is not None: + pieces.append((self._to_data(display), stroke + ["mark=none"])) + if total > self.options["max_points"]: + return None + texts = list(getattr(contours, "labelTexts", [])) + + def emit(): + for data, options in pieces: + self._add(data, options) + for text in texts: + self._text(text)() + + return emit + + def _scatter(self, collection): + offsets = np.ma.asarray(collection.get_offsets()).filled(np.nan) + paths = collection.get_paths() + count = len(offsets) + if count > self.options["max_markers"] or len(paths) != 1: + return None + if count == 0: + return lambda: None + display = collection.get_offset_transform().transform(offsets) + inside = self._inside(display) + data = self._to_data(display) + collection.update_scalarmappable() + faces = collection.get_facecolors() + edges = collection.get_edgecolors() + if isinstance(edges, str) or len(edges) == 0: + edges = np.zeros((1, 4)) + sizes = collection.get_sizes() + widths = np.atleast_1d(collection.get_linewidths()) + marker = _marker_name(paths[0]) + + def rows(values): + values = np.asarray(values) + if len(values) == 0: + return np.zeros((count, 4)) + return values[np.arange(count) % len(values)] + + faces, edges = rows(faces), rows(edges) + # marker sizes are areas in pt^2; marks are sized by their diameter + if len(sizes): + sizes = np.sqrt(np.asarray(sizes, dtype=float))[ + np.arange(count) % len(sizes) + ] + else: + sizes = np.full(count, 6.0) + widths = widths[np.arange(count) % len(widths)] + # one plot per style: colors rounded to 1/63, sizes to quarter points + keys = np.column_stack( + [ + np.round(faces * 63), + np.round(edges * 63), + np.round(sizes * 4), + np.round(widths * 4), + ] + ) + unique, inverse = np.unique(keys, axis=0, return_inverse=True) + inverse = np.ravel(inverse) + if len(unique) > self.options["max_items"]: + return None + label = self._legend_label(collection) + groups = [] + for group in range(len(unique)): + members = np.flatnonzero((inverse == group) & inside) + if not len(members): + continue + first = members[0] + options = ["only marks"] + self._mark( + marker, + sizes[first], + faces[first], + edges[first], + widths[first], + ) + groups.append((data[members], options)) + + def emit(): + for index, (points, options) in enumerate(groups): + self._add(points, options, label if index == 0 else "") + + return emit + + def _line_collection(self, collection): + offsets = collection.get_offsets() + if len(collection.get_transforms()) or (len(offsets) and np.any(offsets)): + return None + collection.update_scalarmappable() + segments = collection.get_segments() + if not segments: + return lambda: None + transform = collection.get_transform() + colors = collection.get_colors() + widths = np.atleast_1d(collection.get_linewidths()) + styles = collection.get_linestyles() + alpha = None # the colors include the collection's alpha + # consecutive segments of one style become one plot, separated by nan + runs = [] + for index, segment in enumerate(segments): + if len(segment) == 0: + continue + color = colors[index % len(colors)] if len(colors) else (0, 0, 0, 0) + width = widths[index % len(widths)] + style = styles[index % len(styles)] if len(styles) else (0, None) + key = (tuple(np.round(color, 4)), float(width), str(style)) + display = self._clip_line( + transform.transform(np.asarray(segment, dtype=float)) + ) + if not len(display): + continue + if runs and runs[-1][0] == key: + runs[-1][1].append(display) + else: + runs.append((key, [display], color, width, style)) + if len(runs) > self.options["max_items"]: + return None + pieces = [] + for key, displays, color, width, style in runs: + stroke = self._stroke(color, width, _dash(style), alpha) + if stroke is None: + continue + joined = [] + for display in displays: + joined.extend([display, [[np.nan, np.nan]]]) + pieces.append( + (self._to_data(np.concatenate(joined[:-1])), stroke + ["mark=none"]) + ) + if sum(len(data) for data, _ in pieces) > self.options["max_points"]: + return None + label = self._legend_label(collection) + + def emit(): + for index, (data, options) in enumerate(pieces): + self._add(data, options, label if index == 0 else "") + + return emit + + def _polygons(self, collection): + offsets = collection.get_offsets() + if len(collection.get_transforms()) or (len(offsets) and np.any(offsets)): + return None + paths = collection.get_paths() + if len(paths) > self.options["max_items"]: + return None + collection.update_scalarmappable() + transform = collection.get_transform() + faces = collection.get_facecolors() + edges = collection.get_edgecolors() + widths = np.atleast_1d(collection.get_linewidths()) + styles = collection.get_linestyles() + pieces = [] + for index, path in enumerate(paths): + face = faces[index % len(faces)] if len(faces) else "none" + edge = edges[index % len(edges)] if len(edges) else "none" + width = widths[index % len(widths)] + style = styles[index % len(styles)] if len(styles) else (0, None) + options = self._area_options(face, edge, width, _dash(style)) + if options is None: + continue + for polygon in transform.transform_path(path).to_polygons(): + polygon = self._clip_polygon(polygon) + if len(polygon): + pieces.append((self._to_data(polygon), options)) + label = self._legend_label(collection) + + def emit(): + for index, (data, options) in enumerate(pieces): + self._add(data, options, label if index == 0 else "", cycle=True) + + return emit + + def _area_options(self, face, edge, width, dash, alpha=None): + fill = self._fill(face, alpha) + stroke = self._stroke(edge, width, dash, alpha) + if fill is None and stroke is None: + return None + options = list(fill or ["fill=none"]) + options.extend(stroke or ["draw=none"]) + options.extend(["mark=none", "area legend"]) + return options + + def _patch(self, patch, clip=None): + if clip is None: + clip = patch.get_clip_on() + transform = patch.get_transform() + path = patch.get_path() + fillable = None + if isinstance(patch, FancyArrowPatch): + try: + paths, fillable = patch._get_path_in_displaycoord() + except Exception: # pragma: no cover - private Matplotlib API + return None + if not np.iterable(fillable): + paths, fillable = [paths], [fillable] + transform = None + else: + paths, fillable = [path], [True] + # the patch's colors include its alpha + face = patch.get_facecolor() if patch.get_fill() else "none" + dash = _dash(patch._dash_pattern) + pieces = [] + for sub_path, can_fill in zip(paths, fillable): + if transform is not None: + sub_path = transform.transform_path(sub_path) + polygons = sub_path.to_polygons(closed_only=False) + options = self._area_options( + face if can_fill else "none", + patch.get_edgecolor(), + patch.get_linewidth(), + dash, + ) + if options is None: + continue + for polygon in polygons: + closed = can_fill and len(polygon) > 2 + polygon = ( + self._clip_polygon(polygon) if closed else self._clip_line(polygon) + ) + if len(polygon): + pieces.append((self._to_data(polygon), options, closed)) + label = self._legend_label(patch) + + def emit(): + for index, (data, options, closed) in enumerate(pieces): + self._add( + data, options, label if index == 0 else "", cycle=closed, clip=clip + ) + + return emit + + def _text(self, text): + if not text.get_text().strip(): + return lambda: None + if isinstance(text, Annotation): + text.update_positions(self.figure._get_renderer()) + arrow = ( + getattr(text, "arrow_patch", None) if isinstance(text, Annotation) else None + ) + arrow_emit = None + if arrow is not None and arrow.get_visible(): + arrow_emit = self._patch(arrow, clip=False) + if arrow_emit is None: + return None + display = _text_display_position(text) + if not self.figure.bbox.contains(*display): + return arrow_emit or (lambda: None) + fx, fy = self.ax.transAxes.inverted().transform(display) + node = _text_node(text, self.colors, f"(axis description cs:{fx:.5g},{fy:.5g})") + + def emit(): + if arrow_emit is not None: + arrow_emit() + self.axis.add_raw(node) + + return emit + + +# artists drawn as images without a warning: images, meshes, filled contours, +# quivers and the like have no better form in pgfplots +_ALWAYS_RASTER = (AxesImage, Collection) + + +def _marker_name(path) -> str: + for marker in _MARKS: + style = MarkerStyle(marker) + candidate = style.get_path().transformed(style.get_transform()) + if candidate.vertices.shape == path.vertices.shape and np.allclose( + candidate.vertices, path.vertices + ): + return marker + return "o" + + +def _with_gaps(path): + """The vertices of a path as one array, its separate pieces joined by nan.""" + out = [] + for vertex, code in zip(path.vertices, path.codes): + if code == Path.STOP: + break + if code == Path.MOVETO and out: + out.append((np.nan, np.nan)) + out.append(tuple(vertex)) + return np.asarray(out, dtype=float).reshape(-1, 2) diff --git a/src/maxplotlib/backends/tikzfigure/text.py b/src/maxplotlib/backends/tikzfigure/text.py new file mode 100644 index 0000000..616fb9c --- /dev/null +++ b/src/maxplotlib/backends/tikzfigure/text.py @@ -0,0 +1,183 @@ +"""Matplotlib text as LaTeX for the tikzfigure backend. + +Matplotlib text is plain text with optional ``$...$`` mathtext. Plain text +is escaped for LaTeX (``_``, ``%``, ``&`` and so on are literal in +Matplotlib); mathtext is passed on, being nearly LaTeX already, without +Matplotlib's own commands such as ``\\mathdefault``. Unicode symbols +(``ω``, ``−``, ``°``) become math, which pdflatex can typeset. +""" + +import re + +# Unicode characters pdflatex does not typeset by default, as math. +_UNICODE_MATH = { + "α": r"\alpha", + "β": r"\beta", + "γ": r"\gamma", + "δ": r"\delta", + "ε": r"\varepsilon", + "ϵ": r"\epsilon", + "ζ": r"\zeta", + "η": r"\eta", + "θ": r"\theta", + "ϑ": r"\vartheta", + "ι": r"\iota", + "κ": r"\kappa", + "λ": r"\lambda", + "μ": r"\mu", + "µ": r"\mu", + "ν": r"\nu", + "ξ": r"\xi", + "π": r"\pi", + "ρ": r"\rho", + "σ": r"\sigma", + "τ": r"\tau", + "υ": r"\upsilon", + "φ": r"\varphi", + "ϕ": r"\phi", + "χ": r"\chi", + "ψ": r"\psi", + "ω": r"\omega", + "Γ": r"\Gamma", + "Δ": r"\Delta", + "Θ": r"\Theta", + "Λ": r"\Lambda", + "Ξ": r"\Xi", + "Π": r"\Pi", + "Σ": r"\Sigma", + "Φ": r"\Phi", + "Ψ": r"\Psi", + "Ω": r"\Omega", + "−": "-", + "±": r"\pm", + "∓": r"\mp", + "×": r"\times", + "·": r"\cdot", + "÷": r"\div", + "°": r"^\circ", + "≈": r"\approx", + "∼": r"\sim", + "≤": r"\leq", + "≥": r"\geq", + "≠": r"\neq", + "∝": r"\propto", + "∞": r"\infty", + "∂": r"\partial", + "∇": r"\nabla", + "∫": r"\int", + "∑": r"\sum", + "√": r"\surd", + "‖": r"\|", + "∥": r"\parallel", + "⊥": r"\perp", + "→": r"\rightarrow", + "←": r"\leftarrow", + "↔": r"\leftrightarrow", + "⟨": r"\langle", + "⟩": r"\rangle", + "ℏ": r"\hbar", + "ℓ": r"\ell", + "′": r"\prime", + "⊙": r"\odot", + "⊗": r"\otimes", + "⁰": "^{0}", + "¹": "^{1}", + "²": "^{2}", + "³": "^{3}", + "⁴": "^{4}", + "⁵": "^{5}", + "⁶": "^{6}", + "⁷": "^{7}", + "⁸": "^{8}", + "⁹": "^{9}", + "⁻": "^{-}", + "₀": "_{0}", + "₁": "_{1}", + "₂": "_{2}", + "₃": "_{3}", +} + +_TEXT_ESCAPES = { + "\\": r"\textbackslash{}", + "&": r"\&", + "%": r"\%", + "#": r"\#", + "_": r"\_", + "{": r"\{", + "}": r"\}", + "~": r"\textasciitilde{}", + "^": r"\textasciicircum{}", + "$": r"\$", +} + +# mathtext commands that LaTeX lacks, and what they mean there +_MATH_COMMANDS = [ + (re.compile(r"\\mathdefault\{([^{}]*)\}"), r"\1"), + (re.compile(r"\\mathdefault\b"), ""), + (re.compile(r"\\degree\b"), r"{^\\circ}"), + (re.compile(r"\\AA\b"), r"\\text{\\AA}"), +] + + +def _math(source: str) -> str: + for pattern, replacement in _MATH_COMMANDS: + source = pattern.sub(replacement, source) + out = [] + for char in source: + symbol = _UNICODE_MATH.get(char) + if symbol is None: + out.append(char) + elif symbol.startswith("\\") and symbol[-1:].isalpha(): + out.append(symbol + " ") + else: + out.append(symbol) + return "".join(out) + + +def _text(source: str) -> str: + out = [] + for char in source: + if char in _TEXT_ESCAPES: + out.append(_TEXT_ESCAPES[char]) + elif char in _UNICODE_MATH: + out.append(f"${_UNICODE_MATH[char]}$") + elif char == "\n": + out.append(r"\\") + else: + out.append(char) + return "".join(out) + + +def latex(text) -> str: + r"""Matplotlib text as LaTeX: plain parts escaped, ``$...$`` parts as math. + + An odd number of unescaped ``$`` makes Matplotlib show the text as it + is, so then every part is plain text. + + Examples + -------- + >>> latex(r"growth rate $\gamma/\omega_{ci}$ (50% fit)") + 'growth rate $\\gamma/\\omega_{ci}$ (50\\% fit)' + >>> latex(r"$\mathdefault{0.5}$") + '$0.5$' + >>> latex("ω = 2.5") + '$\\omega$ = 2.5' + """ + if text is None: + return "" + text = str(text) + parts = re.split(r"(? bool: + """Whether ``text`` has more than one line, needing ``align`` in TikZ.""" + return "\n" in str(text or "") diff --git a/src/maxplotlib/canvas/canvas.py b/src/maxplotlib/canvas/canvas.py index 2a5fcca..fb08d64 100644 --- a/src/maxplotlib/canvas/canvas.py +++ b/src/maxplotlib/canvas/canvas.py @@ -18,13 +18,8 @@ from maxplotlib.backends.plotext import PlotextFigure, create_plotext_figure from maxplotlib.colors.colors import Color from maxplotlib.linestyle.linestyle import Linestyle -from maxplotlib.subfigure.line_plot import ( - _TIKZ_SUPPORTED_PLOT_TYPES, - LinePlot, - _tikz_error_bounds, - _tikz_step_coordinates, - _tikz_style_kwargs, -) +from maxplotlib.subfigure.line_plot import LinePlot +from maxplotlib.utils import xarray_support from maxplotlib.utils.options import Backends @@ -370,6 +365,10 @@ def __init__( self._supylabel_kwargs: dict = {} self._subplots_adjust_kwargs: dict = {} self._tight_layout_kwargs: dict | None = None + self._hide_empty_subplots = False + # Set by Canvas.facet: merge Plotly legend entries across subplots and + # leave room for the figure title above the subplot titles. + self._facet = False self._set_tight_layout = None self._align_labels = False self._align_titles = False @@ -393,6 +392,40 @@ def __init__( # Factory # ------------------------------------------------------------------ + @classmethod + def from_matplotlib( + cls, source, *, strict=False, trusted=False, fallback="native", **canvas_kwargs + ): + """Snapshot a Matplotlib figure, axes, or rectangular axes array. + + Portable geometry becomes independent plot entries. With the default + ``fallback="native"``, other built-in artists retain detached native + geometry for Matplotlib, including transforms and decorations. These + entries reject rendering on backends that cannot represent them. + ``fallback="skip"`` instead warns about unsupported artists; + ``fallback="raster"`` explicitly flattens the selection into an image. + + ``strict=True`` raises on import losses; it does not promise identical + output on every backend. ``canvas.import_report`` contains artist + identities, severities, representations and backend restrictions. + + Paths, pickle bytes and binary streams require ``trusted=True`` before + reading: unpickling can execute arbitrary code. Serialized figures are + only suitable for trusted producers and matching Matplotlib versions. + ``canvas_kwargs`` override figure defaults. Two-dimensional input + arrays define their own slot order; other inputs preserve source layout. + """ + from maxplotlib.backends.matplotlib.importer import import_matplotlib + + return import_matplotlib( + cls, + source, + strict=strict, + trusted=trusted, + fallback=fallback, + **canvas_kwargs, + ) + @classmethod def subplots( cls, @@ -452,6 +485,171 @@ def subplots( return canvas, [row[0] for row in axes] return canvas, axes + _FACET_KINDS = { + "plot": "plot", + "scatter": "scatter", + "pcolormesh": "pcolormesh", + "imshow": "add_imshow", + "contour": "contour", + "contourf": "contourf", + } + + @classmethod + def facet( + cls, + da, + col=None, + row=None, + col_wrap: int | None = None, + kind: str = "pcolormesh", + sharey: bool = True, + canvas_kwargs: dict | None = None, + **kwargs, + ): + """ + Create a Canvas with one subplot per value of a DataArray dimension. + + Parameters: + da (xarray.DataArray): The data. After removing ``col``/``row``, it + must have the dimensions ``kind`` needs: 1-D for ``"plot"`` and + ``"scatter"`` (2-D with ``hue=``), 2-D for the others. + col, row (str): Dimensions to lay out across columns and rows. + col_wrap (int): Wrap a ``col`` facet after this many columns. + kind (str): ``"plot"``, ``"scatter"``, ``"pcolormesh"``, ``"imshow"``, + ``"contour"`` or ``"contourf"``. + sharey (bool): For ``"plot"``/``"scatter"``, give every subplot the + value range of the whole array (on the x-axis with ``ycoord=``). + With ``False`` each subplot scales its own axis and keeps its y + tick labels. + canvas_kwargs (dict): Forwarded to the Canvas constructor. + **kwargs: Forwarded to each subplot's ``kind`` method. + + Color-mapped kinds share one color scale (``vmin``/``vmax`` default to + the data range, and contour levels are shared) and one colorbar for + the whole figure, which ``add_colorbar=False`` turns off. Axis and + tick labels are kept on the outer subplots only. Each subplot is + titled with the coordinate values that differ between subplots, and + the figure with the ones they share. + + Returns: + (canvas, axes): The Canvas and a 2-D list of LinePlots, with ``None`` + where a wrapped grid has no subplot. + + Examples: + >>> canvas, axes = Canvas.facet(da, col="t", col_wrap=3) + >>> canvas, axes = Canvas.facet(da, row="species", kind="plot") + """ + if not xarray_support.is_dataarray(da): + raise TypeError("facet() needs an xarray.DataArray") + if kind not in cls._FACET_KINDS: + raise ValueError( + f"kind must be one of {sorted(cls._FACET_KINDS)}, got {kind!r}" + ) + if col is None and row is None: + raise ValueError("facet() needs col= and/or row=") + if col == row: + raise ValueError("col and row must be different dimensions") + for dim in (col, row): + if dim is not None and dim not in da.dims: + raise ValueError(f"{dim!r} is not a dimension of {da.dims}") + if col_wrap is not None and (col is None or row is not None): + raise ValueError("col_wrap= needs col= and no row=") + + ncol_values = da.sizes[col] if col is not None else 1 + nrow_values = da.sizes[row] if row is not None else 1 + if col_wrap is not None: + ncols = max(1, min(col_wrap, ncol_values)) + nrows = -(-ncol_values // ncols) + panels = [(j // ncols, j % ncols, {col: j}) for j in range(ncol_values)] + else: + ncols, nrows = ncol_values, nrow_values + panels = [ + (i, j, {d: k for d, k in ((row, i), (col, j)) if d is not None}) + for i in range(nrows) + for j in range(ncols) + ] + + mapped = kind not in ("plot", "scatter") + sharey = sharey or mapped + add_colorbar = kwargs.pop("add_colorbar", kind != "contour") + values = xarray_support.magnitude(da) + if mapped: + # Shared limits for every panel, with xarray's color defaults. + xarray_support.color_limits(values, kwargs) + kwargs.setdefault("vmin", float(np.nanmin(values))) + kwargs.setdefault("vmax", float(np.nanmax(values))) + if kind in ("contour", "contourf") and "levels" not in kwargs: + from matplotlib.ticker import MaxNLocator + + kwargs["levels"] = MaxNLocator(10).tick_values( + kwargs["vmin"], kwargs["vmax"] + ) + # One colorbar for the figure, added below; "colorbar" hides the + # per-trace scale Plotly would otherwise show. + kwargs["add_colorbar"] = False + kwargs["colorbar"] = False + + canvas_kwargs = dict(canvas_kwargs or {}) + if not {"subplot_spacing", "gridspec_kw"} & canvas_kwargs.keys(): + canvas_kwargs["subplot_spacing"] = SubplotSpacing( + wspace=0.1 if sharey else 0.3, hspace=0.3 + ) + canvas = cls(nrows=nrows, ncols=ncols, **canvas_kwargs) + canvas._hide_empty_subplots = True + canvas._facet = True + # Coordinates shared by every panel go in the figure title, so the + # panel titles only show what differs between them. + common = [name for name, coord in da.coords.items() if coord.ndim == 0] + common_title = xarray_support.title(da) + if common_title: + canvas.suptitle(common_title) + axes = [[None] * ncols for _ in range(nrows)] + method = cls._FACET_KINDS[kind] + for r, c, selection in panels: + subplot = canvas.add_subplot(row=r, col=c) + panel = da.isel(selection) + subplot.set_title( + xarray_support.title(panel, exclude=common) + or ", ".join(f"{dim} = {index}" for dim, index in selection.items()) + ) + getattr(subplot, method)(panel, **kwargs) + axes[r][c] = subplot + + vertical = "ycoord" in kwargs + if not mapped and sharey: + low, high = float(np.nanmin(values)), float(np.nanmax(values)) + margin = 0.05 * (high - low) + for r, c, _ in panels: + subplot = axes[r][c] + tick_params = {} + if r + 1 < nrows and axes[r + 1][c] is not None: + subplot._xlabel = None + tick_params["labelbottom"] = False + if c > 0 and sharey: + subplot._ylabel = None + tick_params["labelleft"] = False + if tick_params: + subplot.tick_params(**tick_params) + if not mapped and sharey and vertical: + subplot.set_xlim(low - margin, high + margin) + elif not mapped and sharey: + subplot.set_ylim(low - margin, high + margin) + if (r, c) != panels[0][:2]: + subplot._legend = False + if mapped and add_colorbar: + first = axes[panels[0][0]][panels[0][1]] + first._add( + { + "label": xarray_support.value_label(da), + "layer": 0, + "plot_type": "colorbar", + "kwargs": {}, + "span_figure": True, + }, + 0, + ) + return canvas, axes + @property def _subplot_dict(self): return self._subplots @@ -575,7 +773,7 @@ def _get_or_create_subplot(self, row, col): def scatter( self, x, - y, + y=None, layer=0, row: int | None = None, col: int | None = None, @@ -590,6 +788,8 @@ def scatter( layer (int): Layer index (default 0). row, col (int): Subplot position (default top-left). **kwargs: Forwarded to the backend (e.g., color, marker, s, label). + + ``scatter(da)`` accepts an ``xarray.DataArray`` like ``plot(da)``. """ sp = self._get_or_create_subplot(row, col) sp.scatter(x, y, layer=layer, **kwargs) @@ -796,40 +996,44 @@ def eventplot( def contour( self, x, - y, - z, + y=None, + z=None, layer=0, row: int | None = None, col: int | None = None, **kwargs, ): - """Add contour lines to a subplot.""" + """Add contour lines to a subplot; ``contour(da)`` takes a DataArray.""" self._get_or_create_subplot(row, col).contour(x, y, z, layer=layer, **kwargs) def contourf( self, x, - y, - z, + y=None, + z=None, layer=0, row: int | None = None, col: int | None = None, **kwargs, ): - """Add filled contours to a subplot.""" + """Add filled contours to a subplot; ``contourf(da)`` takes a DataArray.""" self._get_or_create_subplot(row, col).contourf(x, y, z, layer=layer, **kwargs) def pcolormesh( self, x, - y, - z, + y=None, + z=None, layer=0, row: int | None = None, col: int | None = None, **kwargs, ): - """Add a pseudocolor mesh to a subplot.""" + """Add a pseudocolor mesh to a subplot. + + ``pcolormesh(da)`` accepts a 2-D ``xarray.DataArray``; see + :meth:`LinePlot.pcolormesh`. + """ self._get_or_create_subplot(row, col).pcolormesh(x, y, z, layer=layer, **kwargs) def hexbin( @@ -1559,7 +1763,7 @@ def imshow( col: int | None = None, **kwargs, ): - """Add an image/matrix plot to a subplot.""" + """Add an image/matrix plot to a subplot; ``imshow(da)`` takes a DataArray.""" self._get_or_create_subplot(row, col).add_imshow(data, layer=layer, **kwargs) def add_image( @@ -1867,7 +2071,17 @@ def savefig( layers: list | None = None, layer_by_layer: bool = False, verbose: bool = False, + include_plotlyjs: bool | str = True, ): + """Render and save the canvas. + + ``include_plotlyjs`` only applies to the Plotly backend when saving + to an ``.html``/``.htm`` file (see ``fig.write_html`` in the Plotly + docs). It defaults to ``True``, which bundles plotly.js into the + file so it works offline. Pass ``"cdn"`` to instead reference + plotly.js from a CDN, producing a much smaller file that requires + network access to render. + """ filename_no_extension, extension = os.path.splitext(filename) if backend == "matplotlib": if layer_by_layer: @@ -1942,7 +2156,9 @@ def savefig( savefig=False, layers=layers, ) - self._save_plotly(fig, full_filepath) + self._save_plotly( + fig, full_filepath, include_plotlyjs=include_plotlyjs + ) if verbose: print(f"Saved {full_filepath}") else: @@ -1956,7 +2172,7 @@ def savefig( savefig=False, layers=layers, ) - self._save_plotly(fig, full_filepath) + self._save_plotly(fig, full_filepath, include_plotlyjs=include_plotlyjs) if verbose: print(f"Saved {full_filepath}") elif backend == "tikzfigure": @@ -2028,7 +2244,7 @@ def _render( verbose=verbose, ) elif backend == "tikzfigure": - return self.plot_tikzfigure(savefig=savefig, verbose=verbose) + return self.plot_tikzfigure(savefig=savefig, layers=layers, verbose=verbose) else: raise ValueError(f"Invalid backend: {backend}") @@ -2036,10 +2252,19 @@ def plot(self, *args, backend=None, **kwargs): """Add a line, or render when called with backend options. ``canvas.plot(x, y, **style)`` is the convenient direct plotting form. + ``canvas.plot(da)`` plots a 1-D ``xarray.DataArray`` against its + coordinate, labelling the axes from its attributes; ``hue=`` + draws a 2-D one as one line per value of ``dim``. Rendering is named explicitly by ``canvas.render(...)``; the legacy ``canvas.plot(backend=...)`` form remains supported. """ explicit_render = backend is not None or (args and isinstance(args[0], str)) + if len(args) == 1 and xarray_support.is_dataarray(args[0]): + layer = kwargs.pop("layer", 0) + row = kwargs.pop("row", None) + col = kwargs.pop("col", None) + self._get_or_create_subplot(row, col).plot(args[0], layer=layer, **kwargs) + return self if args and not isinstance(args[0], str): if len(args) < 2: raise TypeError("plot(x, y) requires both x and y data") @@ -2290,14 +2515,62 @@ def plot_matplotlib( if verbose: print(f"Created Matplotlib figure and axes with shape {axes.shape}") + if hasattr(self, "_import_layout"): + for row in range(self.nrows): + for col in range(self.ncols): + if (row, col) not in self._subplot_dict: + axes[row, col].remove() + axes[row, col] = None + for slot, bounds in self._import_layout.items(): + axes[slot].set_position(bounds) + if hasattr(self, "_import_layout_specs"): + for slot, spec in self._import_layout_specs.clone(fig=fig).items(): + if spec is not None: + axes[slot].set_subplotspec(spec) + else: + bounds = axes[slot].get_position(original=True).bounds + axes[slot].remove() + axes[slot] = fig.add_axes(bounds) + if self._import_layout_engine is not None: + import copy + + fig.set_layout_engine(copy.deepcopy(self._import_layout_engine)) + for slot, in_layout in self._import_in_layout.items(): + axes[slot].set_in_layout(in_layout) + for direction, parent, child in self._import_shared_axes: + getattr(axes[child], "share" + direction)(axes[parent]) + if hasattr(self, "_import_figure_style"): + fig.set(**self._import_figure_style) + fig.patch = self._import_figure_patch.clone(fig=fig) + transform = fig.patch.get_transform() + fig._set_artist_props(fig.patch) + fig.patch.set_transform(transform) for (row, col), subplot in self._subplot_dict.items(): ax = axes[row][col] + if hasattr(subplot, "_import_projection"): + position = ax.get_position().bounds + ax.remove() + ax = subplot._import_projection.clone(fig=fig) + fig.add_axes(ax) + ax.set_position(position) + axes[row, col] = ax if isinstance(subplot, TikzFigure): plot_matplotlib(subplot, ax, layers=layers) + ax.grid(False) else: subplot.plot_matplotlib(ax, layers=layers) - # ax.set_title(f"Subplot ({row}, {col})") - ax.grid() + + if self._hide_empty_subplots: + for row in range(self.nrows): + for col in range(self.ncols): + if (row, col) not in self._subplot_dict: + axes[row][col].set_visible(False) + figure_axes = [ax for ax in fig.axes if ax.get_visible()] + for subplot in self._subplot_dict.values(): + figure_colorbar = getattr(subplot, "_figure_colorbar", None) + if figure_colorbar is not None and figure_colorbar[0] is not None: + mappable, label = figure_colorbar + fig.colorbar(mappable, ax=figure_axes, label=label) if verbose: print("Finished plotting subplots.") @@ -2311,6 +2584,10 @@ def plot_matplotlib( fig.supxlabel(self._supxlabel, **self._supxlabel_kwargs) if self._supylabel: fig.supylabel(self._supylabel, **self._supylabel_kwargs) + if self._facet and self._suptitle and "top" not in self._subplots_adjust_kwargs: + # About four font heights: the figure title plus subplot titles. + room = 4 * self.fontsize / 72 + fig.subplots_adjust(top=max(0.5, 1 - room / fig.get_figheight())) if self._subplots_adjust_kwargs: fig.subplots_adjust(**self._subplots_adjust_kwargs) if self._tight_layout_kwargs is not None: @@ -2343,6 +2620,43 @@ def plot_matplotlib( twin_axis = axes[row][col].twiny() twin_subplot.plot_matplotlib(twin_axis, layers=layers) self._matplotlib_twiny_axes[(row, col)] = twin_axis + if hasattr(self, "_import_layout"): + from maxplotlib.backends.matplotlib.import_state import apply_axis_state + + for slot, direction, subplot in self._import_extra_twins: + twin_axis = getattr(axes[slot], "twin" + direction)() + subplot.plot_matplotlib(twin_axis, layers=layers) + for specification in self._import_colorbars: + if specification["standalone"] is not None: + import copy + + from matplotlib.cm import ScalarMappable + + mappable = ScalarMappable( + **copy.deepcopy(specification["standalone"]) + ) + else: + artists = specification["target"]._import_rendered_artists.get( + specification["artist_id"], [] + ) + mappable = next( + (artist for artist in artists if hasattr(artist, "get_cmap")), + None, + ) + if mappable is None: + continue # Its layer was excluded from this render. + cax = fig.add_axes(specification["position"]) + colorbar = fig.colorbar(mappable, cax=cax, **specification["kwargs"]) + colorbar.set_label( + specification["label"], **specification["label_style"] + ) + colorbar.set_ticks(specification["ticks"]) + colorbar.formatter = specification["formatter"].clone(cax) + colorbar.update_ticks() + apply_axis_state(cax, specification["state"]) + for snapshot in self._import_figure_artists: + artist = snapshot.clone(fig=fig) + fig.add_artist(artist) if matplotlib_customizations is not None: _apply_matplotlib_customizations(fig, axes, matplotlib_customizations) if matplotlib_postprocess is not None: @@ -2351,335 +2665,123 @@ def plot_matplotlib( matplotlib_postprocess(fig, axes) return fig, axes + def _validate_import_backend(self, backend, *, allow_unsupported=False): + """Never silently drop native imported axes or figure decorations.""" + if not hasattr(self, "import_report") or allow_unsupported: + return + reasons = [] + if getattr(self, "_import_extra_twins", []): + reasons.append("multiple twin axes") + if getattr(self, "_import_figure_artists", []): + reasons.append("native figure decorations") + if getattr(self, "_import_colorbars", []): + reasons.append("native colorbars") + subplots = ( + list(self._subplot_dict.values()) + + list(self._twinx_subplots.values()) + + list(self._twiny_subplots.values()) + ) + for subplot in subplots: + if hasattr(subplot, "_import_projection"): + reasons.append("native projections") + if getattr(subplot, "_import_child_axes", []): + reasons.append("inset/secondary axes") + for state in ( + getattr(subplot, "_import_axis_state", {}).get("axes", {}).values() + ): + scale = state["scale"].payload + if scale.name not in ("linear", "log"): + reasons.append("native axis scales") + if reasons: + raise NotImplementedError( + f"{backend} cannot render these imported features: {', '.join(sorted(set(reasons)))}. " + "Use Matplotlib or import with fallback='raster'." + ) + def plot_tikzfigure( self, savefig: bool = False, verbose: bool = False, + *, + layers: list | None = None, + raster_dpi: float = 300, + max_markers: int = 2000, + max_items: int = 500, + max_points: int = 20000, + precision: int = 6, ) -> TikzFigure: - """ - Generate a TikZ figure from subplots. + """Render the canvas as a TikZ/pgfplots figure. - For now, returns the first subplot's TikzFigure. - Full multi-subplot support requires TikzFigure's subfigure_axis API. + The canvas is drawn with Matplotlib, off screen, and the drawn figure + is converted with + :func:`~maxplotlib.backends.tikzfigure.figure_to_tikz`: every subplot + becomes a pgfplots axis at the same place and size, with its labels, + ticks, legend and colorbar, lines, markers, bars, fills and text as + pgfplots code, and meshes, images and other artists without a vector + counterpart as images Matplotlib renders (``\\addplot graphics``). + Every layout, twin axes and imported figure therefore converts. Parameters: - verbose (bool): If True, print debug information. + savefig (bool): Unused; kept for the other backends' signature. + verbose (bool): If True, print progress. + layers (list): Draw only these layers, as with Matplotlib. + raster_dpi (float): Resolution of the parts drawn as images. + max_markers (int): Scatter plots with more points are drawn as an image. + max_items (int): Collections with more differently styled items are + drawn as an image. + max_points (int): Lines with more points (after simplification) are + drawn as an image. + precision (int): Significant digits of the written coordinates. Returns: - TikzFigure: Figure object that can be shown, saved, or compiled. + TikzFigure: Figure object that can be shown, saved (``.tikz`` and + ``.tex`` with the images next to them, ``.pdf``, ``.png``) or + compiled. """ - if verbose: - print(f"Plotting tikzfigure with {len(self._subplot_dict)} subplot(s)") - - if self._twinx_subplots: - raise NotImplementedError( - "twinx plots are currently supported only by the matplotlib and plotly backends" - ) - - # Check for unsupported layouts - if self.nrows > 1: - raise NotImplementedError( - "Vertical/grid layouts (nrows > 1) are not yet supported for tikzfigure backend. " - "Use horizontal layouts (1×n) only." - ) - - # Validate that at least one subplot exists - if len(self._subplot_dict) == 0: - raise ValueError( - "No subplots to plot. Call add_subplot() or Canvas.subplots() first." - ) - - axis_width, axis_height = self._get_tikzfigure_axis_dimensions() - fig = TikzFigure() - - # Add each subplot as a subfigure axis - for (row, col), line_plot in self._subplot_dict.items(): - if verbose: - print(f"Plotting subplot at row {row}, col {col}") - - # Create subfigure axis with subplot metadata - ax = fig.subfigure_axis( - xlabel=line_plot._xlabel or "", - ylabel=line_plot._ylabel or "", - xlim=( - (line_plot._xmin, line_plot._xmax) - if line_plot._xmin is not None - else None - ), - ylim=( - (line_plot._ymin, line_plot._ymax) - if line_plot._ymin is not None - else None - ), - grid=line_plot._grid, - title=line_plot._title or f"Subplot {col + 1}", - width=0.45, - axis_width=axis_width, - height=axis_height, - ) + from maxplotlib.backends.tikzfigure import figure_to_tikz - # Add each plot line to the subfigure - for line_data in line_plot.line_data: - plot_type = line_data.get("plot_type") - if plot_type not in _TIKZ_SUPPORTED_PLOT_TYPES: - raise NotImplementedError( - f"{plot_type} is not supported by the tikzfigure backend" - ) - if plot_type == "plot": - # Extract and transform x, y data - x = (line_data["x"] + line_plot._xshift) * line_plot._xscale - y = (line_data["y"] + line_plot._yshift) * line_plot._yscale - kwargs = line_data.get("kwargs", {}) - if verbose: - print(f"Line {kwargs = }") - # Add plot to subfigure axis - ax.add_plot( - x=x, - y=y, - **_tikz_style_kwargs(kwargs), - ) - elif plot_type == "scatter": - x = (line_data["x"] + line_plot._xshift) * line_plot._xscale - y = (line_data["y"] + line_plot._yshift) * line_plot._yscale - kwargs = _tikz_style_kwargs(line_data.get("kwargs", {})) - kwargs.setdefault("mark", "*") - kwargs["line_width"] = 0 - ax.add_plot(x=x, y=y, **kwargs) - elif plot_type in {"bar", "barh"}: - source_kwargs = line_data.get("kwargs", {}) - kwargs = _tikz_style_kwargs(source_kwargs) - kwargs["fill"] = source_kwargs.get("color", "blue") - kwargs["fill_opacity"] = source_kwargs.get("alpha", 1.0) - kwargs["line_width"] = source_kwargs.get("linewidth", 0) - if plot_type == "bar": - width = source_kwargs.get("width", 0.8) - for x, height in zip(line_data["x"], line_data["height"]): - ax.add_plot( - x=[ - x - width / 2, - x + width / 2, - x + width / 2, - x - width / 2, - ], - y=[0, 0, height, height], - cycle=True, - **kwargs, - ) - else: - height = source_kwargs.get("height", 0.8) - for y, width in zip(line_data["y"], line_data["width"]): - ax.add_plot( - x=[0, width, width, 0], - y=[ - y - height / 2, - y - height / 2, - y + height / 2, - y + height / 2, - ], - cycle=True, - **kwargs, - ) - elif plot_type == "fill_between": - x = line_data["x"] - y1 = np.asarray(line_data["y1"]) - y2 = np.broadcast_to(line_data["y2"], y1.shape) - source_kwargs = line_data.get("kwargs", {}) - kwargs = _tikz_style_kwargs(source_kwargs) - kwargs["fill"] = source_kwargs.get("color", "blue") - kwargs["fill_opacity"] = source_kwargs.get("alpha", 0.25) - ax.add_plot( - x=list(x) + list(x[::-1]), - y=list(y1) + list(y2[::-1]), - cycle=True, - **kwargs, - ) - elif plot_type == "errorbar": - x = line_data["x"] - y = line_data["y"] - kwargs = _tikz_style_kwargs(line_data.get("kwargs", {})) - ax.add_plot(x=x, y=y, **kwargs) - y_bounds = _tikz_error_bounds(line_data["yerr"], y) - if y_bounds is not None: - lower, upper = y_bounds - for xi, low, high in zip(x, y - lower, y + upper): - ax.add_plot(x=[xi, xi], y=[low, high], **kwargs) - x_bounds = _tikz_error_bounds(line_data["xerr"], x) - if x_bounds is not None: - lower, upper = x_bounds - for yi, low, high in zip(y, x - lower, x + upper): - ax.add_plot(x=[low, high], y=[yi, yi], **kwargs) - elif plot_type in {"step", "stairs"}: - source_kwargs = line_data.get("kwargs", {}) - if plot_type == "step": - x = line_data["x"] - y = line_data["y"] - where = source_kwargs.get("where", "pre") - else: - values = line_data["values"] - edges = line_data["edges"] - if edges is None: - edges = np.arange(len(values) + 1) - x = edges - y = np.r_[values, values[-1]] - where = "post" - x, y = _tikz_step_coordinates(x, y, where=where) - ax.add_plot( - x=x, - y=y, - **_tikz_style_kwargs(source_kwargs), - ) - elif plot_type == "stem": - x = line_data["x"] - y = line_data["y"] - source_kwargs = line_data.get("kwargs", {}) - style = _tikz_style_kwargs(source_kwargs) - marker_style = dict(style) - marker_style.update( - mark=source_kwargs.get("marker", "*"), line_width=0 - ) - ax.add_plot(x=x, y=y, **marker_style) - for xi, yi in zip(x, y): - ax.add_plot(x=[xi, xi], y=[0, yi], **style) - elif plot_type in {"hlines", "vlines"}: - style = _tikz_style_kwargs(line_data.get("kwargs", {})) - if plot_type == "hlines": - for yi, left, right in zip( - np.atleast_1d(line_data["y"]), - np.atleast_1d(line_data["xmin"]), - np.atleast_1d(line_data["xmax"]), - ): - ax.add_plot(x=[left, right], y=[yi, yi], **style) - else: - for xi, bottom, top in zip( - np.atleast_1d(line_data["x"]), - np.atleast_1d(line_data["ymin"]), - np.atleast_1d(line_data["ymax"]), - ): - ax.add_plot(x=[xi, xi], y=[bottom, top], **style) - elif plot_type in {"axvspan", "axhspan"}: - source_kwargs = line_data.get("kwargs", {}) - style = _tikz_style_kwargs(source_kwargs) - style["fill"] = source_kwargs.get("color", "blue") - style["fill_opacity"] = source_kwargs.get("alpha", 0.2) - if plot_type == "axvspan": - xmin, xmax = line_data["xmin"], line_data["xmax"] - ymin, ymax = line_plot._ymin or 0, line_plot._ymax or 1 - x = [xmin, xmax, xmax, xmin] - y = [ymin, ymin, ymax, ymax] - else: - ymin, ymax = line_data["ymin"], line_data["ymax"] - xmin, xmax = line_plot._xmin or 0, line_plot._xmax or 1 - x = [xmin, xmax, xmax, xmin] - y = [ymin, ymin, ymax, ymax] - ax.add_plot(x=x, y=y, cycle=True, **style) - elif plot_type == "fill": - if len(line_data["args"]) < 2: - raise ValueError("tikzfigure fill requires x and y coordinates") - x, y = line_data["args"][:2] - source_kwargs = line_data.get("kwargs", {}) - style = _tikz_style_kwargs(source_kwargs) - style["fill"] = source_kwargs.get("color", "blue") - style["fill_opacity"] = source_kwargs.get("alpha", 0.25) - ax.add_plot(x=x, y=y, cycle=True, **style) - elif plot_type == "flame_chart": - labels = line_data["labels"] - parents = line_data["parents"] - values = line_data["values"] * line_plot._xscale - start_times = line_data["start_times"] - depths = np.zeros(len(labels), dtype=int) - if start_times is None: - start_times = np.zeros(len(labels)) - else: - start_times = ( - start_times + line_plot._xshift - ) * line_plot._xscale - for index, parent in enumerate(parents): - if parent is not None: - parent_index = ( - parent - if isinstance(parent, int) - else labels.index(parent) - ) - depths[index] = depths[parent_index] + 1 - colors = ["red", "blue", "green", "orange", "purple", "cyan"] - for index, (start, value) in enumerate(zip(start_times, values)): - y = depths[index] - ax.add_plot( - x=[start, start + value, start + value, start], - y=[y - 0.4, y - 0.4, y + 0.4, y + 0.4], - cycle=True, - fill=colors[y % len(colors)], - line_width=0, - ) - elif plot_type == "gantt": - tasks = line_data["tasks"] - start_times = ( - line_data["start_times"] + line_plot._xshift - ) * line_plot._xscale - durations = line_data["durations"] * line_plot._xscale - y_positions = np.arange(len(tasks)) - kwargs = line_data.get("kwargs", {}) - - # Draw horizontal bars for each task as filled rectangles - for i, (task, start, duration) in enumerate( - zip(tasks, start_times, durations) - ): - x_start = float(start) - x_end = float(start + duration) - y_pos = float(y_positions[i]) - bar_height = 0.8 - - # Create rectangle coordinates for the bar - x_coords = [x_start, x_end, x_end, x_start, x_start] - y_coords = [ - y_pos - bar_height / 2, - y_pos - bar_height / 2, - y_pos + bar_height / 2, - y_pos + bar_height / 2, - y_pos - bar_height / 2, - ] - - # Add as a filled plot - color = kwargs.get("color", "blue") - ax.add_plot( - x=x_coords, - y=y_coords, - color=color, - fill=True, - line_width=0, - ) - - # Set y-axis ticks to show task names - if line_plot._yticks is None: - ax.set_ticks("y", list(y_positions), tasks) - - # Add legend if requested - if line_plot._legend and len(line_plot.line_data) > 0: - ax.set_legend(position="north east") - - return fig - - def _get_tikzfigure_axis_dimensions(self) -> tuple[str | None, str | None]: - if self._width is None: - return None, None - - total_width_in, total_height_in = set_size( - width=self._width, - ratio=self._ratio, - dpi=self._dpi if self._dpi is not None else 300, - ) - total_width_cm = total_width_in * 2.54 - total_height_cm = total_height_in * 2.54 - horizontal_sep_cm = getattr(TikzFigure, "GROUPPLOT_HORIZONTAL_SEP_CM", 1.5) - available_width_cm = total_width_cm - horizontal_sep_cm * (self.ncols - 1) - if available_width_cm <= 0: - raise ValueError( - f'Canvas width "{self._width}" is too small for {self.ncols} ' - "tikzfigure subplot(s)." + if verbose: + print("Drawing the canvas with Matplotlib for the tikzfigure backend") + # drawing with Matplotlib changes the global style and this canvas's + # record of its Matplotlib figure; neither is meant to change here + state = { + name: getattr(self, name) + for name in ( + "_plotted", + "_matplotlib_fig", + "_matplotlib_axes", + "_matplotlib_twin_axes", + "_matplotlib_twiny_axes", ) - - axis_width_cm = available_width_cm / self.ncols - return f"{axis_width_cm:.6g}cm", f"{total_height_cm:.6g}cm" + if hasattr(self, name) + } + with plt.rc_context(), plt.ioff(): + fig, _ = self.plot_matplotlib(savefig=False, layers=layers, verbose=verbose) + try: + tikz = figure_to_tikz( + fig, + raster_dpi=raster_dpi, + max_markers=max_markers, + max_items=max_items, + max_points=max_points, + precision=precision, + ) + finally: + plt.close(fig) + for name in ( + "_plotted", + "_matplotlib_fig", + "_matplotlib_axes", + "_matplotlib_twin_axes", + "_matplotlib_twiny_axes", + ): + if name in state: + setattr(self, name, state[name]) + elif hasattr(self, name): + delattr(self, name) + if verbose: + print(f"Converted {len(tikz.axes)} axes") + return tikz def plot_plotext( self, @@ -2687,6 +2789,7 @@ def plot_plotext( layers: list | None = None, verbose: bool = False, ) -> PlotextFigure: + self._validate_import_backend("plotext") if self._twinx_subplots: raise NotImplementedError( "twinx plots are not supported by the plotext backend" @@ -2734,6 +2837,7 @@ def plot_plotly( """ + self._validate_import_backend("plotly", allow_unsupported=allow_unsupported) resolved_usetex = self._usetex if usetex is None else usetex for subplot in self._subplot_dict.values(): @@ -2771,12 +2875,30 @@ def plot_plotly( subplot_titles=subplot_titles, specs=specs, ) + if self._hide_empty_subplots: + for row in range(self.nrows): + for col in range(self.ncols): + if (row, col) not in self._subplot_dict: + fig.update_xaxes(visible=False, row=row + 1, col=col + 1) + fig.update_yaxes(visible=False, row=row + 1, col=col + 1) # Plot each subplot and propagate axis labels/scale + legend_names = set() for (row, col), line_plot in self._subplot_dict.items(): traces, shapes, annotations = line_plot.plot_plotly( layers=layers, allow_unsupported=allow_unsupported ) + if self._facet: + # One legend entry per label across all subplots; clicking it + # toggles the matching trace in every subplot. + for trace in traces: + name = getattr(trace, "name", None) + if not name or trace.type in ("pie", "table"): + continue + trace.legendgroup = name + if name in legend_names: + trace.showlegend = False + legend_names.add(name) for trace in traces: if trace.type in ("pie", "table"): fig.add_trace(trace) @@ -3037,11 +3159,13 @@ def plot_plotly( self._plotly_fig = fig return fig - def _save_plotly(self, fig, filename: str) -> None: + def _save_plotly( + self, fig, filename: str, include_plotlyjs: bool | str = True + ) -> None: _, extension = os.path.splitext(filename) extension = extension.lower() if extension in {".html", ".htm"}: - fig.write_html(filename) + fig.write_html(filename, include_plotlyjs=include_plotlyjs) return try: fig.write_image(filename) @@ -3051,6 +3175,33 @@ def _save_plotly(self, fig, filename: str) -> None: "(e.g., `pip install -U kaleido`), or export to HTML instead." ) from exc + def to_html( + self, + layers: list | None = None, + include_plotlyjs: bool | str = "cdn", + full_html: bool = True, + verbose: bool = False, + allow_unsupported: bool = False, + ) -> str: + """Render the canvas with the Plotly backend and return standalone HTML. + + The returned markup embeds the figure data and calls plotly.js to + render it in a browser. By default ``include_plotlyjs="cdn"`` + references plotly.js from a CDN instead of bundling it, producing a + much smaller string suited to embedding in an existing page; pass + ``include_plotlyjs=True`` to inline plotly.js for offline use, as + ``savefig(..., backend="plotly")`` does. Set ``full_html=False`` to + get just the ``
    ``/``