jarvisplot 1.4.0__tar.gz → 1.4.2__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 (100) hide show
  1. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/JarvisPLOT.egg-info/PKG-INFO +1 -1
  2. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/JarvisPLOT.egg-info/SOURCES.txt +1 -0
  3. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/PKG-INFO +1 -1
  4. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/Figure/colorbar_runtime.py +2 -0
  5. jarvisplot-1.4.2/jarvisplot/Figure/design_runtime.py +137 -0
  6. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/Figure/figure.py +9 -10
  7. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/Figure/layer_runtime.py +119 -6
  8. {jarvisplot-1.4.0/jarvisplot/cards/a4paper/4x1 → jarvisplot-1.4.2/jarvisplot/cards/a4paper/2x1}/rect.json +4 -0
  9. {jarvisplot-1.4.0/jarvisplot/cards/gambit → jarvisplot-1.4.2/jarvisplot/cards/a4paper}/2x1/ternary.json +3 -0
  10. {jarvisplot-1.4.0/jarvisplot/cards/a4paper/4x1 → jarvisplot-1.4.2/jarvisplot/cards/gambit/2x1}/ternary.json +17 -0
  11. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/flowchart.py +10 -3
  12. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/pyproject.toml +1 -1
  13. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/tests/test_figure_contracts.py +135 -0
  14. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/JarvisPLOT.egg-info/dependency_links.txt +0 -0
  15. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/JarvisPLOT.egg-info/entry_points.txt +0 -0
  16. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/JarvisPLOT.egg-info/requires.txt +0 -0
  17. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/JarvisPLOT.egg-info/top_level.txt +0 -0
  18. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/README.md +0 -0
  19. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/Figure/__init__.py +0 -0
  20. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/Figure/adapters_rect.py +0 -0
  21. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/Figure/adapters_ternary.py +0 -0
  22. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/Figure/cards/std_axes_adapter_config.json +0 -0
  23. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/Figure/config_runtime.py +0 -0
  24. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/Figure/data_pipelines.py +0 -0
  25. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/Figure/density_cell_runtime.py +0 -0
  26. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/Figure/dynesty_runtime.py +0 -0
  27. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/Figure/figure_types.py +0 -0
  28. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/Figure/helper.py +0 -0
  29. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/Figure/interp_2d_runtime.py +0 -0
  30. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/Figure/interp_natural_neighbor.py +0 -0
  31. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/Figure/interp_natural_neighbor_exact.py +0 -0
  32. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/Figure/layout_runtime.py +0 -0
  33. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/Figure/method_registry.py +0 -0
  34. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/Figure/posterior_density_runtime.py +0 -0
  35. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/Figure/posterior_hpd.py +0 -0
  36. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/Figure/posterior_mesh.py +0 -0
  37. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/Figure/preprocessor.py +0 -0
  38. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/Figure/preprocessor_runtime.py +0 -0
  39. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/Figure/profile_runtime.py +0 -0
  40. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/Figure/style_runtime.py +0 -0
  41. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/__init__.py +0 -0
  42. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/cache_store.py +0 -0
  43. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/cards/a4paper/1x1/ternary.json +0 -0
  44. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/cards/a4paper/2x1/dynesty_runplot.json +0 -0
  45. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/cards/a4paper/2x1/rect5x1.json +0 -0
  46. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/cards/a4paper/2x1/rect_cmap.json +0 -0
  47. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/cards/a4paper/2x1/ternary_cmap.json +0 -0
  48. {jarvisplot-1.4.0/jarvisplot/cards/a4paper/2x1 → jarvisplot-1.4.2/jarvisplot/cards/a4paper/4x1}/rect.json +0 -0
  49. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/cards/a4paper/4x1/rect_cmap.json +0 -0
  50. {jarvisplot-1.4.0/jarvisplot/cards/a4paper/2x1 → jarvisplot-1.4.2/jarvisplot/cards/a4paper/4x1}/ternary.json +0 -0
  51. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/cards/a4paper/4x1/ternary_cmap.json +0 -0
  52. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/cards/args.json +0 -0
  53. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/cards/colors/colormaps.json +0 -0
  54. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/cards/default/output.json +0 -0
  55. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/cards/flowchart/default.json +0 -0
  56. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/cards/gambit/1x1/ternary.json +0 -0
  57. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/cards/gambit/2x1/rect_cmap.json +0 -0
  58. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/cards/gambit/2x1/ternary_cmap.json +0 -0
  59. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/cards/icons/JarvisHEP.png +0 -0
  60. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/cards/icons/flowchart/JarvisHEP.png +0 -0
  61. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/cards/icons/flowchart/calculator.png +0 -0
  62. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/cards/icons/flowchart/inputfile.png +0 -0
  63. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/cards/icons/flowchart/opera.png +0 -0
  64. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/cards/icons/flowchart/outputfile.png +0 -0
  65. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/cards/icons/flowchart/sampler.png +0 -0
  66. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/cards/icons/gambit.png +0 -0
  67. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/cards/icons/gambit_small.png +0 -0
  68. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/cards/style_preference.json +0 -0
  69. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/cli.py +0 -0
  70. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/client.py +0 -0
  71. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/config.py +0 -0
  72. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/core.py +0 -0
  73. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/core_assets.py +0 -0
  74. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/core_runtime.py +0 -0
  75. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/data_loader.py +0 -0
  76. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/data_loader_hdf5.py +0 -0
  77. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/data_loader_runtime.py +0 -0
  78. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/data_loader_summary.py +0 -0
  79. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/inner_func.py +0 -0
  80. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/memtrace.py +0 -0
  81. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/utils/__init__.py +0 -0
  82. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/utils/cmaps.py +0 -0
  83. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/utils/dataframes.py +0 -0
  84. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/utils/expression.py +0 -0
  85. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/utils/interpolator.py +0 -0
  86. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/jarvisplot/utils/pathing.py +0 -0
  87. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/setup.cfg +0 -0
  88. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/tests/test_data_loader_contracts.py +0 -0
  89. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/tests/test_density_cell_transform.py +0 -0
  90. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/tests/test_docs_status_labels.py +0 -0
  91. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/tests/test_figure_type_expander.py +0 -0
  92. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/tests/test_flowchart_renderer.py +0 -0
  93. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/tests/test_interp_2d_transform.py +0 -0
  94. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/tests/test_interp_natural_neighbor.py +0 -0
  95. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/tests/test_interpolator_contracts.py +0 -0
  96. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/tests/test_jpcontour_methods.py +0 -0
  97. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/tests/test_parse_data_metadata.py +0 -0
  98. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/tests/test_pathing_contracts.py +0 -0
  99. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/tests/test_posterior_hpd.py +0 -0
  100. {jarvisplot-1.4.0 → jarvisplot-1.4.2}/tests/test_template_json.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: JarvisPLOT
3
- Version: 1.4.0
3
+ Version: 1.4.2
4
4
  Summary: Jarvis-PLOT: YAML-driven plotting engine
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -28,6 +28,7 @@ jarvisplot/Figure/colorbar_runtime.py
28
28
  jarvisplot/Figure/config_runtime.py
29
29
  jarvisplot/Figure/data_pipelines.py
30
30
  jarvisplot/Figure/density_cell_runtime.py
31
+ jarvisplot/Figure/design_runtime.py
31
32
  jarvisplot/Figure/dynesty_runtime.py
32
33
  jarvisplot/Figure/figure.py
33
34
  jarvisplot/Figure/figure_types.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: JarvisPLOT
3
- Version: 1.4.0
3
+ Version: 1.4.2
4
4
  Summary: Jarvis-PLOT: YAML-driven plotting engine
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -394,6 +394,8 @@ def collect_and_attach_colorbar(
394
394
  if not axc._cb.get("used"):
395
395
  frame = getattr(fig, "frame", {})
396
396
  color_cfg = axc_color_config(frame, cb_name)
397
+ if color_cfg.get("cmap") is None and style.get("cmap") is not None:
398
+ color_cfg["cmap"] = style.get("cmap")
397
399
  data_range = collect_layer_color_range(
398
400
  df,
399
401
  coor,
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env python3
2
+ """Design-reference debug overlay for Jarvis-PLOT figures.
3
+
4
+ When a figure is rendered with ``debug: true`` in the YAML, this module draws a
5
+ dimension overlay on top of the finished figure: every axes defined by the style
6
+ card is outlined and annotated with
7
+
8
+ - its name (``ax`` / ``axc`` / ``axtri`` / ``ax0`` ...),
9
+ - its position rect ``[left, bottom, width, height]`` in figure fractions
10
+ (exactly the ``Frame.axes.<name>.rect`` value from the style JSON card), and
11
+ - its width / height converted to centimetres from ``figure.figsize``.
12
+
13
+ The primary axes additionally gets margin dimension lines (left / top / bottom
14
+ insets), and colorbar axes get a gap dimension to the primary axes.
15
+
16
+ The overlay never participates in data rendering; it is a read-only annotation
17
+ layer added just before the figure is saved. All drawing is wrapped so a
18
+ failure here can never break a normal plot.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import matplotlib as mpl
24
+
25
+
26
+ # overlay palette
27
+ _DIM = "#0A7FB6" # blue dimension lines + labels
28
+ _BOX = "#111111" # axes outline
29
+ _NAME = "#1A237E" # axes name + rect text
30
+ _MARGIN = "#FF3FA4" # pink figure border / caption
31
+
32
+ # order in which we look for the "primary" axes to fully dimension
33
+ _PRIMARY_ORDER = ("ax", "ax0", "ax1", "axtri")
34
+
35
+
36
+ def _raw(ax):
37
+ """Return the underlying matplotlib Axes for an adapter or raw Axes."""
38
+ return getattr(ax, "ax", ax)
39
+
40
+
41
+ def _position(ax):
42
+ try:
43
+ bb = _raw(ax).get_position()
44
+ return float(bb.x0), float(bb.y0), float(bb.width), float(bb.height)
45
+ except Exception:
46
+ return None
47
+
48
+
49
+ def draw_design_reference(fig) -> None:
50
+ """Draw the dimension overlay onto ``fig`` (a JarvisPLOT Figure wrapper)."""
51
+ try:
52
+ _draw(fig)
53
+ except Exception as exc: # never break a real plot because of the overlay
54
+ logger = getattr(fig, "logger", None)
55
+ if logger is not None:
56
+ try:
57
+ logger.debug(f"design-reference overlay skipped: {exc}")
58
+ except Exception:
59
+ pass
60
+
61
+
62
+ def _draw(fig) -> None:
63
+ F = fig.fig
64
+ w_in, h_in = (float(v) for v in F.get_size_inches())
65
+ w_cm, h_cm = w_in * 2.54, h_in * 2.54
66
+
67
+ ov = F.add_axes([0.0, 0.0, 1.0, 1.0], zorder=10_000)
68
+ ov.set_xlim(0.0, 1.0)
69
+ ov.set_ylim(0.0, 1.0)
70
+ ov.set_axis_off()
71
+ ov.patch.set_alpha(0.0)
72
+
73
+ def label(x, y, text, *, color=_DIM, size=5.0, ha="center", va="center",
74
+ rotation=0, weight="normal", family="monospace", boxed=True):
75
+ bbox = dict(boxstyle="square,pad=0.12", fc="white", ec="none", alpha=0.82) if boxed else None
76
+ ov.text(x, y, text, color=color, fontsize=size, ha=ha, va=va,
77
+ rotation=rotation, fontweight=weight, family=family, bbox=bbox,
78
+ zorder=10_002, clip_on=False)
79
+
80
+ def dim(x0, y0, x1, y1, text, *, vertical=False):
81
+ ov.annotate("", xy=(x1, y1), xytext=(x0, y0),
82
+ arrowprops=dict(arrowstyle="<|-|>", color=_DIM, lw=0.7,
83
+ shrinkA=0, shrinkB=0, mutation_scale=6),
84
+ zorder=10_001)
85
+ label((x0 + x1) / 2.0, (y0 + y1) / 2.0, text,
86
+ rotation=90 if vertical else 0)
87
+
88
+ # figure border + size caption
89
+ ov.add_patch(mpl.patches.Rectangle((0, 0), 1, 1, fill=False, ec=_MARGIN,
90
+ lw=0.9, zorder=10_001))
91
+ label(0.5, 0.995, f"design reference · figure {w_cm:.2f} cm × {h_cm:.2f} cm",
92
+ color=_MARGIN, size=6.0, va="top", weight="bold", family="DejaVu Sans")
93
+
94
+ # total figure height marker on the far left
95
+ dim(0.012, 0.0, 0.012, 1.0, f"{h_cm:.2f} cm", vertical=True)
96
+
97
+ # pick the primary axes to fully dimension
98
+ primary = next((n for n in _PRIMARY_ORDER if n in fig.axes), None)
99
+ primary_pos = _position(fig.axes[primary]) if primary else None
100
+
101
+ for name, ax in fig.axes.items():
102
+ pos = _position(ax)
103
+ if pos is None:
104
+ continue
105
+ l, b, w, h = pos
106
+
107
+ # outline only, so the real plot underneath keeps its true colors
108
+ ov.add_patch(mpl.patches.Rectangle((l, b), w, h, fill=False, ec=_BOX,
109
+ lw=0.9, zorder=10_001))
110
+
111
+ # name + rect + cm sizes — anchored so the text stays inside the figure
112
+ if l + w / 2.0 > 0.6: # axes sits in the right half
113
+ tx, ha = min(l + w, 0.992), "right"
114
+ else:
115
+ tx, ha = max(l + 0.012, 0.008), "left"
116
+ ty = min(b + h - 0.015, 0.93)
117
+ label(tx, ty, name, color=_NAME, size=7.0, ha=ha, va="top",
118
+ weight="bold", boxed=True)
119
+ label(tx, ty - 0.057,
120
+ f"[{l:.3f}, {b:.3f}, {w:.3f}, {h:.3f}]\n"
121
+ f"width: {w * w_cm:.3f} cm height: {h * h_cm:.3f} cm",
122
+ color=_NAME, size=4.6, ha=ha, va="top", boxed=True)
123
+
124
+ if name == primary:
125
+ yt = b + h
126
+ # left inset (figure left edge → axes left edge), drawn along the top edge
127
+ dim(0.0, yt, l, yt, f"{l * w_cm:.3f} cm")
128
+ # top inset (axes top edge → figure top)
129
+ dim(l, yt, l, 1.0, f"{(1.0 - yt) * h_cm:.3f} cm", vertical=True)
130
+ # bottom inset (figure bottom → axes bottom edge)
131
+ dim(l, 0.0, l, b, f"{b * h_cm:.3f} cm", vertical=True)
132
+ elif name.startswith("axc") and primary_pos is not None:
133
+ # gap between the primary axes' right edge and this colorbar's left edge
134
+ pl, pb, pw, ph = primary_pos
135
+ ygap = b + h / 2.0
136
+ if l > pl + pw:
137
+ dim(pl + pw, ygap, l, ygap, f"{(l - (pl + pw)) * w_cm:.3f} cm")
@@ -13,6 +13,7 @@ from .adapters_ternary import TernaryAxesAdapter
13
13
  from .method_registry import resolve_callable
14
14
  from .style_runtime import resolve_style_bundle_payload
15
15
  from .config_runtime import apply_figure_config
16
+ from .design_runtime import draw_design_reference
16
17
  from .layer_runtime import (
17
18
  load_layer_runtime_data as runtime_load_layer_runtime_data,
18
19
  release_layer_runtime_data as runtime_release_layer_runtime_data,
@@ -711,6 +712,7 @@ class Figure:
711
712
  read-only during the main render loop.
712
713
  """
713
714
  cb_ranges: dict[str, list] = {}
715
+ cb_style_cmaps: dict[str, object] = {}
714
716
 
715
717
  for ax, ly in self._render_queue:
716
718
  cb_name = ly.get("colorbar", "axc")
@@ -724,6 +726,9 @@ class Figure:
724
726
  if not layer_uses_color(style, coor, method_key):
725
727
  continue
726
728
 
729
+ if style.get("cmap") is not None:
730
+ cb_style_cmaps.setdefault(cb_name, style.get("cmap"))
731
+
727
732
  # Load data (pipeline cache hit on second call in render loop)
728
733
  runtime_load_layer_runtime_data(self, ly)
729
734
  df = ly.get("data")
@@ -742,6 +747,8 @@ class Figure:
742
747
  if axc_obj is None or not hasattr(axc_obj, "_cb"):
743
748
  continue
744
749
  color_cfg = axc_color_config(self.frame, cb_name)
750
+ if color_cfg.get("cmap") is None and cb_name in cb_style_cmaps:
751
+ color_cfg["cmap"] = cb_style_cmaps[cb_name]
745
752
  axc_obj._cb.update(
746
753
  precompute_colorbar_cb(color_cfg, ranges, logger=self.logger)
747
754
  )
@@ -969,16 +976,8 @@ class Figure:
969
976
  def plot(self):
970
977
  self.render()
971
978
  # for layer in self.layers:
972
- if self.debug:
973
- if "axtri" in self.axes.keys():
974
- # Demo of Scatter Clip
975
- x = np.linspace(-1, 2, 121)
976
- y = np.linspace(-1, 2, 121)
977
- X, Y = np.meshgrid(x, y)
978
- self.axtri.scatter(x=X.ravel() + 0.5 * Y.ravel(), y=Y.ravel(), marker='.', s=1, facecolor="#0277BA", edgecolor="None")
979
-
980
- # Demo of Plot Clip
981
- self.axtri.plot(x=[-1, 0.5, 0.5, 2], y=[-1.1, 0.6, 0.3, 1.8], linestyle="-", color="#0277BA")
979
+ if self.debug:
980
+ draw_design_reference(self)
982
981
  self.savefig()
983
982
  import matplotlib.pyplot as plt
984
983
  plt.close(self.fig)
@@ -6,6 +6,7 @@ from pathlib import Path
6
6
 
7
7
  import numpy as np
8
8
  import pandas as pd
9
+ from matplotlib.path import Path as MplPath
9
10
 
10
11
  from .preprocessor_runtime import add_column, filter_df, sort_by
11
12
  from .profile_runtime import profiling
@@ -15,6 +16,117 @@ from .interp_natural_neighbor import resolve_backend
15
16
  from .dynesty_runtime import render_dynesty_runplot
16
17
  from .posterior_hpd import prepare_hpd_contour_style
17
18
 
19
+ _MISSING_CLIP_PATH = object()
20
+
21
+
22
+ def _clip_expr_inside(expr: str, point) -> bool:
23
+ px, py = float(point[0]), float(point[1])
24
+ env = {
25
+ "x": px,
26
+ "y": py,
27
+ "np": np,
28
+ "numpy": np,
29
+ "abs": abs,
30
+ "min": min,
31
+ "max": max,
32
+ }
33
+ try:
34
+ return bool(eval(expr, {"__builtins__": {}}, env))
35
+ except Exception as exc:
36
+ raise ValueError(f"layer clip_expr failed for {expr!r}: {exc}") from exc
37
+
38
+
39
+ def _clip_polygon_to_expr(poly, expr: str, *, iterations: int = 48):
40
+ """Clip a polygon to the region where expr is True in data coordinates."""
41
+ expr = str(expr or "").strip()
42
+ if not expr or not poly:
43
+ return poly
44
+
45
+ def inside(pt) -> bool:
46
+ return _clip_expr_inside(expr, pt)
47
+
48
+ def intersect(a, b):
49
+ pa = np.asarray(a, dtype=float)
50
+ pb = np.asarray(b, dtype=float)
51
+ a_inside = inside(pa)
52
+ lo = pa
53
+ hi = pb
54
+ for _ in range(iterations):
55
+ mid = 0.5 * (lo + hi)
56
+ if inside(mid) == a_inside:
57
+ lo = mid
58
+ else:
59
+ hi = mid
60
+ return tuple(0.5 * (lo + hi))
61
+
62
+ out = []
63
+ start = poly[-1]
64
+ start_inside = inside(start)
65
+ for end in poly:
66
+ end_inside = inside(end)
67
+ if end_inside:
68
+ if not start_inside:
69
+ out.append(intersect(start, end))
70
+ out.append(end)
71
+ elif start_inside:
72
+ out.append(intersect(start, end))
73
+ start = end
74
+ start_inside = end_inside
75
+ return out
76
+
77
+
78
+ def _layer_clip_path_from_style(ax, style: dict):
79
+ expr = style.pop("clip_expr", style.pop("clip_where", None))
80
+ if expr is None:
81
+ return None
82
+ raw_ax = getattr(ax, "ax", ax)
83
+ try:
84
+ x0, x1 = raw_ax.get_xlim()
85
+ y0, y1 = raw_ax.get_ylim()
86
+ except Exception as exc:
87
+ raise ValueError("layer clip_expr requires axes with data limits") from exc
88
+
89
+ xmin, xmax = sorted((float(x0), float(x1)))
90
+ ymin, ymax = sorted((float(y0), float(y1)))
91
+ rect = [(xmin, ymin), (xmax, ymin), (xmax, ymax), (xmin, ymax)]
92
+ clipped = _clip_polygon_to_expr(rect, str(expr))
93
+ if len(clipped) < 3:
94
+ return MplPath([(xmin, ymin), (xmin, ymin)], [MplPath.MOVETO, MplPath.CLOSEPOLY])
95
+
96
+ vertices = list(clipped) + [tuple(clipped[0])]
97
+ codes = [MplPath.MOVETO] + [MplPath.LINETO] * (len(clipped) - 1) + [MplPath.CLOSEPOLY]
98
+ return MplPath(vertices, codes)
99
+
100
+
101
+ def _layer_clip_targets(ax):
102
+ raw_ax = getattr(ax, "ax", None)
103
+ targets = []
104
+ for obj in (ax, raw_ax):
105
+ if obj is not None and not any(obj is prev for prev in targets):
106
+ targets.append(obj)
107
+ return targets
108
+
109
+
110
+ def _call_with_layer_clip(ax, clip_path, func, *args, **kwargs):
111
+ if clip_path is None:
112
+ return func(*args, **kwargs)
113
+
114
+ states = []
115
+ for target in _layer_clip_targets(ax):
116
+ states.append((target, getattr(target, "_clip_path", _MISSING_CLIP_PATH)))
117
+ target._clip_path = clip_path
118
+ try:
119
+ return func(*args, **kwargs)
120
+ finally:
121
+ for target, previous in reversed(states):
122
+ if previous is _MISSING_CLIP_PATH:
123
+ try:
124
+ delattr(target, "_clip_path")
125
+ except Exception:
126
+ pass
127
+ else:
128
+ target._clip_path = previous
129
+
18
130
 
19
131
  def _resolve_csv_export_path(fig, target):
20
132
  if isinstance(target, dict):
@@ -664,6 +776,7 @@ def render_layer(fig, ax, layer_info):
664
776
  pass
665
777
  if layer_info.get("style", {}) is not None:
666
778
  style.update(layer_info.get("style", {}))
779
+ layer_clip_path = _layer_clip_path_from_style(ax, style)
667
780
 
668
781
  cb_name = layer_info.get("colorbar", "axc")
669
782
 
@@ -696,8 +809,8 @@ def render_layer(fig, ax, layer_info):
696
809
  required_keys=("z",),
697
810
  include_diagnostics=method_key in {"jpcontour", "jpcontourf"},
698
811
  )
699
- return method(**jp_kwargs)
700
- return method(**style)
812
+ return _call_with_layer_clip(ax, layer_clip_path, method, **jp_kwargs)
813
+ return _call_with_layer_clip(ax, layer_clip_path, method, **style)
701
814
 
702
815
  elif getattr(ax, "_type", None) == "rect":
703
816
  df = fig._ensure_pandas_data(layer_info["data"], reason=f"render:{layer_info.get('name', '')}")
@@ -724,7 +837,7 @@ def render_layer(fig, ax, layer_info):
724
837
  for kk, vv in coor.items():
725
838
  style[kk] = fig._eval_series(df, vv)
726
839
 
727
- return method(**style)
840
+ return _call_with_layer_clip(ax, layer_clip_path, method, **style)
728
841
  if method_key in {"jpcontour", "jpcontourf", "jpfield"}:
729
842
  jp_kwargs = _prepare_jpcontour_style(
730
843
  fig,
@@ -737,7 +850,7 @@ def render_layer(fig, ax, layer_info):
737
850
  required_keys=("x", "y", "z"),
738
851
  include_diagnostics=method_key in {"jpcontour", "jpcontourf"},
739
852
  )
740
- return method(**jp_kwargs)
853
+ return _call_with_layer_clip(ax, layer_clip_path, method, **jp_kwargs)
741
854
  if method_key in {"contour", "contourf"}:
742
855
  contour_coor = {}
743
856
  for kk in ("x", "y", "z"):
@@ -759,7 +872,7 @@ def render_layer(fig, ax, layer_info):
759
872
  contour_args, style = _prepare_contour_args(fig, ax.ax if hasattr(ax, "ax") else ax, method_key, style, contour_coor, df=df)
760
873
  if contour_args is None:
761
874
  return []
762
- return method(*contour_args, **style)
875
+ return _call_with_layer_clip(ax, layer_clip_path, method, *contour_args, **style)
763
876
  else:
764
877
  if not ({"x", "y"} <= set(coor.keys())):
765
878
  raise ValueError("Rectangular layer must define coordinates: {x,y} with exprs.")
@@ -777,7 +890,7 @@ def render_layer(fig, ax, layer_info):
777
890
  if method_key == "pcolormesh":
778
891
  style["__df__"] = df
779
892
  style.pop("interp", None)
780
- return method(**style)
893
+ return _call_with_layer_clip(ax, layer_clip_path, method, **style)
781
894
 
782
895
  else:
783
896
  raise ValueError(f"Axes '{ax}' has unknown _type='{getattr(ax, '_type', None)}'.")
@@ -86,6 +86,10 @@
86
86
  "loc": "top",
87
87
  "fontsize": 12,
88
88
  "fontfamily": "STIXGeneral"
89
+ },
90
+ "ylabel_coords": {
91
+ "x": -0.1,
92
+ "y": 0.5
89
93
  }
90
94
  }
91
95
  },
@@ -124,6 +124,9 @@
124
124
  "va":"center"
125
125
  }
126
126
  }
127
+ },
128
+ "axlogo": {
129
+ "file": "&JP/jarvisplot/cards/icons/jarvisHEP.png"
127
130
  }
128
131
  },
129
132
  "Style": {
@@ -124,6 +124,23 @@
124
124
  "va":"center"
125
125
  }
126
126
  }
127
+ },
128
+ "axlogo": {
129
+ "file": "&JP/jarvisplot/cards/icons/gambit_small.png",
130
+ "text": [
131
+ {
132
+ "x": 0.0,
133
+ "y": -0.5,
134
+ "s": "GAMBIT 2.7",
135
+ "ha": "left",
136
+ "va": "bottom",
137
+ "color": "black",
138
+ "fontfamily": "Fira code",
139
+ "fontsize": 5.0,
140
+ "fontstyle": "normal",
141
+ "fontweight": "normal"
142
+ }
143
+ ]
127
144
  }
128
145
  },
129
146
  "Style": {
@@ -276,13 +276,20 @@ class FlowchartRenderer:
276
276
  from shapely.geometry import Point, box
277
277
  from shapely.ops import unary_union
278
278
  x, y = item["pos"]
279
- circle1 = Point(x-0.15, y).buffer(0.15, resolution=128)
280
- circle2 = Point(x+0.15, y).buffer(0.15, resolution=128)
279
+
280
+ def point_buffer(point, radius, segments=128):
281
+ try:
282
+ return point.buffer(radius, quad_segs=segments)
283
+ except TypeError:
284
+ return point.buffer(radius, resolution=segments)
285
+
286
+ circle1 = point_buffer(Point(x-0.15, y), 0.15)
287
+ circle2 = point_buffer(Point(x+0.15, y), 0.15)
281
288
  square = box(x - 0.15, y - 0.15, x + 0.15, y + 0.15)
282
289
  merged = unary_union([circle1, circle2, square])
283
290
  xx, yy = merged.exterior.xy
284
291
  ax.fill(xx, yy, alpha=0.95, edgecolor="#F9F9F8", facecolor="#00D86B", linewidth=0.1, zorder=120)
285
- circle3 = Point(x-0.15, y).buffer(0.12, resolution=128)
292
+ circle3 = point_buffer(Point(x-0.15, y), 0.12)
286
293
  xx, yy = circle3.exterior.xy
287
294
  ax.fill(xx, yy, alpha=0.95, edgecolor="#FFFFFF", facecolor="#F9F9F8", linewidth=0.8, zorder=120)
288
295
  import matplotlib.patheffects as pe
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "JarvisPLOT"
7
- version = "1.4.0"
7
+ version = "1.4.2"
8
8
  description = "Jarvis-PLOT: YAML-driven plotting engine"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -224,6 +224,41 @@ def test_figure_set_alias_removed():
224
224
  assert not hasattr(Figure, "set")
225
225
 
226
226
 
227
+ def test_layer_clip_expr_builds_data_space_clip_path():
228
+ _, raw_ax = plt.subplots()
229
+ raw_ax.set_xlim(0.0, 10.0)
230
+ raw_ax.set_ylim(0.0, 10.0)
231
+ ax = SimpleNamespace(ax=raw_ax)
232
+ style = {"clip_expr": "x >= 2 * y", "cmap": "viridis"}
233
+
234
+ clip_path = layer_runtime_mod._layer_clip_path_from_style(ax, style)
235
+
236
+ assert "clip_expr" not in style
237
+ assert style["cmap"] == "viridis"
238
+ assert len(clip_path.vertices) >= 3
239
+ assert all(px >= 2 * py - 1e-10 for px, py in clip_path.vertices)
240
+ plt.close(raw_ax.figure)
241
+
242
+
243
+ def test_layer_clip_is_temporary_on_adapter_and_raw_axes():
244
+ raw_ax = SimpleNamespace(_clip_path="raw-original")
245
+ adapter = SimpleNamespace(ax=raw_ax, _clip_path="adapter-original")
246
+ clip_path = object()
247
+ observed = {}
248
+
249
+ def draw():
250
+ observed["adapter"] = adapter._clip_path
251
+ observed["raw"] = raw_ax._clip_path
252
+ return "drawn"
253
+
254
+ out = layer_runtime_mod._call_with_layer_clip(adapter, clip_path, draw)
255
+
256
+ assert out == "drawn"
257
+ assert observed == {"adapter": clip_path, "raw": clip_path}
258
+ assert adapter._clip_path == "adapter-original"
259
+ assert raw_ax._clip_path == "raw-original"
260
+
261
+
227
262
  def test_colorbar_contract_uses_frame_color_config():
228
263
  class DummyColorbarAxis:
229
264
  def __init__(self):
@@ -259,6 +294,106 @@ def test_colorbar_contract_uses_frame_color_config():
259
294
  assert fig.axc._cb["vmax"] == 10.0
260
295
 
261
296
 
297
+ def test_colorbar_contract_falls_back_to_layer_cmap_when_frame_omits_cmap():
298
+ class DummyColorbarAxis:
299
+ def __init__(self):
300
+ self._cb = {
301
+ "cmap": None,
302
+ "vmin": None,
303
+ "vmax": None,
304
+ "norm": None,
305
+ "levels": None,
306
+ "used": False,
307
+ }
308
+
309
+ fig = SimpleNamespace(
310
+ axes={"axc": DummyColorbarAxis()},
311
+ frame={"axc": {"color": {"scale": "linear"}}},
312
+ logger=_logger(),
313
+ )
314
+
315
+ out = collect_and_attach_colorbar(
316
+ fig,
317
+ style={"cmap": "jarvis_rainbow2_r"},
318
+ coor={"z": {"expr": "z"}},
319
+ method_key="voronoi",
320
+ df=pd.DataFrame({"z": [1, 2, 5]}),
321
+ )
322
+
323
+ assert out["cmap"] == "jarvis_rainbow2_r"
324
+ assert fig.axes["axc"]._cb["cmap"] == "jarvis_rainbow2_r"
325
+
326
+
327
+ def test_prescan_colorbar_uses_layer_cmap_when_frame_omits_cmap():
328
+ class DummyColorbarAxis:
329
+ def __init__(self):
330
+ self._cb = {
331
+ "cmap": None,
332
+ "vmin": None,
333
+ "vmax": None,
334
+ "norm": None,
335
+ "levels": None,
336
+ "used": False,
337
+ }
338
+
339
+ fig = Figure()
340
+ fig.logger = _logger()
341
+ fig.frame = {"axc": {"color": {"scale": "linear"}}}
342
+ fig.axes = {"axc": DummyColorbarAxis()}
343
+ layer = {
344
+ "name": "colored_voronoi",
345
+ "method": "voronoi",
346
+ "style": {"cmap": "jarvis_rainbow2_r"},
347
+ "coor": {"z": {"expr": "z"}},
348
+ "colorbar": "axc",
349
+ "data": pd.DataFrame({"z": [1, 2, 5]}),
350
+ "data_loaded": True,
351
+ "layer_spec": {},
352
+ "source_refs": [],
353
+ "share_name": None,
354
+ }
355
+ fig._render_queue = [(None, layer)]
356
+
357
+ fig._prescan_colorbar_ranges()
358
+
359
+ assert fig.axes["axc"]._cb["cmap"] == "jarvis_rainbow2_r"
360
+
361
+
362
+ def test_prescan_colorbar_frame_cmap_overrides_layer_cmap():
363
+ class DummyColorbarAxis:
364
+ def __init__(self):
365
+ self._cb = {
366
+ "cmap": None,
367
+ "vmin": None,
368
+ "vmax": None,
369
+ "norm": None,
370
+ "levels": None,
371
+ "used": False,
372
+ }
373
+
374
+ fig = Figure()
375
+ fig.logger = _logger()
376
+ fig.frame = {"axc": {"color": {"scale": "linear", "cmap": "viridis"}}}
377
+ fig.axes = {"axc": DummyColorbarAxis()}
378
+ layer = {
379
+ "name": "colored_voronoi",
380
+ "method": "voronoi",
381
+ "style": {"cmap": "jarvis_rainbow2_r"},
382
+ "coor": {"z": {"expr": "z"}},
383
+ "colorbar": "axc",
384
+ "data": pd.DataFrame({"z": [1, 2, 5]}),
385
+ "data_loaded": True,
386
+ "layer_spec": {},
387
+ "source_refs": [],
388
+ "share_name": None,
389
+ }
390
+ fig._render_queue = [(None, layer)]
391
+
392
+ fig._prescan_colorbar_ranges()
393
+
394
+ assert fig.axes["axc"]._cb["cmap"] == "viridis"
395
+
396
+
262
397
  def test_colorbar_attachment_skips_plain_scatter_without_color_channel():
263
398
  class DummyColorbarAxis:
264
399
  def __init__(self):
File without changes
File without changes
File without changes