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,68 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Image processing for hitmap generation."""
4
+
5
+ from typing import Any, Dict
6
+
7
+ from matplotlib.image import AxesImage
8
+
9
+ from .._colors import id_to_rgb
10
+
11
+
12
+ def process_images(
13
+ ax,
14
+ ax_idx: int,
15
+ element_id: int,
16
+ color_map: Dict[str, Any],
17
+ ax_info: Dict[str, Any],
18
+ ) -> int:
19
+ """Process images on an axes.
20
+
21
+ Returns updated element_id.
22
+ """
23
+ ax_call_ids = ax_info.get("call_ids", {})
24
+
25
+ imshow_ids = list(ax_call_ids.get("imshow", []))
26
+ specgram_ids = list(ax_call_ids.get("specgram", []))
27
+ contourf_ids = list(ax_call_ids.get("contourf", []))
28
+
29
+ image_idx = 0
30
+
31
+ for i, img in enumerate(ax.images):
32
+ if isinstance(img, AxesImage):
33
+ if not img.get_visible():
34
+ continue
35
+
36
+ key = f"ax{ax_idx}_image{i}"
37
+
38
+ call_id = None
39
+ label = f"image_{i}"
40
+
41
+ if image_idx < len(imshow_ids):
42
+ call_id = imshow_ids[image_idx]
43
+ label = call_id
44
+ elif image_idx < len(specgram_ids):
45
+ call_id = specgram_ids[image_idx]
46
+ label = call_id
47
+ elif image_idx < len(contourf_ids):
48
+ call_id = contourf_ids[image_idx]
49
+ label = call_id
50
+
51
+ image_idx += 1
52
+
53
+ color_map[key] = {
54
+ "id": element_id,
55
+ "type": "image",
56
+ "label": label,
57
+ "ax_index": ax_idx,
58
+ "rgb": list(id_to_rgb(element_id)),
59
+ "call_id": call_id,
60
+ }
61
+ element_id += 1
62
+
63
+ return element_id
64
+
65
+
66
+ __all__ = ["process_images"]
67
+
68
+ # EOF
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Line processing for hitmap generation."""
4
+
5
+ from typing import Any, Dict
6
+
7
+ from .._colors import id_to_rgb, mpl_color_to_hex, normalize_color
8
+ from .._detect import is_boxplot_element
9
+
10
+
11
+ def process_lines(
12
+ ax,
13
+ ax_idx: int,
14
+ element_id: int,
15
+ original_props: Dict[str, Any],
16
+ color_map: Dict[str, Any],
17
+ ax_info: Dict[str, Any],
18
+ ) -> int:
19
+ """Process lines (traces) on an axes.
20
+
21
+ Returns updated element_id.
22
+ """
23
+ ax_plot_types = ax_info.get("types", set())
24
+ ax_call_ids = ax_info.get("call_ids", {})
25
+ has_boxplot = "boxplot" in ax_plot_types
26
+ has_violin = "violinplot" in ax_plot_types
27
+ has_regular_plot = "plot" in ax_plot_types
28
+
29
+ boxplot_ids = list(ax_call_ids.get("boxplot", []))
30
+ violin_ids = list(ax_call_ids.get("violinplot", []))
31
+ plot_ids = list(ax_call_ids.get("plot", []))
32
+
33
+ boxplot_call_id = boxplot_ids[0] if boxplot_ids else None
34
+ violin_call_id = violin_ids[0] if violin_ids else None
35
+ regular_line_idx = 0
36
+ has_record = len(ax_plot_types) > 0
37
+
38
+ for i, line in enumerate(ax.get_lines()):
39
+ if not line.get_visible():
40
+ continue
41
+
42
+ orig_label = line.get_label() or ""
43
+
44
+ if has_record:
45
+ if (
46
+ orig_label.startswith("_child")
47
+ and not has_boxplot
48
+ and not has_violin
49
+ and not has_regular_plot
50
+ ):
51
+ continue
52
+
53
+ xdata = line.get_xdata()
54
+ if len(xdata) == 0:
55
+ continue
56
+
57
+ key = f"ax{ax_idx}_line{i}"
58
+ rgb = id_to_rgb(element_id)
59
+
60
+ original_props[key] = {
61
+ "color": line.get_color(),
62
+ "markerfacecolor": line.get_markerfacecolor(),
63
+ "markeredgecolor": line.get_markeredgecolor(),
64
+ }
65
+
66
+ line.set_color(normalize_color(rgb))
67
+ line.set_markerfacecolor(normalize_color(rgb))
68
+ line.set_markeredgecolor(normalize_color(rgb))
69
+
70
+ call_id = None
71
+ if has_boxplot and (is_boxplot_element(line, ax) or orig_label.startswith("_")):
72
+ elem_type = "boxplot"
73
+ label = boxplot_call_id or "boxplot"
74
+ call_id = boxplot_call_id
75
+ elif has_violin and orig_label.startswith("_"):
76
+ elem_type = "violin"
77
+ label = violin_call_id or "violin"
78
+ call_id = violin_call_id
79
+ else:
80
+ elem_type = "line"
81
+ label = orig_label if orig_label else f"line_{i}"
82
+ if regular_line_idx < len(plot_ids):
83
+ call_id = plot_ids[regular_line_idx]
84
+ label = call_id
85
+ else:
86
+ call_id = f"line_{ax_idx}_{regular_line_idx}"
87
+ if orig_label.startswith("_"):
88
+ label = call_id
89
+ regular_line_idx += 1
90
+
91
+ color_map[key] = {
92
+ "id": element_id,
93
+ "type": elem_type,
94
+ "label": label,
95
+ "ax_index": ax_idx,
96
+ "rgb": list(rgb),
97
+ "original_color": mpl_color_to_hex(original_props[key]["color"]),
98
+ "call_id": call_id,
99
+ }
100
+ element_id += 1
101
+
102
+ return element_id
103
+
104
+
105
+ __all__ = ["process_lines"]
106
+
107
+ # EOF
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Patch processing for hitmap generation."""
4
+
5
+ from typing import Any, Dict
6
+
7
+ from matplotlib.patches import Polygon, Rectangle, Wedge
8
+
9
+ from .._colors import id_to_rgb, mpl_color_to_hex, normalize_color
10
+
11
+
12
+ def process_patches(
13
+ ax,
14
+ ax_idx: int,
15
+ element_id: int,
16
+ original_props: Dict[str, Any],
17
+ color_map: Dict[str, Any],
18
+ ax_info: Dict[str, Any],
19
+ ) -> int:
20
+ """Process patches (bars, wedges, polygons) on an axes.
21
+
22
+ Returns updated element_id.
23
+ """
24
+ ax_plot_types = ax_info.get("types", set())
25
+ ax_call_ids = ax_info.get("call_ids", {})
26
+
27
+ has_hist = "hist" in ax_plot_types
28
+ has_bar = "bar" in ax_plot_types
29
+
30
+ hist_ids = list(ax_call_ids.get("hist", []))
31
+ bar_ids = list(ax_call_ids.get("bar", []))
32
+ pie_ids = list(ax_call_ids.get("pie", []))
33
+
34
+ if has_hist and hist_ids:
35
+ rect_call_id = hist_ids[0]
36
+ rect_type = "hist"
37
+ elif has_bar and bar_ids:
38
+ rect_call_id = bar_ids[0]
39
+ rect_type = "bar"
40
+ else:
41
+ rect_call_id = f"bar_{ax_idx}"
42
+ rect_type = "bar"
43
+
44
+ for i, patch in enumerate(ax.patches):
45
+ if isinstance(patch, Rectangle):
46
+ element_id = _process_rectangle(
47
+ patch,
48
+ i,
49
+ ax_idx,
50
+ element_id,
51
+ original_props,
52
+ color_map,
53
+ rect_call_id,
54
+ rect_type,
55
+ )
56
+ elif isinstance(patch, Wedge):
57
+ element_id = _process_wedge(
58
+ patch, i, ax_idx, element_id, original_props, color_map, pie_ids
59
+ )
60
+ elif isinstance(patch, Polygon):
61
+ element_id = _process_polygon(
62
+ patch, i, ax_idx, element_id, original_props, color_map
63
+ )
64
+
65
+ return element_id
66
+
67
+
68
+ def _process_rectangle(
69
+ patch, i, ax_idx, element_id, original_props, color_map, rect_call_id, rect_type
70
+ ):
71
+ """Process Rectangle patch (bars, histogram bins)."""
72
+ if not patch.get_visible():
73
+ return element_id
74
+ if patch.get_width() == 1.0 and patch.get_height() == 1.0:
75
+ return element_id
76
+
77
+ key = f"ax{ax_idx}_bar{i}"
78
+ rgb = id_to_rgb(element_id)
79
+
80
+ original_props[key] = {
81
+ "facecolor": patch.get_facecolor(),
82
+ "edgecolor": patch.get_edgecolor(),
83
+ }
84
+
85
+ patch.set_facecolor(normalize_color(rgb))
86
+ patch.set_edgecolor(normalize_color(rgb))
87
+
88
+ label = rect_call_id or patch.get_label() or f"bar_{i}"
89
+
90
+ color_map[key] = {
91
+ "id": element_id,
92
+ "type": rect_type,
93
+ "label": label,
94
+ "ax_index": ax_idx,
95
+ "rgb": list(rgb),
96
+ "original_color": mpl_color_to_hex(original_props[key]["facecolor"]),
97
+ "call_id": rect_call_id,
98
+ }
99
+ return element_id + 1
100
+
101
+
102
+ def _process_wedge(patch, i, ax_idx, element_id, original_props, color_map, pie_ids):
103
+ """Process Wedge patch (pie chart slices)."""
104
+ if not patch.get_visible():
105
+ return element_id
106
+
107
+ key = f"ax{ax_idx}_wedge{i}"
108
+ rgb = id_to_rgb(element_id)
109
+
110
+ original_props[key] = {
111
+ "facecolor": patch.get_facecolor(),
112
+ "edgecolor": patch.get_edgecolor(),
113
+ }
114
+
115
+ patch.set_facecolor(normalize_color(rgb))
116
+ patch.set_edgecolor(normalize_color(rgb))
117
+
118
+ call_id = pie_ids[0] if pie_ids else None
119
+ label = call_id or f"wedge_{i}"
120
+
121
+ color_map[key] = {
122
+ "id": element_id,
123
+ "type": "pie",
124
+ "label": label,
125
+ "ax_index": ax_idx,
126
+ "rgb": list(rgb),
127
+ "original_color": mpl_color_to_hex(original_props[key]["facecolor"]),
128
+ "call_id": call_id,
129
+ }
130
+ return element_id + 1
131
+
132
+
133
+ def _process_polygon(patch, i, ax_idx, element_id, original_props, color_map):
134
+ """Process Polygon patch (fill areas)."""
135
+ if not patch.get_visible():
136
+ return element_id
137
+
138
+ key = f"ax{ax_idx}_polygon{i}"
139
+ rgb = id_to_rgb(element_id)
140
+
141
+ original_props[key] = {
142
+ "facecolor": patch.get_facecolor(),
143
+ "edgecolor": patch.get_edgecolor(),
144
+ }
145
+
146
+ patch.set_facecolor(normalize_color(rgb))
147
+ patch.set_edgecolor(normalize_color(rgb))
148
+
149
+ color_map[key] = {
150
+ "id": element_id,
151
+ "type": "fill",
152
+ "label": f"fill_{i}",
153
+ "ax_index": ax_idx,
154
+ "rgb": list(rgb),
155
+ "original_color": mpl_color_to_hex(original_props[key]["facecolor"]),
156
+ "call_id": None,
157
+ }
158
+ return element_id + 1
159
+
160
+
161
+ __all__ = ["process_patches"]
162
+
163
+ # EOF
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Text and legend processing for hitmap generation."""
4
+
5
+ from typing import Any, Dict
6
+
7
+ from .._colors import id_to_rgb, normalize_color
8
+
9
+
10
+ def process_text(
11
+ ax,
12
+ ax_idx: int,
13
+ element_id: int,
14
+ original_props: Dict[str, Any],
15
+ color_map: Dict[str, Any],
16
+ ) -> int:
17
+ """Process text elements (title, labels) on an axes.
18
+
19
+ Returns updated element_id.
20
+ """
21
+ # Title
22
+ title = ax.get_title()
23
+ if title:
24
+ key = f"ax{ax_idx}_title"
25
+ rgb = id_to_rgb(element_id)
26
+ title_obj = ax.title
27
+
28
+ original_props[key] = {"color": title_obj.get_color()}
29
+ title_obj.set_color(normalize_color(rgb))
30
+
31
+ color_map[key] = {
32
+ "id": element_id,
33
+ "type": "title",
34
+ "label": "title",
35
+ "ax_index": ax_idx,
36
+ "rgb": list(rgb),
37
+ }
38
+ element_id += 1
39
+
40
+ # X label
41
+ xlabel = ax.get_xlabel()
42
+ if xlabel:
43
+ key = f"ax{ax_idx}_xlabel"
44
+ rgb = id_to_rgb(element_id)
45
+ xlabel_obj = ax.xaxis.label
46
+
47
+ original_props[key] = {"color": xlabel_obj.get_color()}
48
+ xlabel_obj.set_color(normalize_color(rgb))
49
+
50
+ color_map[key] = {
51
+ "id": element_id,
52
+ "type": "xlabel",
53
+ "label": "xlabel",
54
+ "ax_index": ax_idx,
55
+ "rgb": list(rgb),
56
+ }
57
+ element_id += 1
58
+
59
+ # Y label
60
+ ylabel = ax.get_ylabel()
61
+ if ylabel:
62
+ key = f"ax{ax_idx}_ylabel"
63
+ rgb = id_to_rgb(element_id)
64
+ ylabel_obj = ax.yaxis.label
65
+
66
+ original_props[key] = {"color": ylabel_obj.get_color()}
67
+ ylabel_obj.set_color(normalize_color(rgb))
68
+
69
+ color_map[key] = {
70
+ "id": element_id,
71
+ "type": "ylabel",
72
+ "label": "ylabel",
73
+ "ax_index": ax_idx,
74
+ "rgb": list(rgb),
75
+ }
76
+ element_id += 1
77
+
78
+ return element_id
79
+
80
+
81
+ def process_legend(
82
+ ax,
83
+ ax_idx: int,
84
+ element_id: int,
85
+ original_props: Dict[str, Any],
86
+ color_map: Dict[str, Any],
87
+ ) -> int:
88
+ """Process legend on an axes.
89
+
90
+ Returns updated element_id.
91
+ """
92
+ legend = ax.get_legend()
93
+ if legend is not None and legend.get_visible():
94
+ key = f"ax{ax_idx}_legend"
95
+ rgb = id_to_rgb(element_id)
96
+
97
+ frame = legend.get_frame()
98
+ original_props[key] = {
99
+ "facecolor": frame.get_facecolor(),
100
+ "edgecolor": frame.get_edgecolor(),
101
+ }
102
+
103
+ frame.set_facecolor(normalize_color(rgb))
104
+ frame.set_edgecolor(normalize_color(rgb))
105
+
106
+ color_map[key] = {
107
+ "id": element_id,
108
+ "type": "legend",
109
+ "label": "legend",
110
+ "ax_index": ax_idx,
111
+ "rgb": list(rgb),
112
+ }
113
+ element_id += 1
114
+
115
+ return element_id
116
+
117
+
118
+ def process_figure_text(
119
+ mpl_fig,
120
+ element_id: int,
121
+ original_props: Dict[str, Any],
122
+ color_map: Dict[str, Any],
123
+ ) -> int:
124
+ """Process figure-level text elements (suptitle, supxlabel, supylabel).
125
+
126
+ Returns updated element_id.
127
+ """
128
+ # Suptitle
129
+ if hasattr(mpl_fig, "_suptitle") and mpl_fig._suptitle is not None:
130
+ suptitle_obj = mpl_fig._suptitle
131
+ if suptitle_obj.get_text():
132
+ key = "fig_suptitle"
133
+ rgb = id_to_rgb(element_id)
134
+
135
+ original_props[key] = {"color": suptitle_obj.get_color()}
136
+ suptitle_obj.set_color(normalize_color(rgb))
137
+
138
+ color_map[key] = {
139
+ "id": element_id,
140
+ "type": "suptitle",
141
+ "label": "suptitle",
142
+ "ax_index": -1,
143
+ "rgb": list(rgb),
144
+ }
145
+ element_id += 1
146
+
147
+ # Supxlabel
148
+ if hasattr(mpl_fig, "_supxlabel") and mpl_fig._supxlabel is not None:
149
+ supxlabel_obj = mpl_fig._supxlabel
150
+ if supxlabel_obj.get_text():
151
+ key = "fig_supxlabel"
152
+ rgb = id_to_rgb(element_id)
153
+
154
+ original_props[key] = {"color": supxlabel_obj.get_color()}
155
+ supxlabel_obj.set_color(normalize_color(rgb))
156
+
157
+ color_map[key] = {
158
+ "id": element_id,
159
+ "type": "supxlabel",
160
+ "label": "supxlabel",
161
+ "ax_index": -1,
162
+ "rgb": list(rgb),
163
+ }
164
+ element_id += 1
165
+
166
+ # Supylabel
167
+ if hasattr(mpl_fig, "_supylabel") and mpl_fig._supylabel is not None:
168
+ supylabel_obj = mpl_fig._supylabel
169
+ if supylabel_obj.get_text():
170
+ key = "fig_supylabel"
171
+ rgb = id_to_rgb(element_id)
172
+
173
+ original_props[key] = {"color": supylabel_obj.get_color()}
174
+ supylabel_obj.set_color(normalize_color(rgb))
175
+
176
+ color_map[key] = {
177
+ "id": element_id,
178
+ "type": "supylabel",
179
+ "label": "supylabel",
180
+ "ax_index": -1,
181
+ "rgb": list(rgb),
182
+ }
183
+ element_id += 1
184
+
185
+ return element_id
186
+
187
+
188
+ __all__ = ["process_text", "process_legend", "process_figure_text"]
189
+
190
+ # EOF
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Color utilities for hitmap generation."""
4
+
5
+ from typing import Tuple
6
+
7
+ # Hand-picked distinct colors for first 12 elements (maximum visual distinction)
8
+ DISTINCT_COLORS = [
9
+ (255, 0, 0), # Red
10
+ (0, 200, 0), # Green
11
+ (0, 100, 255), # Blue
12
+ (255, 200, 0), # Yellow
13
+ (255, 0, 255), # Magenta
14
+ (0, 255, 255), # Cyan
15
+ (255, 128, 0), # Orange
16
+ (128, 0, 255), # Purple
17
+ (0, 255, 128), # Spring green
18
+ (255, 0, 128), # Rose
19
+ (128, 255, 0), # Lime
20
+ (0, 128, 255), # Sky blue
21
+ ]
22
+
23
+ # Reserved colors
24
+ BACKGROUND_COLOR = (26, 26, 26) # Dark gray for background
25
+ AXES_COLOR = (64, 64, 64) # Medium gray for non-selectable axes elements
26
+
27
+
28
+ def hsv_to_rgb(h: float, s: float, v: float) -> Tuple[int, int, int]:
29
+ """Convert HSV to RGB (0-255 range).
30
+
31
+ Parameters
32
+ ----------
33
+ h : float
34
+ Hue (0-1).
35
+ s : float
36
+ Saturation (0-1).
37
+ v : float
38
+ Value (0-1).
39
+
40
+ Returns
41
+ -------
42
+ tuple
43
+ RGB tuple (0-255 range).
44
+ """
45
+ if s == 0:
46
+ r = g = b = int(v * 255)
47
+ return (r, g, b)
48
+
49
+ i = int(h * 6)
50
+ f = h * 6 - i
51
+ p = v * (1 - s)
52
+ q = v * (1 - s * f)
53
+ t = v * (1 - s * (1 - f))
54
+
55
+ i %= 6
56
+ if i == 0:
57
+ r, g, b = v, t, p
58
+ elif i == 1:
59
+ r, g, b = q, v, p
60
+ elif i == 2:
61
+ r, g, b = p, v, t
62
+ elif i == 3:
63
+ r, g, b = p, q, v
64
+ elif i == 4:
65
+ r, g, b = t, p, v
66
+ else:
67
+ r, g, b = v, p, q
68
+
69
+ return (int(r * 255), int(g * 255), int(b * 255))
70
+
71
+
72
+ def id_to_rgb(element_id: int) -> Tuple[int, int, int]:
73
+ """Convert element ID to unique RGB color.
74
+
75
+ Parameters
76
+ ----------
77
+ element_id : int
78
+ Unique element identifier (1-based).
79
+
80
+ Returns
81
+ -------
82
+ tuple of int
83
+ RGB color tuple (0-255 range).
84
+
85
+ Notes
86
+ -----
87
+ - ID 0 is reserved for background
88
+ - IDs 1-12 use hand-picked distinct colors
89
+ - IDs 13+ use HSV-based generation
90
+ """
91
+ if element_id <= 0:
92
+ return BACKGROUND_COLOR
93
+
94
+ if element_id <= len(DISTINCT_COLORS):
95
+ return DISTINCT_COLORS[element_id - 1]
96
+
97
+ # HSV-based generation for IDs > 12
98
+ golden_ratio = 0.618033988749895
99
+ hue = ((element_id - len(DISTINCT_COLORS)) * golden_ratio) % 1.0
100
+ saturation = 0.7 + (element_id % 3) * 0.1
101
+ value = 0.75 + (element_id % 4) * 0.0625
102
+
103
+ return hsv_to_rgb(hue, saturation, value)
104
+
105
+
106
+ def rgb_to_id(rgb: Tuple[int, int, int]) -> int:
107
+ """Convert RGB color back to element ID.
108
+
109
+ Parameters
110
+ ----------
111
+ rgb : tuple of int
112
+ RGB color tuple.
113
+
114
+ Returns
115
+ -------
116
+ int
117
+ Element ID, or 0 if background/unknown.
118
+ """
119
+ if rgb == BACKGROUND_COLOR:
120
+ return 0
121
+ if rgb == AXES_COLOR:
122
+ return 0
123
+
124
+ # Check hand-picked colors
125
+ if rgb in DISTINCT_COLORS:
126
+ return DISTINCT_COLORS.index(rgb) + 1
127
+
128
+ # For HSV-generated colors, search
129
+ for test_id in range(len(DISTINCT_COLORS) + 1, 1000):
130
+ if id_to_rgb(test_id) == rgb:
131
+ return test_id
132
+
133
+ return 0
134
+
135
+
136
+ def normalize_color(rgb: Tuple[int, int, int]) -> Tuple[float, float, float]:
137
+ """Normalize RGB from 0-255 to 0-1 range."""
138
+ return (rgb[0] / 255, rgb[1] / 255, rgb[2] / 255)
139
+
140
+
141
+ def mpl_color_to_hex(color) -> str:
142
+ """Convert matplotlib color to hex string.
143
+
144
+ Parameters
145
+ ----------
146
+ color : color
147
+ Matplotlib color (RGBA tuple, hex string, named color).
148
+
149
+ Returns
150
+ -------
151
+ str
152
+ Hex color string (e.g., '#FF0000').
153
+ """
154
+ import matplotlib.colors as mcolors
155
+
156
+ try:
157
+ if hasattr(color, "__iter__") and not isinstance(color, str):
158
+ color = tuple(color)
159
+ if len(color) >= 3:
160
+ if all(isinstance(c, (int, float)) for c in color[:3]):
161
+ if all(c <= 1.0 for c in color[:3]):
162
+ return mcolors.to_hex(color[:3])
163
+ else:
164
+ return mcolors.to_hex(tuple(c / 255 for c in color[:3]))
165
+ return mcolors.to_hex(color)
166
+ except Exception:
167
+ return "#888888"
168
+
169
+
170
+ __all__ = [
171
+ "DISTINCT_COLORS",
172
+ "BACKGROUND_COLOR",
173
+ "AXES_COLOR",
174
+ "hsv_to_rgb",
175
+ "id_to_rgb",
176
+ "rgb_to_id",
177
+ "normalize_color",
178
+ "mpl_color_to_hex",
179
+ ]
180
+
181
+ # EOF