flexdesk 0.16.1 → 0.17.1

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 (262) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/LICENSE +0 -0
  3. package/README.md +2435 -42
  4. package/THIRD_PARTY_LICENSES.md +0 -0
  5. package/canvas.js +18 -0
  6. package/charts.js +21 -0
  7. package/core.js +14 -0
  8. package/css/base.css +1834 -75
  9. package/css/flexdesk.css +3165 -159
  10. package/css/overrides.css +16 -0
  11. package/css/reset.css +0 -0
  12. package/css/tokens.css +0 -0
  13. package/dist/canvas.js +6 -239
  14. package/dist/canvas.js.map +3 -3
  15. package/dist/charts.js +707 -18
  16. package/dist/charts.js.map +4 -4
  17. package/dist/chunk-3PHCPZHT.js +0 -0
  18. package/dist/chunk-3PHCPZHT.js.map +0 -0
  19. package/dist/chunk-4A7SJC3T.js +503 -0
  20. package/dist/chunk-4A7SJC3T.js.map +7 -0
  21. package/dist/{chunk-6JSOVNID.js → chunk-CVMCZZX2.js} +635 -10
  22. package/dist/chunk-CVMCZZX2.js.map +7 -0
  23. package/dist/chunk-D3COLZZ2.js +1311 -0
  24. package/dist/chunk-D3COLZZ2.js.map +7 -0
  25. package/dist/chunk-FOOS3T5L.js +0 -0
  26. package/dist/chunk-FOOS3T5L.js.map +0 -0
  27. package/dist/{chunk-SN4LSOAR.js → chunk-GOQLR5WR.js} +452 -2
  28. package/dist/chunk-GOQLR5WR.js.map +7 -0
  29. package/dist/{chunk-FL5KFNQH.js → chunk-HNU6MM2B.js} +52 -17
  30. package/dist/chunk-HNU6MM2B.js.map +7 -0
  31. package/dist/chunk-HYJYYEPK.js +375 -0
  32. package/dist/chunk-HYJYYEPK.js.map +7 -0
  33. package/dist/chunk-JKZLNKG6.js +2041 -0
  34. package/dist/chunk-JKZLNKG6.js.map +7 -0
  35. package/dist/chunk-JYWURG5T.js +0 -0
  36. package/dist/chunk-JYWURG5T.js.map +0 -0
  37. package/dist/{chunk-FBGP5KN5.js → chunk-LWV23YVE.js} +5073 -532
  38. package/dist/chunk-LWV23YVE.js.map +7 -0
  39. package/dist/chunk-N7HC3ADV.js +30 -0
  40. package/dist/chunk-N7HC3ADV.js.map +7 -0
  41. package/dist/{chunk-RQM5Z2MU.js → chunk-RMTRWWEO.js} +633 -311
  42. package/dist/chunk-RMTRWWEO.js.map +7 -0
  43. package/dist/{chunk-NLTOY57M.js → chunk-UURZ37SD.js} +28 -48
  44. package/dist/chunk-UURZ37SD.js.map +7 -0
  45. package/dist/{chunk-B5Q53LAJ.js → chunk-UWO43BZU.js} +2 -2
  46. package/dist/chunk-UWO43BZU.js.map +7 -0
  47. package/dist/chunk-WVFGV5FT.js +0 -0
  48. package/dist/chunk-WVFGV5FT.js.map +0 -0
  49. package/dist/core.js +170 -118
  50. package/dist/core.js.map +2 -2
  51. package/dist/editor.js +156 -70
  52. package/dist/editor.js.map +2 -2
  53. package/dist/flexdesk.css +3165 -159
  54. package/dist/flow.js +2789 -306
  55. package/dist/flow.js.map +4 -4
  56. package/dist/host.js +109 -3
  57. package/dist/host.js.map +3 -3
  58. package/dist/reset.css +0 -0
  59. package/dist/tile_registry-6WZPTUZV.js +0 -0
  60. package/dist/tile_registry-6WZPTUZV.js.map +0 -0
  61. package/dist/tiles.js +657 -234
  62. package/dist/tiles.js.map +4 -4
  63. package/dist/tokens.css +0 -0
  64. package/dist/widgets.js +3290 -2124
  65. package/dist/widgets.js.map +4 -4
  66. package/dist/wm.js +1227 -347
  67. package/dist/wm.js.map +4 -4
  68. package/editor.js +20 -0
  69. package/flow.js +21 -0
  70. package/host.js +14 -0
  71. package/package.json +12 -1
  72. package/src/canvas/canvas_adapter.js +5 -0
  73. package/src/canvas/edge_router.js +0 -0
  74. package/src/canvas/graph/connector_router.js +0 -0
  75. package/src/canvas/graph/history_service.js +0 -0
  76. package/src/canvas/graph/node_platform.js +0 -0
  77. package/src/canvas/index.js +1 -0
  78. package/src/canvas/layered_layout.js +244 -0
  79. package/src/charts/chart_types.js +0 -0
  80. package/src/charts/data_series_plot_window.js +0 -0
  81. package/src/charts/downsample.js +2 -2
  82. package/src/charts/plot_config.js +53 -29
  83. package/src/charts/plot_config_editor.js +627 -0
  84. package/src/charts/plot_grid.js +449 -0
  85. package/src/charts/plot_popout_window.js +103 -317
  86. package/src/charts/plotly_wrapper.js +22 -2
  87. package/src/charts/series_transforms.js +206 -0
  88. package/src/core/event_bus.js +0 -0
  89. package/src/core/logging.js +1 -1
  90. package/src/core/settings.js +105 -19
  91. package/src/core/state_guard.js +93 -51
  92. package/src/core/state_guard_config.js +85 -74
  93. package/src/core/state_machine.js +0 -0
  94. package/src/editor/editor_pane.js +3 -3
  95. package/src/editor/monaco_editor_factory.js +124 -60
  96. package/src/editor/monaco_loader.js +103 -28
  97. package/src/editor/notebook_search.js +0 -0
  98. package/src/editor/notebook_tab_bar.js +38 -52
  99. package/src/editor/notebook_undo_manager.js +0 -0
  100. package/src/editor/split_pane_container.js +6 -3
  101. package/src/flow/canvas_editor/connections.js +90 -0
  102. package/src/flow/canvas_editor/editor.js +2351 -153
  103. package/src/flow/canvas_editor/index.js +2 -1
  104. package/src/flow/canvas_editor/layout.js +119 -12
  105. package/src/flow/canvas_editor/strings.js +80 -0
  106. package/src/flow/kit/always_before.js +0 -0
  107. package/src/flow/kit/catalogue.js +97 -3
  108. package/src/flow/kit/chip_input.js +0 -0
  109. package/src/flow/kit/dom.js +0 -0
  110. package/src/flow/kit/findings.js +0 -0
  111. package/src/flow/kit/history.js +0 -0
  112. package/src/flow/kit/index.js +3 -3
  113. package/src/flow/kit/keys.js +7 -1
  114. package/src/flow/kit/logic_graph.js +97 -15
  115. package/src/flow/kit/references.js +0 -0
  116. package/src/flow/kit/settings_panel.js +0 -0
  117. package/src/flow/kit/settings_schema.js +2 -1
  118. package/src/flow/kit/step_picker.js +0 -0
  119. package/src/flow/kit/strings.js +3 -1
  120. package/src/flow/kit/value_picker.js +0 -0
  121. package/src/flow/kit/widgets.js +86 -109
  122. package/src/flow/lanes/editor.js +136 -14
  123. package/src/flow/lanes/index.js +3 -1
  124. package/src/flow/lanes/layout.js +0 -0
  125. package/src/flow/lanes/pipeline.js +44 -2
  126. package/src/flow/lanes/preview.js +0 -0
  127. package/src/flow/lanes/strings.js +1 -0
  128. package/src/flow/outline/editor.js +0 -0
  129. package/src/flow/outline/index.js +0 -0
  130. package/src/flow/outline/mapping.js +0 -0
  131. package/src/flow/outline/menu.js +0 -0
  132. package/src/flow/outline/operations.js +0 -0
  133. package/src/flow/outline/recognise.js +0 -0
  134. package/src/flow/outline/strings.js +0 -0
  135. package/src/help/help_modal.js +168 -42
  136. package/src/help/help_registry.js +12 -1
  137. package/src/host/external_links.js +0 -0
  138. package/src/host/file_picker.js +117 -0
  139. package/src/host/host.js +10 -2
  140. package/src/host/photino_host.js +9 -0
  141. package/src/host/photino_host.md +21 -3
  142. package/src/host/pywebview_host.js +10 -0
  143. package/src/tiles/chart_tiles.js +53 -245
  144. package/src/tiles/config_schema.js +8 -0
  145. package/src/tiles/figure_tile.js +322 -0
  146. package/src/tiles/layout_persistence.js +3 -3
  147. package/src/tiles/list_tiles.js +7 -7
  148. package/src/tiles/scope_picker.js +0 -0
  149. package/src/tiles/tile_base.js +1 -1
  150. package/src/tiles/tile_config_form.js +442 -0
  151. package/src/tiles/tile_grid.js +2 -0
  152. package/src/tiles/tile_registry.js +0 -0
  153. package/src/tiling/bottom_panel_tabs.js +67 -99
  154. package/src/tiling/command_palette.js +64 -19
  155. package/src/tiling/content_registry.js +4 -1
  156. package/src/tiling/desktops.js +11 -1
  157. package/src/tiling/entity_sources.js +0 -0
  158. package/src/tiling/keymap.js +403 -128
  159. package/src/tiling/kind_taxonomy.js +78 -0
  160. package/src/tiling/landing_table.js +0 -0
  161. package/src/tiling/loading_overlay.js +0 -0
  162. package/src/tiling/nav_panel.js +59 -10
  163. package/src/tiling/page_factory.js +5 -5
  164. package/src/tiling/panel_availability.js +49 -0
  165. package/src/tiling/panel_keys.js +0 -0
  166. package/src/tiling/shell.js +114 -12
  167. package/src/tiling/tab_strip.js +0 -0
  168. package/src/tiling/tile_breadcrumb.js +0 -0
  169. package/src/tiling/tile_renderer.js +105 -9
  170. package/src/tiling/tile_tab_menu.js +0 -0
  171. package/src/tiling/tile_tree.js +0 -0
  172. package/src/tiling/wm.js +562 -30
  173. package/src/tiling/zoom.js +0 -0
  174. package/src/ui/base/component_base.js +0 -0
  175. package/src/ui/base/controller_base.js +0 -0
  176. package/src/ui/base/page_base.js +0 -0
  177. package/src/ui/components/about_dialog.js +0 -0
  178. package/src/ui/components/action_dropdown.js +30 -2
  179. package/src/ui/components/alert_log.js +0 -0
  180. package/src/ui/components/alerts_pane.js +0 -0
  181. package/src/ui/components/attribute_list_editor.js +0 -0
  182. package/src/ui/components/autocomplete_field.js +0 -0
  183. package/src/ui/components/cell_editor_host.js +293 -0
  184. package/src/ui/components/cell_editors.js +480 -0
  185. package/src/ui/components/cell_grid.js +761 -0
  186. package/src/ui/components/cell_paste.js +243 -0
  187. package/src/ui/components/cell_selection.js +220 -0
  188. package/src/ui/components/choice_controls.js +351 -0
  189. package/src/ui/components/color_field.js +211 -0
  190. package/src/ui/components/color_list_field.js +210 -0
  191. package/src/ui/components/column_fit.js +0 -0
  192. package/src/ui/components/column_groups.js +314 -0
  193. package/src/ui/components/column_order.js +142 -0
  194. package/src/ui/components/computing_status_window.js +98 -25
  195. package/src/ui/components/confirm_dialog.js +81 -38
  196. package/src/ui/components/context_menu.js +2 -2
  197. package/src/ui/components/conversation_view.js +440 -0
  198. package/src/ui/components/crosstab.js +289 -0
  199. package/src/ui/components/data_table.js +696 -71
  200. package/src/ui/components/detail_header.js +22 -11
  201. package/src/ui/components/document_outline.js +397 -0
  202. package/src/ui/components/drag_reorder.js +0 -0
  203. package/src/ui/components/fact_table.js +0 -0
  204. package/src/ui/components/fill_handle.js +322 -0
  205. package/src/ui/components/gallery_picker.js +19 -8
  206. package/src/ui/components/header_actions.js +55 -0
  207. package/src/ui/components/inline_renamer.js +0 -0
  208. package/src/ui/components/job_progress.js +51 -0
  209. package/src/ui/components/managed_window.js +27 -4
  210. package/src/ui/components/menu_bar.js +745 -0
  211. package/src/ui/components/modal.js +449 -75
  212. package/src/ui/components/notification_history.js +3 -3
  213. package/src/ui/components/progress.js +483 -0
  214. package/src/ui/components/record_list.js +0 -0
  215. package/src/ui/components/row_activation.js +0 -0
  216. package/src/ui/components/section_stack.js +298 -0
  217. package/src/ui/components/series.js +173 -0
  218. package/src/ui/components/settings_view.js +634 -0
  219. package/src/ui/components/slide_out_panel.js +43 -6
  220. package/src/ui/components/slider_field.js +275 -18
  221. package/src/ui/components/sortable_list.js +0 -0
  222. package/src/ui/components/staged_edits.js +361 -0
  223. package/src/ui/components/staged_history.js +172 -0
  224. package/src/ui/components/table_filter_chips.js +0 -0
  225. package/src/ui/components/table_state_store.js +0 -0
  226. package/src/ui/components/table_structure.js +182 -0
  227. package/src/ui/components/table_values.js +0 -0
  228. package/src/ui/components/tabs.js +497 -0
  229. package/src/ui/components/tag_input.js +215 -0
  230. package/src/ui/components/toast.js +25 -8
  231. package/src/ui/components/toggle_field.js +175 -0
  232. package/src/ui/components/tree_view.js +55 -34
  233. package/src/ui/components/tsv.js +74 -0
  234. package/src/ui/components/window_taskbar.js +0 -0
  235. package/src/ui/components/wizard.js +563 -0
  236. package/src/ui/controllers/window_chrome_controller.js +2 -2
  237. package/src/ui/notification_center.js +375 -119
  238. package/src/ui/utils/auto_scrollbars.js +0 -0
  239. package/src/ui/utils/markdown.js +709 -0
  240. package/src/ui/utils/native_context_menu.js +0 -0
  241. package/src/ui/utils/overlay_scrollbar.js +16 -12
  242. package/src/ui/utils/raf_resize_observer.js +0 -0
  243. package/src/ui/utils/scroll_lock.js +0 -0
  244. package/src/ui/utils/strip_scroll.js +55 -0
  245. package/src/ui/utils/tooltip_service.js +0 -8
  246. package/tiles.js +27 -0
  247. package/widgets.js +69 -0
  248. package/wm.js +28 -0
  249. package/dist/chunk-6JSOVNID.js.map +0 -7
  250. package/dist/chunk-B5Q53LAJ.js.map +0 -7
  251. package/dist/chunk-FBGP5KN5.js.map +0 -7
  252. package/dist/chunk-FL5KFNQH.js.map +0 -7
  253. package/dist/chunk-NLTOY57M.js.map +0 -7
  254. package/dist/chunk-NNFTETK3.js +0 -633
  255. package/dist/chunk-NNFTETK3.js.map +0 -7
  256. package/dist/chunk-O5OHMWBB.js +0 -284
  257. package/dist/chunk-O5OHMWBB.js.map +0 -7
  258. package/dist/chunk-RQM5Z2MU.js.map +0 -7
  259. package/dist/chunk-SN4LSOAR.js.map +0 -7
  260. package/dist/chunk-W6FBJLPW.js +0 -367
  261. package/dist/chunk-W6FBJLPW.js.map +0 -7
  262. package/src/ui/controllers/panel_state_machine.js +0 -634
@@ -0,0 +1,627 @@
1
+ /**
2
+ * plot_config_editor.js — edit a plot config (plot_config.js: `{ layout, subplots }`)
3
+ * in place.
4
+ *
5
+ * const editor = renderPlotConfigEditor(el, plotConfig, {
6
+ * onChange: () => grid.update({ plotConfig }),
7
+ * seriesProvider: ({ value, role, subplot }) => myNames(value), // or a promise
8
+ * });
9
+ * editor.dispose();
10
+ *
11
+ * It draws a layout picker, a tab per subplot slot (when the layout has more
12
+ * than one) and, for the subplot in front: its title; its chart type, legend,
13
+ * interpolation, NaN handling and data points; its x axis (time, or a series);
14
+ * its y axes (label, scale, position, min, max, step) and their series (a
15
+ * picker, label, colour, width, dash, stack group, the embedder's own fields
16
+ * and, with `overlaySources`, an overlay source); its z axis while the chart
17
+ * type is 3D; its reference lines and shaded bands; and the toggles of any
18
+ * injected `seriesTransforms` (series_transforms.js).
19
+ *
20
+ * Every edit writes the config object it was given and then calls
21
+ * `onChange(config)`. A layout with more slots than the config has subplots
22
+ * gets default subplots (makeDefaultSubplot) for the new slots; a layout with
23
+ * fewer keeps the extra subplots, unseen, so switching back restores them.
24
+ *
25
+ * A 3D chart type gives the subplot a z axis and uses its first y axis only:
26
+ * while one is chosen the other y axes are shown disabled and no y axis can be
27
+ * added. A 2D chart type removes the z axis (`zAxis: null`, as in
28
+ * makeDefaultSubplot).
29
+ *
30
+ * Series names come from `seriesProvider`, through an AutocompleteField per
31
+ * picker: it is asked `{ value, role, subplot }` (`role` is 'series', 'x' or
32
+ * 'z') and answers AutocompleteField items (`{ label, insertText?, type? }`)
33
+ * or a promise of them. One that throws or rejects shows no suggestions and
34
+ * leaves what was typed as it is. Nothing here knows what a series is.
35
+ *
36
+ * It fits PlotPopoutWindow's `configRenderer` slot, which disposes what the
37
+ * renderer returns when the window closes (openPlotPopoutWindow is async):
38
+ *
39
+ * const win = await openPlotPopoutWindow({ id, title, plotConfig, getResults, services,
40
+ * configRenderer: (el) => renderPlotConfigEditor(el, plotConfig,
41
+ * { onChange: () => win.updateConfig(plotConfig) }) });
42
+ */
43
+
44
+ import { AutocompleteField } from '../ui/components/autocomplete_field.js';
45
+ import { ColorField } from '../ui/components/color_field.js';
46
+ import { ToggleField } from '../ui/components/toggle_field.js';
47
+ import { selectBox } from '../ui/components/choice_controls.js';
48
+ import { createTabs } from '../ui/components/tabs.js';
49
+ import { CHART_TYPES_2D, CHART_TYPES_3D, is3DChart } from './chart_types.js';
50
+ import { PLOT_LAYOUTS, makeDefaultSubplot, makeDefaultYAxis, makeDefaultSeries } from './plot_config.js';
51
+ import { renderSeriesTransformControls } from './series_transforms.js';
52
+
53
+ const CHART_TYPE_OPTIONS = Object.freeze([
54
+ { group: '2D', options: CHART_TYPES_2D },
55
+ { group: '3D', options: CHART_TYPES_3D },
56
+ ]);
57
+ const SCALES = Object.freeze([{ value: 'linear', label: 'Linear' }, { value: 'log', label: 'Log' }]);
58
+ const POSITIONS = Object.freeze([{ value: 'left', label: 'Left' }, { value: 'right', label: 'Right' }]);
59
+ const DASHES = Object.freeze([
60
+ { value: 'solid', label: 'Solid' }, { value: 'dashed', label: 'Dashed' }, { value: 'dotted', label: 'Dotted' },
61
+ ]);
62
+ const LEGEND_POSITIONS = Object.freeze([
63
+ { value: 'hidden', label: 'Hidden' }, { value: 'top', label: 'Top' }, { value: 'bottom', label: 'Bottom' },
64
+ { value: 'left', label: 'Left' }, { value: 'right', label: 'Right' },
65
+ ]);
66
+ const INTERPOLATIONS = Object.freeze([
67
+ { value: 'linear', label: 'Linear' }, { value: 'step', label: 'Step' }, { value: 'cubic', label: 'Cubic spline' },
68
+ ]);
69
+ const NAN_STRATEGIES = Object.freeze([
70
+ { value: 'gap', label: 'Gap' }, { value: 'zero', label: 'Zero' },
71
+ { value: 'hold', label: 'Hold last value' }, { value: 'interpolate', label: 'Interpolate' },
72
+ ]);
73
+ const X_SOURCES = Object.freeze([{ value: 'time', label: 'Time' }, { value: 'series', label: 'Series' }]);
74
+ const OVERLAY_MODES = Object.freeze([
75
+ { value: 'markers+lines', label: 'Markers and lines' }, { value: 'markers', label: 'Markers' },
76
+ { value: 'lines', label: 'Lines' },
77
+ ]);
78
+
79
+ function el(tag, className, text) {
80
+ const node = document.createElement(tag);
81
+ if (className) node.className = className;
82
+ if (text != null) node.textContent = String(text);
83
+ return node;
84
+ }
85
+
86
+ function button(text, onClick, { title = null, className = 'twm-btn twm-plot-config__add' } = {}) {
87
+ const b = el('button', className, text);
88
+ b.type = 'button';
89
+ if (title) b.title = title;
90
+ b.addEventListener('click', () => { if (!b.disabled) onClick(); });
91
+ return b;
92
+ }
93
+
94
+ function removeButton(title, onClick) {
95
+ const b = button('', onClick, { title, className: 'twm-btn-icon twm-plot-config__remove' });
96
+ b.setAttribute('aria-label', title);
97
+ b.appendChild(el('span', 'material-symbols-outlined', 'close'));
98
+ return b;
99
+ }
100
+
101
+ /**
102
+ * A labelled control. A single input or select is wrapped in a `<label>`; a
103
+ * compound one (a picker, a colour field) is not, so a click on its text does
104
+ * not land on whichever of its inputs comes first.
105
+ */
106
+ function field(label, control) {
107
+ const single = control.matches('input, select, textarea');
108
+ const row = el(single ? 'label' : 'div', 'twm-plot-config__field');
109
+ row.append(el('span', 'twm-plot-config__label', label), control);
110
+ return row;
111
+ }
112
+
113
+ function textInput(value, onSet, { placeholder = '', label = null } = {}) {
114
+ const input = el('input', 'twm-plot-config__input');
115
+ input.type = 'text';
116
+ input.value = value ?? '';
117
+ input.placeholder = placeholder;
118
+ if (label) input.setAttribute('aria-label', label);
119
+ input.addEventListener('input', () => onSet(input.value));
120
+ return input;
121
+ }
122
+
123
+ /** A number box: empty (or not a number) sets null. */
124
+ function numberInput(value, onSet, { placeholder = 'Auto', label = null, min = null, max = null, step = 'any' } = {}) {
125
+ const input = el('input', 'twm-plot-config__input twm-plot-config__input--number');
126
+ input.type = 'number';
127
+ input.step = String(step);
128
+ if (min != null) input.min = String(min);
129
+ if (max != null) input.max = String(max);
130
+ input.value = value == null ? '' : String(value);
131
+ input.placeholder = placeholder;
132
+ if (label) input.setAttribute('aria-label', label);
133
+ input.addEventListener('change', () => {
134
+ const n = parseFloat(input.value);
135
+ onSet(Number.isFinite(n) ? n : null);
136
+ });
137
+ return input;
138
+ }
139
+
140
+ /** A select over choice options; `onPick(value)` gets the option's own value. */
141
+ function select(options, value, onPick, { label = null, empty = null } = {}) {
142
+ const s = selectBox(options, value, {
143
+ label: label ?? '', empty,
144
+ className: 'twm-plot-config__input twm-plot-config__input--select',
145
+ });
146
+ s.box.addEventListener('change', () => onPick(s.valueOf()));
147
+ return s.box;
148
+ }
149
+
150
+ function makeZAxis() {
151
+ return { label: '', scale: 'linear', min: null, max: null, step: null, variable: '' };
152
+ }
153
+
154
+ let editorCount = 0;
155
+
156
+ /**
157
+ * Edit `config` in place.
158
+ *
159
+ * @param {HTMLElement} container - Receives one `.twm-plot-config` child.
160
+ * @param {{ layout: string, subplots: Object[] }} config - Already migrated (migrateData).
161
+ * @param {Object} [options]
162
+ * @param {Function} [options.onChange] - `(config)` after every edit.
163
+ * @param {Function} [options.seriesProvider] - `({ value, role, subplot }) => items |
164
+ * Promise<items>`: the pickers' suggestions (AutocompleteField items).
165
+ * @param {Function} [options.overlaySources] - `() => options | Promise<options>`: choice
166
+ * options (`{ value, label }`, or `{ group, options }`) for a series' overlay
167
+ * source, written to `series.overlay.source`. Absent: no overlay controls.
168
+ * @param {Object} [options.seriesTransforms] - `{ [id]: transform }` (series_transforms.js);
169
+ * their toggles are drawn per subplot.
170
+ * @param {Function} [options.extraSeriesFields] - `(el, series, { subplot, axis, onChange })
171
+ * => { dispose? } | void`: fields of the embedder's own in each series row.
172
+ * @param {Object} [options.logger] - Given to every AutocompleteField (provider errors);
173
+ * its `warn` also hears a part (an `extraSeriesFields` handle, say) that throws on dispose.
174
+ * @returns {{ dispose: Function, refresh: Function, selectSubplot: Function,
175
+ * readonly activeSubplot: number }}
176
+ */
177
+ function renderPlotConfigEditor(container, config, options = {}) {
178
+ const opts = {
179
+ onChange: null,
180
+ seriesProvider: null,
181
+ overlaySources: null,
182
+ seriesTransforms: null,
183
+ extraSeriesFields: null,
184
+ logger: null,
185
+ ...options,
186
+ };
187
+ const editorId = `twm-plot-config-${++editorCount}`;
188
+ let fieldCount = 0;
189
+ let active = 0;
190
+ let disposed = false;
191
+ /** Teardown for everything the subplot body created; run before each redraw. */
192
+ let bodyDisposers = [];
193
+ /** Bumped on each body redraw, so an overlay-source answer for an old body is dropped. */
194
+ let generation = 0;
195
+
196
+ const notify = () => { if (!disposed) opts.onChange?.(config); };
197
+ const track = (fn) => { bodyDisposers.push(fn); };
198
+ const template = () => PLOT_LAYOUTS[config.layout] || PLOT_LAYOUTS['1x1'];
199
+ const slots = () => template().slots;
200
+
201
+ function ensureSubplots() {
202
+ if (!Array.isArray(config.subplots)) config.subplots = [];
203
+ while (config.subplots.length < slots()) {
204
+ config.subplots.push(makeDefaultSubplot(config.subplots.length));
205
+ }
206
+ }
207
+
208
+ const root = el('div', 'twm-plot-config');
209
+
210
+ // ── layout picker ───────────────────────────────────────────────────
211
+ const layoutButtons = new Map();
212
+ const picker = el('div', 'twm-plot-config__layouts');
213
+ picker.setAttribute('role', 'group');
214
+ picker.setAttribute('aria-label', 'Layout');
215
+ for (const [key, tmpl] of Object.entries(PLOT_LAYOUTS)) {
216
+ const b = button(tmpl.label, () => setLayout(key), { className: 'twm-btn twm-plot-config__layout' });
217
+ layoutButtons.set(key, b);
218
+ picker.appendChild(b);
219
+ }
220
+ const syncLayoutButtons = () => {
221
+ for (const [key, b] of layoutButtons) {
222
+ const on = key === config.layout;
223
+ b.setAttribute('aria-pressed', on ? 'true' : 'false');
224
+ b.classList.toggle('twm-plot-config__layout--on', on);
225
+ }
226
+ };
227
+ root.appendChild(section('Layout', picker));
228
+
229
+ // ── subplot tabs and the body ───────────────────────────────────────
230
+ const tabsHost = el('div', 'twm-plot-config__tabs');
231
+ const body = el('div', 'twm-plot-config__body');
232
+ root.append(tabsHost, body);
233
+ container.appendChild(root);
234
+
235
+ ensureSubplots();
236
+ const tabItems = () => Array.from({ length: slots() }, (_, i) => ({ id: i, label: `Subplot ${i + 1}` }));
237
+ const tabs = createTabs(tabsHost, {
238
+ items: tabItems(),
239
+ active: 0,
240
+ compact: true,
241
+ label: 'Subplots',
242
+ onSelect: (id) => {
243
+ if (id == null || id === active) return;
244
+ active = id;
245
+ renderBody();
246
+ },
247
+ });
248
+ const syncChrome = () => {
249
+ syncLayoutButtons();
250
+ tabs.update(tabItems(), active);
251
+ tabsHost.hidden = slots() < 2;
252
+ };
253
+
254
+ function setLayout(key) {
255
+ if (disposed || key === config.layout) return;
256
+ config.layout = key;
257
+ ensureSubplots();
258
+ active = Math.min(active, slots() - 1);
259
+ syncChrome();
260
+ renderBody();
261
+ notify();
262
+ }
263
+
264
+ function section(title, ...children) {
265
+ const s = el('section', 'twm-plot-config__section');
266
+ s.appendChild(el('h4', 'twm-plot-config__heading', title));
267
+ for (const child of children) if (child) s.appendChild(child);
268
+ return s;
269
+ }
270
+
271
+ function disposeBody() {
272
+ generation += 1;
273
+ const disposers = bodyDisposers;
274
+ bodyDisposers = [];
275
+ for (const fn of disposers) {
276
+ try { fn(); } catch (error) {
277
+ opts.logger?.warn?.('[plot config editor] a field failed to dispose', error);
278
+ }
279
+ }
280
+ }
281
+
282
+ // ── shared controls ─────────────────────────────────────────────────
283
+
284
+ /** A series-name picker writing `target[key]`. */
285
+ function seriesPicker(target, key, role, sp, placeholder) {
286
+ let alive = true;
287
+ const ac = new AutocompleteField({
288
+ fieldId: `${editorId}-field-${++fieldCount}`,
289
+ field: { placeholder, value: target[key] ?? '' },
290
+ skipNamespacePrefix: true,
291
+ logger: opts.logger,
292
+ provider: ({ value }) => (opts.seriesProvider
293
+ ? opts.seriesProvider({ value, role, subplot: sp })
294
+ : []),
295
+ onChange: (value) => {
296
+ // The field reports its value again on blur, after a redraw
297
+ // may have disposed it: a disposed field writes nothing.
298
+ if (!alive || (target[key] ?? '') === value) return;
299
+ target[key] = value;
300
+ notify();
301
+ },
302
+ });
303
+ track(() => { alive = false; ac.dispose(); });
304
+ const node = ac.render();
305
+ node.classList.add('twm-plot-config__picker');
306
+ return node;
307
+ }
308
+
309
+ function colorField(value, onSet, label) {
310
+ const f = new ColorField({
311
+ value: value ?? '',
312
+ label,
313
+ onChange: (v) => { if (v !== null) { onSet(v || null); notify(); } },
314
+ });
315
+ track(() => f.dispose());
316
+ return f.render();
317
+ }
318
+
319
+ function toggle(label, checked, onSet) {
320
+ const t = new ToggleField({ label, checked, compact: true, onChange: (on) => { onSet(on); notify(); } });
321
+ track(() => t.dispose());
322
+ return t.render();
323
+ }
324
+
325
+ /** Rows of objects under `sp[key]`, each with a remove button, and an add button. */
326
+ function listEditor(sp, key, { addLabel, makeItem, fields }) {
327
+ if (!Array.isArray(sp[key])) sp[key] = [];
328
+ const items = sp[key];
329
+ const wrap = el('div', 'twm-plot-config__list');
330
+ items.forEach((item, i) => {
331
+ const row = el('div', 'twm-plot-config__row');
332
+ row.append(...fields(item), removeButton('Remove', () => {
333
+ items.splice(i, 1);
334
+ renderBody();
335
+ notify();
336
+ }));
337
+ wrap.appendChild(row);
338
+ });
339
+ wrap.appendChild(button(addLabel, () => {
340
+ items.push(makeItem());
341
+ renderBody();
342
+ notify();
343
+ }));
344
+ return wrap;
345
+ }
346
+
347
+ // ── the subplot in front ────────────────────────────────────────────
348
+
349
+ function normalise(sp) {
350
+ if (!sp.xAxis) sp.xAxis = { useTime: true, variable: '' };
351
+ if (!Array.isArray(sp.yAxes) || !sp.yAxes.length) sp.yAxes = [makeDefaultYAxis(0)];
352
+ for (const axis of sp.yAxes) if (!Array.isArray(axis.series)) axis.series = [];
353
+ if (is3DChart(sp.chartType) && !sp.zAxis) sp.zAxis = makeZAxis();
354
+ }
355
+
356
+ function renderBody() {
357
+ if (disposed) return;
358
+ disposeBody();
359
+ body.replaceChildren();
360
+ const sp = config.subplots[active];
361
+ if (!sp) return;
362
+ normalise(sp);
363
+ const is3D = is3DChart(sp.chartType);
364
+ let sources = null;
365
+ const overlaySources = () => (sources ??= Promise.resolve().then(() => opts.overlaySources()));
366
+
367
+ body.append(
368
+ section('Subplot', field('Title', textInput(sp.displayName, (v) => { sp.displayName = v; notify(); },
369
+ { placeholder: 'Untitled' }))),
370
+ section('Display', ...displayFields(sp)),
371
+ section('X axis', ...xAxisFields(sp)),
372
+ section('Y axes', yAxesEditor(sp, is3D, overlaySources)),
373
+ is3D ? section('Z axis', ...zAxisFields(sp)) : null,
374
+ section('Reference lines', referenceLinesEditor(sp)),
375
+ section('Shaded bands', shadedBandsEditor(sp)),
376
+ transformsSection(sp),
377
+ );
378
+ }
379
+
380
+ function displayFields(sp) {
381
+ return [
382
+ field('Chart type', select(CHART_TYPE_OPTIONS, sp.chartType ?? 'line', (v) => {
383
+ const was3D = is3DChart(sp.chartType);
384
+ sp.chartType = v;
385
+ const now3D = is3DChart(v);
386
+ if (now3D) sp.zAxis ??= makeZAxis();
387
+ else sp.zAxis = null;
388
+ if (was3D !== now3D) renderBody();
389
+ notify();
390
+ })),
391
+ field('Legend', select(LEGEND_POSITIONS, sp.legendPosition ?? 'hidden',
392
+ (v) => { sp.legendPosition = v; notify(); })),
393
+ field('Interpolation', select(INTERPOLATIONS, sp.interpolation ?? 'linear',
394
+ (v) => { sp.interpolation = v; notify(); })),
395
+ field('Missing values', select(NAN_STRATEGIES, sp.nanHandling ?? 'gap',
396
+ (v) => { sp.nanHandling = v; notify(); })),
397
+ toggle('Data points', sp.showDataPoints === true, (on) => { sp.showDataPoints = on; }),
398
+ ];
399
+ }
400
+
401
+ function xAxisFields(sp) {
402
+ const useTime = sp.xAxis.useTime !== false;
403
+ const out = [field('Against', select(X_SOURCES, useTime ? 'time' : 'series', (v) => {
404
+ sp.xAxis.useTime = v === 'time';
405
+ renderBody();
406
+ notify();
407
+ }))];
408
+ if (!useTime) {
409
+ out.push(
410
+ field('Series', seriesPicker(sp.xAxis, 'variable', 'x', sp, 'X series')),
411
+ field('Label', textInput(sp.xAxis.label, (v) => { sp.xAxis.label = v; notify(); },
412
+ { placeholder: sp.xAxis.variable || 'Axis title' })),
413
+ );
414
+ }
415
+ return out;
416
+ }
417
+
418
+ function zAxisFields(sp) {
419
+ const z = sp.zAxis;
420
+ return [
421
+ field('Series', seriesPicker(z, 'variable', 'z', sp, 'Z series')),
422
+ field('Label', textInput(z.label, (v) => { z.label = v; notify(); }, { placeholder: 'Axis title' })),
423
+ field('Scale', select(SCALES, z.scale ?? 'linear', (v) => { z.scale = v; notify(); })),
424
+ field('Min', numberInput(z.min, (v) => { z.min = v; notify(); })),
425
+ field('Max', numberInput(z.max, (v) => { z.max = v; notify(); })),
426
+ field('Step', numberInput(z.step, (v) => { z.step = v; notify(); })),
427
+ ];
428
+ }
429
+
430
+ function yAxesEditor(sp, is3D, overlaySources) {
431
+ const wrap = el('div', 'twm-plot-config__axes');
432
+ sp.yAxes.forEach((axis, i) => wrap.appendChild(yAxisEditor(sp, axis, i, is3D, overlaySources)));
433
+ const add = button('Add Y axis', () => {
434
+ sp.yAxes.push(makeDefaultYAxis(sp.yAxes.length));
435
+ renderBody();
436
+ notify();
437
+ }, { title: is3D ? 'A 3D chart has one Y axis' : null });
438
+ add.disabled = is3D;
439
+ wrap.appendChild(add);
440
+ return wrap;
441
+ }
442
+
443
+ function yAxisEditor(sp, axis, index, is3D, overlaySources) {
444
+ const group = el('fieldset', 'twm-plot-config__axis');
445
+ const legend = el('legend', 'twm-plot-config__axis-title', `Y axis ${index + 1}`);
446
+ group.appendChild(legend);
447
+ if (sp.yAxes.length > 1) {
448
+ // Inside the legend, so it stays usable while the axis is disabled.
449
+ legend.appendChild(removeButton('Remove Y axis', () => {
450
+ sp.yAxes.splice(index, 1);
451
+ renderBody();
452
+ notify();
453
+ }));
454
+ }
455
+ if (is3D && index > 0) {
456
+ group.disabled = true;
457
+ group.classList.add('twm-plot-config__axis--disabled');
458
+ group.title = 'A 3D chart uses the first Y axis only';
459
+ }
460
+
461
+ const fields = el('div', 'twm-plot-config__axis-fields');
462
+ fields.append(
463
+ field('Label', textInput(axis.label, (v) => { axis.label = v; notify(); }, { placeholder: 'Axis title' })),
464
+ field('Scale', select(SCALES, axis.scale ?? 'linear', (v) => { axis.scale = v; notify(); })),
465
+ field('Position', select(POSITIONS, axis.position ?? 'left', (v) => { axis.position = v; notify(); })),
466
+ field('Min', numberInput(axis.min, (v) => { axis.min = v; notify(); })),
467
+ field('Max', numberInput(axis.max, (v) => { axis.max = v; notify(); })),
468
+ field('Step', numberInput(axis.step, (v) => { axis.step = v; notify(); })),
469
+ );
470
+ group.appendChild(fields);
471
+
472
+ const list = el('div', 'twm-plot-config__series-list');
473
+ if (!axis.series.length) list.appendChild(el('div', 'twm-plot-config__empty', 'No series.'));
474
+ for (const s of axis.series) list.appendChild(seriesRow(sp, axis, s, overlaySources));
475
+ group.appendChild(list);
476
+
477
+ group.appendChild(button('Add series', () => {
478
+ const count = sp.yAxes.reduce((n, a) => n + (a.series?.length ?? 0), 0);
479
+ axis.series.push(makeDefaultSeries(count));
480
+ renderBody();
481
+ notify();
482
+ }));
483
+ return group;
484
+ }
485
+
486
+ function seriesRow(sp, axis, s, overlaySources) {
487
+ const row = el('div', 'twm-plot-config__series');
488
+ const main = el('div', 'twm-plot-config__series-main');
489
+ main.append(
490
+ seriesPicker(s, 'variable', 'series', sp, 'Series'),
491
+ textInput(s.label, (v) => { s.label = v; notify(); }, { placeholder: 'Legend label', label: 'Legend label' }),
492
+ colorField(s.color, (v) => { if (v) s.color = v; else delete s.color; }, 'Colour'),
493
+ numberInput(s.lineWidth ?? 2, (v) => { s.lineWidth = v ?? 2; notify(); },
494
+ { placeholder: '2', label: 'Line width', min: 1, max: 10, step: 1 }),
495
+ select(DASHES, s.lineStyle ?? 'solid', (v) => { s.lineStyle = v; notify(); }, { label: 'Dash' }),
496
+ textInput(s.stackGroup, (v) => { s.stackGroup = v || null; notify(); },
497
+ { placeholder: 'Stack group', label: 'Stack group' }),
498
+ removeButton('Remove series', () => {
499
+ const i = axis.series.indexOf(s);
500
+ if (i >= 0) axis.series.splice(i, 1);
501
+ renderBody();
502
+ notify();
503
+ }),
504
+ );
505
+ row.appendChild(main);
506
+
507
+ if (opts.extraSeriesFields) {
508
+ const extra = el('div', 'twm-plot-config__series-extra');
509
+ row.appendChild(extra);
510
+ const handle = opts.extraSeriesFields(extra, s, { subplot: sp, axis, onChange: notify });
511
+ if (typeof handle?.dispose === 'function') track(() => handle.dispose());
512
+ }
513
+ if (opts.overlaySources) row.appendChild(overlayEditor(s, overlaySources));
514
+ return row;
515
+ }
516
+
517
+ function overlayEditor(s, overlaySources) {
518
+ const wrap = el('div', 'twm-plot-config__overlay');
519
+ const head = el('div', 'twm-plot-config__overlay-source', 'Loading overlay sources…');
520
+ const details = el('div', 'twm-plot-config__overlay-fields');
521
+ wrap.append(head, details);
522
+
523
+ const drawDetails = () => {
524
+ details.replaceChildren();
525
+ const ov = s.overlay;
526
+ if (!ov) return;
527
+ details.append(
528
+ field('Label', textInput(ov.label, (v) => { ov.label = v; notify(); },
529
+ { placeholder: `${s.label || s.variable || 'Series'} (actual)` })),
530
+ field('Colour', colorField(ov.color, (v) => { if (v) ov.color = v; else delete ov.color; }, 'Overlay colour')),
531
+ field('Dash', select(DASHES, ov.lineStyle ?? 'dashed', (v) => { ov.lineStyle = v; notify(); })),
532
+ field('Width', numberInput(ov.lineWidth ?? 2, (v) => { ov.lineWidth = v ?? 2; notify(); },
533
+ { placeholder: '2', min: 1, max: 10, step: 1 })),
534
+ field('Mode', select(OVERLAY_MODES, ov.mode ?? 'markers+lines', (v) => { ov.mode = v; notify(); })),
535
+ );
536
+ };
537
+
538
+ const gen = generation;
539
+ overlaySources().then((list) => {
540
+ if (disposed || gen !== generation) return;
541
+ head.replaceChildren(field('Overlay', select(Array.isArray(list) ? list : [], s.overlay?.source, (v) => {
542
+ if (v === undefined) delete s.overlay;
543
+ else s.overlay = { ...(s.overlay ?? {}), source: v };
544
+ drawDetails();
545
+ notify();
546
+ }, { empty: 'None' })));
547
+ drawDetails();
548
+ }, () => {
549
+ if (disposed || gen !== generation) return;
550
+ head.textContent = 'Could not load overlay sources.';
551
+ head.classList.add('twm-plot-config__overlay-source--error');
552
+ });
553
+ return wrap;
554
+ }
555
+
556
+ function referenceLinesEditor(sp) {
557
+ const axisOptions = sp.yAxes.map((a, i) => ({ value: a.id, label: a.label || `Y axis ${i + 1}` }));
558
+ return listEditor(sp, 'referenceLines', {
559
+ addLabel: 'Add reference line',
560
+ makeItem: () => ({ yAxisId: sp.yAxes[0].id, value: 0, label: '', lineStyle: 'dashed' }),
561
+ fields: (line) => [
562
+ select(axisOptions, line.yAxisId ?? sp.yAxes[0].id, (v) => { line.yAxisId = v; notify(); },
563
+ { label: 'Axis' }),
564
+ numberInput(line.value, (v) => { line.value = v; notify(); }, { placeholder: 'Value', label: 'Value' }),
565
+ textInput(line.label, (v) => { line.label = v; notify(); }, { placeholder: 'Label', label: 'Label' }),
566
+ colorField(line.color, (v) => { if (v) line.color = v; else delete line.color; }, 'Colour'),
567
+ select(DASHES, line.lineStyle ?? 'dashed', (v) => { line.lineStyle = v; notify(); }, { label: 'Dash' }),
568
+ ],
569
+ });
570
+ }
571
+
572
+ function shadedBandsEditor(sp) {
573
+ return listEditor(sp, 'shadedBands', {
574
+ addLabel: 'Add shaded band',
575
+ makeItem: () => ({ x0: null, x1: null, label: '' }),
576
+ fields: (band) => [
577
+ numberInput(band.x0, (v) => { band.x0 = v; notify(); }, { placeholder: 'From', label: 'From' }),
578
+ numberInput(band.x1, (v) => { band.x1 = v; notify(); }, { placeholder: 'To', label: 'To' }),
579
+ textInput(band.label, (v) => { band.label = v; notify(); }, { placeholder: 'Label', label: 'Label' }),
580
+ colorField(band.color, (v) => { if (v) band.color = v; else delete band.color; }, 'Colour'),
581
+ numberInput(band.opacity, (v) => {
582
+ if (v == null) delete band.opacity; else band.opacity = v;
583
+ notify();
584
+ }, { placeholder: 'Opacity', label: 'Opacity', min: 0, max: 1, step: 0.05 }),
585
+ ],
586
+ });
587
+ }
588
+
589
+ function transformsSection(sp) {
590
+ if (!Object.keys(opts.seriesTransforms ?? {}).length) return null;
591
+ const s = section('Derived series');
592
+ const controls = renderSeriesTransformControls(s, sp, opts.seriesTransforms, { onChange: notify });
593
+ track(() => controls.dispose());
594
+ return s;
595
+ }
596
+
597
+ syncChrome();
598
+ renderBody();
599
+
600
+ return {
601
+ /** Dispose every field it created and remove the editor. Later calls do nothing. */
602
+ dispose() {
603
+ if (disposed) return;
604
+ disposed = true;
605
+ disposeBody();
606
+ tabs.destroy();
607
+ root.remove();
608
+ },
609
+ /** Redraw from the config, after it was changed from outside. */
610
+ refresh() {
611
+ if (disposed) return;
612
+ ensureSubplots();
613
+ active = Math.min(active, slots() - 1);
614
+ syncChrome();
615
+ renderBody();
616
+ },
617
+ /** Bring subplot `index` to the front. */
618
+ selectSubplot(index) {
619
+ if (disposed || !Number.isInteger(index) || index < 0 || index >= slots()) return;
620
+ tabs.select(index);
621
+ },
622
+ /** The index of the subplot in front. */
623
+ get activeSubplot() { return active; },
624
+ };
625
+ }
626
+
627
+ export { renderPlotConfigEditor };