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,137 @@
|
|
|
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) -> 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
|
+
|
|
16
|
+
Returns
|
|
17
|
+
-------
|
|
18
|
+
dict
|
|
19
|
+
Mapping from ax_index to plot type info.
|
|
20
|
+
"""
|
|
21
|
+
# Get figure record if available
|
|
22
|
+
if hasattr(fig, "record"):
|
|
23
|
+
record = fig.record
|
|
24
|
+
elif hasattr(fig, "fig") and hasattr(fig.fig, "_record"):
|
|
25
|
+
record = fig.fig._record
|
|
26
|
+
else:
|
|
27
|
+
return {}
|
|
28
|
+
|
|
29
|
+
result = {}
|
|
30
|
+
|
|
31
|
+
# Process each axes in the record
|
|
32
|
+
if hasattr(record, "axes"):
|
|
33
|
+
# First pass: collect all ax_keys to determine grid dimensions
|
|
34
|
+
ax_keys = list(record.axes.keys())
|
|
35
|
+
max_row, max_col = 0, 0
|
|
36
|
+
for ax_key in ax_keys:
|
|
37
|
+
try:
|
|
38
|
+
parts = ax_key.split("_")
|
|
39
|
+
row, col = int(parts[1]), int(parts[2])
|
|
40
|
+
max_row = max(max_row, row)
|
|
41
|
+
max_col = max(max_col, col)
|
|
42
|
+
except (ValueError, IndexError):
|
|
43
|
+
pass
|
|
44
|
+
ncols = max_col + 1
|
|
45
|
+
|
|
46
|
+
for ax_key, ax_record in record.axes.items():
|
|
47
|
+
# Extract ax_index from key (e.g., "ax_0_0" -> 0, "ax_2_2" -> 8 for 3x3)
|
|
48
|
+
try:
|
|
49
|
+
parts = ax_key.split("_")
|
|
50
|
+
row, col = int(parts[1]), int(parts[2])
|
|
51
|
+
ax_idx = row * ncols + col
|
|
52
|
+
except (ValueError, IndexError):
|
|
53
|
+
ax_idx = 0
|
|
54
|
+
|
|
55
|
+
if ax_idx not in result:
|
|
56
|
+
result[ax_idx] = {"types": set(), "call_ids": {}}
|
|
57
|
+
|
|
58
|
+
# Collect all call types and their IDs
|
|
59
|
+
if hasattr(ax_record, "calls"):
|
|
60
|
+
for call in ax_record.calls:
|
|
61
|
+
func_name = call.function
|
|
62
|
+
call_id = call.id
|
|
63
|
+
|
|
64
|
+
result[ax_idx]["types"].add(func_name)
|
|
65
|
+
|
|
66
|
+
if func_name not in result[ax_idx]["call_ids"]:
|
|
67
|
+
result[ax_idx]["call_ids"][func_name] = []
|
|
68
|
+
result[ax_idx]["call_ids"][func_name].append(call_id)
|
|
69
|
+
|
|
70
|
+
return result
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def is_boxplot_element(line, ax) -> bool:
|
|
74
|
+
"""Check if a line element belongs to a boxplot.
|
|
75
|
+
|
|
76
|
+
Parameters
|
|
77
|
+
----------
|
|
78
|
+
line : Line2D
|
|
79
|
+
The line to check.
|
|
80
|
+
ax : Axes
|
|
81
|
+
The axes containing the line.
|
|
82
|
+
|
|
83
|
+
Returns
|
|
84
|
+
-------
|
|
85
|
+
bool
|
|
86
|
+
True if line is a boxplot element.
|
|
87
|
+
"""
|
|
88
|
+
label = line.get_label() or ""
|
|
89
|
+
|
|
90
|
+
# Boxplot whisker/median lines have specific patterns
|
|
91
|
+
if label.startswith("_line"):
|
|
92
|
+
return True
|
|
93
|
+
|
|
94
|
+
# Check if line is horizontal (median) or vertical (whisker)
|
|
95
|
+
xdata = line.get_xdata()
|
|
96
|
+
ydata = line.get_ydata()
|
|
97
|
+
|
|
98
|
+
if len(xdata) == 2 and len(ydata) == 2:
|
|
99
|
+
# Horizontal or vertical line segments
|
|
100
|
+
is_horizontal = ydata[0] == ydata[1]
|
|
101
|
+
is_vertical = xdata[0] == xdata[1]
|
|
102
|
+
if is_horizontal or is_vertical:
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def is_violin_element(coll, ax) -> bool:
|
|
109
|
+
"""Check if a collection element belongs to a violin plot.
|
|
110
|
+
|
|
111
|
+
Parameters
|
|
112
|
+
----------
|
|
113
|
+
coll : Collection
|
|
114
|
+
The collection to check.
|
|
115
|
+
ax : Axes
|
|
116
|
+
The axes containing the collection.
|
|
117
|
+
|
|
118
|
+
Returns
|
|
119
|
+
-------
|
|
120
|
+
bool
|
|
121
|
+
True if collection is a violin element.
|
|
122
|
+
"""
|
|
123
|
+
from matplotlib.collections import PolyCollection
|
|
124
|
+
|
|
125
|
+
if isinstance(coll, PolyCollection):
|
|
126
|
+
# Violin bodies are PolyCollections
|
|
127
|
+
return True
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
__all__ = [
|
|
132
|
+
"detect_plot_types",
|
|
133
|
+
"is_boxplot_element",
|
|
134
|
+
"is_violin_element",
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
# 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)
|
|
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
|
+
buf = io.BytesIO()
|
|
139
|
+
fig.savefig(
|
|
140
|
+
buf, format="png", dpi=dpi, facecolor=fig.get_facecolor(), bbox_inches="tight"
|
|
141
|
+
)
|
|
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
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""User preferences management for the figure editor.
|
|
4
|
+
|
|
5
|
+
Preferences are stored in ~/.figrecipe/preferences.json and persist
|
|
6
|
+
across sessions. This allows users to set defaults like dark mode.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict
|
|
12
|
+
|
|
13
|
+
# Default preferences
|
|
14
|
+
DEFAULT_PREFERENCES = {
|
|
15
|
+
"dark_mode": False,
|
|
16
|
+
"default_style": "SCITEX",
|
|
17
|
+
"auto_save": True,
|
|
18
|
+
"show_hit_regions": False,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
# Preferences file location
|
|
22
|
+
PREFERENCES_DIR = Path.home() / ".figrecipe"
|
|
23
|
+
PREFERENCES_FILE = PREFERENCES_DIR / "preferences.json"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_preferences_path() -> Path:
|
|
27
|
+
"""Get the path to the preferences file."""
|
|
28
|
+
return PREFERENCES_FILE
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def load_preferences() -> Dict[str, Any]:
|
|
32
|
+
"""Load user preferences from disk.
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
dict
|
|
37
|
+
User preferences merged with defaults.
|
|
38
|
+
"""
|
|
39
|
+
prefs = DEFAULT_PREFERENCES.copy()
|
|
40
|
+
|
|
41
|
+
if PREFERENCES_FILE.exists():
|
|
42
|
+
try:
|
|
43
|
+
with open(PREFERENCES_FILE, "r") as f:
|
|
44
|
+
saved = json.load(f)
|
|
45
|
+
prefs.update(saved)
|
|
46
|
+
except (json.JSONDecodeError, IOError):
|
|
47
|
+
# If file is corrupted, use defaults
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
return prefs
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def save_preferences(prefs: Dict[str, Any]) -> bool:
|
|
54
|
+
"""Save user preferences to disk.
|
|
55
|
+
|
|
56
|
+
Parameters
|
|
57
|
+
----------
|
|
58
|
+
prefs : dict
|
|
59
|
+
Preferences to save.
|
|
60
|
+
|
|
61
|
+
Returns
|
|
62
|
+
-------
|
|
63
|
+
bool
|
|
64
|
+
True if save was successful.
|
|
65
|
+
"""
|
|
66
|
+
try:
|
|
67
|
+
PREFERENCES_DIR.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
with open(PREFERENCES_FILE, "w") as f:
|
|
69
|
+
json.dump(prefs, f, indent=2)
|
|
70
|
+
return True
|
|
71
|
+
except IOError:
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def get_preference(key: str, default: Any = None) -> Any:
|
|
76
|
+
"""Get a single preference value.
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
key : str
|
|
81
|
+
Preference key.
|
|
82
|
+
default : Any, optional
|
|
83
|
+
Default value if key not found.
|
|
84
|
+
|
|
85
|
+
Returns
|
|
86
|
+
-------
|
|
87
|
+
Any
|
|
88
|
+
Preference value.
|
|
89
|
+
"""
|
|
90
|
+
prefs = load_preferences()
|
|
91
|
+
return prefs.get(key, default)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def set_preference(key: str, value: Any) -> bool:
|
|
95
|
+
"""Set a single preference value.
|
|
96
|
+
|
|
97
|
+
Parameters
|
|
98
|
+
----------
|
|
99
|
+
key : str
|
|
100
|
+
Preference key.
|
|
101
|
+
value : Any
|
|
102
|
+
Value to set.
|
|
103
|
+
|
|
104
|
+
Returns
|
|
105
|
+
-------
|
|
106
|
+
bool
|
|
107
|
+
True if save was successful.
|
|
108
|
+
"""
|
|
109
|
+
prefs = load_preferences()
|
|
110
|
+
prefs[key] = value
|
|
111
|
+
return save_preferences(prefs)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def reset_preferences() -> bool:
|
|
115
|
+
"""Reset all preferences to defaults.
|
|
116
|
+
|
|
117
|
+
Returns
|
|
118
|
+
-------
|
|
119
|
+
bool
|
|
120
|
+
True if reset was successful.
|
|
121
|
+
"""
|
|
122
|
+
return save_preferences(DEFAULT_PREFERENCES.copy())
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
__all__ = [
|
|
126
|
+
"DEFAULT_PREFERENCES",
|
|
127
|
+
"get_preferences_path",
|
|
128
|
+
"load_preferences",
|
|
129
|
+
"save_preferences",
|
|
130
|
+
"get_preference",
|
|
131
|
+
"set_preference",
|
|
132
|
+
"reset_preferences",
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
# EOF
|