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,310 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Element editor JavaScript for the figure editor.
4
+
5
+ This module contains the JavaScript code for:
6
+ - Dynamic form field creation
7
+ - Call properties display
8
+ - Parameter change handling
9
+ """
10
+
11
+ SCRIPTS_ELEMENT_EDITOR = """
12
+ // ===== ELEMENT EDITOR =====
13
+
14
+ // Create a dynamic form field for call parameter
15
+ function createDynamicField(callId, key, value, sigInfo, isUnused = false) {
16
+ const container = document.createElement('div');
17
+ container.className = 'dynamic-field-container' + (isUnused ? ' unused' : '');
18
+
19
+ const row = document.createElement('div');
20
+ row.className = 'form-row dynamic-field';
21
+
22
+ const label = document.createElement('label');
23
+ label.textContent = key;
24
+
25
+ let input;
26
+
27
+ // Handle color list fields (e.g., pie chart colors array)
28
+ if (isColorListField(key, value)) {
29
+ input = createColorListInput(callId, key, value);
30
+ row.appendChild(label);
31
+ row.appendChild(input);
32
+ container.appendChild(row);
33
+ return container;
34
+ }
35
+
36
+ if (isColorField(key, sigInfo)) {
37
+ input = createColorInput(callId, key, value);
38
+ row.appendChild(label);
39
+ row.appendChild(input);
40
+ container.appendChild(row);
41
+ return container;
42
+ }
43
+
44
+ if (typeof value === 'boolean' || value === true || value === false) {
45
+ input = document.createElement('input');
46
+ input.type = 'checkbox';
47
+ input.checked = value === true;
48
+ } else if (typeof value === 'number') {
49
+ input = document.createElement('input');
50
+ input.type = 'number';
51
+ input.step = 'any';
52
+ input.value = value;
53
+ } else if (value === null || value === undefined) {
54
+ input = document.createElement('input');
55
+ input.type = 'text';
56
+ input.value = '';
57
+ input.placeholder = 'null';
58
+ } else {
59
+ input = document.createElement('input');
60
+ input.type = 'text';
61
+ input.value = String(value);
62
+ }
63
+
64
+ input.dataset.callId = callId;
65
+ input.dataset.param = key;
66
+ input.className = 'dynamic-input';
67
+
68
+ input.addEventListener('change', function() {
69
+ handleDynamicParamChange(callId, key, this);
70
+ });
71
+
72
+ row.appendChild(label);
73
+ row.appendChild(input);
74
+ container.appendChild(row);
75
+
76
+ if (sigInfo?.type) {
77
+ const typeHint = document.createElement('div');
78
+ typeHint.className = 'type-hint';
79
+ let typeText = sigInfo.type
80
+ .replace(/:mpltype:`([^`]+)`/g, '$1')
81
+ .replace(/`~[^`]+`/g, '')
82
+ .replace(/`([^`]+)`/g, '$1');
83
+ typeHint.textContent = typeText;
84
+ container.appendChild(typeHint);
85
+ }
86
+
87
+ return container;
88
+ }
89
+
90
+ // Show dynamic call properties for selected element
91
+ function showDynamicCallProperties(element) {
92
+ const container = document.getElementById('dynamic-call-properties');
93
+ if (!container) return;
94
+
95
+ container.innerHTML = '';
96
+
97
+ const callId = element.call_id || element.label;
98
+
99
+ // If no call data found, show basic element info instead
100
+ if (!callId || !callsData[callId]) {
101
+ container.style.display = 'block';
102
+
103
+ // Create header with element type
104
+ const header = document.createElement('div');
105
+ header.className = 'dynamic-props-header';
106
+ const elemType = element.type || 'element';
107
+ const elemLabel = element.label || callId || 'unknown';
108
+ header.innerHTML = `<strong>${elemType}</strong> <span class="call-id">${elemLabel}</span>`;
109
+ container.appendChild(header);
110
+
111
+ // Show basic info section
112
+ const infoSection = document.createElement('div');
113
+ infoSection.className = 'dynamic-props-section';
114
+ infoSection.innerHTML = '<div class="dynamic-props-label">Element Info:</div>';
115
+
116
+ // Show type
117
+ const typeRow = document.createElement('div');
118
+ typeRow.className = 'form-row dynamic-field';
119
+ typeRow.innerHTML = `<label>Type</label><span class="arg-value">${elemType}</span>`;
120
+ infoSection.appendChild(typeRow);
121
+
122
+ // Show color if available
123
+ if (element.original_color) {
124
+ const colorRow = document.createElement('div');
125
+ colorRow.className = 'form-row dynamic-field';
126
+ colorRow.innerHTML = `<label>Color</label><span class="arg-value" style="color:${element.original_color}">${element.original_color}</span>`;
127
+ infoSection.appendChild(colorRow);
128
+ }
129
+
130
+ // Show axes index
131
+ if (element.ax_index !== undefined) {
132
+ const axRow = document.createElement('div');
133
+ axRow.className = 'form-row dynamic-field';
134
+ axRow.innerHTML = `<label>Axes</label><span class="arg-value">ax_${element.ax_index}</span>`;
135
+ infoSection.appendChild(axRow);
136
+ }
137
+
138
+ container.appendChild(infoSection);
139
+
140
+ // Add note about no recorded call
141
+ const noteDiv = document.createElement('div');
142
+ noteDiv.className = 'dynamic-props-note';
143
+ noteDiv.style.cssText = 'font-size: 11px; color: var(--text-secondary); margin-top: 8px; font-style: italic;';
144
+ noteDiv.textContent = 'No recorded call data available for this element.';
145
+ container.appendChild(noteDiv);
146
+
147
+ return;
148
+ }
149
+
150
+ const callData = callsData[callId];
151
+ container.style.display = 'block';
152
+
153
+ // Create header
154
+ const header = document.createElement('div');
155
+ header.className = 'dynamic-props-header';
156
+ header.innerHTML = `<strong>${callData.function}()</strong> <span class="call-id">${callId}</span>`;
157
+ container.appendChild(header);
158
+
159
+ const usedArgs = callData.args || [];
160
+ const usedKwargs = { ...callData.kwargs } || {};
161
+ const sigArgs = callData.signature?.args || [];
162
+ const sigKwargs = callData.signature?.kwargs || {};
163
+
164
+ // Get representative color if not set
165
+ if (!usedKwargs.color && !usedKwargs.c) {
166
+ if ('color' in sigKwargs || 'c' in sigKwargs) {
167
+ const hitmapCallId = element.call_id;
168
+ const groupColor = getGroupRepresentativeColor(hitmapCallId, element.original_color) ||
169
+ element.original_color;
170
+ if (groupColor) {
171
+ usedKwargs.color = groupColor;
172
+ }
173
+ }
174
+ }
175
+
176
+ // Show args (read-only)
177
+ if (usedArgs.length > 0) {
178
+ const argsSection = document.createElement('div');
179
+ argsSection.className = 'dynamic-props-section';
180
+ argsSection.innerHTML = '<div class="dynamic-props-label">Arguments:</div>';
181
+
182
+ for (let i = 0; i < usedArgs.length; i++) {
183
+ const arg = usedArgs[i];
184
+ const sigArg = sigArgs[i] || {};
185
+ const row = document.createElement('div');
186
+ row.className = 'form-row dynamic-field arg-field';
187
+
188
+ const label = document.createElement('label');
189
+ label.textContent = arg.name;
190
+ if (sigArg.type) label.title = `Type: ${sigArg.type}`;
191
+ if (sigArg.optional) label.textContent += ' (opt)';
192
+
193
+ const valueSpan = document.createElement('span');
194
+ valueSpan.className = 'arg-value';
195
+ if (arg.data && Array.isArray(arg.data)) {
196
+ valueSpan.textContent = `[${arg.data.length} items]`;
197
+ } else if (arg.data === '__FILE__') {
198
+ valueSpan.textContent = '[external file]';
199
+ } else {
200
+ valueSpan.textContent = String(arg.data).substring(0, 30);
201
+ }
202
+
203
+ row.appendChild(label);
204
+ row.appendChild(valueSpan);
205
+ argsSection.appendChild(row);
206
+ }
207
+ container.appendChild(argsSection);
208
+ }
209
+
210
+ // Show used kwargs (editable)
211
+ if (Object.keys(usedKwargs).length > 0) {
212
+ const usedSection = document.createElement('div');
213
+ usedSection.className = 'dynamic-props-section';
214
+ usedSection.innerHTML = '<div class="dynamic-props-label">Used Parameters:</div>';
215
+
216
+ for (const [key, value] of Object.entries(usedKwargs)) {
217
+ const field = createDynamicField(callId, key, value, sigKwargs[key]);
218
+ usedSection.appendChild(field);
219
+ }
220
+ container.appendChild(usedSection);
221
+ }
222
+
223
+ // Show available (unused) params
224
+ const availableParams = Object.keys(sigKwargs).filter(k => !(k in usedKwargs));
225
+ if (availableParams.length > 0) {
226
+ const availSection = document.createElement('details');
227
+ availSection.className = 'dynamic-props-available';
228
+ availSection.setAttribute('open', '');
229
+ availSection.innerHTML = `<summary>Available Parameters (${availableParams.length})</summary>`;
230
+
231
+ const availContent = document.createElement('div');
232
+ availContent.className = 'dynamic-props-section';
233
+ for (const key of availableParams) {
234
+ const sigInfo = sigKwargs[key];
235
+ const field = createDynamicField(callId, key, sigInfo?.default, sigInfo, true);
236
+ availContent.appendChild(field);
237
+ }
238
+ availSection.appendChild(availContent);
239
+ container.appendChild(availSection);
240
+ }
241
+ }
242
+
243
+ // Handle change to dynamic call parameter
244
+ async function handleDynamicParamChange(callId, param, input) {
245
+ let value;
246
+ if (input.type === 'checkbox') {
247
+ value = input.checked;
248
+ } else if (input.type === 'number') {
249
+ value = parseFloat(input.value);
250
+ if (isNaN(value)) value = null;
251
+ } else {
252
+ value = input.value || null;
253
+ if (value === 'null') value = null;
254
+ }
255
+
256
+ // Resolve color to hex
257
+ const colorParams = ['color', 'facecolor', 'edgecolor', 'markerfacecolor', 'markeredgecolor', 'c'];
258
+ if (value && typeof value === 'string' && colorParams.includes(param.toLowerCase())) {
259
+ value = resolveColorToHex(value);
260
+ }
261
+
262
+ console.log(`Dynamic param change: ${callId}.${param} = ${value}`);
263
+
264
+ document.body.classList.add('loading');
265
+ if (input.disabled !== undefined) input.disabled = true;
266
+
267
+ try {
268
+ const response = await fetch('/update_call', {
269
+ method: 'POST',
270
+ headers: { 'Content-Type': 'application/json' },
271
+ body: JSON.stringify({ call_id: callId, param: param, value: value })
272
+ });
273
+
274
+ const data = await response.json();
275
+
276
+ if (data.success) {
277
+ const img = document.getElementById('preview-image');
278
+ img.src = 'data:image/png;base64,' + data.image;
279
+
280
+ if (data.img_size) {
281
+ currentImgWidth = data.img_size.width;
282
+ currentImgHeight = data.img_size.height;
283
+ }
284
+
285
+ currentBboxes = data.bboxes;
286
+ loadHitmap();
287
+ updateHitRegions();
288
+
289
+ if (callsData[callId]) {
290
+ if (value === null) {
291
+ delete callsData[callId].kwargs[param];
292
+ } else {
293
+ callsData[callId].kwargs[param] = value;
294
+ }
295
+ }
296
+ } else {
297
+ alert('Update failed: ' + data.error);
298
+ }
299
+ } catch (error) {
300
+ alert('Update failed: ' + error.message);
301
+ }
302
+
303
+ if (input.disabled !== undefined) input.disabled = false;
304
+ document.body.classList.remove('loading');
305
+ }
306
+ """
307
+
308
+ __all__ = ["SCRIPTS_ELEMENT_EDITOR"]
309
+
310
+ # EOF
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """File switcher JavaScript for switching between recipe files."""
4
+
5
+ SCRIPTS_FILES = """
6
+ // ==================== FILE SWITCHER ====================
7
+ // Allows switching between recipe files without restarting the server
8
+
9
+ let currentFilePath = null;
10
+
11
+ async function loadFileList() {
12
+ const selector = document.getElementById('file-selector');
13
+ if (!selector) return;
14
+
15
+ try {
16
+ const response = await fetch('/api/files');
17
+ if (!response.ok) {
18
+ selector.innerHTML = '<option value="">No files found</option>';
19
+ return;
20
+ }
21
+
22
+ const data = await response.json();
23
+ const files = data.files || [];
24
+ currentFilePath = data.current_file;
25
+
26
+ if (files.length === 0) {
27
+ selector.innerHTML = '<option value="">(No recipe files in directory)</option>';
28
+ return;
29
+ }
30
+
31
+ // Build options
32
+ let optionsHtml = '';
33
+ if (!currentFilePath) {
34
+ optionsHtml += '<option value="" selected>(Unsaved figure)</option>';
35
+ }
36
+
37
+ files.forEach(file => {
38
+ const isCurrent = file.is_current;
39
+ const icon = file.has_image ? '📊 ' : '📄 ';
40
+ const selected = isCurrent ? ' selected' : '';
41
+ optionsHtml += `<option value="${file.path}"${selected}>${icon}${file.name}</option>`;
42
+ });
43
+
44
+ selector.innerHTML = optionsHtml;
45
+
46
+ console.log('[FileSwitcher] Loaded', files.length, 'files');
47
+
48
+ } catch (error) {
49
+ console.error('[FileSwitcher] Error loading files:', error);
50
+ selector.innerHTML = '<option value="">Error loading files</option>';
51
+ }
52
+ }
53
+
54
+ async function switchToFile(filePath) {
55
+ if (!filePath || filePath === currentFilePath) return;
56
+
57
+ showToast('Loading figure...', 'info');
58
+
59
+ try {
60
+ const response = await fetch('/api/switch', {
61
+ method: 'POST',
62
+ headers: { 'Content-Type': 'application/json' },
63
+ body: JSON.stringify({ path: filePath })
64
+ });
65
+
66
+ if (!response.ok) {
67
+ const error = await response.json();
68
+ throw new Error(error.error || 'Failed to switch file');
69
+ }
70
+
71
+ const data = await response.json();
72
+
73
+ // Update preview image
74
+ const img = document.getElementById('preview-image');
75
+ if (img && data.image) {
76
+ img.src = 'data:image/png;base64,' + data.image;
77
+ }
78
+
79
+ // Update bboxes
80
+ if (data.bboxes) {
81
+ window.currentBboxes = data.bboxes;
82
+ }
83
+
84
+ // Update color map for hitmap
85
+ if (data.color_map) {
86
+ window.currentColorMap = data.color_map;
87
+ }
88
+
89
+ // Update current file path
90
+ currentFilePath = filePath;
91
+
92
+ // Clear selection
93
+ clearSelection();
94
+ document.getElementById('selected-element-panel')?.style.setProperty('display', 'none');
95
+
96
+ showToast('Loaded: ' + filePath, 'success');
97
+ console.log('[FileSwitcher] Switched to:', filePath);
98
+
99
+ // Reload file list to update selection state
100
+ loadFileList();
101
+
102
+ } catch (error) {
103
+ console.error('[FileSwitcher] Error switching file:', error);
104
+ showToast('Error: ' + error.message, 'error');
105
+ // Revert selector
106
+ loadFileList();
107
+ }
108
+ }
109
+
110
+ async function createNewFigure() {
111
+ showToast('Creating new figure...', 'info');
112
+
113
+ try {
114
+ const response = await fetch('/api/new', {
115
+ method: 'POST',
116
+ headers: { 'Content-Type': 'application/json' }
117
+ });
118
+
119
+ if (!response.ok) {
120
+ const error = await response.json();
121
+ throw new Error(error.error || 'Failed to create new figure');
122
+ }
123
+
124
+ const data = await response.json();
125
+
126
+ // Update preview image
127
+ const img = document.getElementById('preview-image');
128
+ if (img && data.image) {
129
+ img.src = 'data:image/png;base64,' + data.image;
130
+ }
131
+
132
+ // Update bboxes
133
+ if (data.bboxes) {
134
+ window.currentBboxes = data.bboxes;
135
+ }
136
+
137
+ // Update color map for hitmap
138
+ if (data.color_map) {
139
+ window.currentColorMap = data.color_map;
140
+ }
141
+
142
+ // Clear current file path (unsaved figure)
143
+ currentFilePath = null;
144
+
145
+ // Clear selection
146
+ if (typeof clearSelection === 'function') {
147
+ clearSelection();
148
+ }
149
+ const selectedPanel = document.getElementById('selected-element-panel');
150
+ if (selectedPanel) selectedPanel.style.display = 'none';
151
+
152
+ showToast('New blank figure created', 'success');
153
+ console.log('[FileSwitcher] Created new blank figure');
154
+
155
+ // Reload file list to show (Unsaved figure)
156
+ loadFileList();
157
+
158
+ } catch (error) {
159
+ console.error('[FileSwitcher] Error creating new figure:', error);
160
+ showToast('Error: ' + error.message, 'error');
161
+ }
162
+ }
163
+
164
+ function initFileSwitcher() {
165
+ const selector = document.getElementById('file-selector');
166
+ const newBtn = document.getElementById('btn-new-figure');
167
+
168
+ if (selector) {
169
+ selector.addEventListener('change', (e) => {
170
+ const filePath = e.target.value;
171
+ if (filePath) {
172
+ switchToFile(filePath);
173
+ }
174
+ });
175
+ }
176
+
177
+ if (newBtn) {
178
+ newBtn.addEventListener('click', createNewFigure);
179
+ }
180
+
181
+ // Load file list on init
182
+ loadFileList();
183
+ }
184
+
185
+ // Initialize file switcher after DOM is ready
186
+ if (document.readyState === 'loading') {
187
+ document.addEventListener('DOMContentLoaded', initFileSwitcher);
188
+ } else {
189
+ initFileSwitcher();
190
+ }
191
+
192
+ console.log('[FileSwitcher] Loaded - Use dropdown to switch figures');
193
+ """
194
+
195
+ __all__ = ["SCRIPTS_FILES"]