jarvisplot 1.0.1__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.
Files changed (42) hide show
  1. jarvisplot/Figure/adapters.py +773 -0
  2. jarvisplot/Figure/cards/std_axes_adapter_config.json +23 -0
  3. jarvisplot/Figure/data_pipelines.py +87 -0
  4. jarvisplot/Figure/figure.py +1573 -0
  5. jarvisplot/Figure/helper.py +217 -0
  6. jarvisplot/Figure/load_data.py +252 -0
  7. jarvisplot/__init__.py +0 -0
  8. jarvisplot/cards/a4paper/1x1/ternary.json +6 -0
  9. jarvisplot/cards/a4paper/2x1/rect.json +106 -0
  10. jarvisplot/cards/a4paper/2x1/rect5x1.json +344 -0
  11. jarvisplot/cards/a4paper/2x1/rect_cmap.json +181 -0
  12. jarvisplot/cards/a4paper/2x1/ternary.json +139 -0
  13. jarvisplot/cards/a4paper/2x1/ternary_cmap.json +189 -0
  14. jarvisplot/cards/a4paper/4x1/rect.json +106 -0
  15. jarvisplot/cards/a4paper/4x1/rect_cmap.json +174 -0
  16. jarvisplot/cards/a4paper/4x1/ternary.json +139 -0
  17. jarvisplot/cards/a4paper/4x1/ternary_cmap.json +189 -0
  18. jarvisplot/cards/args.json +50 -0
  19. jarvisplot/cards/colors/colormaps.json +140 -0
  20. jarvisplot/cards/default/output.json +11 -0
  21. jarvisplot/cards/gambit/1x1/ternary.json +6 -0
  22. jarvisplot/cards/gambit/2x1/rect_cmap.json +200 -0
  23. jarvisplot/cards/gambit/2x1/ternary.json +139 -0
  24. jarvisplot/cards/gambit/2x1/ternary_cmap.json +205 -0
  25. jarvisplot/cards/icons/JarvisHEP.png +0 -0
  26. jarvisplot/cards/icons/gambit.png +0 -0
  27. jarvisplot/cards/icons/gambit_small.png +0 -0
  28. jarvisplot/cards/style_preference.json +23 -0
  29. jarvisplot/cli.py +64 -0
  30. jarvisplot/client.py +6 -0
  31. jarvisplot/config.py +69 -0
  32. jarvisplot/core.py +237 -0
  33. jarvisplot/data_loader.py +441 -0
  34. jarvisplot/inner_func.py +162 -0
  35. jarvisplot/utils/__init__.py +0 -0
  36. jarvisplot/utils/cmaps.py +258 -0
  37. jarvisplot/utils/interpolator.py +377 -0
  38. jarvisplot-1.0.1.dist-info/METADATA +80 -0
  39. jarvisplot-1.0.1.dist-info/RECORD +42 -0
  40. jarvisplot-1.0.1.dist-info/WHEEL +5 -0
  41. jarvisplot-1.0.1.dist-info/entry_points.txt +2 -0
  42. jarvisplot-1.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,773 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from __future__ import annotations
4
+ from typing import Any, Dict, Optional
5
+ import numpy as np
6
+ import os
7
+ import json
8
+
9
+ from matplotlib.axes import Axes
10
+ from matplotlib.collections import PolyCollection, LineCollection
11
+ from matplotlib.path import Path
12
+ from matplotlib.patches import PathPatch
13
+ #
14
+ from .helper import _auto_clip, _mask_by_extend, voronoi_finite_polygons_2d, _clip_poly_to_rect
15
+
16
+
17
+ # —— Basic Adapter: Forward to the underlying Axes, merge default parameters, perform automatic clipping ——
18
+ class StdAxesAdapter:
19
+ def __init__(self, ax: Axes, defaults: Optional[Dict[str, Dict[str, Any]]] = None,
20
+ clip_path=None):
21
+ """
22
+ ax: 原始 matplotlib Axes
23
+ defaults: 分方法的默认参数,如 {"scatter": {"s": 8, "alpha": 0.8}}
24
+ clip_path: 可选 Path/PathPatch,用于 set_clip_path
25
+ """
26
+ self.ax = ax
27
+ self._defaults = defaults or {}
28
+ self._clip_path = clip_path # None means no cropping
29
+ self.config = self._load_internal_config()
30
+ self._legend = False
31
+ self.status = "init" # lifecycle: init -> configured -> drawn -> finalized
32
+ self.needs_finalize = True # allow some axes (e.g., logo) to opt out
33
+
34
+ def finalize(self):
35
+ """Finalize axes after all layers/legends/colorbars applied.
36
+ Override in specialized adapters if needed. Here we just mark status.
37
+ """
38
+ self.status = "finalized"
39
+
40
+ def _load_internal_config(self):
41
+ default_path = os.path.join(os.path.dirname(__file__), "cards", "std_axes_adapter_config.json")
42
+ try:
43
+ with open(default_path, "r") as f:
44
+ return json.load(f)
45
+ except FileNotFoundError:
46
+ # Optional: return default empty configuration or raise
47
+ return {}
48
+
49
+ # Parameter merging: user preference takes priority, default as fallback
50
+ def _merge(self, method: str, kwargs: Dict[str, Any]) -> Dict[str, Any]:
51
+ base = dict(self._defaults.get(method, {}))
52
+ base.update(kwargs or {})
53
+ return base
54
+
55
+ # —— Common method forwarding (add as needed) ——
56
+ def scatter(self, **kwargs):
57
+ x, y = kwargs.pop("x"), kwargs.pop("y")
58
+ kw = self._merge("scatter", kwargs)
59
+ artists = self.ax.scatter(x, y, **kw)
60
+ return _auto_clip(artists, self.ax, self._clip_path)
61
+
62
+ def plot(self, *args, **kwargs):
63
+ x, y = kwargs.pop("x"), kwargs.pop("y")
64
+ # print("x:", x, "y:", y)
65
+ kw = self._merge("plot", kwargs)
66
+ fmt = kw.pop("fmt", None)
67
+ if fmt is not None:
68
+ artists = self.ax.plot(x, y, fmt, **kw)
69
+ else:
70
+ artists = self.ax.plot(x, y, **kw)
71
+ return _auto_clip(artists, self.ax, self._clip_path)
72
+
73
+ def fill(self, **kwargs):
74
+ x, y = kwargs.pop("x"), kwargs.pop("y")
75
+ kw = self._merge("fill", kwargs)
76
+ artists = self.ax.fill(x, y, **kw)
77
+ return _auto_clip(artists, self.ax, self._clip_path)
78
+
79
+ def contour(self, *args, **kwargs):
80
+ kw = self._merge("contour", kwargs)
81
+ artists = self.ax.contour(*args, **kw)
82
+ return _auto_clip(artists, self.ax, self._clip_path)
83
+
84
+ def contourf(self, *args, **kwargs):
85
+ kw = self._merge("contourf", kwargs)
86
+ artists = self.ax.contourf(*args, **kw)
87
+ return _auto_clip(artists, self.ax, self._clip_path)
88
+
89
+ def imshow(self, *args, **kwargs):
90
+ kw = self._merge("imshow", kwargs)
91
+ artists = self.ax.imshow(*args, **kw)
92
+ return _auto_clip(artists, self.ax, self._clip_path)
93
+
94
+ def tricontour(self, **kwargs):
95
+ x, y, z = kwargs.pop("x"), kwargs.pop("y"), kwargs.pop("z")
96
+ import matplotlib.tri as tri
97
+ triang = tri.Triangulation(x, y)
98
+ refiner = tri.UniformTriRefiner(triang)
99
+ tri_refi, z_test_refi = refiner.refine_field(z, subdiv=3)
100
+ kw = self._merge("tricontour", kwargs)
101
+ artists = self.ax.tricontour(tri_refi, z_test_refi, **kw)
102
+ return _auto_clip(artists, self.ax, self._clip_path)
103
+
104
+ def tricontourf(self, **kwargs):
105
+ x, y, z = kwargs.pop("x"), kwargs.pop("y"), kwargs.pop("z")
106
+ import matplotlib.tri as tri
107
+
108
+ triang = tri.Triangulation(x, y)
109
+ refiner = tri.UniformTriRefiner(triang)
110
+ tri_refi, z_refi = refiner.refine_field(z, subdiv=3)
111
+
112
+ kw = self._merge("tricontourf", kwargs)
113
+
114
+ z_masked, vmin_eff, vmax_eff = _mask_by_extend(
115
+ z_refi,
116
+ extend=kw.get("extend", "neither"),
117
+ vmin=kw.get("vmin"),
118
+ vmax=kw.get("vmax"),
119
+ levels=kw.get("levels"),
120
+ norm=kw.get("norm"),
121
+ )
122
+ try:
123
+ print("Adapter 184 -> ", z_refi.max(), z_refi.min())
124
+ if kw.get("levels", False) and isinstance(kw.get("levels", False), int):
125
+ kw["levels"] = np.linspace(kw.get("vmin"), kw.get("vmax"), kw.get("levels"))
126
+ except TypeError:
127
+ pass
128
+ if kw.get("norm") is not None:
129
+ kw.pop("vmin", None)
130
+ kw.pop("vmax", None)
131
+ else:
132
+ kw.setdefault("vmin", vmin_eff)
133
+ kw.setdefault("vmax", vmax_eff)
134
+
135
+ z_mask_arr = np.ma.getmaskarray(z_masked)
136
+ if z_mask_arr is not False and z_mask_arr is not None:
137
+ tri_mask = np.any(z_mask_arr[tri_refi.triangles], axis=1)
138
+ tri_refi.set_mask(tri_mask)
139
+ z_for_plot = np.asarray(np.ma.filled(z_masked, 0.0))
140
+ else:
141
+ z_for_plot = np.asarray(z_masked)
142
+
143
+ artists = self.ax.tricontourf(tri_refi, z_for_plot, **kw)
144
+ return _auto_clip(artists, self.ax, self._clip_path)
145
+
146
+
147
+
148
+ def voronoi(self, **kwargs):
149
+ if {"x", "y", "z"}.issubset(kwargs.keys()):
150
+ return self.voronoi_cmapfill(**kwargs)
151
+ elif {"x", "y"}.issubset(kwargs.keys()):
152
+ return self.voronoi_colorfill(**kwargs)
153
+
154
+ def voronoi_colorfill(self, **kwargs):
155
+ """Fill selected Voronoi cells with a single facecolor (no z / no colorbar).
156
+
157
+ Required:
158
+ - x, y
159
+ Optional:
160
+ - where: boolean mask (same shape as x/y). If provided, only True cells are filled.
161
+ - facecolor, edgecolor, linewidth/linewidths, draw_edges, antialiased, extent, radius, zorder
162
+ """
163
+ import numpy as _np
164
+ try:
165
+ from scipy.spatial import Voronoi
166
+ except Exception as e:
167
+ raise ImportError("voronoi requires scipy.spatial.Voronoi. Please install scipy.") from e
168
+
169
+ x = kwargs.pop("x")
170
+ y = kwargs.pop("y")
171
+ where = kwargs.pop("where", None)
172
+
173
+ # Keep matplotlib fill kwargs intact (facecolor/edgecolor/linewidth/alpha/etc.).
174
+ # Only consume voronoi-specific options here.
175
+ extent = kwargs.pop("extent", None) # data-space
176
+ radius = kwargs.pop("radius", None)
177
+
178
+ if x.size == 0:
179
+ return []
180
+
181
+ # ---- derive view box & transforms from axes ----
182
+ xlim = self.ax.get_xlim()
183
+ ylim = self.ax.get_ylim()
184
+ if extent is None:
185
+ extent = (min(xlim), max(xlim), min(ylim), max(ylim))
186
+ xmin, xmax, ymin, ymax = extent
187
+
188
+ t_data_to_disp = self.ax.transData
189
+ t_disp_to_data = t_data_to_disp.inverted()
190
+
191
+ disp_ll = t_data_to_disp.transform((xmin, ymin))
192
+ disp_ur = t_data_to_disp.transform((xmax, ymax))
193
+ disp_x0, disp_y0 = disp_ll
194
+ disp_x1, disp_y1 = disp_ur
195
+
196
+ # robust ordering / non-zero spans
197
+ if disp_x1 == disp_x0:
198
+ disp_x1 = disp_x0 + 1.0
199
+ if disp_y1 == disp_y0:
200
+ disp_y1 = disp_y0 + 1.0
201
+ if disp_x1 < disp_x0:
202
+ disp_x0, disp_x1 = disp_x1, disp_x0
203
+ if disp_y1 < disp_y0:
204
+ disp_y0, disp_y1 = disp_y1, disp_y0
205
+
206
+ disp_w = (disp_x1 - disp_x0)
207
+ disp_h = (disp_y1 - disp_y0)
208
+
209
+ pts_disp = t_data_to_disp.transform(_np.c_[x, y])
210
+ pts_norm = _np.c_[
211
+ (pts_disp[:, 0] - disp_x0) / disp_w,
212
+ (pts_disp[:, 1] - disp_y0) / disp_h,
213
+ ]
214
+
215
+ if not _np.all(_np.isfinite(pts_norm)):
216
+ pts_norm = _np.nan_to_num(pts_norm, nan=0.5, posinf=1.0, neginf=0.0)
217
+
218
+ vor = Voronoi(pts_norm)
219
+
220
+ regions, vertices = voronoi_finite_polygons_2d(vor, radius=radius)
221
+ unit_rect = (0.0, 1.0, 0.0, 1.0)
222
+
223
+ polys_fill = []
224
+ for i_pt, region in enumerate(regions):
225
+ if not region:
226
+ continue
227
+ if where is not None and (not bool(where[i_pt])):
228
+ continue
229
+
230
+ poly = vertices[region]
231
+ poly = [(float(px), float(py)) for px, py in poly]
232
+ poly = _clip_poly_to_rect(poly, unit_rect)
233
+ if len(poly) < 3:
234
+ continue
235
+
236
+ # norm -> display -> data
237
+ poly_disp = _np.c_[
238
+ _np.array([p[0] for p in poly]) * disp_w + disp_x0,
239
+ _np.array([p[1] for p in poly]) * disp_h + disp_y0,
240
+ ]
241
+ poly_data = [tuple(p) for p in t_disp_to_data.transform(poly_disp)]
242
+ polys_fill.append(poly_data)
243
+
244
+ if len(polys_fill) == 0:
245
+ return []
246
+
247
+ # Merge selected Voronoi cells into one (possibly multi-) polygon, then fill using ax.fill
248
+ try:
249
+ from shapely.geometry import Polygon as _SHPPolygon
250
+ from shapely.ops import unary_union as _shp_unary_union
251
+ except Exception as e:
252
+ raise ImportError("voronoi_colorfill merge requires shapely. Please install shapely.") from e
253
+
254
+ shp_polys = []
255
+ for poly in polys_fill:
256
+ try:
257
+ g = _SHPPolygon(poly)
258
+ if not g.is_valid:
259
+ g = g.buffer(0)
260
+ if (not g.is_empty) and g.area > 0:
261
+ shp_polys.append(g)
262
+ except Exception:
263
+ continue
264
+
265
+ if not shp_polys:
266
+ return []
267
+
268
+ merged = _shp_unary_union(shp_polys)
269
+ if merged.is_empty:
270
+ return []
271
+
272
+ # Inherit matplotlib fill kwargs (plus any defaults for 'fill')
273
+ kw = self._merge("fill", kwargs)
274
+
275
+ artists = []
276
+ if merged.geom_type == "Polygon":
277
+ parts = [merged]
278
+ else:
279
+ parts = list(getattr(merged, "geoms", []))
280
+
281
+ for g in parts:
282
+ if g.is_empty:
283
+ continue
284
+ xs, ys = g.exterior.coords.xy
285
+ artists.extend(self.ax.fill(list(xs), list(ys), **kw))
286
+
287
+ return _auto_clip(artists, self.ax, self._clip_path)
288
+
289
+
290
+ def voronoi_cmapfill(self, **kwargs):
291
+ import matplotlib as mpl
292
+ x = kwargs.pop("x")
293
+ y = kwargs.pop("y")
294
+ z = kwargs.pop("z")
295
+ where = kwargs.pop("where", None)
296
+ cmap = kwargs.pop("cmap", None)
297
+ if isinstance(cmap, str):
298
+ try:
299
+ cmap = mpl.colormaps.get(cmap)
300
+ except Exception:
301
+ cmap = None
302
+ # Now proceed as before
303
+ import numpy as _np
304
+ try:
305
+ from scipy.spatial import Voronoi
306
+ except Exception as e:
307
+ raise ImportError("voronoi requires scipy.spatial.Voronoi. Please install scipy.") from e
308
+
309
+ # ---- inputs ----
310
+ x = _np.asarray(x)
311
+ y = _np.asarray(y)
312
+ z = _np.asarray(z)
313
+ if where is not None:
314
+ where = _np.asarray(where, dtype=bool)
315
+ if where.shape != x.shape:
316
+ raise ValueError("voronoi: 'where' must have the same shape as x/y")
317
+ # print(x, y, z)
318
+ # self.ax.scatter(x, y, s=0.3, marker='.', c="#FF42A1", zorder=10, edgecolors="none")
319
+ vmin = kwargs.pop("vmin", None)
320
+ vmax = kwargs.pop("vmax", None)
321
+ edgecolor = kwargs.pop("edgecolor", 'none')
322
+ draw_edges = kwargs.pop("draw_edges", True)
323
+ antialiased = kwargs.pop("antialiased", False)
324
+ orig_lw = kwargs.pop("linewidth", kwargs.pop("linewidths", 0.0))
325
+ extent = kwargs.pop("extent", None) # data-space
326
+ radius = kwargs.pop("radius", None)
327
+ nan_color = kwargs.pop("nan_color", None)
328
+ zorder = kwargs.pop("zorder", None)
329
+
330
+ # ---- derive view box & transforms from axes ----
331
+ xlim = self.ax.get_xlim()
332
+ ylim = self.ax.get_ylim()
333
+ # print(xlim, ylim)
334
+ if extent is None:
335
+ extent = (min(xlim), max(xlim), min(ylim), max(ylim))
336
+ xmin, xmax, ymin, ymax = extent
337
+
338
+ t_data_to_disp = self.ax.transData
339
+ t_disp_to_data = t_data_to_disp.inverted()
340
+
341
+ disp_ll = t_data_to_disp.transform((xmin, ymin))
342
+ disp_ur = t_data_to_disp.transform((xmax, ymax))
343
+ disp_x0, disp_y0 = disp_ll
344
+ disp_x1, disp_y1 = disp_ur
345
+ if disp_x1 == disp_x0:
346
+ disp_x1 = disp_x0 + 1.0
347
+ if disp_y1 == disp_y0:
348
+ disp_y1 = disp_y0 + 1.0
349
+ if disp_x1 < disp_x0:
350
+ disp_x0, disp_x1 = disp_x1, disp_x0
351
+ if disp_y1 < disp_y0:
352
+ disp_y0, disp_y1 = disp_y1, disp_y0
353
+ disp_w = (disp_x1 - disp_x0)
354
+ disp_h = (disp_y1 - disp_y0)
355
+
356
+ pts_disp = t_data_to_disp.transform(_np.c_[x, y])
357
+ pts_norm = _np.c_[ (pts_disp[:,0] - disp_x0)/disp_w, (pts_disp[:,1] - disp_y0)/disp_h ]
358
+
359
+ if not _np.all(_np.isfinite(pts_norm)):
360
+ pts_norm = _np.nan_to_num(pts_norm, nan=0.5, posinf=1.0, neginf=0.0)
361
+
362
+ vor = Voronoi(pts_norm)
363
+
364
+ from .helper import voronoi_finite_polygons_2d, _clip_poly_to_rect
365
+ regions, vertices = voronoi_finite_polygons_2d(vor, radius=radius)
366
+ unit_rect = (0.0, 1.0, 0.0, 1.0)
367
+
368
+ polys_valid, zvals_valid = [], []
369
+ polys_bg = []
370
+ def _is_invalid(val):
371
+ try:
372
+ return (val is None) or (not _np.isfinite(float(val)))
373
+ except Exception:
374
+ return True
375
+
376
+ for i_pt, region in enumerate(regions):
377
+ if not region:
378
+ continue
379
+ if where is not None and (not bool(where[i_pt])):
380
+ continue
381
+ poly = vertices[region]
382
+ poly = [(float(px), float(py)) for px, py in poly]
383
+ poly = _clip_poly_to_rect(poly, unit_rect)
384
+ if len(poly) < 3:
385
+ continue
386
+ poly_disp = _np.c_[_np.array([p[0] for p in poly])*disp_w + disp_x0,
387
+ _np.array([p[1] for p in poly])*disp_h + disp_y0]
388
+ poly_data = [tuple(p) for p in t_disp_to_data.transform(poly_disp)]
389
+ val = z[i_pt]
390
+ if _is_invalid(val):
391
+ polys_bg.append(poly_data)
392
+ else:
393
+ polys_valid.append(poly_data)
394
+ zvals_valid.append(float(val))
395
+
396
+ from matplotlib.collections import PolyCollection
397
+ artists = []
398
+ from matplotlib import colors as mcolors
399
+ norm = kwargs.pop("norm", None)
400
+ if norm is None and (vmin is not None or vmax is not None):
401
+ norm = mcolors.Normalize(vmin=vmin, vmax=vmax)
402
+
403
+ pc = PolyCollection(
404
+ polys_valid,
405
+ array=_np.asarray(zvals_valid),
406
+ cmap=cmap,
407
+ edgecolor='none',
408
+ linewidth=0.0,
409
+ norm=norm,
410
+ antialiased=antialiased,
411
+ )
412
+ if zorder is not None:
413
+ pc.set_zorder(zorder)
414
+
415
+ artists.append(self.ax.add_collection(pc))
416
+
417
+ return _auto_clip(artists, self.ax, self._clip_path)
418
+
419
+ def voronoif(self, **kwargs):
420
+ """Hatched fill for a boundary layer of a where-selected Voronoi region.
421
+
422
+ Algorithm:
423
+ 1) Use `where` to select Voronoi cells and union them into region A.
424
+ 2) For each selected cell, keep it only if the site point (core) is within
425
+ `core_dist` (in axes-intrinsic unit-square coords) of the *boundary of A*.
426
+ The kept cells form region B.
427
+ 3) Hatch-fill the union of B.
428
+
429
+ Inputs:
430
+ - x, y: 1D arrays of site positions (data coordinates)
431
+ - where: optional 1D boolean array; True selects the corresponding Voronoi cell for region A
432
+
433
+ Keyword options:
434
+ - core_dist: float, core-to-boundary(A) distance threshold in unit-square coords (default 0.05)
435
+ - hatch: str (default '///')
436
+ - extent: (xmin, xmax, ymin, ymax) in data coords (default: current view)
437
+ - radius: passed to voronoi_finite_polygons_2d
438
+ - frame_strip: float, exclude hatch within this strip near axes frame (unit-square, default 0.0)
439
+ - All standard `fill` kwargs are inherited (facecolor/edgecolor/linewidth/alpha/...) via adapter defaults.
440
+ """
441
+ import numpy as _np
442
+
443
+ x = kwargs.pop("x")
444
+ y = kwargs.pop("y")
445
+ where = kwargs.pop("where", None)
446
+
447
+ core_dist = 0.025
448
+ frame_strip = 0.0
449
+
450
+ kw_all = self._merge("fill", kwargs)
451
+ from .helper import split_fill_kwargs
452
+ kw_edge, kw_face, kw_rest = split_fill_kwargs(kw_all)
453
+ kw_edge.update(kw_rest)
454
+ kw_face.update(kw_rest)
455
+ # ---- inputs ----
456
+ x = _np.asarray(x)
457
+ y = _np.asarray(y)
458
+ if x.shape != y.shape:
459
+ raise ValueError("voronoif: x and y must have the same shape")
460
+ if x.size == 0:
461
+ return []
462
+
463
+ if where is None:
464
+ where = _np.ones_like(x, dtype=bool)
465
+
466
+ try:
467
+ from scipy.spatial import Voronoi
468
+ except Exception as e:
469
+ raise ImportError("voronoif requires scipy.spatial.Voronoi. Please install scipy.") from e
470
+
471
+ try:
472
+ from shapely.geometry import Polygon as _SHPPolygon, Point as _SHPPoint, box as _SHPBox
473
+ from shapely.ops import unary_union as _shp_unary_union
474
+ except Exception as e:
475
+ raise ImportError("voronoif requires shapely. Please install shapely.") from e
476
+
477
+ # ---- derive view box & transforms from axes ----
478
+ xlim = self.ax.get_xlim()
479
+ ylim = self.ax.get_ylim()
480
+ extent = (min(xlim), max(xlim), min(ylim), max(ylim))
481
+ xmin, xmax, ymin, ymax = extent
482
+
483
+ t_data_to_disp = self.ax.transData
484
+
485
+ disp_ll = t_data_to_disp.transform((xmin, ymin))
486
+ disp_ur = t_data_to_disp.transform((xmax, ymax))
487
+ disp_x0, disp_y0 = disp_ll
488
+ disp_x1, disp_y1 = disp_ur
489
+
490
+ # robust ordering / non-zero spans
491
+ if disp_x1 == disp_x0:
492
+ disp_x1 = disp_x0 + 1.0
493
+ if disp_y1 == disp_y0:
494
+ disp_y1 = disp_y0 + 1.0
495
+ if disp_x1 < disp_x0:
496
+ disp_x0, disp_x1 = disp_x1, disp_x0
497
+ if disp_y1 < disp_y0:
498
+ disp_y0, disp_y1 = disp_y1, disp_y0
499
+
500
+ disp_w = (disp_x1 - disp_x0)
501
+ disp_h = (disp_y1 - disp_y0)
502
+
503
+ # ---- sites in axes-intrinsic unit-square coords ----
504
+ pts_disp = t_data_to_disp.transform(_np.c_[x, y])
505
+ pts_norm = _np.c_[
506
+ (pts_disp[:, 0] - disp_x0) / disp_w,
507
+ (pts_disp[:, 1] - disp_y0) / disp_h,
508
+ ]
509
+ if not _np.all(_np.isfinite(pts_norm)):
510
+ pts_norm = _np.nan_to_num(pts_norm, nan=0.5, posinf=1.0, neginf=0.0)
511
+
512
+ vor = Voronoi(pts_norm)
513
+ regions, vertices = voronoi_finite_polygons_2d(vor)
514
+ unit_rect = (0.0, 1.0, 0.0, 1.0)
515
+
516
+ # ---- collect A: where-selected cell polygons (unit-square) ----
517
+ polys_A_unit = []
518
+ idx_A = []
519
+ poly_by_idx = {}
520
+ for i_pt, region in enumerate(regions):
521
+ if (not where[i_pt]) or (not region):
522
+ continue
523
+ poly = vertices[region]
524
+ poly = [(float(px), float(py)) for px, py in poly]
525
+ poly = _clip_poly_to_rect(poly, unit_rect)
526
+ if len(poly) < 3:
527
+ continue
528
+ try:
529
+ g = _SHPPolygon(poly)
530
+ if not g.is_valid:
531
+ g = g.buffer(0)
532
+ if g.is_empty or g.area <= 0:
533
+ continue
534
+ polys_A_unit.append(g)
535
+ idx_A.append(i_pt)
536
+ poly_by_idx[i_pt] = g
537
+ except Exception:
538
+ continue
539
+
540
+ if not polys_A_unit:
541
+ return []
542
+
543
+ A = _shp_unary_union(polys_A_unit)
544
+ if A.is_empty:
545
+ return []
546
+
547
+ # ---- filter cells in A by core distance to boundary(A) ----
548
+ B_polys = []
549
+ bnd = A.boundary
550
+
551
+ # Exclude boundary segments that coincide with the axes frame (unit-square edges).
552
+ # We do this by intersecting with a slightly shrunken unit box, removing edges at u/v=0/1.
553
+ eps = 1.e-9
554
+ inner = _SHPBox(eps, eps, 1.0 - eps, 1.0 - eps)
555
+ bnd = bnd.intersection(inner)
556
+ # bnd: shapely LineString/MultiLineString in unit-square coords
557
+ if not (bnd is None or bnd.is_empty):
558
+ lines = []
559
+ gt = bnd.geom_type
560
+ if gt == "LineString":
561
+ lines = [bnd]
562
+ elif gt == "MultiLineString":
563
+ lines = list(bnd.geoms)
564
+ elif gt == "GeometryCollection":
565
+ for g in bnd.geoms:
566
+ if g.geom_type == "LineString":
567
+ lines.append(g)
568
+ elif g.geom_type == "MultiLineString":
569
+ lines.extend(list(g.geoms))
570
+ for line in lines:
571
+ lind = line.buffer(0.001, cap_style=2, join_style=2)
572
+ xs, ys = lind.exterior.coords.xy
573
+ self.ax.fill(list(xs), list(ys), **kw_edge, transform=self.ax.transAxes)
574
+
575
+
576
+ for i_pt in idx_A:
577
+ u, v = float(pts_norm[i_pt, 0]), float(pts_norm[i_pt, 1])
578
+ try:
579
+ d = _SHPPoint(u, v).distance(bnd)
580
+ except Exception:
581
+ continue
582
+ if d < core_dist:
583
+ B_polys.append(poly_by_idx[i_pt])
584
+
585
+ if not B_polys:
586
+ return []
587
+
588
+ B = _shp_unary_union(B_polys)
589
+ if B.is_empty:
590
+ return []
591
+
592
+ # Inherit matplotlib fill kwargs (plus any defaults for 'fill')
593
+ kw_face = self._merge("fill", kw_face)
594
+ kw_face['linewidth'] = 0.
595
+
596
+ artists = []
597
+ if B.geom_type == "Polygon":
598
+ parts = [B]
599
+ else:
600
+ parts = list(getattr(B, "geoms", []))
601
+
602
+ for g in parts:
603
+ if g.is_empty:
604
+ continue
605
+ xs, ys = g.exterior.coords.xy
606
+ artists.extend(self.ax.fill(list(xs), list(ys), **kw_face, transform=self.ax.transAxes))
607
+
608
+ return _auto_clip(artists, self.ax, self._clip_path)
609
+
610
+
611
+
612
+
613
+ # ---- optionally exclude hatch near the axes frame ----
614
+ if frame_strip > 0:
615
+ F = _SHPBox(0.0, 0.0, 1.0, 1.0)
616
+ Fin = F.buffer(-frame_strip)
617
+ strip = F.difference(Fin) if not Fin.is_empty else F
618
+ B = B.difference(strip.intersection(B))
619
+ if B.is_empty:
620
+ return []
621
+
622
+ # ---- draw hatch fill (HOLE-AWARE) ----
623
+ # NOTE: ax.fill cannot represent holes; use a PathPatch so removed interior cells stay unhatched.
624
+ kw = self._merge("fill", kw_face)
625
+ # kw.setdefault("facecolor", "none")
626
+ kw["linewidth"]= 0.
627
+ print(kw)
628
+
629
+ # kw.setdefault("hatch", hatch)
630
+
631
+ # Build a compound Path from shapely geometry (Polygon/MultiPolygon) with holes.
632
+ def _geom_to_path(geom):
633
+ if geom.is_empty:
634
+ return None
635
+ polys = [geom] if geom.geom_type == "Polygon" else list(getattr(geom, "geoms", []))
636
+ verts = []
637
+ codes = []
638
+
639
+ def _add_ring(coords):
640
+ ring = list(coords)
641
+ if len(ring) < 3:
642
+ return
643
+ # Matplotlib expects the last vertex for CLOSEPOLY; shapely rings already repeat the first.
644
+ # Ensure we have at least 4 points including the closing point.
645
+ if ring[0] != ring[-1]:
646
+ ring.append(ring[0])
647
+ verts.extend(ring)
648
+ codes.extend([Path.MOVETO] + [Path.LINETO] * (len(ring) - 2) + [Path.CLOSEPOLY])
649
+
650
+ for p in polys:
651
+ if p.is_empty:
652
+ continue
653
+ _add_ring(p.exterior.coords)
654
+ for hole in p.interiors:
655
+ _add_ring(hole.coords)
656
+
657
+ if not verts:
658
+ return None
659
+ return Path(verts, codes)
660
+
661
+ path = _geom_to_path(B)
662
+ if path is None:
663
+ return []
664
+
665
+ patch = PathPatch(path, transform=self.ax.transAxes, **kw)
666
+ if zorder is not None:
667
+ patch.set_zorder(zorder)
668
+
669
+ artist = self.ax.add_patch(patch)
670
+ return _auto_clip([artist], self.ax, self._clip_path)
671
+
672
+ # 为了兼容现有框架,暴露底层的方法/属性
673
+ def __getattr__(self, name: str):
674
+ # 未覆写的方法透传给原始 Axes
675
+ return getattr(self.ax, name)
676
+
677
+ def hist(self, *args, **kwargs):
678
+ import matplotlib.pyplot as plt
679
+ stacked = kwargs.get('stacked', False)
680
+ colors = kwargs.get('color', None) or kwargs.get('colors', None)
681
+
682
+ # Get the number of data groups: supports x as args[0] or kwargs['x']
683
+ x = kwargs.get('x', args[0] if args else None)
684
+ n_groups = None
685
+ if stacked and colors is None and x is not None:
686
+ # Only when x is two-dimensional data, len(x) is the number of groups (in the case of multiple arrays stacked)
687
+ try:
688
+ n_groups = len(x) if hasattr(x, '__len__') and not isinstance(x, (str, bytes)) else 1
689
+ except Exception:
690
+ n_groups = 1
691
+ default_colors = self._get_default_colors(n_groups)
692
+ kwargs['color'] = default_colors
693
+
694
+ kw = self._merge("hist", kwargs)
695
+ artists = self.ax.hist(*args, **kw)
696
+ return _auto_clip(artists, self.ax, self._clip_path)
697
+
698
+ def _get_default_colors(self, n):
699
+ import matplotlib.pyplot as plt
700
+ # print(getattr(self, "color_palette"))
701
+ palette = self.config['hist']['color']
702
+ print(len(palette), n)
703
+
704
+ # palette = getattr(self, 'color_palette', plt.cm.tab10.colors)
705
+ # print(palette)
706
+ return [palette[i % len(palette)] for i in range(n)]
707
+
708
+ # —— Ternary 适配器:在 Std 基础上增加 (a,b,c)->(x,y) 投影 ——
709
+ class TernaryAxesAdapter(StdAxesAdapter):
710
+ def __init__(self, ax: Axes, defaults: Optional[Dict[str, Any]] = None,
711
+ clip_path=None):
712
+ # Allow a flat defaults dict like {"facecolor": "..."} and keep it internal.
713
+ d = dict(defaults) if isinstance(defaults, dict) else {}
714
+ facecolor = d.pop("facecolor", None)
715
+
716
+ super().__init__(ax, defaults=d or None, clip_path=clip_path)
717
+
718
+ if facecolor is not None:
719
+ self.set_facecolor(facecolor)
720
+
721
+ self.status = "init"
722
+
723
+ def set_facecolor(self, color, zorder=-100):
724
+ self.ax.fill(
725
+ [0.0, 1.0, 0.5],
726
+ [0.0, 0.0, 1.0],
727
+ facecolor=color,
728
+ edgecolor="none",
729
+ zorder=zorder
730
+ )
731
+
732
+ @staticmethod
733
+ def _lbr_to_xy(a, b, c):
734
+ s = (a + b + c)
735
+ s = np.where(s == 0.0, 1.0, s) # 避免除零
736
+ aa, bb, cc = a/s, b/s, c/s
737
+ x = bb + 0.5 * cc
738
+ y = cc
739
+ return x, y
740
+
741
+ def scatter(self, **kwargs):
742
+ if {"left", "right", "bottom"}.issubset(kwargs.keys()):
743
+ x, y = self._lbr_to_xy(kwargs.pop('left'), kwargs.pop('right'), kwargs.pop('bottom'))
744
+ kwargs['x'] = x
745
+ kwargs['y'] = y
746
+ return super().scatter(**kwargs)
747
+
748
+
749
+ def plot(self, **kwargs):
750
+ if {"left", "right", "bottom"}.issubset(kwargs.keys()):
751
+ x, y = self._lbr_to_xy(kwargs.pop('left'), kwargs.pop('right'), kwargs.pop('bottom'))
752
+ kwargs['x'] = x
753
+ kwargs['y'] = y
754
+ return super().plot(**kwargs)
755
+
756
+
757
+ def tricontour(self, **kwargs):
758
+ if {"left", "right", "bottom"}.issubset(kwargs.keys()):
759
+ x, y = self._lbr_to_xy(kwargs.pop('left'), kwargs.pop('right'), kwargs.pop('bottom'))
760
+ kwargs['x'] = x
761
+ kwargs['y'] = y
762
+ return super().tricontour( **kwargs)
763
+
764
+
765
+ def tricontourf(self, **kwargs):
766
+ if {"left", "right", "bottom"}.issubset(kwargs.keys()):
767
+ x, y = self._lbr_to_xy(kwargs.pop('left'), kwargs.pop('right'), kwargs.pop('bottom'))
768
+ kwargs['x'] = x
769
+ kwargs['y'] = y
770
+
771
+ return super().tricontourf(**kwargs)
772
+ # else:
773
+ # raise ValueError("scatter() needs either (a,b,c) or (x,y) inputs")