figrecipe 0.6.0__py3-none-any.whl → 0.9.0__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 +161 -1030
- figrecipe/__main__.py +12 -0
- figrecipe/_api/__init__.py +48 -0
- figrecipe/_api/_extract.py +108 -0
- figrecipe/_api/_notebook.py +61 -0
- figrecipe/_api/_panel.py +113 -0
- figrecipe/_api/_save.py +287 -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/_cli/__init__.py +7 -0
- figrecipe/_cli/_compose.py +87 -0
- figrecipe/_cli/_convert.py +117 -0
- figrecipe/_cli/_crop.py +82 -0
- figrecipe/_cli/_edit.py +70 -0
- figrecipe/_cli/_extract.py +128 -0
- figrecipe/_cli/_fonts.py +47 -0
- figrecipe/_cli/_info.py +67 -0
- figrecipe/_cli/_main.py +58 -0
- figrecipe/_cli/_reproduce.py +79 -0
- figrecipe/_cli/_style.py +77 -0
- figrecipe/_cli/_validate.py +66 -0
- figrecipe/_cli/_version.py +50 -0
- figrecipe/_composition/__init__.py +32 -0
- figrecipe/_composition/_alignment.py +452 -0
- figrecipe/_composition/_compose.py +179 -0
- figrecipe/_composition/_import_axes.py +127 -0
- figrecipe/_composition/_visibility.py +125 -0
- figrecipe/_dev/__init__.py +4 -93
- figrecipe/_dev/_plotters.py +76 -0
- figrecipe/_dev/_run_demos.py +56 -0
- figrecipe/_dev/browser/__init__.py +69 -0
- figrecipe/_dev/browser/_audio.py +240 -0
- figrecipe/_dev/browser/_caption.py +356 -0
- figrecipe/_dev/browser/_click_effect.py +146 -0
- figrecipe/_dev/browser/_cursor.py +196 -0
- figrecipe/_dev/browser/_highlight.py +105 -0
- figrecipe/_dev/browser/_narration.py +237 -0
- figrecipe/_dev/browser/_recorder.py +446 -0
- figrecipe/_dev/browser/_utils.py +178 -0
- figrecipe/_dev/browser/_video_trim/__init__.py +152 -0
- figrecipe/_dev/browser/_video_trim/_detection.py +223 -0
- figrecipe/_dev/browser/_video_trim/_markers.py +140 -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 +61 -13
- 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 +402 -0
- figrecipe/_editor/_bbox/_extract_axes.py +370 -0
- figrecipe/_editor/_bbox/_extract_text.py +466 -0
- figrecipe/_editor/_bbox/_lines.py +173 -0
- figrecipe/_editor/_bbox/_transforms.py +146 -0
- figrecipe/_editor/_call_overrides.py +183 -0
- figrecipe/_editor/_datatable_plot_handlers.py +249 -0
- figrecipe/_editor/_figure_layout.py +211 -0
- figrecipe/_editor/_flask_app.py +200 -1030
- figrecipe/_editor/_helpers.py +251 -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 +194 -0
- figrecipe/_editor/_hitmap/_restore.py +154 -0
- figrecipe/_editor/_hitmap_main.py +182 -0
- figrecipe/_editor/_overrides.py +4 -1
- figrecipe/_editor/_plot_types_registry.py +190 -0
- figrecipe/_editor/_preferences.py +135 -0
- figrecipe/_editor/_render_overrides.py +507 -0
- figrecipe/_editor/_renderer.py +81 -186
- figrecipe/_editor/_routes_annotation.py +114 -0
- figrecipe/_editor/_routes_axis.py +482 -0
- figrecipe/_editor/_routes_captions.py +130 -0
- figrecipe/_editor/_routes_composition.py +270 -0
- figrecipe/_editor/_routes_core.py +126 -0
- figrecipe/_editor/_routes_datatable.py +364 -0
- figrecipe/_editor/_routes_element.py +335 -0
- figrecipe/_editor/_routes_files.py +443 -0
- figrecipe/_editor/_routes_image.py +200 -0
- figrecipe/_editor/_routes_snapshot.py +94 -0
- figrecipe/_editor/_routes_style.py +243 -0
- figrecipe/_editor/_templates/__init__.py +116 -1
- figrecipe/_editor/_templates/_html.py +154 -64
- figrecipe/_editor/_templates/_html_components/__init__.py +13 -0
- figrecipe/_editor/_templates/_html_components/_composition_toolbar.py +79 -0
- figrecipe/_editor/_templates/_html_components/_file_browser.py +41 -0
- figrecipe/_editor/_templates/_html_datatable.py +92 -0
- figrecipe/_editor/_templates/_scripts/__init__.py +178 -0
- figrecipe/_editor/_templates/_scripts/_accordion.py +328 -0
- figrecipe/_editor/_templates/_scripts/_annotation_drag.py +504 -0
- figrecipe/_editor/_templates/_scripts/_api.py +228 -0
- figrecipe/_editor/_templates/_scripts/_canvas_context_menu.py +182 -0
- figrecipe/_editor/_templates/_scripts/_captions.py +231 -0
- figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
- figrecipe/_editor/_templates/_scripts/_composition.py +283 -0
- figrecipe/_editor/_templates/_scripts/_core.py +493 -0
- figrecipe/_editor/_templates/_scripts/_datatable/__init__.py +59 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_cell_edit.py +97 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_clipboard.py +164 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_context_menu.py +221 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_core.py +150 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_editable.py +511 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_import.py +161 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_plot.py +261 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_selection.py +438 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_table.py +256 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_tabs.py +354 -0
- figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
- figrecipe/_editor/_templates/_scripts/_element_editor.py +325 -0
- figrecipe/_editor/_templates/_scripts/_files.py +429 -0
- figrecipe/_editor/_templates/_scripts/_files_context_menu.py +240 -0
- figrecipe/_editor/_templates/_scripts/_hitmap.py +512 -0
- figrecipe/_editor/_templates/_scripts/_image_drop.py +428 -0
- figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
- figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
- figrecipe/_editor/_templates/_scripts/_legend_drag.py +270 -0
- figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
- figrecipe/_editor/_templates/_scripts/_multi_select.py +198 -0
- figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag.py +505 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag_snapshot.py +33 -0
- figrecipe/_editor/_templates/_scripts/_panel_position.py +463 -0
- figrecipe/_editor/_templates/_scripts/_panel_resize.py +230 -0
- figrecipe/_editor/_templates/_scripts/_panel_snap.py +307 -0
- figrecipe/_editor/_templates/_scripts/_region_select.py +255 -0
- figrecipe/_editor/_templates/_scripts/_selection.py +244 -0
- figrecipe/_editor/_templates/_scripts/_sync.py +242 -0
- figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
- figrecipe/_editor/_templates/_scripts/_undo_redo.py +348 -0
- figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
- figrecipe/_editor/_templates/_scripts/_zoom.py +212 -0
- figrecipe/_editor/_templates/_styles/__init__.py +78 -0
- figrecipe/_editor/_templates/_styles/_base.py +111 -0
- figrecipe/_editor/_templates/_styles/_buttons.py +327 -0
- figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
- figrecipe/_editor/_templates/_styles/_composition.py +87 -0
- figrecipe/_editor/_templates/_styles/_controls.py +430 -0
- figrecipe/_editor/_templates/_styles/_datatable/__init__.py +40 -0
- figrecipe/_editor/_templates/_styles/_datatable/_editable.py +203 -0
- figrecipe/_editor/_templates/_styles/_datatable/_panel.py +268 -0
- figrecipe/_editor/_templates/_styles/_datatable/_table.py +479 -0
- figrecipe/_editor/_templates/_styles/_datatable/_toolbar.py +384 -0
- figrecipe/_editor/_templates/_styles/_datatable/_vars.py +123 -0
- figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
- figrecipe/_editor/_templates/_styles/_file_browser.py +466 -0
- figrecipe/_editor/_templates/_styles/_forms.py +224 -0
- figrecipe/_editor/_templates/_styles/_hitmap.py +191 -0
- figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
- figrecipe/_editor/_templates/_styles/_labels.py +118 -0
- figrecipe/_editor/_templates/_styles/_modals.py +127 -0
- figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
- figrecipe/_editor/_templates/_styles/_preview.py +430 -0
- figrecipe/_editor/_templates/_styles/_selection.py +73 -0
- figrecipe/_editor/_templates/_styles/_spinner.py +117 -0
- figrecipe/_editor/static/audio/click.mp3 +0 -0
- figrecipe/_editor/static/click.mp3 +0 -0
- figrecipe/_editor/static/icons/favicon.ico +0 -0
- figrecipe/_integrations/__init__.py +17 -0
- figrecipe/_integrations/_scitex_stats.py +298 -0
- figrecipe/_params/_DECORATION_METHODS.py +8 -0
- figrecipe/_recorder.py +63 -109
- figrecipe/_recorder_utils.py +124 -0
- figrecipe/_reproducer/__init__.py +18 -0
- figrecipe/_reproducer/_core.py +509 -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/_utils/__init__.py +3 -0
- figrecipe/_utils/_bundle.py +205 -0
- figrecipe/_wrappers/_axes.py +252 -895
- figrecipe/_wrappers/_axes_helpers.py +136 -0
- figrecipe/_wrappers/_axes_plots.py +418 -0
- figrecipe/_wrappers/_axes_seaborn.py +157 -0
- figrecipe/_wrappers/_caption_generator.py +218 -0
- figrecipe/_wrappers/_figure.py +188 -1
- figrecipe/_wrappers/_panel_labels.py +127 -0
- figrecipe/_wrappers/_plot_helpers.py +143 -0
- figrecipe/_wrappers/_stat_annotation.py +274 -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 +42 -480
- 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 +40 -28
- figrecipe-0.9.0.dist-info/METADATA +427 -0
- figrecipe-0.9.0.dist-info/RECORD +277 -0
- figrecipe-0.9.0.dist-info/entry_points.txt +2 -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/METADATA +0 -394
- 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.9.0.dist-info}/WHEEL +0 -0
- {figrecipe-0.6.0.dist-info → figrecipe-0.9.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Plot type detection utilities for hitmap generation."""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def detect_plot_types(fig, debug: bool = False) -> Dict[int, Dict[str, Any]]:
|
|
9
|
+
"""Detect plot types from recorded calls in figure.
|
|
10
|
+
|
|
11
|
+
Parameters
|
|
12
|
+
----------
|
|
13
|
+
fig : Figure
|
|
14
|
+
The figure to analyze.
|
|
15
|
+
debug : bool
|
|
16
|
+
If True, print debug information.
|
|
17
|
+
|
|
18
|
+
Returns
|
|
19
|
+
-------
|
|
20
|
+
dict
|
|
21
|
+
Mapping from ax_index (matching fig.get_axes() order) to plot type info.
|
|
22
|
+
"""
|
|
23
|
+
# Get figure record if available
|
|
24
|
+
if hasattr(fig, "record"):
|
|
25
|
+
record = fig.record
|
|
26
|
+
elif hasattr(fig, "fig") and hasattr(fig.fig, "_record"):
|
|
27
|
+
record = fig.fig._record
|
|
28
|
+
else:
|
|
29
|
+
if debug:
|
|
30
|
+
print("[detect_plot_types] No record found")
|
|
31
|
+
return {}
|
|
32
|
+
|
|
33
|
+
# Get the actual matplotlib figure and its axes
|
|
34
|
+
mpl_fig = fig.fig if hasattr(fig, "fig") else fig
|
|
35
|
+
axes_list = mpl_fig.get_axes()
|
|
36
|
+
|
|
37
|
+
result = {}
|
|
38
|
+
|
|
39
|
+
# Process each axes in the record
|
|
40
|
+
if hasattr(record, "axes"):
|
|
41
|
+
# Build mapping from ax_key to ax_record's plot info
|
|
42
|
+
ax_key_to_info = {}
|
|
43
|
+
for ax_key, ax_record in record.axes.items():
|
|
44
|
+
info = {"types": set(), "call_ids": {}}
|
|
45
|
+
|
|
46
|
+
if hasattr(ax_record, "calls"):
|
|
47
|
+
for call in ax_record.calls:
|
|
48
|
+
func_name = call.function
|
|
49
|
+
call_id = call.id
|
|
50
|
+
|
|
51
|
+
info["types"].add(func_name)
|
|
52
|
+
|
|
53
|
+
if func_name not in info["call_ids"]:
|
|
54
|
+
info["call_ids"][func_name] = []
|
|
55
|
+
info["call_ids"][func_name].append(call_id)
|
|
56
|
+
|
|
57
|
+
ax_key_to_info[ax_key] = info
|
|
58
|
+
|
|
59
|
+
# Map ax_keys to current axes positions using position matching
|
|
60
|
+
# This handles the case where panels have been dragged to new positions
|
|
61
|
+
ax_keys_sorted = sorted(record.axes.keys())
|
|
62
|
+
|
|
63
|
+
# Debug: Check which ax_keys have position_override
|
|
64
|
+
overrides = {
|
|
65
|
+
k: getattr(record.axes[k], "position_override", None)
|
|
66
|
+
for k in ax_keys_sorted
|
|
67
|
+
if hasattr(record.axes[k], "position_override")
|
|
68
|
+
and record.axes[k].position_override
|
|
69
|
+
}
|
|
70
|
+
if overrides:
|
|
71
|
+
print(f"[detect_plot_types] Position overrides: {overrides}")
|
|
72
|
+
|
|
73
|
+
for ax_idx, ax in enumerate(axes_list):
|
|
74
|
+
# Try to find the matching ax_record by comparing positions
|
|
75
|
+
# or fall back to index-based matching
|
|
76
|
+
matched = False
|
|
77
|
+
ax_pos = ax.get_position()
|
|
78
|
+
|
|
79
|
+
for ax_key in ax_keys_sorted:
|
|
80
|
+
ax_record = record.axes[ax_key]
|
|
81
|
+
# Check if there's a position_override that matches
|
|
82
|
+
# Must check ALL 4 coordinates to avoid false matches
|
|
83
|
+
if (
|
|
84
|
+
hasattr(ax_record, "position_override")
|
|
85
|
+
and ax_record.position_override
|
|
86
|
+
):
|
|
87
|
+
rec_pos = ax_record.position_override
|
|
88
|
+
# Position override is [x0, y0, width, height]
|
|
89
|
+
if len(rec_pos) >= 4:
|
|
90
|
+
if (
|
|
91
|
+
abs(rec_pos[0] - ax_pos.x0) < 0.01
|
|
92
|
+
and abs(rec_pos[1] - ax_pos.y0) < 0.01
|
|
93
|
+
and abs(rec_pos[2] - ax_pos.width) < 0.01
|
|
94
|
+
and abs(rec_pos[3] - ax_pos.height) < 0.01
|
|
95
|
+
):
|
|
96
|
+
print(
|
|
97
|
+
f"[detect_plot_types] ax_idx={ax_idx} matched {ax_key} "
|
|
98
|
+
f"via position_override"
|
|
99
|
+
)
|
|
100
|
+
result[ax_idx] = ax_key_to_info.get(
|
|
101
|
+
ax_key, {"types": set(), "call_ids": {}}
|
|
102
|
+
)
|
|
103
|
+
matched = True
|
|
104
|
+
break
|
|
105
|
+
else:
|
|
106
|
+
# Fallback for old format with only x0, y0
|
|
107
|
+
if (
|
|
108
|
+
abs(rec_pos[0] - ax_pos.x0) < 0.01
|
|
109
|
+
and abs(rec_pos[1] - ax_pos.y0) < 0.01
|
|
110
|
+
):
|
|
111
|
+
result[ax_idx] = ax_key_to_info.get(
|
|
112
|
+
ax_key, {"types": set(), "call_ids": {}}
|
|
113
|
+
)
|
|
114
|
+
matched = True
|
|
115
|
+
break
|
|
116
|
+
|
|
117
|
+
# Fall back to index-based matching if position match failed
|
|
118
|
+
if not matched and ax_idx < len(ax_keys_sorted):
|
|
119
|
+
ax_key = ax_keys_sorted[ax_idx]
|
|
120
|
+
info = ax_key_to_info.get(ax_key, {"types": set(), "call_ids": {}})
|
|
121
|
+
print(
|
|
122
|
+
f"[detect_plot_types] ax_idx={ax_idx} fallback to {ax_key}, "
|
|
123
|
+
f"types={info.get('types', set())}"
|
|
124
|
+
)
|
|
125
|
+
result[ax_idx] = info
|
|
126
|
+
|
|
127
|
+
return result
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def is_boxplot_element(line, ax) -> bool:
|
|
131
|
+
"""Check if a line element belongs to a boxplot.
|
|
132
|
+
|
|
133
|
+
Parameters
|
|
134
|
+
----------
|
|
135
|
+
line : Line2D
|
|
136
|
+
The line to check.
|
|
137
|
+
ax : Axes
|
|
138
|
+
The axes containing the line.
|
|
139
|
+
|
|
140
|
+
Returns
|
|
141
|
+
-------
|
|
142
|
+
bool
|
|
143
|
+
True if line is a boxplot element.
|
|
144
|
+
"""
|
|
145
|
+
label = line.get_label() or ""
|
|
146
|
+
|
|
147
|
+
# Boxplot whisker/median lines have specific patterns
|
|
148
|
+
if label.startswith("_line"):
|
|
149
|
+
return True
|
|
150
|
+
|
|
151
|
+
# Check if line is horizontal (median) or vertical (whisker)
|
|
152
|
+
xdata = line.get_xdata()
|
|
153
|
+
ydata = line.get_ydata()
|
|
154
|
+
|
|
155
|
+
if len(xdata) == 2 and len(ydata) == 2:
|
|
156
|
+
# Horizontal or vertical line segments
|
|
157
|
+
is_horizontal = ydata[0] == ydata[1]
|
|
158
|
+
is_vertical = xdata[0] == xdata[1]
|
|
159
|
+
if is_horizontal or is_vertical:
|
|
160
|
+
return True
|
|
161
|
+
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def is_violin_element(coll, ax) -> bool:
|
|
166
|
+
"""Check if a collection element belongs to a violin plot.
|
|
167
|
+
|
|
168
|
+
Parameters
|
|
169
|
+
----------
|
|
170
|
+
coll : Collection
|
|
171
|
+
The collection to check.
|
|
172
|
+
ax : Axes
|
|
173
|
+
The axes containing the collection.
|
|
174
|
+
|
|
175
|
+
Returns
|
|
176
|
+
-------
|
|
177
|
+
bool
|
|
178
|
+
True if collection is a violin element.
|
|
179
|
+
"""
|
|
180
|
+
from matplotlib.collections import PolyCollection
|
|
181
|
+
|
|
182
|
+
if isinstance(coll, PolyCollection):
|
|
183
|
+
# Violin bodies are PolyCollections
|
|
184
|
+
return True
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
__all__ = [
|
|
189
|
+
"detect_plot_types",
|
|
190
|
+
"is_boxplot_element",
|
|
191
|
+
"is_violin_element",
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
# EOF
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Property restoration for hitmap generation."""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List
|
|
6
|
+
|
|
7
|
+
from matplotlib.collections import (
|
|
8
|
+
LineCollection,
|
|
9
|
+
PathCollection,
|
|
10
|
+
PolyCollection,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def restore_axes_properties(
|
|
15
|
+
axes_list: List,
|
|
16
|
+
original_props: Dict[str, Any],
|
|
17
|
+
include_text: bool = True,
|
|
18
|
+
) -> None:
|
|
19
|
+
"""Restore original properties to axes elements.
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
axes_list : list
|
|
24
|
+
List of axes to restore.
|
|
25
|
+
original_props : dict
|
|
26
|
+
Dictionary of original properties keyed by element key.
|
|
27
|
+
include_text : bool
|
|
28
|
+
Whether text elements were modified.
|
|
29
|
+
"""
|
|
30
|
+
for ax_idx, ax in enumerate(axes_list):
|
|
31
|
+
# Restore lines
|
|
32
|
+
for i, line in enumerate(ax.get_lines()):
|
|
33
|
+
key = f"ax{ax_idx}_line{i}"
|
|
34
|
+
if key in original_props:
|
|
35
|
+
props = original_props[key]
|
|
36
|
+
line.set_color(props["color"])
|
|
37
|
+
line.set_markerfacecolor(props["markerfacecolor"])
|
|
38
|
+
line.set_markeredgecolor(props["markeredgecolor"])
|
|
39
|
+
|
|
40
|
+
# Restore collections
|
|
41
|
+
for i, coll in enumerate(ax.collections):
|
|
42
|
+
if isinstance(coll, PathCollection):
|
|
43
|
+
key = f"ax{ax_idx}_scatter{i}"
|
|
44
|
+
if key in original_props:
|
|
45
|
+
props = original_props[key]
|
|
46
|
+
coll.set_facecolors(props["facecolors"])
|
|
47
|
+
coll.set_edgecolors(props["edgecolors"])
|
|
48
|
+
elif isinstance(coll, PolyCollection):
|
|
49
|
+
key = f"ax{ax_idx}_fill{i}"
|
|
50
|
+
if key in original_props:
|
|
51
|
+
props = original_props[key]
|
|
52
|
+
coll.set_facecolors(props["facecolors"])
|
|
53
|
+
coll.set_edgecolors(props["edgecolors"])
|
|
54
|
+
elif isinstance(coll, LineCollection):
|
|
55
|
+
key = f"ax{ax_idx}_linecoll{i}"
|
|
56
|
+
if key in original_props:
|
|
57
|
+
props = original_props[key]
|
|
58
|
+
if len(props["colors"]) > 0:
|
|
59
|
+
coll.set_color(props["colors"])
|
|
60
|
+
|
|
61
|
+
# Restore patches
|
|
62
|
+
for i, patch in enumerate(ax.patches):
|
|
63
|
+
key = f"ax{ax_idx}_bar{i}"
|
|
64
|
+
if key in original_props:
|
|
65
|
+
props = original_props[key]
|
|
66
|
+
patch.set_facecolor(props["facecolor"])
|
|
67
|
+
patch.set_edgecolor(props["edgecolor"])
|
|
68
|
+
|
|
69
|
+
# Restore text
|
|
70
|
+
if include_text:
|
|
71
|
+
key = f"ax{ax_idx}_title"
|
|
72
|
+
if key in original_props:
|
|
73
|
+
ax.title.set_color(original_props[key]["color"])
|
|
74
|
+
|
|
75
|
+
key = f"ax{ax_idx}_xlabel"
|
|
76
|
+
if key in original_props:
|
|
77
|
+
ax.xaxis.label.set_color(original_props[key]["color"])
|
|
78
|
+
|
|
79
|
+
key = f"ax{ax_idx}_ylabel"
|
|
80
|
+
if key in original_props:
|
|
81
|
+
ax.yaxis.label.set_color(original_props[key]["color"])
|
|
82
|
+
|
|
83
|
+
# Restore legend
|
|
84
|
+
key = f"ax{ax_idx}_legend"
|
|
85
|
+
if key in original_props:
|
|
86
|
+
legend = ax.get_legend()
|
|
87
|
+
if legend:
|
|
88
|
+
frame = legend.get_frame()
|
|
89
|
+
props = original_props[key]
|
|
90
|
+
frame.set_facecolor(props["facecolor"])
|
|
91
|
+
frame.set_edgecolor(props["edgecolor"])
|
|
92
|
+
|
|
93
|
+
# Restore spines
|
|
94
|
+
for spine in ax.spines.values():
|
|
95
|
+
spine.set_color("black")
|
|
96
|
+
|
|
97
|
+
# Restore tick colors
|
|
98
|
+
ax.tick_params(colors="black")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def restore_figure_text(
|
|
102
|
+
mpl_fig,
|
|
103
|
+
original_props: Dict[str, Any],
|
|
104
|
+
include_text: bool = True,
|
|
105
|
+
) -> None:
|
|
106
|
+
"""Restore figure-level text properties.
|
|
107
|
+
|
|
108
|
+
Parameters
|
|
109
|
+
----------
|
|
110
|
+
mpl_fig : Figure
|
|
111
|
+
The matplotlib figure.
|
|
112
|
+
original_props : dict
|
|
113
|
+
Dictionary of original properties.
|
|
114
|
+
include_text : bool
|
|
115
|
+
Whether text elements were modified.
|
|
116
|
+
"""
|
|
117
|
+
if not include_text:
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
key = "fig_suptitle"
|
|
121
|
+
if key in original_props and hasattr(mpl_fig, "_suptitle") and mpl_fig._suptitle:
|
|
122
|
+
mpl_fig._suptitle.set_color(original_props[key]["color"])
|
|
123
|
+
|
|
124
|
+
key = "fig_supxlabel"
|
|
125
|
+
if key in original_props and hasattr(mpl_fig, "_supxlabel") and mpl_fig._supxlabel:
|
|
126
|
+
mpl_fig._supxlabel.set_color(original_props[key]["color"])
|
|
127
|
+
|
|
128
|
+
key = "fig_supylabel"
|
|
129
|
+
if key in original_props and hasattr(mpl_fig, "_supylabel") and mpl_fig._supylabel:
|
|
130
|
+
mpl_fig._supylabel.set_color(original_props[key]["color"])
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def restore_backgrounds(fig, axes_list: List) -> None:
|
|
134
|
+
"""Restore background colors.
|
|
135
|
+
|
|
136
|
+
Parameters
|
|
137
|
+
----------
|
|
138
|
+
fig : Figure
|
|
139
|
+
The figure.
|
|
140
|
+
axes_list : list
|
|
141
|
+
List of axes.
|
|
142
|
+
"""
|
|
143
|
+
fig.patch.set_facecolor("white")
|
|
144
|
+
for ax in axes_list:
|
|
145
|
+
ax.set_facecolor("white")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
__all__ = [
|
|
149
|
+
"restore_axes_properties",
|
|
150
|
+
"restore_figure_text",
|
|
151
|
+
"restore_backgrounds",
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
# EOF
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Hitmap generation for interactive element selection.
|
|
5
|
+
|
|
6
|
+
This module generates color-coded images where each figure element
|
|
7
|
+
(line, scatter, bar, text, etc.) is rendered with a unique RGB color.
|
|
8
|
+
This enables precise pixel-based element detection when users click
|
|
9
|
+
on the figure preview.
|
|
10
|
+
|
|
11
|
+
The color encoding uses 24-bit RGB:
|
|
12
|
+
- First 12 elements: hand-picked visually distinct colors
|
|
13
|
+
- Elements 13+: HSV-based generation for deterministic uniqueness
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import io
|
|
17
|
+
from typing import Any, Dict, Tuple
|
|
18
|
+
|
|
19
|
+
from matplotlib.figure import Figure
|
|
20
|
+
from PIL import Image
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def generate_hitmap(
|
|
24
|
+
fig: Figure,
|
|
25
|
+
dpi: int = 150,
|
|
26
|
+
include_text: bool = True,
|
|
27
|
+
) -> Tuple[Image.Image, Dict[str, Any]]:
|
|
28
|
+
"""
|
|
29
|
+
Generate hitmap with unique colors per element.
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
fig : matplotlib.figure.Figure
|
|
34
|
+
Figure to generate hitmap for.
|
|
35
|
+
dpi : int, optional
|
|
36
|
+
Resolution for hitmap rendering (default: 150).
|
|
37
|
+
include_text : bool, optional
|
|
38
|
+
Whether to include text elements like labels (default: True).
|
|
39
|
+
|
|
40
|
+
Returns
|
|
41
|
+
-------
|
|
42
|
+
hitmap : PIL.Image.Image
|
|
43
|
+
RGB image where each element has unique color.
|
|
44
|
+
color_map : dict
|
|
45
|
+
Mapping from element key to metadata:
|
|
46
|
+
{
|
|
47
|
+
'element_key': {
|
|
48
|
+
'id': int,
|
|
49
|
+
'type': str, # 'line', 'scatter', 'bar', 'boxplot', 'violin', etc.
|
|
50
|
+
'label': str,
|
|
51
|
+
'ax_index': int,
|
|
52
|
+
'rgb': [r, g, b],
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
"""
|
|
56
|
+
# Import from helper modules (inside function to avoid circular imports)
|
|
57
|
+
from ._hitmap._artists import (
|
|
58
|
+
process_collections,
|
|
59
|
+
process_figure_text,
|
|
60
|
+
process_images,
|
|
61
|
+
process_legend,
|
|
62
|
+
process_lines,
|
|
63
|
+
process_patches,
|
|
64
|
+
process_text,
|
|
65
|
+
)
|
|
66
|
+
from ._hitmap._colors import (
|
|
67
|
+
AXES_COLOR,
|
|
68
|
+
BACKGROUND_COLOR,
|
|
69
|
+
normalize_color,
|
|
70
|
+
)
|
|
71
|
+
from ._hitmap._detect import detect_plot_types
|
|
72
|
+
from ._hitmap._restore import (
|
|
73
|
+
restore_axes_properties,
|
|
74
|
+
restore_backgrounds,
|
|
75
|
+
restore_figure_text,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Store original properties for restoration
|
|
79
|
+
original_props = {}
|
|
80
|
+
color_map = {}
|
|
81
|
+
element_id = 1
|
|
82
|
+
|
|
83
|
+
# Detect plot types from record
|
|
84
|
+
plot_types = detect_plot_types(fig, debug=False)
|
|
85
|
+
|
|
86
|
+
# Get all axes (handle RecordingFigure wrapper)
|
|
87
|
+
if hasattr(fig, "fig"):
|
|
88
|
+
mpl_fig = fig.fig
|
|
89
|
+
else:
|
|
90
|
+
mpl_fig = fig
|
|
91
|
+
axes_list = mpl_fig.get_axes()
|
|
92
|
+
|
|
93
|
+
# Process all artists and assign colors
|
|
94
|
+
for ax_idx, ax in enumerate(axes_list):
|
|
95
|
+
ax_info = plot_types.get(ax_idx, {"types": set(), "call_ids": {}})
|
|
96
|
+
|
|
97
|
+
# Process lines
|
|
98
|
+
element_id = process_lines(
|
|
99
|
+
ax, ax_idx, element_id, original_props, color_map, ax_info
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Process collections (scatter, fills, etc.)
|
|
103
|
+
element_id = process_collections(
|
|
104
|
+
ax, ax_idx, element_id, original_props, color_map, ax_info
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Process patches (bars, wedges, polygons)
|
|
108
|
+
element_id = process_patches(
|
|
109
|
+
ax, ax_idx, element_id, original_props, color_map, ax_info
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Process images
|
|
113
|
+
element_id = process_images(ax, ax_idx, element_id, color_map, ax_info)
|
|
114
|
+
|
|
115
|
+
# Process text elements
|
|
116
|
+
if include_text:
|
|
117
|
+
element_id = process_text(ax, ax_idx, element_id, original_props, color_map)
|
|
118
|
+
|
|
119
|
+
# Process legend
|
|
120
|
+
element_id = process_legend(ax, ax_idx, element_id, original_props, color_map)
|
|
121
|
+
|
|
122
|
+
# Process figure-level text elements
|
|
123
|
+
if include_text:
|
|
124
|
+
element_id = process_figure_text(mpl_fig, element_id, original_props, color_map)
|
|
125
|
+
|
|
126
|
+
# Set non-selectable elements to axes color
|
|
127
|
+
for ax in axes_list:
|
|
128
|
+
for spine in ax.spines.values():
|
|
129
|
+
spine.set_color(normalize_color(AXES_COLOR))
|
|
130
|
+
ax.tick_params(colors=normalize_color(AXES_COLOR))
|
|
131
|
+
|
|
132
|
+
# Set figure background
|
|
133
|
+
fig.patch.set_facecolor(normalize_color(BACKGROUND_COLOR))
|
|
134
|
+
for ax in axes_list:
|
|
135
|
+
ax.set_facecolor(normalize_color(BACKGROUND_COLOR))
|
|
136
|
+
|
|
137
|
+
# Render to buffer
|
|
138
|
+
# IMPORTANT: Do NOT use bbox_inches="tight" - it causes dimension changes
|
|
139
|
+
# between renders when elements change (e.g., color). Must match main render.
|
|
140
|
+
buf = io.BytesIO()
|
|
141
|
+
fig.savefig(buf, format="png", dpi=dpi, facecolor=fig.get_facecolor())
|
|
142
|
+
buf.seek(0)
|
|
143
|
+
|
|
144
|
+
# Load as PIL Image
|
|
145
|
+
hitmap = Image.open(buf).convert("RGB")
|
|
146
|
+
|
|
147
|
+
# Restore original properties
|
|
148
|
+
restore_axes_properties(axes_list, original_props, include_text)
|
|
149
|
+
restore_figure_text(mpl_fig, original_props, include_text)
|
|
150
|
+
restore_backgrounds(fig, axes_list)
|
|
151
|
+
|
|
152
|
+
return hitmap, color_map
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def hitmap_to_base64(hitmap: Image.Image) -> str:
|
|
156
|
+
"""
|
|
157
|
+
Convert hitmap image to base64 string.
|
|
158
|
+
|
|
159
|
+
Parameters
|
|
160
|
+
----------
|
|
161
|
+
hitmap : PIL.Image.Image
|
|
162
|
+
Hitmap image.
|
|
163
|
+
|
|
164
|
+
Returns
|
|
165
|
+
-------
|
|
166
|
+
str
|
|
167
|
+
Base64-encoded PNG string.
|
|
168
|
+
"""
|
|
169
|
+
import base64
|
|
170
|
+
|
|
171
|
+
buf = io.BytesIO()
|
|
172
|
+
hitmap.save(buf, format="PNG")
|
|
173
|
+
buf.seek(0)
|
|
174
|
+
return base64.b64encode(buf.read()).decode("utf-8")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
__all__ = [
|
|
178
|
+
"generate_hitmap",
|
|
179
|
+
"hitmap_to_base64",
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
# EOF
|
figrecipe/_editor/_overrides.py
CHANGED
|
@@ -58,12 +58,15 @@ class StyleOverrides:
|
|
|
58
58
|
Returns
|
|
59
59
|
-------
|
|
60
60
|
dict
|
|
61
|
-
Merged style dictionary.
|
|
61
|
+
Merged style dictionary including call_overrides.
|
|
62
62
|
"""
|
|
63
63
|
result = {}
|
|
64
64
|
result.update(self.base_style)
|
|
65
65
|
result.update(self.programmatic_style)
|
|
66
66
|
result.update(self.manual_overrides)
|
|
67
|
+
# Include call_overrides for apply_overrides to use
|
|
68
|
+
if self.call_overrides:
|
|
69
|
+
result["call_overrides"] = self.call_overrides
|
|
67
70
|
return result
|
|
68
71
|
|
|
69
72
|
def get_original_style(self) -> Dict[str, Any]:
|