jarvisplot 1.3.3__tar.gz → 1.3.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/JarvisPLOT.egg-info/PKG-INFO +1 -1
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/JarvisPLOT.egg-info/SOURCES.txt +1 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/PKG-INFO +1 -1
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/Figure/adapters_rect.py +16 -131
- jarvisplot-1.3.5/jarvisplot/Figure/colorbar_runtime.py +393 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/Figure/figure.py +180 -86
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/Figure/layer_runtime.py +47 -20
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/Figure/preprocessor.py +53 -3
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/Figure/preprocessor_runtime.py +76 -1
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/Figure/profile_runtime.py +126 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/cards/gambit/2x1/rect_cmap.json +2 -2
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/core.py +2 -145
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/core_runtime.py +122 -8
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/utils/interpolator.py +1 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/pyproject.toml +1 -1
- jarvisplot-1.3.5/tests/test_figure_contracts.py +482 -0
- jarvisplot-1.3.5/tests/test_interpolator_contracts.py +9 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/tests/test_pathing_contracts.py +25 -0
- jarvisplot-1.3.3/jarvisplot/Figure/colorbar_runtime.py +0 -221
- jarvisplot-1.3.3/tests/test_figure_contracts.py +0 -237
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/JarvisPLOT.egg-info/dependency_links.txt +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/JarvisPLOT.egg-info/entry_points.txt +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/JarvisPLOT.egg-info/requires.txt +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/JarvisPLOT.egg-info/top_level.txt +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/README.md +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/Figure/__init__.py +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/Figure/adapters.py +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/Figure/adapters_ternary.py +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/Figure/cards/std_axes_adapter_config.json +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/Figure/config_runtime.py +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/Figure/data_pipelines.py +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/Figure/helper.py +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/Figure/layout_runtime.py +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/Figure/load_data.py +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/Figure/method_registry.py +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/Figure/style_runtime.py +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/__init__.py +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/cache_store.py +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/cards/a4paper/1x1/ternary.json +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/cards/a4paper/2x1/rect.json +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/cards/a4paper/2x1/rect5x1.json +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/cards/a4paper/2x1/rect_cmap.json +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/cards/a4paper/2x1/ternary.json +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/cards/a4paper/2x1/ternary_cmap.json +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/cards/a4paper/4x1/rect.json +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/cards/a4paper/4x1/rect_cmap.json +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/cards/a4paper/4x1/ternary.json +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/cards/a4paper/4x1/ternary_cmap.json +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/cards/args.json +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/cards/colors/colormaps.json +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/cards/default/output.json +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/cards/gambit/1x1/ternary.json +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/cards/gambit/2x1/ternary.json +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/cards/gambit/2x1/ternary_cmap.json +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/cards/icons/JarvisHEP.png +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/cards/icons/gambit.png +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/cards/icons/gambit_small.png +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/cards/style_preference.json +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/cli.py +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/client.py +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/config.py +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/core_assets.py +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/data_loader.py +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/data_loader_hdf5.py +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/data_loader_runtime.py +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/data_loader_summary.py +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/inner_func.py +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/memtrace.py +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/utils/__init__.py +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/utils/cmaps.py +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/utils/expression.py +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/jarvisplot/utils/pathing.py +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/setup.cfg +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/tests/test_data_loader_contracts.py +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/tests/test_docs_status_labels.py +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/tests/test_parse_data_metadata.py +0 -0
- {jarvisplot-1.3.3 → jarvisplot-1.3.5}/tests/test_template_json.py +0 -0
|
@@ -67,6 +67,7 @@ jarvisplot/utils/pathing.py
|
|
|
67
67
|
tests/test_data_loader_contracts.py
|
|
68
68
|
tests/test_docs_status_labels.py
|
|
69
69
|
tests/test_figure_contracts.py
|
|
70
|
+
tests/test_interpolator_contracts.py
|
|
70
71
|
tests/test_parse_data_metadata.py
|
|
71
72
|
tests/test_pathing_contracts.py
|
|
72
73
|
tests/test_template_json.py
|
|
@@ -10,6 +10,7 @@ from matplotlib.axes import Axes
|
|
|
10
10
|
from matplotlib.collections import PolyCollection
|
|
11
11
|
#
|
|
12
12
|
from .helper import _auto_clip, _mask_by_extend, voronoi_finite_polygons_2d, _clip_poly_to_rect
|
|
13
|
+
from .profile_runtime import grid_profile_mesh
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
# —— Basic Adapter: Forward to the underlying Axes, merge default parameters, perform automatic clipping ——
|
|
@@ -94,142 +95,26 @@ class StdAxesAdapter:
|
|
|
94
95
|
xscale = str(kw.pop("xscale", self.ax.get_xscale())).lower()
|
|
95
96
|
yscale = str(kw.pop("yscale", self.ax.get_yscale())).lower()
|
|
96
97
|
|
|
97
|
-
ix = None
|
|
98
|
-
iy = None
|
|
99
|
-
cols = getattr(df, "columns", [])
|
|
100
|
-
if df is not None and ("__grid_ix__" in cols) and ("__grid_iy__" in cols):
|
|
101
|
-
try:
|
|
102
|
-
ix = np.asarray(df["__grid_ix__"], dtype=np.int32)
|
|
103
|
-
iy = np.asarray(df["__grid_iy__"], dtype=np.int32)
|
|
104
|
-
if "__grid_bin__" in cols and grid_bin is None:
|
|
105
|
-
grid_bin = int(np.asarray(df["__grid_bin__"])[0])
|
|
106
|
-
if "__grid_xmin__" in cols and "__grid_xmax__" in cols and xlim is None:
|
|
107
|
-
xlim = [
|
|
108
|
-
float(np.asarray(df["__grid_xmin__"])[0]),
|
|
109
|
-
float(np.asarray(df["__grid_xmax__"])[0]),
|
|
110
|
-
]
|
|
111
|
-
if "__grid_ymin__" in cols and "__grid_ymax__" in cols and ylim is None:
|
|
112
|
-
ylim = [
|
|
113
|
-
float(np.asarray(df["__grid_ymin__"])[0]),
|
|
114
|
-
float(np.asarray(df["__grid_ymax__"])[0]),
|
|
115
|
-
]
|
|
116
|
-
if "__grid_xscale__" in cols:
|
|
117
|
-
xscale = str(np.asarray(df["__grid_xscale__"])[0]).lower()
|
|
118
|
-
if "__grid_yscale__" in cols:
|
|
119
|
-
yscale = str(np.asarray(df["__grid_yscale__"])[0]).lower()
|
|
120
|
-
if (not objective_from_style) and ("__grid_objective__" in cols):
|
|
121
|
-
objective = str(np.asarray(df["__grid_objective__"])[0]).lower()
|
|
122
|
-
except Exception:
|
|
123
|
-
ix, iy = None, None
|
|
124
|
-
|
|
125
|
-
if grid_bin is None:
|
|
126
|
-
if ix is not None and ix.size > 0:
|
|
127
|
-
try:
|
|
128
|
-
grid_bin = int(max(np.nanmax(ix), np.nanmax(iy)) + 1)
|
|
129
|
-
except Exception:
|
|
130
|
-
grid_bin = None
|
|
131
|
-
if grid_bin is None:
|
|
132
|
-
# fallback when metadata is absent
|
|
133
|
-
grid_bin = max(1, int(np.sqrt(max(len(x), 1))))
|
|
134
|
-
grid_bin = max(int(grid_bin), 1)
|
|
135
|
-
|
|
136
98
|
if xlim is None:
|
|
137
99
|
xlim = [float(self.ax.get_xlim()[0]), float(self.ax.get_xlim()[1])]
|
|
138
100
|
if ylim is None:
|
|
139
101
|
ylim = [float(self.ax.get_ylim()[0]), float(self.ax.get_ylim()[1])]
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
def _norm(arr, lim, scale):
|
|
156
|
-
arr = np.asarray(arr, dtype=float)
|
|
157
|
-
lo = float(lim[0])
|
|
158
|
-
hi = float(lim[1])
|
|
159
|
-
if str(scale).lower() == "log":
|
|
160
|
-
tiny = np.finfo(float).tiny
|
|
161
|
-
lo = max(lo, tiny)
|
|
162
|
-
hi = max(hi, lo * 10.0)
|
|
163
|
-
den = np.log(hi) - np.log(lo)
|
|
164
|
-
if den == 0:
|
|
165
|
-
den = 1.0
|
|
166
|
-
arr = np.where(arr > 0, arr, np.nan)
|
|
167
|
-
return (np.log(arr) - np.log(lo)) / den
|
|
168
|
-
den = hi - lo
|
|
169
|
-
if den == 0:
|
|
170
|
-
den = 1.0
|
|
171
|
-
return (arr - lo) / den
|
|
172
|
-
|
|
173
|
-
n = min(len(x), len(y), len(z))
|
|
174
|
-
if n == 0:
|
|
175
|
-
return []
|
|
176
|
-
x = x[:n]
|
|
177
|
-
y = y[:n]
|
|
178
|
-
z = z[:n]
|
|
179
|
-
|
|
180
|
-
if ix is None or iy is None:
|
|
181
|
-
xn = _norm(x, xlim, xscale)
|
|
182
|
-
yn = _norm(y, ylim, yscale)
|
|
183
|
-
valid = np.isfinite(xn) & np.isfinite(yn)
|
|
184
|
-
if not np.any(valid):
|
|
185
|
-
return []
|
|
186
|
-
xv = np.clip(xn[valid], 0.0, 1.0 - 1e-12)
|
|
187
|
-
yv = np.clip(yn[valid], 0.0, 1.0 - 1e-12)
|
|
188
|
-
ix = (xv * grid_bin).astype(np.int32)
|
|
189
|
-
iy = (yv * grid_bin).astype(np.int32)
|
|
190
|
-
zv = z[valid]
|
|
191
|
-
else:
|
|
192
|
-
ix = np.asarray(ix, dtype=np.int32)[:n]
|
|
193
|
-
iy = np.asarray(iy, dtype=np.int32)[:n]
|
|
194
|
-
valid = np.isfinite(x) & np.isfinite(y) & np.isfinite(z)
|
|
195
|
-
valid &= np.isfinite(ix) & np.isfinite(iy)
|
|
196
|
-
if not np.any(valid):
|
|
197
|
-
return []
|
|
198
|
-
ix = ix[valid]
|
|
199
|
-
iy = iy[valid]
|
|
200
|
-
zv = z[valid]
|
|
201
|
-
|
|
202
|
-
try:
|
|
203
|
-
import pandas as _pd
|
|
204
|
-
zkey = np.where(np.isfinite(zv), zv, np.inf if objective == "min" else -np.inf)
|
|
205
|
-
tmp = _pd.DataFrame({"ix": ix, "iy": iy, "z": zv, "zkey": zkey})
|
|
206
|
-
if objective == "min":
|
|
207
|
-
loc = tmp.groupby(["ix", "iy"], sort=False)["zkey"].idxmin()
|
|
208
|
-
else:
|
|
209
|
-
loc = tmp.groupby(["ix", "iy"], sort=False)["zkey"].idxmax()
|
|
210
|
-
pick = tmp.loc[loc, ["ix", "iy", "z"]]
|
|
211
|
-
ix_u = np.asarray(pick["ix"], dtype=np.int32)
|
|
212
|
-
iy_u = np.asarray(pick["iy"], dtype=np.int32)
|
|
213
|
-
z_u = np.asarray(pick["z"], dtype=float)
|
|
214
|
-
except Exception:
|
|
215
|
-
ix_u = np.asarray(ix, dtype=np.int32)
|
|
216
|
-
iy_u = np.asarray(iy, dtype=np.int32)
|
|
217
|
-
z_u = np.asarray(zv, dtype=float)
|
|
218
|
-
|
|
219
|
-
x_edges = _grid_edges(xlim[0], xlim[1], grid_bin, xscale)
|
|
220
|
-
y_edges = _grid_edges(ylim[0], ylim[1], grid_bin, yscale)
|
|
221
|
-
|
|
222
|
-
in_range = (ix_u >= 0) & (ix_u < grid_bin) & (iy_u >= 0) & (iy_u < grid_bin)
|
|
223
|
-
if not np.any(in_range):
|
|
102
|
+
mesh = grid_profile_mesh(
|
|
103
|
+
x,
|
|
104
|
+
y,
|
|
105
|
+
z,
|
|
106
|
+
df=df,
|
|
107
|
+
grid_bin=grid_bin,
|
|
108
|
+
xlim=xlim,
|
|
109
|
+
ylim=ylim,
|
|
110
|
+
xscale=xscale,
|
|
111
|
+
yscale=yscale,
|
|
112
|
+
objective=objective,
|
|
113
|
+
objective_from_style=objective_from_style,
|
|
114
|
+
)
|
|
115
|
+
if mesh is None:
|
|
224
116
|
return []
|
|
225
|
-
|
|
226
|
-
iy_u = iy_u[in_range]
|
|
227
|
-
z_u = z_u[in_range]
|
|
228
|
-
|
|
229
|
-
grid = np.full((grid_bin, grid_bin), np.nan, dtype=float)
|
|
230
|
-
if len(ix_u) > 0:
|
|
231
|
-
grid[iy_u, ix_u] = z_u
|
|
232
|
-
grid = np.ma.masked_invalid(grid)
|
|
117
|
+
x_edges, y_edges, grid = mesh
|
|
233
118
|
|
|
234
119
|
kw.setdefault("shading", "flat")
|
|
235
120
|
if edgecolor is not None:
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Mapping, Optional
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import matplotlib.colors as mcolors
|
|
7
|
+
|
|
8
|
+
from ..utils.expression import eval_dataframe_expression
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# ---------------------------------------------------------------------------
|
|
12
|
+
# Frame config helpers
|
|
13
|
+
# ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
def axc_is_horizontal(frame: Mapping[str, Any], cb_name: str = "axc") -> bool:
|
|
16
|
+
cb_cfg = frame.get(cb_name, {}) if isinstance(frame, Mapping) else {}
|
|
17
|
+
return str(cb_cfg.get("orientation", "")).lower() == "horizontal"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def axc_color_config(frame: Mapping[str, Any], cb_name: str = "axc") -> dict:
|
|
21
|
+
"""Return normalized color config for a named colorbar axis.
|
|
22
|
+
|
|
23
|
+
Reads frame[cb_name].color. A value of None or the string "auto" means
|
|
24
|
+
auto-range from data. Falls back to legacy frame[cb_name].xscale /
|
|
25
|
+
yscale when 'scale' is absent from the color block.
|
|
26
|
+
"""
|
|
27
|
+
out: dict[str, Any] = {}
|
|
28
|
+
cb_cfg = frame.get(cb_name, {}) if isinstance(frame, Mapping) else {}
|
|
29
|
+
if not isinstance(cb_cfg, dict):
|
|
30
|
+
cb_cfg = {}
|
|
31
|
+
color_cfg = cb_cfg.get("color", {})
|
|
32
|
+
if not isinstance(color_cfg, dict):
|
|
33
|
+
color_cfg = {}
|
|
34
|
+
|
|
35
|
+
if "cmap" in color_cfg:
|
|
36
|
+
out["cmap"] = color_cfg.get("cmap")
|
|
37
|
+
|
|
38
|
+
for key in ("vmin", "vmax"):
|
|
39
|
+
raw = color_cfg.get(key, None)
|
|
40
|
+
if raw is None or str(raw).strip().lower() == "auto":
|
|
41
|
+
out[key] = None # explicit auto → leave as None
|
|
42
|
+
else:
|
|
43
|
+
try:
|
|
44
|
+
val = float(raw)
|
|
45
|
+
if np.isfinite(val):
|
|
46
|
+
out[key] = val
|
|
47
|
+
# non-finite treated as auto (leave as None)
|
|
48
|
+
except Exception:
|
|
49
|
+
pass # non-numeric treated as auto
|
|
50
|
+
|
|
51
|
+
scale = color_cfg.get("scale", None)
|
|
52
|
+
if scale is None:
|
|
53
|
+
is_h = axc_is_horizontal(frame, cb_name)
|
|
54
|
+
if is_h:
|
|
55
|
+
scale = cb_cfg.get("xscale", cb_cfg.get("yscale", None))
|
|
56
|
+
else:
|
|
57
|
+
scale = cb_cfg.get("yscale", cb_cfg.get("xscale", None))
|
|
58
|
+
if isinstance(scale, str):
|
|
59
|
+
scale = scale.strip().lower()
|
|
60
|
+
if scale:
|
|
61
|
+
out["scale"] = scale
|
|
62
|
+
|
|
63
|
+
return out
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
# Scalar helpers
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
def _coerce_scalar(v) -> Optional[float]:
|
|
71
|
+
try:
|
|
72
|
+
vv = float(v)
|
|
73
|
+
if np.isfinite(vv):
|
|
74
|
+
return vv
|
|
75
|
+
except Exception:
|
|
76
|
+
pass
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _resolve_norm(nv, *, vmin=None, vmax=None, logger=None):
|
|
81
|
+
if nv is None:
|
|
82
|
+
return None
|
|
83
|
+
if isinstance(nv, mcolors.Normalize):
|
|
84
|
+
return nv
|
|
85
|
+
if isinstance(nv, str):
|
|
86
|
+
key = nv.strip().lower()
|
|
87
|
+
if key in {"log", "lognorm"}:
|
|
88
|
+
if (vmin is None) or (vmax is None) or (vmin <= 0) or (vmax <= 0):
|
|
89
|
+
if logger:
|
|
90
|
+
logger.warning("axc log scale requires positive vmin/vmax; fallback to linear Normalize.")
|
|
91
|
+
return mcolors.Normalize(vmin=vmin, vmax=vmax)
|
|
92
|
+
return mcolors.LogNorm(vmin=vmin, vmax=vmax)
|
|
93
|
+
if key in {"twoslopenorm", "diverging"}:
|
|
94
|
+
return mcolors.TwoSlopeNorm(vcenter=0.0, vmin=vmin, vmax=vmax)
|
|
95
|
+
if key in {"symlog", "symlognorm"}:
|
|
96
|
+
return mcolors.SymLogNorm(linthresh=1.0, linscale=1.0, base=10, vmin=vmin, vmax=vmax)
|
|
97
|
+
return mcolors.Normalize(vmin=vmin, vmax=vmax)
|
|
98
|
+
if isinstance(nv, dict):
|
|
99
|
+
t = str(nv.get("type", "Normalize")).strip().lower()
|
|
100
|
+
_vmin = nv.get("vmin", vmin)
|
|
101
|
+
_vmax = nv.get("vmax", vmax)
|
|
102
|
+
if t in {"log", "lognorm"}:
|
|
103
|
+
if (_vmin is None) or (_vmax is None) or (_vmin <= 0) or (_vmax <= 0):
|
|
104
|
+
if logger:
|
|
105
|
+
logger.warning("axc log norm requires positive vmin/vmax; fallback to linear Normalize.")
|
|
106
|
+
return mcolors.Normalize(vmin=_vmin, vmax=_vmax)
|
|
107
|
+
return mcolors.LogNorm(vmin=_vmin, vmax=_vmax)
|
|
108
|
+
if t in {"twoslopenorm", "diverging"}:
|
|
109
|
+
vcenter = nv.get("vcenter", 0.0)
|
|
110
|
+
return mcolors.TwoSlopeNorm(vcenter=vcenter, vmin=_vmin, vmax=_vmax)
|
|
111
|
+
if t in {"symlog", "symlognorm"}:
|
|
112
|
+
linthresh = nv.get("linthresh", 1.0)
|
|
113
|
+
linscale = nv.get("linscale", 1.0)
|
|
114
|
+
base = nv.get("base", 10)
|
|
115
|
+
return mcolors.SymLogNorm(
|
|
116
|
+
linthresh=linthresh, linscale=linscale, base=base,
|
|
117
|
+
vmin=_vmin, vmax=_vmax,
|
|
118
|
+
)
|
|
119
|
+
return mcolors.Normalize(vmin=_vmin, vmax=_vmax)
|
|
120
|
+
return mcolors.Normalize(vmin=vmin, vmax=vmax)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
# Pre-scan helpers
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
_COLORED_Z_METHODS = frozenset({
|
|
128
|
+
"contour", "contourf", "tricontour", "tricontourf",
|
|
129
|
+
"tripcolor", "tripcolor_axes",
|
|
130
|
+
"pcolor", "pcolormesh", "imshow",
|
|
131
|
+
"voronoi", "voronoif",
|
|
132
|
+
"grid_profile", "grid_profiling",
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def layer_uses_color(style: dict, coor: dict, method_key: str) -> bool:
|
|
137
|
+
"""Return True if this layer will need a colorbar."""
|
|
138
|
+
return (
|
|
139
|
+
bool(style.get("cmap"))
|
|
140
|
+
or ("c" in coor)
|
|
141
|
+
or (("z" in coor) and (method_key in _COLORED_Z_METHODS))
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def collect_layer_color_range(df, coor: dict, style: dict, *, scale: str | None = None):
|
|
146
|
+
"""Extract (data_min, data_max) for the colour channel of a single layer.
|
|
147
|
+
|
|
148
|
+
Returns (None, None) when no finite data is available.
|
|
149
|
+
"""
|
|
150
|
+
arr = None
|
|
151
|
+
if isinstance(coor.get("z"), dict) and "expr" in coor["z"]:
|
|
152
|
+
arr = eval_dataframe_expression(df, coor["z"]["expr"], logger=None)
|
|
153
|
+
elif isinstance(coor.get("c"), dict) and "expr" in coor["c"]:
|
|
154
|
+
arr = eval_dataframe_expression(df, coor["c"]["expr"], logger=None)
|
|
155
|
+
elif "z" in style and not isinstance(style.get("z"), str):
|
|
156
|
+
arr = style.get("z")
|
|
157
|
+
elif "c" in style and not isinstance(style.get("c"), str):
|
|
158
|
+
arr = style.get("c")
|
|
159
|
+
|
|
160
|
+
if arr is None:
|
|
161
|
+
return None, None
|
|
162
|
+
try:
|
|
163
|
+
arr = np.asarray(arr, dtype=float)
|
|
164
|
+
arr = arr[np.isfinite(arr)]
|
|
165
|
+
if str(scale or "").strip().lower() == "log":
|
|
166
|
+
arr = arr[arr > 0]
|
|
167
|
+
if arr.size == 0:
|
|
168
|
+
return None, None
|
|
169
|
+
return float(np.min(arr)), float(np.max(arr))
|
|
170
|
+
except Exception:
|
|
171
|
+
return None, None
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _validate_colorbar_limits(vmin, vmax, scale: str, logger=None):
|
|
175
|
+
"""Validate vmin/vmax; replace invalid values with None (auto).
|
|
176
|
+
|
|
177
|
+
Rules applied in order:
|
|
178
|
+
1. Non-finite value → warn, fall back to auto.
|
|
179
|
+
2. log scale + value <= 0 → warn, fall back to auto for that value.
|
|
180
|
+
3. Both set but vmin >= vmax → warn, fall back to auto for both.
|
|
181
|
+
"""
|
|
182
|
+
scale = str(scale or "").strip().lower()
|
|
183
|
+
|
|
184
|
+
# Rule 1: finiteness
|
|
185
|
+
if vmin is not None and not np.isfinite(vmin):
|
|
186
|
+
if logger:
|
|
187
|
+
logger.warning(f"colorbar vmin={vmin!r} is not finite; falling back to auto.")
|
|
188
|
+
vmin = None
|
|
189
|
+
if vmax is not None and not np.isfinite(vmax):
|
|
190
|
+
if logger:
|
|
191
|
+
logger.warning(f"colorbar vmax={vmax!r} is not finite; falling back to auto.")
|
|
192
|
+
vmax = None
|
|
193
|
+
|
|
194
|
+
# Rule 2: log scale positivity
|
|
195
|
+
if scale == "log":
|
|
196
|
+
if vmin is not None and vmin <= 0:
|
|
197
|
+
if logger:
|
|
198
|
+
logger.warning(
|
|
199
|
+
f"colorbar log scale requires vmin > 0, got vmin={vmin}; "
|
|
200
|
+
"falling back to auto."
|
|
201
|
+
)
|
|
202
|
+
vmin = None
|
|
203
|
+
if vmax is not None and vmax <= 0:
|
|
204
|
+
if logger:
|
|
205
|
+
logger.warning(
|
|
206
|
+
f"colorbar log scale requires vmax > 0, got vmax={vmax}; "
|
|
207
|
+
"falling back to auto."
|
|
208
|
+
)
|
|
209
|
+
vmax = None
|
|
210
|
+
|
|
211
|
+
# Rule 3: ordering
|
|
212
|
+
if vmin is not None and vmax is not None and vmin >= vmax:
|
|
213
|
+
if logger:
|
|
214
|
+
logger.warning(
|
|
215
|
+
f"colorbar vmin={vmin} >= vmax={vmax}; "
|
|
216
|
+
"falling back to auto for both."
|
|
217
|
+
)
|
|
218
|
+
vmin = None
|
|
219
|
+
vmax = None
|
|
220
|
+
|
|
221
|
+
return vmin, vmax
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def precompute_colorbar_cb(color_cfg: dict, data_ranges: list, logger=None) -> dict:
|
|
225
|
+
"""Build the complete _cb state dict from frame color config + data ranges.
|
|
226
|
+
|
|
227
|
+
vmin and vmax are resolved independently:
|
|
228
|
+
- Explicit value in frame config that passes validation → used as-is.
|
|
229
|
+
- Missing or invalid value → auto from the data range of bound layers.
|
|
230
|
+
|
|
231
|
+
This function is called once per colorbar during the pre-scan phase in
|
|
232
|
+
Figure._prescan_colorbar_ranges(), before any layer is rendered. After
|
|
233
|
+
this call _cb is treated as read-only during rendering.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
color_cfg: result of axc_color_config() for this colorbar.
|
|
237
|
+
data_ranges: list of (data_min, data_max) collected from pre-scanned layers.
|
|
238
|
+
logger: optional loguru logger.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
Complete _cb dict to be stored on the colorbar axes object.
|
|
242
|
+
"""
|
|
243
|
+
scale = str(color_cfg.get("scale", "linear")).strip().lower()
|
|
244
|
+
|
|
245
|
+
cb: dict[str, Any] = {
|
|
246
|
+
"mode": "auto",
|
|
247
|
+
"levels": None,
|
|
248
|
+
"vmin": None,
|
|
249
|
+
"vmax": None,
|
|
250
|
+
"norm": None,
|
|
251
|
+
"cmap": color_cfg.get("cmap"),
|
|
252
|
+
"used": bool(data_ranges),
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if not data_ranges:
|
|
256
|
+
return cb
|
|
257
|
+
|
|
258
|
+
# --- Resolve vmin (explicit wins; absent → auto from data) ---
|
|
259
|
+
frame_vmin = color_cfg.get("vmin")
|
|
260
|
+
if frame_vmin is not None:
|
|
261
|
+
cb["vmin"] = frame_vmin
|
|
262
|
+
else:
|
|
263
|
+
valid_mins = [r[0] for r in data_ranges if r[0] is not None]
|
|
264
|
+
cb["vmin"] = min(valid_mins) if valid_mins else None
|
|
265
|
+
|
|
266
|
+
# --- Resolve vmax (explicit wins; absent → auto from data) ---
|
|
267
|
+
frame_vmax = color_cfg.get("vmax")
|
|
268
|
+
if frame_vmax is not None:
|
|
269
|
+
cb["vmax"] = frame_vmax
|
|
270
|
+
else:
|
|
271
|
+
valid_maxes = [r[1] for r in data_ranges if r[1] is not None]
|
|
272
|
+
cb["vmax"] = max(valid_maxes) if valid_maxes else None
|
|
273
|
+
|
|
274
|
+
# --- Validate (may reset invalid values back to None) ---
|
|
275
|
+
cb["vmin"], cb["vmax"] = _validate_colorbar_limits(
|
|
276
|
+
cb["vmin"], cb["vmax"], scale, logger=logger
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
# --- Second-pass auto: if validation reset a value, refill from data ---
|
|
280
|
+
if cb["vmin"] is None:
|
|
281
|
+
valid_mins = [r[0] for r in data_ranges if r[0] is not None]
|
|
282
|
+
cb["vmin"] = min(valid_mins) if valid_mins else None
|
|
283
|
+
if cb["vmax"] is None:
|
|
284
|
+
valid_maxes = [r[1] for r in data_ranges if r[1] is not None]
|
|
285
|
+
cb["vmax"] = max(valid_maxes) if valid_maxes else None
|
|
286
|
+
|
|
287
|
+
# --- Build norm ---
|
|
288
|
+
if cb["vmin"] is not None and cb["vmax"] is not None:
|
|
289
|
+
if scale in {"linear", "norm", "normalize", ""}:
|
|
290
|
+
resolved = mcolors.Normalize(vmin=cb["vmin"], vmax=cb["vmax"])
|
|
291
|
+
elif scale == "log":
|
|
292
|
+
resolved = _resolve_norm("lognorm", vmin=cb["vmin"], vmax=cb["vmax"], logger=logger)
|
|
293
|
+
else:
|
|
294
|
+
resolved = _resolve_norm(scale, vmin=cb["vmin"], vmax=cb["vmax"], logger=logger)
|
|
295
|
+
if resolved is None:
|
|
296
|
+
resolved = mcolors.Normalize(vmin=cb["vmin"], vmax=cb["vmax"])
|
|
297
|
+
|
|
298
|
+
cb["norm"] = resolved
|
|
299
|
+
if isinstance(resolved, mcolors.LogNorm):
|
|
300
|
+
cb["mode"] = "log"
|
|
301
|
+
elif isinstance(resolved, mcolors.TwoSlopeNorm):
|
|
302
|
+
cb["mode"] = "diverging"
|
|
303
|
+
elif isinstance(resolved, mcolors.SymLogNorm):
|
|
304
|
+
cb["mode"] = "log"
|
|
305
|
+
else:
|
|
306
|
+
cb["mode"] = "norm"
|
|
307
|
+
|
|
308
|
+
return cb
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
# ---------------------------------------------------------------------------
|
|
312
|
+
# Render-time attachment (read-only after pre-scan)
|
|
313
|
+
# ---------------------------------------------------------------------------
|
|
314
|
+
|
|
315
|
+
def collect_and_attach_colorbar(
|
|
316
|
+
fig, style: dict, coor: dict, method_key: str, df, cb_name: str = "axc"
|
|
317
|
+
) -> dict:
|
|
318
|
+
"""Attach pre-computed colorbar norm/cmap to the layer style dict.
|
|
319
|
+
|
|
320
|
+
Called once per layer during rendering. By this point _cb has already
|
|
321
|
+
been fully built by Figure._prescan_colorbar_ranges() so this function
|
|
322
|
+
is read-only — it never mutates _cb.
|
|
323
|
+
|
|
324
|
+
The pre-scan / read-only split ensures:
|
|
325
|
+
- colorbar limits are determined by ALL bound layers, not just those
|
|
326
|
+
rendered so far (no render-order dependency).
|
|
327
|
+
- the final colorbar is always predictable from the YAML alone:
|
|
328
|
+
explicit frame.axc.color.vmin/vmax win; absent values fall back to
|
|
329
|
+
the data-driven auto range.
|
|
330
|
+
"""
|
|
331
|
+
axc = fig.axes.get(cb_name)
|
|
332
|
+
if axc is None or not hasattr(axc, "_cb"):
|
|
333
|
+
return style
|
|
334
|
+
|
|
335
|
+
if not isinstance(axc._cb, dict):
|
|
336
|
+
axc._cb = {
|
|
337
|
+
"mode": "auto",
|
|
338
|
+
"levels": None,
|
|
339
|
+
"vmin": None,
|
|
340
|
+
"vmax": None,
|
|
341
|
+
"norm": None,
|
|
342
|
+
"cmap": None,
|
|
343
|
+
"used": False,
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if not axc._cb.get("used"):
|
|
347
|
+
frame = getattr(fig, "frame", {})
|
|
348
|
+
color_cfg = axc_color_config(frame, cb_name)
|
|
349
|
+
data_range = collect_layer_color_range(
|
|
350
|
+
df,
|
|
351
|
+
coor,
|
|
352
|
+
style,
|
|
353
|
+
scale=color_cfg.get("scale"),
|
|
354
|
+
)
|
|
355
|
+
if data_range != (None, None):
|
|
356
|
+
axc._cb.update(
|
|
357
|
+
precompute_colorbar_cb(color_cfg, [data_range], logger=getattr(fig, "logger", None))
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
if not axc._cb.get("used"):
|
|
361
|
+
return style
|
|
362
|
+
|
|
363
|
+
s = dict(style)
|
|
364
|
+
|
|
365
|
+
# Attach cmap
|
|
366
|
+
chosen_cmap = axc._cb.get("cmap")
|
|
367
|
+
if chosen_cmap is not None:
|
|
368
|
+
s["cmap"] = chosen_cmap
|
|
369
|
+
|
|
370
|
+
# Attach norm (strip raw vmin/vmax so matplotlib uses the norm object)
|
|
371
|
+
norm = axc._cb.get("norm")
|
|
372
|
+
if norm is not None:
|
|
373
|
+
s["norm"] = norm
|
|
374
|
+
s.pop("vmin", None)
|
|
375
|
+
s.pop("vmax", None)
|
|
376
|
+
s.pop("mode", None)
|
|
377
|
+
|
|
378
|
+
# Contour levels (lazily generated on first contour layer, then reused)
|
|
379
|
+
if (
|
|
380
|
+
method_key in ("contour", "contourf", "tricontour", "tricontourf")
|
|
381
|
+
and axc._cb.get("levels") is None
|
|
382
|
+
and axc._cb.get("vmin") is not None
|
|
383
|
+
and axc._cb.get("vmax") is not None
|
|
384
|
+
):
|
|
385
|
+
lv = s.get("levels", 10)
|
|
386
|
+
if isinstance(lv, int):
|
|
387
|
+
axc._cb["levels"] = np.linspace(axc._cb["vmin"], axc._cb["vmax"], lv)
|
|
388
|
+
elif hasattr(lv, "__len__"):
|
|
389
|
+
axc._cb["levels"] = lv
|
|
390
|
+
if axc._cb.get("levels") is not None:
|
|
391
|
+
s["levels"] = axc._cb["levels"]
|
|
392
|
+
|
|
393
|
+
return s
|