flexdesk 0.16.1 → 0.17.0

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 (252) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/LICENSE +0 -0
  3. package/README.md +2217 -42
  4. package/THIRD_PARTY_LICENSES.md +0 -0
  5. package/css/base.css +1834 -75
  6. package/css/flexdesk.css +3146 -164
  7. package/css/overrides.css +16 -0
  8. package/css/reset.css +0 -0
  9. package/css/tokens.css +0 -0
  10. package/dist/canvas.js +6 -239
  11. package/dist/canvas.js.map +3 -3
  12. package/dist/charts.js +709 -18
  13. package/dist/charts.js.map +4 -4
  14. package/dist/chunk-3PHCPZHT.js +0 -0
  15. package/dist/chunk-3PHCPZHT.js.map +0 -0
  16. package/dist/chunk-4A7SJC3T.js +503 -0
  17. package/dist/chunk-4A7SJC3T.js.map +7 -0
  18. package/dist/{chunk-6JSOVNID.js → chunk-CVMCZZX2.js} +635 -10
  19. package/dist/chunk-CVMCZZX2.js.map +7 -0
  20. package/dist/chunk-D3COLZZ2.js +1311 -0
  21. package/dist/chunk-D3COLZZ2.js.map +7 -0
  22. package/dist/chunk-FOOS3T5L.js +0 -0
  23. package/dist/chunk-FOOS3T5L.js.map +0 -0
  24. package/dist/{chunk-SN4LSOAR.js → chunk-GOQLR5WR.js} +452 -2
  25. package/dist/chunk-GOQLR5WR.js.map +7 -0
  26. package/dist/{chunk-FL5KFNQH.js → chunk-HNU6MM2B.js} +52 -17
  27. package/dist/chunk-HNU6MM2B.js.map +7 -0
  28. package/dist/chunk-HYJYYEPK.js +375 -0
  29. package/dist/chunk-HYJYYEPK.js.map +7 -0
  30. package/dist/chunk-JKZLNKG6.js +2041 -0
  31. package/dist/chunk-JKZLNKG6.js.map +7 -0
  32. package/dist/chunk-JYWURG5T.js +0 -0
  33. package/dist/chunk-JYWURG5T.js.map +0 -0
  34. package/dist/{chunk-RQM5Z2MU.js → chunk-MCIXFNVF.js} +633 -311
  35. package/dist/chunk-MCIXFNVF.js.map +7 -0
  36. package/dist/chunk-N7HC3ADV.js +30 -0
  37. package/dist/chunk-N7HC3ADV.js.map +7 -0
  38. package/dist/{chunk-FBGP5KN5.js → chunk-Q36EPT2R.js} +3268 -203
  39. package/dist/chunk-Q36EPT2R.js.map +7 -0
  40. package/dist/chunk-QWQJUDPD.js +610 -0
  41. package/dist/chunk-QWQJUDPD.js.map +7 -0
  42. package/dist/{chunk-NLTOY57M.js → chunk-UURZ37SD.js} +28 -48
  43. package/dist/chunk-UURZ37SD.js.map +7 -0
  44. package/dist/{chunk-B5Q53LAJ.js → chunk-UWO43BZU.js} +2 -2
  45. package/dist/chunk-UWO43BZU.js.map +7 -0
  46. package/dist/chunk-WVFGV5FT.js +0 -0
  47. package/dist/chunk-WVFGV5FT.js.map +0 -0
  48. package/dist/core.js +170 -118
  49. package/dist/core.js.map +2 -2
  50. package/dist/editor.js +156 -70
  51. package/dist/editor.js.map +2 -2
  52. package/dist/flexdesk.css +3146 -164
  53. package/dist/flow.js +2789 -306
  54. package/dist/flow.js.map +4 -4
  55. package/dist/host.js +109 -3
  56. package/dist/host.js.map +3 -3
  57. package/dist/reset.css +0 -0
  58. package/dist/tile_registry-6WZPTUZV.js +0 -0
  59. package/dist/tile_registry-6WZPTUZV.js.map +0 -0
  60. package/dist/tiles.js +657 -234
  61. package/dist/tiles.js.map +4 -4
  62. package/dist/tokens.css +0 -0
  63. package/dist/widgets.js +3252 -2124
  64. package/dist/widgets.js.map +4 -4
  65. package/dist/wm.js +1227 -347
  66. package/dist/wm.js.map +4 -4
  67. package/package.json +3 -1
  68. package/src/canvas/canvas_adapter.js +5 -0
  69. package/src/canvas/edge_router.js +0 -0
  70. package/src/canvas/graph/connector_router.js +0 -0
  71. package/src/canvas/graph/history_service.js +0 -0
  72. package/src/canvas/graph/node_platform.js +0 -0
  73. package/src/canvas/index.js +1 -0
  74. package/src/canvas/layered_layout.js +244 -0
  75. package/src/charts/chart_types.js +0 -0
  76. package/src/charts/data_series_plot_window.js +0 -0
  77. package/src/charts/downsample.js +2 -2
  78. package/src/charts/plot_config.js +53 -29
  79. package/src/charts/plot_config_editor.js +627 -0
  80. package/src/charts/plot_grid.js +449 -0
  81. package/src/charts/plot_popout_window.js +103 -317
  82. package/src/charts/plotly_wrapper.js +22 -2
  83. package/src/charts/series_transforms.js +206 -0
  84. package/src/core/event_bus.js +0 -0
  85. package/src/core/logging.js +1 -1
  86. package/src/core/settings.js +105 -19
  87. package/src/core/state_guard.js +93 -51
  88. package/src/core/state_guard_config.js +85 -74
  89. package/src/core/state_machine.js +0 -0
  90. package/src/editor/editor_pane.js +3 -3
  91. package/src/editor/monaco_editor_factory.js +124 -60
  92. package/src/editor/monaco_loader.js +103 -28
  93. package/src/editor/notebook_search.js +0 -0
  94. package/src/editor/notebook_tab_bar.js +38 -52
  95. package/src/editor/notebook_undo_manager.js +0 -0
  96. package/src/editor/split_pane_container.js +6 -3
  97. package/src/flow/canvas_editor/connections.js +90 -0
  98. package/src/flow/canvas_editor/editor.js +2351 -153
  99. package/src/flow/canvas_editor/index.js +2 -1
  100. package/src/flow/canvas_editor/layout.js +119 -12
  101. package/src/flow/canvas_editor/strings.js +80 -0
  102. package/src/flow/kit/always_before.js +0 -0
  103. package/src/flow/kit/catalogue.js +97 -3
  104. package/src/flow/kit/chip_input.js +0 -0
  105. package/src/flow/kit/dom.js +0 -0
  106. package/src/flow/kit/findings.js +0 -0
  107. package/src/flow/kit/history.js +0 -0
  108. package/src/flow/kit/index.js +3 -3
  109. package/src/flow/kit/keys.js +7 -1
  110. package/src/flow/kit/logic_graph.js +97 -15
  111. package/src/flow/kit/references.js +0 -0
  112. package/src/flow/kit/settings_panel.js +0 -0
  113. package/src/flow/kit/settings_schema.js +2 -1
  114. package/src/flow/kit/step_picker.js +0 -0
  115. package/src/flow/kit/strings.js +3 -1
  116. package/src/flow/kit/value_picker.js +0 -0
  117. package/src/flow/kit/widgets.js +86 -109
  118. package/src/flow/lanes/editor.js +136 -14
  119. package/src/flow/lanes/index.js +3 -1
  120. package/src/flow/lanes/layout.js +0 -0
  121. package/src/flow/lanes/pipeline.js +44 -2
  122. package/src/flow/lanes/preview.js +0 -0
  123. package/src/flow/lanes/strings.js +1 -0
  124. package/src/flow/outline/editor.js +0 -0
  125. package/src/flow/outline/index.js +0 -0
  126. package/src/flow/outline/mapping.js +0 -0
  127. package/src/flow/outline/menu.js +0 -0
  128. package/src/flow/outline/operations.js +0 -0
  129. package/src/flow/outline/recognise.js +0 -0
  130. package/src/flow/outline/strings.js +0 -0
  131. package/src/help/help_modal.js +168 -42
  132. package/src/help/help_registry.js +12 -1
  133. package/src/host/external_links.js +0 -0
  134. package/src/host/file_picker.js +117 -0
  135. package/src/host/host.js +10 -2
  136. package/src/host/photino_host.js +9 -0
  137. package/src/host/photino_host.md +21 -3
  138. package/src/host/pywebview_host.js +10 -0
  139. package/src/tiles/chart_tiles.js +53 -245
  140. package/src/tiles/config_schema.js +8 -0
  141. package/src/tiles/figure_tile.js +322 -0
  142. package/src/tiles/layout_persistence.js +3 -3
  143. package/src/tiles/list_tiles.js +7 -7
  144. package/src/tiles/scope_picker.js +0 -0
  145. package/src/tiles/tile_base.js +1 -1
  146. package/src/tiles/tile_config_form.js +442 -0
  147. package/src/tiles/tile_grid.js +2 -0
  148. package/src/tiles/tile_registry.js +0 -0
  149. package/src/tiling/bottom_panel_tabs.js +67 -99
  150. package/src/tiling/command_palette.js +64 -19
  151. package/src/tiling/content_registry.js +4 -1
  152. package/src/tiling/desktops.js +11 -1
  153. package/src/tiling/entity_sources.js +0 -0
  154. package/src/tiling/keymap.js +403 -128
  155. package/src/tiling/kind_taxonomy.js +78 -0
  156. package/src/tiling/landing_table.js +0 -0
  157. package/src/tiling/loading_overlay.js +0 -0
  158. package/src/tiling/nav_panel.js +59 -10
  159. package/src/tiling/page_factory.js +5 -5
  160. package/src/tiling/panel_availability.js +49 -0
  161. package/src/tiling/panel_keys.js +0 -0
  162. package/src/tiling/shell.js +114 -12
  163. package/src/tiling/tab_strip.js +0 -0
  164. package/src/tiling/tile_breadcrumb.js +0 -0
  165. package/src/tiling/tile_renderer.js +105 -9
  166. package/src/tiling/tile_tab_menu.js +0 -0
  167. package/src/tiling/tile_tree.js +0 -0
  168. package/src/tiling/wm.js +562 -30
  169. package/src/tiling/zoom.js +0 -0
  170. package/src/ui/base/component_base.js +0 -0
  171. package/src/ui/base/controller_base.js +0 -0
  172. package/src/ui/base/page_base.js +0 -0
  173. package/src/ui/components/about_dialog.js +0 -0
  174. package/src/ui/components/action_dropdown.js +30 -2
  175. package/src/ui/components/alert_log.js +0 -0
  176. package/src/ui/components/alerts_pane.js +0 -0
  177. package/src/ui/components/attribute_list_editor.js +0 -0
  178. package/src/ui/components/autocomplete_field.js +0 -0
  179. package/src/ui/components/cell_editor_host.js +211 -0
  180. package/src/ui/components/cell_grid.js +648 -0
  181. package/src/ui/components/cell_selection.js +220 -0
  182. package/src/ui/components/choice_controls.js +351 -0
  183. package/src/ui/components/color_field.js +211 -0
  184. package/src/ui/components/color_list_field.js +210 -0
  185. package/src/ui/components/column_fit.js +0 -0
  186. package/src/ui/components/column_groups.js +314 -0
  187. package/src/ui/components/column_order.js +142 -0
  188. package/src/ui/components/computing_status_window.js +98 -25
  189. package/src/ui/components/confirm_dialog.js +81 -38
  190. package/src/ui/components/context_menu.js +2 -2
  191. package/src/ui/components/conversation_view.js +440 -0
  192. package/src/ui/components/crosstab.js +289 -0
  193. package/src/ui/components/data_table.js +601 -62
  194. package/src/ui/components/detail_header.js +22 -11
  195. package/src/ui/components/document_outline.js +397 -0
  196. package/src/ui/components/drag_reorder.js +0 -0
  197. package/src/ui/components/fact_table.js +0 -0
  198. package/src/ui/components/fill_handle.js +322 -0
  199. package/src/ui/components/gallery_picker.js +19 -8
  200. package/src/ui/components/header_actions.js +55 -0
  201. package/src/ui/components/inline_renamer.js +0 -0
  202. package/src/ui/components/job_progress.js +51 -0
  203. package/src/ui/components/managed_window.js +27 -4
  204. package/src/ui/components/menu_bar.js +745 -0
  205. package/src/ui/components/modal.js +449 -75
  206. package/src/ui/components/notification_history.js +3 -3
  207. package/src/ui/components/progress.js +483 -0
  208. package/src/ui/components/record_list.js +0 -0
  209. package/src/ui/components/row_activation.js +0 -0
  210. package/src/ui/components/section_stack.js +298 -0
  211. package/src/ui/components/series.js +173 -0
  212. package/src/ui/components/settings_view.js +634 -0
  213. package/src/ui/components/slide_out_panel.js +43 -6
  214. package/src/ui/components/slider_field.js +275 -18
  215. package/src/ui/components/sortable_list.js +0 -0
  216. package/src/ui/components/staged_edits.js +343 -0
  217. package/src/ui/components/staged_history.js +172 -0
  218. package/src/ui/components/table_filter_chips.js +0 -0
  219. package/src/ui/components/table_state_store.js +0 -0
  220. package/src/ui/components/table_structure.js +182 -0
  221. package/src/ui/components/table_values.js +0 -0
  222. package/src/ui/components/tabs.js +497 -0
  223. package/src/ui/components/tag_input.js +215 -0
  224. package/src/ui/components/toast.js +25 -8
  225. package/src/ui/components/toggle_field.js +175 -0
  226. package/src/ui/components/tree_view.js +55 -34
  227. package/src/ui/components/window_taskbar.js +0 -0
  228. package/src/ui/components/wizard.js +563 -0
  229. package/src/ui/controllers/window_chrome_controller.js +2 -2
  230. package/src/ui/notification_center.js +375 -119
  231. package/src/ui/utils/auto_scrollbars.js +0 -0
  232. package/src/ui/utils/markdown.js +709 -0
  233. package/src/ui/utils/native_context_menu.js +0 -0
  234. package/src/ui/utils/overlay_scrollbar.js +16 -12
  235. package/src/ui/utils/raf_resize_observer.js +0 -0
  236. package/src/ui/utils/scroll_lock.js +0 -0
  237. package/src/ui/utils/strip_scroll.js +55 -0
  238. package/src/ui/utils/tooltip_service.js +0 -8
  239. package/dist/chunk-6JSOVNID.js.map +0 -7
  240. package/dist/chunk-B5Q53LAJ.js.map +0 -7
  241. package/dist/chunk-FBGP5KN5.js.map +0 -7
  242. package/dist/chunk-FL5KFNQH.js.map +0 -7
  243. package/dist/chunk-NLTOY57M.js.map +0 -7
  244. package/dist/chunk-NNFTETK3.js +0 -633
  245. package/dist/chunk-NNFTETK3.js.map +0 -7
  246. package/dist/chunk-O5OHMWBB.js +0 -284
  247. package/dist/chunk-O5OHMWBB.js.map +0 -7
  248. package/dist/chunk-RQM5Z2MU.js.map +0 -7
  249. package/dist/chunk-SN4LSOAR.js.map +0 -7
  250. package/dist/chunk-W6FBJLPW.js +0 -367
  251. package/dist/chunk-W6FBJLPW.js.map +0 -7
  252. package/src/ui/controllers/panel_state_machine.js +0 -634
@@ -0,0 +1,449 @@
1
+ /**
2
+ * plot_grid.js — draw a plot config into any container.
3
+ *
4
+ * A plot config (plot_config.js: `{ layout, subplots }`) is a grid of subplots,
5
+ * each with one or more y-axes of series. `renderPlotGrid` lays the grid out
6
+ * from PLOT_LAYOUTS, draws one Plotly figure per subplot and keeps them drawn:
7
+ *
8
+ * const grid = renderPlotGrid(el, plotConfig, { time, series }, { overlays });
9
+ * await grid.update({ results: next }); // new data, same grid
10
+ * grid.resize(); // after the container changed size
11
+ * grid.dispose(); // purge every figure, remove the grid
12
+ *
13
+ * Per subplot it draws: the chart type, the interpolation, data points, stack
14
+ * groups, every y-axis (left/right, linear/log, min/max/step), reference lines
15
+ * and shaded bands, the NaN strategy, the x-axis clipped to the results' time
16
+ * range — or an x variable instead of time (`subplot.xAxis = { useTime: false,
17
+ * variable }`) — per-series overlay data, injected series transforms, and
18
+ * whatever extra traces the embedder derives. A subplot with nothing to draw
19
+ * shows the empty message; a grid slot with no subplot shows another.
20
+ *
21
+ * A figure is drawn only into a cell that is in the document and has a size.
22
+ * One that is not (a hidden tab, a window not yet shown) is remembered and
23
+ * drawn the next time the grid is resized, which its own ResizeObserver does
24
+ * when the container gets a size.
25
+ *
26
+ * It was PlotPopoutWindow's private renderer; the window now calls this.
27
+ */
28
+
29
+ import { createRafResizeObserver } from '../ui/utils/raf_resize_observer.js';
30
+ import { ensurePlotly } from './plotly_wrapper.js';
31
+ import { buildTrace, buildYAxisLayout, buildLegendLayout, buildOverlayShapes } from './chart_types.js';
32
+ import { PLOT_LAYOUTS, applyNanHandling } from './plot_config.js';
33
+ import { deriveSeriesTraces } from './series_transforms.js';
34
+
35
+ /** The figure colours and font. `options.theme` overrides any of them. */
36
+ const PLOT_GRID_THEME = Object.freeze({
37
+ fontFamily: 'Inter, -apple-system, BlinkMacSystemFont, sans-serif',
38
+ fontColor: '#cccccc',
39
+ fontSize: 11,
40
+ paperBackground: 'transparent',
41
+ plotBackground: 'transparent',
42
+ gridColor: 'rgba(255,255,255,0.05)',
43
+ lineColor: 'rgba(255,255,255,0.08)',
44
+ tickColor: 'rgba(255,255,255,0.3)',
45
+ zeroLineColor: 'rgba(255,255,255,0.06)',
46
+ hoverBackground: '#1e2228',
47
+ hoverBorder: '#444444',
48
+ });
49
+
50
+ const OVERLAY_DASH = { dashed: 'dash', dotted: 'dot', solid: 'solid' };
51
+
52
+ /**
53
+ * The default series lookup: `results.series[series._resultKey ?? series.variable]`,
54
+ * then `series.variable`, then the variable without its `prefix.`. Its own x is
55
+ * `series._resultTime` when it has one.
56
+ *
57
+ * @param {Object} series - A series of a subplot's y-axis.
58
+ * @param {{ time?: number[], series?: Object }} results
59
+ * @returns {{ y: number[], x?: number[] } | null}
60
+ */
61
+ function resolveSeriesData(series, results) {
62
+ const table = results?.series;
63
+ const variable = series?.variable;
64
+ if (!table || !variable) return null;
65
+ let y = table[series._resultKey ?? variable];
66
+ if (!y && series._resultKey) y = table[variable];
67
+ if (!y) {
68
+ const dot = variable.indexOf('.');
69
+ if (dot >= 0) y = table[variable.slice(dot + 1)];
70
+ }
71
+ if (!y) return null;
72
+ return series._resultTime ? { y, x: series._resultTime } : { y };
73
+ }
74
+
75
+ /**
76
+ * Draw `plotConfig` into `container`.
77
+ *
78
+ * @param {HTMLElement} container - Receives one `.twm-plot-grid` child.
79
+ * @param {{ layout: string, subplots: Object[] }} plotConfig - Already migrated (migrateData).
80
+ * @param {{ time: number[], series: Object } | null} results - null: every subplot is empty.
81
+ * @param {Object} [options]
82
+ * @param {Function} [options.resolveSeries] - `(series, results, subplot) => { y, x? } | null`;
83
+ * default resolveSeriesData. Also resolves the x variable, as `{ variable }`.
84
+ * @param {Object} [options.overlays] - `{ [series.id]: { years, values } }`, drawn for
85
+ * every series with an `overlay` description.
86
+ * @param {Object} [options.seriesTransforms] - `{ [id]: transform }` (series_transforms.js).
87
+ * @param {Function} [options.onTransformError] - `({ transformId, traceName, error })`.
88
+ * @param {Function} [options.derivedTraces] - `(traces, subplot, { results, index })
89
+ * => trace[] | Promise<trace[]>`: extra traces appended to a subplot's figure.
90
+ * @param {Set<string>|string[]} [options.hiddenSeries] - Trace names not to draw.
91
+ * @param {Object} [options.theme] - Overrides for PLOT_GRID_THEME.
92
+ * @param {boolean} [options.alwaysShowLegend=false] - Draw a 'hidden' legend at the top.
93
+ * @param {Object} [options.layout] - Plotly layout fields merged over every figure's.
94
+ * @param {string} [options.emptyMessage='No data yet.']
95
+ * @param {string} [options.noSubplotMessage='No subplot configured.']
96
+ * @param {Function} [options.onError] - `({ index, error })` when drawing a subplot
97
+ * throws — on an update or on a resize that draws a deferred one; that
98
+ * subplot shows the empty message.
99
+ * @returns {{ update: Function, resize: Function, dispose: Function,
100
+ * readonly charts: HTMLElement[], readonly plotted: HTMLElement[] }}
101
+ */
102
+ function renderPlotGrid(container, plotConfig, results, options = {}) {
103
+ const opts = {
104
+ resolveSeries: resolveSeriesData,
105
+ overlays: null,
106
+ seriesTransforms: null,
107
+ onTransformError: null,
108
+ derivedTraces: null,
109
+ hiddenSeries: null,
110
+ theme: null,
111
+ alwaysShowLegend: false,
112
+ layout: null,
113
+ emptyMessage: 'No data yet.',
114
+ noSubplotMessage: 'No subplot configured.',
115
+ onError: null,
116
+ ...options,
117
+ };
118
+ let config = plotConfig;
119
+ let data = results;
120
+ let disposed = false;
121
+ let cells = [];
122
+ let cellsLayout = null;
123
+ /**
124
+ * Per cell: the latest draw's token, from one grid-wide counter that a
125
+ * rebuild never resets, so no draw for an old layout shares a token with a
126
+ * draw for the new one. An older draw that resolves late is dropped.
127
+ */
128
+ let tokens = [];
129
+ let lastToken = 0;
130
+ /** Cells whose last draw found them detached or zero-size. */
131
+ const deferred = new Set();
132
+
133
+ const grid = document.createElement('div');
134
+ grid.className = 'twm-plot-grid';
135
+ container.appendChild(grid);
136
+
137
+ const observer = createRafResizeObserver(() => resize());
138
+ observer.observe(grid);
139
+
140
+ /** Cells Plotly has been asked to draw into: every one of them is purged. */
141
+ const plotted = new Set();
142
+ /** Cells whose latest Plotly draw finished: they show a figure. */
143
+ const drawn = new Set();
144
+ const hasPlot = (cell) => plotted.has(cell);
145
+ const purgeCell = (cell) => {
146
+ drawn.delete(cell);
147
+ if (!plotted.delete(cell)) return;
148
+ try { globalThis.Plotly?.purge(cell); } catch (_) { /* already gone */ }
149
+ };
150
+
151
+ function buildCells() {
152
+ for (const cell of cells) purgeCell(cell);
153
+ grid.replaceChildren();
154
+ const tmpl = PLOT_LAYOUTS[config?.layout] || PLOT_LAYOUTS['1x1'];
155
+ grid.style.gridTemplateColumns = tmpl.grid.columns;
156
+ grid.style.gridTemplateRows = tmpl.grid.rows;
157
+ cells = tmpl.areas.map(({ gridArea }) => {
158
+ const cell = document.createElement('div');
159
+ cell.className = 'twm-plot-grid__cell';
160
+ cell.style.gridArea = gridArea;
161
+ grid.appendChild(cell);
162
+ return cell;
163
+ });
164
+ cellsLayout = config?.layout;
165
+ tokens = cells.map(() => 0);
166
+ deferred.clear();
167
+ }
168
+
169
+ function showMessage(cell, text) {
170
+ purgeCell(cell);
171
+ const msg = document.createElement('div');
172
+ msg.className = 'twm-plot-no-data';
173
+ msg.textContent = text;
174
+ cell.replaceChildren(msg);
175
+ }
176
+
177
+ const theme = () => ({ ...PLOT_GRID_THEME, ...(opts.theme ?? {}) });
178
+ const isHidden = (name) => {
179
+ const h = opts.hiddenSeries;
180
+ if (!h) return false;
181
+ return typeof h.has === 'function' ? h.has(name) : h.includes(name);
182
+ };
183
+ const axisColours = (t) => ({
184
+ gridcolor: t.gridColor, linecolor: t.lineColor,
185
+ tickcolor: t.tickColor, zerolinecolor: t.zeroLineColor,
186
+ });
187
+
188
+ /** The traces and layout of one subplot, or null when it has nothing to draw. */
189
+ async function buildFigure(sp, index) {
190
+ // No results is the empty message, whatever is hidden.
191
+ if (!data) return null;
192
+ const t = theme();
193
+ const time = data.time ?? [];
194
+ const traces = [];
195
+ const layoutAxes = {};
196
+ const yRefByAxisId = {};
197
+ /** The subplot's own series (overlays and derived traces aside), and how many are hidden. */
198
+ let ownCount = 0;
199
+ let hiddenCount = 0;
200
+
201
+ const useX = sp.xAxis && sp.xAxis.useTime === false && sp.xAxis.variable;
202
+ const xData = useX
203
+ ? (opts.resolveSeries({ variable: sp.xAxis.variable }, data, sp)?.y ?? time)
204
+ : time;
205
+
206
+ const yAxes = sp.yAxes ?? [];
207
+ yAxes.forEach((axis, a) => {
208
+ const n = a + 1;
209
+ const yAxisKey = n === 1 ? 'yaxis' : `yaxis${n}`;
210
+ const yRef = n === 1 ? 'y' : `y${n}`;
211
+ if (axis.id) yRefByAxisId[axis.id] = yRef;
212
+
213
+ layoutAxes[yAxisKey] = {
214
+ ...buildYAxisLayout({
215
+ axisId: yAxisKey,
216
+ title: axis.label || '',
217
+ position: axis.position || 'left',
218
+ scaleType: axis.scale || 'linear',
219
+ min: axis.min ?? null,
220
+ max: axis.max ?? null,
221
+ step: axis.step ?? null,
222
+ showGrid: yAxes.length === 1,
223
+ axisPosition: n > 1 ? (axis.position === 'right' ? 1 : 0) : null,
224
+ }),
225
+ ...axisColours(t),
226
+ };
227
+
228
+ for (const s of axis.series ?? []) {
229
+ if (!s.variable) continue;
230
+ const label = s.label || s.variable;
231
+ ownCount++;
232
+ if (isHidden(label)) { hiddenCount++; continue; }
233
+
234
+ const resolved = opts.resolveSeries(s, data, sp);
235
+ if (!resolved?.y) continue;
236
+ const x = resolved.x ?? xData;
237
+ const y = Array.isArray(resolved.y)
238
+ ? applyNanHandling(resolved.y, sp.nanHandling ?? 'gap', x)
239
+ : resolved.y;
240
+ traces.push(buildTrace({
241
+ chartType: sp.chartType || 'line',
242
+ x,
243
+ y,
244
+ name: label,
245
+ color: s.color,
246
+ lineWidth: s.lineWidth ?? 2,
247
+ lineStyle: s.lineStyle ?? 'solid',
248
+ interpolation: sp.interpolation ?? 'linear',
249
+ yAxisId: yRef,
250
+ showMarkers: sp.showDataPoints ?? false,
251
+ stackGroup: s.stackGroup ?? null,
252
+ }));
253
+
254
+ const overlay = s.overlay ? opts.overlays?.[s.id] : null;
255
+ if (!overlay?.years?.length) continue;
256
+ const ov = s.overlay;
257
+ const ovLabel = ov.label || `${label} (actual)`;
258
+ if (isHidden(ovLabel)) continue;
259
+ const ovTrace = {
260
+ type: 'scatter',
261
+ mode: ov.mode || 'markers+lines',
262
+ x: overlay.years,
263
+ y: overlay.values,
264
+ name: ovLabel,
265
+ yaxis: yRef === 'y' ? undefined : yRef,
266
+ line: { color: ov.color || s.color, width: ov.lineWidth ?? 2,
267
+ dash: OVERLAY_DASH[ov.lineStyle] || 'dash' },
268
+ marker: { color: ov.color || s.color, size: 4, symbol: 'circle' },
269
+ showlegend: true,
270
+ };
271
+ // A stacked-percent chart stacks its overlays too, in a group of
272
+ // their own, so they read on the same 0..1 scale.
273
+ if ((sp.chartType || 'line') === 'area-stacked') {
274
+ ovTrace.stackgroup = `overlay-${yRef}`;
275
+ ovTrace.groupnorm = 'fraction';
276
+ ovTrace.fill = 'none';
277
+ ovTrace.mode = 'lines';
278
+ }
279
+ traces.push(ovTrace);
280
+ }
281
+ });
282
+
283
+ traces.push(...await deriveSeriesTraces(traces, sp, opts.seriesTransforms,
284
+ { onError: opts.onTransformError ?? undefined }));
285
+ if (opts.derivedTraces) {
286
+ const extra = await opts.derivedTraces(traces, sp, { results: data, index });
287
+ if (Array.isArray(extra)) traces.push(...extra);
288
+ }
289
+
290
+ // Nothing to draw is the empty message — unless there are results and
291
+ // every one of the subplot's own series is hidden: then it is an empty
292
+ // figure, so the viewer can tell "hidden" from "no data".
293
+ if (traces.length === 0 && !(ownCount > 0 && hiddenCount === ownCount)) return null;
294
+
295
+ const legendPos = opts.alwaysShowLegend && (sp.legendPosition ?? 'hidden') === 'hidden'
296
+ ? 'top' : (sp.legendPosition ?? 'hidden');
297
+ const xRange = !useX && time.length >= 2 ? [time[0], time[time.length - 1]] : undefined;
298
+ const { shapes, annotations } = buildOverlayShapes({
299
+ referenceLines: sp.referenceLines,
300
+ shadedBands: sp.shadedBands,
301
+ yRefByAxisId,
302
+ });
303
+ const font = { family: t.fontFamily, color: t.fontColor };
304
+
305
+ const layout = {
306
+ ...layoutAxes,
307
+ ...(shapes.length ? { shapes } : {}),
308
+ ...(annotations.length ? { annotations } : {}),
309
+ xaxis: {
310
+ ...(useX ? { title: { text: sp.xAxis.label || sp.xAxis.variable, font: { size: t.fontSize } } } : {}),
311
+ ...axisColours(t),
312
+ automargin: true,
313
+ ...(xRange ? { range: xRange } : {}),
314
+ },
315
+ ...buildLegendLayout(legendPos),
316
+ paper_bgcolor: t.paperBackground,
317
+ plot_bgcolor: t.plotBackground,
318
+ font: { ...font, size: t.fontSize },
319
+ margin: { t: 10, r: 20, b: 40, l: 50 },
320
+ hoverlabel: { bgcolor: t.hoverBackground, bordercolor: t.hoverBorder, font },
321
+ ...(opts.layout ?? {}),
322
+ };
323
+ return { traces, layout };
324
+ }
325
+
326
+ const hasSize = (cell) => cell.isConnected && cell.offsetWidth > 0 && cell.offsetHeight > 0;
327
+
328
+ /** Draw cell `index`; resolves true when a figure was drawn. */
329
+ async function drawCell(index) {
330
+ const cell = cells[index];
331
+ const token = ++lastToken;
332
+ tokens[index] = token;
333
+ const current = () => !disposed && cells[index] === cell && tokens[index] === token;
334
+ const sp = config?.subplots?.[index];
335
+ if (!sp) { showMessage(cell, opts.noSubplotMessage); deferred.delete(index); return false; }
336
+
337
+ const figure = await buildFigure(sp, index);
338
+ if (!current()) return false;
339
+ if (!figure) { showMessage(cell, opts.emptyMessage); deferred.delete(index); return false; }
340
+
341
+ if (!hasSize(cell)) { deferred.add(index); return false; }
342
+ deferred.delete(index);
343
+
344
+ const Plotly = await ensurePlotly();
345
+ if (!current()) return false;
346
+ if (!hasPlot(cell)) cell.replaceChildren();
347
+ plotted.add(cell);
348
+ try {
349
+ await Plotly.react(cell, figure.traces, figure.layout, { responsive: true, displayModeBar: false });
350
+ if (plotted.has(cell)) drawn.add(cell);
351
+ return true;
352
+ } catch (_) {
353
+ // Plotly throws when the cell is removed mid-draw.
354
+ drawn.delete(cell);
355
+ return false;
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Draw cell `index` and handle a failure: a resolver, transform or
361
+ * derivedTraces that throws costs its own subplot only — it shows the empty
362
+ * message and is reported to onError. Every draw goes through here.
363
+ */
364
+ function drawCellOrFail(index) {
365
+ const cell = cells[index];
366
+ const pending = drawCell(index);
367
+ const token = tokens[index];
368
+ return pending.catch((error) => {
369
+ if (disposed) return false;
370
+ if (cells[index] === cell && tokens[index] === token) {
371
+ deferred.delete(index);
372
+ showMessage(cell, opts.emptyMessage);
373
+ }
374
+ opts.onError?.({ index, error });
375
+ return false;
376
+ });
377
+ }
378
+
379
+ async function drawAll() {
380
+ if (disposed) return false;
381
+ if (cellsLayout !== config?.layout || !cells.length) buildCells();
382
+ const results = await Promise.all(cells.map((_, i) => drawCellOrFail(i)));
383
+ return results.some(Boolean);
384
+ }
385
+
386
+ /**
387
+ * Change what is drawn and redraw. Every field is optional; omitted ones keep
388
+ * their value. A config with another layout rebuilds the grid.
389
+ *
390
+ * @param {Object} [patch]
391
+ * @param {Object} [patch.plotConfig]
392
+ * @param {Object|null} [patch.results]
393
+ * @param {Object|null} [patch.overlays]
394
+ * @param {Set<string>|string[]|null} [patch.hiddenSeries]
395
+ * @returns {Promise<boolean>} whether any figure was drawn
396
+ */
397
+ function update(patch = {}) {
398
+ if (disposed) return Promise.resolve(false);
399
+ if ('plotConfig' in patch) config = patch.plotConfig;
400
+ if ('results' in patch) data = patch.results;
401
+ if ('overlays' in patch) opts.overlays = patch.overlays;
402
+ if ('hiddenSeries' in patch) opts.hiddenSeries = patch.hiddenSeries;
403
+ return drawAll();
404
+ }
405
+
406
+ /** Fit every figure to its cell, and draw the ones that were waiting for a size. */
407
+ function resize() {
408
+ if (disposed) return;
409
+ for (const index of [...deferred]) {
410
+ if (!hasSize(cells[index])) continue;
411
+ drawCellOrFail(index);
412
+ }
413
+ const Plotly = globalThis.Plotly;
414
+ if (!Plotly?.Plots?.resize) return;
415
+ for (const cell of cells) {
416
+ if (hasPlot(cell) && hasSize(cell)) {
417
+ Promise.resolve().then(() => Plotly.Plots.resize(cell)).catch(() => {});
418
+ }
419
+ }
420
+ }
421
+
422
+ /** Purge every figure and remove the grid. Later calls do nothing. */
423
+ function dispose() {
424
+ if (disposed) return;
425
+ disposed = true;
426
+ observer.disconnect();
427
+ for (const cell of cells) purgeCell(cell);
428
+ cells = [];
429
+ deferred.clear();
430
+ grid.remove();
431
+ }
432
+
433
+ drawAll();
434
+
435
+ return {
436
+ update,
437
+ resize,
438
+ dispose,
439
+ /** The cells, one per grid slot, in layout order. */
440
+ get charts() { return [...cells]; },
441
+ /**
442
+ * The cells that show a figure now, in layout order — however it got
443
+ * there: an update, or a deferred draw on a resize.
444
+ */
445
+ get plotted() { return cells.filter((cell) => drawn.has(cell)); },
446
+ };
447
+ }
448
+
449
+ export { renderPlotGrid, resolveSeriesData, PLOT_GRID_THEME };