jarvisplot 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.

Potentially problematic release.


This version of jarvisplot might be problematic. Click here for more details.

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.0.dist-info/METADATA +93 -0
  39. jarvisplot-1.0.0.dist-info/RECORD +42 -0
  40. jarvisplot-1.0.0.dist-info/WHEEL +5 -0
  41. jarvisplot-1.0.0.dist-info/entry_points.txt +2 -0
  42. jarvisplot-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1573 @@
1
+ #!/usr/bin/env python3
2
+ from copy import deepcopy
3
+ from typing import Optional, Mapping
4
+ import numpy as np
5
+ import os, sys
6
+ from ..core import jppwd
7
+ import matplotlib.ticker as mticker
8
+ import pandas as pd
9
+ import matplotlib as mpl
10
+ from types import MethodType
11
+ from .adapters import StdAxesAdapter, TernaryAxesAdapter
12
+
13
+ import json
14
+ import time
15
+ import re
16
+
17
+
18
+ class Figure:
19
+ def _is_numbered_ax(self, name: str) -> bool:
20
+ """Return True iff name matches ax<NUMBER>, e.g. ax1, ax2, ax10."""
21
+ return isinstance(name, str) and re.fullmatch(r"ax\d+", name) is not None
22
+
23
+ def _ensure_numbered_rect_axes(self, ax_name: str, kwgs: dict):
24
+ """Create/configure a numbered rectangular axes (ax1, ax2, ...) using ax-style logic."""
25
+ if not self._is_numbered_ax(ax_name):
26
+ raise ValueError(f"Illegal dynamic axes name '{ax_name}'. Only ax<NUMBER> is allowed.")
27
+
28
+ # Reuse ax-style construction, but read frame config from frame[ax_name]
29
+ if ax_name not in self.axes.keys():
30
+ raw_ax = self.fig.add_axes(**kwgs)
31
+ if isinstance(kwgs, dict) and ("facecolor" in kwgs):
32
+ raw_ax.set_facecolor(kwgs['facecolor'])
33
+ adapter = StdAxesAdapter(raw_ax)
34
+ adapter._type = "rect"
35
+ adapter.layers = []
36
+ adapter._legend = self.frame.get(ax_name, {}).get("legend", False)
37
+ self.axes[ax_name] = adapter
38
+ adapter.status = 'configured'
39
+
40
+ ax_obj = self.axes[ax_name]
41
+
42
+ # ---- identical configuration path as ax, but keyed by ax_name ----
43
+ if self.frame.get(ax_name, {}).get("spines"):
44
+ if "color" in self.frame[ax_name]["spines"]:
45
+ for s in ax_obj.spines.values():
46
+ s.set_color(self.frame[ax_name]['spines']['color'])
47
+
48
+ # y scale
49
+ if self.frame.get(ax_name, {}).get("yscale", "").lower() == 'log':
50
+ ax_obj.set_yscale("log")
51
+ from matplotlib.ticker import LogLocator
52
+ ax_obj.yaxis.set_minor_locator(LogLocator(subs='auto'))
53
+ else:
54
+ from matplotlib.ticker import AutoMinorLocator
55
+ ax_obj.yaxis.set_minor_locator(AutoMinorLocator())
56
+
57
+ # x scale
58
+ if self.frame.get(ax_name, {}).get("xscale", "").lower() == 'log':
59
+ ax_obj.set_xscale("log")
60
+ from matplotlib.ticker import LogLocator
61
+ ax_obj.xaxis.set_minor_locator(LogLocator(subs='auto'))
62
+ else:
63
+ from matplotlib.ticker import AutoMinorLocator
64
+ ax_obj.xaxis.set_minor_locator(AutoMinorLocator())
65
+
66
+ def _safe_cast(v):
67
+ try:
68
+ return float(v)
69
+ except Exception:
70
+ return v
71
+
72
+ # text
73
+ if self.frame.get(ax_name, {}).get("text"):
74
+ for txt in self.frame[ax_name]["text"]:
75
+ if txt.get("transform", False):
76
+ txt.pop("transform")
77
+ ax_obj.text(**txt, transform=ax_obj.transAxes)
78
+ else:
79
+ ax_obj.text(**txt)
80
+
81
+ # limits
82
+ xlim = self.frame.get(ax_name, {}).get("xlim")
83
+ if xlim:
84
+ ax_obj.set_xlim(list(map(_safe_cast, xlim)))
85
+
86
+ ylim = self.frame.get(ax_name, {}).get("ylim")
87
+ if ylim:
88
+ ax_obj.set_ylim(list(map(_safe_cast, ylim)))
89
+
90
+ # labels
91
+ if self.frame.get(ax_name, {}).get('labels', {}).get("x"):
92
+ ax_obj.set_xlabel(self.frame[ax_name]['labels']['x'], **self.frame[ax_name]['labels']['xlabel'])
93
+ if self.frame.get(ax_name, {}).get('labels', {}).get("y"):
94
+ ax_obj.set_ylabel(self.frame[ax_name]['labels']['y'], **self.frame[ax_name]['labels']['ylabel'])
95
+ # ax_obj.yaxis.set_label_coords(
96
+ # self.frame[ax_name]['labels']['ylabel_coords']['x'],
97
+ # self.frame[ax_name]['labels']['ylabel_coords']['y']
98
+ # )
99
+
100
+ # ticks
101
+ ax_obj.tick_params(**self.frame.get(ax_name, {}).get('ticks', {}).get("both", {}))
102
+ ax_obj.tick_params(**self.frame.get(ax_name, {}).get('ticks', {}).get("major", {}))
103
+ ax_obj.tick_params(**self.frame.get(ax_name, {}).get('ticks', {}).get("minor", {}))
104
+
105
+ self._apply_axis_endpoints(ax_obj, self.frame.get(ax_name, {}).get('xaxis', {}), "x")
106
+ self._apply_axis_endpoints(ax_obj, self.frame.get(ax_name, {}).get('yaxis', {}), "y")
107
+
108
+ # finalize
109
+ if getattr(ax_obj, 'needs_finalize', True) and hasattr(ax_obj, 'finalize'):
110
+ try:
111
+ ax_obj.finalize()
112
+ except Exception as e:
113
+ if self.logger:
114
+ self.logger.warning(f"Finalize failed on axes '{ax_name}': {e}")
115
+
116
+ try:
117
+ self.logger.debug(f"Loaded numbered rectangle axes -> {ax_name}")
118
+ except Exception:
119
+ pass
120
+
121
+ return ax_obj
122
+ def _has_manual_ticks(self, ax_key: str, which: str) -> bool:
123
+ """Return True if YAML provides manual tick positions for given axis."""
124
+ try:
125
+ if ax_key == 'ax':
126
+ ticks_cfg = self.frame.get('ax', {}).get('ticks', {})
127
+ elif ax_key == 'axc':
128
+ ticks_cfg = self.frame.get('axc', {}).get('ticks', {})
129
+ else:
130
+ return False
131
+ node = ticks_cfg.get(which, {})
132
+ return isinstance(node, dict) and ((node.get('positions') is not None) or (node.get('pos') is not None))
133
+ except Exception:
134
+ return False
135
+
136
+ def _apply_axis_endpoints(self, ax_obj, axis_cfg: dict, which: str):
137
+ """
138
+ which: 'x' or 'y'
139
+ axis_cfg: self.frame['ax'].get('xaxis', {}) / 'yaxis'
140
+ """
141
+ if not isinstance(axis_cfg, dict):
142
+ return
143
+
144
+ # Resolve underlying Matplotlib Axes (StdAxesAdapter or plain Axes)
145
+ target = ax_obj.ax if hasattr(ax_obj, "ax") else ax_obj
146
+
147
+ if which == 'x':
148
+ ticks = target.xaxis.get_major_ticks()
149
+ locs = target.xaxis.get_majorticklocs()
150
+ else:
151
+ ticks = target.yaxis.get_major_ticks()
152
+ locs = target.yaxis.get_majorticklocs()
153
+ if not ticks:
154
+ return
155
+
156
+ # Get axis limits for boundary check
157
+ if which == 'x':
158
+ lim0, lim1 = target.get_xlim()
159
+ else:
160
+ lim0, lim1 = target.get_ylim()
161
+
162
+ min_cfg = axis_cfg.get("min_endpoints", {})
163
+ max_cfg = axis_cfg.get("max_endpoints", {})
164
+ width = abs(lim0 - lim1)
165
+
166
+ # 第一个 tick = min 端点
167
+ t0 = ticks[0]
168
+ # Check whether first tick is at the lower boundary
169
+ t0_loc = locs[0]
170
+
171
+ if abs(t0_loc - lim0) < 1e-3 * width:
172
+ if min_cfg.get("tick") is False:
173
+ t0.tick1line.set_visible(False)
174
+ t0.tick2line.set_visible(False)
175
+ if min_cfg.get("label") is False:
176
+ t0.label1.set_visible(False)
177
+ t0.label2.set_visible(False)
178
+
179
+ # 最后一个 tick = max 端点
180
+ t1 = ticks[-1]
181
+ # Check whether last tick is at the upper boundary
182
+ t1_loc = locs[-1]
183
+
184
+ if abs(t1_loc - lim1) < 1e-3 * width:
185
+ if max_cfg.get("tick") is False:
186
+ t1.tick1line.set_visible(False)
187
+ t1.tick2line.set_visible(False)
188
+ if max_cfg.get("label") is False:
189
+ t1.label1.set_visible(False)
190
+ t1.label2.set_visible(False)
191
+
192
+ def _apply_auto_ticks(self, ax_obj, which: str):
193
+ """Lightweight auto-tick post-processing at finalize stage.
194
+
195
+ Goals:
196
+ - Never print/debug here.
197
+ - X: rotate long labels a bit.
198
+ - Y: if log-like scale -> keep log spacing and use compact decimals for decades in [1e-2, 1e2],
199
+ otherwise defer to Matplotlib's LogFormatter.
200
+ if linear scale -> use ScalarFormatter with sci notation, but never touch log formatters.
201
+ """
202
+ target = ax_obj.ax if hasattr(ax_obj, "ax") else ax_obj
203
+ axis = target.xaxis if which == 'x' else target.yaxis
204
+
205
+ try:
206
+ # Ensure ticks/labels exist
207
+ labels = axis.get_ticklabels()
208
+
209
+ # --- X axis formatting (match Y axis behavior)
210
+ if which == 'x':
211
+ try:
212
+ xscale = target.get_xscale()
213
+ except Exception:
214
+ xscale = None
215
+
216
+ # only touch linear x-axis
217
+ if xscale not in ('log', 'symlog', 'logit'):
218
+ fmt = mticker.ScalarFormatter(useMathText=True)
219
+ # Do not use sci/offset for small exponents like 10^-1; reserve for <=1e-2 or >=1e4
220
+ fmt.set_powerlimits((-2, 4))
221
+ axis.set_major_formatter(fmt)
222
+ try:
223
+ target.ticklabel_format(style='sci', axis='x', scilimits=(-2, 4))
224
+ except Exception:
225
+ pass
226
+ try:
227
+ axis.set_offset_position('bottom')
228
+ except Exception:
229
+ pass
230
+ mpl.rcParams['axes.formatter.useoffset'] = True
231
+
232
+ # Ensure offset text is rendered and sized like tick labels (avoid huge ×10^n)
233
+ try:
234
+ target.figure.canvas.draw_idle()
235
+ tl = axis.get_ticklabels()
236
+ if tl:
237
+ axis.offsetText.set_fontsize(tl[0].get_size() * 0.8)
238
+ # Place the offset near the left of the x-axis (like y-axis), and nudge up/right
239
+ axis.offsetText.set_horizontalalignment('left')
240
+ # axis.offsetText.set_verticalalignment('top')
241
+ axis.offsetText.set_x(1.02) # axes fraction
242
+ # axis.offsetText.set_y(1.08) # small upward nudge (axes fraction)
243
+
244
+ except Exception:
245
+ pass
246
+
247
+ # optional: rotate long labels
248
+ try:
249
+ long = any((t is not None) and (len(t) > 6) for t in (l.get_text() for l in labels))
250
+ except Exception:
251
+ long = False
252
+ # if long:
253
+ # target.tick_params(axis='x', labelrotation=35)
254
+
255
+ return
256
+
257
+ # --- Y axis formatting
258
+ if which != 'y':
259
+ return
260
+
261
+ # Detect scale
262
+ try:
263
+ yscale = target.get_yscale()
264
+ except Exception:
265
+ yscale = None
266
+
267
+ # 1) Log-like y-axis: compact decimals in range, otherwise default log formatter
268
+ if yscale in ('log', 'symlog', 'logit'):
269
+ from matplotlib.ticker import LogFormatterMathtext, FuncFormatter
270
+
271
+ base = LogFormatterMathtext()
272
+ lo, hi = 1e-2, 1e2 # compact decimal range
273
+
274
+ def _fmt(val, pos=None):
275
+ if val is None or val <= 0:
276
+ return ""
277
+ try:
278
+ exp = np.log10(val)
279
+ except Exception:
280
+ return ""
281
+ # Only label exact decades
282
+ if (not np.isfinite(exp)) or (not np.isclose(exp, round(exp))):
283
+ return ""
284
+
285
+ if lo <= val <= hi:
286
+ e = int(round(exp))
287
+ if e >= 0:
288
+ return f"{int(10**e)}"
289
+ # e=-1 -> 0.1 (1 dp), e=-2 -> 0.01 (2 dp)
290
+ return f"{10**e:.{abs(e)}f}"
291
+
292
+ # outside compact range: defer to Matplotlib
293
+ return base(val, pos)
294
+
295
+ axis.set_major_formatter(FuncFormatter(_fmt))
296
+ return
297
+
298
+ # 2) Linear y-axis: ScalarFormatter sci notation
299
+ fmt = mticker.ScalarFormatter(useMathText=True)
300
+ # Do not use sci/offset for small exponents like 10^-1; reserve for <=1e-2 or >=1e4
301
+ fmt.set_powerlimits((-2, 4))
302
+ axis.set_major_formatter(fmt)
303
+ try:
304
+ target.ticklabel_format(style='sci', axis='y', scilimits=(-2, 4))
305
+ except Exception:
306
+ pass
307
+ try:
308
+ axis.set_offset_position('left')
309
+ except Exception:
310
+ pass
311
+ mpl.rcParams['axes.formatter.useoffset'] = True
312
+ try:
313
+ target.figure.canvas.draw_idle()
314
+ # shrink offset text a bit
315
+ try:
316
+ tl = axis.get_ticklabels()
317
+ if tl:
318
+ axis.offsetText.set_fontsize(tl[0].get_size() * 0.8)
319
+ except Exception:
320
+ pass
321
+ except Exception:
322
+ pass
323
+
324
+ except Exception:
325
+ # Always fail silently here; tick post-processing must never crash plotting.
326
+ return
327
+
328
+ def __init__(self, info: Optional[Mapping] = None):
329
+ self.t0 = time.perf_counter()
330
+
331
+ # internal state
332
+ self._name: Optional[str] = None
333
+ self._jpstyles: Optional[dict] = None
334
+ self._style: Optional[dict] = {}
335
+ self.print = False
336
+ self.mode = "Jarvis"
337
+ # self._jpdatas: Optional[list] = []
338
+ self._logger = None
339
+ self._frame = {}
340
+ self._outinfo = {}
341
+ self._yaml_dir = None # directory of the active YAML file (used to resolve relative paths)
342
+ self.axes = {}
343
+ self.debug = False
344
+ # self._axtri = None
345
+ self._layers = {}
346
+ self._ctx = None
347
+ # allow optional initialization from a dict
348
+ if info:
349
+ self.from_dict(info)
350
+
351
+ # --- name property ---
352
+ @property
353
+ def name(self) -> Optional[str]:
354
+ return self._name
355
+
356
+ @name.setter
357
+ def name(self, value: Optional[str]) -> None:
358
+ """Set figure name as a string (or None)."""
359
+ if value is None:
360
+ self._name = None
361
+ return
362
+ if not isinstance(value, str):
363
+ raise TypeError("Figure.name must be a string or None")
364
+ self._name = value
365
+
366
+ @property
367
+ def config(self):
368
+ return None
369
+
370
+ @config.setter
371
+ def config(self, infos):
372
+ self.dir = self.load_path(infos['output'].get("dir", "."), base_dir=self._yaml_dir)
373
+ if not os.path.exists(self.dir):
374
+ os.makedirs(self.dir)
375
+ self.fmts = infos['output'].get("formats", ['png'])
376
+ self.dpi = infos['output'].get('dpi', 600)
377
+
378
+ @property
379
+ def jpstyles(self) -> Optional[dict]:
380
+ return self._jpstyles
381
+
382
+ @jpstyles.setter
383
+ def jpstyles(self, value) -> None:
384
+ self._jpstyles = value
385
+
386
+ @property
387
+ def logger(self):
388
+ return self._logger
389
+
390
+ @logger.setter
391
+ def logger(self, value):
392
+ if value is not None:
393
+ self._logger = value
394
+
395
+ @property
396
+ def frame(self):
397
+ return self._frame
398
+
399
+ @frame.setter
400
+ def frame(self, value) -> None:
401
+ if self._frame is None:
402
+ self._frame = value
403
+ else:
404
+ from deepmerge import always_merger
405
+ self._frame = always_merger.merge(self._frame, value)
406
+
407
+ @property
408
+ def style(self) -> Optional[dict]:
409
+ return self._style
410
+
411
+
412
+ @style.setter
413
+ def style(self, value) -> None:
414
+ from copy import deepcopy
415
+ if len(value) == 2:
416
+ self._frame = deepcopy(self.jpstyles[value[0]][value[1]]['Frame'])
417
+ self._style = deepcopy(self.jpstyles[value[0]][value[1]]['Style'])
418
+ self.logger.debug("Style: [{} : {}] used for figure -> {}".format(value[0], value[1], self.name))
419
+ elif len(value) == 1:
420
+ self._frame = deepcopy(self.jpstyles[value[0]]["default"]['Frame'])
421
+ self._style = deepcopy(self.jpstyles[value[0]]["default"]['Style'])
422
+ self.logger.debug("Style: [{} : {}] used for figure -> {}".format(value[0], "default", self.name))
423
+ else:
424
+ self.logger.error("Undefined style -> {}".format(value))
425
+ raise TypeError
426
+
427
+ @property
428
+ def context(self):
429
+ return self._ctx
430
+
431
+ @context.setter
432
+ def context(self, value):
433
+ self._ctx = value # 期望是 DataContext
434
+
435
+
436
+
437
+
438
+ @property
439
+ def layers(self):
440
+ return self._layers
441
+
442
+ @layers.setter
443
+ def layers(self, infos):
444
+ for layer in infos:
445
+ info = {}
446
+ ax = self.axes[layer['axes']]
447
+ info['name'] = layer.get("name", "")
448
+ info['data'] = self.load_layer_data(layer)
449
+ info['combine'] = layer.get("combine", "concat")
450
+ if layer.get("share_data") and info['data'] is not None:
451
+ from copy import deepcopy
452
+ self.context.update(layer["share_data"], deepcopy(info['data']))
453
+ info['coor'] = layer['coordinates']
454
+ info['method'] = layer.get("method", "scatter")
455
+ info['style'] = layer.get("style", {})
456
+ ax.layers.append(info)
457
+ self.logger.debug("Successfully loaded layer -> {}".format(info["name"]))
458
+
459
+ def load_layer_data(self, layer):
460
+ lyinfo = layer.get("data", False)
461
+ lycomb = layer.get("combine", "concat")
462
+ if lyinfo:
463
+ if lycomb == "concat":
464
+ dts = []
465
+ for ds in lyinfo:
466
+ src = ds.get('source')
467
+ self.logger.debug("Loading layer data source -> {}".format(src))
468
+ if src and self.context:
469
+ from copy import deepcopy
470
+ if isinstance(src, (list, tuple)):
471
+ self.logger.debug("loading datasets in list mode")
472
+ dsrc = []
473
+ for srcitem in src:
474
+ self.logger.debug("loading layer data source item -> {}".format(srcitem))
475
+ dt = deepcopy(self.context.get(srcitem))
476
+ dsrc.append(dt)
477
+ dfsrcs = pd.concat(dsrc, ignore_index=False)
478
+ dt = self.load_bool_df(dfsrcs, ds.get("transform", None))
479
+ dts.append(dt)
480
+ elif self.context.get(src) is not None:
481
+ dt = deepcopy(self.context.get(src))
482
+ dt = self.load_bool_df(dt, ds.get("transform", None))
483
+ dts.append(dt)
484
+ else:
485
+ self.logger.error("DataSet -> {} not specified".format(src))
486
+ if len(dts) == 0:
487
+ return None
488
+ try:
489
+ return pd.concat(dts, ignore_index=False)
490
+ except Exception:
491
+ return dts[0]
492
+ elif lycomb == "seperate":
493
+ dts = {}
494
+ for ds in lyinfo:
495
+ src = ds.get("source")
496
+ label = ds.get("label")
497
+ self.logger.debug("Loading layer data source -> {}".format(src))
498
+ if src and self.context and self.context.get(src) is not None:
499
+ from copy import deepcopy
500
+ dt = deepcopy(self.context.get(src))
501
+ dt = self.load_bool_df(dt, ds.get("transform", None))
502
+ dts[label] = dt
503
+ else:
504
+ self.logger.error("DataSet -> {} not specified".format(src))
505
+ if len(dts) == 0:
506
+ return None
507
+ return dts
508
+ # Unsupported lyinfo shape -> no data
509
+ return None
510
+
511
+
512
+ def load_bool_df(self, df, transform):
513
+ if transform is None:
514
+ return df
515
+ elif not isinstance(transform, list):
516
+ self.logger.error("illegal transform format, list type needed ->".format(json.dump(transform)))
517
+ return df
518
+ else:
519
+ for trans in transform:
520
+ self.logger.debug("Applying the transform ... ")
521
+ if "filter" in trans.keys():
522
+ self.logger.debug("Before filtering -> {}".format(df.shape))
523
+ from .load_data import filter
524
+ df = filter(df, trans['filter'], self.logger)
525
+ self.logger.debug("After filtering -> {}".format(df.shape))
526
+ elif "profile" in trans.keys():
527
+ from .load_data import profiling
528
+ df = profiling(df, trans['profile'], self.logger)
529
+ self.logger.debug("After profiling -> {}".format(df.shape))
530
+ elif "sortby" in trans.keys():
531
+ from .load_data import sortby
532
+ df = sortby(df, trans['sortby'], self.logger)
533
+ self.logger.debug("After sortby -> {}".format(df.shape))
534
+ elif "add_column" in trans.keys():
535
+ from .load_data import addcolumn
536
+ df = addcolumn(df, trans['add_column'], self.logger)
537
+ self.logger.debug("After Add-column -> {}".format(df.shape))
538
+
539
+ return df
540
+
541
+
542
+
543
+ @property
544
+ def axlogo(self):
545
+ return self.axes['axlogo']
546
+
547
+ @axlogo.setter
548
+ def axlogo(self, kwgs):
549
+ if "axlogo" not in self.axes.keys():
550
+ axtp = self.fig.add_axes(**kwgs)
551
+ axtp.set_zorder(200)
552
+ axtp.patch.set_alpha(0)
553
+
554
+ self.axes['axlogo'] = axtp
555
+ self.axes['axlogo'].needs_finalize = False
556
+ self.axes['axlogo'].status = 'finalized'
557
+
558
+ self.axlogo.layers = []
559
+ jhlogo = self.load_path(self.frame['axlogo']['file'])
560
+ from PIL import Image
561
+ with Image.open(jhlogo) as image:
562
+ arr = np.asarray(image.convert("RGBA"))
563
+ self.axlogo.imshow(arr)
564
+ if self.frame['axlogo'].get("text"):
565
+ for txt in self.frame['axlogo']['text']:
566
+ self.axlogo.text(**txt, transform=self.axlogo.transAxes)
567
+ # else:
568
+ # self.axlogo.text(1., 0., "Jarvis-HEP", ha="left", va='bottom', color="black", fontfamily="Fira code", fontsize="x-small", fontstyle="normal", fontweight="bold", transform=self.axlogo.transAxes)
569
+ # self.axlogo.text(1., 0.9, " Powered by", ha="left", va='top', color="black", fontfamily="Fira code", fontsize="xx-small", fontstyle="normal", fontweight="normal", transform=self.axlogo.transAxes)
570
+
571
+ @property
572
+ def axtri(self):
573
+ return self.axes['axtri']
574
+
575
+ @axtri.setter
576
+ def axtri(self, kwgs):
577
+ if "axtri" not in self.axes.keys():
578
+ facecolor = kwgs.pop("facecolor", None)
579
+ raw_ax = self.fig.add_axes(**kwgs)
580
+ # Booking Ternary Plot Clip_path
581
+ from matplotlib.path import Path
582
+ vertices = [(0.0, 0.0), (1.0, 0.0), (0.5, 1.0), (0.0, 0.0)]
583
+ codes = [Path.MOVETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY]
584
+ raw_ax._clip_path = Path(vertices, codes)
585
+ self._install_tri_auto_clip(raw_ax)
586
+
587
+ # Keep rect axes patch transparent; ternary background is handled by adapter.
588
+ raw_ax.patch.set_alpha(0)
589
+
590
+ adapter = TernaryAxesAdapter(
591
+ raw_ax,
592
+ defaults={"facecolor": facecolor} if facecolor is not None else None,
593
+ clip_path=Path(vertices, codes) # 用 path 做 clip,transform 使用 ax.transData 已在适配器里处理
594
+ )
595
+ adapter._type = 'tri'
596
+ adapter._legend = False
597
+ adapter.layers = []
598
+ adapter.status = 'configured'
599
+ self.axes["axtri"] = adapter
600
+
601
+ self.axtri.plot(
602
+ x=[0.5, 1.0, 0.5, 0.0, 0.5],
603
+ y=[0.0, 0.0, 1.0, 0.0, 0.0],
604
+ **self.frame['axtri']['frame'])
605
+
606
+ arr = np.arange(0., 1.0, self.frame['axtri']['grid']['sep'])
607
+ seps = np.empty(len(arr) * 2 - 1)
608
+ seps[0::2] = arr
609
+ seps[1::2] = np.nan
610
+
611
+ x0 = seps
612
+ x1 = 0.5 * seps
613
+ x2 = 0.5 + 0.5 * seps
614
+ x3 = 1.0 - 0.5 * seps
615
+
616
+ y0 = 0.0 * seps
617
+ y1 = seps
618
+ y2 = 1 - seps
619
+
620
+ # Major ticks
621
+ arrt = np.arange(0., 1.0001, self.frame['axtri']['ticks']['majorsep'])
622
+ sept = np.empty(len(arrt) * 2 - 1)
623
+ sept[0::2] = arrt
624
+ sept[1::2] = np.nan
625
+ matl = self.frame['axtri']['ticks']['majorlength']
626
+
627
+ txb0 = sept
628
+ txb1 = sept - 0.5 * matl
629
+ tyb0 = 0.0 * sept
630
+ tyb1 = 0.0 * sept - matl
631
+
632
+ txl0 = 0.5 * sept
633
+ txl1 = 0.5 * sept - 0.5 * matl
634
+ tyl0 = sept
635
+ tyl1 = sept + matl
636
+
637
+ txr0 = 1.0 - 0.5 * sept
638
+ txr1 = 1.0 - 0.5 * sept + matl
639
+ tyr0 = sept
640
+ tyr1 = sept
641
+
642
+
643
+ # Minor ticks
644
+ arrm = np.arange(0., 1.0001, self.frame['axtri']['ticks']['minorsep'])
645
+ sepm = np.empty(len(arrm) * 2 - 1)
646
+ sepm[0::2] = arrm
647
+ sepm[1::2] = np.nan
648
+ matm = self.frame['axtri']['ticks']['minorlength']
649
+
650
+ mxb0 = sepm
651
+ mxb1 = sepm - 0.5 * matm
652
+ myb0 = 0.0 * sepm
653
+ myb1 = 0.0 * sepm - matm
654
+
655
+ mxl0 = 0.5 * sepm
656
+ mxl1 = 0.5 * sepm - 0.5 * matm
657
+ myl0 = sepm
658
+ myl1 = sepm + matm
659
+
660
+ mxr0 = 1.0 - 0.5 * sepm
661
+ mxr1 = 1.0 - 0.5 * sepm + matm
662
+ myr0 = sepm
663
+ myr1 = sepm
664
+
665
+ ticklabels = [f"{v*100:.0f}%" for v in arrt]
666
+ # Ticks label positions
667
+ lbx = arrt - 0.7 * matl
668
+ lby = 0.0 * arrt - 1.4 * matl
669
+
670
+ llx = (1.0 - arrt) / 2.0 - 1.5 * matl
671
+ lly = 1 - arrt + 2.0 * matl
672
+
673
+ lrx = 1.0 - arrt / 2.0 + 1.3 * matl
674
+ lry = arrt
675
+
676
+ # Bottom Axis
677
+ gridbx = np.array([val for pair in zip(x0, x1) for val in pair])
678
+ gridby = np.array([val for pair in zip(y0, y1) for val in pair])
679
+ tickbx = np.array([val for pair in zip(txb0, txb1) for val in pair])
680
+ tickby = np.array([val for pair in zip(tyb0, tyb1) for val in pair])
681
+ minorbx = np.array([val for pair in zip(mxb0, mxb1) for val in pair])
682
+ minorby = np.array([val for pair in zip(myb0, myb1) for val in pair])
683
+
684
+ # Left Axis
685
+ gridlx = np.array([val for pair in zip(x0, x2) for val in pair])
686
+ gridly = np.array([val for pair in zip(y0, y2) for val in pair])
687
+ ticklx = np.array([val for pair in zip(txl0, txl1) for val in pair])
688
+ tickly = np.array([val for pair in zip(tyl0, tyl1) for val in pair])
689
+ minorlx = np.array([val for pair in zip(mxl0, mxl1) for val in pair])
690
+ minorly = np.array([val for pair in zip(myl0, myl1) for val in pair])
691
+
692
+ # Right Axis
693
+ gridrx = np.array([val for pair in zip(x1, x3) for val in pair])
694
+ gridry = np.array([val for pair in zip(y1, y1) for val in pair])
695
+ tickrx = np.array([val for pair in zip(txr0, txr1) for val in pair])
696
+ tickry = np.array([val for pair in zip(tyr0, tyr1) for val in pair])
697
+ minorrx = np.array([val for pair in zip(mxr0, mxr1) for val in pair])
698
+ minorry = np.array([val for pair in zip(myr0, myr1) for val in pair])
699
+
700
+ # Grids // Major Ticks // Minor Ticks // Tick Lables
701
+ # Bottom Axis
702
+
703
+ self.axtri.plot(x=gridbx, y=gridby, **self.frame['axtri']['grid']['style'])
704
+ self.axtri.plot(x=tickbx, y=tickby, **self.frame['axtri']['ticks']['majorstyle'])
705
+ self.axtri.plot(x=minorbx, y=minorby, **self.frame['axtri']['ticks']['minorstyle'])
706
+ self.axtri.text(s=self.frame['axtri']['labels']['bottom'], **self.frame['axtri']['labels']['bottomstyle'])
707
+ for x, y, label in zip(lbx, lby, ticklabels):
708
+ self.axtri.text(x, y, label, **self.frame['axtri']['ticks']['bottomticklables'])
709
+
710
+ # Right Axis
711
+ self.axtri.plot(x=gridrx, y=gridry, **self.frame['axtri']['grid']['style'])
712
+ self.axtri.plot(x=tickrx, y=tickry, **self.frame['axtri']['ticks']['majorstyle'])
713
+ self.axtri.plot(x=minorrx, y=minorry, **self.frame['axtri']['ticks']['minorstyle'])
714
+ self.axtri.text(s=self.frame['axtri']['labels']['right'], **self.frame['axtri']['labels']['rightstyle'])
715
+ for x, y, label in zip(lrx, lry, ticklabels):
716
+ self.axtri.text(x, y, label, **self.frame['axtri']['ticks']['rightticklables'])
717
+
718
+ # Left Axis
719
+ self.axtri.plot(x=gridlx, y=gridly, **self.frame['axtri']['grid']['style'])
720
+ self.axtri.plot(x=ticklx, y=tickly, **self.frame['axtri']['ticks']['majorstyle'])
721
+ self.axtri.plot(x=minorlx, y=minorly, **self.frame['axtri']['ticks']['minorstyle'])
722
+ self.axtri.text(s=self.frame['axtri']['labels']['left'], **self.frame['axtri']['labels']['leftstyle'])
723
+ for x, y, label in zip(llx, lly, ticklabels):
724
+ self.axtri.text(x, y, label, **self.frame['axtri']['ticks']['leftticklables'])
725
+
726
+ if self.debug:
727
+ self.axtri.scatter(x=lbx, y=lby, s=1.0, marker='.', c="#FF42A1", clip_on=False)
728
+ self.axtri.scatter(x=lrx, y=lry, s=1.0, marker='.', c="#FF42A1", clip_on=False)
729
+ self.axtri.scatter(x=llx, y=lly, s=1.0, marker='.', c="#FF42A1", clip_on=False)
730
+ self.axtri.plot(x=[0., 0.75, 0.84], y=[0., 0.5, 0.56], marker=".", linestyle="-", lw=0.3, markersize=1, c="#FF42A1", clip_on=False)
731
+ self.axtri.plot(x=[0.5, 0.5, 0.5], y=[1., 0.0, -0.12], marker=".", linestyle="-", lw=0.3, markersize=1, c="#FF42A1", clip_on=False)
732
+ self.axtri.plot(x=[1., 0.25, 0.16], y=[0., 0.5, 0.56], marker=".", linestyle="-", lw=0.3, markersize=1, c="#FF42A1", clip_on=False)
733
+
734
+ @property
735
+ def axc(self):
736
+ return self.axes['axc']
737
+
738
+ @axc.setter
739
+ def axc(self, kwgs):
740
+ if "axc" not in self.axes.keys():
741
+ axc = self.fig.add_axes(**kwgs)
742
+ axc._cb = {
743
+ "mode": "auto", # auto|linear|log|diverging
744
+ "levels": None,
745
+ "label": None,
746
+ "vmin": None, "vmax": None,
747
+ "norm": None,
748
+ "used": False
749
+ }
750
+ self.axes["axc"] = axc
751
+ else:
752
+ if not self.axc._cb.get("used"):
753
+ return
754
+
755
+ # Build mappable for the colorbar
756
+ mappable = mpl.cm.ScalarMappable(
757
+ cmap=self.axc._cb.get("cmap") or mpl.rcParams.get("image.cmap", "rainbow"),
758
+ norm=self.axc._cb.get("norm")
759
+ )
760
+ mappable.set_array([])
761
+ if self.frame['axc'].get('orientation') != "horizontal":
762
+ cbar = self.fig.colorbar(mappable, cax=self.axc)
763
+ cbar.minorticks_on()
764
+ self.axc.set_ylim(self.axc._cb['vmin'], self.axc._cb['vmax'])
765
+
766
+ if str(self.axc._cb.get('mode', 'auto')).lower() == 'log':
767
+ from matplotlib.ticker import LogLocator
768
+ # Use default subs for log scale minor ticks
769
+ self.axc.yaxis.set_minor_locator(LogLocator(subs='auto'))
770
+ else:
771
+ from matplotlib.ticker import AutoMinorLocator
772
+ self.axc.yaxis.set_minor_locator(AutoMinorLocator())
773
+
774
+ self.axc.yaxis.set_ticks_position(self.frame['axc']['ticks']['ticks_position'])
775
+ self.axc.yaxis.set_label_position("right")
776
+
777
+ # Apply tick params (major/minor) as provided in frame config
778
+ self.axc.tick_params(**self.frame['axc']['ticks'].get('both', {}))
779
+ self.axc.tick_params(**self.frame['axc']['ticks'].get('major', {}))
780
+ self.axc.tick_params(**self.frame['axc']['ticks'].get('minor', {}))
781
+
782
+ self.axc.yaxis.set_ticks_position(self.frame['axc']['ticks']['ticks_position'])
783
+ self.axc.yaxis.set_label_position("right")
784
+
785
+ # Apply tick params (major/minor) as provided in frame config
786
+ self.axc.tick_params(**self.frame['axc']['ticks'].get('both', {}))
787
+ self.axc.tick_params(**self.frame['axc']['ticks'].get('major', {}))
788
+ self.axc.tick_params(**self.frame['axc']['ticks'].get('minor', {}))
789
+ self.axc.set_ylabel(**self.frame['axc'].get('label', {}))
790
+ if self.frame['axc'].get('ylabel_coords'):
791
+ self.axc.yaxis.set_label_coords(self.frame['axc']['ylabel_coords']['x'], self.frame['axc']['ylabel_coords']['y'])
792
+ # Apply manual ticks for colorbar (y-axis) at initialization if provided
793
+ cbar_ticks_cfg = self.frame.get('axc', {}).get('ticks', {}).get('y', {})
794
+ # self._apply_manual_ticks(self.axc, 'y', cbar_ticks_cfg)
795
+ self.logger.debug("Loaded colorbar axes -> axc")
796
+ # else:
797
+ # cbar = self.fig.colorbar(mappable, cax=self.axc, orientation="horizontal")
798
+ # cbar.minorticks_on()
799
+ # self.axc.xaxis.set_ticks_position(self.frame['axc']['ticks']['ticks_position'])
800
+ # self.axc.xaxis.set_label_position("top")
801
+ # self.axc.set_xlim(self.axc._cb['vmin'], self.axc._cb['vmax'])
802
+
803
+ # self.axc.set_xlabel(**self.frame['axc'].get("label", {}))
804
+
805
+ else:
806
+ # Horizontal colorbar: drive everything from x-axis
807
+ cbar = self.fig.colorbar(mappable, cax=self.axc, orientation="horizontal")
808
+ cbar.minorticks_on()
809
+
810
+ # Limits use xlim for horizontal bars
811
+ self.axc.set_xlim(self.axc._cb['vmin'], self.axc._cb['vmax'])
812
+
813
+ # Minor locator depends on mode
814
+ if str(self.axc._cb.get('mode', 'auto')).lower() == 'log':
815
+ from matplotlib.ticker import LogLocator
816
+ self.axc.xaxis.set_minor_locator(LogLocator(subs='auto'))
817
+ else:
818
+ from matplotlib.ticker import AutoMinorLocator
819
+ self.axc.xaxis.set_minor_locator(AutoMinorLocator())
820
+
821
+ # Ticks/label positions (top/bottom for horizontal)
822
+ _tp = self.frame.get('axc', {}).get('ticks', {}).get('ticks_position', 'top')
823
+ self.axc.xaxis.set_ticks_position(_tp)
824
+ self.axc.xaxis.set_label_position('top' if _tp == 'top' else 'bottom')
825
+
826
+ # Apply tick params (both/major/minor)
827
+ self.axc.tick_params(**self.frame.get('axc', {}).get('ticks', {}).get('both', {}))
828
+ self.axc.tick_params(**self.frame.get('axc', {}).get('ticks', {}).get('major', {}))
829
+ self.axc.tick_params(**self.frame.get('axc', {}).get('ticks', {}).get('minor', {}))
830
+ # Apply label if configured
831
+ if self.frame.get('axc', {}).get('isxlabel'):
832
+ self.axc.set_xlabel(**self.frame.get('axc', {}).get('label', {}))
833
+
834
+
835
+
836
+
837
+
838
+
839
+ @property
840
+ def ax(self):
841
+ return self.axes['ax']
842
+
843
+ @ax.setter
844
+ def ax(self, kwgs):
845
+ if "ax" not in self.axes.keys():
846
+ raw_ax = self.fig.add_axes(**kwgs)
847
+ if "facecolor" in kwgs.keys():
848
+ raw_ax.set_facecolor(kwgs['facecolor'])
849
+ adapter = StdAxesAdapter(raw_ax)
850
+ adapter._type = "rect"
851
+ adapter.layers = []
852
+ adapter._legend = self.frame['ax'].get("legend", False)
853
+ self.axes['ax'] = adapter
854
+ adapter.status = 'configured'
855
+
856
+ if self.frame['ax'].get("spines"):
857
+ if "color" in self.frame['ax']['spines']:
858
+ for s in self.axes['ax'].spines.values():
859
+ s.set_color(self.frame['ax']['spines']['color'])
860
+
861
+ if self.frame['ax'].get("yscale", "").lower() == 'log':
862
+ self.ax.set_yscale("log")
863
+ from matplotlib.ticker import LogLocator
864
+ self.ax.yaxis.set_minor_locator(LogLocator(subs='auto'))
865
+ else:
866
+ from matplotlib.ticker import AutoMinorLocator
867
+ self.ax.yaxis.set_minor_locator(AutoMinorLocator())
868
+
869
+ if self.frame['ax'].get("xscale", "").lower() == 'log':
870
+ self.ax.set_xscale("log")
871
+ from matplotlib.ticker import LogLocator
872
+ self.ax.xaxis.set_minor_locator(LogLocator(subs='auto'))
873
+ else:
874
+ from matplotlib.ticker import AutoMinorLocator
875
+ self.ax.xaxis.set_minor_locator(AutoMinorLocator())
876
+
877
+ def _safe_cast(v):
878
+ try:
879
+ return float(v)
880
+ except Exception:
881
+ return v
882
+
883
+ if self.frame["ax"].get("text"):
884
+ for txt in self.frame["ax"]["text"]:
885
+ if txt.get("transform", False):
886
+ txt.pop("transform")
887
+ self.ax.text(**txt, transform=self.ax.transAxes)
888
+ else:
889
+ self.ax.text(**txt)
890
+
891
+
892
+ xlim = self.frame["ax"].get("xlim")
893
+ if xlim:
894
+ xlim = list(map(_safe_cast, xlim))
895
+ self.ax.set_xlim(xlim)
896
+
897
+ ylim = self.frame["ax"].get("ylim")
898
+ if ylim:
899
+ ylim = list(map(_safe_cast, ylim))
900
+ self.ax.set_ylim(ylim)
901
+
902
+ if self.frame['ax']['labels'].get("x"):
903
+ self.ax.set_xlabel(self.frame['ax']['labels']['x'], **self.frame['ax']['labels']['xlabel'])
904
+ if self.frame['ax']['labels'].get("y"):
905
+ self.ax.set_ylabel(self.frame['ax']['labels']['y'], **self.frame['ax']['labels']['ylabel'])
906
+ self.ax.yaxis.set_label_coords(self.frame['ax']['labels']['ylabel_coords']['x'], self.frame['ax']['labels']['ylabel_coords']['y'])
907
+
908
+ if self.frame['ax']['labels'].get("zorder"):
909
+ for spine in self.ax.spines.values():
910
+ spine.set_zorder(self.frame['ax']['labels']['zorder'])
911
+
912
+ # Apply manual ticks here at initialization if provided in YAML
913
+ ax_ticks_cfg = self.frame.get('ax', {}).get('ticks', {})
914
+ # self._apply_manual_ticks(self.ax, "x", ax_ticks_cfg.get('x', {}))
915
+ # self._apply_manual_ticks(self.ax, "y", ax_ticks_cfg.get('y', {}))
916
+
917
+
918
+ self.ax.tick_params(**self.frame['ax']['ticks'].get("both", {}))
919
+ self.ax.tick_params(**self.frame['ax']['ticks'].get("major", {}))
920
+ self.ax.tick_params(**self.frame['ax']['ticks'].get("minor", {}))
921
+
922
+ self._apply_axis_endpoints(self.axes['ax'], self.frame['ax'].get('xaxis', {}), "x")
923
+ self._apply_axis_endpoints(self.axes['ax'], self.frame['ax'].get('yaxis', {}), "y")
924
+
925
+ # ---- Finalize logic with auto-ticks injection ----
926
+ if getattr(self.ax, 'needs_finalize', True) and hasattr(self.ax, 'finalize'):
927
+ orig_finalize = self.ax.finalize
928
+ def wrapped_finalize():
929
+ # try:
930
+ # if not self._has_manual_ticks('ax', 'x'):
931
+ # self._apply_auto_ticks(self.ax, 'x')
932
+ # if not self._has_manual_ticks('ax', 'y'):
933
+ # self._apply_auto_ticks(self.ax, 'y')
934
+ # except Exception as e:
935
+ # if self.logger:
936
+ # self.logger.warning(f"Auto ticks failed on ax: {e}")
937
+ try:
938
+ orig_finalize()
939
+ except Exception as e:
940
+ if self.logger:
941
+ self.logger.warning(f"Finalize failed on axes 'ax': {e}")
942
+ self.ax.finalize = wrapped_finalize
943
+ self.ax.finalize()
944
+
945
+ self.logger.debug("Loaded main rectangle axes -> ax")
946
+
947
+ def _apply_legend_on_axes(self, ax_name: str, ax_obj, leg_cfg: dict):
948
+ """Apply a legend on a specific axes using a YAML dict stored under frame['axes'][ax_name]['legend'].
949
+ Supports an optional 'enabled' key (default True). Any 'axes' key will be ignored here."""
950
+ if not isinstance(leg_cfg, dict):
951
+ return
952
+ if leg_cfg.get("enabled", True) is False:
953
+ return
954
+ kw = dict(leg_cfg)
955
+ kw.pop("axes", None) # per-axes legend doesn't need this
956
+ try:
957
+ (ax_obj.ax if hasattr(ax_obj, "ax") else ax_obj).legend(**kw)
958
+ except Exception as e:
959
+ if self.logger:
960
+ self.logger.warning(f"Legend apply failed on '{ax_name}': {e}")
961
+
962
+
963
+ def _install_tri_auto_clip(self, ax):
964
+ """
965
+ Install auto-clip wrappers on this Axes so that any newly created or
966
+ added artists are clipped to ax._clip_path (data coords) automatically.
967
+ This affects high-level draw calls (plot/scatter/contour/contourf/imshow)
968
+ and low-level add_* entry points (add_line/add_collection/add_patch/add_artist).
969
+ """
970
+ if not hasattr(ax, "_jp_orig"):
971
+ ax._jp_orig = {}
972
+
973
+ def _wrap_high_level(name):
974
+ if hasattr(ax, name) and name not in ax._jp_orig:
975
+ ax._jp_orig[name] = getattr(ax, name)
976
+ def wrapped(self_ax, *args, **kwargs):
977
+ out = ax._jp_orig[name](*args, **kwargs)
978
+ # Only auto-clip if a triangular clip path is defined
979
+ if getattr(self_ax, "_clip_path", None) is not None:
980
+ auto_clip(out, self_ax)
981
+ return out
982
+ setattr(ax, name, MethodType(wrapped, ax))
983
+
984
+ # Wrap common high-level APIs
985
+ for m in ("plot", "scatter", "contour", "contourf", "imshow"):
986
+ _wrap_high_level(m)
987
+
988
+ # Wrap low-level add_* so indirect additions are also clipped
989
+ def _wrap_add(name):
990
+ if hasattr(ax, name) and name not in ax._jp_orig:
991
+ ax._jp_orig[name] = getattr(ax, name)
992
+ def wrapped_add(self_ax, artist, *args, **kwargs):
993
+ if getattr(self_ax, "_clip_path", None) is not None:
994
+ try:
995
+ artist.set_clip_path(self_ax._clip_path, transform=self_ax.transData)
996
+ except Exception:
997
+ pass
998
+ return ax._jp_orig[name](artist, *args, **kwargs)
999
+ setattr(ax, name, MethodType(wrapped_add, ax))
1000
+
1001
+ for m in ("add_line", "add_collection", "add_patch", "add_artist"):
1002
+ _wrap_add(m)
1003
+
1004
+ def savefig(self):
1005
+ # self.ax.tight_layout()
1006
+ for fmt in self.fmts:
1007
+ spf = os.path.join(self.dir, "{}.{}".format(self.name, fmt))
1008
+ try:
1009
+ self.logger.warning(
1010
+ "JarvisPlot successfully draw {}\t in {:.3f}s sec\n\t-> {}".format(self.name, float(time.perf_counter() - self.t0), spf)
1011
+ )
1012
+ except:
1013
+ pass
1014
+ self.fig.savefig(spf, dpi=self.dpi)
1015
+
1016
+ def load_axes(self):
1017
+ for ax, kws in self.frame['axes'].items():
1018
+ try:
1019
+ self.logger.debug("Loading axes -> {}".format(ax))
1020
+ except Exception:
1021
+ pass
1022
+
1023
+ if ax == "axlogo":
1024
+ self.axlogo = kws
1025
+ elif ax == "axtri":
1026
+ self.axtri = kws
1027
+ elif ax == "axc":
1028
+ self.axc = kws
1029
+ elif ax == "ax":
1030
+ self.ax = kws
1031
+ elif self._is_numbered_ax(ax):
1032
+ self._ensure_numbered_rect_axes(ax, kws)
1033
+ else:
1034
+ try:
1035
+ self.logger.warning(f"Unsupported axes key '{ax}'. Only 'ax' or 'ax<NUMBER>' are allowed.")
1036
+ except Exception:
1037
+ pass
1038
+
1039
+ # import matplotlib.pyplot as plt
1040
+ # plt.show()
1041
+
1042
+ def plot(self):
1043
+ self.render()
1044
+ # for layer in self.layers:
1045
+ if self.debug:
1046
+ if "axtri" in self.axes.keys():
1047
+ # Demo of Scatter Clip
1048
+ x = np.linspace(-1, 2, 121)
1049
+ y = np.linspace(-1, 2, 121)
1050
+ X, Y = np.meshgrid(x, y)
1051
+ self.axtri.scatter(x=X.ravel() + 0.5 * Y.ravel(), y=Y.ravel(), marker='.', s=1, facecolor="#0277BA", edgecolor="None")
1052
+
1053
+ # Demo of Plot Clip
1054
+ self.axtri.plot(x=[-1, 0.5, 0.5, 2], y=[-1.1, 0.6, 0.3, 1.8], linestyle="-", color="#0277BA")
1055
+ self.savefig()
1056
+ import matplotlib.pyplot as plt
1057
+ plt.close(self.fig)
1058
+
1059
+ def render(self):
1060
+ """
1061
+ Render all layers attached to each axes (we appended them in axtri/axlogo setters).
1062
+ """
1063
+ for ax_name, ax in self.axes.items():
1064
+ ly_list = getattr(ax, "layers", [])
1065
+ for ly in ly_list:
1066
+ self.render_layer(ax, ly)
1067
+ # mark drawn after all layers on this axes
1068
+ if hasattr(ax, 'status'):
1069
+ ax.status = 'drawn'
1070
+
1071
+ for name, ax in self.axes.items():
1072
+ try:
1073
+ if hasattr(ax, "_legend") and ax._legend:
1074
+ target_ax = ax.ax if hasattr(ax, "ax") else ax
1075
+ target_ax.legend(**ax._legend)
1076
+ except Exception as e:
1077
+ if self.logger:
1078
+ self.logger.warning(f"Legend draw failed on axes '{name}': {e}")
1079
+
1080
+ # finalize colorbar lazily (only if any colored layer appeared)
1081
+ if "axc" in self.axes:
1082
+ self.axc = True
1083
+
1084
+ # ---- Finalize axes that want it ----
1085
+ for name, ax in self.axes.items():
1086
+ # Auto ticks only if user did not provide manual ticks for this axis
1087
+ if name == 'ax':
1088
+ if not self._has_manual_ticks('ax', 'x'):
1089
+ self._apply_auto_ticks(ax, 'x')
1090
+ if not self._has_manual_ticks('ax', 'y'):
1091
+ self._apply_auto_ticks(ax, 'y')
1092
+ elif name == 'axc':
1093
+ if not self._has_manual_ticks('axc', 'y'):
1094
+ self._apply_auto_ticks(ax, 'y')
1095
+ if getattr(ax, 'needs_finalize', True) and hasattr(ax, 'finalize'):
1096
+ try:
1097
+ ax.finalize()
1098
+ except Exception as e:
1099
+ if self.logger:
1100
+ self.logger.warning(f"Finalize failed on axes '{name}': {e}")
1101
+
1102
+ def _apply_manual_ticks(self, ax_obj, which: str, ticks_cfg: dict):
1103
+ """Apply manual ticks if YAML provides them; otherwise keep auto.
1104
+ YAML:
1105
+ frame.ax.ticks.x: { positions: [...], labels: [...] }
1106
+ frame.ax.ticks.y: { positions: [...], labels: [...] }
1107
+ frame.axc.ticks.y: { positions: [...], labels: [...] }
1108
+ """
1109
+ if not isinstance(ticks_cfg, dict):
1110
+ return
1111
+ pos = ticks_cfg.get("positions") or ticks_cfg.get("pos")
1112
+ labs = ticks_cfg.get("labels")
1113
+ if pos is None:
1114
+ return
1115
+ target = ax_obj.ax if hasattr(ax_obj, "ax") else ax_obj
1116
+ try:
1117
+ if which == "x":
1118
+ target.set_xticks(pos)
1119
+ if labs is not None:
1120
+ target.set_xticklabels(labs)
1121
+ elif which == "y":
1122
+ target.set_yticks(pos)
1123
+ if labs is not None:
1124
+ target.set_yticklabels(labs)
1125
+ except Exception as e:
1126
+ if self.logger:
1127
+ self.logger.warning(f"Manual ticks apply failed on {which}-axis: {e}")
1128
+
1129
+ # --- config ingestion ---
1130
+ def from_dict(self, info: Mapping) -> bool:
1131
+ """Apply settings from a dict. Returns True if any field was set.
1132
+ Expected keys (so far): 'name'.
1133
+ """
1134
+ if not isinstance(info, Mapping):
1135
+ raise TypeError("from_dict expects a mapping/dict")
1136
+
1137
+ try:
1138
+ changed = True
1139
+ if "name" in info:
1140
+ self.name = info["name"] # use the property setter correctly
1141
+ else:
1142
+ changed = False
1143
+
1144
+ # Base directory for resolving relative paths (e.g. output.dir, resources, etc.)
1145
+ # Core should pass one of these keys.
1146
+ if "yaml_dir" in info:
1147
+ self._yaml_dir = info.get("yaml_dir")
1148
+ elif "_yaml_dir" in info:
1149
+ self._yaml_dir = info.get("_yaml_dir")
1150
+ elif "yaml_path" in info:
1151
+ try:
1152
+ from pathlib import Path
1153
+ self._yaml_dir = str(Path(info.get("yaml_path")).expanduser().resolve().parent)
1154
+ except Exception:
1155
+ pass
1156
+
1157
+ if "debug" in info:
1158
+ self.debug = info['debug']
1159
+ try:
1160
+ self.logger.debug("Loading plot -> {} in debug mode".format(self.name))
1161
+ except:
1162
+ pass
1163
+ self._enable = info.get("enable", True)
1164
+ if not self._enable:
1165
+ self.logger.warning("Skip plot -> {}".format(self.name))
1166
+ return False
1167
+
1168
+ if "style" in info:
1169
+ self.style = info['style']
1170
+ else:
1171
+ self.style = ["a4paper_2x1", "default"]
1172
+ self.logger.debug("Figure style loaded")
1173
+ if "gambit" in info['style'][0]:
1174
+ self.mode = "gambit"
1175
+
1176
+
1177
+ if "frame" in info:
1178
+ self.frame = info['frame']
1179
+
1180
+ import matplotlib.pyplot as plt
1181
+ plt.rcParams['mathtext.fontset'] = 'stix'
1182
+ # --- Ensure JarvisPLOT colormaps are registered globally before plotting ---
1183
+ try:
1184
+ from ..utils import cmaps as _jp_cmaps
1185
+ _cmaps_summary = _jp_cmaps.setup(force=True)
1186
+ if self.logger:
1187
+ try:
1188
+ self.logger.debug(f"JarvisPLOT: colormaps registered (builtin/external): {_cmaps_summary}")
1189
+ self.logger.debug(f"JarvisPLOT: available cmaps sample: {_jp_cmaps.list_available()[:10]} ...")
1190
+ except:
1191
+ pass
1192
+ except Exception as _e:
1193
+ if self.logger:
1194
+ self.logger.warning(f"JarvisPLOT: failed to register colormaps: {_e}")
1195
+ # plt.rcParams['font.family'] = 'STIXGeneral'
1196
+ self.fig = plt.figure(**self.frame['figure'])
1197
+ # CLI override: disable logo panel
1198
+ if self.print:
1199
+ try:
1200
+ if self.mode == "Jarvis":
1201
+ if isinstance(self.frame.get("axes"), dict):
1202
+ self.frame["axes"].pop("axlogo", None)
1203
+ self.frame.pop("axlogo", None) # optional: drop logo content too
1204
+ except Exception:
1205
+ pass
1206
+ self.load_axes()
1207
+
1208
+ if "layers" in info:
1209
+ self.layers = info['layers']
1210
+ else:
1211
+ changed = False
1212
+
1213
+ return changed
1214
+ except:
1215
+ return False
1216
+
1217
+
1218
+ # Backward-compatible alias if other code still calls `set(info)`
1219
+
1220
+ def set(self, info: Mapping) -> bool:
1221
+ return self.from_dict(info)
1222
+
1223
+ def load_path(self, path, base_dir=None):
1224
+ """Resolve a path string.
1225
+
1226
+ Rules:
1227
+ - "&JP/..." is resolved relative to JarvisPLOT project root (jppwd).
1228
+ - Absolute paths are kept.
1229
+ - Relative paths are resolved relative to `base_dir` if provided, otherwise CWD.
1230
+ """
1231
+ path = str(path)
1232
+ if path.startswith("&JP/"):
1233
+ return os.path.abspath(os.path.join(jppwd, path[4:]))
1234
+
1235
+ from pathlib import Path
1236
+ p = Path(path).expanduser()
1237
+ if p.is_absolute():
1238
+ return str(p.resolve())
1239
+
1240
+ if base_dir:
1241
+ try:
1242
+ bd = Path(str(base_dir)).expanduser().resolve()
1243
+ return str((bd / p).resolve())
1244
+ except Exception:
1245
+ pass
1246
+
1247
+ return str(p.resolve())
1248
+
1249
+ # --- unified method dispatch ---
1250
+ METHOD_DISPATCH = {
1251
+ "scatter": "scatter",
1252
+ "plot": "plot",
1253
+ "fill": "fill",
1254
+ "contour": "contour",
1255
+ "contourf": "contourf",
1256
+ "imshow": "imshow",
1257
+ "hist": "hist",
1258
+ "hexbin": "hexbin",
1259
+ "tricontour": "tricontour",
1260
+ "tricontourf": "tricontourf",
1261
+ "voronoi": "voronoi",
1262
+ "voronoif": "voronoif"
1263
+ }
1264
+
1265
+ def _eval_series(self, df: pd.DataFrame, set: dict):
1266
+ """
1267
+ Evaluate an expression/column name against df safely.
1268
+ - If expr is a direct column name, returns that series.
1269
+ - If expr is a python expression, eval with df columns in scope.
1270
+ """
1271
+ try:
1272
+ self.logger.debug("Loading variable expression -> {}".format(set['expr']))
1273
+ except:
1274
+ pass
1275
+ if not "expr" in set.keys():
1276
+ raise ValueError(f"expr need for axes {set}.")
1277
+ if set["expr"] in df.columns:
1278
+ arr = df[set["expr"]].values
1279
+ if np.isnan(arr).sum() and "fillna" in set.keys():
1280
+ arr = np.where(np.isnan(arr), float(set['fillna']), arr)
1281
+ else:
1282
+ # safe-ish eval with only df columns in locals
1283
+ local_vars = df.to_dict("series")
1284
+ import math
1285
+ from ..inner_func import update_funcs
1286
+ allowed_globals = update_funcs({"np": np, "math": math})
1287
+ arr = eval(set["expr"], allowed_globals, local_vars)
1288
+ if np.isnan(arr).sum() and "fillna" in set.keys():
1289
+ arr = np.where(np.isnan(arr), float(set['fillna']), arr)
1290
+ return np.asarray(arr)
1291
+
1292
+ def _cb_collect_and_attach(self, style: dict, coor: dict, method_key: str, df: pd.DataFrame) -> dict:
1293
+ import matplotlib.colors as mcolors
1294
+ axc = self.axes.get("axc")
1295
+ if axc is None or not hasattr(axc, "_cb"):
1296
+ return style
1297
+
1298
+ s = dict(style)
1299
+ uses_color = bool(style.get("cmap")) or ("c" in coor)
1300
+ if not uses_color:
1301
+ return style
1302
+ self.axc._cb["cmap"] = s.get("cmap")
1303
+
1304
+ # ---- 1) records vmin and vmax ----
1305
+ z = None
1306
+ if self.axc._cb["vmin"] is None:
1307
+ if ("vmin" in s and isinstance(s['vmin'], (int, float))):
1308
+ self.axc._cb['vmin'] = s['vmin']
1309
+ else:
1310
+ if z is None:
1311
+ if "z" in coor and isinstance(coor["z"], dict) and "expr" in coor["z"]:
1312
+ z = self._eval_series(df, {"expr": coor["z"]["expr"]})
1313
+ elif "c" in coor and isinstance(coor["c"], dict) and "expr" in coor["c"]:
1314
+ z = self._eval_series(df, {"expr": coor["c"]["expr"]})
1315
+ if z is not None:
1316
+ z = z[np.isfinite(z)]
1317
+ if z.size:
1318
+ self.axc._cb["vmin"] = float(np.min(z))
1319
+
1320
+ if self.axc._cb["vmax"] is None:
1321
+ if ("vmax" in s and isinstance(s['vmax'], (int, float))):
1322
+ self.axc._cb['vmax'] = s['vmax']
1323
+ else:
1324
+ if z is None:
1325
+ if "z" in coor and isinstance(coor["z"], dict) and "expr" in coor["z"]:
1326
+ z = self._eval_series(df, {"expr": coor["z"]["expr"]})
1327
+ elif "c" in coor and isinstance(coor["c"], dict) and "expr" in coor["c"]:
1328
+ z = self._eval_series(df, {"expr": coor["c"]["expr"]})
1329
+ if z is not None:
1330
+ z = z[np.isfinite(z)]
1331
+ if z.size:
1332
+ self.axc._cb["vmax"] = float(np.max(z))
1333
+
1334
+ # ---- 2) Resolve/attach norm (priority: explicit 'norm' in style) ----
1335
+ if (self.axc._cb["vmin"] is not None) and (self.axc._cb["vmax"] is not None):
1336
+ vmin, vmax = self.axc._cb["vmin"], self.axc._cb["vmax"]
1337
+
1338
+ def _resolve_norm(nv, *, vmin=None, vmax=None):
1339
+ """Turn user-specified 'norm' into a matplotlib.colors.Normalize instance."""
1340
+ if nv is None:
1341
+ return None
1342
+ # Already a Normalize subclass
1343
+ if isinstance(nv, mcolors.Normalize):
1344
+ return nv
1345
+ # String shorthand, e.g. "LogNorm", "TwoSlopeNorm", "Normalize"
1346
+ if isinstance(nv, str):
1347
+ key = nv.strip().lower()
1348
+ if key in {"log", "lognorm"}:
1349
+ return mcolors.LogNorm(vmin=vmin, vmax=vmax)
1350
+ if key in {"twoslopenorm", "diverging"}:
1351
+ # default vcenter=0 for diverging data; user can override via dict form
1352
+ return mcolors.TwoSlopeNorm(vcenter=0.0, vmin=vmin, vmax=vmax)
1353
+ if key in {"norm", "normalize", "linear"}:
1354
+ return mcolors.Normalize(vmin=vmin, vmax=vmax)
1355
+ # Fallback: unknown string → default linear
1356
+ return mcolors.Normalize(vmin=vmin, vmax=vmax)
1357
+ # Dict form: {"type": "LogNorm", "vmin": ..., "vmax": ..., ...}
1358
+ if isinstance(nv, dict):
1359
+ t = str(nv.get("type", "Normalize")).strip().lower()
1360
+ # Prefer explicit vmin/vmax in dict; otherwise use inferred values
1361
+ _vmin = nv.get("vmin", vmin)
1362
+ _vmax = nv.get("vmax", vmax)
1363
+ if t in {"log", "lognorm"}:
1364
+ return mcolors.LogNorm(vmin=_vmin, vmax=_vmax)
1365
+ if t in {"twoslopenorm", "diverging"}:
1366
+ vcenter = nv.get("vcenter", 0.0)
1367
+ return mcolors.TwoSlopeNorm(vcenter=vcenter, vmin=_vmin, vmax=_vmax)
1368
+ if t in {"symlog", "symlognorm"}:
1369
+ # Optional parameters for SymLogNorm
1370
+ linthresh = nv.get("linthresh", 1.0)
1371
+ linscale = nv.get("linscale", 1.0)
1372
+ base = nv.get("base", 10)
1373
+ return mcolors.SymLogNorm(linthresh=linthresh, linscale=linscale, base=base, vmin=_vmin, vmax=_vmax)
1374
+ # Default linear
1375
+ return mcolors.Normalize(vmin=_vmin, vmax=_vmax)
1376
+ # Anything else → default linear
1377
+ return mcolors.Normalize(vmin=vmin, vmax=vmax)
1378
+
1379
+ # Priority 1: explicit norm in style
1380
+ user_norm = style.get("norm", None)
1381
+ resolved = _resolve_norm(user_norm, vmin=vmin, vmax=vmax)
1382
+
1383
+ # If no explicit norm provided, keep existing (if any) or use linear as default
1384
+ if resolved is None:
1385
+ if self.axc._cb["norm"] is None:
1386
+ self.axc._cb["norm"] = mcolors.Normalize(vmin=vmin, vmax=vmax)
1387
+ else:
1388
+ self.axc._cb["norm"] = resolved
1389
+
1390
+ # Also store the resolved norm back into the outgoing style so that plotting receives a proper Normalize
1391
+ if self.axc._cb["norm"] is not None:
1392
+ s["norm"] = self.axc._cb["norm"]
1393
+
1394
+ # Set a backward-compatible 'mode' tag based on the resolved norm
1395
+ if isinstance(self.axc._cb["norm"], mcolors.LogNorm):
1396
+ self.axc._cb['mode'] = "log"
1397
+ elif isinstance(self.axc._cb["norm"], mcolors.TwoSlopeNorm):
1398
+ self.axc._cb['mode'] = "diverging"
1399
+ elif isinstance(self.axc._cb["norm"], mcolors.SymLogNorm):
1400
+ # Mark as 'log' to trigger log-style minor locators on colorbar if needed
1401
+ self.axc._cb['mode'] = "log"
1402
+ else:
1403
+ self.axc._cb['mode'] = "norm"
1404
+
1405
+ if method_key in ("contour","contourf","tricontour","tricontourf") and self.axc._cb["levels"] is None:
1406
+ lv = s.get("levels", 10)
1407
+ if isinstance(lv, int) and self.axc._cb["vmin"] is not None and self.axc._cb["vmax"] is not None:
1408
+ self.axc._cb["levels"] = np.linspace(self.axc._cb["vmin"], self.axc._cb["vmax"], lv)
1409
+ elif hasattr(lv, "__len__"):
1410
+ self.axc._cb["levels"] = lv
1411
+ if 'norm' in s and s['norm'] is not None:
1412
+ s.pop('vmin', None)
1413
+ s.pop('vmax', None)
1414
+ s.pop('mode', None)
1415
+ self.axc._cb["used"] = uses_color
1416
+ return s
1417
+
1418
+
1419
+
1420
+
1421
+
1422
+ def render_layer(self, ax, layer_info):
1423
+ """
1424
+ Render one layer on the given axes using METHOD_DISPATCH and the layer's
1425
+ data/coordinates/style fields assembled earlier in self.layers setter.
1426
+ This function now routes arguments based on the axes type:
1427
+ - ternary axes (ax._type == 'tri'): methods expect (a,b,c, ...)
1428
+ * profile_scatter additionally expects z -> (a,b,c,z, ...)
1429
+ - rectangular axes (ax._type == 'rect'): methods expect (x,y, ...)
1430
+ """
1431
+ # 1) Resolve method
1432
+ try:
1433
+ self.logger.debug(f"Drawing layer -> {layer_info['name']}")
1434
+ except:
1435
+ pass
1436
+ method_key = str(layer_info.get("method", "scatter")).lower()
1437
+ method_name = self.METHOD_DISPATCH.get(method_key)
1438
+ if not method_name or not hasattr(ax, method_name):
1439
+ raise ValueError(f"Unknown/unsupported method '{method_key}' for axes {ax}.")
1440
+ method = getattr(ax, method_name)
1441
+
1442
+ # 2) Merge style (bundle default -> layer override)
1443
+ style = dict(self.style.get(method_key, {}))
1444
+ if layer_info.get("style", {}) is not None:
1445
+ style.update(layer_info.get("style", {}))
1446
+
1447
+ if getattr(ax, "_type", None) == "tri":
1448
+ df = layer_info["data"]
1449
+ coor = layer_info.get("coor", {})
1450
+
1451
+ # Apply per-figure shared colorbar (lazy) if an axc exists
1452
+ try:
1453
+ style = self._cb_collect_and_attach(style, coor, method_key, df)
1454
+ self.logger.debug("Successful loading colorbar style")
1455
+ except Exception as _e:
1456
+ self._logger.debug(f"colorbar lazy-attach failed: {_e}")
1457
+ # Ternary coordinates required: left/right/bottom
1458
+ requiredlbr = {"left", "right", "bottom"}
1459
+ requiredxy = {"x", "y"}
1460
+ if not ((requiredlbr <= set(coor.keys())) or (requiredxy <= set(coor.keys()))):
1461
+ raise ValueError("Ternary layer must define coordinates: {left, right, bottom} or {x, y} with exprs.")
1462
+ for kk, vv in coor.items():
1463
+ style[kk] = self._eval_series(df, vv)
1464
+ return method(**style)
1465
+
1466
+ elif getattr(ax, "_type", None) == "rect":
1467
+ df = layer_info["data"]
1468
+ coor = layer_info.get("coor", {})
1469
+ try:
1470
+ style = self._cb_collect_and_attach(style, coor, method_key, df)
1471
+ self.logger.debug("Successful loading colorbar style")
1472
+ except Exception as _e:
1473
+ self._logger.debug(f"colorbar lazy-attach failed: {_e}")
1474
+
1475
+ if layer_info['method'] == "hist":
1476
+ if isinstance(layer_info['data'], dict):
1477
+ if "label" not in style.keys():
1478
+ style['label'] = []
1479
+ for kk, vv in coor.items():
1480
+ style[kk] = []
1481
+ for dn, ddf in df.items():
1482
+ style['label'].append(dn)
1483
+ for kk, vv in coor.items():
1484
+ style[kk].append( self._eval_series(ddf, vv) )
1485
+ else:
1486
+ for kk, vv in coor.items():
1487
+ style[kk] = self._eval_series(df, vv)
1488
+
1489
+ return method(**style)
1490
+ # Generic x/y coordinates required
1491
+ else:
1492
+ if not ({"x", "y"} <= set(coor.keys())):
1493
+ raise ValueError("Rectangular layer must define coordinates: {x,y} with exprs.")
1494
+
1495
+ for kk, vv in coor.items():
1496
+ # style[kk] = self._eval_series(df, vv)
1497
+ # Mode 1:expr → DataFrame evaluation
1498
+ if isinstance(vv, dict) and "expr" in vv:
1499
+ if df is None:
1500
+ raise ValueError(
1501
+ f"Layer '{layer_info.get('name', '')}' defines expression-based "
1502
+ f"coordinate for '{kk}' but has no data source."
1503
+ )
1504
+ style[kk] = self._eval_series(df, vv)
1505
+ else:
1506
+ # Mode 2:(list/tuple/ndarray/scalar)
1507
+ style[kk] = vv
1508
+ # if "norm" in style.keys():
1509
+ # style = self.load_norm(style)
1510
+ return method(**style)
1511
+
1512
+ else:
1513
+ # Unknown axes adapter type
1514
+ raise ValueError(f"Axes '{ax}' has unknown _type='{getattr(ax, '_type', None)}'.")
1515
+
1516
+
1517
+
1518
+
1519
+
1520
+ # def auto_clip(artists, ax, clip_obj=None, transform=None):
1521
+ def auto_clip(artists, ax):
1522
+
1523
+ from matplotlib.path import Path as MplPath
1524
+ from matplotlib.patches import Patch as MplPatch
1525
+ from matplotlib.container import BarContainer, ErrorbarContainer
1526
+
1527
+ def _apply_to_one(a):
1528
+ try:
1529
+ a.set_clip_path(ax._clip_path, transform=ax.transData)
1530
+ return True
1531
+ except Exception:
1532
+ return False
1533
+
1534
+ def _apply(obj):
1535
+ if _apply_to_one(obj):
1536
+ return True
1537
+ coll = getattr(obj, "collections", None)
1538
+ if coll is not None:
1539
+ for c in coll:
1540
+ _apply_to_one(c)
1541
+ return True
1542
+ if isinstance(obj, BarContainer):
1543
+ for p in obj.patches:
1544
+ _apply_to_one(p)
1545
+ return True
1546
+ if isinstance(obj, ErrorbarContainer):
1547
+ for line in obj.lines:
1548
+ _apply_to_one(line)
1549
+ if hasattr(obj, "has_xerr") and obj.has_xerr and obj.has_yerr:
1550
+ for lc in getattr(obj, "barlinecols", []):
1551
+ _apply_to_one(lc)
1552
+ return True
1553
+ # 4) violinplot
1554
+ if isinstance(obj, dict):
1555
+ for v in obj.values():
1556
+ if isinstance(v, (list, tuple)):
1557
+ for a in v:
1558
+ _apply_to_one(a)
1559
+ else:
1560
+ _apply_to_one(v)
1561
+ return True
1562
+ # 5) iterabile object
1563
+ try:
1564
+ iterator = iter(obj)
1565
+ except TypeError:
1566
+ return False
1567
+ else:
1568
+ for a in iterator:
1569
+ _apply_to_one(a)
1570
+ return True
1571
+
1572
+ _apply(artists)
1573
+ return artists