figrecipe 0.6.0__py3-none-any.whl → 0.9.0__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 (269) hide show
  1. figrecipe/__init__.py +161 -1030
  2. figrecipe/__main__.py +12 -0
  3. figrecipe/_api/__init__.py +48 -0
  4. figrecipe/_api/_extract.py +108 -0
  5. figrecipe/_api/_notebook.py +61 -0
  6. figrecipe/_api/_panel.py +113 -0
  7. figrecipe/_api/_save.py +287 -0
  8. figrecipe/_api/_seaborn_proxy.py +34 -0
  9. figrecipe/_api/_style_manager.py +153 -0
  10. figrecipe/_api/_subplots.py +333 -0
  11. figrecipe/_api/_validate.py +82 -0
  12. figrecipe/_cli/__init__.py +7 -0
  13. figrecipe/_cli/_compose.py +87 -0
  14. figrecipe/_cli/_convert.py +117 -0
  15. figrecipe/_cli/_crop.py +82 -0
  16. figrecipe/_cli/_edit.py +70 -0
  17. figrecipe/_cli/_extract.py +128 -0
  18. figrecipe/_cli/_fonts.py +47 -0
  19. figrecipe/_cli/_info.py +67 -0
  20. figrecipe/_cli/_main.py +58 -0
  21. figrecipe/_cli/_reproduce.py +79 -0
  22. figrecipe/_cli/_style.py +77 -0
  23. figrecipe/_cli/_validate.py +66 -0
  24. figrecipe/_cli/_version.py +50 -0
  25. figrecipe/_composition/__init__.py +32 -0
  26. figrecipe/_composition/_alignment.py +452 -0
  27. figrecipe/_composition/_compose.py +179 -0
  28. figrecipe/_composition/_import_axes.py +127 -0
  29. figrecipe/_composition/_visibility.py +125 -0
  30. figrecipe/_dev/__init__.py +4 -93
  31. figrecipe/_dev/_plotters.py +76 -0
  32. figrecipe/_dev/_run_demos.py +56 -0
  33. figrecipe/_dev/browser/__init__.py +69 -0
  34. figrecipe/_dev/browser/_audio.py +240 -0
  35. figrecipe/_dev/browser/_caption.py +356 -0
  36. figrecipe/_dev/browser/_click_effect.py +146 -0
  37. figrecipe/_dev/browser/_cursor.py +196 -0
  38. figrecipe/_dev/browser/_highlight.py +105 -0
  39. figrecipe/_dev/browser/_narration.py +237 -0
  40. figrecipe/_dev/browser/_recorder.py +446 -0
  41. figrecipe/_dev/browser/_utils.py +178 -0
  42. figrecipe/_dev/browser/_video_trim/__init__.py +152 -0
  43. figrecipe/_dev/browser/_video_trim/_detection.py +223 -0
  44. figrecipe/_dev/browser/_video_trim/_markers.py +140 -0
  45. figrecipe/_dev/demo_plotters/__init__.py +35 -166
  46. figrecipe/_dev/demo_plotters/_categories.py +81 -0
  47. figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
  48. figrecipe/_dev/demo_plotters/_helpers.py +31 -0
  49. figrecipe/_dev/demo_plotters/_registry.py +50 -0
  50. figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
  51. figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
  52. figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
  53. figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
  54. figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
  55. figrecipe/_dev/demo_plotters/{plot_plot.py → line_curve/plot_plot.py} +3 -2
  56. figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
  57. figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
  58. figrecipe/_dev/demo_plotters/{plot_pie.py → special/plot_pie.py} +5 -1
  59. figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
  60. figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
  61. figrecipe/_editor/__init__.py +61 -13
  62. figrecipe/_editor/_bbox/__init__.py +43 -0
  63. figrecipe/_editor/_bbox/_collections.py +177 -0
  64. figrecipe/_editor/_bbox/_elements.py +159 -0
  65. figrecipe/_editor/_bbox/_extract.py +402 -0
  66. figrecipe/_editor/_bbox/_extract_axes.py +370 -0
  67. figrecipe/_editor/_bbox/_extract_text.py +466 -0
  68. figrecipe/_editor/_bbox/_lines.py +173 -0
  69. figrecipe/_editor/_bbox/_transforms.py +146 -0
  70. figrecipe/_editor/_call_overrides.py +183 -0
  71. figrecipe/_editor/_datatable_plot_handlers.py +249 -0
  72. figrecipe/_editor/_figure_layout.py +211 -0
  73. figrecipe/_editor/_flask_app.py +200 -1030
  74. figrecipe/_editor/_helpers.py +251 -0
  75. figrecipe/_editor/_hitmap/__init__.py +76 -0
  76. figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
  77. figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
  78. figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
  79. figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
  80. figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
  81. figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
  82. figrecipe/_editor/_hitmap/_colors.py +181 -0
  83. figrecipe/_editor/_hitmap/_detect.py +194 -0
  84. figrecipe/_editor/_hitmap/_restore.py +154 -0
  85. figrecipe/_editor/_hitmap_main.py +182 -0
  86. figrecipe/_editor/_overrides.py +4 -1
  87. figrecipe/_editor/_plot_types_registry.py +190 -0
  88. figrecipe/_editor/_preferences.py +135 -0
  89. figrecipe/_editor/_render_overrides.py +507 -0
  90. figrecipe/_editor/_renderer.py +81 -186
  91. figrecipe/_editor/_routes_annotation.py +114 -0
  92. figrecipe/_editor/_routes_axis.py +482 -0
  93. figrecipe/_editor/_routes_captions.py +130 -0
  94. figrecipe/_editor/_routes_composition.py +270 -0
  95. figrecipe/_editor/_routes_core.py +126 -0
  96. figrecipe/_editor/_routes_datatable.py +364 -0
  97. figrecipe/_editor/_routes_element.py +335 -0
  98. figrecipe/_editor/_routes_files.py +443 -0
  99. figrecipe/_editor/_routes_image.py +200 -0
  100. figrecipe/_editor/_routes_snapshot.py +94 -0
  101. figrecipe/_editor/_routes_style.py +243 -0
  102. figrecipe/_editor/_templates/__init__.py +116 -1
  103. figrecipe/_editor/_templates/_html.py +154 -64
  104. figrecipe/_editor/_templates/_html_components/__init__.py +13 -0
  105. figrecipe/_editor/_templates/_html_components/_composition_toolbar.py +79 -0
  106. figrecipe/_editor/_templates/_html_components/_file_browser.py +41 -0
  107. figrecipe/_editor/_templates/_html_datatable.py +92 -0
  108. figrecipe/_editor/_templates/_scripts/__init__.py +178 -0
  109. figrecipe/_editor/_templates/_scripts/_accordion.py +328 -0
  110. figrecipe/_editor/_templates/_scripts/_annotation_drag.py +504 -0
  111. figrecipe/_editor/_templates/_scripts/_api.py +228 -0
  112. figrecipe/_editor/_templates/_scripts/_canvas_context_menu.py +182 -0
  113. figrecipe/_editor/_templates/_scripts/_captions.py +231 -0
  114. figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
  115. figrecipe/_editor/_templates/_scripts/_composition.py +283 -0
  116. figrecipe/_editor/_templates/_scripts/_core.py +493 -0
  117. figrecipe/_editor/_templates/_scripts/_datatable/__init__.py +59 -0
  118. figrecipe/_editor/_templates/_scripts/_datatable/_cell_edit.py +97 -0
  119. figrecipe/_editor/_templates/_scripts/_datatable/_clipboard.py +164 -0
  120. figrecipe/_editor/_templates/_scripts/_datatable/_context_menu.py +221 -0
  121. figrecipe/_editor/_templates/_scripts/_datatable/_core.py +150 -0
  122. figrecipe/_editor/_templates/_scripts/_datatable/_editable.py +511 -0
  123. figrecipe/_editor/_templates/_scripts/_datatable/_import.py +161 -0
  124. figrecipe/_editor/_templates/_scripts/_datatable/_plot.py +261 -0
  125. figrecipe/_editor/_templates/_scripts/_datatable/_selection.py +438 -0
  126. figrecipe/_editor/_templates/_scripts/_datatable/_table.py +256 -0
  127. figrecipe/_editor/_templates/_scripts/_datatable/_tabs.py +354 -0
  128. figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
  129. figrecipe/_editor/_templates/_scripts/_element_editor.py +325 -0
  130. figrecipe/_editor/_templates/_scripts/_files.py +429 -0
  131. figrecipe/_editor/_templates/_scripts/_files_context_menu.py +240 -0
  132. figrecipe/_editor/_templates/_scripts/_hitmap.py +512 -0
  133. figrecipe/_editor/_templates/_scripts/_image_drop.py +428 -0
  134. figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
  135. figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
  136. figrecipe/_editor/_templates/_scripts/_legend_drag.py +270 -0
  137. figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
  138. figrecipe/_editor/_templates/_scripts/_multi_select.py +198 -0
  139. figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
  140. figrecipe/_editor/_templates/_scripts/_panel_drag.py +505 -0
  141. figrecipe/_editor/_templates/_scripts/_panel_drag_snapshot.py +33 -0
  142. figrecipe/_editor/_templates/_scripts/_panel_position.py +463 -0
  143. figrecipe/_editor/_templates/_scripts/_panel_resize.py +230 -0
  144. figrecipe/_editor/_templates/_scripts/_panel_snap.py +307 -0
  145. figrecipe/_editor/_templates/_scripts/_region_select.py +255 -0
  146. figrecipe/_editor/_templates/_scripts/_selection.py +244 -0
  147. figrecipe/_editor/_templates/_scripts/_sync.py +242 -0
  148. figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
  149. figrecipe/_editor/_templates/_scripts/_undo_redo.py +348 -0
  150. figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
  151. figrecipe/_editor/_templates/_scripts/_zoom.py +212 -0
  152. figrecipe/_editor/_templates/_styles/__init__.py +78 -0
  153. figrecipe/_editor/_templates/_styles/_base.py +111 -0
  154. figrecipe/_editor/_templates/_styles/_buttons.py +327 -0
  155. figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
  156. figrecipe/_editor/_templates/_styles/_composition.py +87 -0
  157. figrecipe/_editor/_templates/_styles/_controls.py +430 -0
  158. figrecipe/_editor/_templates/_styles/_datatable/__init__.py +40 -0
  159. figrecipe/_editor/_templates/_styles/_datatable/_editable.py +203 -0
  160. figrecipe/_editor/_templates/_styles/_datatable/_panel.py +268 -0
  161. figrecipe/_editor/_templates/_styles/_datatable/_table.py +479 -0
  162. figrecipe/_editor/_templates/_styles/_datatable/_toolbar.py +384 -0
  163. figrecipe/_editor/_templates/_styles/_datatable/_vars.py +123 -0
  164. figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
  165. figrecipe/_editor/_templates/_styles/_file_browser.py +466 -0
  166. figrecipe/_editor/_templates/_styles/_forms.py +224 -0
  167. figrecipe/_editor/_templates/_styles/_hitmap.py +191 -0
  168. figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
  169. figrecipe/_editor/_templates/_styles/_labels.py +118 -0
  170. figrecipe/_editor/_templates/_styles/_modals.py +127 -0
  171. figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
  172. figrecipe/_editor/_templates/_styles/_preview.py +430 -0
  173. figrecipe/_editor/_templates/_styles/_selection.py +73 -0
  174. figrecipe/_editor/_templates/_styles/_spinner.py +117 -0
  175. figrecipe/_editor/static/audio/click.mp3 +0 -0
  176. figrecipe/_editor/static/click.mp3 +0 -0
  177. figrecipe/_editor/static/icons/favicon.ico +0 -0
  178. figrecipe/_integrations/__init__.py +17 -0
  179. figrecipe/_integrations/_scitex_stats.py +298 -0
  180. figrecipe/_params/_DECORATION_METHODS.py +8 -0
  181. figrecipe/_recorder.py +63 -109
  182. figrecipe/_recorder_utils.py +124 -0
  183. figrecipe/_reproducer/__init__.py +18 -0
  184. figrecipe/_reproducer/_core.py +509 -0
  185. figrecipe/_reproducer/_custom_plots.py +279 -0
  186. figrecipe/_reproducer/_seaborn.py +100 -0
  187. figrecipe/_reproducer/_violin.py +186 -0
  188. figrecipe/_signatures/_kwargs.py +273 -0
  189. figrecipe/_signatures/_loader.py +21 -423
  190. figrecipe/_signatures/_parsing.py +147 -0
  191. figrecipe/_utils/__init__.py +3 -0
  192. figrecipe/_utils/_bundle.py +205 -0
  193. figrecipe/_wrappers/_axes.py +252 -895
  194. figrecipe/_wrappers/_axes_helpers.py +136 -0
  195. figrecipe/_wrappers/_axes_plots.py +418 -0
  196. figrecipe/_wrappers/_axes_seaborn.py +157 -0
  197. figrecipe/_wrappers/_caption_generator.py +218 -0
  198. figrecipe/_wrappers/_figure.py +188 -1
  199. figrecipe/_wrappers/_panel_labels.py +127 -0
  200. figrecipe/_wrappers/_plot_helpers.py +143 -0
  201. figrecipe/_wrappers/_stat_annotation.py +274 -0
  202. figrecipe/_wrappers/_violin_helpers.py +180 -0
  203. figrecipe/styles/__init__.py +8 -6
  204. figrecipe/styles/_dotdict.py +72 -0
  205. figrecipe/styles/_finalize.py +134 -0
  206. figrecipe/styles/_fonts.py +77 -0
  207. figrecipe/styles/_kwargs_converter.py +178 -0
  208. figrecipe/styles/_plot_styles.py +209 -0
  209. figrecipe/styles/_style_applier.py +42 -480
  210. figrecipe/styles/_style_loader.py +16 -192
  211. figrecipe/styles/_themes.py +151 -0
  212. figrecipe/styles/presets/MATPLOTLIB.yaml +2 -1
  213. figrecipe/styles/presets/SCITEX.yaml +40 -28
  214. figrecipe-0.9.0.dist-info/METADATA +427 -0
  215. figrecipe-0.9.0.dist-info/RECORD +277 -0
  216. figrecipe-0.9.0.dist-info/entry_points.txt +2 -0
  217. figrecipe/_editor/_bbox.py +0 -978
  218. figrecipe/_editor/_hitmap.py +0 -937
  219. figrecipe/_editor/_templates/_scripts.py +0 -2778
  220. figrecipe/_editor/_templates/_styles.py +0 -1326
  221. figrecipe/_reproducer.py +0 -975
  222. figrecipe-0.6.0.dist-info/METADATA +0 -394
  223. figrecipe-0.6.0.dist-info/RECORD +0 -90
  224. /figrecipe/_dev/demo_plotters/{plot_bar.py → bar_categorical/plot_bar.py} +0 -0
  225. /figrecipe/_dev/demo_plotters/{plot_barh.py → bar_categorical/plot_barh.py} +0 -0
  226. /figrecipe/_dev/demo_plotters/{plot_contour.py → contour_surface/plot_contour.py} +0 -0
  227. /figrecipe/_dev/demo_plotters/{plot_contourf.py → contour_surface/plot_contourf.py} +0 -0
  228. /figrecipe/_dev/demo_plotters/{plot_tricontour.py → contour_surface/plot_tricontour.py} +0 -0
  229. /figrecipe/_dev/demo_plotters/{plot_tricontourf.py → contour_surface/plot_tricontourf.py} +0 -0
  230. /figrecipe/_dev/demo_plotters/{plot_tripcolor.py → contour_surface/plot_tripcolor.py} +0 -0
  231. /figrecipe/_dev/demo_plotters/{plot_triplot.py → contour_surface/plot_triplot.py} +0 -0
  232. /figrecipe/_dev/demo_plotters/{plot_boxplot.py → distribution/plot_boxplot.py} +0 -0
  233. /figrecipe/_dev/demo_plotters/{plot_ecdf.py → distribution/plot_ecdf.py} +0 -0
  234. /figrecipe/_dev/demo_plotters/{plot_hist.py → distribution/plot_hist.py} +0 -0
  235. /figrecipe/_dev/demo_plotters/{plot_hist2d.py → distribution/plot_hist2d.py} +0 -0
  236. /figrecipe/_dev/demo_plotters/{plot_violinplot.py → distribution/plot_violinplot.py} +0 -0
  237. /figrecipe/_dev/demo_plotters/{plot_hexbin.py → image_matrix/plot_hexbin.py} +0 -0
  238. /figrecipe/_dev/demo_plotters/{plot_imshow.py → image_matrix/plot_imshow.py} +0 -0
  239. /figrecipe/_dev/demo_plotters/{plot_matshow.py → image_matrix/plot_matshow.py} +0 -0
  240. /figrecipe/_dev/demo_plotters/{plot_pcolor.py → image_matrix/plot_pcolor.py} +0 -0
  241. /figrecipe/_dev/demo_plotters/{plot_pcolormesh.py → image_matrix/plot_pcolormesh.py} +0 -0
  242. /figrecipe/_dev/demo_plotters/{plot_spy.py → image_matrix/plot_spy.py} +0 -0
  243. /figrecipe/_dev/demo_plotters/{plot_errorbar.py → line_curve/plot_errorbar.py} +0 -0
  244. /figrecipe/_dev/demo_plotters/{plot_fill.py → line_curve/plot_fill.py} +0 -0
  245. /figrecipe/_dev/demo_plotters/{plot_fill_between.py → line_curve/plot_fill_between.py} +0 -0
  246. /figrecipe/_dev/demo_plotters/{plot_fill_betweenx.py → line_curve/plot_fill_betweenx.py} +0 -0
  247. /figrecipe/_dev/demo_plotters/{plot_stackplot.py → line_curve/plot_stackplot.py} +0 -0
  248. /figrecipe/_dev/demo_plotters/{plot_stairs.py → line_curve/plot_stairs.py} +0 -0
  249. /figrecipe/_dev/demo_plotters/{plot_step.py → line_curve/plot_step.py} +0 -0
  250. /figrecipe/_dev/demo_plotters/{plot_scatter.py → scatter_points/plot_scatter.py} +0 -0
  251. /figrecipe/_dev/demo_plotters/{plot_eventplot.py → special/plot_eventplot.py} +0 -0
  252. /figrecipe/_dev/demo_plotters/{plot_loglog.py → special/plot_loglog.py} +0 -0
  253. /figrecipe/_dev/demo_plotters/{plot_semilogx.py → special/plot_semilogx.py} +0 -0
  254. /figrecipe/_dev/demo_plotters/{plot_semilogy.py → special/plot_semilogy.py} +0 -0
  255. /figrecipe/_dev/demo_plotters/{plot_stem.py → special/plot_stem.py} +0 -0
  256. /figrecipe/_dev/demo_plotters/{plot_acorr.py → spectral_signal/plot_acorr.py} +0 -0
  257. /figrecipe/_dev/demo_plotters/{plot_angle_spectrum.py → spectral_signal/plot_angle_spectrum.py} +0 -0
  258. /figrecipe/_dev/demo_plotters/{plot_cohere.py → spectral_signal/plot_cohere.py} +0 -0
  259. /figrecipe/_dev/demo_plotters/{plot_csd.py → spectral_signal/plot_csd.py} +0 -0
  260. /figrecipe/_dev/demo_plotters/{plot_magnitude_spectrum.py → spectral_signal/plot_magnitude_spectrum.py} +0 -0
  261. /figrecipe/_dev/demo_plotters/{plot_phase_spectrum.py → spectral_signal/plot_phase_spectrum.py} +0 -0
  262. /figrecipe/_dev/demo_plotters/{plot_psd.py → spectral_signal/plot_psd.py} +0 -0
  263. /figrecipe/_dev/demo_plotters/{plot_specgram.py → spectral_signal/plot_specgram.py} +0 -0
  264. /figrecipe/_dev/demo_plotters/{plot_xcorr.py → spectral_signal/plot_xcorr.py} +0 -0
  265. /figrecipe/_dev/demo_plotters/{plot_barbs.py → vector_flow/plot_barbs.py} +0 -0
  266. /figrecipe/_dev/demo_plotters/{plot_quiver.py → vector_flow/plot_quiver.py} +0 -0
  267. /figrecipe/_dev/demo_plotters/{plot_streamplot.py → vector_flow/plot_streamplot.py} +0 -0
  268. {figrecipe-0.6.0.dist-info → figrecipe-0.9.0.dist-info}/WHEEL +0 -0
  269. {figrecipe-0.6.0.dist-info → figrecipe-0.9.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,493 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Core state, initialization, and utility JavaScript for the figure editor."""
4
+
5
+ SCRIPTS_CORE = """
6
+ // ==================== CORE STATE & INITIALIZATION ====================
7
+
8
+ // Debug mode - enabled via FIGRECIPE_DEBUG_MODE=1 env var
9
+ const DEBUG_MODE = DEBUG_MODE_PLACEHOLDER;
10
+
11
+ // State
12
+ let currentBboxes = initialBboxes;
13
+ let colorMap = initialColorMap;
14
+ let callsData = {}; // Recorded calls with signatures
15
+ let selectedElement = null;
16
+ let hitmapLoaded = false;
17
+ let hitmapCtx = null;
18
+ let hitmapImg = null;
19
+ let updateTimeout = null;
20
+ let currentImgWidth = imgWidth; // Track current preview dimensions
21
+ let currentImgHeight = imgHeight;
22
+ let hitmapVisible = false; // Hitmap overlay visibility (hover-only by default)
23
+ const UPDATE_DEBOUNCE = 500; // ms
24
+
25
+ // Overlapping element cycling state
26
+ let lastClickPosition = null;
27
+ let overlappingElements = [];
28
+ let cycleIndex = 0;
29
+ let hoveredElement = null; // Track currently hovered element for click priority
30
+
31
+ // View mode: 'all' shows all properties, 'selected' shows only element-specific
32
+ let viewMode = 'all';
33
+
34
+ // Zoom/Pan state
35
+ let zoomLevel = 1.0;
36
+ const ZOOM_MIN = 0.1;
37
+ const ZOOM_MAX = 5.0;
38
+ const ZOOM_STEP = 0.25;
39
+ let isPanning = false;
40
+ let panTarget = null; // Current scrollable element being panned
41
+ let panStartX = 0;
42
+ let panStartY = 0;
43
+ let scrollStartX = 0;
44
+ let scrollStartY = 0;
45
+
46
+ // Initialize
47
+ document.addEventListener('DOMContentLoaded', function() {
48
+ initializeValues();
49
+ initializeEventListeners();
50
+ loadHitmap();
51
+ loadLabels(); // Load current axis labels
52
+
53
+ // Update hit regions on window resize
54
+ window.addEventListener('resize', updateHitRegions);
55
+
56
+ // Update hit regions and overlays when preview image loads
57
+ const previewImg = document.getElementById('preview-image');
58
+ previewImg.addEventListener('load', updateHitRegions);
59
+ previewImg.addEventListener('load', updateOverlays);
60
+
61
+ // Initialize hit regions visibility state
62
+ const overlay = document.getElementById('hitregion-overlay');
63
+ const btn = document.getElementById('btn-show-hitmap');
64
+
65
+ if (hitmapVisible) {
66
+ if (overlay) overlay.classList.add('visible');
67
+ if (btn) {
68
+ btn.classList.add('active');
69
+ btn.textContent = 'Hide Hit Regions';
70
+ }
71
+ } else {
72
+ // Hover-only mode when hidden
73
+ if (overlay) overlay.classList.add('hover-mode');
74
+ }
75
+
76
+ // Draw hit regions - handle both already-loaded and loading images
77
+ function initHitRegions() {
78
+ if (previewImg.complete && previewImg.naturalWidth > 0) {
79
+ console.log('Image already loaded, drawing hit regions');
80
+ drawHitRegions();
81
+ } else {
82
+ console.log('Image not loaded yet, waiting...');
83
+ setTimeout(initHitRegions, 100);
84
+ }
85
+ }
86
+ setTimeout(initHitRegions, 50);
87
+
88
+ // Initialize zoom/pan
89
+ initializeZoomPan();
90
+
91
+ // Initialize measurement overlay controls
92
+ initializeOverlayControls();
93
+
94
+ // Initialize context menus
95
+ if (typeof initializeCanvasContextMenu === 'function') initializeCanvasContextMenu();
96
+ if (typeof initializeFilesContextMenu === 'function') initializeFilesContextMenu();
97
+ });
98
+
99
+ // Theme values are passed from server via initialValues
100
+ // These come from the applied theme (SCITEX, MATPLOTLIB, etc.)
101
+ // initialValues is populated by the server from the loaded style preset
102
+
103
+ // Store original theme defaults for comparison
104
+ const themeDefaults = {...initialValues};
105
+
106
+ // Initialize form values and placeholders from applied theme
107
+ function initializeValues() {
108
+ // initialValues contains the theme's default values from the server
109
+ // These are the actual values from the applied style preset (not hardcoded)
110
+
111
+ for (const [key, value] of Object.entries(initialValues)) {
112
+ const element = document.getElementById(key);
113
+ if (element) {
114
+ if (element.type === 'checkbox') {
115
+ element.checked = Boolean(value);
116
+ } else if (element.type === 'range') {
117
+ element.value = value;
118
+ const valueSpan = document.getElementById(key + '_value');
119
+ if (valueSpan) valueSpan.textContent = value;
120
+ } else {
121
+ // Set the value
122
+ element.value = value;
123
+ // Set placeholder to show theme default (visible when field is cleared)
124
+ if (element.type === 'number' || element.type === 'text') {
125
+ element.placeholder = value;
126
+ }
127
+ }
128
+ }
129
+ }
130
+
131
+ // Log applied theme info
132
+ const styleNameEl = document.getElementById('style-name');
133
+ if (styleNameEl) {
134
+ console.log('Applied theme:', styleNameEl.textContent);
135
+ }
136
+ }
137
+
138
+ // Check if a field value differs from the theme default
139
+ function updateModifiedState(element) {
140
+ const key = element.id;
141
+ const defaultValue = themeDefaults[key];
142
+ const formRow = element.closest('.form-row');
143
+ if (!formRow || defaultValue === undefined) return;
144
+
145
+ let currentValue;
146
+ if (element.type === 'checkbox') {
147
+ currentValue = element.checked;
148
+ } else if (element.type === 'number') {
149
+ currentValue = parseFloat(element.value);
150
+ } else {
151
+ currentValue = element.value;
152
+ }
153
+
154
+ // Compare values (handle type conversion)
155
+ const isModified = String(currentValue) !== String(defaultValue);
156
+ formRow.classList.toggle('value-modified', isModified);
157
+ }
158
+
159
+ // Update all modified states
160
+ function updateAllModifiedStates() {
161
+ const inputs = document.querySelectorAll('input, select');
162
+ inputs.forEach(input => {
163
+ if (input.id && input.id !== 'dark-mode-toggle') {
164
+ updateModifiedState(input);
165
+ }
166
+ });
167
+ }
168
+
169
+ // ==================== EVENT LISTENERS ====================
170
+
171
+ // Initialize event listeners
172
+ function initializeEventListeners() {
173
+ // Preview image click for element selection
174
+ const previewImg = document.getElementById('preview-image');
175
+ if (previewImg) previewImg.addEventListener('click', handlePreviewClick);
176
+
177
+ // SVG overlay click - deselect when clicking on empty area (not on a shape)
178
+ const hitregionOverlay = document.getElementById('hitregion-overlay');
179
+ if (hitregionOverlay) {
180
+ hitregionOverlay.addEventListener('click', function(event) {
181
+ if (event.target === hitregionOverlay) clearSelection();
182
+ });
183
+ }
184
+
185
+ // Selection overlay click - same behavior
186
+ const selectionOverlay = document.getElementById('selection-overlay');
187
+ if (selectionOverlay) {
188
+ selectionOverlay.addEventListener('click', function(event) {
189
+ if (event.target === selectionOverlay) clearSelection();
190
+ });
191
+ }
192
+
193
+ // Dark mode toggle button
194
+ const darkModeToggle = document.getElementById('dark-mode-toggle');
195
+ if (darkModeToggle) {
196
+ const updateThemeIcon = (theme) => { darkModeToggle.textContent = theme === 'dark' ? '🌙' : '☀️'; };
197
+ darkModeToggle.addEventListener('click', function() {
198
+ const currentTheme = document.documentElement.getAttribute('data-theme');
199
+ const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
200
+ document.documentElement.setAttribute('data-theme', newTheme);
201
+ updateThemeIcon(newTheme);
202
+ scheduleUpdate();
203
+ });
204
+ updateThemeIcon(document.documentElement.getAttribute('data-theme'));
205
+ }
206
+
207
+ // Form inputs - auto update on change
208
+ // Exclude panel position inputs - they have their own Apply button
209
+ const panelPositionInputIds = ['panel_left', 'panel_top', 'panel_width', 'panel_height'];
210
+ const inputs = document.querySelectorAll('input, select');
211
+ inputs.forEach(input => {
212
+ if (panelPositionInputIds.includes(input.id)) return; // Skip panel position inputs
213
+
214
+ // Update modified state and trigger preview update
215
+ input.addEventListener('change', function() {
216
+ updateModifiedState(this);
217
+ scheduleUpdate();
218
+ });
219
+ if (input.type === 'number' || input.type === 'text') {
220
+ input.addEventListener('input', function() {
221
+ updateModifiedState(this);
222
+ scheduleUpdate();
223
+ });
224
+ }
225
+
226
+ // Range slider value display
227
+ if (input.type === 'range') {
228
+ input.addEventListener('input', function() {
229
+ const valueSpan = document.getElementById(this.id + '_value');
230
+ if (valueSpan) valueSpan.textContent = this.value;
231
+ updateModifiedState(this);
232
+ });
233
+ }
234
+ });
235
+
236
+ // Buttons - with null checks
237
+ const btnRefresh = document.getElementById('btn-refresh');
238
+ const btnReset = document.getElementById('btn-reset');
239
+ const btnSave = document.getElementById('btn-save');
240
+ const btnRestore = document.getElementById('btn-restore');
241
+ if (btnRefresh) btnRefresh.addEventListener('click', updatePreview);
242
+ if (btnReset) btnReset.addEventListener('click', resetValues);
243
+ if (btnSave) btnSave.addEventListener('click', saveOverrides);
244
+ if (btnRestore) btnRestore.addEventListener('click', restoreOriginal);
245
+ const hitmapBtn = document.getElementById('btn-show-hitmap');
246
+ if (hitmapBtn) hitmapBtn.addEventListener('click', toggleHitmapOverlay);
247
+
248
+ // Download dropdown, label inputs, and captions
249
+ initializeDownloadDropdown();
250
+ initializeLabelInputs();
251
+ if (typeof initializeCaptionInputs === 'function') initializeCaptionInputs();
252
+
253
+ // View mode toggle buttons (legacy)
254
+ const btnAll = document.getElementById('btn-show-all');
255
+ const btnSelected = document.getElementById('btn-show-selected');
256
+ if (btnAll) btnAll.addEventListener('click', () => setViewMode('all'));
257
+ if (btnSelected) btnSelected.addEventListener('click', () => setViewMode('selected'));
258
+
259
+ // Tab navigation
260
+ const tabFigure = document.getElementById('tab-figure');
261
+ const tabAxis = document.getElementById('tab-axis');
262
+ const tabElement = document.getElementById('tab-element');
263
+ if (tabFigure) tabFigure.addEventListener('click', () => switchTab('figure'));
264
+ if (tabAxis) tabAxis.addEventListener('click', () => switchTab('axis'));
265
+ if (tabElement) tabElement.addEventListener('click', () => switchTab('element'));
266
+
267
+ // Theme modal handlers
268
+ initializeThemeModal();
269
+ initializeShortcutsModal();
270
+
271
+ // Check initial override status
272
+ checkOverrideStatus();
273
+
274
+ // Check modified states after initial values are set
275
+ setTimeout(updateAllModifiedStates, 100);
276
+
277
+ // Keyboard shortcuts
278
+ document.addEventListener('keydown', handleKeyboardShortcuts);
279
+ }
280
+
281
+ // Handle keyboard shortcuts
282
+ function handleKeyboardShortcuts(event) {
283
+ // Ignore shortcuts when typing in input fields
284
+ const activeElement = document.activeElement;
285
+ const isInputField = activeElement.tagName === 'INPUT' ||
286
+ activeElement.tagName === 'TEXTAREA' ||
287
+ activeElement.tagName === 'SELECT';
288
+
289
+ // Ctrl+Alt+I: Debug snapshot (screenshot + console logs)
290
+ if (event.ctrlKey && event.altKey && (event.key === 'i' || event.key === 'I')) {
291
+ event.preventDefault();
292
+ event.stopPropagation();
293
+ console.log('[DEBUG] Ctrl+Alt+I pressed, calling captureDebugSnapshot');
294
+ if (typeof captureDebugSnapshot === 'function') {
295
+ captureDebugSnapshot();
296
+ } else {
297
+ console.error('[DEBUG] captureDebugSnapshot is not defined!');
298
+ showToast('Debug snapshot not available', 'error');
299
+ }
300
+ return;
301
+ }
302
+
303
+ // Alt+I (without Ctrl): Element Inspector toggle (DEBUG MODE ONLY)
304
+ if (DEBUG_MODE && event.altKey && !event.ctrlKey && !event.shiftKey && (event.key === 'i' || event.key === 'I')) {
305
+ event.preventDefault();
306
+ event.stopPropagation();
307
+ if (typeof toggleElementInspector === 'function') {
308
+ toggleElementInspector();
309
+ }
310
+ return;
311
+ }
312
+
313
+ // Alt+B: Show All Bboxes toggle (DEBUG MODE ONLY)
314
+ if (DEBUG_MODE && event.altKey && !event.ctrlKey && !event.shiftKey && (event.key === 'b' || event.key === 'B')) {
315
+ event.preventDefault();
316
+ event.stopPropagation();
317
+ if (typeof toggleAllBboxes === 'function') {
318
+ toggleAllBboxes();
319
+ }
320
+ return;
321
+ }
322
+
323
+ // Ctrl+S: Save overrides
324
+ if (event.ctrlKey && event.key === 's') {
325
+ event.preventDefault();
326
+ saveOverrides();
327
+ showToast('Saved!', 'success');
328
+ return;
329
+ }
330
+
331
+ // Ctrl+N: New blank figure
332
+ if (event.ctrlKey && event.key === 'n') {
333
+ event.preventDefault();
334
+ if (typeof createNewFigure === 'function') {
335
+ createNewFigure();
336
+ }
337
+ return;
338
+ }
339
+
340
+ // Ctrl+Z: Undo
341
+ if (event.ctrlKey && !event.shiftKey && event.key === 'z') {
342
+ event.preventDefault();
343
+ if (typeof undo === 'function') {
344
+ undo();
345
+ }
346
+ return;
347
+ }
348
+
349
+ // Ctrl+Shift+Z or Ctrl+Y: Redo
350
+ if ((event.ctrlKey && event.shiftKey && event.key === 'Z') ||
351
+ (event.ctrlKey && event.key === 'y')) {
352
+ event.preventDefault();
353
+ if (typeof redo === 'function') {
354
+ redo();
355
+ }
356
+ return;
357
+ }
358
+
359
+ // Ctrl+Shift+S: Download PNG
360
+ if (event.ctrlKey && event.shiftKey && event.key === 'S') {
361
+ event.preventDefault();
362
+ downloadFigure('png');
363
+ return;
364
+ }
365
+
366
+ // F5 or Ctrl+R: Render preview
367
+ if (event.key === 'F5' || (event.ctrlKey && event.key === 'r')) {
368
+ event.preventDefault();
369
+ updatePreview();
370
+ showToast('Rendered', 'info');
371
+ return;
372
+ }
373
+
374
+ // Only handle the following shortcuts if not in an input field
375
+ if (isInputField) return;
376
+
377
+ // Escape: Close modals or clear selection
378
+ if (event.key === 'Escape') {
379
+ const shortcutsModal = document.getElementById('shortcuts-modal');
380
+ if (shortcutsModal && shortcutsModal.style.display === 'flex') {
381
+ hideShortcutsModal();
382
+ return;
383
+ }
384
+ clearSelection();
385
+ return;
386
+ }
387
+
388
+ // Tab navigation: 1, 2, 3 keys
389
+ if (event.key === '1') {
390
+ switchTab('figure');
391
+ return;
392
+ }
393
+ if (event.key === '2') {
394
+ switchTab('axis');
395
+ return;
396
+ }
397
+ if (event.key === '3') {
398
+ switchTab('element');
399
+ return;
400
+ }
401
+
402
+ // R: Render (re-render figure)
403
+ if (event.key === 'r' || event.key === 'R') {
404
+ updatePreview();
405
+ showToast('Rendered', 'info');
406
+ return;
407
+ }
408
+
409
+ // G: Toggle rulers and grid
410
+ if (event.key === 'g' || event.key === 'G') {
411
+ toggleRulerGrid();
412
+ const state = rulerGridVisible ? 'ON' : 'OFF';
413
+ showToast(`Ruler & Grid: ${state}`, 'info');
414
+ return;
415
+ }
416
+
417
+ // ?: Show keyboard shortcuts
418
+ if (event.key === '?') {
419
+ showShortcutsModal();
420
+ return;
421
+ }
422
+ }
423
+
424
+ // ==================== UTILITY FUNCTIONS ====================
425
+
426
+ // Show toast notification
427
+ function showToast(message, type = 'info') {
428
+ // Remove existing toast if any
429
+ const existingToast = document.querySelector('.toast-notification');
430
+ if (existingToast) {
431
+ existingToast.remove();
432
+ }
433
+
434
+ // Create toast element
435
+ const toast = document.createElement('div');
436
+ toast.className = 'toast-notification toast-' + type;
437
+ toast.textContent = message;
438
+
439
+ // Style the toast
440
+ Object.assign(toast.style, {
441
+ position: 'fixed',
442
+ bottom: '20px',
443
+ left: '50%',
444
+ transform: 'translateX(-50%)',
445
+ padding: '10px 20px',
446
+ borderRadius: '4px',
447
+ color: 'white',
448
+ fontWeight: '500',
449
+ zIndex: '10000',
450
+ opacity: '0',
451
+ transition: 'opacity 0.3s ease',
452
+ boxShadow: '0 2px 8px rgba(0,0,0,0.3)'
453
+ });
454
+
455
+ // Set background color based on type
456
+ const colors = {
457
+ success: '#4CAF50',
458
+ info: '#2196F3',
459
+ warning: '#ff9800',
460
+ error: '#f44336'
461
+ };
462
+ toast.style.backgroundColor = colors[type] || colors.info;
463
+
464
+ document.body.appendChild(toast);
465
+
466
+ // Fade in
467
+ requestAnimationFrame(() => {
468
+ toast.style.opacity = '1';
469
+ });
470
+
471
+ // Remove after delay
472
+ setTimeout(() => {
473
+ toast.style.opacity = '0';
474
+ setTimeout(() => toast.remove(), 300);
475
+ }, 2000);
476
+ }
477
+
478
+ // Debounce utility
479
+ function debounce(func, wait) {
480
+ let timeout;
481
+ return function(...args) {
482
+ clearTimeout(timeout);
483
+ timeout = setTimeout(() => func.apply(this, args), wait);
484
+ };
485
+ }
486
+
487
+ // Note: scheduleUpdate() is defined in _api.py to avoid duplication
488
+ // It calls updatePreview() with debounce, which properly includes dark_mode
489
+
490
+ // ==================== END CORE ====================
491
+ """
492
+
493
+ __all__ = ["SCRIPTS_CORE"]
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Datatable JavaScript modules - orchestrator.
4
+
5
+ Combines all datatable JavaScript modules:
6
+ - _core.py: State, panel toggle, initialization
7
+ - _tabs.py: Multi-tab management for multiple datasets
8
+ - _import.py: Drag-drop, file parsing
9
+ - _table.py: Table rendering with smart truncation
10
+ - _selection.py: Multi-cell selection and highlights
11
+ - _cell_edit.py: Inline cell editing
12
+ - _clipboard.py: Copy, paste, cut operations
13
+ - _plot.py: Plot type hints, variable assignment, plotting
14
+ - _editable.py: Create and edit tables manually
15
+ """
16
+
17
+ from ._cell_edit import JS_DATATABLE_CELL_EDIT
18
+ from ._clipboard import JS_DATATABLE_CLIPBOARD
19
+ from ._context_menu import JS_DATATABLE_CONTEXT_MENU
20
+ from ._core import JS_DATATABLE_CORE
21
+ from ._editable import JS_DATATABLE_EDITABLE
22
+ from ._import import JS_DATATABLE_IMPORT
23
+ from ._plot import get_js_datatable_plot
24
+ from ._selection import JS_DATATABLE_SELECTION
25
+ from ._table import JS_DATATABLE_TABLE
26
+ from ._tabs import JS_DATATABLE_TABS
27
+
28
+
29
+ def get_scripts_datatable() -> str:
30
+ """Generate combined datatable JavaScript."""
31
+ return (
32
+ JS_DATATABLE_CORE
33
+ + "\n"
34
+ + JS_DATATABLE_TABS
35
+ + "\n"
36
+ + JS_DATATABLE_IMPORT
37
+ + "\n"
38
+ + JS_DATATABLE_TABLE
39
+ + "\n"
40
+ + JS_DATATABLE_SELECTION
41
+ + "\n"
42
+ + JS_DATATABLE_CELL_EDIT
43
+ + "\n"
44
+ + JS_DATATABLE_CLIPBOARD
45
+ + "\n"
46
+ + JS_DATATABLE_CONTEXT_MENU
47
+ + "\n"
48
+ + JS_DATATABLE_EDITABLE
49
+ + "\n"
50
+ + get_js_datatable_plot()
51
+ )
52
+
53
+
54
+ # For backward compatibility
55
+ SCRIPTS_DATATABLE = get_scripts_datatable()
56
+
57
+ __all__ = ["SCRIPTS_DATATABLE", "get_scripts_datatable"]
58
+
59
+ # EOF
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Datatable inline cell editing JavaScript."""
4
+
5
+ JS_DATATABLE_CELL_EDIT = """
6
+ // ============================================================================
7
+ // Cell Editing (Inline Edit Mode)
8
+ // ============================================================================
9
+ let datatableEditingCell = null; // Track which cell is being edited
10
+ let datatableSkipBlur = false; // Flag to skip blur during Tab/Enter navigation
11
+
12
+ function enterCellEditMode(cell) {
13
+ if (datatableEditMode) return;
14
+
15
+ datatableEditMode = true;
16
+ datatableEditingCell = cell;
17
+ cell.classList.add('cell-editing');
18
+
19
+ const span = cell.querySelector('.cell-text');
20
+ const originalValue = span ? span.textContent : '';
21
+
22
+ // Replace span with input
23
+ cell.innerHTML = `<input type="text" class="cell-edit-input" value="${originalValue}">`;
24
+ const input = cell.querySelector('input');
25
+ input.focus();
26
+ input.select();
27
+
28
+ // Handle input events
29
+ input.addEventListener('blur', (e) => {
30
+ // Skip if Tab/Enter is handling navigation, or if this isn't the editing cell
31
+ if (datatableSkipBlur || datatableEditingCell !== cell) {
32
+ return;
33
+ }
34
+ if (datatableEditMode && datatableEditingCell === cell) {
35
+ exitCellEditMode(cell, input.value, false);
36
+ }
37
+ });
38
+ input.addEventListener('keydown', (e) => {
39
+ if (e.key === 'Enter') {
40
+ e.preventDefault();
41
+ datatableSkipBlur = true; // Prevent blur from interfering
42
+ exitCellEditMode(cell, input.value, true);
43
+ navigateWithTabEnterAndEdit('enter', e.shiftKey);
44
+ datatableSkipBlur = false;
45
+ } else if (e.key === 'Escape') {
46
+ e.preventDefault();
47
+ exitCellEditMode(cell, originalValue, false);
48
+ } else if (e.key === 'Tab') {
49
+ e.preventDefault();
50
+ datatableSkipBlur = true; // Prevent blur from interfering
51
+ exitCellEditMode(cell, input.value, true);
52
+ navigateWithTabEnterAndEdit('tab', e.shiftKey);
53
+ datatableSkipBlur = false;
54
+ }
55
+ });
56
+ }
57
+
58
+ function exitCellEditMode(cell, value, continueEditing = false) {
59
+ if (!datatableEditMode) return;
60
+
61
+ const row = parseInt(cell.dataset.row);
62
+ const col = parseInt(cell.dataset.col);
63
+
64
+ // Update data - preserve empty values as empty (not 0)
65
+ if (datatableData && datatableData.rows[row]) {
66
+ if (value === '' || value === null || value === undefined) {
67
+ datatableData.rows[row][col] = '';
68
+ } else {
69
+ const colType = datatableData.columns[col]?.type;
70
+ if (colType === 'numeric') {
71
+ const num = parseFloat(value);
72
+ datatableData.rows[row][col] = isNaN(num) ? value : num;
73
+ } else {
74
+ datatableData.rows[row][col] = value;
75
+ }
76
+ }
77
+ }
78
+
79
+ // Restore cell display with span wrapper
80
+ const displayValue = value === null || value === undefined ? '' : value;
81
+ cell.innerHTML = `<span class="cell-text">${displayValue}</span>`;
82
+ cell.classList.remove('cell-editing');
83
+ cell.setAttribute('title', displayValue);
84
+
85
+ datatableEditMode = false;
86
+ datatableEditingCell = null; // Clear editing cell reference
87
+
88
+ // Only focus this cell if we're NOT continuing to next cell
89
+ if (!continueEditing) {
90
+ cell.focus();
91
+ }
92
+ }
93
+ """
94
+
95
+ __all__ = ["JS_DATATABLE_CELL_EDIT"]
96
+
97
+ # EOF