figrecipe 0.5.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 (189) hide show
  1. figrecipe/__init__.py +220 -819
  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 +29 -0
  12. figrecipe/_dev/_plotters.py +76 -0
  13. figrecipe/_dev/_run_demos.py +56 -0
  14. figrecipe/_dev/demo_plotters/__init__.py +64 -0
  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/bar_categorical/plot_bar.py +25 -0
  21. figrecipe/_dev/demo_plotters/bar_categorical/plot_barh.py +25 -0
  22. figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
  23. figrecipe/_dev/demo_plotters/contour_surface/plot_contour.py +30 -0
  24. figrecipe/_dev/demo_plotters/contour_surface/plot_contourf.py +29 -0
  25. figrecipe/_dev/demo_plotters/contour_surface/plot_tricontour.py +28 -0
  26. figrecipe/_dev/demo_plotters/contour_surface/plot_tricontourf.py +28 -0
  27. figrecipe/_dev/demo_plotters/contour_surface/plot_tripcolor.py +29 -0
  28. figrecipe/_dev/demo_plotters/contour_surface/plot_triplot.py +25 -0
  29. figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
  30. figrecipe/_dev/demo_plotters/distribution/plot_boxplot.py +24 -0
  31. figrecipe/_dev/demo_plotters/distribution/plot_ecdf.py +24 -0
  32. figrecipe/_dev/demo_plotters/distribution/plot_hist.py +24 -0
  33. figrecipe/_dev/demo_plotters/distribution/plot_hist2d.py +25 -0
  34. figrecipe/_dev/demo_plotters/distribution/plot_violinplot.py +25 -0
  35. figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
  36. figrecipe/_dev/demo_plotters/image_matrix/plot_hexbin.py +25 -0
  37. figrecipe/_dev/demo_plotters/image_matrix/plot_imshow.py +23 -0
  38. figrecipe/_dev/demo_plotters/image_matrix/plot_matshow.py +23 -0
  39. figrecipe/_dev/demo_plotters/image_matrix/plot_pcolor.py +29 -0
  40. figrecipe/_dev/demo_plotters/image_matrix/plot_pcolormesh.py +29 -0
  41. figrecipe/_dev/demo_plotters/image_matrix/plot_spy.py +29 -0
  42. figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
  43. figrecipe/_dev/demo_plotters/line_curve/plot_errorbar.py +28 -0
  44. figrecipe/_dev/demo_plotters/line_curve/plot_fill.py +29 -0
  45. figrecipe/_dev/demo_plotters/line_curve/plot_fill_between.py +30 -0
  46. figrecipe/_dev/demo_plotters/line_curve/plot_fill_betweenx.py +28 -0
  47. figrecipe/_dev/demo_plotters/line_curve/plot_plot.py +28 -0
  48. figrecipe/_dev/demo_plotters/line_curve/plot_stackplot.py +29 -0
  49. figrecipe/_dev/demo_plotters/line_curve/plot_stairs.py +27 -0
  50. figrecipe/_dev/demo_plotters/line_curve/plot_step.py +27 -0
  51. figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
  52. figrecipe/_dev/demo_plotters/scatter_points/plot_scatter.py +24 -0
  53. figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
  54. figrecipe/_dev/demo_plotters/special/plot_eventplot.py +25 -0
  55. figrecipe/_dev/demo_plotters/special/plot_loglog.py +27 -0
  56. figrecipe/_dev/demo_plotters/special/plot_pie.py +27 -0
  57. figrecipe/_dev/demo_plotters/special/plot_semilogx.py +27 -0
  58. figrecipe/_dev/demo_plotters/special/plot_semilogy.py +27 -0
  59. figrecipe/_dev/demo_plotters/special/plot_stem.py +27 -0
  60. figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
  61. figrecipe/_dev/demo_plotters/spectral_signal/plot_acorr.py +24 -0
  62. figrecipe/_dev/demo_plotters/spectral_signal/plot_angle_spectrum.py +28 -0
  63. figrecipe/_dev/demo_plotters/spectral_signal/plot_cohere.py +29 -0
  64. figrecipe/_dev/demo_plotters/spectral_signal/plot_csd.py +29 -0
  65. figrecipe/_dev/demo_plotters/spectral_signal/plot_magnitude_spectrum.py +28 -0
  66. figrecipe/_dev/demo_plotters/spectral_signal/plot_phase_spectrum.py +28 -0
  67. figrecipe/_dev/demo_plotters/spectral_signal/plot_psd.py +29 -0
  68. figrecipe/_dev/demo_plotters/spectral_signal/plot_specgram.py +30 -0
  69. figrecipe/_dev/demo_plotters/spectral_signal/plot_xcorr.py +25 -0
  70. figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
  71. figrecipe/_dev/demo_plotters/vector_flow/plot_barbs.py +30 -0
  72. figrecipe/_dev/demo_plotters/vector_flow/plot_quiver.py +30 -0
  73. figrecipe/_dev/demo_plotters/vector_flow/plot_streamplot.py +30 -0
  74. figrecipe/_editor/__init__.py +278 -0
  75. figrecipe/_editor/_bbox/__init__.py +43 -0
  76. figrecipe/_editor/_bbox/_collections.py +177 -0
  77. figrecipe/_editor/_bbox/_elements.py +159 -0
  78. figrecipe/_editor/_bbox/_extract.py +256 -0
  79. figrecipe/_editor/_bbox/_extract_axes.py +370 -0
  80. figrecipe/_editor/_bbox/_extract_text.py +342 -0
  81. figrecipe/_editor/_bbox/_lines.py +173 -0
  82. figrecipe/_editor/_bbox/_transforms.py +146 -0
  83. figrecipe/_editor/_flask_app.py +258 -0
  84. figrecipe/_editor/_helpers.py +242 -0
  85. figrecipe/_editor/_hitmap/__init__.py +76 -0
  86. figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
  87. figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
  88. figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
  89. figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
  90. figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
  91. figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
  92. figrecipe/_editor/_hitmap/_colors.py +181 -0
  93. figrecipe/_editor/_hitmap/_detect.py +137 -0
  94. figrecipe/_editor/_hitmap/_restore.py +154 -0
  95. figrecipe/_editor/_hitmap_main.py +182 -0
  96. figrecipe/_editor/_overrides.py +318 -0
  97. figrecipe/_editor/_preferences.py +135 -0
  98. figrecipe/_editor/_render_overrides.py +480 -0
  99. figrecipe/_editor/_renderer.py +199 -0
  100. figrecipe/_editor/_routes_axis.py +453 -0
  101. figrecipe/_editor/_routes_core.py +284 -0
  102. figrecipe/_editor/_routes_element.py +317 -0
  103. figrecipe/_editor/_routes_style.py +223 -0
  104. figrecipe/_editor/_templates/__init__.py +152 -0
  105. figrecipe/_editor/_templates/_html.py +502 -0
  106. figrecipe/_editor/_templates/_scripts/__init__.py +120 -0
  107. figrecipe/_editor/_templates/_scripts/_api.py +228 -0
  108. figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
  109. figrecipe/_editor/_templates/_scripts/_core.py +436 -0
  110. figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
  111. figrecipe/_editor/_templates/_scripts/_element_editor.py +310 -0
  112. figrecipe/_editor/_templates/_scripts/_files.py +195 -0
  113. figrecipe/_editor/_templates/_scripts/_hitmap.py +509 -0
  114. figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
  115. figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
  116. figrecipe/_editor/_templates/_scripts/_legend_drag.py +265 -0
  117. figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
  118. figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
  119. figrecipe/_editor/_templates/_scripts/_panel_drag.py +334 -0
  120. figrecipe/_editor/_templates/_scripts/_panel_position.py +279 -0
  121. figrecipe/_editor/_templates/_scripts/_selection.py +237 -0
  122. figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
  123. figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
  124. figrecipe/_editor/_templates/_scripts/_zoom.py +179 -0
  125. figrecipe/_editor/_templates/_styles/__init__.py +69 -0
  126. figrecipe/_editor/_templates/_styles/_base.py +64 -0
  127. figrecipe/_editor/_templates/_styles/_buttons.py +206 -0
  128. figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
  129. figrecipe/_editor/_templates/_styles/_controls.py +265 -0
  130. figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
  131. figrecipe/_editor/_templates/_styles/_forms.py +126 -0
  132. figrecipe/_editor/_templates/_styles/_hitmap.py +184 -0
  133. figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
  134. figrecipe/_editor/_templates/_styles/_labels.py +118 -0
  135. figrecipe/_editor/_templates/_styles/_modals.py +98 -0
  136. figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
  137. figrecipe/_editor/_templates/_styles/_preview.py +225 -0
  138. figrecipe/_editor/_templates/_styles/_selection.py +73 -0
  139. figrecipe/_params/_DECORATION_METHODS.py +33 -0
  140. figrecipe/_params/_PLOTTING_METHODS.py +58 -0
  141. figrecipe/_params/__init__.py +9 -0
  142. figrecipe/_recorder.py +92 -110
  143. figrecipe/_recorder_utils.py +124 -0
  144. figrecipe/_reproducer/__init__.py +18 -0
  145. figrecipe/_reproducer/_core.py +498 -0
  146. figrecipe/_reproducer/_custom_plots.py +279 -0
  147. figrecipe/_reproducer/_seaborn.py +100 -0
  148. figrecipe/_reproducer/_violin.py +186 -0
  149. figrecipe/_seaborn.py +14 -9
  150. figrecipe/_serializer.py +2 -2
  151. figrecipe/_signatures/README.md +68 -0
  152. figrecipe/_signatures/__init__.py +12 -2
  153. figrecipe/_signatures/_kwargs.py +273 -0
  154. figrecipe/_signatures/_loader.py +114 -57
  155. figrecipe/_signatures/_parsing.py +147 -0
  156. figrecipe/_utils/__init__.py +6 -4
  157. figrecipe/_utils/_crop.py +10 -4
  158. figrecipe/_utils/_image_diff.py +37 -33
  159. figrecipe/_utils/_numpy_io.py +0 -1
  160. figrecipe/_utils/_units.py +11 -3
  161. figrecipe/_validator.py +12 -3
  162. figrecipe/_wrappers/_axes.py +193 -170
  163. figrecipe/_wrappers/_axes_helpers.py +136 -0
  164. figrecipe/_wrappers/_axes_plots.py +418 -0
  165. figrecipe/_wrappers/_axes_seaborn.py +157 -0
  166. figrecipe/_wrappers/_figure.py +277 -18
  167. figrecipe/_wrappers/_panel_labels.py +127 -0
  168. figrecipe/_wrappers/_plot_helpers.py +143 -0
  169. figrecipe/_wrappers/_violin_helpers.py +180 -0
  170. figrecipe/plt.py +0 -1
  171. figrecipe/pyplot.py +2 -1
  172. figrecipe/styles/__init__.py +12 -11
  173. figrecipe/styles/_dotdict.py +72 -0
  174. figrecipe/styles/_finalize.py +134 -0
  175. figrecipe/styles/_fonts.py +77 -0
  176. figrecipe/styles/_kwargs_converter.py +178 -0
  177. figrecipe/styles/_plot_styles.py +209 -0
  178. figrecipe/styles/_style_applier.py +60 -202
  179. figrecipe/styles/_style_loader.py +73 -121
  180. figrecipe/styles/_themes.py +151 -0
  181. figrecipe/styles/presets/MATPLOTLIB.yaml +95 -0
  182. figrecipe/styles/presets/SCITEX.yaml +181 -0
  183. figrecipe-0.7.4.dist-info/METADATA +429 -0
  184. figrecipe-0.7.4.dist-info/RECORD +188 -0
  185. figrecipe/_reproducer.py +0 -358
  186. figrecipe-0.5.0.dist-info/METADATA +0 -336
  187. figrecipe-0.5.0.dist-info/RECORD +0 -26
  188. {figrecipe-0.5.0.dist-info → figrecipe-0.7.4.dist-info}/WHEEL +0 -0
  189. {figrecipe-0.5.0.dist-info → figrecipe-0.7.4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,485 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Color handling JavaScript for the figure editor.
4
+
5
+ This module contains the JavaScript code for:
6
+ - Color presets (SCITEX, matplotlib, CSS)
7
+ - Color conversion utilities
8
+ - Color input widget creation
9
+ """
10
+
11
+ SCRIPTS_COLORS = """
12
+ // ===== COLOR HANDLING =====
13
+
14
+ // Color presets from SCITEX theme (priority 1 - highest)
15
+ const COLOR_PRESETS = {
16
+ 'blue': { hex: '#0080c0', rgb: [0, 128, 192] },
17
+ 'red': { hex: '#ff4632', rgb: [255, 70, 50] },
18
+ 'green': { hex: '#14b414', rgb: [20, 180, 20] },
19
+ 'yellow': { hex: '#e6a014', rgb: [230, 160, 20] },
20
+ 'purple': { hex: '#c832ff', rgb: [200, 50, 255] },
21
+ 'lightblue': { hex: '#14c8c8', rgb: [20, 200, 200] },
22
+ 'orange': { hex: '#e45e32', rgb: [228, 94, 50] },
23
+ 'pink': { hex: '#ff96c8', rgb: [255, 150, 200] },
24
+ 'black': { hex: '#000000', rgb: [0, 0, 0] },
25
+ 'white': { hex: '#ffffff', rgb: [255, 255, 255] },
26
+ 'gray': { hex: '#808080', rgb: [128, 128, 128] }
27
+ };
28
+
29
+ // Matplotlib single-letter colors (priority 2)
30
+ const MATPLOTLIB_SINGLE = {
31
+ 'b': { hex: '#1f77b4', rgb: [31, 119, 180] },
32
+ 'g': { hex: '#2ca02c', rgb: [44, 160, 44] },
33
+ 'r': { hex: '#d62728', rgb: [214, 39, 40] },
34
+ 'c': { hex: '#17becf', rgb: [23, 190, 207] },
35
+ 'm': { hex: '#9467bd', rgb: [148, 103, 189] },
36
+ 'y': { hex: '#bcbd22', rgb: [188, 189, 34] },
37
+ 'k': { hex: '#000000', rgb: [0, 0, 0] },
38
+ 'w': { hex: '#ffffff', rgb: [255, 255, 255] }
39
+ };
40
+
41
+ // Common matplotlib/CSS named colors (priority 3)
42
+ const MATPLOTLIB_NAMED = {
43
+ 'aqua': '#00ffff', 'coral': '#ff7f50', 'crimson': '#dc143c',
44
+ 'cyan': '#00ffff', 'gold': '#ffd700', 'indigo': '#4b0082',
45
+ 'lime': '#00ff00', 'magenta': '#ff00ff', 'maroon': '#800000',
46
+ 'navy': '#000080', 'olive': '#808000', 'salmon': '#fa8072',
47
+ 'silver': '#c0c0c0', 'teal': '#008080', 'tomato': '#ff6347',
48
+ 'turquoise': '#40e0d0', 'violet': '#ee82ee'
49
+ };
50
+
51
+ // Check if a field is a color field (single color, not list)
52
+ function isColorField(key, sigInfo) {
53
+ // 'colors' (plural) is a list of colors - handle separately
54
+ if (key.toLowerCase() === 'colors') return false;
55
+ const colorKeywords = ['color', 'facecolor', 'edgecolor', 'markerfacecolor', 'markeredgecolor', 'c'];
56
+ if (colorKeywords.includes(key.toLowerCase())) return true;
57
+ if (sigInfo?.type && sigInfo.type.toLowerCase().includes('color')) return true;
58
+ return false;
59
+ }
60
+
61
+ // Check if a field is a color list field
62
+ function isColorListField(key, value) {
63
+ if (key.toLowerCase() === 'colors' && Array.isArray(value)) return true;
64
+ return false;
65
+ }
66
+
67
+ // Convert color to RGB string for display
68
+ function colorToRGB(color) {
69
+ if (!color) return '';
70
+ if (typeof color === 'string' && color.match(/^rgb/i)) return color;
71
+ if (typeof color === 'string' && color.startsWith('#')) {
72
+ const hex = color.slice(1);
73
+ if (hex.length === 3) {
74
+ const r = parseInt(hex[0] + hex[0], 16);
75
+ const g = parseInt(hex[1] + hex[1], 16);
76
+ const b = parseInt(hex[2] + hex[2], 16);
77
+ return `rgb(${r}, ${g}, ${b})`;
78
+ } else if (hex.length === 6) {
79
+ const r = parseInt(hex.slice(0, 2), 16);
80
+ const g = parseInt(hex.slice(2, 4), 16);
81
+ const b = parseInt(hex.slice(4, 6), 16);
82
+ return `rgb(${r}, ${g}, ${b})`;
83
+ }
84
+ }
85
+ return color;
86
+ }
87
+
88
+ // Convert color to hex for color picker
89
+ function colorToHex(color) {
90
+ return resolveColorToHex(color);
91
+ }
92
+
93
+ // Find preset color matching input (name, hex, RGB array, or RGB string)
94
+ function findPresetColor(input) {
95
+ if (!input) return null;
96
+
97
+ // Handle array input [r, g, b] where values are 0-1
98
+ if (Array.isArray(input) && input.length >= 3) {
99
+ const r = Math.round(parseFloat(input[0]) * 255);
100
+ const g = Math.round(parseFloat(input[1]) * 255);
101
+ const b = Math.round(parseFloat(input[2]) * 255);
102
+ // Check against preset RGB values
103
+ for (const [name, preset] of Object.entries(COLOR_PRESETS)) {
104
+ const [pr, pg, pb] = preset.rgb;
105
+ if (Math.abs(r - pr) <= 1 && Math.abs(g - pg) <= 1 && Math.abs(b - pb) <= 1) {
106
+ return { name, ...preset };
107
+ }
108
+ }
109
+ for (const [name, preset] of Object.entries(MATPLOTLIB_SINGLE)) {
110
+ const [pr, pg, pb] = preset.rgb;
111
+ if (Math.abs(r - pr) <= 1 && Math.abs(g - pg) <= 1 && Math.abs(b - pb) <= 1) {
112
+ return { name, ...preset };
113
+ }
114
+ }
115
+ return null;
116
+ }
117
+
118
+ const inputLower = typeof input === 'string' ? input.toLowerCase().trim() : '';
119
+
120
+ // Check preset names
121
+ if (COLOR_PRESETS[inputLower]) return { name: inputLower, ...COLOR_PRESETS[inputLower] };
122
+ if (MATPLOTLIB_SINGLE[inputLower]) return { name: inputLower, ...MATPLOTLIB_SINGLE[inputLower] };
123
+
124
+ // Check hex values
125
+ for (const [name, preset] of Object.entries(COLOR_PRESETS)) {
126
+ if (preset.hex.toLowerCase() === inputLower) return { name, ...preset };
127
+ }
128
+
129
+ // Check RGB string format like "rgb(0, 128, 192)"
130
+ const rgbMatch = inputLower.match(/rgb\\s*\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*\\)/);
131
+ if (rgbMatch) {
132
+ const r = parseInt(rgbMatch[1]);
133
+ const g = parseInt(rgbMatch[2]);
134
+ const b = parseInt(rgbMatch[3]);
135
+ for (const [name, preset] of Object.entries(COLOR_PRESETS)) {
136
+ const [pr, pg, pb] = preset.rgb;
137
+ if (Math.abs(r - pr) <= 1 && Math.abs(g - pg) <= 1 && Math.abs(b - pb) <= 1) {
138
+ return { name, ...preset };
139
+ }
140
+ }
141
+ }
142
+
143
+ return null;
144
+ }
145
+
146
+ // Find preset by hex value
147
+ function findPresetByHex(hexValue) {
148
+ if (!hexValue) return null;
149
+ const hex = hexValue.toLowerCase();
150
+
151
+ for (const [name, preset] of Object.entries(COLOR_PRESETS)) {
152
+ if (preset.hex.toLowerCase() === hex) return { name, ...preset };
153
+ }
154
+ for (const [name, preset] of Object.entries(MATPLOTLIB_SINGLE)) {
155
+ if (preset.hex.toLowerCase() === hex) return { name, ...preset };
156
+ }
157
+ return null;
158
+ }
159
+
160
+ // Resolve color to hex using priority: theme > matplotlib > CSS
161
+ function resolveColorToHex(input) {
162
+ if (!input) return '#000000';
163
+
164
+ if (typeof input === 'string' && input.startsWith('#')) {
165
+ return input.length === 4 ?
166
+ '#' + input[1] + input[1] + input[2] + input[2] + input[3] + input[3] :
167
+ input;
168
+ }
169
+
170
+ if (typeof input === 'string' && input.startsWith('(')) {
171
+ const match = input.match(/\\(([\\d.]+),\\s*([\\d.]+),\\s*([\\d.]+)/);
172
+ if (match) {
173
+ const r = Math.round(parseFloat(match[1]) * 255);
174
+ const g = Math.round(parseFloat(match[2]) * 255);
175
+ const b = Math.round(parseFloat(match[3]) * 255);
176
+ return '#' + [r, g, b].map(c => c.toString(16).padStart(2, '0')).join('');
177
+ }
178
+ }
179
+
180
+ // Handle array format [r, g, b] where values are 0-1
181
+ if (Array.isArray(input) && input.length >= 3) {
182
+ const r = Math.round(parseFloat(input[0]) * 255);
183
+ const g = Math.round(parseFloat(input[1]) * 255);
184
+ const b = Math.round(parseFloat(input[2]) * 255);
185
+ return '#' + [r, g, b].map(c => c.toString(16).padStart(2, '0')).join('');
186
+ }
187
+
188
+ // Ensure input is a string before calling toLowerCase
189
+ if (typeof input !== 'string') return '#000000';
190
+
191
+ const inputLower = input.toLowerCase().trim();
192
+ if (COLOR_PRESETS[inputLower]) return COLOR_PRESETS[inputLower].hex;
193
+ if (MATPLOTLIB_SINGLE[inputLower]) return MATPLOTLIB_SINGLE[inputLower].hex;
194
+ if (MATPLOTLIB_NAMED[inputLower]) return MATPLOTLIB_NAMED[inputLower];
195
+
196
+ return '#000000';
197
+ }
198
+
199
+ // Format color for display
200
+ function formatColorDisplay(value) {
201
+ if (!value) return '';
202
+ const preset = findPresetColor(value);
203
+ if (preset) return preset.name;
204
+
205
+ // Convert to hex first, then to clean RGB display
206
+ const hex = resolveColorToHex(value);
207
+ if (hex && hex.startsWith('#') && hex.length === 7) {
208
+ const r = parseInt(hex.slice(1, 3), 16);
209
+ const g = parseInt(hex.slice(3, 5), 16);
210
+ const b = parseInt(hex.slice(5, 7), 16);
211
+ return `rgb(${r}, ${g}, ${b})`;
212
+ }
213
+ return value;
214
+ }
215
+
216
+ // Convert hex to RGB tuple string
217
+ function hexToRGBTuple(hex) {
218
+ if (!hex || !hex.startsWith('#')) return null;
219
+ const h = hex.slice(1);
220
+ if (h.length !== 6) return null;
221
+ const r = parseInt(h.slice(0, 2), 16) / 255;
222
+ const g = parseInt(h.slice(2, 4), 16) / 255;
223
+ const b = parseInt(h.slice(4, 6), 16) / 255;
224
+ return `(${r.toFixed(3)}, ${g.toFixed(3)}, ${b.toFixed(3)})`;
225
+ }
226
+
227
+ // Create color input with preset dropdown and picker
228
+ function createColorInput(callId, key, value) {
229
+ const wrapper = document.createElement('div');
230
+ wrapper.className = 'color-input-wrapper';
231
+
232
+ const swatch = document.createElement('div');
233
+ swatch.className = 'color-swatch';
234
+
235
+ const select = document.createElement('select');
236
+ select.className = 'color-select';
237
+
238
+ for (const [name, preset] of Object.entries(COLOR_PRESETS)) {
239
+ const opt = document.createElement('option');
240
+ opt.value = name;
241
+ opt.textContent = name;
242
+ select.appendChild(opt);
243
+ }
244
+
245
+ const separator = document.createElement('option');
246
+ separator.disabled = true;
247
+ separator.textContent = '───────────';
248
+ select.appendChild(separator);
249
+
250
+ const customOpt = document.createElement('option');
251
+ customOpt.value = '__custom__';
252
+ customOpt.textContent = 'Custom...';
253
+ select.appendChild(customOpt);
254
+
255
+ const customInput = document.createElement('input');
256
+ customInput.type = 'text';
257
+ customInput.className = 'color-custom-input';
258
+ customInput.placeholder = '#rrggbb or name';
259
+ customInput.style.display = 'none';
260
+
261
+ // rgbDisplay removed - dropdown now shows RGB format directly
262
+
263
+ const colorPicker = document.createElement('input');
264
+ colorPicker.type = 'color';
265
+ colorPicker.className = 'color-picker-hidden';
266
+
267
+ function updateDisplay(colorValue) {
268
+ const hex = resolveColorToHex(colorValue);
269
+ swatch.style.backgroundColor = hex;
270
+ colorPicker.value = hex;
271
+ }
272
+
273
+ const initialPreset = findPresetColor(value);
274
+ if (initialPreset) {
275
+ select.value = initialPreset.name;
276
+ } else if (value) {
277
+ // Store hex internally, display RGB for users
278
+ const hexValue = resolveColorToHex(value);
279
+ const currentOpt = document.createElement('option');
280
+ currentOpt.value = hexValue;
281
+ currentOpt.textContent = formatColorDisplay(value);
282
+ select.insertBefore(currentOpt, separator);
283
+ select.value = hexValue;
284
+ }
285
+ updateDisplay(value || 'blue');
286
+
287
+ select.addEventListener('change', function() {
288
+ if (this.value === '__custom__') {
289
+ customInput.style.display = '';
290
+ customInput.focus();
291
+ swatch.style.display = 'none';
292
+ } else {
293
+ customInput.style.display = 'none';
294
+ swatch.style.display = '';
295
+ updateDisplay(this.value);
296
+ handleDynamicParamChange(callId, key, { value: this.value });
297
+ }
298
+ });
299
+
300
+ customInput.addEventListener('keydown', function(e) {
301
+ if (e.key === 'Enter') {
302
+ const inputValue = this.value.trim();
303
+ if (inputValue) {
304
+ swatch.style.display = '';
305
+ const preset = findPresetColor(inputValue);
306
+ if (preset) {
307
+ select.value = preset.name;
308
+ } else {
309
+ let existingOpt = Array.from(select.options).find(o => o.value === inputValue);
310
+ if (!existingOpt) {
311
+ const newOpt = document.createElement('option');
312
+ newOpt.value = inputValue;
313
+ newOpt.textContent = inputValue;
314
+ select.insertBefore(newOpt, separator);
315
+ }
316
+ select.value = inputValue;
317
+ }
318
+ customInput.style.display = 'none';
319
+ updateDisplay(inputValue);
320
+ handleDynamicParamChange(callId, key, { value: inputValue });
321
+ }
322
+ } else if (e.key === 'Escape') {
323
+ customInput.style.display = 'none';
324
+ if (select.value === '__custom__') select.selectedIndex = 0;
325
+ }
326
+ });
327
+
328
+ swatch.addEventListener('click', () => colorPicker.click());
329
+
330
+ colorPicker.addEventListener('change', function() {
331
+ const pickedColor = this.value;
332
+ const preset = findPresetColor(pickedColor);
333
+ if (preset) {
334
+ select.value = preset.name;
335
+ } else {
336
+ const rgbTuple = hexToRGBTuple(pickedColor);
337
+ let existingOpt = Array.from(select.options).find(o => o.value === pickedColor || o.value === rgbTuple);
338
+ if (!existingOpt) {
339
+ const newOpt = document.createElement('option');
340
+ newOpt.value = rgbTuple || pickedColor;
341
+ newOpt.textContent = rgbTuple || pickedColor;
342
+ select.insertBefore(newOpt, separator);
343
+ }
344
+ select.value = rgbTuple || pickedColor;
345
+ }
346
+ swatch.style.display = '';
347
+ customInput.style.display = 'none';
348
+ updateDisplay(select.value);
349
+ handleDynamicParamChange(callId, key, { value: select.value });
350
+ });
351
+
352
+ wrapper.appendChild(swatch);
353
+ wrapper.appendChild(select);
354
+ wrapper.appendChild(customInput);
355
+ wrapper.appendChild(colorPicker);
356
+
357
+ return wrapper;
358
+ }
359
+
360
+ // Create color list input for arrays of colors (e.g., pie chart colors)
361
+ function createColorListInput(callId, key, colorArray) {
362
+ const wrapper = document.createElement('div');
363
+ wrapper.className = 'color-list-wrapper';
364
+
365
+ if (!Array.isArray(colorArray) || colorArray.length === 0) {
366
+ wrapper.textContent = 'No colors';
367
+ return wrapper;
368
+ }
369
+
370
+ // Create a color swatch + dropdown for each color in the list
371
+ colorArray.forEach((color, index) => {
372
+ const itemWrapper = document.createElement('div');
373
+ itemWrapper.className = 'color-list-item';
374
+
375
+ const indexLabel = document.createElement('span');
376
+ indexLabel.className = 'color-list-index';
377
+ indexLabel.textContent = `${index + 1}:`;
378
+
379
+ const swatch = document.createElement('div');
380
+ swatch.className = 'color-swatch color-swatch-small';
381
+ const hex = resolveColorToHex(color);
382
+ swatch.style.backgroundColor = hex;
383
+
384
+ const select = document.createElement('select');
385
+ select.className = 'color-select color-select-small';
386
+ select.dataset.index = index;
387
+
388
+ // Add preset color options
389
+ for (const [name, preset] of Object.entries(COLOR_PRESETS)) {
390
+ const opt = document.createElement('option');
391
+ opt.value = name;
392
+ opt.textContent = name;
393
+ select.appendChild(opt);
394
+ }
395
+
396
+ // Set current value
397
+ const preset = findPresetColor(color);
398
+ if (preset) {
399
+ select.value = preset.name;
400
+ } else {
401
+ // Add current color as option
402
+ const currentOpt = document.createElement('option');
403
+ currentOpt.value = hex;
404
+ currentOpt.textContent = formatColorDisplay(color);
405
+ select.insertBefore(currentOpt, select.firstChild);
406
+ select.value = hex;
407
+ }
408
+
409
+ // Handle color change
410
+ select.addEventListener('change', function() {
411
+ const newColor = this.value;
412
+ swatch.style.backgroundColor = resolveColorToHex(newColor);
413
+
414
+ // Update the colors array and send to backend
415
+ const newColors = [...colorArray];
416
+ newColors[index] = newColor;
417
+
418
+ // Send the entire updated array to the backend
419
+ handleColorListChange(callId, key, newColors);
420
+ });
421
+
422
+ itemWrapper.appendChild(indexLabel);
423
+ itemWrapper.appendChild(swatch);
424
+ itemWrapper.appendChild(select);
425
+ wrapper.appendChild(itemWrapper);
426
+ });
427
+
428
+ return wrapper;
429
+ }
430
+
431
+ // Handle color list change - update entire array
432
+ async function handleColorListChange(callId, key, colorsArray) {
433
+ // Normalize all colors to hex format for consistency
434
+ const normalizedColors = colorsArray.map(color => {
435
+ // If it's already a preset name, use it directly
436
+ if (typeof color === 'string' && COLOR_PRESETS[color.toLowerCase()]) {
437
+ return color;
438
+ }
439
+ // Otherwise convert to hex
440
+ return resolveColorToHex(color);
441
+ });
442
+
443
+ console.log(`Color list change: ${callId}.${key} = [${normalizedColors.join(', ')}]`);
444
+
445
+ document.body.classList.add('loading');
446
+
447
+ try {
448
+ const response = await fetch('/update_call', {
449
+ method: 'POST',
450
+ headers: { 'Content-Type': 'application/json' },
451
+ body: JSON.stringify({ call_id: callId, param: key, value: normalizedColors })
452
+ });
453
+
454
+ const data = await response.json();
455
+
456
+ if (data.success) {
457
+ const img = document.getElementById('preview-image');
458
+ img.src = 'data:image/png;base64,' + data.image;
459
+
460
+ if (data.img_size) {
461
+ currentImgWidth = data.img_size.width;
462
+ currentImgHeight = data.img_size.height;
463
+ }
464
+
465
+ currentBboxes = data.bboxes;
466
+ loadHitmap();
467
+ updateHitRegions();
468
+
469
+ if (callsData[callId]) {
470
+ callsData[callId].kwargs[key] = colorsArray;
471
+ }
472
+ } else {
473
+ alert('Update failed: ' + data.error);
474
+ }
475
+ } catch (error) {
476
+ alert('Update failed: ' + error.message);
477
+ }
478
+
479
+ document.body.classList.remove('loading');
480
+ }
481
+ """
482
+
483
+ __all__ = ["SCRIPTS_COLORS"]
484
+
485
+ # EOF