maxplotlibx 0.1.9__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {maxplotlibx-0.1.9/src/maxplotlibx.egg-info → maxplotlibx-0.2.0}/PKG-INFO +74 -16
- {maxplotlibx-0.1.9 → maxplotlibx-0.2.0}/README.md +68 -14
- {maxplotlibx-0.1.9 → maxplotlibx-0.2.0}/pyproject.toml +7 -2
- maxplotlibx-0.2.0/src/maxplotlib/backends/matplotlib/import_state.py +342 -0
- maxplotlibx-0.2.0/src/maxplotlib/backends/matplotlib/importer.py +1078 -0
- maxplotlibx-0.2.0/src/maxplotlib/backends/tikzfigure/__init__.py +9 -0
- maxplotlibx-0.2.0/src/maxplotlib/backends/tikzfigure/convert.py +1331 -0
- maxplotlibx-0.2.0/src/maxplotlib/backends/tikzfigure/text.py +183 -0
- {maxplotlibx-0.1.9 → maxplotlibx-0.2.0}/src/maxplotlib/canvas/canvas.py +492 -341
- {maxplotlibx-0.1.9 → maxplotlibx-0.2.0}/src/maxplotlib/subfigure/line_plot.py +481 -439
- {maxplotlibx-0.1.9 → maxplotlibx-0.2.0}/src/maxplotlib/tests/test_canvas.py +36 -21
- {maxplotlibx-0.1.9 → maxplotlibx-0.2.0}/src/maxplotlib/tests/test_flame_chart.py +9 -0
- {maxplotlibx-0.1.9 → maxplotlibx-0.2.0}/src/maxplotlib/tests/test_gantt_chart.py +9 -0
- maxplotlibx-0.2.0/src/maxplotlib/tests/test_matplotlib_import.py +348 -0
- maxplotlibx-0.2.0/src/maxplotlib/tests/test_matplotlib_import_extended.py +612 -0
- maxplotlibx-0.2.0/src/maxplotlib/tests/test_tikzfigure_backend.py +259 -0
- maxplotlibx-0.2.0/src/maxplotlib/tests/test_xarray.py +896 -0
- maxplotlibx-0.2.0/src/maxplotlib/utils/xarray_support.py +346 -0
- maxplotlibx-0.2.0/src/maxplotlib/xarray.py +170 -0
- {maxplotlibx-0.1.9 → maxplotlibx-0.2.0/src/maxplotlibx.egg-info}/PKG-INFO +74 -16
- {maxplotlibx-0.1.9 → maxplotlibx-0.2.0}/src/maxplotlibx.egg-info/SOURCES.txt +11 -0
- {maxplotlibx-0.1.9 → maxplotlibx-0.2.0}/src/maxplotlibx.egg-info/requires.txt +6 -1
- {maxplotlibx-0.1.9 → maxplotlibx-0.2.0}/LICENSE +0 -0
- {maxplotlibx-0.1.9 → maxplotlibx-0.2.0}/setup.cfg +0 -0
- {maxplotlibx-0.1.9 → maxplotlibx-0.2.0}/src/maxplotlib/__init__.py +0 -0
- {maxplotlibx-0.1.9 → maxplotlibx-0.2.0}/src/maxplotlib/backends/matplotlib/utils.py +0 -0
- {maxplotlibx-0.1.9 → maxplotlibx-0.2.0}/src/maxplotlib/backends/plotext/__init__.py +0 -0
- {maxplotlibx-0.1.9 → maxplotlibx-0.2.0}/src/maxplotlib/backends/plotext/figure.py +0 -0
- {maxplotlibx-0.1.9 → maxplotlibx-0.2.0}/src/maxplotlib/backends/plotly/__init__.py +0 -0
- {maxplotlibx-0.1.9 → maxplotlibx-0.2.0}/src/maxplotlib/backends/plotly/utils.py +0 -0
- {maxplotlibx-0.1.9 → maxplotlibx-0.2.0}/src/maxplotlib/canvas/__init__.py +0 -0
- {maxplotlibx-0.1.9 → maxplotlibx-0.2.0}/src/maxplotlib/colors/__init__.py +0 -0
- {maxplotlibx-0.1.9 → maxplotlibx-0.2.0}/src/maxplotlib/colors/colors.py +0 -0
- {maxplotlibx-0.1.9 → maxplotlibx-0.2.0}/src/maxplotlib/linestyle/__init__.py +0 -0
- {maxplotlibx-0.1.9 → maxplotlibx-0.2.0}/src/maxplotlib/linestyle/linestyle.py +0 -0
- {maxplotlibx-0.1.9 → maxplotlibx-0.2.0}/src/maxplotlib/objects/__init__.py +0 -0
- {maxplotlibx-0.1.9 → maxplotlibx-0.2.0}/src/maxplotlib/objects/node.py +0 -0
- {maxplotlibx-0.1.9 → maxplotlibx-0.2.0}/src/maxplotlib/objects/path.py +0 -0
- {maxplotlibx-0.1.9 → maxplotlibx-0.2.0}/src/maxplotlib/subfigure/__init__.py +0 -0
- {maxplotlibx-0.1.9 → maxplotlibx-0.2.0}/src/maxplotlib/subfigure/subfigure.py +0 -0
- {maxplotlibx-0.1.9 → maxplotlibx-0.2.0}/src/maxplotlib/tests/test_imports.py +0 -0
- {maxplotlibx-0.1.9 → maxplotlibx-0.2.0}/src/maxplotlib/tests/test_plot.py +0 -0
- {maxplotlibx-0.1.9 → maxplotlibx-0.2.0}/src/maxplotlib/tests/test_plotext.py +0 -0
- {maxplotlibx-0.1.9 → maxplotlibx-0.2.0}/src/maxplotlib/tests/test_plotly_backend.py +0 -0
- {maxplotlibx-0.1.9 → maxplotlibx-0.2.0}/src/maxplotlib/tests/test_styles_and_colors.py +0 -0
- {maxplotlibx-0.1.9 → maxplotlibx-0.2.0}/src/maxplotlib/utils/__init__.py +0 -0
- {maxplotlibx-0.1.9 → maxplotlibx-0.2.0}/src/maxplotlib/utils/options.py +0 -0
- {maxplotlibx-0.1.9 → maxplotlibx-0.2.0}/src/maxplotlibx.egg-info/dependency_links.txt +0 -0
- {maxplotlibx-0.1.9 → maxplotlibx-0.2.0}/src/maxplotlibx.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: maxplotlibx
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A reproducible plotting module with various backends and export options.
|
|
5
5
|
Author: Max
|
|
6
6
|
Project-URL: Source, https://github.com/max-models/maxplotlib
|
|
@@ -13,11 +13,15 @@ Requires-Dist: matplotlib
|
|
|
13
13
|
Requires-Dist: pint
|
|
14
14
|
Requires-Dist: plotly
|
|
15
15
|
Requires-Dist: plotext<7,>=6.0
|
|
16
|
-
Requires-Dist: tikzfigure[vis]>=0.
|
|
16
|
+
Requires-Dist: tikzfigure[vis]>=0.4.0
|
|
17
17
|
Provides-Extra: test
|
|
18
18
|
Requires-Dist: pytest; extra == "test"
|
|
19
19
|
Requires-Dist: coverage; extra == "test"
|
|
20
|
+
Requires-Dist: xarray; extra == "test"
|
|
21
|
+
Provides-Extra: xarray
|
|
22
|
+
Requires-Dist: xarray; extra == "xarray"
|
|
20
23
|
Provides-Extra: docs
|
|
24
|
+
Requires-Dist: xarray; extra == "docs"
|
|
21
25
|
Requires-Dist: myst-parser; extra == "docs"
|
|
22
26
|
Requires-Dist: sphinx; extra == "docs"
|
|
23
27
|
Requires-Dist: sphinx-rtd-theme; extra == "docs"
|
|
@@ -35,7 +39,7 @@ Requires-Dist: pre-commit; extra == "dev"
|
|
|
35
39
|
Requires-Dist: pyproject-fmt; extra == "dev"
|
|
36
40
|
Dynamic: license-file
|
|
37
41
|
|
|
38
|
-
#
|
|
42
|
+
# Maxplotlib
|
|
39
43
|
|
|
40
44
|
|
|
41
45
|
# Maxplotlib
|
|
@@ -256,23 +260,29 @@ canvas.show(backend="tikzfigure")
|
|
|
256
260
|
|
|
257
261
|

|
|
258
262
|
|
|
259
|
-
###
|
|
263
|
+
### Subplots and Meshes with the TikZ Backend
|
|
260
264
|
|
|
261
|
-
The tikzfigure backend
|
|
262
|
-
|
|
265
|
+
The tikzfigure backend draws the canvas with Matplotlib and converts the
|
|
266
|
+
drawn figure into pgfplots axes, so every layout converts (rows, columns,
|
|
267
|
+
grids, twin axes), with LaTeX text, legends and colorbars. Lines,
|
|
268
|
+
markers, bars and text are pgfplots code; meshes and images are included
|
|
269
|
+
as images:
|
|
263
270
|
|
|
264
271
|
``` python
|
|
265
272
|
x = np.linspace(0, 2 * np.pi, 200)
|
|
266
|
-
canvas, (ax1, ax2) = Canvas.subplots(ncols=2, width="
|
|
273
|
+
canvas, (ax1, ax2) = Canvas.subplots(ncols=2, width="12cm", ratio=0.45)
|
|
267
274
|
|
|
268
|
-
ax1.plot(x, np.sin(x), color="royalblue")
|
|
269
|
-
ax1.
|
|
275
|
+
ax1.plot(x, np.sin(x), color="royalblue", label="$\\sin x$")
|
|
276
|
+
ax1.plot(x, np.cos(x), color="tomato", label="$\\cos x$")
|
|
277
|
+
ax1.set_title("Lines")
|
|
278
|
+
ax1.set_legend(True)
|
|
270
279
|
|
|
271
|
-
|
|
272
|
-
ax2.
|
|
280
|
+
xx, yy = np.meshgrid(x, x)
|
|
281
|
+
ax2.pcolormesh(xx, yy, np.sin(xx) * np.cos(yy), cmap="RdBu_r")
|
|
282
|
+
ax2.add_colorbar(label="$\\sin x \\cos y$")
|
|
283
|
+
ax2.set_title("A mesh")
|
|
273
284
|
|
|
274
|
-
canvas.
|
|
275
|
-
canvas.show(backend="tikzfigure") # Generates LaTeX subfigures
|
|
285
|
+
canvas.show(backend="tikzfigure") # compiles with pdflatex
|
|
276
286
|
```
|
|
277
287
|
|
|
278
288
|
<div id="fig-showcase-subplots">
|
|
@@ -285,9 +295,11 @@ Figure 2
|
|
|
285
295
|
|
|
286
296
|
</div>
|
|
287
297
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
298
|
+
`canvas.render(backend="tikzfigure").savefig("figure.tikz")` writes the
|
|
299
|
+
code for `\\input` in a LaTeX document, with the images next to it. Any
|
|
300
|
+
Matplotlib figure converts the same way with
|
|
301
|
+
`maxplotlib.backends.tikzfigure.figure_to_tikz(fig)`. See the tutorials
|
|
302
|
+
for more examples.
|
|
291
303
|
|
|
292
304
|
### Terminal Backend with plotext
|
|
293
305
|
|
|
@@ -386,3 +398,49 @@ canvas.show()
|
|
|
386
398
|
|
|
387
399
|
(<Figure size 590.551x324.803 with 1 Axes>,
|
|
388
400
|
array([[<Axes: xlabel='x'>]], dtype=object))
|
|
401
|
+
|
|
402
|
+
### xarray data
|
|
403
|
+
|
|
404
|
+
Plot labelled [xarray](https://docs.xarray.dev) data directly
|
|
405
|
+
(`pip install maxplotlibx[xarray]`). Axes come from the coordinates,
|
|
406
|
+
labels from the `long_name` and `units` attributes, and titles from the
|
|
407
|
+
coordinates you selected. `import maxplotlib.xarray` adds a `.maxplot`
|
|
408
|
+
accessor that mirrors xarray’s own `.plot` API and returns an ordinary
|
|
409
|
+
`Canvas`, so the backend is still chosen when rendering:
|
|
410
|
+
|
|
411
|
+
``` python
|
|
412
|
+
import xarray as xr
|
|
413
|
+
|
|
414
|
+
import maxplotlib.xarray # registers da.maxplot and ds.maxplot
|
|
415
|
+
|
|
416
|
+
t = np.linspace(0, 1.5, 6)
|
|
417
|
+
xs = np.linspace(0, 2 * np.pi, 80)
|
|
418
|
+
ys = np.linspace(-1, 1, 50)
|
|
419
|
+
wave = xr.DataArray(
|
|
420
|
+
np.sin(xs - 2 * t[:, None, None]) * np.exp(-3 * ys[None, :, None] ** 2),
|
|
421
|
+
dims=("t", "y", "x"),
|
|
422
|
+
coords={"t": ("t", t, {"units": "s"}), "y": ys, "x": ("x", xs, {"units": "m"})},
|
|
423
|
+
name="phi",
|
|
424
|
+
attrs={"long_name": "Potential", "units": "V"},
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
wave.maxplot.pcolormesh(col="t", col_wrap=3, canvas_kwargs={"width": "16cm", "ratio": 0.6}).show()
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+

|
|
431
|
+
|
|
432
|
+
(<Figure size 944.882x566.929 with 7 Axes>,
|
|
433
|
+
array([[<Axes: title={'center': 't = 0 s'}, ylabel='y'>,
|
|
434
|
+
<Axes: title={'center': 't = 0.3 s'}>,
|
|
435
|
+
<Axes: title={'center': 't = 0.6 s'}>],
|
|
436
|
+
[<Axes: title={'center': 't = 0.9 s'}, xlabel='x [m]', ylabel='y'>,
|
|
437
|
+
<Axes: title={'center': 't = 1.2 s'}, xlabel='x [m]'>,
|
|
438
|
+
<Axes: title={'center': 't = 1.5 s'}, xlabel='x [m]'>]],
|
|
439
|
+
dtype=object))
|
|
440
|
+
|
|
441
|
+
The same works through Canvas methods,
|
|
442
|
+
e.g. `canvas.plot(da, hue="species")`,
|
|
443
|
+
`ax.pcolormesh(da, xcoord="R", ycoord="Z")` for curvilinear grids, or
|
|
444
|
+
`Canvas.facet(da, col="t")`. `ds.maxplot.scatter(x=..., y=..., hue=...)`
|
|
445
|
+
plots one Dataset variable against another. See the [xarray
|
|
446
|
+
tutorial](tutorials/tutorial_17_xarray.ipynb) for more.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Maxplotlib
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
# Maxplotlib
|
|
@@ -219,23 +219,29 @@ canvas.show(backend="tikzfigure")
|
|
|
219
219
|
|
|
220
220
|

|
|
221
221
|
|
|
222
|
-
###
|
|
222
|
+
### Subplots and Meshes with the TikZ Backend
|
|
223
223
|
|
|
224
|
-
The tikzfigure backend
|
|
225
|
-
|
|
224
|
+
The tikzfigure backend draws the canvas with Matplotlib and converts the
|
|
225
|
+
drawn figure into pgfplots axes, so every layout converts (rows, columns,
|
|
226
|
+
grids, twin axes), with LaTeX text, legends and colorbars. Lines,
|
|
227
|
+
markers, bars and text are pgfplots code; meshes and images are included
|
|
228
|
+
as images:
|
|
226
229
|
|
|
227
230
|
``` python
|
|
228
231
|
x = np.linspace(0, 2 * np.pi, 200)
|
|
229
|
-
canvas, (ax1, ax2) = Canvas.subplots(ncols=2, width="
|
|
232
|
+
canvas, (ax1, ax2) = Canvas.subplots(ncols=2, width="12cm", ratio=0.45)
|
|
230
233
|
|
|
231
|
-
ax1.plot(x, np.sin(x), color="royalblue")
|
|
232
|
-
ax1.
|
|
234
|
+
ax1.plot(x, np.sin(x), color="royalblue", label="$\\sin x$")
|
|
235
|
+
ax1.plot(x, np.cos(x), color="tomato", label="$\\cos x$")
|
|
236
|
+
ax1.set_title("Lines")
|
|
237
|
+
ax1.set_legend(True)
|
|
233
238
|
|
|
234
|
-
|
|
235
|
-
ax2.
|
|
239
|
+
xx, yy = np.meshgrid(x, x)
|
|
240
|
+
ax2.pcolormesh(xx, yy, np.sin(xx) * np.cos(yy), cmap="RdBu_r")
|
|
241
|
+
ax2.add_colorbar(label="$\\sin x \\cos y$")
|
|
242
|
+
ax2.set_title("A mesh")
|
|
236
243
|
|
|
237
|
-
canvas.
|
|
238
|
-
canvas.show(backend="tikzfigure") # Generates LaTeX subfigures
|
|
244
|
+
canvas.show(backend="tikzfigure") # compiles with pdflatex
|
|
239
245
|
```
|
|
240
246
|
|
|
241
247
|
<div id="fig-showcase-subplots">
|
|
@@ -248,9 +254,11 @@ Figure 2
|
|
|
248
254
|
|
|
249
255
|
</div>
|
|
250
256
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
257
|
+
`canvas.render(backend="tikzfigure").savefig("figure.tikz")` writes the
|
|
258
|
+
code for `\\input` in a LaTeX document, with the images next to it. Any
|
|
259
|
+
Matplotlib figure converts the same way with
|
|
260
|
+
`maxplotlib.backends.tikzfigure.figure_to_tikz(fig)`. See the tutorials
|
|
261
|
+
for more examples.
|
|
254
262
|
|
|
255
263
|
### Terminal Backend with plotext
|
|
256
264
|
|
|
@@ -349,3 +357,49 @@ canvas.show()
|
|
|
349
357
|
|
|
350
358
|
(<Figure size 590.551x324.803 with 1 Axes>,
|
|
351
359
|
array([[<Axes: xlabel='x'>]], dtype=object))
|
|
360
|
+
|
|
361
|
+
### xarray data
|
|
362
|
+
|
|
363
|
+
Plot labelled [xarray](https://docs.xarray.dev) data directly
|
|
364
|
+
(`pip install maxplotlibx[xarray]`). Axes come from the coordinates,
|
|
365
|
+
labels from the `long_name` and `units` attributes, and titles from the
|
|
366
|
+
coordinates you selected. `import maxplotlib.xarray` adds a `.maxplot`
|
|
367
|
+
accessor that mirrors xarray’s own `.plot` API and returns an ordinary
|
|
368
|
+
`Canvas`, so the backend is still chosen when rendering:
|
|
369
|
+
|
|
370
|
+
``` python
|
|
371
|
+
import xarray as xr
|
|
372
|
+
|
|
373
|
+
import maxplotlib.xarray # registers da.maxplot and ds.maxplot
|
|
374
|
+
|
|
375
|
+
t = np.linspace(0, 1.5, 6)
|
|
376
|
+
xs = np.linspace(0, 2 * np.pi, 80)
|
|
377
|
+
ys = np.linspace(-1, 1, 50)
|
|
378
|
+
wave = xr.DataArray(
|
|
379
|
+
np.sin(xs - 2 * t[:, None, None]) * np.exp(-3 * ys[None, :, None] ** 2),
|
|
380
|
+
dims=("t", "y", "x"),
|
|
381
|
+
coords={"t": ("t", t, {"units": "s"}), "y": ys, "x": ("x", xs, {"units": "m"})},
|
|
382
|
+
name="phi",
|
|
383
|
+
attrs={"long_name": "Potential", "units": "V"},
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
wave.maxplot.pcolormesh(col="t", col_wrap=3, canvas_kwargs={"width": "16cm", "ratio": 0.6}).show()
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+

|
|
390
|
+
|
|
391
|
+
(<Figure size 944.882x566.929 with 7 Axes>,
|
|
392
|
+
array([[<Axes: title={'center': 't = 0 s'}, ylabel='y'>,
|
|
393
|
+
<Axes: title={'center': 't = 0.3 s'}>,
|
|
394
|
+
<Axes: title={'center': 't = 0.6 s'}>],
|
|
395
|
+
[<Axes: title={'center': 't = 0.9 s'}, xlabel='x [m]', ylabel='y'>,
|
|
396
|
+
<Axes: title={'center': 't = 1.2 s'}, xlabel='x [m]'>,
|
|
397
|
+
<Axes: title={'center': 't = 1.5 s'}, xlabel='x [m]'>]],
|
|
398
|
+
dtype=object))
|
|
399
|
+
|
|
400
|
+
The same works through Canvas methods,
|
|
401
|
+
e.g. `canvas.plot(da, hue="species")`,
|
|
402
|
+
`ax.pcolormesh(da, xcoord="R", ycoord="Z")` for curvilinear grids, or
|
|
403
|
+
`Canvas.facet(da, col="t")`. `ds.maxplot.scatter(x=..., y=..., hue=...)`
|
|
404
|
+
plots one Dataset variable against another. See the [xarray
|
|
405
|
+
tutorial](tutorials/tutorial_17_xarray.ipynb) for more.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "maxplotlibx"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "A reproducible plotting module with various backends and export options."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -19,14 +19,19 @@ dependencies = [
|
|
|
19
19
|
"pint",
|
|
20
20
|
"plotly",
|
|
21
21
|
"plotext >= 6.0, < 7",
|
|
22
|
-
"tikzfigure[vis]>=0.
|
|
22
|
+
"tikzfigure[vis]>=0.4.0",
|
|
23
23
|
]
|
|
24
24
|
[project.optional-dependencies]
|
|
25
25
|
test = [
|
|
26
26
|
"pytest",
|
|
27
27
|
"coverage",
|
|
28
|
+
"xarray",
|
|
29
|
+
]
|
|
30
|
+
xarray = [
|
|
31
|
+
"xarray",
|
|
28
32
|
]
|
|
29
33
|
docs = [
|
|
34
|
+
"xarray",
|
|
30
35
|
"myst-parser",
|
|
31
36
|
"sphinx",
|
|
32
37
|
"sphinx-rtd-theme",
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""Detached Matplotlib state used by the object importer.
|
|
2
|
+
|
|
3
|
+
Transforms must be rebound, not frozen in source display coordinates. The
|
|
4
|
+
snapshot memo cuts ownership links before copying and replaces them with tokens;
|
|
5
|
+
each render binds those tokens to its new figure/axes. No source artists are
|
|
6
|
+
reparented, and repeated renders do not share mutable Matplotlib state.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import copy
|
|
10
|
+
from dataclasses import asdict, dataclass, field
|
|
11
|
+
|
|
12
|
+
from matplotlib.artist import Artist
|
|
13
|
+
from matplotlib.cbook import CallbackRegistry
|
|
14
|
+
from matplotlib.collections import Collection
|
|
15
|
+
from matplotlib.image import AxesImage
|
|
16
|
+
from matplotlib.lines import Line2D
|
|
17
|
+
from matplotlib.patches import Patch
|
|
18
|
+
from matplotlib.table import Table
|
|
19
|
+
from matplotlib.text import Text
|
|
20
|
+
from matplotlib.transforms import Bbox, BboxTransformTo
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class ImportDiagnostic:
|
|
25
|
+
artist_id: int
|
|
26
|
+
artist_type: str
|
|
27
|
+
label: str
|
|
28
|
+
severity: str
|
|
29
|
+
message: str
|
|
30
|
+
fallback: str | None = None
|
|
31
|
+
backends: tuple = ("matplotlib",)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class ImportReport:
|
|
36
|
+
"""Import decisions, including losses and backend-specific representations."""
|
|
37
|
+
|
|
38
|
+
diagnostics: list = field(default_factory=list)
|
|
39
|
+
|
|
40
|
+
def add(
|
|
41
|
+
self,
|
|
42
|
+
artist,
|
|
43
|
+
message,
|
|
44
|
+
*,
|
|
45
|
+
severity="info",
|
|
46
|
+
fallback=None,
|
|
47
|
+
backends=("matplotlib",),
|
|
48
|
+
):
|
|
49
|
+
item = ImportDiagnostic(
|
|
50
|
+
id(artist),
|
|
51
|
+
type(artist).__name__,
|
|
52
|
+
str(getattr(artist, "get_label", lambda: "")()),
|
|
53
|
+
severity,
|
|
54
|
+
message,
|
|
55
|
+
fallback,
|
|
56
|
+
backends,
|
|
57
|
+
)
|
|
58
|
+
self.diagnostics.append(item)
|
|
59
|
+
return item
|
|
60
|
+
|
|
61
|
+
def to_dict(self):
|
|
62
|
+
return asdict(self)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def root_figure(figure):
|
|
66
|
+
while getattr(figure, "figure", figure) is not figure:
|
|
67
|
+
figure = figure.figure
|
|
68
|
+
return figure
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def figure_bounds(ax, figure):
|
|
72
|
+
return tuple(
|
|
73
|
+
ax.get_position(original=True)
|
|
74
|
+
.transformed(ax.figure.transSubfigure)
|
|
75
|
+
.transformed(figure.transFigure.inverted())
|
|
76
|
+
.bounds
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _bindings(ax, fig):
|
|
81
|
+
bindings = {
|
|
82
|
+
"figure": fig,
|
|
83
|
+
"transFigure": fig.transFigure,
|
|
84
|
+
"transSubfigure": fig.transSubfigure,
|
|
85
|
+
"dpi_scale_trans": fig.dpi_scale_trans,
|
|
86
|
+
"fig_bbox": fig.bbox,
|
|
87
|
+
}
|
|
88
|
+
if ax is not None:
|
|
89
|
+
bindings.update(
|
|
90
|
+
axes=ax,
|
|
91
|
+
transData=ax.transData,
|
|
92
|
+
transAxes=ax.transAxes,
|
|
93
|
+
transScale=ax.transScale,
|
|
94
|
+
transLimits=ax.transLimits,
|
|
95
|
+
ax_bbox=ax.bbox,
|
|
96
|
+
xaxis=ax.xaxis,
|
|
97
|
+
yaxis=ax.yaxis,
|
|
98
|
+
xaxis_transform=ax.get_xaxis_transform(),
|
|
99
|
+
yaxis_transform=ax.get_yaxis_transform(),
|
|
100
|
+
)
|
|
101
|
+
return bindings
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class _BindingToken:
|
|
105
|
+
"""Weak-referenceable marker for copied Transform parent links."""
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class ReboundSnapshot:
|
|
109
|
+
"""Copy a payload without retaining its owning axes, figure or callbacks."""
|
|
110
|
+
|
|
111
|
+
def __init__(self, payload, ax=None, fig=None):
|
|
112
|
+
owner = fig if fig is not None else ax.figure
|
|
113
|
+
fig = root_figure(owner)
|
|
114
|
+
self.owner_bounds = None
|
|
115
|
+
self.tokens = {}
|
|
116
|
+
memo = {}
|
|
117
|
+
bindings = _bindings(ax, fig)
|
|
118
|
+
if owner is not fig:
|
|
119
|
+
self.owner_bounds = tuple(
|
|
120
|
+
owner.bbox.transformed(fig.transFigure.inverted()).bounds
|
|
121
|
+
)
|
|
122
|
+
bindings.update(owner=owner, owner_transform=owner.transSubfigure)
|
|
123
|
+
for name, value in bindings.items():
|
|
124
|
+
# Some canonical transforms are aliases of one another.
|
|
125
|
+
if id(value) not in memo:
|
|
126
|
+
self.tokens[name] = memo[id(value)] = _BindingToken()
|
|
127
|
+
artists = []
|
|
128
|
+
if isinstance(payload, Artist):
|
|
129
|
+
artists = payload.findobj()
|
|
130
|
+
for artist in artists:
|
|
131
|
+
for name in ("_remove_method", "stale_callback"):
|
|
132
|
+
value = getattr(artist, name, None)
|
|
133
|
+
if value is not None:
|
|
134
|
+
memo[id(value)] = None
|
|
135
|
+
callbacks = getattr(artist, "_callbacks", None)
|
|
136
|
+
if callbacks is not None:
|
|
137
|
+
memo[id(callbacks)] = CallbackRegistry()
|
|
138
|
+
self.payload = copy.deepcopy(payload, memo)
|
|
139
|
+
|
|
140
|
+
def clone(self, ax=None, fig=None):
|
|
141
|
+
fig = fig if fig is not None else ax.figure
|
|
142
|
+
bindings = _bindings(ax, fig)
|
|
143
|
+
if self.owner_bounds is not None:
|
|
144
|
+
bindings.update(
|
|
145
|
+
owner=fig,
|
|
146
|
+
owner_transform=BboxTransformTo(Bbox.from_bounds(*self.owner_bounds))
|
|
147
|
+
+ fig.transFigure,
|
|
148
|
+
)
|
|
149
|
+
return copy.deepcopy(
|
|
150
|
+
self.payload,
|
|
151
|
+
{id(token): bindings[name] for name, token in self.tokens.items()},
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def draw(self, ax, **overrides):
|
|
155
|
+
artist = self.clone(ax)
|
|
156
|
+
artist.set(**overrides)
|
|
157
|
+
clipbox, clippath = artist.get_clip_box(), artist.get_clip_path()
|
|
158
|
+
# add_* establishes the new removal and stale callbacks.
|
|
159
|
+
if isinstance(artist, Collection):
|
|
160
|
+
ax.add_collection(artist, autolim=False)
|
|
161
|
+
elif isinstance(artist, Line2D):
|
|
162
|
+
ax.add_line(artist)
|
|
163
|
+
elif isinstance(artist, Patch):
|
|
164
|
+
ax.add_patch(artist)
|
|
165
|
+
elif isinstance(artist, AxesImage):
|
|
166
|
+
ax.add_image(artist)
|
|
167
|
+
elif isinstance(artist, Table):
|
|
168
|
+
ax.add_table(artist)
|
|
169
|
+
elif isinstance(artist, Text):
|
|
170
|
+
ax._add_text(artist)
|
|
171
|
+
else:
|
|
172
|
+
ax.add_artist(artist)
|
|
173
|
+
artist.set_clip_path(clippath)
|
|
174
|
+
artist.set_clip_box(clipbox)
|
|
175
|
+
return artist
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def capture_axis_state(ax):
|
|
179
|
+
"""Capture decorations which have no backend-neutral equivalent yet."""
|
|
180
|
+
state = {"axes": {}, "spines": {}, "titles": {}}
|
|
181
|
+
for name in ("x", "y"):
|
|
182
|
+
axis = getattr(ax, name + "axis")
|
|
183
|
+
state["axes"][name] = dict(
|
|
184
|
+
scale=ReboundSnapshot(axis._scale, ax),
|
|
185
|
+
major_locator=ReboundSnapshot(axis.get_major_locator(), ax),
|
|
186
|
+
minor_locator=ReboundSnapshot(axis.get_minor_locator(), ax),
|
|
187
|
+
major_formatter=ReboundSnapshot(axis.get_major_formatter(), ax),
|
|
188
|
+
minor_formatter=ReboundSnapshot(axis.get_minor_formatter(), ax),
|
|
189
|
+
units=copy.deepcopy(axis.get_units()),
|
|
190
|
+
converter=copy.deepcopy(
|
|
191
|
+
axis.get_converter()
|
|
192
|
+
if hasattr(axis, "get_converter")
|
|
193
|
+
else axis.converter
|
|
194
|
+
),
|
|
195
|
+
major_kw=_tick_params(axis, "major"),
|
|
196
|
+
minor_kw=_tick_params(axis, "minor"),
|
|
197
|
+
ticks={
|
|
198
|
+
which: [
|
|
199
|
+
_tick_style(tick)
|
|
200
|
+
for tick in getattr(axis, "get_" + which + "_ticks")()
|
|
201
|
+
]
|
|
202
|
+
for which in ("major", "minor")
|
|
203
|
+
},
|
|
204
|
+
label_position=axis.get_label_position(),
|
|
205
|
+
offset_style=_text_properties(axis.get_offset_text()),
|
|
206
|
+
)
|
|
207
|
+
for name, spine in ax.spines.items():
|
|
208
|
+
state["spines"][name] = dict(
|
|
209
|
+
visible=spine.get_visible(),
|
|
210
|
+
edgecolor=spine.get_edgecolor(),
|
|
211
|
+
linewidth=spine.get_linewidth(),
|
|
212
|
+
linestyle=spine.get_linestyle(),
|
|
213
|
+
bounds=spine.get_bounds(),
|
|
214
|
+
)
|
|
215
|
+
if spine.spine_type in ("left", "right", "top", "bottom"):
|
|
216
|
+
state["spines"][name]["position"] = copy.deepcopy(spine.get_position())
|
|
217
|
+
for loc, title in (
|
|
218
|
+
("left", ax._left_title),
|
|
219
|
+
("center", ax.title),
|
|
220
|
+
("right", ax._right_title),
|
|
221
|
+
):
|
|
222
|
+
state["titles"][loc] = (
|
|
223
|
+
title.get_text(),
|
|
224
|
+
_text_properties(title),
|
|
225
|
+
title.get_position(),
|
|
226
|
+
)
|
|
227
|
+
state["autotitlepos"] = ax._autotitlepos
|
|
228
|
+
state["label_coords"] = {
|
|
229
|
+
name: (
|
|
230
|
+
axis.label.get_position(),
|
|
231
|
+
ReboundSnapshot(axis.label.get_transform(), ax),
|
|
232
|
+
)
|
|
233
|
+
for name, axis in (("x", ax.xaxis), ("y", ax.yaxis))
|
|
234
|
+
if not axis._autolabelpos
|
|
235
|
+
}
|
|
236
|
+
return state
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _text_properties(text):
|
|
240
|
+
return dict(
|
|
241
|
+
fontproperties=copy.deepcopy(text.get_fontproperties()),
|
|
242
|
+
color=text.get_color(),
|
|
243
|
+
rotation=text.get_rotation(),
|
|
244
|
+
rotation_mode=text.get_rotation_mode(),
|
|
245
|
+
horizontalalignment=text.get_ha(),
|
|
246
|
+
verticalalignment=text.get_va(),
|
|
247
|
+
visible=text.get_visible(),
|
|
248
|
+
usetex=text.get_usetex(),
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _tick_params(axis, which):
|
|
253
|
+
tick = getattr(axis, "get_" + which + "_ticks")(1)[0]
|
|
254
|
+
defaults = dict(
|
|
255
|
+
length=tick._size,
|
|
256
|
+
width=tick._width,
|
|
257
|
+
pad=tick._base_pad,
|
|
258
|
+
direction=tick._tickdir,
|
|
259
|
+
color=tick.tick1line.get_color(),
|
|
260
|
+
)
|
|
261
|
+
defaults.update(copy.deepcopy(getattr(axis, "_" + which + "_tick_kw")))
|
|
262
|
+
return defaults
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _tick_line_style(line):
|
|
266
|
+
return {
|
|
267
|
+
name: getattr(line, "get_" + name)()
|
|
268
|
+
for name in (
|
|
269
|
+
"color",
|
|
270
|
+
"marker",
|
|
271
|
+
"markersize",
|
|
272
|
+
"markeredgewidth",
|
|
273
|
+
"markeredgecolor",
|
|
274
|
+
"visible",
|
|
275
|
+
"zorder",
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _tick_style(tick):
|
|
281
|
+
return dict(
|
|
282
|
+
tick1line=_tick_line_style(tick.tick1line),
|
|
283
|
+
tick2line=_tick_line_style(tick.tick2line),
|
|
284
|
+
label1=_text_properties(tick.label1),
|
|
285
|
+
label2=_text_properties(tick.label2),
|
|
286
|
+
gridline=dict(
|
|
287
|
+
visible=tick.gridline.get_visible(),
|
|
288
|
+
color=tick.gridline.get_color(),
|
|
289
|
+
linewidth=tick.gridline.get_linewidth(),
|
|
290
|
+
linestyle=tick.gridline.get_linestyle(),
|
|
291
|
+
alpha=tick.gridline.get_alpha(),
|
|
292
|
+
),
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def apply_axis_state(ax, state, *, units=True):
|
|
297
|
+
for name, props in state["spines"].items():
|
|
298
|
+
props = copy.deepcopy(props)
|
|
299
|
+
bounds = props.pop("bounds")
|
|
300
|
+
ax.spines[name].set(**props)
|
|
301
|
+
if bounds is not None:
|
|
302
|
+
ax.spines[name].set_bounds(*bounds)
|
|
303
|
+
for name, settings in state["axes"].items():
|
|
304
|
+
axis = getattr(ax, name + "axis")
|
|
305
|
+
# Scales are installed before locators/formatters: changing a scale
|
|
306
|
+
# installs defaults and would otherwise erase the imported tick setup.
|
|
307
|
+
getattr(ax, "set_" + name + "scale")(settings["scale"].clone(ax))
|
|
308
|
+
if units:
|
|
309
|
+
converter = copy.deepcopy(settings["converter"])
|
|
310
|
+
if hasattr(axis, "set_converter"):
|
|
311
|
+
if not getattr(axis, "_converter_is_explicit", False):
|
|
312
|
+
axis.set_converter(converter)
|
|
313
|
+
else:
|
|
314
|
+
axis.converter = converter
|
|
315
|
+
axis.set_units(copy.deepcopy(settings["units"]))
|
|
316
|
+
for kind in ("major", "minor"):
|
|
317
|
+
getattr(axis, "set_" + kind + "_locator")(
|
|
318
|
+
settings[kind + "_locator"].clone(ax)
|
|
319
|
+
)
|
|
320
|
+
getattr(axis, "set_" + kind + "_formatter")(
|
|
321
|
+
settings[kind + "_formatter"].clone(ax)
|
|
322
|
+
)
|
|
323
|
+
axis.set_tick_params(which=kind, **copy.deepcopy(settings[kind + "_kw"]))
|
|
324
|
+
ticks = getattr(axis, "get_" + kind + "_ticks")()
|
|
325
|
+
styles = settings["ticks"][kind]
|
|
326
|
+
for i, tick in enumerate(ticks):
|
|
327
|
+
if styles:
|
|
328
|
+
style = styles[min(i, len(styles) - 1)]
|
|
329
|
+
for part, props in style.items():
|
|
330
|
+
getattr(tick, part).set(**props)
|
|
331
|
+
axis.set_label_position(settings["label_position"])
|
|
332
|
+
axis.get_offset_text().set(**settings["offset_style"])
|
|
333
|
+
for loc, (text, props, position) in (
|
|
334
|
+
state["titles"].items() if hasattr(ax, "set_title") else ()
|
|
335
|
+
):
|
|
336
|
+
title = ax.set_title(text, loc=loc, **props)
|
|
337
|
+
title.set_position(position)
|
|
338
|
+
ax._autotitlepos = state["autotitlepos"]
|
|
339
|
+
for name, (position, transform) in state["label_coords"].items():
|
|
340
|
+
getattr(ax, name + "axis").set_label_coords(
|
|
341
|
+
*position, transform=transform.clone(ax)
|
|
342
|
+
)
|