jarvisplot 1.3.4__tar.gz → 1.3.6__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.4 → jarvisplot-1.3.6}/JarvisPLOT.egg-info/PKG-INFO +1 -1
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/JarvisPLOT.egg-info/SOURCES.txt +5 -2
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/PKG-INFO +1 -1
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/Figure/adapters_rect.py +445 -1
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/Figure/adapters_ternary.py +21 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/Figure/colorbar_runtime.py +64 -15
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/Figure/config_runtime.py +8 -7
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/Figure/figure.py +18 -58
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/Figure/helper.py +1 -1
- jarvisplot-1.3.6/jarvisplot/Figure/interp_natural_neighbor.py +507 -0
- jarvisplot-1.3.6/jarvisplot/Figure/interp_natural_neighbor_exact.py +1064 -0
- jarvisplot-1.3.6/jarvisplot/Figure/layer_runtime.py +642 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/Figure/layout_runtime.py +1 -1
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/Figure/method_registry.py +17 -79
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/Figure/preprocessor.py +11 -42
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/Figure/preprocessor_runtime.py +170 -7
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/Figure/profile_runtime.py +1 -1
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/core.py +23 -48
- jarvisplot-1.3.6/jarvisplot/core_assets.py +68 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/core_runtime.py +0 -59
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/data_loader.py +10 -110
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/data_loader_runtime.py +47 -61
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/inner_func.py +2 -2
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/utils/cmaps.py +1 -14
- jarvisplot-1.3.6/jarvisplot/utils/dataframes.py +56 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/pyproject.toml +1 -1
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/tests/test_data_loader_contracts.py +122 -207
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/tests/test_docs_status_labels.py +5 -1
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/tests/test_figure_contracts.py +308 -15
- jarvisplot-1.3.6/tests/test_interp_natural_neighbor.py +390 -0
- jarvisplot-1.3.6/tests/test_interpolator_contracts.py +44 -0
- jarvisplot-1.3.6/tests/test_jpcontour_methods.py +629 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/tests/test_parse_data_metadata.py +0 -1
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/tests/test_pathing_contracts.py +8 -4
- jarvisplot-1.3.6/tests/test_template_json.py +26 -0
- jarvisplot-1.3.4/jarvisplot/Figure/adapters.py +0 -6
- jarvisplot-1.3.4/jarvisplot/Figure/layer_runtime.py +0 -308
- jarvisplot-1.3.4/jarvisplot/Figure/load_data.py +0 -169
- jarvisplot-1.3.4/jarvisplot/core_assets.py +0 -63
- jarvisplot-1.3.4/tests/test_interpolator_contracts.py +0 -9
- jarvisplot-1.3.4/tests/test_template_json.py +0 -10
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/JarvisPLOT.egg-info/dependency_links.txt +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/JarvisPLOT.egg-info/entry_points.txt +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/JarvisPLOT.egg-info/requires.txt +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/JarvisPLOT.egg-info/top_level.txt +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/README.md +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/Figure/__init__.py +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/Figure/cards/std_axes_adapter_config.json +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/Figure/data_pipelines.py +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/Figure/style_runtime.py +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/__init__.py +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/cache_store.py +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/cards/a4paper/1x1/ternary.json +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/cards/a4paper/2x1/rect.json +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/cards/a4paper/2x1/rect5x1.json +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/cards/a4paper/2x1/rect_cmap.json +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/cards/a4paper/2x1/ternary.json +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/cards/a4paper/2x1/ternary_cmap.json +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/cards/a4paper/4x1/rect.json +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/cards/a4paper/4x1/rect_cmap.json +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/cards/a4paper/4x1/ternary.json +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/cards/a4paper/4x1/ternary_cmap.json +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/cards/args.json +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/cards/colors/colormaps.json +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/cards/default/output.json +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/cards/gambit/1x1/ternary.json +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/cards/gambit/2x1/rect_cmap.json +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/cards/gambit/2x1/ternary.json +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/cards/gambit/2x1/ternary_cmap.json +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/cards/icons/JarvisHEP.png +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/cards/icons/gambit.png +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/cards/icons/gambit_small.png +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/cards/style_preference.json +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/cli.py +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/client.py +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/config.py +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/data_loader_hdf5.py +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/data_loader_summary.py +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/memtrace.py +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/utils/__init__.py +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/utils/expression.py +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/utils/interpolator.py +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/jarvisplot/utils/pathing.py +0 -0
- {jarvisplot-1.3.4 → jarvisplot-1.3.6}/setup.cfg +0 -0
|
@@ -21,7 +21,6 @@ jarvisplot/data_loader_summary.py
|
|
|
21
21
|
jarvisplot/inner_func.py
|
|
22
22
|
jarvisplot/memtrace.py
|
|
23
23
|
jarvisplot/Figure/__init__.py
|
|
24
|
-
jarvisplot/Figure/adapters.py
|
|
25
24
|
jarvisplot/Figure/adapters_rect.py
|
|
26
25
|
jarvisplot/Figure/adapters_ternary.py
|
|
27
26
|
jarvisplot/Figure/colorbar_runtime.py
|
|
@@ -29,9 +28,10 @@ jarvisplot/Figure/config_runtime.py
|
|
|
29
28
|
jarvisplot/Figure/data_pipelines.py
|
|
30
29
|
jarvisplot/Figure/figure.py
|
|
31
30
|
jarvisplot/Figure/helper.py
|
|
31
|
+
jarvisplot/Figure/interp_natural_neighbor.py
|
|
32
|
+
jarvisplot/Figure/interp_natural_neighbor_exact.py
|
|
32
33
|
jarvisplot/Figure/layer_runtime.py
|
|
33
34
|
jarvisplot/Figure/layout_runtime.py
|
|
34
|
-
jarvisplot/Figure/load_data.py
|
|
35
35
|
jarvisplot/Figure/method_registry.py
|
|
36
36
|
jarvisplot/Figure/preprocessor.py
|
|
37
37
|
jarvisplot/Figure/preprocessor_runtime.py
|
|
@@ -61,13 +61,16 @@ jarvisplot/cards/icons/gambit.png
|
|
|
61
61
|
jarvisplot/cards/icons/gambit_small.png
|
|
62
62
|
jarvisplot/utils/__init__.py
|
|
63
63
|
jarvisplot/utils/cmaps.py
|
|
64
|
+
jarvisplot/utils/dataframes.py
|
|
64
65
|
jarvisplot/utils/expression.py
|
|
65
66
|
jarvisplot/utils/interpolator.py
|
|
66
67
|
jarvisplot/utils/pathing.py
|
|
67
68
|
tests/test_data_loader_contracts.py
|
|
68
69
|
tests/test_docs_status_labels.py
|
|
69
70
|
tests/test_figure_contracts.py
|
|
71
|
+
tests/test_interp_natural_neighbor.py
|
|
70
72
|
tests/test_interpolator_contracts.py
|
|
73
|
+
tests/test_jpcontour_methods.py
|
|
71
74
|
tests/test_parse_data_metadata.py
|
|
72
75
|
tests/test_pathing_contracts.py
|
|
73
76
|
tests/test_template_json.py
|
|
@@ -5,12 +5,14 @@ from typing import Any, Dict, Optional
|
|
|
5
5
|
import numpy as np
|
|
6
6
|
import os
|
|
7
7
|
import json
|
|
8
|
+
import warnings
|
|
8
9
|
|
|
9
10
|
from matplotlib.axes import Axes
|
|
10
11
|
from matplotlib.collections import PolyCollection
|
|
11
12
|
#
|
|
12
13
|
from .helper import _auto_clip, _mask_by_extend, voronoi_finite_polygons_2d, _clip_poly_to_rect
|
|
13
14
|
from .profile_runtime import grid_profile_mesh
|
|
15
|
+
from .interp_natural_neighbor import resolve_backend
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
# —— Basic Adapter: Forward to the underlying Axes, merge default parameters, perform automatic clipping ——
|
|
@@ -52,6 +54,308 @@ class StdAxesAdapter:
|
|
|
52
54
|
base.update(kwargs or {})
|
|
53
55
|
return base
|
|
54
56
|
|
|
57
|
+
@staticmethod
|
|
58
|
+
def _coerce_positive_int(value, default: int) -> int:
|
|
59
|
+
try:
|
|
60
|
+
n = int(value)
|
|
61
|
+
if n > 0:
|
|
62
|
+
return n
|
|
63
|
+
except Exception:
|
|
64
|
+
pass
|
|
65
|
+
return int(default)
|
|
66
|
+
|
|
67
|
+
def _resolve_jpcontour_axis(self, axis_name: str, values, *, npts: int, bounds=None):
|
|
68
|
+
values = np.asarray(values, dtype=float).reshape(-1)
|
|
69
|
+
finite = values[np.isfinite(values)]
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
scale = str(self.ax.get_xscale() if axis_name == "x" else self.ax.get_yscale()).lower()
|
|
73
|
+
except Exception:
|
|
74
|
+
scale = "linear"
|
|
75
|
+
|
|
76
|
+
if bounds is None:
|
|
77
|
+
if finite.size:
|
|
78
|
+
lo = float(np.min(finite))
|
|
79
|
+
hi = float(np.max(finite))
|
|
80
|
+
else:
|
|
81
|
+
try:
|
|
82
|
+
lo, hi = self.ax.get_xlim() if axis_name == "x" else self.ax.get_ylim()
|
|
83
|
+
lo = float(lo)
|
|
84
|
+
hi = float(hi)
|
|
85
|
+
except Exception:
|
|
86
|
+
lo, hi = 0.0, 1.0
|
|
87
|
+
else:
|
|
88
|
+
try:
|
|
89
|
+
lo = float(bounds[0])
|
|
90
|
+
hi = float(bounds[1])
|
|
91
|
+
except Exception:
|
|
92
|
+
lo, hi = 0.0, 1.0
|
|
93
|
+
|
|
94
|
+
if not np.isfinite(lo) or not np.isfinite(hi):
|
|
95
|
+
if finite.size:
|
|
96
|
+
lo = float(np.min(finite))
|
|
97
|
+
hi = float(np.max(finite))
|
|
98
|
+
else:
|
|
99
|
+
lo, hi = 0.0, 1.0
|
|
100
|
+
|
|
101
|
+
if lo == hi:
|
|
102
|
+
if scale == "log" and lo > 0:
|
|
103
|
+
hi = lo * 10.0
|
|
104
|
+
else:
|
|
105
|
+
hi = lo + 1.0
|
|
106
|
+
|
|
107
|
+
if scale == "log":
|
|
108
|
+
positive = finite[finite > 0]
|
|
109
|
+
if lo <= 0 or hi <= 0:
|
|
110
|
+
if positive.size:
|
|
111
|
+
lo = float(np.min(positive))
|
|
112
|
+
hi = float(np.max(positive))
|
|
113
|
+
else:
|
|
114
|
+
warnings.warn(
|
|
115
|
+
f"jpcontour: {axis_name}-axis is log-scaled but has no positive finite values; "
|
|
116
|
+
"falling back to a linear grid",
|
|
117
|
+
RuntimeWarning,
|
|
118
|
+
stacklevel=3,
|
|
119
|
+
)
|
|
120
|
+
return np.linspace(lo, hi, npts)
|
|
121
|
+
lo = max(lo, np.finfo(float).tiny)
|
|
122
|
+
hi = max(hi, lo * 10.0)
|
|
123
|
+
return np.geomspace(lo, hi, npts)
|
|
124
|
+
|
|
125
|
+
return np.linspace(lo, hi, npts)
|
|
126
|
+
|
|
127
|
+
def _normalize_jpfield_axis(self, axis_name: str, values, *, bounds=None):
|
|
128
|
+
"""Map data coordinates onto axes-fraction coordinates in [0, 1].
|
|
129
|
+
|
|
130
|
+
This is used by jpfield so interpolation and rendering both happen in
|
|
131
|
+
the axes transform frame, avoiding mixed log/linear geometry.
|
|
132
|
+
"""
|
|
133
|
+
values = np.asarray(values, dtype=float).reshape(-1)
|
|
134
|
+
out = np.full(values.shape, np.nan, dtype=float)
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
scale = str(self.ax.get_xscale() if axis_name == "x" else self.ax.get_yscale()).lower()
|
|
138
|
+
except Exception:
|
|
139
|
+
scale = "linear"
|
|
140
|
+
|
|
141
|
+
if bounds is None:
|
|
142
|
+
try:
|
|
143
|
+
bounds = self.ax.get_xlim() if axis_name == "x" else self.ax.get_ylim()
|
|
144
|
+
except Exception:
|
|
145
|
+
bounds = None
|
|
146
|
+
|
|
147
|
+
if bounds is None:
|
|
148
|
+
finite = values[np.isfinite(values)]
|
|
149
|
+
if finite.size:
|
|
150
|
+
lo = float(np.min(finite))
|
|
151
|
+
hi = float(np.max(finite))
|
|
152
|
+
else:
|
|
153
|
+
lo, hi = 0.0, 1.0
|
|
154
|
+
else:
|
|
155
|
+
try:
|
|
156
|
+
lo = float(bounds[0])
|
|
157
|
+
hi = float(bounds[1])
|
|
158
|
+
except Exception:
|
|
159
|
+
lo, hi = 0.0, 1.0
|
|
160
|
+
|
|
161
|
+
if not np.isfinite(lo) or not np.isfinite(hi):
|
|
162
|
+
finite = values[np.isfinite(values)]
|
|
163
|
+
if finite.size:
|
|
164
|
+
lo = float(np.min(finite))
|
|
165
|
+
hi = float(np.max(finite))
|
|
166
|
+
else:
|
|
167
|
+
lo, hi = 0.0, 1.0
|
|
168
|
+
|
|
169
|
+
if lo == hi:
|
|
170
|
+
if scale == "log" and lo > 0:
|
|
171
|
+
hi = lo * 10.0
|
|
172
|
+
else:
|
|
173
|
+
hi = lo + 1.0
|
|
174
|
+
|
|
175
|
+
if scale == "log":
|
|
176
|
+
positive = values[np.isfinite(values) & (values > 0)]
|
|
177
|
+
if lo <= 0 or hi <= 0:
|
|
178
|
+
if positive.size:
|
|
179
|
+
lo = float(np.min(positive))
|
|
180
|
+
hi = float(np.max(positive))
|
|
181
|
+
else:
|
|
182
|
+
warnings.warn(
|
|
183
|
+
f"jpfield: {axis_name}-axis is log-scaled but has no positive finite values; "
|
|
184
|
+
"rendering an empty field in axes-fraction space",
|
|
185
|
+
RuntimeWarning,
|
|
186
|
+
stacklevel=3,
|
|
187
|
+
)
|
|
188
|
+
return out
|
|
189
|
+
lo = max(lo, np.finfo(float).tiny)
|
|
190
|
+
hi = max(hi, lo * 10.0)
|
|
191
|
+
den = np.log(hi) - np.log(lo)
|
|
192
|
+
if den == 0:
|
|
193
|
+
den = 1.0
|
|
194
|
+
valid = np.isfinite(values) & (values > 0)
|
|
195
|
+
out[valid] = (np.log(values[valid]) - np.log(lo)) / den
|
|
196
|
+
return out
|
|
197
|
+
|
|
198
|
+
den = hi - lo
|
|
199
|
+
if den == 0:
|
|
200
|
+
den = 1.0
|
|
201
|
+
valid = np.isfinite(values)
|
|
202
|
+
out[valid] = (values[valid] - lo) / den
|
|
203
|
+
return out
|
|
204
|
+
|
|
205
|
+
def _build_jpfield_grid(
|
|
206
|
+
self,
|
|
207
|
+
x,
|
|
208
|
+
y,
|
|
209
|
+
*,
|
|
210
|
+
bin: int = 128,
|
|
211
|
+
nx: Optional[int] = None,
|
|
212
|
+
ny: Optional[int] = None,
|
|
213
|
+
xlim=None,
|
|
214
|
+
ylim=None,
|
|
215
|
+
):
|
|
216
|
+
x = np.asarray(x, dtype=float).reshape(-1)
|
|
217
|
+
y = np.asarray(y, dtype=float).reshape(-1)
|
|
218
|
+
if x.shape[0] != y.shape[0]:
|
|
219
|
+
raise ValueError("x and y must have the same length")
|
|
220
|
+
|
|
221
|
+
nx_eff = self._coerce_positive_int(nx if nx is not None else bin, 128)
|
|
222
|
+
ny_eff = self._coerce_positive_int(ny if ny is not None else bin, 128)
|
|
223
|
+
x_norm = self._normalize_jpfield_axis("x", x, bounds=xlim)
|
|
224
|
+
y_norm = self._normalize_jpfield_axis("y", y, bounds=ylim)
|
|
225
|
+
xq = np.linspace(0.0, 1.0, nx_eff)
|
|
226
|
+
yq = np.linspace(0.0, 1.0, ny_eff)
|
|
227
|
+
X, Y = np.meshgrid(xq, yq)
|
|
228
|
+
return X, Y, x_norm, y_norm
|
|
229
|
+
|
|
230
|
+
def _interpolate_jpfield_grid(
|
|
231
|
+
self,
|
|
232
|
+
x,
|
|
233
|
+
y,
|
|
234
|
+
z,
|
|
235
|
+
*,
|
|
236
|
+
interp_method: str = "natural_neighbor",
|
|
237
|
+
bin: int = 128,
|
|
238
|
+
nx: Optional[int] = None,
|
|
239
|
+
ny: Optional[int] = None,
|
|
240
|
+
xlim=None,
|
|
241
|
+
ylim=None,
|
|
242
|
+
nan_policy: str = "strict",
|
|
243
|
+
backend_options: Optional[dict[str, Any]] = None,
|
|
244
|
+
):
|
|
245
|
+
x = np.asarray(x, dtype=float).reshape(-1)
|
|
246
|
+
y = np.asarray(y, dtype=float).reshape(-1)
|
|
247
|
+
z = np.asarray(z, dtype=float).reshape(-1)
|
|
248
|
+
if x.shape[0] != y.shape[0] or x.shape[0] != z.shape[0]:
|
|
249
|
+
raise ValueError("x, y, and z must have the same length")
|
|
250
|
+
|
|
251
|
+
X, Y, x_norm, y_norm = self._build_jpfield_grid(
|
|
252
|
+
x,
|
|
253
|
+
y,
|
|
254
|
+
bin=bin,
|
|
255
|
+
nx=nx,
|
|
256
|
+
ny=ny,
|
|
257
|
+
xlim=xlim,
|
|
258
|
+
ylim=ylim,
|
|
259
|
+
)
|
|
260
|
+
try:
|
|
261
|
+
backend = resolve_backend(interp_method)
|
|
262
|
+
except Exception as exc:
|
|
263
|
+
raise ValueError(
|
|
264
|
+
f"Unsupported jpfield interpolation backend: {interp_method!r}"
|
|
265
|
+
) from exc
|
|
266
|
+
Z = backend(
|
|
267
|
+
x_norm,
|
|
268
|
+
y_norm,
|
|
269
|
+
z,
|
|
270
|
+
X,
|
|
271
|
+
Y,
|
|
272
|
+
nan_policy=nan_policy,
|
|
273
|
+
backend_options=backend_options,
|
|
274
|
+
)
|
|
275
|
+
return X, Y, np.asarray(Z, dtype=float)
|
|
276
|
+
|
|
277
|
+
def _build_jpcontour_grid(
|
|
278
|
+
self,
|
|
279
|
+
x,
|
|
280
|
+
y,
|
|
281
|
+
*,
|
|
282
|
+
bin: int = 128,
|
|
283
|
+
nx: Optional[int] = None,
|
|
284
|
+
ny: Optional[int] = None,
|
|
285
|
+
xlim=None,
|
|
286
|
+
ylim=None,
|
|
287
|
+
):
|
|
288
|
+
x = np.asarray(x, dtype=float).reshape(-1)
|
|
289
|
+
y = np.asarray(y, dtype=float).reshape(-1)
|
|
290
|
+
if x.shape[0] != y.shape[0]:
|
|
291
|
+
raise ValueError("x and y must have the same length")
|
|
292
|
+
|
|
293
|
+
if xlim is None:
|
|
294
|
+
try:
|
|
295
|
+
xlim = self.ax.get_xlim()
|
|
296
|
+
except Exception:
|
|
297
|
+
xlim = None
|
|
298
|
+
if ylim is None:
|
|
299
|
+
try:
|
|
300
|
+
ylim = self.ax.get_ylim()
|
|
301
|
+
except Exception:
|
|
302
|
+
ylim = None
|
|
303
|
+
|
|
304
|
+
nx_eff = self._coerce_positive_int(nx if nx is not None else bin, 128)
|
|
305
|
+
ny_eff = self._coerce_positive_int(ny if ny is not None else bin, 128)
|
|
306
|
+
xq = self._resolve_jpcontour_axis("x", x, npts=nx_eff, bounds=xlim)
|
|
307
|
+
yq = self._resolve_jpcontour_axis("y", y, npts=ny_eff, bounds=ylim)
|
|
308
|
+
return np.meshgrid(xq, yq)
|
|
309
|
+
|
|
310
|
+
def _interpolate_jpcontour_grid(
|
|
311
|
+
self,
|
|
312
|
+
x,
|
|
313
|
+
y,
|
|
314
|
+
z,
|
|
315
|
+
*,
|
|
316
|
+
interp_method: str = "natural_neighbor",
|
|
317
|
+
bin: int = 128,
|
|
318
|
+
nx: Optional[int] = None,
|
|
319
|
+
ny: Optional[int] = None,
|
|
320
|
+
xlim=None,
|
|
321
|
+
ylim=None,
|
|
322
|
+
nan_policy: str = "strict",
|
|
323
|
+
diagnostics: bool = False,
|
|
324
|
+
backend_options: Optional[dict[str, Any]] = None,
|
|
325
|
+
):
|
|
326
|
+
x = np.asarray(x, dtype=float).reshape(-1)
|
|
327
|
+
y = np.asarray(y, dtype=float).reshape(-1)
|
|
328
|
+
z = np.asarray(z, dtype=float).reshape(-1)
|
|
329
|
+
if x.shape[0] != y.shape[0] or x.shape[0] != z.shape[0]:
|
|
330
|
+
raise ValueError("x, y, and z must have the same length")
|
|
331
|
+
|
|
332
|
+
X, Y = self._build_jpcontour_grid(
|
|
333
|
+
x,
|
|
334
|
+
y,
|
|
335
|
+
bin=bin,
|
|
336
|
+
nx=nx,
|
|
337
|
+
ny=ny,
|
|
338
|
+
xlim=xlim,
|
|
339
|
+
ylim=ylim,
|
|
340
|
+
)
|
|
341
|
+
try:
|
|
342
|
+
backend = resolve_backend(interp_method)
|
|
343
|
+
except Exception as exc:
|
|
344
|
+
raise ValueError(
|
|
345
|
+
f"Unsupported jpcontour interpolation backend: {interp_method!r}"
|
|
346
|
+
) from exc
|
|
347
|
+
Z = backend(
|
|
348
|
+
x,
|
|
349
|
+
y,
|
|
350
|
+
z,
|
|
351
|
+
X,
|
|
352
|
+
Y,
|
|
353
|
+
nan_policy=nan_policy,
|
|
354
|
+
diagnostics=diagnostics,
|
|
355
|
+
backend_options=backend_options,
|
|
356
|
+
)
|
|
357
|
+
return X, Y, np.asarray(Z, dtype=float)
|
|
358
|
+
|
|
55
359
|
# —— Common method forwarding (add as needed) ——
|
|
56
360
|
def scatter(self, **kwargs):
|
|
57
361
|
x, y = kwargs.pop("x"), kwargs.pop("y")
|
|
@@ -71,7 +375,7 @@ class StdAxesAdapter:
|
|
|
71
375
|
df = kwargs.pop("__df__", None)
|
|
72
376
|
kw = self._merge("grid_profile", kwargs)
|
|
73
377
|
|
|
74
|
-
#
|
|
378
|
+
# Strip kwargs that are not used by grid_profile rendering.
|
|
75
379
|
for k in ("shading", "levels", "extend", "space", "marker", "s", "c"):
|
|
76
380
|
kw.pop(k, None)
|
|
77
381
|
|
|
@@ -178,6 +482,146 @@ class StdAxesAdapter:
|
|
|
178
482
|
artists = self.ax.contourf(*args, **kw)
|
|
179
483
|
return _auto_clip(artists, self.ax, self._clip_path)
|
|
180
484
|
|
|
485
|
+
def jpcontour(
|
|
486
|
+
self,
|
|
487
|
+
x,
|
|
488
|
+
y,
|
|
489
|
+
z,
|
|
490
|
+
*args,
|
|
491
|
+
interp_method: str = "natural_neighbor",
|
|
492
|
+
bin: int = 128,
|
|
493
|
+
nx: Optional[int] = None,
|
|
494
|
+
ny: Optional[int] = None,
|
|
495
|
+
xlim=None,
|
|
496
|
+
ylim=None,
|
|
497
|
+
nan_policy: str = "strict",
|
|
498
|
+
diagnostics: bool = False,
|
|
499
|
+
backend_options: Optional[dict[str, Any]] = None,
|
|
500
|
+
**kwargs,
|
|
501
|
+
):
|
|
502
|
+
kw = self._merge("contour", kwargs)
|
|
503
|
+
X, Y, Z = self._interpolate_jpcontour_grid(
|
|
504
|
+
x,
|
|
505
|
+
y,
|
|
506
|
+
z,
|
|
507
|
+
interp_method=interp_method,
|
|
508
|
+
bin=bin,
|
|
509
|
+
nx=nx,
|
|
510
|
+
ny=ny,
|
|
511
|
+
xlim=xlim,
|
|
512
|
+
ylim=ylim,
|
|
513
|
+
nan_policy=nan_policy,
|
|
514
|
+
diagnostics=diagnostics,
|
|
515
|
+
backend_options=backend_options,
|
|
516
|
+
)
|
|
517
|
+
artists = self.ax.contour(X, Y, np.ma.masked_invalid(Z), *args, **kw)
|
|
518
|
+
return _auto_clip(artists, self.ax, self._clip_path)
|
|
519
|
+
|
|
520
|
+
def jpcontourf(
|
|
521
|
+
self,
|
|
522
|
+
x,
|
|
523
|
+
y,
|
|
524
|
+
z,
|
|
525
|
+
*args,
|
|
526
|
+
interp_method: str = "natural_neighbor",
|
|
527
|
+
bin: int = 500,
|
|
528
|
+
nx: Optional[int] = None,
|
|
529
|
+
ny: Optional[int] = None,
|
|
530
|
+
xlim=None,
|
|
531
|
+
ylim=None,
|
|
532
|
+
nan_policy: str = "strict",
|
|
533
|
+
diagnostics: bool = False,
|
|
534
|
+
backend_options: Optional[dict[str, Any]] = None,
|
|
535
|
+
**kwargs,
|
|
536
|
+
):
|
|
537
|
+
kw = self._merge("contourf", kwargs)
|
|
538
|
+
X, Y, Z = self._interpolate_jpcontour_grid(
|
|
539
|
+
x,
|
|
540
|
+
y,
|
|
541
|
+
z,
|
|
542
|
+
interp_method=interp_method,
|
|
543
|
+
bin=bin,
|
|
544
|
+
nx=nx,
|
|
545
|
+
ny=ny,
|
|
546
|
+
xlim=xlim,
|
|
547
|
+
ylim=ylim,
|
|
548
|
+
nan_policy=nan_policy,
|
|
549
|
+
diagnostics=diagnostics,
|
|
550
|
+
backend_options=backend_options,
|
|
551
|
+
)
|
|
552
|
+
artists = self.ax.contourf(X, Y, np.ma.masked_invalid(Z), *args, **kw)
|
|
553
|
+
return _auto_clip(artists, self.ax, self._clip_path)
|
|
554
|
+
|
|
555
|
+
def jpfield(
|
|
556
|
+
self,
|
|
557
|
+
x,
|
|
558
|
+
y,
|
|
559
|
+
z,
|
|
560
|
+
*args,
|
|
561
|
+
interp_method: str = "natural_neighbor",
|
|
562
|
+
bin: int = 128,
|
|
563
|
+
nx: Optional[int] = None,
|
|
564
|
+
ny: Optional[int] = None,
|
|
565
|
+
xlim=None,
|
|
566
|
+
ylim=None,
|
|
567
|
+
nan_policy: str = "strict",
|
|
568
|
+
backend_options: Optional[dict[str, Any]] = None,
|
|
569
|
+
shading: str = "auto",
|
|
570
|
+
**kwargs,
|
|
571
|
+
):
|
|
572
|
+
"""Scattered-data field plot.
|
|
573
|
+
|
|
574
|
+
Interpolates scattered x/y/z samples onto a regular grid in axes-fraction
|
|
575
|
+
space, then renders the result with pcolormesh(transform=ax.transAxes).
|
|
576
|
+
The plot is drawn from explicit cell edges so raster backends do not
|
|
577
|
+
introduce a half-cell seam.
|
|
578
|
+
Undefined regions remain masked. Contour-only kwargs such as levels /
|
|
579
|
+
colors / extend are ignored.
|
|
580
|
+
"""
|
|
581
|
+
kw = self._merge("pcolormesh", kwargs)
|
|
582
|
+
for k in ("levels", "extend", "locator", "corner_mask", "colors", "linestyles"):
|
|
583
|
+
kw.pop(k, None)
|
|
584
|
+
kw.pop("shading", None)
|
|
585
|
+
kw.pop("transform", None)
|
|
586
|
+
X, Y, Z = self._interpolate_jpfield_grid(
|
|
587
|
+
x,
|
|
588
|
+
y,
|
|
589
|
+
z,
|
|
590
|
+
interp_method=interp_method,
|
|
591
|
+
bin=bin,
|
|
592
|
+
nx=nx,
|
|
593
|
+
ny=ny,
|
|
594
|
+
xlim=xlim,
|
|
595
|
+
ylim=ylim,
|
|
596
|
+
nan_policy=nan_policy,
|
|
597
|
+
backend_options=backend_options,
|
|
598
|
+
)
|
|
599
|
+
X = np.asarray(X, dtype=float)
|
|
600
|
+
Y = np.asarray(Y, dtype=float)
|
|
601
|
+
Z = np.ma.masked_invalid(np.asarray(Z, dtype=float))
|
|
602
|
+
if str(shading).lower() in {"auto", "flat"}:
|
|
603
|
+
ny, nx = Z.shape
|
|
604
|
+
xe = np.linspace(0.0, 1.0, nx + 1)
|
|
605
|
+
ye = np.linspace(0.0, 1.0, ny + 1)
|
|
606
|
+
X, Y = np.meshgrid(xe, ye)
|
|
607
|
+
shading = "flat"
|
|
608
|
+
else:
|
|
609
|
+
shading = str(shading)
|
|
610
|
+
kw.setdefault("edgecolors", "none")
|
|
611
|
+
kw.setdefault("linewidth", 0.0)
|
|
612
|
+
kw.setdefault("antialiased", False)
|
|
613
|
+
kw.setdefault("snap", True)
|
|
614
|
+
artists = self.ax.pcolormesh(
|
|
615
|
+
X,
|
|
616
|
+
Y,
|
|
617
|
+
Z,
|
|
618
|
+
*args,
|
|
619
|
+
transform=self.ax.transAxes,
|
|
620
|
+
shading=shading,
|
|
621
|
+
**kw,
|
|
622
|
+
)
|
|
623
|
+
return _auto_clip(artists, self.ax, self._clip_path)
|
|
624
|
+
|
|
181
625
|
def imshow(self, *args, **kwargs):
|
|
182
626
|
kw = self._merge("imshow", kwargs)
|
|
183
627
|
artists = self.ax.imshow(*args, **kw)
|
|
@@ -87,3 +87,24 @@ class TernaryAxesAdapter(StdAxesAdapter):
|
|
|
87
87
|
return super().tricontourf(**kwargs)
|
|
88
88
|
# else:
|
|
89
89
|
# raise ValueError("scatter() needs either (a,b,c) or (x,y) inputs")
|
|
90
|
+
|
|
91
|
+
def jpcontour(self, *args, **kwargs):
|
|
92
|
+
if {"left", "right", "bottom"}.issubset(kwargs.keys()):
|
|
93
|
+
x, y = self._lbr_to_xy(kwargs.pop("left"), kwargs.pop("right"), kwargs.pop("bottom"))
|
|
94
|
+
kwargs["x"] = x
|
|
95
|
+
kwargs["y"] = y
|
|
96
|
+
return super().jpcontour(*args, **kwargs)
|
|
97
|
+
|
|
98
|
+
def jpcontourf(self, *args, **kwargs):
|
|
99
|
+
if {"left", "right", "bottom"}.issubset(kwargs.keys()):
|
|
100
|
+
x, y = self._lbr_to_xy(kwargs.pop("left"), kwargs.pop("right"), kwargs.pop("bottom"))
|
|
101
|
+
kwargs["x"] = x
|
|
102
|
+
kwargs["y"] = y
|
|
103
|
+
return super().jpcontourf(*args, **kwargs)
|
|
104
|
+
|
|
105
|
+
def jpfield(self, *args, **kwargs):
|
|
106
|
+
if {"left", "right", "bottom"}.issubset(kwargs.keys()):
|
|
107
|
+
x, y = self._lbr_to_xy(kwargs.pop("left"), kwargs.pop("right"), kwargs.pop("bottom"))
|
|
108
|
+
kwargs["x"] = x
|
|
109
|
+
kwargs["y"] = y
|
|
110
|
+
return super().jpfield(*args, **kwargs)
|
|
@@ -21,8 +21,7 @@ def axc_color_config(frame: Mapping[str, Any], cb_name: str = "axc") -> dict:
|
|
|
21
21
|
"""Return normalized color config for a named colorbar axis.
|
|
22
22
|
|
|
23
23
|
Reads frame[cb_name].color. A value of None or the string "auto" means
|
|
24
|
-
auto-range from data.
|
|
25
|
-
yscale when 'scale' is absent from the color block.
|
|
24
|
+
auto-range from data.
|
|
26
25
|
"""
|
|
27
26
|
out: dict[str, Any] = {}
|
|
28
27
|
cb_cfg = frame.get(cb_name, {}) if isinstance(frame, Mapping) else {}
|
|
@@ -49,12 +48,6 @@ def axc_color_config(frame: Mapping[str, Any], cb_name: str = "axc") -> dict:
|
|
|
49
48
|
pass # non-numeric treated as auto
|
|
50
49
|
|
|
51
50
|
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
51
|
if isinstance(scale, str):
|
|
59
52
|
scale = scale.strip().lower()
|
|
60
53
|
if scale:
|
|
@@ -77,6 +70,27 @@ def _coerce_scalar(v) -> Optional[float]:
|
|
|
77
70
|
return None
|
|
78
71
|
|
|
79
72
|
|
|
73
|
+
def _is_numeric_color_source(spec) -> bool:
|
|
74
|
+
"""Return True when a color spec looks like numeric scalar/mapping data.
|
|
75
|
+
|
|
76
|
+
We treat expression dictionaries and numeric array-likes as color sources,
|
|
77
|
+
but ignore plain strings such as literal colors or colormap names.
|
|
78
|
+
"""
|
|
79
|
+
if spec is None:
|
|
80
|
+
return False
|
|
81
|
+
if isinstance(spec, str):
|
|
82
|
+
return False
|
|
83
|
+
if isinstance(spec, Mapping):
|
|
84
|
+
return "expr" in spec
|
|
85
|
+
try:
|
|
86
|
+
arr = np.asarray(spec, dtype=float)
|
|
87
|
+
except Exception:
|
|
88
|
+
return False
|
|
89
|
+
if arr.size == 0:
|
|
90
|
+
return False
|
|
91
|
+
return bool(np.isfinite(arr).any())
|
|
92
|
+
|
|
93
|
+
|
|
80
94
|
def _resolve_norm(nv, *, vmin=None, vmax=None, logger=None):
|
|
81
95
|
if nv is None:
|
|
82
96
|
return None
|
|
@@ -126,20 +140,37 @@ def _resolve_norm(nv, *, vmin=None, vmax=None, logger=None):
|
|
|
126
140
|
|
|
127
141
|
_COLORED_Z_METHODS = frozenset({
|
|
128
142
|
"contour", "contourf", "tricontour", "tricontourf",
|
|
143
|
+
"jpcontour", "jpcontourf", "jpfield",
|
|
129
144
|
"tripcolor", "tripcolor_axes",
|
|
130
145
|
"pcolor", "pcolormesh", "imshow",
|
|
131
146
|
"voronoi", "voronoif",
|
|
132
|
-
"grid_profile",
|
|
147
|
+
"grid_profile",
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
_CONTOUR_LIKE_METHODS = frozenset({
|
|
151
|
+
"contour", "contourf", "tricontour", "tricontourf",
|
|
152
|
+
"jpcontour", "jpcontourf",
|
|
133
153
|
})
|
|
134
154
|
|
|
135
155
|
|
|
136
156
|
def layer_uses_color(style: dict, coor: dict, method_key: str) -> bool:
|
|
137
157
|
"""Return True if this layer will need a colorbar."""
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
158
|
+
c_src = coor.get("c")
|
|
159
|
+
z_src = coor.get("z")
|
|
160
|
+
|
|
161
|
+
if _is_numeric_color_source(c_src):
|
|
162
|
+
return True
|
|
163
|
+
if method_key in _COLORED_Z_METHODS and _is_numeric_color_source(z_src):
|
|
164
|
+
return True
|
|
165
|
+
|
|
166
|
+
style_c = style.get("c")
|
|
167
|
+
style_z = style.get("z")
|
|
168
|
+
if _is_numeric_color_source(style_c):
|
|
169
|
+
return True
|
|
170
|
+
if method_key in _COLORED_Z_METHODS and _is_numeric_color_source(style_z):
|
|
171
|
+
return True
|
|
172
|
+
|
|
173
|
+
return False
|
|
143
174
|
|
|
144
175
|
|
|
145
176
|
def collect_layer_color_range(df, coor: dict, style: dict, *, scale: str | None = None):
|
|
@@ -328,6 +359,24 @@ def collect_and_attach_colorbar(
|
|
|
328
359
|
explicit frame.axc.color.vmin/vmax win; absent values fall back to
|
|
329
360
|
the data-driven auto range.
|
|
330
361
|
"""
|
|
362
|
+
if not layer_uses_color(style, coor, method_key):
|
|
363
|
+
return style
|
|
364
|
+
|
|
365
|
+
# If the layer already declares an explicit contour color, do not inject a
|
|
366
|
+
# colormap. Matplotlib treats `colors` and `cmap` as mutually exclusive for
|
|
367
|
+
# contour-style artists, and overlay layers often want a fixed line color
|
|
368
|
+
# while another layer owns the shared colorbar.
|
|
369
|
+
if method_key in _CONTOUR_LIKE_METHODS and (
|
|
370
|
+
"colors" in style or "color" in style
|
|
371
|
+
):
|
|
372
|
+
s = dict(style)
|
|
373
|
+
s.pop("cmap", None)
|
|
374
|
+
s.pop("norm", None)
|
|
375
|
+
s.pop("vmin", None)
|
|
376
|
+
s.pop("vmax", None)
|
|
377
|
+
s.pop("mode", None)
|
|
378
|
+
return s
|
|
379
|
+
|
|
331
380
|
axc = fig.axes.get(cb_name)
|
|
332
381
|
if axc is None or not hasattr(axc, "_cb"):
|
|
333
382
|
return style
|
|
@@ -377,7 +426,7 @@ def collect_and_attach_colorbar(
|
|
|
377
426
|
|
|
378
427
|
# Contour levels (lazily generated on first contour layer, then reused)
|
|
379
428
|
if (
|
|
380
|
-
method_key in ("contour", "contourf", "tricontour", "tricontourf")
|
|
429
|
+
method_key in ("contour", "contourf", "tricontour", "tricontourf", "jpcontour", "jpcontourf")
|
|
381
430
|
and axc._cb.get("levels") is None
|
|
382
431
|
and axc._cb.get("vmin") is not None
|
|
383
432
|
and axc._cb.get("vmax") is not None
|