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,164 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Datatable clipboard operations JavaScript - copy, paste, cut."""
4
+
5
+ JS_DATATABLE_CLIPBOARD = """
6
+ // ============================================================================
7
+ // Clipboard Operations (Copy, Paste, Cut)
8
+ // ============================================================================
9
+ let datatableCopiedRange = null; // Track copied cell range for marching ants
10
+ let datatableIsCutOperation = false; // Track if it was cut (not copy)
11
+
12
+ function handleCopy(e) {
13
+ if (!datatableSelectedCells || datatableEditMode) return;
14
+
15
+ e.preventDefault();
16
+ const text = getSelectedCellsAsTSV();
17
+ e.clipboardData.setData('text/plain', text);
18
+
19
+ // Store copied range and show marching ants
20
+ datatableCopiedRange = { ...datatableSelectedCells };
21
+ datatableIsCutOperation = false;
22
+ showMarchingAnts();
23
+ }
24
+
25
+ function handleCut(e) {
26
+ if (!datatableSelectedCells || datatableEditMode) return;
27
+
28
+ e.preventDefault();
29
+ const text = getSelectedCellsAsTSV();
30
+ e.clipboardData.setData('text/plain', text);
31
+
32
+ // Store cut range and show marching ants with faded cells
33
+ datatableCopiedRange = { ...datatableSelectedCells };
34
+ datatableIsCutOperation = true;
35
+ showMarchingAnts();
36
+ }
37
+
38
+ // Show Excel-style marching ants border around copied/cut cells
39
+ function showMarchingAnts() {
40
+ clearMarchingAnts(); // Clear any existing marching ants
41
+ if (!datatableCopiedRange) return;
42
+
43
+ const { startRow, startCol, endRow, endCol } = datatableCopiedRange;
44
+ const minRow = Math.min(startRow, endRow);
45
+ const maxRow = Math.max(startRow, endRow);
46
+ const minCol = Math.min(startCol, endCol);
47
+ const maxCol = Math.max(startCol, endCol);
48
+
49
+ for (let r = minRow; r <= maxRow; r++) {
50
+ for (let c = minCol; c <= maxCol; c++) {
51
+ const cell = document.querySelector(`td[data-row="${r}"][data-col="${c}"]`);
52
+ if (!cell) continue;
53
+
54
+ // Add border classes based on position in selection
55
+ if (r === minRow) cell.classList.add('copy-border-top');
56
+ if (r === maxRow) cell.classList.add('copy-border-bottom');
57
+ if (c === minCol) cell.classList.add('copy-border-left');
58
+ if (c === maxCol) cell.classList.add('copy-border-right');
59
+
60
+ // Add faded effect for cut operation
61
+ if (datatableIsCutOperation) {
62
+ cell.classList.add('cut-pending');
63
+ }
64
+ }
65
+ }
66
+ }
67
+
68
+ // Clear marching ants border
69
+ function clearMarchingAnts() {
70
+ document.querySelectorAll('.copy-border-top, .copy-border-bottom, .copy-border-left, .copy-border-right, .cut-pending').forEach(cell => {
71
+ cell.classList.remove('copy-border-top', 'copy-border-bottom', 'copy-border-left', 'copy-border-right', 'cut-pending');
72
+ });
73
+ }
74
+
75
+ function handlePaste(e) {
76
+ if (!datatableCurrentCell || datatableEditMode || !datatableData) return;
77
+
78
+ e.preventDefault();
79
+ const text = e.clipboardData.getData('text/plain');
80
+ if (!text) return;
81
+
82
+ // If this was a cut operation, clear the source cells
83
+ if (datatableIsCutOperation && datatableCopiedRange) {
84
+ const { startRow, startCol, endRow, endCol } = datatableCopiedRange;
85
+ const minRow = Math.min(startRow, endRow);
86
+ const maxRow = Math.max(startRow, endRow);
87
+ const minCol = Math.min(startCol, endCol);
88
+ const maxCol = Math.max(startCol, endCol);
89
+
90
+ for (let r = minRow; r <= maxRow; r++) {
91
+ for (let c = minCol; c <= maxCol; c++) {
92
+ if (datatableData.rows[r]) {
93
+ datatableData.rows[r][c] = '';
94
+ }
95
+ }
96
+ }
97
+ }
98
+
99
+ const rows = text.split('\\n').map(line => line.split('\\t'));
100
+ const startRow = datatableCurrentCell.row;
101
+ const startCol = datatableCurrentCell.col;
102
+
103
+ rows.forEach((rowData, rOffset) => {
104
+ const targetRow = startRow + rOffset;
105
+ if (targetRow >= datatableData.rows.length) return;
106
+
107
+ rowData.forEach((value, cOffset) => {
108
+ const targetCol = startCol + cOffset;
109
+ if (targetCol >= datatableData.columns.length) return;
110
+
111
+ // Preserve empty strings as empty (not convert to 0)
112
+ if (value === '' || value === null || value === undefined) {
113
+ datatableData.rows[targetRow][targetCol] = '';
114
+ } else {
115
+ const colType = datatableData.columns[targetCol]?.type;
116
+ if (colType === 'numeric') {
117
+ const num = parseFloat(value);
118
+ datatableData.rows[targetRow][targetCol] = isNaN(num) ? value : num;
119
+ } else {
120
+ datatableData.rows[targetRow][targetCol] = value;
121
+ }
122
+ }
123
+ });
124
+ });
125
+
126
+ // Clear marching ants and reset cut state
127
+ clearMarchingAnts();
128
+ datatableCopiedRange = null;
129
+ datatableIsCutOperation = false;
130
+
131
+ renderDatatable();
132
+ updateCellSelectionDisplay();
133
+ }
134
+
135
+ function getSelectedCellsAsTSV() {
136
+ if (!datatableSelectedCells || !datatableData) return '';
137
+
138
+ const { startRow, startCol, endRow, endCol } = datatableSelectedCells;
139
+ const minRow = Math.min(startRow, endRow);
140
+ const maxRow = Math.max(startRow, endRow);
141
+ const minCol = Math.min(startCol, endCol);
142
+ const maxCol = Math.max(startCol, endCol);
143
+
144
+ const lines = [];
145
+ for (let r = minRow; r <= maxRow; r++) {
146
+ const cells = [];
147
+ for (let c = minCol; c <= maxCol; c++) {
148
+ const value = datatableData.rows[r]?.[c];
149
+ // Preserve None as "None" string for copy
150
+ if (value === null || value === undefined) {
151
+ cells.push('');
152
+ } else {
153
+ cells.push(String(value));
154
+ }
155
+ }
156
+ lines.push(cells.join('\\t'));
157
+ }
158
+ return lines.join('\\n');
159
+ }
160
+ """
161
+
162
+ __all__ = ["JS_DATATABLE_CLIPBOARD"]
163
+
164
+ # EOF
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Datatable right-click context menu JavaScript."""
4
+
5
+ JS_DATATABLE_CONTEXT_MENU = """
6
+ // ============================================================================
7
+ // Context Menu (Right-Click Menu)
8
+ // ============================================================================
9
+ let datatableContextMenu = null;
10
+
11
+ function createContextMenu() {
12
+ if (datatableContextMenu) return;
13
+
14
+ datatableContextMenu = document.createElement('div');
15
+ datatableContextMenu.className = 'datatable-context-menu';
16
+ datatableContextMenu.style.display = 'none';
17
+ datatableContextMenu.innerHTML = `
18
+ <div class="context-menu-item" data-action="cut">
19
+ Cut<span class="shortcut">Ctrl+X</span>
20
+ </div>
21
+ <div class="context-menu-item" data-action="copy">
22
+ Copy<span class="shortcut">Ctrl+C</span>
23
+ </div>
24
+ <div class="context-menu-item" data-action="paste">
25
+ Paste<span class="shortcut">Ctrl+V</span>
26
+ </div>
27
+ <div class="context-menu-divider"></div>
28
+ <div class="context-menu-item" data-action="clear">
29
+ Clear cells<span class="shortcut">Del</span>
30
+ </div>
31
+ <div class="context-menu-divider"></div>
32
+ <div class="context-menu-item" data-action="insert-row">
33
+ Insert row below
34
+ </div>
35
+ <div class="context-menu-item" data-action="insert-col">
36
+ Insert column right
37
+ </div>
38
+ <div class="context-menu-divider"></div>
39
+ <div class="context-menu-item" data-action="delete-row">
40
+ Delete row
41
+ </div>
42
+ <div class="context-menu-item" data-action="delete-col">
43
+ Delete column
44
+ </div>
45
+ `;
46
+ document.body.appendChild(datatableContextMenu);
47
+ setupContextMenuListeners();
48
+ }
49
+
50
+ function setupContextMenuListeners() {
51
+ if (!datatableContextMenu) return;
52
+
53
+ // Click on menu items
54
+ datatableContextMenu.querySelectorAll('.context-menu-item').forEach(item => {
55
+ item.addEventListener('click', (e) => {
56
+ e.stopPropagation();
57
+ const action = item.dataset.action;
58
+ handleContextMenuAction(action);
59
+ hideContextMenu();
60
+ });
61
+ });
62
+
63
+ // Hide on click outside
64
+ document.addEventListener('click', hideContextMenu);
65
+ document.addEventListener('scroll', hideContextMenu, true);
66
+ document.addEventListener('keydown', (e) => {
67
+ if (e.key === 'Escape') hideContextMenu();
68
+ });
69
+ }
70
+
71
+ function handleContextMenuAction(action) {
72
+ if (!datatableData) return;
73
+
74
+ switch (action) {
75
+ case 'cut':
76
+ // Simulate Ctrl+X
77
+ document.execCommand('cut');
78
+ break;
79
+ case 'copy':
80
+ // Copy selected cells as TSV
81
+ const copyText = getSelectedCellsAsTSV();
82
+ navigator.clipboard.writeText(copyText).catch(console.error);
83
+ break;
84
+ case 'paste':
85
+ // Paste from clipboard
86
+ navigator.clipboard.readText().then(text => {
87
+ if (!text || !datatableCurrentCell) return;
88
+ const rows = text.split('\\n').map(line => line.split('\\t'));
89
+ const startRow = datatableCurrentCell.row;
90
+ const startCol = datatableCurrentCell.col;
91
+ rows.forEach((rowData, rOffset) => {
92
+ const targetRow = startRow + rOffset;
93
+ if (targetRow >= datatableData.rows.length) return;
94
+ rowData.forEach((value, cOffset) => {
95
+ const targetCol = startCol + cOffset;
96
+ if (targetCol >= datatableData.columns.length) return;
97
+ datatableData.rows[targetRow][targetCol] = value;
98
+ });
99
+ });
100
+ renderDatatable();
101
+ }).catch(console.error);
102
+ break;
103
+ case 'clear':
104
+ clearSelectedCells();
105
+ break;
106
+ case 'insert-row':
107
+ insertRowBelow();
108
+ break;
109
+ case 'insert-col':
110
+ insertColumnRight();
111
+ break;
112
+ case 'delete-row':
113
+ deleteCurrentRow();
114
+ break;
115
+ case 'delete-col':
116
+ deleteCurrentColumn();
117
+ break;
118
+ }
119
+ }
120
+
121
+ function insertRowBelow() {
122
+ if (!datatableData || !datatableCurrentCell) return;
123
+ const row = datatableCurrentCell.row;
124
+ const newRow = datatableData.columns.map(() => '');
125
+ datatableData.rows.splice(row + 1, 0, newRow);
126
+ renderDatatable();
127
+ }
128
+
129
+ function insertColumnRight() {
130
+ if (!datatableData || !datatableCurrentCell) return;
131
+ const col = datatableCurrentCell.col;
132
+ const newColIdx = datatableData.columns.length;
133
+ datatableData.columns.splice(col + 1, 0, {
134
+ name: `col${newColIdx + 1}`,
135
+ type: 'numeric',
136
+ index: col + 1
137
+ });
138
+ // Reindex columns
139
+ datatableData.columns.forEach((c, i) => c.index = i);
140
+ // Add cell to all rows
141
+ datatableData.rows.forEach(row => row.splice(col + 1, 0, ''));
142
+ renderDatatable();
143
+ updateVarAssignSlots();
144
+ }
145
+
146
+ function deleteCurrentRow() {
147
+ if (!datatableData || !datatableCurrentCell) return;
148
+ if (datatableData.rows.length <= 1) return; // Keep at least 1 row
149
+ const row = datatableCurrentCell.row;
150
+ datatableData.rows.splice(row, 1);
151
+ if (datatableCurrentCell.row >= datatableData.rows.length) {
152
+ datatableCurrentCell.row = datatableData.rows.length - 1;
153
+ }
154
+ renderDatatable();
155
+ }
156
+
157
+ function deleteCurrentColumn() {
158
+ if (!datatableData || !datatableCurrentCell) return;
159
+ if (datatableData.columns.length <= 1) return; // Keep at least 1 col
160
+ const col = datatableCurrentCell.col;
161
+ datatableData.columns.splice(col, 1);
162
+ // Reindex columns
163
+ datatableData.columns.forEach((c, i) => c.index = i);
164
+ // Remove cell from all rows
165
+ datatableData.rows.forEach(row => row.splice(col, 1));
166
+ if (datatableCurrentCell.col >= datatableData.columns.length) {
167
+ datatableCurrentCell.col = datatableData.columns.length - 1;
168
+ }
169
+ renderDatatable();
170
+ updateVarAssignSlots();
171
+ }
172
+
173
+ function showContextMenu(e) {
174
+ if (!datatableContextMenu) createContextMenu();
175
+
176
+ e.preventDefault();
177
+ e.stopPropagation();
178
+
179
+ const x = e.clientX;
180
+ const y = e.clientY;
181
+
182
+ // Position off-screen to measure
183
+ datatableContextMenu.style.left = '-9999px';
184
+ datatableContextMenu.style.top = '-9999px';
185
+ datatableContextMenu.style.display = 'block';
186
+
187
+ const menuWidth = datatableContextMenu.offsetWidth;
188
+ const menuHeight = datatableContextMenu.offsetHeight;
189
+
190
+ // Adjust position to fit in viewport
191
+ let left = x;
192
+ let top = y;
193
+ if (x + menuWidth > window.innerWidth - 10) {
194
+ left = x - menuWidth;
195
+ }
196
+ if (y + menuHeight > window.innerHeight - 10) {
197
+ top = y - menuHeight;
198
+ }
199
+
200
+ datatableContextMenu.style.left = `${Math.max(10, left)}px`;
201
+ datatableContextMenu.style.top = `${Math.max(10, top)}px`;
202
+ }
203
+
204
+ function hideContextMenu() {
205
+ if (datatableContextMenu) {
206
+ datatableContextMenu.style.display = 'none';
207
+ }
208
+ }
209
+
210
+ // Attach context menu to table
211
+ function attachContextMenuListener() {
212
+ const table = document.querySelector('.datatable-table');
213
+ if (table) {
214
+ table.addEventListener('contextmenu', showContextMenu);
215
+ }
216
+ }
217
+ """
218
+
219
+ __all__ = ["JS_DATATABLE_CONTEXT_MENU"]
220
+
221
+ # EOF
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Core datatable JavaScript: state, panel toggle, initialization."""
4
+
5
+ JS_DATATABLE_CORE = """
6
+ // ============================================================================
7
+ // Datatable Panel State
8
+ // ============================================================================
9
+ let datatableData = null; // Parsed data: {columns: [...], rows: [...]}
10
+ let datatableSelectedColumns = new Set(); // Selected column indices
11
+ let datatablePlotType = 'plot'; // Default plot type
12
+ let datatableTargetAxis = null; // null = new figure, 0+ = existing axis index
13
+ let datatableVarAssignments = {}; // Variable name -> column index mapping
14
+
15
+ // ============================================================================
16
+ // Panel Initialization
17
+ // ============================================================================
18
+ function initDatatablePanel() {
19
+ // Initialize sub-modules
20
+ initDatatableDropzone();
21
+ initPlotTypeButtons();
22
+
23
+ // Initialize plot button (New = create new panel)
24
+ const plotBtn = document.getElementById('btn-datatable-plot');
25
+ if (plotBtn) {
26
+ plotBtn.addEventListener('click', () => {
27
+ datatableTargetAxis = null; // New panel
28
+ plotFromVarAssignments();
29
+ });
30
+ }
31
+
32
+ // Initialize split button dropdown
33
+ initPlotDropdown();
34
+
35
+ // Load existing data if available from figure
36
+ loadExistingData();
37
+
38
+ // Hook into canvas selection to sync with datatable
39
+ hookCanvasSelection();
40
+ }
41
+
42
+ function hookCanvasSelection() {
43
+ // Wrap the selectElement function to add datatable sync
44
+ if (typeof window.selectElement === 'function') {
45
+ const originalSelectElement = window.selectElement;
46
+ window.selectElement = function(element) {
47
+ originalSelectElement(element);
48
+ if (typeof syncDatatableToElement === 'function') {
49
+ syncDatatableToElement(element);
50
+ }
51
+ };
52
+ }
53
+
54
+ // Also hook clearSelection
55
+ if (typeof window.clearSelection === 'function') {
56
+ const originalClearSelection = window.clearSelection;
57
+ window.clearSelection = function() {
58
+ originalClearSelection();
59
+ if (typeof clearDatatableHighlight === 'function') {
60
+ clearDatatableHighlight();
61
+ }
62
+ };
63
+ }
64
+ }
65
+
66
+ function initPlotDropdown() {
67
+ const dropdownBtn = document.getElementById('btn-plot-dropdown');
68
+ const dropdownMenu = document.getElementById('plot-dropdown-menu');
69
+ if (!dropdownBtn || !dropdownMenu) return;
70
+
71
+ // Toggle dropdown
72
+ dropdownBtn.addEventListener('click', (e) => {
73
+ e.stopPropagation();
74
+ dropdownMenu.classList.toggle('show');
75
+ if (dropdownMenu.classList.contains('show')) {
76
+ populatePlotDropdown();
77
+ }
78
+ });
79
+
80
+ // Close on outside click
81
+ document.addEventListener('click', () => {
82
+ dropdownMenu.classList.remove('show');
83
+ });
84
+ }
85
+
86
+ function populatePlotDropdown() {
87
+ const menu = document.getElementById('plot-dropdown-menu');
88
+ if (!menu) return;
89
+
90
+ fetch('/get_axes_positions').then(r => r.json()).then(data => {
91
+ const axes = Object.keys(data)
92
+ .filter(k => k.startsWith('ax_'))
93
+ .sort((a, b) => {
94
+ const matchA = a.match(/ax_(\\d+)_(\\d+)/);
95
+ const matchB = b.match(/ax_(\\d+)_(\\d+)/);
96
+ if (matchA && matchB) {
97
+ return parseInt(matchA[2]) - parseInt(matchB[2]) || parseInt(matchA[1]) - parseInt(matchB[1]);
98
+ }
99
+ return 0;
100
+ });
101
+
102
+ let html = '';
103
+ axes.forEach((key, i) => {
104
+ html += `<button class="dropdown-item" onclick="addToPanel(${i})">Add to P${i + 1}</button>`;
105
+ });
106
+ menu.innerHTML = html || '<div class="dropdown-item" style="color:var(--text-secondary)">No panels</div>';
107
+ }).catch(() => {
108
+ menu.innerHTML = '<div class="dropdown-item" style="color:var(--text-secondary)">No panels</div>';
109
+ });
110
+ }
111
+
112
+ function addToPanel(axisIndex) {
113
+ datatableTargetAxis = axisIndex;
114
+ document.getElementById('plot-dropdown-menu').classList.remove('show');
115
+ plotFromVarAssignments();
116
+ }
117
+
118
+ // ============================================================================
119
+ // Clear Data
120
+ // ============================================================================
121
+ function clearDatatableData() {
122
+ datatableData = null;
123
+ datatableSelectedColumns.clear();
124
+ datatableVarAssignments = {};
125
+
126
+ const content = document.getElementById('datatable-content');
127
+ if (content) {
128
+ content.innerHTML = '';
129
+ }
130
+
131
+ // Show dropzone again
132
+ const dropzone = document.getElementById('datatable-dropzone');
133
+ if (dropzone) dropzone.style.display = 'block';
134
+
135
+ const toolbar = document.querySelector('.datatable-toolbar');
136
+ if (toolbar) toolbar.style.display = 'none';
137
+
138
+ const varAssign = document.getElementById('datatable-var-assign');
139
+ if (varAssign) varAssign.style.display = 'none';
140
+
141
+ updateSelectionInfo();
142
+ }
143
+
144
+ // Initialize on page load
145
+ document.addEventListener('DOMContentLoaded', initDatatablePanel);
146
+ """
147
+
148
+ __all__ = ["JS_DATATABLE_CORE"]
149
+
150
+ # EOF