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.
Files changed (177) hide show
  1. figrecipe/__init__.py +106 -973
  2. figrecipe/_api/__init__.py +48 -0
  3. figrecipe/_api/_extract.py +108 -0
  4. figrecipe/_api/_notebook.py +61 -0
  5. figrecipe/_api/_panel.py +46 -0
  6. figrecipe/_api/_save.py +191 -0
  7. figrecipe/_api/_seaborn_proxy.py +34 -0
  8. figrecipe/_api/_style_manager.py +153 -0
  9. figrecipe/_api/_subplots.py +333 -0
  10. figrecipe/_api/_validate.py +82 -0
  11. figrecipe/_dev/__init__.py +2 -93
  12. figrecipe/_dev/_plotters.py +76 -0
  13. figrecipe/_dev/_run_demos.py +56 -0
  14. figrecipe/_dev/demo_plotters/__init__.py +35 -166
  15. figrecipe/_dev/demo_plotters/_categories.py +81 -0
  16. figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
  17. figrecipe/_dev/demo_plotters/_helpers.py +31 -0
  18. figrecipe/_dev/demo_plotters/_registry.py +50 -0
  19. figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
  20. figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
  21. figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
  22. figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
  23. figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
  24. figrecipe/_dev/demo_plotters/{plot_plot.py → line_curve/plot_plot.py} +3 -2
  25. figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
  26. figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
  27. figrecipe/_dev/demo_plotters/{plot_pie.py → special/plot_pie.py} +5 -1
  28. figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
  29. figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
  30. figrecipe/_editor/__init__.py +57 -9
  31. figrecipe/_editor/_bbox/__init__.py +43 -0
  32. figrecipe/_editor/_bbox/_collections.py +177 -0
  33. figrecipe/_editor/_bbox/_elements.py +159 -0
  34. figrecipe/_editor/_bbox/_extract.py +256 -0
  35. figrecipe/_editor/_bbox/_extract_axes.py +370 -0
  36. figrecipe/_editor/_bbox/_extract_text.py +342 -0
  37. figrecipe/_editor/_bbox/_lines.py +173 -0
  38. figrecipe/_editor/_bbox/_transforms.py +146 -0
  39. figrecipe/_editor/_flask_app.py +68 -1039
  40. figrecipe/_editor/_helpers.py +242 -0
  41. figrecipe/_editor/_hitmap/__init__.py +76 -0
  42. figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
  43. figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
  44. figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
  45. figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
  46. figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
  47. figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
  48. figrecipe/_editor/_hitmap/_colors.py +181 -0
  49. figrecipe/_editor/_hitmap/_detect.py +137 -0
  50. figrecipe/_editor/_hitmap/_restore.py +154 -0
  51. figrecipe/_editor/_hitmap_main.py +182 -0
  52. figrecipe/_editor/_preferences.py +135 -0
  53. figrecipe/_editor/_render_overrides.py +480 -0
  54. figrecipe/_editor/_renderer.py +35 -185
  55. figrecipe/_editor/_routes_axis.py +453 -0
  56. figrecipe/_editor/_routes_core.py +284 -0
  57. figrecipe/_editor/_routes_element.py +317 -0
  58. figrecipe/_editor/_routes_style.py +223 -0
  59. figrecipe/_editor/_templates/__init__.py +78 -1
  60. figrecipe/_editor/_templates/_html.py +109 -13
  61. figrecipe/_editor/_templates/_scripts/__init__.py +120 -0
  62. figrecipe/_editor/_templates/_scripts/_api.py +228 -0
  63. figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
  64. figrecipe/_editor/_templates/_scripts/_core.py +436 -0
  65. figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
  66. figrecipe/_editor/_templates/_scripts/_element_editor.py +310 -0
  67. figrecipe/_editor/_templates/_scripts/_files.py +195 -0
  68. figrecipe/_editor/_templates/_scripts/_hitmap.py +509 -0
  69. figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
  70. figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
  71. figrecipe/_editor/_templates/_scripts/_legend_drag.py +265 -0
  72. figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
  73. figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
  74. figrecipe/_editor/_templates/_scripts/_panel_drag.py +334 -0
  75. figrecipe/_editor/_templates/_scripts/_panel_position.py +279 -0
  76. figrecipe/_editor/_templates/_scripts/_selection.py +237 -0
  77. figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
  78. figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
  79. figrecipe/_editor/_templates/_scripts/_zoom.py +179 -0
  80. figrecipe/_editor/_templates/_styles/__init__.py +69 -0
  81. figrecipe/_editor/_templates/_styles/_base.py +64 -0
  82. figrecipe/_editor/_templates/_styles/_buttons.py +206 -0
  83. figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
  84. figrecipe/_editor/_templates/_styles/_controls.py +265 -0
  85. figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
  86. figrecipe/_editor/_templates/_styles/_forms.py +126 -0
  87. figrecipe/_editor/_templates/_styles/_hitmap.py +184 -0
  88. figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
  89. figrecipe/_editor/_templates/_styles/_labels.py +118 -0
  90. figrecipe/_editor/_templates/_styles/_modals.py +98 -0
  91. figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
  92. figrecipe/_editor/_templates/_styles/_preview.py +225 -0
  93. figrecipe/_editor/_templates/_styles/_selection.py +73 -0
  94. figrecipe/_params/_DECORATION_METHODS.py +6 -0
  95. figrecipe/_recorder.py +35 -106
  96. figrecipe/_recorder_utils.py +124 -0
  97. figrecipe/_reproducer/__init__.py +18 -0
  98. figrecipe/_reproducer/_core.py +498 -0
  99. figrecipe/_reproducer/_custom_plots.py +279 -0
  100. figrecipe/_reproducer/_seaborn.py +100 -0
  101. figrecipe/_reproducer/_violin.py +186 -0
  102. figrecipe/_signatures/_kwargs.py +273 -0
  103. figrecipe/_signatures/_loader.py +21 -423
  104. figrecipe/_signatures/_parsing.py +147 -0
  105. figrecipe/_wrappers/_axes.py +119 -910
  106. figrecipe/_wrappers/_axes_helpers.py +136 -0
  107. figrecipe/_wrappers/_axes_plots.py +418 -0
  108. figrecipe/_wrappers/_axes_seaborn.py +157 -0
  109. figrecipe/_wrappers/_figure.py +162 -0
  110. figrecipe/_wrappers/_panel_labels.py +127 -0
  111. figrecipe/_wrappers/_plot_helpers.py +143 -0
  112. figrecipe/_wrappers/_violin_helpers.py +180 -0
  113. figrecipe/styles/__init__.py +8 -6
  114. figrecipe/styles/_dotdict.py +72 -0
  115. figrecipe/styles/_finalize.py +134 -0
  116. figrecipe/styles/_fonts.py +77 -0
  117. figrecipe/styles/_kwargs_converter.py +178 -0
  118. figrecipe/styles/_plot_styles.py +209 -0
  119. figrecipe/styles/_style_applier.py +32 -478
  120. figrecipe/styles/_style_loader.py +16 -192
  121. figrecipe/styles/_themes.py +151 -0
  122. figrecipe/styles/presets/MATPLOTLIB.yaml +2 -1
  123. figrecipe/styles/presets/SCITEX.yaml +29 -24
  124. {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/METADATA +37 -2
  125. figrecipe-0.7.4.dist-info/RECORD +188 -0
  126. figrecipe/_editor/_bbox.py +0 -978
  127. figrecipe/_editor/_hitmap.py +0 -937
  128. figrecipe/_editor/_templates/_scripts.py +0 -2778
  129. figrecipe/_editor/_templates/_styles.py +0 -1326
  130. figrecipe/_reproducer.py +0 -975
  131. figrecipe-0.6.0.dist-info/RECORD +0 -90
  132. /figrecipe/_dev/demo_plotters/{plot_bar.py → bar_categorical/plot_bar.py} +0 -0
  133. /figrecipe/_dev/demo_plotters/{plot_barh.py → bar_categorical/plot_barh.py} +0 -0
  134. /figrecipe/_dev/demo_plotters/{plot_contour.py → contour_surface/plot_contour.py} +0 -0
  135. /figrecipe/_dev/demo_plotters/{plot_contourf.py → contour_surface/plot_contourf.py} +0 -0
  136. /figrecipe/_dev/demo_plotters/{plot_tricontour.py → contour_surface/plot_tricontour.py} +0 -0
  137. /figrecipe/_dev/demo_plotters/{plot_tricontourf.py → contour_surface/plot_tricontourf.py} +0 -0
  138. /figrecipe/_dev/demo_plotters/{plot_tripcolor.py → contour_surface/plot_tripcolor.py} +0 -0
  139. /figrecipe/_dev/demo_plotters/{plot_triplot.py → contour_surface/plot_triplot.py} +0 -0
  140. /figrecipe/_dev/demo_plotters/{plot_boxplot.py → distribution/plot_boxplot.py} +0 -0
  141. /figrecipe/_dev/demo_plotters/{plot_ecdf.py → distribution/plot_ecdf.py} +0 -0
  142. /figrecipe/_dev/demo_plotters/{plot_hist.py → distribution/plot_hist.py} +0 -0
  143. /figrecipe/_dev/demo_plotters/{plot_hist2d.py → distribution/plot_hist2d.py} +0 -0
  144. /figrecipe/_dev/demo_plotters/{plot_violinplot.py → distribution/plot_violinplot.py} +0 -0
  145. /figrecipe/_dev/demo_plotters/{plot_hexbin.py → image_matrix/plot_hexbin.py} +0 -0
  146. /figrecipe/_dev/demo_plotters/{plot_imshow.py → image_matrix/plot_imshow.py} +0 -0
  147. /figrecipe/_dev/demo_plotters/{plot_matshow.py → image_matrix/plot_matshow.py} +0 -0
  148. /figrecipe/_dev/demo_plotters/{plot_pcolor.py → image_matrix/plot_pcolor.py} +0 -0
  149. /figrecipe/_dev/demo_plotters/{plot_pcolormesh.py → image_matrix/plot_pcolormesh.py} +0 -0
  150. /figrecipe/_dev/demo_plotters/{plot_spy.py → image_matrix/plot_spy.py} +0 -0
  151. /figrecipe/_dev/demo_plotters/{plot_errorbar.py → line_curve/plot_errorbar.py} +0 -0
  152. /figrecipe/_dev/demo_plotters/{plot_fill.py → line_curve/plot_fill.py} +0 -0
  153. /figrecipe/_dev/demo_plotters/{plot_fill_between.py → line_curve/plot_fill_between.py} +0 -0
  154. /figrecipe/_dev/demo_plotters/{plot_fill_betweenx.py → line_curve/plot_fill_betweenx.py} +0 -0
  155. /figrecipe/_dev/demo_plotters/{plot_stackplot.py → line_curve/plot_stackplot.py} +0 -0
  156. /figrecipe/_dev/demo_plotters/{plot_stairs.py → line_curve/plot_stairs.py} +0 -0
  157. /figrecipe/_dev/demo_plotters/{plot_step.py → line_curve/plot_step.py} +0 -0
  158. /figrecipe/_dev/demo_plotters/{plot_scatter.py → scatter_points/plot_scatter.py} +0 -0
  159. /figrecipe/_dev/demo_plotters/{plot_eventplot.py → special/plot_eventplot.py} +0 -0
  160. /figrecipe/_dev/demo_plotters/{plot_loglog.py → special/plot_loglog.py} +0 -0
  161. /figrecipe/_dev/demo_plotters/{plot_semilogx.py → special/plot_semilogx.py} +0 -0
  162. /figrecipe/_dev/demo_plotters/{plot_semilogy.py → special/plot_semilogy.py} +0 -0
  163. /figrecipe/_dev/demo_plotters/{plot_stem.py → special/plot_stem.py} +0 -0
  164. /figrecipe/_dev/demo_plotters/{plot_acorr.py → spectral_signal/plot_acorr.py} +0 -0
  165. /figrecipe/_dev/demo_plotters/{plot_angle_spectrum.py → spectral_signal/plot_angle_spectrum.py} +0 -0
  166. /figrecipe/_dev/demo_plotters/{plot_cohere.py → spectral_signal/plot_cohere.py} +0 -0
  167. /figrecipe/_dev/demo_plotters/{plot_csd.py → spectral_signal/plot_csd.py} +0 -0
  168. /figrecipe/_dev/demo_plotters/{plot_magnitude_spectrum.py → spectral_signal/plot_magnitude_spectrum.py} +0 -0
  169. /figrecipe/_dev/demo_plotters/{plot_phase_spectrum.py → spectral_signal/plot_phase_spectrum.py} +0 -0
  170. /figrecipe/_dev/demo_plotters/{plot_psd.py → spectral_signal/plot_psd.py} +0 -0
  171. /figrecipe/_dev/demo_plotters/{plot_specgram.py → spectral_signal/plot_specgram.py} +0 -0
  172. /figrecipe/_dev/demo_plotters/{plot_xcorr.py → spectral_signal/plot_xcorr.py} +0 -0
  173. /figrecipe/_dev/demo_plotters/{plot_barbs.py → vector_flow/plot_barbs.py} +0 -0
  174. /figrecipe/_dev/demo_plotters/{plot_quiver.py → vector_flow/plot_quiver.py} +0 -0
  175. /figrecipe/_dev/demo_plotters/{plot_streamplot.py → vector_flow/plot_streamplot.py} +0 -0
  176. {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/WHEEL +0 -0
  177. {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