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,261 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Datatable plotting JavaScript: hints, variable assignment, plotting."""
4
+
5
+ from ...._plot_types_registry import generate_js_hints
6
+
7
+
8
+ def get_js_datatable_plot() -> str:
9
+ """Generate JavaScript for plot type hints and variable assignment."""
10
+ js_hints = generate_js_hints()
11
+
12
+ return f"""
13
+ // ============================================================================
14
+ // Plot Type Hints (Generated from matplotlib signatures)
15
+ // ============================================================================
16
+ {js_hints}
17
+
18
+ function initPlotTypeButtons() {{
19
+ const selector = document.getElementById('datatable-plot-type');
20
+ if (!selector) return;
21
+
22
+ selector.addEventListener('change', (e) => {{
23
+ datatablePlotType = e.target.value;
24
+ updateVarAssignSlots();
25
+ updatePlotButtonState();
26
+ }});
27
+ }}
28
+
29
+ // ============================================================================
30
+ // Variable Assignment UI with Color-Coded Linking
31
+ // ============================================================================
32
+ let datatableVarColors = {{}}; // Maps varName -> colorIndex
33
+
34
+ function updateVarAssignSlots() {{
35
+ const container = document.getElementById('var-assign-slots');
36
+ const varAssignDiv = document.getElementById('datatable-var-assign');
37
+ if (!container || !varAssignDiv) return;
38
+
39
+ const info = PLOT_TYPE_HINTS[datatablePlotType];
40
+ if (!info || !datatableData) {{
41
+ varAssignDiv.style.display = 'none';
42
+ return;
43
+ }}
44
+
45
+ // Parse variables from hint to preserve signature order
46
+ const hintMatch = info.hint.match(/\\((.*)\\)/);
47
+ const argsStr = hintMatch ? hintMatch[1] : '';
48
+
49
+ // Parse each arg, preserving order and optional status
50
+ const allVars = [];
51
+ argsStr.split(',').forEach(arg => {{
52
+ arg = arg.trim();
53
+ if (!arg || arg === 'data') return;
54
+ const isOptional = arg.startsWith('[') && arg.endsWith(']');
55
+ const varName = arg.replace(/[\\[\\]]/g, '');
56
+ allVars.push({{ name: varName, optional: isOptional }});
57
+ }});
58
+
59
+ // Reset assignments and colors
60
+ datatableVarAssignments = {{}};
61
+ datatableVarColors = {{}};
62
+
63
+ // Assign colors to each variable (0-5 cycle)
64
+ allVars.forEach((v, idx) => {{
65
+ datatableVarColors[v.name] = idx % 6;
66
+ }});
67
+
68
+ // Build column options
69
+ const columns = datatableData.columns || [];
70
+ let colOptions = '<option value="">--</option>';
71
+ columns.forEach((col, idx) => {{
72
+ colOptions += `<option value="${{idx}}">${{col.name}}</option>`;
73
+ }});
74
+
75
+ // Build slots HTML in signature order with color classes
76
+ let html = '';
77
+ let requiredIdx = 0;
78
+
79
+ allVars.forEach((v, idx) => {{
80
+ const isOptional = v.optional;
81
+ const varName = v.name;
82
+ const colorClass = `var-color-${{idx % 6}}`;
83
+
84
+ if (isOptional) {{
85
+ html += `
86
+ <div class="var-assign-slot optional ${{colorClass}}" data-var="${{varName}}">
87
+ <span class="var-name">[${{varName}}]:</span>
88
+ <select onchange="assignVariable('${{varName}}', this.value, false)">
89
+ ${{colOptions}}
90
+ </select>
91
+ </div>`;
92
+ }} else {{
93
+ const autoAssign = requiredIdx < columns.length ? requiredIdx : '';
94
+ if (autoAssign !== '') {{
95
+ datatableVarAssignments[varName] = autoAssign;
96
+ }}
97
+ const selectedOpt = autoAssign !== '' ? colOptions.replace(`value="${{autoAssign}}"`, `value="${{autoAssign}}" selected`) : colOptions;
98
+ html += `
99
+ <div class="var-assign-slot required ${{colorClass}}${{autoAssign !== '' ? ' assigned' : ''}}" data-var="${{varName}}">
100
+ <span class="var-name">${{varName}}:</span>
101
+ <select onchange="assignVariable('${{varName}}', this.value, true)">
102
+ ${{selectedOpt}}
103
+ </select>
104
+ </div>`;
105
+ requiredIdx++;
106
+ }}
107
+ }});
108
+
109
+ container.innerHTML = html;
110
+ varAssignDiv.style.display = html ? 'block' : 'none';
111
+
112
+ updatePlotButtonState();
113
+ updateSelectionInfo();
114
+ updateColumnHighlights();
115
+ }}
116
+
117
+ function updateColumnHighlights() {{
118
+ // Remove all existing highlights from headers
119
+ document.querySelectorAll('.datatable-table th').forEach(th => {{
120
+ th.classList.remove('var-linked', 'var-color-0', 'var-color-1', 'var-color-2', 'var-color-3', 'var-color-4', 'var-color-5');
121
+ }});
122
+
123
+ // Remove highlights from cells
124
+ document.querySelectorAll('.datatable-table td.var-linked-cell').forEach(td => {{
125
+ td.classList.remove('var-linked-cell');
126
+ }});
127
+
128
+ // Add highlights based on current assignments
129
+ Object.entries(datatableVarAssignments).forEach(([varName, colIdx]) => {{
130
+ const colorIdx = datatableVarColors[varName];
131
+ if (colorIdx === undefined) return;
132
+
133
+ // Find the column header using data-col attribute (more reliable)
134
+ const th = document.querySelector(`.datatable-table th[data-col="${{colIdx}}"]`);
135
+ if (th) {{
136
+ th.classList.add('var-linked', `var-color-${{colorIdx}}`);
137
+ }}
138
+
139
+ // Also highlight all cells in this column
140
+ document.querySelectorAll(`.datatable-table td[data-col="${{colIdx}}"]`).forEach(td => {{
141
+ td.classList.add('var-linked-cell');
142
+ }});
143
+ }});
144
+ }}
145
+
146
+ function assignVariable(varName, colIdx, isRequired) {{
147
+ const slot = event.target.closest('.var-assign-slot');
148
+
149
+ if (colIdx === '' || colIdx === null) {{
150
+ delete datatableVarAssignments[varName];
151
+ if (slot) slot.classList.remove('assigned');
152
+ }} else {{
153
+ datatableVarAssignments[varName] = parseInt(colIdx);
154
+ if (slot) slot.classList.add('assigned');
155
+ }}
156
+
157
+ updatePlotButtonState();
158
+ updateSelectionInfo();
159
+ updateColumnHighlights();
160
+ }}
161
+
162
+ function updatePlotButtonState() {{
163
+ const plotBtn = document.getElementById('btn-datatable-plot');
164
+ if (!plotBtn) return;
165
+
166
+ const info = PLOT_TYPE_HINTS[datatablePlotType];
167
+ if (!info) {{
168
+ plotBtn.disabled = true;
169
+ return;
170
+ }}
171
+
172
+ // Check if all required variables are assigned
173
+ const requiredVars = info.required.split(',').map(s => s.trim()).filter(s => s && s !== 'data');
174
+ const allAssigned = requiredVars.length === 0 || requiredVars.every(v => datatableVarAssignments[v] !== undefined);
175
+
176
+ plotBtn.disabled = !allAssigned || !datatableData;
177
+ }}
178
+
179
+ // ============================================================================
180
+ // Plot from Variable Assignments
181
+ // ============================================================================
182
+ async function plotFromVarAssignments() {{
183
+ if (!datatableData || Object.keys(datatableVarAssignments).length === 0) return;
184
+
185
+ // Build data and columns from variable assignments
186
+ const plotData = {{}};
187
+ const columns = [];
188
+
189
+ // Order matters: required vars first, then optional
190
+ const info = PLOT_TYPE_HINTS[datatablePlotType];
191
+ const requiredVars = info.required.split(',').map(s => s.trim()).filter(s => s && s !== 'data');
192
+ const optionalVars = info.optional.split(',').map(s => s.replace(/[\\[\\]]/g, '').trim()).filter(s => s);
193
+
194
+ [...requiredVars, ...optionalVars].forEach(varName => {{
195
+ const colIdx = datatableVarAssignments[varName];
196
+ if (colIdx !== undefined) {{
197
+ const col = datatableData.columns[colIdx];
198
+ plotData[col.name] = datatableData.rows.map(row => row[colIdx]);
199
+ columns.push(col.name);
200
+ }}
201
+ }});
202
+
203
+ if (columns.length === 0) return;
204
+
205
+ // Send to backend for plotting
206
+ if (typeof showSpinner === 'function') showSpinner();
207
+
208
+ try {{
209
+ const response = await fetch('/datatable/plot', {{
210
+ method: 'POST',
211
+ headers: {{ 'Content-Type': 'application/json' }},
212
+ body: JSON.stringify({{
213
+ data: plotData,
214
+ columns: columns,
215
+ plot_type: datatablePlotType,
216
+ target_axis: datatableTargetAxis
217
+ }})
218
+ }});
219
+
220
+ const result = await response.json();
221
+
222
+ if (result.success) {{
223
+ // Update preview
224
+ const previewImg = document.getElementById('preview-image');
225
+ if (previewImg && result.image) {{
226
+ previewImg.src = 'data:image/png;base64,' + result.image;
227
+ }}
228
+
229
+ // Update bboxes
230
+ if (result.bboxes && typeof currentBboxes !== 'undefined') {{
231
+ currentBboxes = result.bboxes;
232
+ }}
233
+
234
+ // Reload hitmap
235
+ if (typeof loadHitmap === 'function') loadHitmap();
236
+
237
+ // Refresh panel selector (in case new panel was added)
238
+ if (typeof updatePanelSelector === 'function') updatePanelSelector();
239
+
240
+ // Clear selection
241
+ if (typeof clearSelection === 'function') clearSelection();
242
+ }} else {{
243
+ console.error('Plot failed:', result.error);
244
+ alert('Failed to create plot: ' + (result.error || 'Unknown error'));
245
+ }}
246
+ }} catch (err) {{
247
+ console.error('Plot request failed:', err);
248
+ alert('Failed to create plot: ' + err.message);
249
+ }} finally {{
250
+ if (typeof hideSpinner === 'function') hideSpinner();
251
+ }}
252
+ }}
253
+ """
254
+
255
+
256
+ # For backward compatibility
257
+ JS_DATATABLE_PLOT = get_js_datatable_plot()
258
+
259
+ __all__ = ["JS_DATATABLE_PLOT", "get_js_datatable_plot"]
260
+
261
+ # EOF
@@ -0,0 +1,438 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Datatable cell selection JavaScript - multi-cell selection and highlights."""
4
+
5
+ JS_DATATABLE_SELECTION = """
6
+ // ============================================================================
7
+ // Cell Selection State
8
+ // ============================================================================
9
+ let datatableCurrentCell = null; // { row, col } - active cell for input
10
+ let datatableSelectedCells = null; // { startRow, startCol, endRow, endCol } - range
11
+ let datatableIsSelecting = false; // Mouse drag selection in progress
12
+ let datatableEditMode = false; // Cell is being edited
13
+
14
+ // ============================================================================
15
+ // Cell Event Listeners (Event Delegation Pattern - vis_app)
16
+ // ============================================================================
17
+ let datatableListenersAttached = false;
18
+
19
+ function attachCellEventListeners() {
20
+ const table = document.querySelector('.datatable-table');
21
+ if (!table || datatableListenersAttached) return;
22
+
23
+ // Use event delegation - attach once to table, handle all cells
24
+ // This is efficient for large tables (100+ rows)
25
+
26
+ // Click to select cell (delegated)
27
+ table.addEventListener('click', handleCellClick);
28
+
29
+ // Double-click to edit (delegated)
30
+ table.addEventListener('dblclick', handleCellDoubleClick);
31
+
32
+ // Mouse drag for range selection (delegated)
33
+ table.addEventListener('mousedown', handleMouseDown);
34
+
35
+ // These need document-level for drag outside table
36
+ if (!datatableListenersAttached) {
37
+ document.addEventListener('mousemove', handleMouseMove);
38
+ document.addEventListener('mouseup', handleMouseUp);
39
+ }
40
+
41
+ // Keyboard navigation and editing (delegated)
42
+ table.addEventListener('keydown', handleCellKeydown);
43
+
44
+ // Clipboard events (delegated)
45
+ table.addEventListener('copy', handleCopy);
46
+ table.addEventListener('paste', handlePaste);
47
+ table.addEventListener('cut', handleCut);
48
+
49
+ // Context menu (right-click)
50
+ if (typeof attachContextMenuListener === 'function') {
51
+ attachContextMenuListener();
52
+ }
53
+
54
+ datatableListenersAttached = true;
55
+ }
56
+
57
+ function handleCellClick(e) {
58
+ const cell = e.target.closest('td[data-row][data-col]');
59
+ if (!cell || datatableEditMode) return;
60
+
61
+ const row = parseInt(cell.dataset.row);
62
+ const col = parseInt(cell.dataset.col);
63
+
64
+ if (e.shiftKey && datatableCurrentCell) {
65
+ // Shift+click: extend selection
66
+ datatableSelectedCells = {
67
+ startRow: datatableCurrentCell.row,
68
+ startCol: datatableCurrentCell.col,
69
+ endRow: row,
70
+ endCol: col
71
+ };
72
+ } else {
73
+ // Regular click: single cell selection
74
+ datatableCurrentCell = { row, col };
75
+ datatableSelectedCells = {
76
+ startRow: row, startCol: col, endRow: row, endCol: col
77
+ };
78
+ }
79
+
80
+ updateCellSelectionDisplay();
81
+ cell.focus();
82
+ }
83
+
84
+ function handleCellDoubleClick(e) {
85
+ const cell = e.target.closest('td[data-row][data-col]');
86
+ if (!cell) return;
87
+ enterCellEditMode(cell);
88
+ }
89
+
90
+ function handleMouseDown(e) {
91
+ const cell = e.target.closest('td[data-row][data-col]');
92
+ if (!cell || datatableEditMode) return;
93
+
94
+ datatableIsSelecting = true;
95
+ const row = parseInt(cell.dataset.row);
96
+ const col = parseInt(cell.dataset.col);
97
+ datatableCurrentCell = { row, col };
98
+ datatableSelectedCells = {
99
+ startRow: row, startCol: col, endRow: row, endCol: col
100
+ };
101
+ updateCellSelectionDisplay();
102
+ }
103
+
104
+ function handleMouseMove(e) {
105
+ if (!datatableIsSelecting) return;
106
+
107
+ const elem = document.elementFromPoint(e.clientX, e.clientY);
108
+ const cell = elem?.closest('td[data-row][data-col]');
109
+ if (!cell) return;
110
+
111
+ const row = parseInt(cell.dataset.row);
112
+ const col = parseInt(cell.dataset.col);
113
+ datatableSelectedCells.endRow = row;
114
+ datatableSelectedCells.endCol = col;
115
+ updateCellSelectionDisplay();
116
+ }
117
+
118
+ function handleMouseUp() {
119
+ datatableIsSelecting = false;
120
+ }
121
+
122
+ // ============================================================================
123
+ // Cell Selection Display
124
+ // ============================================================================
125
+ function updateCellSelectionDisplay() {
126
+ // Clear previous selection
127
+ document.querySelectorAll('.datatable-table td.cell-selected').forEach(td => {
128
+ td.classList.remove('cell-selected', 'cell-current');
129
+ });
130
+
131
+ if (!datatableSelectedCells) return;
132
+
133
+ const { startRow, startCol, endRow, endCol } = datatableSelectedCells;
134
+ const minRow = Math.min(startRow, endRow);
135
+ const maxRow = Math.max(startRow, endRow);
136
+ const minCol = Math.min(startCol, endCol);
137
+ const maxCol = Math.max(startCol, endCol);
138
+
139
+ // Highlight selected range
140
+ for (let r = minRow; r <= maxRow; r++) {
141
+ for (let c = minCol; c <= maxCol; c++) {
142
+ const cell = document.querySelector(
143
+ `td[data-row="${r}"][data-col="${c}"]`
144
+ );
145
+ if (cell) cell.classList.add('cell-selected');
146
+ }
147
+ }
148
+
149
+ // Mark current cell
150
+ if (datatableCurrentCell) {
151
+ const current = document.querySelector(
152
+ `td[data-row="${datatableCurrentCell.row}"][data-col="${datatableCurrentCell.col}"]`
153
+ );
154
+ if (current) current.classList.add('cell-current');
155
+ }
156
+ }
157
+
158
+ function moveToNextCell(deltaRow, deltaCol) {
159
+ if (!datatableCurrentCell || !datatableData) return;
160
+
161
+ let newRow = datatableCurrentCell.row + deltaRow;
162
+ let newCol = datatableCurrentCell.col + deltaCol;
163
+
164
+ // Wrap around columns
165
+ if (newCol >= datatableData.columns.length) {
166
+ newCol = 0;
167
+ newRow++;
168
+ } else if (newCol < 0) {
169
+ newCol = datatableData.columns.length - 1;
170
+ newRow--;
171
+ }
172
+
173
+ // Clamp rows
174
+ const maxRows = Math.min(datatableData.rows.length, 100);
175
+ newRow = Math.max(0, Math.min(newRow, maxRows - 1));
176
+
177
+ datatableCurrentCell = { row: newRow, col: newCol };
178
+ datatableSelectedCells = {
179
+ startRow: newRow, startCol: newCol, endRow: newRow, endCol: newCol
180
+ };
181
+ updateCellSelectionDisplay();
182
+
183
+ const nextCell = document.querySelector(
184
+ `td[data-row="${newRow}"][data-col="${newCol}"]`
185
+ );
186
+ if (nextCell) nextCell.focus();
187
+ }
188
+
189
+ // Calculate next cell position (shared by both functions)
190
+ function calculateNextPosition(mode, reverse) {
191
+ if (!datatableCurrentCell || !datatableData) return null;
192
+
193
+ const maxRows = Math.min(datatableData.rows.length, datatableRenderedRows || 100) - 1;
194
+ const maxCols = datatableData.columns.length - 1;
195
+
196
+ // Get selection bounds (or just current cell if no selection)
197
+ let minRow = 0, minCol = 0, selMaxRow = maxRows, selMaxCol = maxCols;
198
+ const hasRangeSelection = datatableSelectedCells &&
199
+ (datatableSelectedCells.startRow !== datatableSelectedCells.endRow ||
200
+ datatableSelectedCells.startCol !== datatableSelectedCells.endCol);
201
+
202
+ if (hasRangeSelection) {
203
+ minRow = Math.min(datatableSelectedCells.startRow, datatableSelectedCells.endRow);
204
+ selMaxRow = Math.max(datatableSelectedCells.startRow, datatableSelectedCells.endRow);
205
+ minCol = Math.min(datatableSelectedCells.startCol, datatableSelectedCells.endCol);
206
+ selMaxCol = Math.max(datatableSelectedCells.startCol, datatableSelectedCells.endCol);
207
+ }
208
+
209
+ let { row, col } = datatableCurrentCell;
210
+
211
+ if (mode === 'tab') {
212
+ // Tab: horizontal movement (left-to-right, wrap to next row)
213
+ if (!reverse) {
214
+ if (col < selMaxCol) {
215
+ col++;
216
+ } else {
217
+ col = minCol;
218
+ row = row < selMaxRow ? row + 1 : minRow;
219
+ }
220
+ } else {
221
+ if (col > minCol) {
222
+ col--;
223
+ } else {
224
+ col = selMaxCol;
225
+ row = row > minRow ? row - 1 : selMaxRow;
226
+ }
227
+ }
228
+ } else {
229
+ // Enter: vertical movement (top-to-bottom, wrap to next column)
230
+ if (!reverse) {
231
+ if (row < selMaxRow) {
232
+ row++;
233
+ } else {
234
+ row = minRow;
235
+ col = col < selMaxCol ? col + 1 : minCol;
236
+ }
237
+ } else {
238
+ if (row > minRow) {
239
+ row--;
240
+ } else {
241
+ row = selMaxRow;
242
+ col = col > minCol ? col - 1 : selMaxCol;
243
+ }
244
+ }
245
+ }
246
+
247
+ return { row, col, hasRangeSelection };
248
+ }
249
+
250
+ // Smart Tab/Enter navigation (vis_app pattern) - selection mode only (no edit)
251
+ // Tab: horizontal (left-to-right, wrap to next row)
252
+ // Enter: vertical (top-to-bottom, wrap to next column)
253
+ function navigateWithTabEnter(mode, reverse) {
254
+ const result = calculateNextPosition(mode, reverse);
255
+ if (!result) return;
256
+
257
+ const { row, col, hasRangeSelection } = result;
258
+
259
+ datatableCurrentCell = { row, col };
260
+
261
+ // If we have a range selection, keep it; otherwise move selection too
262
+ if (!hasRangeSelection) {
263
+ datatableSelectedCells = {
264
+ startRow: row, startCol: col, endRow: row, endCol: col
265
+ };
266
+ }
267
+
268
+ updateCellSelectionDisplay();
269
+
270
+ const nextCell = document.querySelector(`td[data-row="${row}"][data-col="${col}"]`);
271
+ if (nextCell) nextCell.focus();
272
+ }
273
+
274
+ // Smart Tab/Enter navigation AND enter edit mode (scitex-cloud pattern)
275
+ // Used when exiting edit mode with Tab/Enter to continue editing in next cell
276
+ function navigateWithTabEnterAndEdit(mode, reverse) {
277
+ const result = calculateNextPosition(mode, reverse);
278
+ if (!result) return;
279
+
280
+ const { row, col, hasRangeSelection } = result;
281
+
282
+ datatableCurrentCell = { row, col };
283
+
284
+ // If we have a range selection, keep it; otherwise move selection too
285
+ if (!hasRangeSelection) {
286
+ datatableSelectedCells = {
287
+ startRow: row, startCol: col, endRow: row, endCol: col
288
+ };
289
+ }
290
+
291
+ updateCellSelectionDisplay();
292
+
293
+ const nextCell = document.querySelector(`td[data-row="${row}"][data-col="${col}"]`);
294
+ if (nextCell) {
295
+ // Enter edit mode on the next cell (scitex-cloud behavior)
296
+ enterCellEditMode(nextCell);
297
+ }
298
+ }
299
+
300
+ // ============================================================================
301
+ // Keyboard Navigation
302
+ // ============================================================================
303
+ function handleCellKeydown(e) {
304
+ if (datatableEditMode) return;
305
+
306
+ const cell = e.target.closest('td[data-row][data-col]');
307
+ if (!cell) return;
308
+
309
+ switch (e.key) {
310
+ case 'ArrowUp':
311
+ e.preventDefault();
312
+ moveToNextCell(-1, 0);
313
+ break;
314
+ case 'ArrowDown':
315
+ e.preventDefault();
316
+ moveToNextCell(1, 0);
317
+ break;
318
+ case 'ArrowLeft':
319
+ e.preventDefault();
320
+ moveToNextCell(0, -1);
321
+ break;
322
+ case 'ArrowRight':
323
+ e.preventDefault();
324
+ moveToNextCell(0, 1);
325
+ break;
326
+ case 'Tab':
327
+ e.preventDefault();
328
+ navigateWithTabEnter('tab', e.shiftKey);
329
+ break;
330
+ case 'Enter':
331
+ e.preventDefault();
332
+ navigateWithTabEnter('enter', e.shiftKey);
333
+ break;
334
+ case 'F2':
335
+ e.preventDefault();
336
+ enterCellEditMode(cell);
337
+ break;
338
+ case 'Delete':
339
+ case 'Backspace':
340
+ e.preventDefault();
341
+ clearSelectedCells();
342
+ break;
343
+ default:
344
+ // Type to start editing
345
+ if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) {
346
+ enterCellEditMode(cell);
347
+ }
348
+ }
349
+ }
350
+
351
+ function clearSelectedCells() {
352
+ if (!datatableSelectedCells || !datatableData) return;
353
+
354
+ const { startRow, startCol, endRow, endCol } = datatableSelectedCells;
355
+ const minRow = Math.min(startRow, endRow);
356
+ const maxRow = Math.max(startRow, endRow);
357
+ const minCol = Math.min(startCol, endCol);
358
+ const maxCol = Math.max(startCol, endCol);
359
+
360
+ for (let r = minRow; r <= maxRow; r++) {
361
+ for (let c = minCol; c <= maxCol; c++) {
362
+ if (datatableData.rows[r]) {
363
+ datatableData.rows[r][c] = '';
364
+ }
365
+ }
366
+ }
367
+
368
+ renderDatatable();
369
+ updateCellSelectionDisplay();
370
+ }
371
+
372
+ // ============================================================================
373
+ // Shortcuts Info Popup
374
+ // ============================================================================
375
+ let shortcutsPopup = null;
376
+
377
+ function showShortcutsPopup() {
378
+ if (!shortcutsPopup) {
379
+ shortcutsPopup = document.createElement('div');
380
+ shortcutsPopup.className = 'shortcuts-popup';
381
+ shortcutsPopup.innerHTML = `
382
+ <div class="shortcuts-header">
383
+ <h4>Keyboard Shortcuts</h4>
384
+ <button class="btn-close" onclick="hideShortcutsPopup()">&times;</button>
385
+ </div>
386
+ <div class="shortcuts-content">
387
+ <div class="shortcut-group">
388
+ <div class="shortcut-row"><kbd>Tab</kbd> Move right (wrap to next row)</div>
389
+ <div class="shortcut-row"><kbd>Shift+Tab</kbd> Move left</div>
390
+ <div class="shortcut-row"><kbd>Enter</kbd> Move down (wrap to next col)</div>
391
+ <div class="shortcut-row"><kbd>Shift+Enter</kbd> Move up</div>
392
+ </div>
393
+ <div class="shortcut-group">
394
+ <div class="shortcut-row"><kbd>F2</kbd> Edit cell</div>
395
+ <div class="shortcut-row"><kbd>Esc</kbd> Cancel edit</div>
396
+ <div class="shortcut-row"><kbd>Delete</kbd> Clear selected cells</div>
397
+ </div>
398
+ <div class="shortcut-group">
399
+ <div class="shortcut-row"><kbd>Ctrl+C</kbd> Copy</div>
400
+ <div class="shortcut-row"><kbd>Ctrl+X</kbd> Cut</div>
401
+ <div class="shortcut-row"><kbd>Ctrl+V</kbd> Paste</div>
402
+ </div>
403
+ <div class="shortcut-group">
404
+ <div class="shortcut-row"><kbd>Click</kbd> Select cell</div>
405
+ <div class="shortcut-row"><kbd>Shift+Click</kbd> Extend selection</div>
406
+ <div class="shortcut-row"><kbd>Drag</kbd> Range selection</div>
407
+ <div class="shortcut-row"><kbd>Right-click</kbd> Context menu</div>
408
+ </div>
409
+ </div>
410
+ `;
411
+ document.body.appendChild(shortcutsPopup);
412
+ }
413
+ shortcutsPopup.style.display = 'block';
414
+ }
415
+
416
+ function hideShortcutsPopup() {
417
+ if (shortcutsPopup) {
418
+ shortcutsPopup.style.display = 'none';
419
+ }
420
+ }
421
+
422
+ // Attach shortcuts button listener
423
+ function initShortcutsButton() {
424
+ const btn = document.getElementById('btn-shortcuts-info');
425
+ if (btn) {
426
+ btn.addEventListener('click', showShortcutsPopup);
427
+ }
428
+ }
429
+
430
+ // Initialize on load
431
+ if (typeof window !== 'undefined') {
432
+ window.addEventListener('DOMContentLoaded', initShortcutsButton);
433
+ }
434
+ """
435
+
436
+ __all__ = ["JS_DATATABLE_SELECTION"]
437
+
438
+ # EOF