bfplot 1.0.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.
- bfplot/__init__.py +38 -0
- bfplot/adapters/__init__.py +1 -0
- bfplot/adapters/matplotlib.py +543 -0
- bfplot/api.py +196 -0
- bfplot/artifact.py +302 -0
- bfplot/bfir.py +263 -0
- bfplot/bfir.schema.json +110 -0
- bfplot/preview.py +553 -0
- bfplot/py.typed +0 -0
- bfplot/result.py +231 -0
- bfplot/temporal.py +212 -0
- bfplot-1.0.0.dist-info/METADATA +220 -0
- bfplot-1.0.0.dist-info/RECORD +15 -0
- bfplot-1.0.0.dist-info/WHEEL +4 -0
- bfplot-1.0.0.dist-info/licenses/LICENSE +21 -0
bfplot/__init__.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Script-first, content-addressed interactive scientific plots."""
|
|
2
|
+
|
|
3
|
+
from .api import (
|
|
4
|
+
Animation,
|
|
5
|
+
Result,
|
|
6
|
+
ResultError,
|
|
7
|
+
animation,
|
|
8
|
+
compile,
|
|
9
|
+
hash,
|
|
10
|
+
result,
|
|
11
|
+
same,
|
|
12
|
+
save_animation,
|
|
13
|
+
savefig,
|
|
14
|
+
show,
|
|
15
|
+
translate,
|
|
16
|
+
verify,
|
|
17
|
+
)
|
|
18
|
+
from .bfir import BfirBuffer, BfirDocument, BfirValidationError
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"Animation",
|
|
22
|
+
"BfirBuffer",
|
|
23
|
+
"BfirDocument",
|
|
24
|
+
"BfirValidationError",
|
|
25
|
+
"Result",
|
|
26
|
+
"ResultError",
|
|
27
|
+
"animation",
|
|
28
|
+
"compile",
|
|
29
|
+
"hash",
|
|
30
|
+
"result",
|
|
31
|
+
"same",
|
|
32
|
+
"save_animation",
|
|
33
|
+
"savefig",
|
|
34
|
+
"show",
|
|
35
|
+
"translate",
|
|
36
|
+
"verify",
|
|
37
|
+
]
|
|
38
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Source-library adapters that translate plots into storage-independent BFIR."""
|
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import matplotlib.dates as mdates
|
|
8
|
+
import numpy as np
|
|
9
|
+
from matplotlib import colors
|
|
10
|
+
from matplotlib.artist import Artist
|
|
11
|
+
from matplotlib.collections import LineCollection, PathCollection, PolyCollection
|
|
12
|
+
from matplotlib.container import BarContainer, ErrorbarContainer
|
|
13
|
+
from matplotlib.image import AxesImage
|
|
14
|
+
from matplotlib.lines import Line2D
|
|
15
|
+
from matplotlib.patches import Patch, Polygon
|
|
16
|
+
from matplotlib.spines import Spine
|
|
17
|
+
from matplotlib.text import Text
|
|
18
|
+
|
|
19
|
+
from ..bfir import BfirBuilder, BfirDocument
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _finite(value: Any) -> Any:
|
|
23
|
+
if isinstance(value, np.generic):
|
|
24
|
+
value = value.item()
|
|
25
|
+
if isinstance(value, float):
|
|
26
|
+
return value if math.isfinite(value) else None
|
|
27
|
+
if isinstance(value, (str, int, bool)) or value is None:
|
|
28
|
+
return value
|
|
29
|
+
if isinstance(value, dict):
|
|
30
|
+
return {str(key): _finite(item) for key, item in value.items()}
|
|
31
|
+
if isinstance(value, (list, tuple, np.ndarray)):
|
|
32
|
+
return [_finite(item) for item in value]
|
|
33
|
+
return str(value)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _color(value: Any) -> list[float] | None:
|
|
37
|
+
try:
|
|
38
|
+
return [float(item) for item in colors.to_rgba(value)]
|
|
39
|
+
except (TypeError, ValueError):
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _common(artist: Artist) -> dict[str, Any]:
|
|
44
|
+
label = artist.get_label() if hasattr(artist, "get_label") else None
|
|
45
|
+
return {
|
|
46
|
+
"visible": bool(artist.get_visible()),
|
|
47
|
+
"alpha": _finite(artist.get_alpha()),
|
|
48
|
+
"zorder": _finite(artist.get_zorder()),
|
|
49
|
+
"label": label if isinstance(label, str) and not label.startswith("_") else None,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class ExtractionContext:
|
|
55
|
+
builder: BfirBuilder
|
|
56
|
+
next_artist: int = 0
|
|
57
|
+
artist_ids: dict[int, str] = field(default_factory=dict)
|
|
58
|
+
|
|
59
|
+
def array(self, value: Any) -> dict[str, str]:
|
|
60
|
+
return self.builder.buffer(value)
|
|
61
|
+
|
|
62
|
+
def artist_id(self, source: Any = None) -> str:
|
|
63
|
+
"""A fresh artist ID, or the one already given to ``source`` (e.g. a colorbar mappable)."""
|
|
64
|
+
if source is not None and id(source) in self.artist_ids:
|
|
65
|
+
return self.artist_ids[id(source)]
|
|
66
|
+
artist_id = f"artist-{self.next_artist}"
|
|
67
|
+
self.next_artist += 1
|
|
68
|
+
if source is not None:
|
|
69
|
+
self.artist_ids[id(source)] = artist_id
|
|
70
|
+
return artist_id
|
|
71
|
+
|
|
72
|
+
def artist(
|
|
73
|
+
self,
|
|
74
|
+
type_name: str,
|
|
75
|
+
axes_id: str,
|
|
76
|
+
props: dict[str, Any],
|
|
77
|
+
data: dict[str, Any],
|
|
78
|
+
source: Any | None = None,
|
|
79
|
+
) -> dict[str, Any]:
|
|
80
|
+
return {
|
|
81
|
+
"type": type_name,
|
|
82
|
+
"id": self.artist_id(source),
|
|
83
|
+
"axes_id": axes_id,
|
|
84
|
+
"props": _finite(props),
|
|
85
|
+
"data": data,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def translate_figures(
|
|
90
|
+
figures: list[Any],
|
|
91
|
+
*,
|
|
92
|
+
names: list[str | None] | None = None,
|
|
93
|
+
first_figure: int = 0,
|
|
94
|
+
first_artist: int = 0,
|
|
95
|
+
) -> tuple[BfirDocument, int]:
|
|
96
|
+
"""Translate Matplotlib figures into BFIR and return the next unused artist number.
|
|
97
|
+
|
|
98
|
+
``first_figure`` and ``first_artist`` offset generated IDs so separately translated
|
|
99
|
+
groups can be combined into one document without collisions.
|
|
100
|
+
"""
|
|
101
|
+
names = names or [None] * len(figures)
|
|
102
|
+
builder = BfirBuilder()
|
|
103
|
+
context = ExtractionContext(builder, next_artist=first_artist)
|
|
104
|
+
records = []
|
|
105
|
+
for index, (figure, name) in enumerate(zip(figures, names, strict=True)):
|
|
106
|
+
record = _extract_figure(figure, f"figure-{first_figure + index}", context)
|
|
107
|
+
if name is not None:
|
|
108
|
+
record["name"] = name
|
|
109
|
+
records.append(record)
|
|
110
|
+
return builder.build({"figures": records}), context.next_artist
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _extract_figure(figure: Any, figure_id: str, context: ExtractionContext) -> dict[str, Any]:
|
|
114
|
+
width, height = figure.get_size_inches()
|
|
115
|
+
result = {
|
|
116
|
+
"id": figure_id,
|
|
117
|
+
"size_inches": [float(width), float(height)],
|
|
118
|
+
"dpi": float(figure.dpi),
|
|
119
|
+
"facecolor": _color(figure.get_facecolor()),
|
|
120
|
+
"axes": [],
|
|
121
|
+
}
|
|
122
|
+
suptitle = getattr(figure, "_suptitle", None)
|
|
123
|
+
if suptitle is not None and suptitle.get_text():
|
|
124
|
+
result["title"] = suptitle.get_text()
|
|
125
|
+
axes_entries: list[tuple[Any, Any | None]] = []
|
|
126
|
+
for axes in figure.axes:
|
|
127
|
+
axes_entries.append((axes, None))
|
|
128
|
+
axes_entries.extend((child, axes) for child in getattr(axes, "child_axes", []))
|
|
129
|
+
subplot_axes = {id(axes) for axes, _ in axes_entries if _has_subplotspec(axes)}
|
|
130
|
+
axes_ids = {
|
|
131
|
+
id(axes): f"{figure_id}-axes-{index}" for index, (axes, _) in enumerate(axes_entries)
|
|
132
|
+
}
|
|
133
|
+
for index, (axes, explicit_parent) in enumerate(axes_entries):
|
|
134
|
+
extracted_axes = _extract_axes(axes, axes_ids[id(axes)], context)
|
|
135
|
+
if explicit_parent is not None and extracted_axes.get("role") != "colorbar":
|
|
136
|
+
extracted_axes["role"] = "inset"
|
|
137
|
+
extracted_axes["parent_axes_id"] = axes_ids[id(explicit_parent)]
|
|
138
|
+
elif id(axes) not in subplot_axes and extracted_axes.get("role") != "colorbar":
|
|
139
|
+
parent = _inset_parent(axes, [item for item, _ in axes_entries], subplot_axes)
|
|
140
|
+
if parent is not None:
|
|
141
|
+
extracted_axes["role"] = "inset"
|
|
142
|
+
extracted_axes["parent_axes_id"] = axes_ids[id(parent)]
|
|
143
|
+
result["axes"].append(extracted_axes)
|
|
144
|
+
return result
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _has_subplotspec(axes: Any) -> bool:
|
|
148
|
+
try:
|
|
149
|
+
return axes.get_subplotspec() is not None
|
|
150
|
+
except (AttributeError, TypeError):
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _inset_parent(axes: Any, candidates: list[Any], subplot_axes: set[int]) -> Any | None:
|
|
155
|
+
left, bottom, width, height = axes.get_position().bounds
|
|
156
|
+
center = (left + width / 2, bottom + height / 2)
|
|
157
|
+
parents = []
|
|
158
|
+
for candidate in candidates:
|
|
159
|
+
if id(candidate) not in subplot_axes:
|
|
160
|
+
continue
|
|
161
|
+
candidate_left, candidate_bottom, candidate_width, candidate_height = (
|
|
162
|
+
candidate.get_position().bounds
|
|
163
|
+
)
|
|
164
|
+
if (
|
|
165
|
+
candidate_left <= center[0] <= candidate_left + candidate_width
|
|
166
|
+
and candidate_bottom <= center[1] <= candidate_bottom + candidate_height
|
|
167
|
+
):
|
|
168
|
+
overlap_width = max(
|
|
169
|
+
0.0, min(left + width, candidate_left + candidate_width) - max(left, candidate_left)
|
|
170
|
+
)
|
|
171
|
+
overlap_height = max(
|
|
172
|
+
0.0,
|
|
173
|
+
min(bottom + height, candidate_bottom + candidate_height)
|
|
174
|
+
- max(bottom, candidate_bottom),
|
|
175
|
+
)
|
|
176
|
+
parents.append((overlap_width * overlap_height, candidate))
|
|
177
|
+
return max(parents, key=lambda item: item[0])[1] if parents else None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _extract_axes(axes: Any, axes_id: str, context: ExtractionContext) -> dict[str, Any]:
|
|
181
|
+
is_3d = getattr(axes, "name", "") == "3d"
|
|
182
|
+
result: dict[str, Any] = {
|
|
183
|
+
"id": axes_id,
|
|
184
|
+
"projection": "3d" if is_3d else getattr(axes, "name", "rectilinear"),
|
|
185
|
+
"bounds": [float(item) for item in axes.get_position().bounds],
|
|
186
|
+
"title": axes.get_title(),
|
|
187
|
+
"x": _axis(axes, "x"),
|
|
188
|
+
"y": _axis(axes, "y"),
|
|
189
|
+
"facecolor": _color(axes.get_facecolor()),
|
|
190
|
+
"artists": [],
|
|
191
|
+
}
|
|
192
|
+
colorbar = getattr(axes, "_colorbar", None)
|
|
193
|
+
if axes.get_label() == "<colorbar>" or colorbar is not None:
|
|
194
|
+
result["role"] = "colorbar"
|
|
195
|
+
if colorbar is not None:
|
|
196
|
+
mappable = colorbar.mappable
|
|
197
|
+
lower, upper = mappable.get_clim()
|
|
198
|
+
result["colorbar"] = {
|
|
199
|
+
"orientation": colorbar.orientation,
|
|
200
|
+
"label": colorbar.ax.get_ylabel()
|
|
201
|
+
if colorbar.orientation == "vertical"
|
|
202
|
+
else colorbar.ax.get_xlabel(),
|
|
203
|
+
"limits": [_finite(lower), _finite(upper)],
|
|
204
|
+
"ticks": [_finite(item) for item in colorbar.get_ticks()],
|
|
205
|
+
"ticklabels": [item.get_text() for item in colorbar.ax.get_yticklabels()]
|
|
206
|
+
if colorbar.orientation == "vertical"
|
|
207
|
+
else [item.get_text() for item in colorbar.ax.get_xticklabels()],
|
|
208
|
+
"cmap": mappable.cmap.name,
|
|
209
|
+
"colors": [_color(mappable.cmap(value)) for value in np.linspace(0, 1, 64)],
|
|
210
|
+
"mappable": context.artist_id(mappable),
|
|
211
|
+
}
|
|
212
|
+
return result
|
|
213
|
+
if is_3d:
|
|
214
|
+
result["z"] = _axis(axes, "z")
|
|
215
|
+
result["view"] = {
|
|
216
|
+
"elev": float(axes.elev),
|
|
217
|
+
"azim": float(axes.azim),
|
|
218
|
+
"roll": float(getattr(axes, "roll", 0)),
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
consumed: set[int] = set()
|
|
222
|
+
axes_text = {
|
|
223
|
+
id(item)
|
|
224
|
+
for item in (
|
|
225
|
+
axes.title,
|
|
226
|
+
getattr(axes, "_left_title", None),
|
|
227
|
+
getattr(axes, "_right_title", None),
|
|
228
|
+
axes.xaxis.label,
|
|
229
|
+
axes.yaxis.label,
|
|
230
|
+
getattr(getattr(axes, "zaxis", None), "label", None),
|
|
231
|
+
)
|
|
232
|
+
if item is not None
|
|
233
|
+
}
|
|
234
|
+
for container in axes.containers:
|
|
235
|
+
extracted, children = _extract_container(container, axes_id, context)
|
|
236
|
+
if extracted:
|
|
237
|
+
result["artists"].append(extracted)
|
|
238
|
+
consumed.update(id(child) for child in children if child is not None)
|
|
239
|
+
|
|
240
|
+
for image in axes.images:
|
|
241
|
+
if id(image) not in consumed:
|
|
242
|
+
result["artists"].append(_extract_image(image, axes_id, context))
|
|
243
|
+
consumed.add(id(image))
|
|
244
|
+
|
|
245
|
+
for artist in axes.get_children():
|
|
246
|
+
if (
|
|
247
|
+
id(artist) in consumed
|
|
248
|
+
or id(artist) in axes_text
|
|
249
|
+
or not artist.get_visible()
|
|
250
|
+
or artist is axes.patch
|
|
251
|
+
or isinstance(artist, Spine)
|
|
252
|
+
):
|
|
253
|
+
continue
|
|
254
|
+
extracted = _extract_artist(artist, axes_id, context, is_3d)
|
|
255
|
+
if extracted is not None:
|
|
256
|
+
result["artists"].append(extracted)
|
|
257
|
+
return result
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _axis(axes: Any, name: str) -> dict[str, Any]:
|
|
261
|
+
axis = getattr(axes, f"{name}axis")
|
|
262
|
+
lower, upper = getattr(axes, f"get_{name}lim")()
|
|
263
|
+
converter = (
|
|
264
|
+
axis.get_converter()
|
|
265
|
+
if hasattr(axis, "get_converter")
|
|
266
|
+
else getattr(axis, "_converter", None)
|
|
267
|
+
)
|
|
268
|
+
is_date = isinstance(converter, mdates.DateConverter) or isinstance(
|
|
269
|
+
axis.get_major_locator(), mdates.DateLocator
|
|
270
|
+
)
|
|
271
|
+
formatter = axis.get_major_formatter()
|
|
272
|
+
result = {
|
|
273
|
+
"label": axis.get_label_text(),
|
|
274
|
+
"scale": "date" if is_date else getattr(axes, f"get_{name}scale")(),
|
|
275
|
+
"limits": [_finite(lower), _finite(upper)],
|
|
276
|
+
"inverted": bool(getattr(axes, f"{name}axis_inverted")()),
|
|
277
|
+
"ticks": [_finite(item) for item in axis.get_ticklocs()],
|
|
278
|
+
"ticklabels": [item.get_text() for item in axis.get_ticklabels()],
|
|
279
|
+
}
|
|
280
|
+
if is_date:
|
|
281
|
+
result["date_epoch"] = "matplotlib"
|
|
282
|
+
result["date_format"] = getattr(formatter, "fmt", None) or "%b %d"
|
|
283
|
+
return result
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _extract_container(
|
|
287
|
+
container: Any, axes_id: str, context: ExtractionContext
|
|
288
|
+
) -> tuple[dict[str, Any] | None, list[Any]]:
|
|
289
|
+
if isinstance(container, ErrorbarContainer):
|
|
290
|
+
data_line, caplines, barcols = container.lines
|
|
291
|
+
if data_line is not None:
|
|
292
|
+
x, y = data_line.get_data()
|
|
293
|
+
props = _line_props(data_line)
|
|
294
|
+
else:
|
|
295
|
+
x, y, props = [], [], {}
|
|
296
|
+
segments = [segment for collection in barcols for segment in collection.get_segments()]
|
|
297
|
+
cap_segments = [np.column_stack(cap.get_data()) for cap in caplines]
|
|
298
|
+
data = {"x": context.array(x), "y": context.array(y)}
|
|
299
|
+
if segments:
|
|
300
|
+
data["error_segments"] = context.array(np.asarray(segments))
|
|
301
|
+
if cap_segments:
|
|
302
|
+
data["cap_segments"] = context.array(np.asarray(cap_segments))
|
|
303
|
+
children = [data_line, *caplines, *barcols]
|
|
304
|
+
return context.artist("bfplot.errorbar", axes_id, props, data), children
|
|
305
|
+
if isinstance(container, BarContainer):
|
|
306
|
+
patches = list(container.patches)
|
|
307
|
+
if not patches:
|
|
308
|
+
return None, []
|
|
309
|
+
data = {
|
|
310
|
+
"left": context.array([patch.get_x() for patch in patches]),
|
|
311
|
+
"bottom": context.array([patch.get_y() for patch in patches]),
|
|
312
|
+
"width": context.array([patch.get_width() for patch in patches]),
|
|
313
|
+
"height": context.array([patch.get_height() for patch in patches]),
|
|
314
|
+
}
|
|
315
|
+
props = _patch_props(patches[0])
|
|
316
|
+
props["orientation"] = getattr(container, "orientation", None)
|
|
317
|
+
return context.artist("bfplot.histogram", axes_id, props, data), patches
|
|
318
|
+
return None, []
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _extract_artist(
|
|
322
|
+
artist: Any, axes_id: str, context: ExtractionContext, is_3d: bool
|
|
323
|
+
) -> dict[str, Any] | None:
|
|
324
|
+
class_name = artist.__class__.__name__
|
|
325
|
+
if isinstance(artist, Line2D) and artist.axes is not None:
|
|
326
|
+
x, y = artist.get_data(orig=False)
|
|
327
|
+
if is_3d and hasattr(artist, "get_data_3d"):
|
|
328
|
+
x, y, z = artist.get_data_3d()
|
|
329
|
+
return context.artist(
|
|
330
|
+
"bfplot.line3d",
|
|
331
|
+
axes_id,
|
|
332
|
+
_line_props(artist),
|
|
333
|
+
{"x": context.array(x), "y": context.array(y), "z": context.array(z)},
|
|
334
|
+
)
|
|
335
|
+
return context.artist(
|
|
336
|
+
"bfplot.line",
|
|
337
|
+
axes_id,
|
|
338
|
+
_line_props(artist),
|
|
339
|
+
{"x": context.array(x), "y": context.array(y)},
|
|
340
|
+
)
|
|
341
|
+
if isinstance(artist, PathCollection):
|
|
342
|
+
type_name = "bfplot.scatter"
|
|
343
|
+
if is_3d and hasattr(artist, "_offsets3d"):
|
|
344
|
+
x, y, z = artist._offsets3d
|
|
345
|
+
data: dict[str, Any] = {
|
|
346
|
+
"x": context.array(x),
|
|
347
|
+
"y": context.array(y),
|
|
348
|
+
"z": context.array(z),
|
|
349
|
+
}
|
|
350
|
+
type_name = "bfplot.scatter3d"
|
|
351
|
+
else:
|
|
352
|
+
offsets = np.asarray(artist.get_offsets())
|
|
353
|
+
data = {"xy": context.array(offsets)}
|
|
354
|
+
sizes = artist.get_sizes()
|
|
355
|
+
if len(sizes):
|
|
356
|
+
data["sizes"] = context.array(sizes)
|
|
357
|
+
values = artist.get_array()
|
|
358
|
+
if values is not None:
|
|
359
|
+
data["values"] = context.array(values)
|
|
360
|
+
return context.artist(type_name, axes_id, _collection_props(artist), data, source=artist)
|
|
361
|
+
if class_name == "FillBetweenPolyCollection":
|
|
362
|
+
return _extract_paths("bfplot.fill_between", artist, axes_id, context)
|
|
363
|
+
if class_name in {"QuadContourSet", "ContourSet"}:
|
|
364
|
+
return _extract_contour(artist, axes_id, context)
|
|
365
|
+
if is_3d and class_name == "Poly3DCollection":
|
|
366
|
+
faces = _poly3d_faces(artist)
|
|
367
|
+
return _extract_3d_paths("bfplot.surface3d", faces, artist, axes_id, context)
|
|
368
|
+
if is_3d and isinstance(artist, LineCollection) and hasattr(artist, "_segments3d"):
|
|
369
|
+
return _extract_3d_paths(
|
|
370
|
+
"bfplot.mesh3d", list(artist._segments3d), artist, axes_id, context
|
|
371
|
+
)
|
|
372
|
+
if isinstance(artist, PolyCollection):
|
|
373
|
+
return _extract_paths("bfplot.patch", artist, axes_id, context)
|
|
374
|
+
if isinstance(artist, LineCollection):
|
|
375
|
+
return context.artist(
|
|
376
|
+
"bfplot.contour",
|
|
377
|
+
axes_id,
|
|
378
|
+
_collection_props(artist),
|
|
379
|
+
{"segments": context.array(np.asarray(artist.get_segments()))},
|
|
380
|
+
)
|
|
381
|
+
if isinstance(artist, AxesImage):
|
|
382
|
+
return _extract_image(artist, axes_id, context)
|
|
383
|
+
if isinstance(artist, Text) and artist.axes is not None and artist.get_text():
|
|
384
|
+
position = artist.get_position()
|
|
385
|
+
data = {"position": [_finite(position[0]), _finite(position[1])], "text": artist.get_text()}
|
|
386
|
+
props = _common(artist) | {
|
|
387
|
+
"color": _color(artist.get_color()),
|
|
388
|
+
"size": float(artist.get_fontsize()),
|
|
389
|
+
"rotation": float(artist.get_rotation()),
|
|
390
|
+
"horizontalalignment": artist.get_horizontalalignment(),
|
|
391
|
+
"verticalalignment": artist.get_verticalalignment(),
|
|
392
|
+
}
|
|
393
|
+
return context.artist("bfplot.text", axes_id, props, data)
|
|
394
|
+
if isinstance(artist, Patch) and artist.axes is not None:
|
|
395
|
+
data: dict[str, Any] = {}
|
|
396
|
+
if isinstance(artist, Polygon):
|
|
397
|
+
data["vertices"] = context.array(artist.get_xy())
|
|
398
|
+
else:
|
|
399
|
+
data["path"] = context.array(artist.get_path().vertices)
|
|
400
|
+
data["transform"] = _finite(artist.get_patch_transform().get_matrix())
|
|
401
|
+
return context.artist("bfplot.patch", axes_id, _patch_props(artist), data)
|
|
402
|
+
return None
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _line_props(line: Any) -> dict[str, Any]:
|
|
406
|
+
return _common(line) | {
|
|
407
|
+
"color": _color(line.get_color()),
|
|
408
|
+
"linewidth": float(line.get_linewidth()),
|
|
409
|
+
"linestyle": line.get_linestyle(),
|
|
410
|
+
"marker": line.get_marker(),
|
|
411
|
+
"markersize": float(line.get_markersize()),
|
|
412
|
+
"drawstyle": line.get_drawstyle(),
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _patch_props(patch: Any) -> dict[str, Any]:
|
|
417
|
+
return _common(patch) | {
|
|
418
|
+
"facecolor": _color(patch.get_facecolor()),
|
|
419
|
+
"edgecolor": _color(patch.get_edgecolor()),
|
|
420
|
+
"linewidth": float(patch.get_linewidth()),
|
|
421
|
+
"linestyle": patch.get_linestyle(),
|
|
422
|
+
"hatch": patch.get_hatch(),
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _collection_props(collection: Any) -> dict[str, Any]:
|
|
427
|
+
result = _common(collection)
|
|
428
|
+
for name, getter in (
|
|
429
|
+
("facecolors", "get_facecolors"),
|
|
430
|
+
("edgecolors", "get_edgecolors"),
|
|
431
|
+
("linewidths", "get_linewidths"),
|
|
432
|
+
):
|
|
433
|
+
value = getattr(collection, getter)()
|
|
434
|
+
if len(value):
|
|
435
|
+
result[name] = _finite(value)
|
|
436
|
+
cmap = getattr(collection, "cmap", None)
|
|
437
|
+
norm = getattr(collection, "norm", None)
|
|
438
|
+
if cmap is not None:
|
|
439
|
+
result["cmap"] = cmap.name
|
|
440
|
+
if norm is not None:
|
|
441
|
+
result["clim"] = [_finite(norm.vmin), _finite(norm.vmax)]
|
|
442
|
+
return result
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _extract_paths(
|
|
446
|
+
type_name: str, artist: Any, axes_id: str, context: ExtractionContext
|
|
447
|
+
) -> dict[str, Any]:
|
|
448
|
+
paths = [path.vertices for path in artist.get_paths()]
|
|
449
|
+
lengths = np.asarray([len(path) for path in paths], dtype=np.int64)
|
|
450
|
+
vertices = np.concatenate(paths) if paths else np.empty((0, 2))
|
|
451
|
+
data = {"vertices": context.array(vertices), "path_lengths": context.array(lengths)}
|
|
452
|
+
values = artist.get_array() if hasattr(artist, "get_array") else None
|
|
453
|
+
if values is not None:
|
|
454
|
+
data["values"] = context.array(values)
|
|
455
|
+
return context.artist(type_name, axes_id, _collection_props(artist), data, source=artist)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _poly3d_faces(artist: Any) -> list[np.ndarray]:
|
|
459
|
+
faces = getattr(artist, "_faces", None)
|
|
460
|
+
if isinstance(faces, np.ndarray) and faces.ndim == 3 and faces.shape[-1] == 3:
|
|
461
|
+
# Matplotlib pads faces to a common length; the padding vertices are garbage.
|
|
462
|
+
invalid = getattr(artist, "_invalid_vertices", None)
|
|
463
|
+
if isinstance(invalid, np.ndarray) and invalid.shape == faces.shape[:2]:
|
|
464
|
+
return [face[~mask] for face, mask in zip(faces, invalid, strict=True)]
|
|
465
|
+
return [face for face in faces]
|
|
466
|
+
vector = getattr(artist, "_vec", None)
|
|
467
|
+
slices = getattr(artist, "_segslices", None)
|
|
468
|
+
if vector is not None and slices is not None:
|
|
469
|
+
values = np.asarray(vector)
|
|
470
|
+
if values.ndim == 2 and values.shape[0] >= 3:
|
|
471
|
+
return [values[:3, segment].T for segment in slices]
|
|
472
|
+
return []
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _extract_3d_paths(
|
|
476
|
+
type_name: str, paths: list[Any], artist: Any, axes_id: str, context: ExtractionContext
|
|
477
|
+
) -> dict[str, Any]:
|
|
478
|
+
arrays = [np.asarray(path, dtype=float) for path in paths]
|
|
479
|
+
arrays = [path for path in arrays if path.ndim == 2 and path.shape[1] == 3 and len(path)]
|
|
480
|
+
lengths = np.asarray([len(path) for path in arrays], dtype=np.int64)
|
|
481
|
+
vertices = np.concatenate(arrays) if arrays else np.empty((0, 3))
|
|
482
|
+
data = {"vertices": context.array(vertices), "path_lengths": context.array(lengths)}
|
|
483
|
+
values = artist.get_array() if hasattr(artist, "get_array") else None
|
|
484
|
+
if values is not None:
|
|
485
|
+
data["values"] = context.array(values)
|
|
486
|
+
return context.artist(type_name, axes_id, _collection_props(artist), data, source=artist)
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _extract_contour(artist: Any, axes_id: str, context: ExtractionContext) -> dict[str, Any]:
|
|
490
|
+
paths = []
|
|
491
|
+
path_values = []
|
|
492
|
+
cvalues = np.asarray(getattr(artist, "cvalues", []), dtype=float)
|
|
493
|
+
collections = getattr(artist, "collections", [])
|
|
494
|
+
for index, collection in enumerate(collections):
|
|
495
|
+
collection_paths = collection.get_paths()
|
|
496
|
+
paths.extend(collection_paths)
|
|
497
|
+
path_values.extend(
|
|
498
|
+
[cvalues[index] if index < len(cvalues) else float(index)] * len(collection_paths)
|
|
499
|
+
)
|
|
500
|
+
if not paths and hasattr(artist, "get_paths"):
|
|
501
|
+
paths.extend(artist.get_paths())
|
|
502
|
+
path_values.extend(cvalues[: len(paths)])
|
|
503
|
+
lengths = np.asarray([len(path.vertices) for path in paths], dtype=np.int64)
|
|
504
|
+
vertices = np.concatenate([path.vertices for path in paths]) if paths else np.empty((0, 2))
|
|
505
|
+
codes = (
|
|
506
|
+
np.concatenate(
|
|
507
|
+
[
|
|
508
|
+
path.codes
|
|
509
|
+
if path.codes is not None
|
|
510
|
+
else np.full(len(path.vertices), 2, dtype=np.uint8)
|
|
511
|
+
for path in paths
|
|
512
|
+
]
|
|
513
|
+
)
|
|
514
|
+
if paths
|
|
515
|
+
else np.empty(0, dtype=np.uint8)
|
|
516
|
+
)
|
|
517
|
+
filled = bool(getattr(artist, "filled", False))
|
|
518
|
+
data = {
|
|
519
|
+
"vertices": context.array(vertices),
|
|
520
|
+
"path_codes": context.array(codes),
|
|
521
|
+
"path_lengths": context.array(lengths),
|
|
522
|
+
"path_values": context.array(np.asarray(path_values, dtype=float)),
|
|
523
|
+
"levels": context.array(artist.levels),
|
|
524
|
+
}
|
|
525
|
+
props = _collection_props(artist)
|
|
526
|
+
props["cmap"] = artist.cmap.name
|
|
527
|
+
return context.artist(
|
|
528
|
+
"bfplot.contourf" if filled else "bfplot.contour", axes_id, props, data, source=artist
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _extract_image(image: Any, axes_id: str, context: ExtractionContext) -> dict[str, Any]:
|
|
533
|
+
data = {
|
|
534
|
+
"values": context.array(image.get_array()),
|
|
535
|
+
"extent": [_finite(item) for item in image.get_extent()],
|
|
536
|
+
}
|
|
537
|
+
props = _common(image) | {
|
|
538
|
+
"cmap": image.get_cmap().name,
|
|
539
|
+
"clim": [_finite(item) for item in image.get_clim()],
|
|
540
|
+
"origin": image.origin,
|
|
541
|
+
"interpolation": image.get_interpolation(),
|
|
542
|
+
}
|
|
543
|
+
return context.artist("bfplot.image", axes_id, props, data, source=image)
|