jarvisplot 1.3.5__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.
Files changed (84) hide show
  1. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/JarvisPLOT.egg-info/PKG-INFO +1 -1
  2. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/JarvisPLOT.egg-info/SOURCES.txt +5 -2
  3. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/PKG-INFO +1 -1
  4. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/Figure/adapters_rect.py +445 -1
  5. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/Figure/adapters_ternary.py +21 -0
  6. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/Figure/colorbar_runtime.py +64 -15
  7. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/Figure/config_runtime.py +8 -7
  8. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/Figure/figure.py +18 -58
  9. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/Figure/helper.py +1 -1
  10. jarvisplot-1.3.6/jarvisplot/Figure/interp_natural_neighbor.py +507 -0
  11. jarvisplot-1.3.6/jarvisplot/Figure/interp_natural_neighbor_exact.py +1064 -0
  12. jarvisplot-1.3.6/jarvisplot/Figure/layer_runtime.py +642 -0
  13. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/Figure/layout_runtime.py +1 -1
  14. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/Figure/method_registry.py +17 -79
  15. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/Figure/preprocessor.py +11 -42
  16. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/Figure/preprocessor_runtime.py +170 -7
  17. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/Figure/profile_runtime.py +1 -1
  18. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/core.py +23 -48
  19. jarvisplot-1.3.6/jarvisplot/core_assets.py +68 -0
  20. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/core_runtime.py +0 -59
  21. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/data_loader.py +10 -110
  22. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/data_loader_runtime.py +47 -61
  23. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/inner_func.py +2 -2
  24. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/utils/cmaps.py +1 -14
  25. jarvisplot-1.3.6/jarvisplot/utils/dataframes.py +56 -0
  26. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/pyproject.toml +1 -1
  27. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/tests/test_data_loader_contracts.py +122 -207
  28. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/tests/test_docs_status_labels.py +5 -1
  29. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/tests/test_figure_contracts.py +281 -15
  30. jarvisplot-1.3.6/tests/test_interp_natural_neighbor.py +390 -0
  31. jarvisplot-1.3.6/tests/test_interpolator_contracts.py +44 -0
  32. jarvisplot-1.3.6/tests/test_jpcontour_methods.py +629 -0
  33. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/tests/test_parse_data_metadata.py +0 -1
  34. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/tests/test_pathing_contracts.py +8 -4
  35. jarvisplot-1.3.6/tests/test_template_json.py +26 -0
  36. jarvisplot-1.3.5/jarvisplot/Figure/adapters.py +0 -6
  37. jarvisplot-1.3.5/jarvisplot/Figure/layer_runtime.py +0 -309
  38. jarvisplot-1.3.5/jarvisplot/Figure/load_data.py +0 -169
  39. jarvisplot-1.3.5/jarvisplot/core_assets.py +0 -63
  40. jarvisplot-1.3.5/tests/test_interpolator_contracts.py +0 -9
  41. jarvisplot-1.3.5/tests/test_template_json.py +0 -10
  42. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/JarvisPLOT.egg-info/dependency_links.txt +0 -0
  43. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/JarvisPLOT.egg-info/entry_points.txt +0 -0
  44. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/JarvisPLOT.egg-info/requires.txt +0 -0
  45. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/JarvisPLOT.egg-info/top_level.txt +0 -0
  46. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/README.md +0 -0
  47. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/Figure/__init__.py +0 -0
  48. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/Figure/cards/std_axes_adapter_config.json +0 -0
  49. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/Figure/data_pipelines.py +0 -0
  50. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/Figure/style_runtime.py +0 -0
  51. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/__init__.py +0 -0
  52. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/cache_store.py +0 -0
  53. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/cards/a4paper/1x1/ternary.json +0 -0
  54. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/cards/a4paper/2x1/rect.json +0 -0
  55. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/cards/a4paper/2x1/rect5x1.json +0 -0
  56. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/cards/a4paper/2x1/rect_cmap.json +0 -0
  57. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/cards/a4paper/2x1/ternary.json +0 -0
  58. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/cards/a4paper/2x1/ternary_cmap.json +0 -0
  59. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/cards/a4paper/4x1/rect.json +0 -0
  60. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/cards/a4paper/4x1/rect_cmap.json +0 -0
  61. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/cards/a4paper/4x1/ternary.json +0 -0
  62. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/cards/a4paper/4x1/ternary_cmap.json +0 -0
  63. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/cards/args.json +0 -0
  64. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/cards/colors/colormaps.json +0 -0
  65. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/cards/default/output.json +0 -0
  66. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/cards/gambit/1x1/ternary.json +0 -0
  67. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/cards/gambit/2x1/rect_cmap.json +0 -0
  68. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/cards/gambit/2x1/ternary.json +0 -0
  69. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/cards/gambit/2x1/ternary_cmap.json +0 -0
  70. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/cards/icons/JarvisHEP.png +0 -0
  71. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/cards/icons/gambit.png +0 -0
  72. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/cards/icons/gambit_small.png +0 -0
  73. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/cards/style_preference.json +0 -0
  74. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/cli.py +0 -0
  75. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/client.py +0 -0
  76. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/config.py +0 -0
  77. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/data_loader_hdf5.py +0 -0
  78. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/data_loader_summary.py +0 -0
  79. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/memtrace.py +0 -0
  80. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/utils/__init__.py +0 -0
  81. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/utils/expression.py +0 -0
  82. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/utils/interpolator.py +0 -0
  83. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/jarvisplot/utils/pathing.py +0 -0
  84. {jarvisplot-1.3.5 → jarvisplot-1.3.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: JarvisPLOT
3
- Version: 1.3.5
3
+ Version: 1.3.6
4
4
  Summary: Jarvis-PLOT: YAML-driven plotting engine
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: JarvisPLOT
3
- Version: 1.3.5
3
+ Version: 1.3.6
4
4
  Summary: Jarvis-PLOT: YAML-driven plotting engine
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -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
- # Keep compatibility with configs copied from tripcolor/scatter.
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. Falls back to legacy frame[cb_name].xscale /
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", "grid_profiling",
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
- return (
139
- bool(style.get("cmap"))
140
- or ("c" in coor)
141
- or (("z" in coor) and (method_key in _COLORED_Z_METHODS))
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