maxplotlibx 0.1.9__py3-none-any.whl → 0.2.0__py3-none-any.whl
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.
- maxplotlib/backends/matplotlib/import_state.py +342 -0
- maxplotlib/backends/matplotlib/importer.py +1078 -0
- maxplotlib/backends/tikzfigure/__init__.py +9 -0
- maxplotlib/backends/tikzfigure/convert.py +1331 -0
- maxplotlib/backends/tikzfigure/text.py +183 -0
- maxplotlib/canvas/canvas.py +492 -341
- maxplotlib/subfigure/line_plot.py +481 -439
- maxplotlib/tests/test_canvas.py +36 -21
- maxplotlib/tests/test_flame_chart.py +9 -0
- maxplotlib/tests/test_gantt_chart.py +9 -0
- maxplotlib/tests/test_matplotlib_import.py +348 -0
- maxplotlib/tests/test_matplotlib_import_extended.py +612 -0
- maxplotlib/tests/test_tikzfigure_backend.py +259 -0
- maxplotlib/tests/test_xarray.py +896 -0
- maxplotlib/utils/xarray_support.py +346 -0
- maxplotlib/xarray.py +170 -0
- {maxplotlibx-0.1.9.dist-info → maxplotlibx-0.2.0.dist-info}/METADATA +74 -16
- {maxplotlibx-0.1.9.dist-info → maxplotlibx-0.2.0.dist-info}/RECORD +21 -10
- {maxplotlibx-0.1.9.dist-info → maxplotlibx-0.2.0.dist-info}/WHEEL +0 -0
- {maxplotlibx-0.1.9.dist-info → maxplotlibx-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {maxplotlibx-0.1.9.dist-info → maxplotlibx-0.2.0.dist-info}/top_level.txt +0 -0
|
@@ -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
|
+
)
|