jarvisplot 1.3.5__tar.gz → 1.3.7__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.7}/JarvisPLOT.egg-info/PKG-INFO +1 -1
  2. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/JarvisPLOT.egg-info/SOURCES.txt +5 -2
  3. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/PKG-INFO +1 -1
  4. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/Figure/adapters_rect.py +471 -1
  5. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/Figure/adapters_ternary.py +21 -0
  6. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/Figure/colorbar_runtime.py +64 -15
  7. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/Figure/config_runtime.py +8 -7
  8. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/Figure/figure.py +18 -58
  9. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/Figure/helper.py +1 -1
  10. jarvisplot-1.3.7/jarvisplot/Figure/interp_natural_neighbor.py +507 -0
  11. jarvisplot-1.3.7/jarvisplot/Figure/interp_natural_neighbor_exact.py +1068 -0
  12. jarvisplot-1.3.7/jarvisplot/Figure/layer_runtime.py +642 -0
  13. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/Figure/layout_runtime.py +1 -1
  14. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/Figure/method_registry.py +17 -79
  15. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/Figure/preprocessor.py +11 -42
  16. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/Figure/preprocessor_runtime.py +170 -7
  17. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/Figure/profile_runtime.py +1 -1
  18. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/core.py +23 -48
  19. jarvisplot-1.3.7/jarvisplot/core_assets.py +68 -0
  20. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/core_runtime.py +0 -59
  21. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/data_loader.py +10 -110
  22. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/data_loader_runtime.py +47 -61
  23. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/inner_func.py +2 -2
  24. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/utils/cmaps.py +1 -14
  25. jarvisplot-1.3.7/jarvisplot/utils/dataframes.py +56 -0
  26. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/pyproject.toml +1 -1
  27. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/tests/test_data_loader_contracts.py +122 -207
  28. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/tests/test_docs_status_labels.py +5 -1
  29. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/tests/test_figure_contracts.py +281 -15
  30. jarvisplot-1.3.7/tests/test_interp_natural_neighbor.py +391 -0
  31. jarvisplot-1.3.7/tests/test_interpolator_contracts.py +44 -0
  32. jarvisplot-1.3.7/tests/test_jpcontour_methods.py +746 -0
  33. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/tests/test_parse_data_metadata.py +0 -1
  34. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/tests/test_pathing_contracts.py +8 -4
  35. jarvisplot-1.3.7/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.7}/JarvisPLOT.egg-info/dependency_links.txt +0 -0
  43. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/JarvisPLOT.egg-info/entry_points.txt +0 -0
  44. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/JarvisPLOT.egg-info/requires.txt +0 -0
  45. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/JarvisPLOT.egg-info/top_level.txt +0 -0
  46. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/README.md +0 -0
  47. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/Figure/__init__.py +0 -0
  48. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/Figure/cards/std_axes_adapter_config.json +0 -0
  49. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/Figure/data_pipelines.py +0 -0
  50. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/Figure/style_runtime.py +0 -0
  51. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/__init__.py +0 -0
  52. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/cache_store.py +0 -0
  53. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/cards/a4paper/1x1/ternary.json +0 -0
  54. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/cards/a4paper/2x1/rect.json +0 -0
  55. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/cards/a4paper/2x1/rect5x1.json +0 -0
  56. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/cards/a4paper/2x1/rect_cmap.json +0 -0
  57. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/cards/a4paper/2x1/ternary.json +0 -0
  58. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/cards/a4paper/2x1/ternary_cmap.json +0 -0
  59. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/cards/a4paper/4x1/rect.json +0 -0
  60. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/cards/a4paper/4x1/rect_cmap.json +0 -0
  61. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/cards/a4paper/4x1/ternary.json +0 -0
  62. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/cards/a4paper/4x1/ternary_cmap.json +0 -0
  63. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/cards/args.json +0 -0
  64. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/cards/colors/colormaps.json +0 -0
  65. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/cards/default/output.json +0 -0
  66. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/cards/gambit/1x1/ternary.json +0 -0
  67. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/cards/gambit/2x1/rect_cmap.json +0 -0
  68. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/cards/gambit/2x1/ternary.json +0 -0
  69. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/cards/gambit/2x1/ternary_cmap.json +0 -0
  70. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/cards/icons/JarvisHEP.png +0 -0
  71. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/cards/icons/gambit.png +0 -0
  72. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/cards/icons/gambit_small.png +0 -0
  73. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/cards/style_preference.json +0 -0
  74. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/cli.py +0 -0
  75. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/client.py +0 -0
  76. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/config.py +0 -0
  77. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/data_loader_hdf5.py +0 -0
  78. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/data_loader_summary.py +0 -0
  79. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/memtrace.py +0 -0
  80. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/utils/__init__.py +0 -0
  81. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/utils/expression.py +0 -0
  82. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/utils/interpolator.py +0 -0
  83. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/jarvisplot/utils/pathing.py +0 -0
  84. {jarvisplot-1.3.5 → jarvisplot-1.3.7}/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.7
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.7
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,16 @@ 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
16
+
17
+ DEFAULT_JP_SAMPLE_GRID = 500
14
18
 
15
19
 
16
20
  # —— Basic Adapter: Forward to the underlying Axes, merge default parameters, perform automatic clipping ——
@@ -52,6 +56,314 @@ class StdAxesAdapter:
52
56
  base.update(kwargs or {})
53
57
  return base
54
58
 
59
+ @staticmethod
60
+ def _coerce_positive_int(value, default: int) -> int:
61
+ try:
62
+ n = int(value)
63
+ if n > 0:
64
+ return n
65
+ except Exception:
66
+ pass
67
+ return int(default)
68
+
69
+ def _resolve_jpcontour_axis(self, axis_name: str, values, *, npts: int, bounds=None):
70
+ values = np.asarray(values, dtype=float).reshape(-1)
71
+ finite = values[np.isfinite(values)]
72
+
73
+ try:
74
+ scale = str(self.ax.get_xscale() if axis_name == "x" else self.ax.get_yscale()).lower()
75
+ except Exception:
76
+ scale = "linear"
77
+
78
+ if bounds is None:
79
+ if finite.size:
80
+ lo = float(np.min(finite))
81
+ hi = float(np.max(finite))
82
+ else:
83
+ try:
84
+ lo, hi = self.ax.get_xlim() if axis_name == "x" else self.ax.get_ylim()
85
+ lo = float(lo)
86
+ hi = float(hi)
87
+ except Exception:
88
+ lo, hi = 0.0, 1.0
89
+ else:
90
+ try:
91
+ lo = float(bounds[0])
92
+ hi = float(bounds[1])
93
+ except Exception:
94
+ lo, hi = 0.0, 1.0
95
+
96
+ if not np.isfinite(lo) or not np.isfinite(hi):
97
+ if finite.size:
98
+ lo = float(np.min(finite))
99
+ hi = float(np.max(finite))
100
+ else:
101
+ lo, hi = 0.0, 1.0
102
+
103
+ if lo == hi:
104
+ if scale == "log" and lo > 0:
105
+ hi = lo * 10.0
106
+ else:
107
+ hi = lo + 1.0
108
+
109
+ if scale == "log":
110
+ positive = finite[finite > 0]
111
+ if lo <= 0 or hi <= 0:
112
+ if positive.size:
113
+ lo = float(np.min(positive))
114
+ hi = float(np.max(positive))
115
+ else:
116
+ warnings.warn(
117
+ f"jpcontour: {axis_name}-axis is log-scaled but has no positive finite values; "
118
+ "falling back to a linear grid",
119
+ RuntimeWarning,
120
+ stacklevel=3,
121
+ )
122
+ return np.linspace(lo, hi, npts)
123
+ lo = max(lo, np.finfo(float).tiny)
124
+ hi = max(hi, lo * 10.0)
125
+ return np.geomspace(lo, hi, npts)
126
+
127
+ return np.linspace(lo, hi, npts)
128
+
129
+ def _normalize_jpfield_axis(self, axis_name: str, values, *, bounds=None):
130
+ """Map data coordinates onto axes-fraction coordinates in [0, 1].
131
+
132
+ This is used by jpfield so interpolation and rendering both happen in
133
+ the axes transform frame, avoiding mixed log/linear geometry.
134
+ """
135
+ values = np.asarray(values, dtype=float).reshape(-1)
136
+ out = np.full(values.shape, np.nan, dtype=float)
137
+
138
+ try:
139
+ scale = str(self.ax.get_xscale() if axis_name == "x" else self.ax.get_yscale()).lower()
140
+ except Exception:
141
+ scale = "linear"
142
+
143
+ if bounds is None:
144
+ try:
145
+ bounds = self.ax.get_xlim() if axis_name == "x" else self.ax.get_ylim()
146
+ except Exception:
147
+ bounds = None
148
+
149
+ if bounds is None:
150
+ finite = values[np.isfinite(values)]
151
+ if finite.size:
152
+ lo = float(np.min(finite))
153
+ hi = float(np.max(finite))
154
+ else:
155
+ lo, hi = 0.0, 1.0
156
+ else:
157
+ try:
158
+ lo = float(bounds[0])
159
+ hi = float(bounds[1])
160
+ except Exception:
161
+ lo, hi = 0.0, 1.0
162
+
163
+ if not np.isfinite(lo) or not np.isfinite(hi):
164
+ finite = values[np.isfinite(values)]
165
+ if finite.size:
166
+ lo = float(np.min(finite))
167
+ hi = float(np.max(finite))
168
+ else:
169
+ lo, hi = 0.0, 1.0
170
+
171
+ if lo == hi:
172
+ if scale == "log" and lo > 0:
173
+ hi = lo * 10.0
174
+ else:
175
+ hi = lo + 1.0
176
+
177
+ if scale == "log":
178
+ positive = values[np.isfinite(values) & (values > 0)]
179
+ if lo <= 0 or hi <= 0:
180
+ if positive.size:
181
+ lo = float(np.min(positive))
182
+ hi = float(np.max(positive))
183
+ else:
184
+ warnings.warn(
185
+ f"jpfield: {axis_name}-axis is log-scaled but has no positive finite values; "
186
+ "rendering an empty field in axes-fraction space",
187
+ RuntimeWarning,
188
+ stacklevel=3,
189
+ )
190
+ return out
191
+ lo = max(lo, np.finfo(float).tiny)
192
+ hi = max(hi, lo * 10.0)
193
+ den = np.log(hi) - np.log(lo)
194
+ if den == 0:
195
+ den = 1.0
196
+ valid = np.isfinite(values) & (values > 0)
197
+ out[valid] = (np.log(values[valid]) - np.log(lo)) / den
198
+ return out
199
+
200
+ den = hi - lo
201
+ if den == 0:
202
+ den = 1.0
203
+ valid = np.isfinite(values)
204
+ out[valid] = (values[valid] - lo) / den
205
+ return out
206
+
207
+ def _build_jpfield_grid(
208
+ self,
209
+ x,
210
+ y,
211
+ *,
212
+ bin: int = DEFAULT_JP_SAMPLE_GRID,
213
+ nx: Optional[int] = None,
214
+ ny: Optional[int] = None,
215
+ xlim=None,
216
+ ylim=None,
217
+ ):
218
+ x = np.asarray(x, dtype=float).reshape(-1)
219
+ y = np.asarray(y, dtype=float).reshape(-1)
220
+ if x.shape[0] != y.shape[0]:
221
+ raise ValueError("x and y must have the same length")
222
+
223
+ nx_eff = self._coerce_positive_int(nx if nx is not None else bin, DEFAULT_JP_SAMPLE_GRID)
224
+ ny_eff = self._coerce_positive_int(ny if ny is not None else bin, DEFAULT_JP_SAMPLE_GRID)
225
+ x_norm = self._normalize_jpfield_axis("x", x, bounds=xlim)
226
+ y_norm = self._normalize_jpfield_axis("y", y, bounds=ylim)
227
+ xq = np.linspace(0.0, 1.0, nx_eff)
228
+ yq = np.linspace(0.0, 1.0, ny_eff)
229
+ X, Y = np.meshgrid(xq, yq)
230
+ return X, Y, x_norm, y_norm
231
+
232
+ def _interpolate_jpfield_grid(
233
+ self,
234
+ x,
235
+ y,
236
+ z,
237
+ *,
238
+ interp_method: str = "natural_neighbor",
239
+ bin: int = DEFAULT_JP_SAMPLE_GRID,
240
+ nx: Optional[int] = None,
241
+ ny: Optional[int] = None,
242
+ xlim=None,
243
+ ylim=None,
244
+ nan_policy: str = "strict",
245
+ backend_options: Optional[dict[str, Any]] = None,
246
+ ):
247
+ x = np.asarray(x, dtype=float).reshape(-1)
248
+ y = np.asarray(y, dtype=float).reshape(-1)
249
+ z = np.asarray(z, dtype=float).reshape(-1)
250
+ if x.shape[0] != y.shape[0] or x.shape[0] != z.shape[0]:
251
+ raise ValueError("x, y, and z must have the same length")
252
+
253
+ X, Y, x_norm, y_norm = self._build_jpfield_grid(
254
+ x,
255
+ y,
256
+ bin=bin,
257
+ nx=nx,
258
+ ny=ny,
259
+ xlim=xlim,
260
+ ylim=ylim,
261
+ )
262
+ try:
263
+ backend = resolve_backend(interp_method)
264
+ except Exception as exc:
265
+ raise ValueError(
266
+ f"Unsupported jpfield interpolation backend: {interp_method!r}"
267
+ ) from exc
268
+ Z = backend(
269
+ x_norm,
270
+ y_norm,
271
+ z,
272
+ X,
273
+ Y,
274
+ nan_policy=nan_policy,
275
+ backend_options=backend_options,
276
+ )
277
+ return X, Y, np.asarray(Z, dtype=float)
278
+
279
+ def _build_jpcontour_grid(
280
+ self,
281
+ x,
282
+ y,
283
+ *,
284
+ bin: int = DEFAULT_JP_SAMPLE_GRID,
285
+ nx: Optional[int] = None,
286
+ ny: Optional[int] = None,
287
+ xlim=None,
288
+ ylim=None,
289
+ ):
290
+ x = np.asarray(x, dtype=float).reshape(-1)
291
+ y = np.asarray(y, dtype=float).reshape(-1)
292
+ if x.shape[0] != y.shape[0]:
293
+ raise ValueError("x and y must have the same length")
294
+
295
+ if xlim is None:
296
+ try:
297
+ xlim = self.ax.get_xlim()
298
+ except Exception:
299
+ xlim = None
300
+ if ylim is None:
301
+ try:
302
+ ylim = self.ax.get_ylim()
303
+ except Exception:
304
+ ylim = None
305
+
306
+ nx_eff = self._coerce_positive_int(nx if nx is not None else bin, DEFAULT_JP_SAMPLE_GRID)
307
+ ny_eff = self._coerce_positive_int(ny if ny is not None else bin, DEFAULT_JP_SAMPLE_GRID)
308
+ xq = self._resolve_jpcontour_axis("x", x, npts=nx_eff, bounds=xlim)
309
+ yq = self._resolve_jpcontour_axis("y", y, npts=ny_eff, bounds=ylim)
310
+ return np.meshgrid(xq, yq)
311
+
312
+ def _interpolate_jpcontour_grid(
313
+ self,
314
+ x,
315
+ y,
316
+ z,
317
+ *,
318
+ interp_method: str = "natural_neighbor",
319
+ bin: int = DEFAULT_JP_SAMPLE_GRID,
320
+ nx: Optional[int] = None,
321
+ ny: Optional[int] = None,
322
+ xlim=None,
323
+ ylim=None,
324
+ nan_policy: str = "strict",
325
+ diagnostics: bool = False,
326
+ backend_options: Optional[dict[str, Any]] = None,
327
+ ):
328
+ """Interpolate scattered data on an axes-fraction grid for contour rendering.
329
+
330
+ This keeps jpcontour/jpcontourf aligned with jpfield: coordinates are
331
+ normalized into the current Axes frame before interpolation, then the
332
+ contour artist is drawn with transform=ax.transAxes.
333
+ """
334
+ x = np.asarray(x, dtype=float).reshape(-1)
335
+ y = np.asarray(y, dtype=float).reshape(-1)
336
+ z = np.asarray(z, dtype=float).reshape(-1)
337
+ if x.shape[0] != y.shape[0] or x.shape[0] != z.shape[0]:
338
+ raise ValueError("x, y, and z must have the same length")
339
+
340
+ X, Y, x_norm, y_norm = self._build_jpfield_grid(
341
+ x,
342
+ y,
343
+ bin=bin,
344
+ nx=nx,
345
+ ny=ny,
346
+ xlim=xlim,
347
+ ylim=ylim,
348
+ )
349
+ try:
350
+ backend = resolve_backend(interp_method)
351
+ except Exception as exc:
352
+ raise ValueError(
353
+ f"Unsupported jpcontour interpolation backend: {interp_method!r}"
354
+ ) from exc
355
+ Z = backend(
356
+ x_norm,
357
+ y_norm,
358
+ z,
359
+ X,
360
+ Y,
361
+ nan_policy=nan_policy,
362
+ diagnostics=diagnostics,
363
+ backend_options=backend_options,
364
+ )
365
+ return X, Y, np.asarray(Z, dtype=float)
366
+
55
367
  # —— Common method forwarding (add as needed) ——
56
368
  def scatter(self, **kwargs):
57
369
  x, y = kwargs.pop("x"), kwargs.pop("y")
@@ -71,7 +383,7 @@ class StdAxesAdapter:
71
383
  df = kwargs.pop("__df__", None)
72
384
  kw = self._merge("grid_profile", kwargs)
73
385
 
74
- # Keep compatibility with configs copied from tripcolor/scatter.
386
+ # Strip kwargs that are not used by grid_profile rendering.
75
387
  for k in ("shading", "levels", "extend", "space", "marker", "s", "c"):
76
388
  kw.pop(k, None)
77
389
 
@@ -178,6 +490,164 @@ class StdAxesAdapter:
178
490
  artists = self.ax.contourf(*args, **kw)
179
491
  return _auto_clip(artists, self.ax, self._clip_path)
180
492
 
493
+ def jpcontour(
494
+ self,
495
+ x,
496
+ y,
497
+ z,
498
+ *args,
499
+ interp_method: str = "natural_neighbor",
500
+ bin: int = DEFAULT_JP_SAMPLE_GRID,
501
+ nx: Optional[int] = None,
502
+ ny: Optional[int] = None,
503
+ xlim=None,
504
+ ylim=None,
505
+ nan_policy: str = "strict",
506
+ diagnostics: bool = False,
507
+ backend_options: Optional[dict[str, Any]] = None,
508
+ **kwargs,
509
+ ):
510
+ """Scattered-data contour plot rendered in axes-fraction coordinates."""
511
+ kw = self._merge("contour", kwargs)
512
+ kw.pop("transform", None)
513
+ X, Y, Z = self._interpolate_jpcontour_grid(
514
+ x,
515
+ y,
516
+ z,
517
+ interp_method=interp_method,
518
+ bin=bin,
519
+ nx=nx,
520
+ ny=ny,
521
+ xlim=xlim,
522
+ ylim=ylim,
523
+ nan_policy=nan_policy,
524
+ diagnostics=diagnostics,
525
+ backend_options=backend_options,
526
+ )
527
+ artists = self.ax.contour(
528
+ X,
529
+ Y,
530
+ np.ma.masked_invalid(Z),
531
+ *args,
532
+ transform=self.ax.transAxes,
533
+ **kw,
534
+ )
535
+ return _auto_clip(artists, self.ax, self._clip_path)
536
+
537
+ def jpcontourf(
538
+ self,
539
+ x,
540
+ y,
541
+ z,
542
+ *args,
543
+ interp_method: str = "natural_neighbor",
544
+ bin: int = DEFAULT_JP_SAMPLE_GRID,
545
+ nx: Optional[int] = None,
546
+ ny: Optional[int] = None,
547
+ xlim=None,
548
+ ylim=None,
549
+ nan_policy: str = "strict",
550
+ diagnostics: bool = False,
551
+ backend_options: Optional[dict[str, Any]] = None,
552
+ **kwargs,
553
+ ):
554
+ """Filled scattered-data contour plot rendered in axes-fraction coordinates."""
555
+ kw = self._merge("contourf", kwargs)
556
+ kw.pop("transform", None)
557
+ X, Y, Z = self._interpolate_jpcontour_grid(
558
+ x,
559
+ y,
560
+ z,
561
+ interp_method=interp_method,
562
+ bin=bin,
563
+ nx=nx,
564
+ ny=ny,
565
+ xlim=xlim,
566
+ ylim=ylim,
567
+ nan_policy=nan_policy,
568
+ diagnostics=diagnostics,
569
+ backend_options=backend_options,
570
+ )
571
+ artists = self.ax.contourf(
572
+ X,
573
+ Y,
574
+ np.ma.masked_invalid(Z),
575
+ *args,
576
+ transform=self.ax.transAxes,
577
+ **kw,
578
+ )
579
+ return _auto_clip(artists, self.ax, self._clip_path)
580
+
581
+ def jpfield(
582
+ self,
583
+ x,
584
+ y,
585
+ z,
586
+ *args,
587
+ interp_method: str = "natural_neighbor",
588
+ bin: int = DEFAULT_JP_SAMPLE_GRID,
589
+ nx: Optional[int] = None,
590
+ ny: Optional[int] = None,
591
+ xlim=None,
592
+ ylim=None,
593
+ nan_policy: str = "strict",
594
+ backend_options: Optional[dict[str, Any]] = None,
595
+ shading: str = "auto",
596
+ **kwargs,
597
+ ):
598
+ """Scattered-data field plot.
599
+
600
+ Interpolates scattered x/y/z samples onto a regular grid in axes-fraction
601
+ space, then renders the result with pcolormesh(transform=ax.transAxes).
602
+ The plot is drawn from explicit cell edges so raster backends do not
603
+ introduce a half-cell seam.
604
+ Undefined regions remain masked. Contour-only kwargs such as levels /
605
+ colors / extend are ignored.
606
+ """
607
+ kw = self._merge("pcolormesh", kwargs)
608
+ for k in ("levels", "extend", "locator", "corner_mask", "colors", "linestyles"):
609
+ kw.pop(k, None)
610
+ kw.pop("shading", None)
611
+ kw.pop("transform", None)
612
+ X, Y, Z = self._interpolate_jpfield_grid(
613
+ x,
614
+ y,
615
+ z,
616
+ interp_method=interp_method,
617
+ bin=bin,
618
+ nx=nx,
619
+ ny=ny,
620
+ xlim=xlim,
621
+ ylim=ylim,
622
+ nan_policy=nan_policy,
623
+ backend_options=backend_options,
624
+ )
625
+ X = np.asarray(X, dtype=float)
626
+ Y = np.asarray(Y, dtype=float)
627
+ Z = np.ma.masked_invalid(np.asarray(Z, dtype=float))
628
+ if str(shading).lower() in {"auto", "flat"}:
629
+ ny, nx = Z.shape
630
+ xe = np.linspace(0.0, 1.0, nx + 1)
631
+ ye = np.linspace(0.0, 1.0, ny + 1)
632
+ X, Y = np.meshgrid(xe, ye)
633
+ shading = "flat"
634
+ else:
635
+ shading = str(shading)
636
+ kw.setdefault("edgecolors", "none")
637
+ kw.setdefault("linewidth", 0.0)
638
+ kw.setdefault("antialiased", False)
639
+ kw.setdefault("snap", True)
640
+ artists = self.ax.pcolormesh(
641
+ X,
642
+ Y,
643
+ Z,
644
+ *args,
645
+ transform=self.ax.transAxes,
646
+ shading=shading,
647
+ **kw,
648
+ )
649
+ return _auto_clip(artists, self.ax, self._clip_path)
650
+
181
651
  def imshow(self, *args, **kwargs):
182
652
  kw = self._merge("imshow", kwargs)
183
653
  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)