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,348 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Undo/Redo functionality for the figure editor.
4
+
5
+ This module provides a history stack for tracking changes and
6
+ enabling undo/redo operations with Ctrl+Z and Ctrl+Shift+Z.
7
+ """
8
+
9
+ SCRIPTS_UNDO_REDO = """
10
+ // ==================== UNDO/REDO HISTORY ====================
11
+
12
+ // History state
13
+ const historyStack = [];
14
+ const redoStack = [];
15
+ const MAX_HISTORY = 50; // Maximum number of undo steps
16
+ let isUndoRedoInProgress = false; // Prevent recursive history recording
17
+
18
+ // Capture current state as a snapshot
19
+ function captureState() {
20
+ const state = {
21
+ overrides: collectOverrides(),
22
+ panelPositions: typeof panelPositions !== 'undefined' ? JSON.parse(JSON.stringify(panelPositions)) : {},
23
+ annotationPositions: typeof annotationPositions !== 'undefined' ? JSON.parse(JSON.stringify(annotationPositions)) : {},
24
+ timestamp: Date.now()
25
+ };
26
+ return state;
27
+ }
28
+
29
+ // Compare two states for equality
30
+ function statesEqual(a, b) {
31
+ return JSON.stringify(a.overrides) === JSON.stringify(b.overrides) &&
32
+ JSON.stringify(a.panelPositions) === JSON.stringify(b.panelPositions) &&
33
+ JSON.stringify(a.annotationPositions) === JSON.stringify(b.annotationPositions);
34
+ }
35
+
36
+ // Push current state to history (call before making changes)
37
+ function pushToHistory() {
38
+ if (isUndoRedoInProgress) return;
39
+
40
+ const state = captureState();
41
+
42
+ // Don't push if identical to last state
43
+ if (historyStack.length > 0) {
44
+ const lastState = historyStack[historyStack.length - 1];
45
+ if (statesEqual(lastState, state)) {
46
+ return;
47
+ }
48
+ }
49
+
50
+ historyStack.push(state);
51
+
52
+ // Clear redo stack when new action is performed
53
+ redoStack.length = 0;
54
+
55
+ // Trim history if too long
56
+ while (historyStack.length > MAX_HISTORY) {
57
+ historyStack.shift();
58
+ }
59
+
60
+ updateUndoRedoButtons();
61
+ console.log('[History] Pushed state, stack size:', historyStack.length);
62
+ }
63
+
64
+ // Apply a state snapshot to the form
65
+ async function applyState(state) {
66
+ isUndoRedoInProgress = true;
67
+
68
+ try {
69
+ const overrides = state.overrides;
70
+
71
+ for (const [key, value] of Object.entries(overrides)) {
72
+ const element = document.getElementById(key);
73
+ if (!element) continue;
74
+
75
+ if (element.type === 'checkbox') {
76
+ element.checked = Boolean(value);
77
+ } else if (element.type === 'range') {
78
+ element.value = value;
79
+ const valueSpan = document.getElementById(key + '_value');
80
+ if (valueSpan) valueSpan.textContent = value;
81
+ } else if (element.type === 'color') {
82
+ element.value = value;
83
+ } else if (element.tagName === 'SELECT') {
84
+ element.value = value;
85
+ } else {
86
+ element.value = value;
87
+ }
88
+ }
89
+
90
+ // Restore panel positions if they differ
91
+ if (state.panelPositions && typeof panelPositions !== 'undefined') {
92
+ const axKeys = Object.keys(state.panelPositions).sort();
93
+ for (let i = 0; i < axKeys.length; i++) {
94
+ const axKey = axKeys[i];
95
+ const savedPos = state.panelPositions[axKey];
96
+ const currentPos = panelPositions[axKey];
97
+
98
+ // Check if position changed
99
+ if (currentPos && savedPos &&
100
+ (Math.abs(savedPos.left - currentPos.left) > 0.1 ||
101
+ Math.abs(savedPos.top - currentPos.top) > 0.1)) {
102
+ // Restore panel position via API
103
+ try {
104
+ await fetch('/update_axes_position', {
105
+ method: 'POST',
106
+ headers: { 'Content-Type': 'application/json' },
107
+ body: JSON.stringify({
108
+ ax_index: i,
109
+ left: savedPos.left,
110
+ top: savedPos.top,
111
+ width: savedPos.width,
112
+ height: savedPos.height
113
+ })
114
+ });
115
+ // Update local panelPositions to match restored state
116
+ panelPositions[axKey] = { ...savedPos };
117
+ } catch (e) {
118
+ console.error('[History] Failed to restore panel position:', e);
119
+ }
120
+ }
121
+ }
122
+ }
123
+
124
+ // Restore annotation positions if they differ
125
+ if (state.annotationPositions && typeof annotationPositions !== 'undefined') {
126
+ let needsRefresh = false;
127
+ for (const [key, savedPos] of Object.entries(state.annotationPositions)) {
128
+ const currentPos = annotationPositions[key];
129
+
130
+ // Check if position changed
131
+ if (!currentPos ||
132
+ Math.abs(savedPos.x - (currentPos?.x || 0)) > 0.001 ||
133
+ Math.abs(savedPos.y - (currentPos?.y || 0)) > 0.001) {
134
+
135
+ // Parse key formats:
136
+ // "ax0_panel_label" -> axIndex=0, type=panel_label, textIndex=0
137
+ // "ax0_text_0" -> axIndex=0, type=text, textIndex=0
138
+ let axIndex, annotationType, textIndex;
139
+
140
+ if (key.includes('_panel_label')) {
141
+ const match = key.match(/ax(\\d+)_panel_label/);
142
+ if (match) {
143
+ axIndex = parseInt(match[1], 10);
144
+ annotationType = 'panel_label';
145
+ textIndex = 0;
146
+ }
147
+ } else if (key.includes('_text_')) {
148
+ const match = key.match(/ax(\\d+)_text_(\\d+)/);
149
+ if (match) {
150
+ axIndex = parseInt(match[1], 10);
151
+ annotationType = 'text';
152
+ textIndex = parseInt(match[2], 10);
153
+ }
154
+ }
155
+
156
+ if (axIndex !== undefined) {
157
+ try {
158
+ const response = await fetch('/update_annotation_position', {
159
+ method: 'POST',
160
+ headers: { 'Content-Type': 'application/json' },
161
+ body: JSON.stringify({
162
+ ax_index: axIndex,
163
+ annotation_type: annotationType,
164
+ text_index: textIndex,
165
+ x: savedPos.x,
166
+ y: savedPos.y
167
+ })
168
+ });
169
+ const data = await response.json();
170
+
171
+ if (data.success && data.image) {
172
+ // Update preview image
173
+ const img = document.getElementById('preview-image');
174
+ if (img) {
175
+ img.src = 'data:image/png;base64,' + data.image;
176
+ }
177
+ // Update bboxes
178
+ if (data.bboxes && typeof currentBboxes !== 'undefined') {
179
+ currentBboxes = data.bboxes;
180
+ }
181
+ needsRefresh = true;
182
+ }
183
+
184
+ // Update local annotationPositions to match restored state
185
+ annotationPositions[key] = { ...savedPos };
186
+ console.log('[History] Restored annotation position:', key);
187
+ } catch (e) {
188
+ console.error('[History] Failed to restore annotation position:', e);
189
+ }
190
+ }
191
+ }
192
+ }
193
+
194
+ // Refresh hitmap if positions were restored
195
+ if (needsRefresh && typeof loadHitmap === 'function') {
196
+ loadHitmap();
197
+ if (typeof updateHitRegions === 'function') {
198
+ updateHitRegions();
199
+ }
200
+ if (typeof initAnnotationPositions === 'function') {
201
+ initAnnotationPositions();
202
+ }
203
+ }
204
+ }
205
+
206
+ // Update preview with the restored state
207
+ updatePreview();
208
+
209
+ } finally {
210
+ isUndoRedoInProgress = false;
211
+ }
212
+ }
213
+
214
+ // Undo last action
215
+ async function undo() {
216
+ if (historyStack.length === 0) {
217
+ showToast('Nothing to undo', 'info');
218
+ return;
219
+ }
220
+
221
+ // Save current state to redo stack
222
+ const currentState = captureState();
223
+ redoStack.push(currentState);
224
+
225
+ // Pop and apply previous state
226
+ const previousState = historyStack.pop();
227
+ await applyState(previousState);
228
+
229
+ updateUndoRedoButtons();
230
+ showToast('Undo', 'info');
231
+ console.log('[History] Undo, remaining:', historyStack.length);
232
+ }
233
+
234
+ // Redo last undone action
235
+ async function redo() {
236
+ if (redoStack.length === 0) {
237
+ showToast('Nothing to redo', 'info');
238
+ return;
239
+ }
240
+
241
+ // Save current state to history
242
+ const currentState = captureState();
243
+ historyStack.push(currentState);
244
+
245
+ // Pop and apply redo state
246
+ const redoState = redoStack.pop();
247
+ await applyState(redoState);
248
+
249
+ updateUndoRedoButtons();
250
+ showToast('Redo', 'info');
251
+ console.log('[History] Redo, remaining redo:', redoStack.length);
252
+ }
253
+
254
+ // Update undo/redo button states
255
+ function updateUndoRedoButtons() {
256
+ const undoBtn = document.getElementById('btn-undo');
257
+ const redoBtn = document.getElementById('btn-redo');
258
+
259
+ if (undoBtn) {
260
+ undoBtn.disabled = historyStack.length === 0;
261
+ undoBtn.title = historyStack.length > 0
262
+ ? `Undo (${historyStack.length} steps available)`
263
+ : 'Nothing to undo';
264
+ }
265
+
266
+ if (redoBtn) {
267
+ redoBtn.disabled = redoStack.length === 0;
268
+ redoBtn.title = redoStack.length > 0
269
+ ? `Redo (${redoStack.length} steps available)`
270
+ : 'Nothing to redo';
271
+ }
272
+ }
273
+
274
+ // Clear all history (e.g., when switching files)
275
+ function clearHistory() {
276
+ historyStack.length = 0;
277
+ redoStack.length = 0;
278
+ updateUndoRedoButtons();
279
+ console.log('[History] Cleared');
280
+ }
281
+
282
+ // Hook into form changes to record history
283
+ function initUndoRedo() {
284
+ // Capture initial state
285
+ pushToHistory();
286
+
287
+ // Add change listeners to all form inputs
288
+ const inputs = document.querySelectorAll('input, select');
289
+ inputs.forEach(input => {
290
+ if (input.id === 'dark-mode-toggle') return;
291
+ if (!input.id) return;
292
+
293
+ // Capture state before change
294
+ input.addEventListener('focus', () => {
295
+ pushToHistory();
296
+ });
297
+
298
+ // For inputs without focus events (like range sliders)
299
+ if (input.type === 'range') {
300
+ let rangeStartValue = null;
301
+ input.addEventListener('mousedown', () => {
302
+ rangeStartValue = input.value;
303
+ pushToHistory();
304
+ });
305
+ }
306
+
307
+ // For select elements
308
+ if (input.tagName === 'SELECT') {
309
+ input.addEventListener('mousedown', () => {
310
+ pushToHistory();
311
+ });
312
+ }
313
+ });
314
+
315
+ // Initialize button states
316
+ updateUndoRedoButtons();
317
+
318
+ console.log('[History] Undo/Redo initialized');
319
+ }
320
+
321
+ // Initialize when DOM is ready
322
+ if (document.readyState === 'loading') {
323
+ document.addEventListener('DOMContentLoaded', initUndoRedo);
324
+ } else {
325
+ // Small delay to ensure other scripts have initialized
326
+ setTimeout(initUndoRedo, 100);
327
+ }
328
+
329
+ // Add button click handlers after DOM is ready
330
+ document.addEventListener('DOMContentLoaded', function() {
331
+ const undoBtn = document.getElementById('btn-undo');
332
+ const redoBtn = document.getElementById('btn-redo');
333
+
334
+ if (undoBtn) {
335
+ undoBtn.addEventListener('click', undo);
336
+ }
337
+
338
+ if (redoBtn) {
339
+ redoBtn.addEventListener('click', redo);
340
+ }
341
+ });
342
+
343
+ console.log('[UndoRedo] Module loaded - Ctrl+Z to undo, Ctrl+Shift+Z to redo');
344
+ """
345
+
346
+ __all__ = ["SCRIPTS_UNDO_REDO"]
347
+
348
+ # EOF
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """View mode JavaScript for the figure editor.
4
+
5
+ This module contains the JavaScript code for:
6
+ - View mode management (all/selected)
7
+ - Property filtering by element type
8
+ - Section visibility control
9
+ """
10
+
11
+ SCRIPTS_VIEW_MODE = """
12
+ // ===== VIEW MODE MANAGEMENT =====
13
+ // Note: viewMode variable is declared in _core.py
14
+
15
+ // Set view mode (all or selected)
16
+ function setViewMode(mode) {
17
+ viewMode = mode;
18
+
19
+ // Update toggle buttons (legacy)
20
+ const btnAll = document.getElementById('btn-show-all');
21
+ const btnSelected = document.getElementById('btn-show-selected');
22
+ if (btnAll) btnAll.classList.toggle('active', mode === 'all');
23
+ if (btnSelected) btnSelected.classList.toggle('active', mode === 'selected');
24
+
25
+ // Update controls sections class
26
+ const controlsSections = document.querySelector('.controls-sections');
27
+ controlsSections.classList.toggle('filter-mode', mode === 'selected');
28
+
29
+ // Update selection hint
30
+ const hint = document.getElementById('selection-hint');
31
+ if (mode === 'selected') {
32
+ if (selectedElement) {
33
+ hint.textContent = `Showing: ${selectedElement.type}`;
34
+ hint.style.color = 'var(--accent-color)';
35
+ // Hide all style sections - only show call properties
36
+ hideAllStyleSections();
37
+ } else {
38
+ hint.textContent = '';
39
+ hint.style.color = '';
40
+ // Show all when no selection in filter mode
41
+ showAllProperties();
42
+ }
43
+ } else {
44
+ hint.textContent = '';
45
+ showAllProperties();
46
+ }
47
+ }
48
+
49
+ // Hide all style sections (for Selected mode - only show call properties)
50
+ function hideAllStyleSections() {
51
+ const sections = document.querySelectorAll('.section[data-element-types]');
52
+ sections.forEach(section => {
53
+ section.classList.add('section-hidden');
54
+ section.classList.remove('section-visible');
55
+ });
56
+ }
57
+
58
+ // Filter properties by element type
59
+ function filterPropertiesByElementType(elementType) {
60
+ const sections = document.querySelectorAll('.section[data-element-types]');
61
+
62
+ sections.forEach(section => {
63
+ const types = section.getAttribute('data-element-types').split(',');
64
+ const isGlobal = types.includes('global');
65
+ const matches = isGlobal || types.includes(elementType);
66
+
67
+ section.classList.toggle('section-hidden', !matches);
68
+ section.classList.toggle('section-visible', matches);
69
+
70
+ // If section matches, filter individual form-rows within it
71
+ if (matches && !isGlobal) {
72
+ const formRows = section.querySelectorAll('.form-row[data-element-types]');
73
+ formRows.forEach(row => {
74
+ const rowTypes = row.getAttribute('data-element-types').split(',');
75
+ const rowMatches = rowTypes.includes(elementType);
76
+ row.classList.toggle('field-hidden', !rowMatches);
77
+ });
78
+
79
+ // Open matching sections
80
+ section.setAttribute('open', '');
81
+ }
82
+ });
83
+
84
+ // Update hint
85
+ const hint = document.getElementById('selection-hint');
86
+ hint.textContent = `Showing: ${elementType}`;
87
+ hint.style.color = 'var(--accent-color)';
88
+ }
89
+
90
+ // Show all properties (remove filtering)
91
+ function showAllProperties() {
92
+ const sections = document.querySelectorAll('.section[data-element-types]');
93
+
94
+ sections.forEach(section => {
95
+ section.classList.remove('section-hidden', 'section-visible');
96
+
97
+ const formRows = section.querySelectorAll('.form-row[data-element-types]');
98
+ formRows.forEach(row => {
99
+ row.classList.remove('field-hidden');
100
+ });
101
+ });
102
+ }
103
+ """
104
+
105
+ __all__ = ["SCRIPTS_VIEW_MODE"]
106
+
107
+ # EOF
@@ -0,0 +1,212 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Zoom and pan JavaScript for the figure editor."""
4
+
5
+ SCRIPTS_ZOOM = """
6
+ // ==================== ZOOM/PAN FUNCTIONS ====================
7
+
8
+ function initializeZoomPan() {
9
+ const wrapper = document.getElementById('preview-wrapper');
10
+ const container = document.getElementById('zoom-container');
11
+
12
+ if (!wrapper || !container) return;
13
+
14
+ // Zoom dropdown
15
+ const zoomSelect = document.getElementById('zoom-select');
16
+ zoomSelect?.addEventListener('change', (e) => {
17
+ setZoom(parseInt(e.target.value) / 100);
18
+ });
19
+
20
+ // Fit button
21
+ document.getElementById('btn-zoom-fit')?.addEventListener('click', zoomToFit);
22
+
23
+ // Mouse wheel zoom
24
+ wrapper.addEventListener('wheel', (e) => {
25
+ if (e.ctrlKey || e.metaKey) {
26
+ e.preventDefault();
27
+ const delta = e.deltaY > 0 ? -ZOOM_STEP : ZOOM_STEP;
28
+ setZoom(zoomLevel + delta);
29
+ }
30
+ }, { passive: false });
31
+
32
+ // Pan with middle mouse, alt+drag, or left-click on empty area when zoomed
33
+ wrapper.addEventListener('mousedown', (e) => {
34
+ // Middle mouse or Alt+drag always pans
35
+ if (e.button === 1 || (e.button === 0 && e.altKey)) {
36
+ e.preventDefault();
37
+ startPan(e);
38
+ return;
39
+ }
40
+ // Left-click when zoomed > 100% and clicking on background (not on elements)
41
+ if (e.button === 0 && zoomLevel > 1.0) {
42
+ const target = e.target;
43
+ // Only pan if clicking on wrapper/container background, not on canvas elements
44
+ if (target.id === 'preview-wrapper' || target.classList.contains('zoom-container') ||
45
+ target.tagName === 'svg' || target.id === 'preview-image') {
46
+ // Don't pan if clicking on hitmap regions (they have data attributes)
47
+ const hitRegion = document.elementFromPoint(e.clientX, e.clientY);
48
+ if (!hitRegion || !hitRegion.closest('.hit-region')) {
49
+ e.preventDefault();
50
+ startPan(e);
51
+ }
52
+ }
53
+ }
54
+ });
55
+
56
+ wrapper.addEventListener('mousemove', (e) => {
57
+ if (isPanning) {
58
+ doPan(e);
59
+ }
60
+ });
61
+
62
+ wrapper.addEventListener('mouseup', endPan);
63
+ wrapper.addEventListener('mouseleave', endPan);
64
+
65
+ // Keyboard shortcuts for zoom
66
+ document.addEventListener('keydown', (e) => {
67
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
68
+
69
+ if (e.key === '+' || e.key === '=') {
70
+ e.preventDefault();
71
+ setZoom(zoomLevel + ZOOM_STEP);
72
+ } else if (e.key === '-' || e.key === '_') {
73
+ e.preventDefault();
74
+ setZoom(zoomLevel - ZOOM_STEP);
75
+ } else if (e.key === '0') {
76
+ e.preventDefault();
77
+ setZoom(1.0);
78
+ } else if (e.key === 'f' || e.key === 'F') {
79
+ e.preventDefault();
80
+ zoomToFit();
81
+ }
82
+ });
83
+
84
+ // Initialize fit to view
85
+ setTimeout(zoomToFit, 200);
86
+ }
87
+
88
+ function setZoom(newLevel) {
89
+ zoomLevel = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, newLevel));
90
+
91
+ const container = document.getElementById('zoom-container');
92
+ const wrapper = document.getElementById('preview-wrapper');
93
+ const img = document.getElementById('preview-image');
94
+
95
+ if (container && wrapper) {
96
+ container.style.transform = `scale(${zoomLevel})`;
97
+
98
+ // Update container size to enable proper scrolling
99
+ // Transform scale doesn't change layout size, so we set explicit dimensions
100
+ if (img) {
101
+ // Use rendered dimensions if naturalWidth not available
102
+ const imgWidth = img.naturalWidth || img.width || img.clientWidth;
103
+ const imgHeight = img.naturalHeight || img.height || img.clientHeight;
104
+
105
+ if (imgWidth && imgHeight) {
106
+ const scaledWidth = imgWidth * zoomLevel;
107
+ const scaledHeight = imgHeight * zoomLevel;
108
+
109
+ // Set container dimensions for scroll area calculation
110
+ container.style.width = `${imgWidth}px`;
111
+ container.style.height = `${imgHeight}px`;
112
+ container.style.minWidth = `${scaledWidth}px`;
113
+ container.style.minHeight = `${scaledHeight}px`;
114
+ }
115
+ }
116
+
117
+ // Update wrapper class for cursor hint
118
+ if (zoomLevel > 1.0) {
119
+ wrapper.classList.add('zoomed-in');
120
+ } else {
121
+ wrapper.classList.remove('zoomed-in');
122
+ // Reset scroll position when not zoomed
123
+ wrapper.scrollLeft = 0;
124
+ wrapper.scrollTop = 0;
125
+ }
126
+ }
127
+
128
+ // Update zoom dropdown to nearest value
129
+ const zoomSelect = document.getElementById('zoom-select');
130
+ if (zoomSelect) {
131
+ const percent = Math.round(zoomLevel * 100);
132
+ // Find closest option
133
+ const options = Array.from(zoomSelect.options).map(o => parseInt(o.value));
134
+ const closest = options.reduce((prev, curr) =>
135
+ Math.abs(curr - percent) < Math.abs(prev - percent) ? curr : prev
136
+ );
137
+ zoomSelect.value = closest;
138
+ }
139
+ }
140
+
141
+ function zoomToFit() {
142
+ const wrapper = document.getElementById('preview-wrapper');
143
+ const img = document.getElementById('preview-image');
144
+
145
+ if (!wrapper || !img || !img.naturalWidth) return;
146
+
147
+ const wrapperRect = wrapper.getBoundingClientRect();
148
+ const padding = 40;
149
+
150
+ const scaleX = (wrapperRect.width - padding) / img.naturalWidth;
151
+ const scaleY = (wrapperRect.height - padding) / img.naturalHeight;
152
+
153
+ setZoom(Math.min(scaleX, scaleY, 1.0));
154
+ }
155
+
156
+ // Find nearest scrollable parent element
157
+ function findScrollableParent(element) {
158
+ while (element && element !== document.body) {
159
+ const style = window.getComputedStyle(element);
160
+ const overflowY = style.overflowY;
161
+ const overflowX = style.overflowX;
162
+ const isScrollable = (overflowY === 'auto' || overflowY === 'scroll' ||
163
+ overflowX === 'auto' || overflowX === 'scroll');
164
+ const canScroll = element.scrollHeight > element.clientHeight ||
165
+ element.scrollWidth > element.clientWidth;
166
+ if (isScrollable && canScroll) {
167
+ return element;
168
+ }
169
+ element = element.parentElement;
170
+ }
171
+ return null;
172
+ }
173
+
174
+ function startPan(e) {
175
+ // Find scrollable container under mouse
176
+ panTarget = findScrollableParent(e.target);
177
+ if (!panTarget) {
178
+ // Fallback to preview-wrapper for canvas
179
+ panTarget = document.getElementById('preview-wrapper');
180
+ }
181
+ if (!panTarget) return;
182
+
183
+ isPanning = true;
184
+ panStartX = e.clientX;
185
+ panStartY = e.clientY;
186
+ scrollStartX = panTarget.scrollLeft;
187
+ scrollStartY = panTarget.scrollTop;
188
+ panTarget.classList.add('panning');
189
+ }
190
+
191
+ function doPan(e) {
192
+ if (!isPanning || !panTarget) return;
193
+
194
+ const dx = e.clientX - panStartX;
195
+ const dy = e.clientY - panStartY;
196
+
197
+ panTarget.scrollLeft = scrollStartX - dx;
198
+ panTarget.scrollTop = scrollStartY - dy;
199
+ }
200
+
201
+ function endPan() {
202
+ if (isPanning && panTarget) {
203
+ panTarget.classList.remove('panning');
204
+ isPanning = false;
205
+ panTarget = null;
206
+ }
207
+ }
208
+
209
+ // ==================== END ZOOM/PAN ====================
210
+ """
211
+
212
+ __all__ = ["SCRIPTS_ZOOM"]