figrecipe 0.6.0__py3-none-any.whl → 0.7.4__py3-none-any.whl
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.
- figrecipe/__init__.py +106 -973
- figrecipe/_api/__init__.py +48 -0
- figrecipe/_api/_extract.py +108 -0
- figrecipe/_api/_notebook.py +61 -0
- figrecipe/_api/_panel.py +46 -0
- figrecipe/_api/_save.py +191 -0
- figrecipe/_api/_seaborn_proxy.py +34 -0
- figrecipe/_api/_style_manager.py +153 -0
- figrecipe/_api/_subplots.py +333 -0
- figrecipe/_api/_validate.py +82 -0
- figrecipe/_dev/__init__.py +2 -93
- figrecipe/_dev/_plotters.py +76 -0
- figrecipe/_dev/_run_demos.py +56 -0
- figrecipe/_dev/demo_plotters/__init__.py +35 -166
- figrecipe/_dev/demo_plotters/_categories.py +81 -0
- figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
- figrecipe/_dev/demo_plotters/_helpers.py +31 -0
- figrecipe/_dev/demo_plotters/_registry.py +50 -0
- figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/{plot_plot.py → line_curve/plot_plot.py} +3 -2
- figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/{plot_pie.py → special/plot_pie.py} +5 -1
- figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
- figrecipe/_editor/__init__.py +57 -9
- figrecipe/_editor/_bbox/__init__.py +43 -0
- figrecipe/_editor/_bbox/_collections.py +177 -0
- figrecipe/_editor/_bbox/_elements.py +159 -0
- figrecipe/_editor/_bbox/_extract.py +256 -0
- figrecipe/_editor/_bbox/_extract_axes.py +370 -0
- figrecipe/_editor/_bbox/_extract_text.py +342 -0
- figrecipe/_editor/_bbox/_lines.py +173 -0
- figrecipe/_editor/_bbox/_transforms.py +146 -0
- figrecipe/_editor/_flask_app.py +68 -1039
- figrecipe/_editor/_helpers.py +242 -0
- figrecipe/_editor/_hitmap/__init__.py +76 -0
- figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
- figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
- figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
- figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
- figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
- figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
- figrecipe/_editor/_hitmap/_colors.py +181 -0
- figrecipe/_editor/_hitmap/_detect.py +137 -0
- figrecipe/_editor/_hitmap/_restore.py +154 -0
- figrecipe/_editor/_hitmap_main.py +182 -0
- figrecipe/_editor/_preferences.py +135 -0
- figrecipe/_editor/_render_overrides.py +480 -0
- figrecipe/_editor/_renderer.py +35 -185
- figrecipe/_editor/_routes_axis.py +453 -0
- figrecipe/_editor/_routes_core.py +284 -0
- figrecipe/_editor/_routes_element.py +317 -0
- figrecipe/_editor/_routes_style.py +223 -0
- figrecipe/_editor/_templates/__init__.py +78 -1
- figrecipe/_editor/_templates/_html.py +109 -13
- figrecipe/_editor/_templates/_scripts/__init__.py +120 -0
- figrecipe/_editor/_templates/_scripts/_api.py +228 -0
- figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
- figrecipe/_editor/_templates/_scripts/_core.py +436 -0
- figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
- figrecipe/_editor/_templates/_scripts/_element_editor.py +310 -0
- figrecipe/_editor/_templates/_scripts/_files.py +195 -0
- figrecipe/_editor/_templates/_scripts/_hitmap.py +509 -0
- figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
- figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
- figrecipe/_editor/_templates/_scripts/_legend_drag.py +265 -0
- figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
- figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag.py +334 -0
- figrecipe/_editor/_templates/_scripts/_panel_position.py +279 -0
- figrecipe/_editor/_templates/_scripts/_selection.py +237 -0
- figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
- figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
- figrecipe/_editor/_templates/_scripts/_zoom.py +179 -0
- figrecipe/_editor/_templates/_styles/__init__.py +69 -0
- figrecipe/_editor/_templates/_styles/_base.py +64 -0
- figrecipe/_editor/_templates/_styles/_buttons.py +206 -0
- figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
- figrecipe/_editor/_templates/_styles/_controls.py +265 -0
- figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
- figrecipe/_editor/_templates/_styles/_forms.py +126 -0
- figrecipe/_editor/_templates/_styles/_hitmap.py +184 -0
- figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
- figrecipe/_editor/_templates/_styles/_labels.py +118 -0
- figrecipe/_editor/_templates/_styles/_modals.py +98 -0
- figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
- figrecipe/_editor/_templates/_styles/_preview.py +225 -0
- figrecipe/_editor/_templates/_styles/_selection.py +73 -0
- figrecipe/_params/_DECORATION_METHODS.py +6 -0
- figrecipe/_recorder.py +35 -106
- figrecipe/_recorder_utils.py +124 -0
- figrecipe/_reproducer/__init__.py +18 -0
- figrecipe/_reproducer/_core.py +498 -0
- figrecipe/_reproducer/_custom_plots.py +279 -0
- figrecipe/_reproducer/_seaborn.py +100 -0
- figrecipe/_reproducer/_violin.py +186 -0
- figrecipe/_signatures/_kwargs.py +273 -0
- figrecipe/_signatures/_loader.py +21 -423
- figrecipe/_signatures/_parsing.py +147 -0
- figrecipe/_wrappers/_axes.py +119 -910
- figrecipe/_wrappers/_axes_helpers.py +136 -0
- figrecipe/_wrappers/_axes_plots.py +418 -0
- figrecipe/_wrappers/_axes_seaborn.py +157 -0
- figrecipe/_wrappers/_figure.py +162 -0
- figrecipe/_wrappers/_panel_labels.py +127 -0
- figrecipe/_wrappers/_plot_helpers.py +143 -0
- figrecipe/_wrappers/_violin_helpers.py +180 -0
- figrecipe/styles/__init__.py +8 -6
- figrecipe/styles/_dotdict.py +72 -0
- figrecipe/styles/_finalize.py +134 -0
- figrecipe/styles/_fonts.py +77 -0
- figrecipe/styles/_kwargs_converter.py +178 -0
- figrecipe/styles/_plot_styles.py +209 -0
- figrecipe/styles/_style_applier.py +32 -478
- figrecipe/styles/_style_loader.py +16 -192
- figrecipe/styles/_themes.py +151 -0
- figrecipe/styles/presets/MATPLOTLIB.yaml +2 -1
- figrecipe/styles/presets/SCITEX.yaml +29 -24
- {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/METADATA +37 -2
- figrecipe-0.7.4.dist-info/RECORD +188 -0
- figrecipe/_editor/_bbox.py +0 -978
- figrecipe/_editor/_hitmap.py +0 -937
- figrecipe/_editor/_templates/_scripts.py +0 -2778
- figrecipe/_editor/_templates/_styles.py +0 -1326
- figrecipe/_reproducer.py +0 -975
- figrecipe-0.6.0.dist-info/RECORD +0 -90
- /figrecipe/_dev/demo_plotters/{plot_bar.py → bar_categorical/plot_bar.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_barh.py → bar_categorical/plot_barh.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_contour.py → contour_surface/plot_contour.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_contourf.py → contour_surface/plot_contourf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tricontour.py → contour_surface/plot_tricontour.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tricontourf.py → contour_surface/plot_tricontourf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tripcolor.py → contour_surface/plot_tripcolor.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_triplot.py → contour_surface/plot_triplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_boxplot.py → distribution/plot_boxplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_ecdf.py → distribution/plot_ecdf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hist.py → distribution/plot_hist.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hist2d.py → distribution/plot_hist2d.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_violinplot.py → distribution/plot_violinplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hexbin.py → image_matrix/plot_hexbin.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_imshow.py → image_matrix/plot_imshow.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_matshow.py → image_matrix/plot_matshow.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_pcolor.py → image_matrix/plot_pcolor.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_pcolormesh.py → image_matrix/plot_pcolormesh.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_spy.py → image_matrix/plot_spy.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_errorbar.py → line_curve/plot_errorbar.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill.py → line_curve/plot_fill.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill_between.py → line_curve/plot_fill_between.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill_betweenx.py → line_curve/plot_fill_betweenx.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stackplot.py → line_curve/plot_stackplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stairs.py → line_curve/plot_stairs.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_step.py → line_curve/plot_step.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_scatter.py → scatter_points/plot_scatter.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_eventplot.py → special/plot_eventplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_loglog.py → special/plot_loglog.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_semilogx.py → special/plot_semilogx.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_semilogy.py → special/plot_semilogy.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stem.py → special/plot_stem.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_acorr.py → spectral_signal/plot_acorr.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_angle_spectrum.py → spectral_signal/plot_angle_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_cohere.py → spectral_signal/plot_cohere.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_csd.py → spectral_signal/plot_csd.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_magnitude_spectrum.py → spectral_signal/plot_magnitude_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_phase_spectrum.py → spectral_signal/plot_phase_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_psd.py → spectral_signal/plot_psd.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_specgram.py → spectral_signal/plot_specgram.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_xcorr.py → spectral_signal/plot_xcorr.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_barbs.py → vector_flow/plot_barbs.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_quiver.py → vector_flow/plot_quiver.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_streamplot.py → vector_flow/plot_streamplot.py} +0 -0
- {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/WHEEL +0 -0
- {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Helper functions for the figure editor.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_form_values_from_style(style: Dict[str, Any]) -> Dict[str, Any]:
|
|
11
|
+
"""Extract form field values from a style dictionary.
|
|
12
|
+
|
|
13
|
+
Maps style dictionary values to HTML form input IDs.
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
style : dict
|
|
18
|
+
Style configuration dictionary
|
|
19
|
+
|
|
20
|
+
Returns
|
|
21
|
+
-------
|
|
22
|
+
dict
|
|
23
|
+
Mapping of form input IDs to values
|
|
24
|
+
"""
|
|
25
|
+
values = {}
|
|
26
|
+
|
|
27
|
+
# Axes dimensions
|
|
28
|
+
if "axes" in style:
|
|
29
|
+
values["axes_width_mm"] = style["axes"].get("width_mm", 80)
|
|
30
|
+
values["axes_height_mm"] = style["axes"].get("height_mm", 55)
|
|
31
|
+
values["axes_thickness_mm"] = style["axes"].get("thickness_mm", 0.2)
|
|
32
|
+
|
|
33
|
+
# Margins
|
|
34
|
+
if "margins" in style:
|
|
35
|
+
values["margins_left_mm"] = style["margins"].get("left_mm", 12)
|
|
36
|
+
values["margins_right_mm"] = style["margins"].get("right_mm", 3)
|
|
37
|
+
values["margins_bottom_mm"] = style["margins"].get("bottom_mm", 10)
|
|
38
|
+
values["margins_top_mm"] = style["margins"].get("top_mm", 3)
|
|
39
|
+
|
|
40
|
+
# Spacing
|
|
41
|
+
if "spacing" in style:
|
|
42
|
+
values["spacing_horizontal_mm"] = style["spacing"].get("horizontal_mm", 8)
|
|
43
|
+
values["spacing_vertical_mm"] = style["spacing"].get("vertical_mm", 8)
|
|
44
|
+
|
|
45
|
+
# Fonts
|
|
46
|
+
if "fonts" in style:
|
|
47
|
+
values["fonts_family"] = style["fonts"].get("family", "Arial")
|
|
48
|
+
values["fonts_axis_label_pt"] = style["fonts"].get("axis_label_pt", 7)
|
|
49
|
+
values["fonts_tick_label_pt"] = style["fonts"].get("tick_label_pt", 6)
|
|
50
|
+
values["fonts_title_pt"] = style["fonts"].get("title_pt", 8)
|
|
51
|
+
values["fonts_legend_pt"] = style["fonts"].get("legend_pt", 6)
|
|
52
|
+
|
|
53
|
+
# Ticks
|
|
54
|
+
if "ticks" in style:
|
|
55
|
+
values["ticks_length_mm"] = style["ticks"].get("length_mm", 1.0)
|
|
56
|
+
values["ticks_thickness_mm"] = style["ticks"].get("thickness_mm", 0.2)
|
|
57
|
+
values["ticks_direction"] = style["ticks"].get("direction", "out")
|
|
58
|
+
|
|
59
|
+
# Lines
|
|
60
|
+
if "lines" in style:
|
|
61
|
+
values["lines_trace_mm"] = style["lines"].get("trace_mm", 0.2)
|
|
62
|
+
|
|
63
|
+
# Markers
|
|
64
|
+
if "markers" in style:
|
|
65
|
+
values["markers_size_mm"] = style["markers"].get("size_mm", 0.8)
|
|
66
|
+
|
|
67
|
+
# Output
|
|
68
|
+
if "output" in style:
|
|
69
|
+
values["output_dpi"] = style["output"].get("dpi", 300)
|
|
70
|
+
|
|
71
|
+
# Behavior
|
|
72
|
+
if "behavior" in style:
|
|
73
|
+
values["behavior_hide_top_spine"] = style["behavior"].get(
|
|
74
|
+
"hide_top_spine", True
|
|
75
|
+
)
|
|
76
|
+
values["behavior_hide_right_spine"] = style["behavior"].get(
|
|
77
|
+
"hide_right_spine", True
|
|
78
|
+
)
|
|
79
|
+
values["behavior_grid"] = style["behavior"].get("grid", False)
|
|
80
|
+
|
|
81
|
+
# Legend
|
|
82
|
+
if "legend" in style:
|
|
83
|
+
values["legend_frameon"] = style["legend"].get("frameon", True)
|
|
84
|
+
|
|
85
|
+
return values
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def render_with_overrides(
|
|
89
|
+
fig, overrides: Optional[Dict[str, Any]], dark_mode: bool = False
|
|
90
|
+
):
|
|
91
|
+
"""
|
|
92
|
+
Re-render figure with overrides applied directly.
|
|
93
|
+
|
|
94
|
+
Applies style overrides directly to the existing figure for reliable rendering.
|
|
95
|
+
"""
|
|
96
|
+
import base64
|
|
97
|
+
import io
|
|
98
|
+
import warnings
|
|
99
|
+
|
|
100
|
+
from matplotlib.backends.backend_agg import FigureCanvasAgg
|
|
101
|
+
from PIL import Image
|
|
102
|
+
|
|
103
|
+
from ._bbox import extract_bboxes
|
|
104
|
+
|
|
105
|
+
# Get the underlying matplotlib figure
|
|
106
|
+
new_fig = fig.fig if hasattr(fig, "fig") else fig
|
|
107
|
+
|
|
108
|
+
# Safety check: validate figure size before rendering
|
|
109
|
+
fig_width, fig_height = new_fig.get_size_inches()
|
|
110
|
+
dpi = 150
|
|
111
|
+
pixel_width = fig_width * dpi
|
|
112
|
+
pixel_height = fig_height * dpi
|
|
113
|
+
|
|
114
|
+
# Sanity check: prevent enormous figures (max 10000x10000 pixels)
|
|
115
|
+
MAX_PIXELS = 10000
|
|
116
|
+
if pixel_width > MAX_PIXELS or pixel_height > MAX_PIXELS:
|
|
117
|
+
# Reset to reasonable size
|
|
118
|
+
new_fig.set_size_inches(
|
|
119
|
+
min(fig_width, MAX_PIXELS / dpi), min(fig_height, MAX_PIXELS / dpi)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Switch to Agg backend to avoid Tkinter thread issues
|
|
123
|
+
new_fig.set_canvas(FigureCanvasAgg(new_fig))
|
|
124
|
+
|
|
125
|
+
# Disable constrained_layout if present (can cause rendering issues)
|
|
126
|
+
layout_engine = new_fig.get_layout_engine()
|
|
127
|
+
if layout_engine is not None and hasattr(layout_engine, "__class__"):
|
|
128
|
+
layout_name = layout_engine.__class__.__name__
|
|
129
|
+
if "Constrained" in layout_name:
|
|
130
|
+
new_fig.set_layout_engine("none")
|
|
131
|
+
|
|
132
|
+
# Apply overrides directly to existing figure
|
|
133
|
+
# Get record for call_id grouping (if fig is a RecordingFigure)
|
|
134
|
+
record = fig.record if hasattr(fig, "record") else None
|
|
135
|
+
if overrides:
|
|
136
|
+
from ._render_overrides import apply_overrides
|
|
137
|
+
|
|
138
|
+
apply_overrides(new_fig, overrides, record)
|
|
139
|
+
|
|
140
|
+
# Apply dark mode if requested
|
|
141
|
+
if dark_mode:
|
|
142
|
+
from ._render_overrides import apply_dark_mode
|
|
143
|
+
|
|
144
|
+
apply_dark_mode(new_fig)
|
|
145
|
+
|
|
146
|
+
# Validate axes bounds before rendering (prevent infinite/invalid extents)
|
|
147
|
+
for ax in new_fig.get_axes():
|
|
148
|
+
xlim = ax.get_xlim()
|
|
149
|
+
ylim = ax.get_ylim()
|
|
150
|
+
# Check for invalid limits (inf, nan, or extremely large)
|
|
151
|
+
if any(not (-1e10 < v < 1e10) for v in xlim + ylim):
|
|
152
|
+
ax.set_xlim(-1, 1)
|
|
153
|
+
ax.set_ylim(-1, 1)
|
|
154
|
+
|
|
155
|
+
# Save to PNG using same params as static save
|
|
156
|
+
# Re-create canvas to ensure clean state (avoids matplotlib renderer issues)
|
|
157
|
+
# Also save/restore the figure's draw method which can get corrupted by _get_renderer
|
|
158
|
+
original_draw = getattr(new_fig, "draw", None)
|
|
159
|
+
new_fig.set_canvas(FigureCanvasAgg(new_fig))
|
|
160
|
+
|
|
161
|
+
buf = io.BytesIO()
|
|
162
|
+
with warnings.catch_warnings():
|
|
163
|
+
warnings.filterwarnings("ignore", "constrained_layout not applied")
|
|
164
|
+
warnings.filterwarnings("ignore", category=UserWarning)
|
|
165
|
+
try:
|
|
166
|
+
new_fig.savefig(buf, format="png", dpi=150, bbox_inches="tight")
|
|
167
|
+
except Exception:
|
|
168
|
+
# Fall back to saving without bbox_inches="tight"
|
|
169
|
+
# Catches matplotlib internal exceptions (e.g., Done from _get_renderer)
|
|
170
|
+
buf = io.BytesIO()
|
|
171
|
+
# Reset canvas and draw method to clean state
|
|
172
|
+
new_fig.set_canvas(FigureCanvasAgg(new_fig))
|
|
173
|
+
if original_draw is not None:
|
|
174
|
+
new_fig.draw = original_draw
|
|
175
|
+
try:
|
|
176
|
+
new_fig.savefig(buf, format="png", dpi=150)
|
|
177
|
+
except Exception:
|
|
178
|
+
# Last resort: create empty placeholder
|
|
179
|
+
from PIL import Image as PILImage
|
|
180
|
+
|
|
181
|
+
placeholder = PILImage.new("RGB", (400, 300), color=(240, 240, 240))
|
|
182
|
+
placeholder.save(buf, format="PNG")
|
|
183
|
+
buf.seek(0)
|
|
184
|
+
finally:
|
|
185
|
+
# Always restore original draw method to prevent corruption
|
|
186
|
+
if original_draw is not None and hasattr(new_fig, "draw"):
|
|
187
|
+
try:
|
|
188
|
+
new_fig.draw = original_draw
|
|
189
|
+
except AttributeError:
|
|
190
|
+
pass
|
|
191
|
+
buf.seek(0)
|
|
192
|
+
png_bytes = buf.read()
|
|
193
|
+
base64_str = base64.b64encode(png_bytes).decode("utf-8")
|
|
194
|
+
|
|
195
|
+
# Get image size
|
|
196
|
+
buf.seek(0)
|
|
197
|
+
img = Image.open(buf)
|
|
198
|
+
img_size = img.size
|
|
199
|
+
|
|
200
|
+
# Extract bboxes
|
|
201
|
+
# Ensure clean canvas state before drawing
|
|
202
|
+
new_fig.set_canvas(FigureCanvasAgg(new_fig))
|
|
203
|
+
original_dpi = new_fig.dpi
|
|
204
|
+
new_fig.set_dpi(150)
|
|
205
|
+
try:
|
|
206
|
+
new_fig.canvas.draw()
|
|
207
|
+
except Exception:
|
|
208
|
+
# Canvas draw failed, likely due to corrupted state - reset and retry
|
|
209
|
+
new_fig.set_canvas(FigureCanvasAgg(new_fig))
|
|
210
|
+
try:
|
|
211
|
+
new_fig.canvas.draw()
|
|
212
|
+
except Exception:
|
|
213
|
+
pass # If still fails, proceed with possibly stale bboxes
|
|
214
|
+
bboxes = extract_bboxes(new_fig, img_size[0], img_size[1])
|
|
215
|
+
new_fig.set_dpi(original_dpi)
|
|
216
|
+
|
|
217
|
+
return base64_str, bboxes, img_size
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def to_json_serializable(obj):
|
|
221
|
+
"""Convert numpy arrays and other non-serializable objects to JSON-safe types."""
|
|
222
|
+
import numpy as np
|
|
223
|
+
|
|
224
|
+
if isinstance(obj, np.ndarray):
|
|
225
|
+
return obj.tolist()
|
|
226
|
+
elif isinstance(obj, (np.integer, np.floating)):
|
|
227
|
+
return obj.item()
|
|
228
|
+
# Handle pandas Series
|
|
229
|
+
elif hasattr(obj, "values") and hasattr(obj, "tolist"):
|
|
230
|
+
return obj.tolist()
|
|
231
|
+
elif isinstance(obj, dict):
|
|
232
|
+
return {k: to_json_serializable(v) for k, v in obj.items()}
|
|
233
|
+
elif isinstance(obj, (list, tuple)):
|
|
234
|
+
return [to_json_serializable(item) for item in obj]
|
|
235
|
+
return obj
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
__all__ = [
|
|
239
|
+
"get_form_values_from_style",
|
|
240
|
+
"render_with_overrides",
|
|
241
|
+
"to_json_serializable",
|
|
242
|
+
]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Hitmap generation package.
|
|
4
|
+
|
|
5
|
+
This package provides hitmap generation for interactive element selection.
|
|
6
|
+
The main entry point is `generate_hitmap`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# Re-export main functions from _hitmap_main
|
|
10
|
+
from .._hitmap_main import generate_hitmap, hitmap_to_base64
|
|
11
|
+
|
|
12
|
+
# Re-export from artist processing
|
|
13
|
+
from ._artists import (
|
|
14
|
+
process_collections,
|
|
15
|
+
process_figure_text,
|
|
16
|
+
process_images,
|
|
17
|
+
process_legend,
|
|
18
|
+
process_lines,
|
|
19
|
+
process_patches,
|
|
20
|
+
process_text,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Re-export from color utilities
|
|
24
|
+
from ._colors import (
|
|
25
|
+
AXES_COLOR,
|
|
26
|
+
BACKGROUND_COLOR,
|
|
27
|
+
DISTINCT_COLORS,
|
|
28
|
+
hsv_to_rgb,
|
|
29
|
+
id_to_rgb,
|
|
30
|
+
mpl_color_to_hex,
|
|
31
|
+
normalize_color,
|
|
32
|
+
rgb_to_id,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Re-export from detection utilities
|
|
36
|
+
from ._detect import detect_plot_types, is_boxplot_element, is_violin_element
|
|
37
|
+
|
|
38
|
+
# Re-export from restoration utilities
|
|
39
|
+
from ._restore import (
|
|
40
|
+
restore_axes_properties,
|
|
41
|
+
restore_backgrounds,
|
|
42
|
+
restore_figure_text,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
# Main functions
|
|
47
|
+
"generate_hitmap",
|
|
48
|
+
"hitmap_to_base64",
|
|
49
|
+
# Color utilities
|
|
50
|
+
"id_to_rgb",
|
|
51
|
+
"rgb_to_id",
|
|
52
|
+
"DISTINCT_COLORS",
|
|
53
|
+
"BACKGROUND_COLOR",
|
|
54
|
+
"AXES_COLOR",
|
|
55
|
+
"hsv_to_rgb",
|
|
56
|
+
"normalize_color",
|
|
57
|
+
"mpl_color_to_hex",
|
|
58
|
+
# Detection utilities
|
|
59
|
+
"detect_plot_types",
|
|
60
|
+
"is_boxplot_element",
|
|
61
|
+
"is_violin_element",
|
|
62
|
+
# Artist processing
|
|
63
|
+
"process_collections",
|
|
64
|
+
"process_figure_text",
|
|
65
|
+
"process_images",
|
|
66
|
+
"process_legend",
|
|
67
|
+
"process_lines",
|
|
68
|
+
"process_patches",
|
|
69
|
+
"process_text",
|
|
70
|
+
# Restoration utilities
|
|
71
|
+
"restore_axes_properties",
|
|
72
|
+
"restore_backgrounds",
|
|
73
|
+
"restore_figure_text",
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
# EOF
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Artist processing package for hitmap generation."""
|
|
4
|
+
|
|
5
|
+
from ._collections import process_collections
|
|
6
|
+
from ._images import process_images
|
|
7
|
+
from ._lines import process_lines
|
|
8
|
+
from ._patches import process_patches
|
|
9
|
+
from ._text import process_figure_text, process_legend, process_text
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"process_lines",
|
|
13
|
+
"process_collections",
|
|
14
|
+
"process_patches",
|
|
15
|
+
"process_images",
|
|
16
|
+
"process_text",
|
|
17
|
+
"process_legend",
|
|
18
|
+
"process_figure_text",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
# EOF
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Collection processing for hitmap generation."""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict
|
|
6
|
+
|
|
7
|
+
from matplotlib.collections import (
|
|
8
|
+
LineCollection,
|
|
9
|
+
PathCollection,
|
|
10
|
+
PolyCollection,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from .._colors import id_to_rgb, mpl_color_to_hex, normalize_color
|
|
14
|
+
from .._detect import is_violin_element
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def process_collections(
|
|
18
|
+
ax,
|
|
19
|
+
ax_idx: int,
|
|
20
|
+
element_id: int,
|
|
21
|
+
original_props: Dict[str, Any],
|
|
22
|
+
color_map: Dict[str, Any],
|
|
23
|
+
ax_info: Dict[str, Any],
|
|
24
|
+
) -> int:
|
|
25
|
+
"""Process collections (scatter, fills, etc.) on an axes.
|
|
26
|
+
|
|
27
|
+
Returns updated element_id.
|
|
28
|
+
"""
|
|
29
|
+
from matplotlib.collections import QuadMesh
|
|
30
|
+
from matplotlib.contour import QuadContourSet
|
|
31
|
+
from matplotlib.quiver import Quiver
|
|
32
|
+
|
|
33
|
+
ax_plot_types = ax_info.get("types", set())
|
|
34
|
+
ax_call_ids = ax_info.get("call_ids", {})
|
|
35
|
+
has_violin = "violinplot" in ax_plot_types
|
|
36
|
+
has_record = len(ax_plot_types) > 0
|
|
37
|
+
|
|
38
|
+
violin_ids = list(ax_call_ids.get("violinplot", []))
|
|
39
|
+
scatter_ids = list(ax_call_ids.get("scatter", []))
|
|
40
|
+
quiver_ids = list(ax_call_ids.get("quiver", []))
|
|
41
|
+
fill_between_ids = list(ax_call_ids.get("fill_between", []))
|
|
42
|
+
pcolormesh_ids = list(ax_call_ids.get("pcolormesh", []))
|
|
43
|
+
contour_ids = list(ax_call_ids.get("contour", []))
|
|
44
|
+
contourf_ids = list(ax_call_ids.get("contourf", []))
|
|
45
|
+
|
|
46
|
+
violin_call_id = violin_ids[0] if violin_ids else None
|
|
47
|
+
pcolormesh_call_id = pcolormesh_ids[0] if pcolormesh_ids else None
|
|
48
|
+
contour_call_id = contour_ids[0] if contour_ids else None
|
|
49
|
+
contourf_call_id = contourf_ids[0] if contourf_ids else None
|
|
50
|
+
scatter_coll_idx = 0
|
|
51
|
+
fill_coll_idx = 0
|
|
52
|
+
|
|
53
|
+
for i, coll in enumerate(ax.collections):
|
|
54
|
+
if isinstance(coll, Quiver):
|
|
55
|
+
element_id = _process_quiver(
|
|
56
|
+
coll, i, ax_idx, element_id, original_props, color_map, quiver_ids
|
|
57
|
+
)
|
|
58
|
+
elif isinstance(coll, PathCollection):
|
|
59
|
+
element_id, scatter_coll_idx = _process_scatter(
|
|
60
|
+
coll,
|
|
61
|
+
i,
|
|
62
|
+
ax_idx,
|
|
63
|
+
element_id,
|
|
64
|
+
original_props,
|
|
65
|
+
color_map,
|
|
66
|
+
scatter_ids,
|
|
67
|
+
scatter_coll_idx,
|
|
68
|
+
)
|
|
69
|
+
elif isinstance(coll, PolyCollection):
|
|
70
|
+
element_id, fill_coll_idx = _process_polycoll(
|
|
71
|
+
coll,
|
|
72
|
+
i,
|
|
73
|
+
ax_idx,
|
|
74
|
+
element_id,
|
|
75
|
+
original_props,
|
|
76
|
+
color_map,
|
|
77
|
+
has_violin,
|
|
78
|
+
violin_call_id,
|
|
79
|
+
has_record,
|
|
80
|
+
ax,
|
|
81
|
+
fill_between_ids,
|
|
82
|
+
fill_coll_idx,
|
|
83
|
+
)
|
|
84
|
+
elif isinstance(coll, LineCollection):
|
|
85
|
+
element_id = _process_linecoll(
|
|
86
|
+
coll,
|
|
87
|
+
i,
|
|
88
|
+
ax_idx,
|
|
89
|
+
element_id,
|
|
90
|
+
original_props,
|
|
91
|
+
color_map,
|
|
92
|
+
has_violin,
|
|
93
|
+
violin_call_id,
|
|
94
|
+
)
|
|
95
|
+
elif isinstance(coll, QuadMesh):
|
|
96
|
+
element_id = _process_quadmesh(
|
|
97
|
+
coll, i, ax_idx, element_id, color_map, pcolormesh_call_id
|
|
98
|
+
)
|
|
99
|
+
elif isinstance(coll, QuadContourSet):
|
|
100
|
+
element_id = _process_contour(
|
|
101
|
+
coll,
|
|
102
|
+
i,
|
|
103
|
+
ax_idx,
|
|
104
|
+
element_id,
|
|
105
|
+
color_map,
|
|
106
|
+
contour_call_id,
|
|
107
|
+
contourf_call_id,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return element_id
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _process_quiver(coll, i, ax_idx, element_id, original_props, color_map, quiver_ids):
|
|
114
|
+
"""Process Quiver collection."""
|
|
115
|
+
if not coll.get_visible():
|
|
116
|
+
return element_id
|
|
117
|
+
|
|
118
|
+
key = f"ax{ax_idx}_quiver{i}"
|
|
119
|
+
rgb = id_to_rgb(element_id)
|
|
120
|
+
|
|
121
|
+
original_props[key] = {"color": coll.get_facecolor().copy()}
|
|
122
|
+
coll.set_color(normalize_color(rgb))
|
|
123
|
+
|
|
124
|
+
call_id = quiver_ids[0] if quiver_ids else None
|
|
125
|
+
label = call_id or f"quiver_{i}"
|
|
126
|
+
|
|
127
|
+
color_map[key] = {
|
|
128
|
+
"id": element_id,
|
|
129
|
+
"type": "quiver",
|
|
130
|
+
"label": label,
|
|
131
|
+
"ax_index": ax_idx,
|
|
132
|
+
"rgb": list(rgb),
|
|
133
|
+
"call_id": call_id,
|
|
134
|
+
}
|
|
135
|
+
return element_id + 1
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _process_scatter(
|
|
139
|
+
coll,
|
|
140
|
+
i,
|
|
141
|
+
ax_idx,
|
|
142
|
+
element_id,
|
|
143
|
+
original_props,
|
|
144
|
+
color_map,
|
|
145
|
+
scatter_ids,
|
|
146
|
+
scatter_coll_idx,
|
|
147
|
+
):
|
|
148
|
+
"""Process PathCollection (scatter)."""
|
|
149
|
+
if not coll.get_visible():
|
|
150
|
+
return element_id, scatter_coll_idx
|
|
151
|
+
|
|
152
|
+
key = f"ax{ax_idx}_scatter{i}"
|
|
153
|
+
rgb = id_to_rgb(element_id)
|
|
154
|
+
|
|
155
|
+
original_props[key] = {
|
|
156
|
+
"facecolors": coll.get_facecolors().copy(),
|
|
157
|
+
"edgecolors": coll.get_edgecolors().copy(),
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
coll.set_facecolors([normalize_color(rgb)])
|
|
161
|
+
coll.set_edgecolors([normalize_color(rgb)])
|
|
162
|
+
|
|
163
|
+
orig_fc = original_props[key]["facecolors"]
|
|
164
|
+
orig_color = orig_fc[0] if len(orig_fc) > 0 else [0.5, 0.5, 0.5, 1]
|
|
165
|
+
|
|
166
|
+
orig_label = coll.get_label() or f"scatter_{i}"
|
|
167
|
+
if scatter_coll_idx < len(scatter_ids):
|
|
168
|
+
call_id = scatter_ids[scatter_coll_idx]
|
|
169
|
+
label = call_id
|
|
170
|
+
else:
|
|
171
|
+
call_id = f"scatter_{ax_idx}_{scatter_coll_idx}"
|
|
172
|
+
label = call_id if orig_label.startswith("_") else orig_label
|
|
173
|
+
|
|
174
|
+
color_map[key] = {
|
|
175
|
+
"id": element_id,
|
|
176
|
+
"type": "scatter",
|
|
177
|
+
"label": label,
|
|
178
|
+
"ax_index": ax_idx,
|
|
179
|
+
"rgb": list(rgb),
|
|
180
|
+
"original_color": mpl_color_to_hex(orig_color),
|
|
181
|
+
"call_id": call_id,
|
|
182
|
+
}
|
|
183
|
+
return element_id + 1, scatter_coll_idx + 1
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _process_polycoll(
|
|
187
|
+
coll,
|
|
188
|
+
i,
|
|
189
|
+
ax_idx,
|
|
190
|
+
element_id,
|
|
191
|
+
original_props,
|
|
192
|
+
color_map,
|
|
193
|
+
has_violin,
|
|
194
|
+
violin_call_id,
|
|
195
|
+
has_record,
|
|
196
|
+
ax,
|
|
197
|
+
fill_between_ids=None,
|
|
198
|
+
fill_coll_idx=0,
|
|
199
|
+
):
|
|
200
|
+
"""Process PolyCollection (fills, violin bodies)."""
|
|
201
|
+
if fill_between_ids is None:
|
|
202
|
+
fill_between_ids = []
|
|
203
|
+
|
|
204
|
+
if not coll.get_visible():
|
|
205
|
+
return element_id, fill_coll_idx
|
|
206
|
+
|
|
207
|
+
orig_label = coll.get_label() or ""
|
|
208
|
+
|
|
209
|
+
# Check if this is a fill_between element (should NOT be skipped)
|
|
210
|
+
has_fill_between = len(fill_between_ids) > 0
|
|
211
|
+
|
|
212
|
+
if has_record and not has_fill_between:
|
|
213
|
+
# Only skip _child/_nolegend if NOT from fill_between
|
|
214
|
+
if orig_label.startswith("_child") or orig_label.startswith("_nolegend"):
|
|
215
|
+
return element_id, fill_coll_idx
|
|
216
|
+
|
|
217
|
+
key = f"ax{ax_idx}_fill{i}"
|
|
218
|
+
rgb = id_to_rgb(element_id)
|
|
219
|
+
|
|
220
|
+
original_props[key] = {
|
|
221
|
+
"facecolors": coll.get_facecolors().copy(),
|
|
222
|
+
"edgecolors": coll.get_edgecolors().copy(),
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
coll.set_facecolors([normalize_color(rgb)])
|
|
226
|
+
coll.set_edgecolors([normalize_color(rgb)])
|
|
227
|
+
|
|
228
|
+
orig_fc = original_props[key]["facecolors"]
|
|
229
|
+
orig_color = orig_fc[0] if len(orig_fc) > 0 else [0.5, 0.5, 0.5, 1]
|
|
230
|
+
|
|
231
|
+
if has_violin and is_violin_element(coll, ax):
|
|
232
|
+
elem_type = "violin"
|
|
233
|
+
label = violin_call_id or "violin"
|
|
234
|
+
call_id = violin_call_id
|
|
235
|
+
else:
|
|
236
|
+
elem_type = "fill"
|
|
237
|
+
# Try to get fill_between call_id
|
|
238
|
+
if fill_coll_idx < len(fill_between_ids):
|
|
239
|
+
call_id = fill_between_ids[fill_coll_idx]
|
|
240
|
+
label = call_id
|
|
241
|
+
fill_coll_idx += 1
|
|
242
|
+
else:
|
|
243
|
+
label = orig_label if not orig_label.startswith("_") else f"fill_{i}"
|
|
244
|
+
call_id = None
|
|
245
|
+
|
|
246
|
+
color_map[key] = {
|
|
247
|
+
"id": element_id,
|
|
248
|
+
"type": elem_type,
|
|
249
|
+
"label": label,
|
|
250
|
+
"ax_index": ax_idx,
|
|
251
|
+
"rgb": list(rgb),
|
|
252
|
+
"original_color": mpl_color_to_hex(orig_color),
|
|
253
|
+
"call_id": call_id,
|
|
254
|
+
}
|
|
255
|
+
return element_id + 1, fill_coll_idx
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _process_linecoll(
|
|
259
|
+
coll, i, ax_idx, element_id, original_props, color_map, has_violin, violin_call_id
|
|
260
|
+
):
|
|
261
|
+
"""Process LineCollection (violin inner lines)."""
|
|
262
|
+
if not coll.get_visible():
|
|
263
|
+
return element_id
|
|
264
|
+
|
|
265
|
+
key = f"ax{ax_idx}_linecoll{i}"
|
|
266
|
+
rgb = id_to_rgb(element_id)
|
|
267
|
+
|
|
268
|
+
original_props[key] = {
|
|
269
|
+
"colors": coll.get_colors().copy() if hasattr(coll, "get_colors") else [],
|
|
270
|
+
"edgecolors": coll.get_edgecolors().copy(),
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
coll.set_color(normalize_color(rgb))
|
|
274
|
+
|
|
275
|
+
orig_colors = original_props[key]["colors"]
|
|
276
|
+
orig_color = orig_colors[0] if len(orig_colors) > 0 else [0.5, 0.5, 0.5, 1]
|
|
277
|
+
|
|
278
|
+
if has_violin:
|
|
279
|
+
elem_type = "violin"
|
|
280
|
+
label = violin_call_id or "violin"
|
|
281
|
+
call_id = violin_call_id
|
|
282
|
+
else:
|
|
283
|
+
elem_type = "linecollection"
|
|
284
|
+
label = f"linecoll_{i}"
|
|
285
|
+
call_id = None
|
|
286
|
+
|
|
287
|
+
color_map[key] = {
|
|
288
|
+
"id": element_id,
|
|
289
|
+
"type": elem_type,
|
|
290
|
+
"label": label,
|
|
291
|
+
"ax_index": ax_idx,
|
|
292
|
+
"rgb": list(rgb),
|
|
293
|
+
"original_color": mpl_color_to_hex(orig_color),
|
|
294
|
+
"call_id": call_id,
|
|
295
|
+
}
|
|
296
|
+
return element_id + 1
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _process_quadmesh(coll, i, ax_idx, element_id, color_map, pcolormesh_call_id=None):
|
|
300
|
+
"""Process QuadMesh (pcolormesh, hexbin, hist2d)."""
|
|
301
|
+
if not coll.get_visible():
|
|
302
|
+
return element_id
|
|
303
|
+
|
|
304
|
+
key = f"ax{ax_idx}_quadmesh{i}"
|
|
305
|
+
rgb = id_to_rgb(element_id)
|
|
306
|
+
|
|
307
|
+
call_id = pcolormesh_call_id
|
|
308
|
+
label = call_id or f"quadmesh_{i}"
|
|
309
|
+
|
|
310
|
+
color_map[key] = {
|
|
311
|
+
"id": element_id,
|
|
312
|
+
"type": "quadmesh",
|
|
313
|
+
"label": label,
|
|
314
|
+
"ax_index": ax_idx,
|
|
315
|
+
"rgb": list(rgb),
|
|
316
|
+
"call_id": call_id,
|
|
317
|
+
}
|
|
318
|
+
return element_id + 1
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _process_contour(
|
|
322
|
+
coll, i, ax_idx, element_id, color_map, contour_call_id=None, contourf_call_id=None
|
|
323
|
+
):
|
|
324
|
+
"""Process QuadContourSet (contour, contourf)."""
|
|
325
|
+
key = f"ax{ax_idx}_contour{i}"
|
|
326
|
+
rgb = id_to_rgb(element_id)
|
|
327
|
+
|
|
328
|
+
# Try contourf first, then contour
|
|
329
|
+
call_id = contourf_call_id or contour_call_id
|
|
330
|
+
label = call_id or f"contour_{i}"
|
|
331
|
+
|
|
332
|
+
color_map[key] = {
|
|
333
|
+
"id": element_id,
|
|
334
|
+
"type": "contour",
|
|
335
|
+
"label": label,
|
|
336
|
+
"ax_index": ax_idx,
|
|
337
|
+
"rgb": list(rgb),
|
|
338
|
+
"call_id": call_id,
|
|
339
|
+
}
|
|
340
|
+
return element_id + 1
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
__all__ = ["process_collections"]
|
|
344
|
+
|
|
345
|
+
# EOF
|