flexdesk 0.1.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 (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +195 -0
  3. package/THIRD_PARTY_LICENSES.md +53 -0
  4. package/css/base.css +3439 -0
  5. package/css/flexdesk.css +5797 -0
  6. package/css/overrides.css +2349 -0
  7. package/css/reset.css +46 -0
  8. package/css/tokens.css +137 -0
  9. package/dist/charts.js +780 -0
  10. package/dist/charts.js.map +7 -0
  11. package/dist/chunk-3PHCPZHT.js +222 -0
  12. package/dist/chunk-3PHCPZHT.js.map +7 -0
  13. package/dist/chunk-CT4YXXLP.js +1770 -0
  14. package/dist/chunk-CT4YXXLP.js.map +7 -0
  15. package/dist/chunk-DRYCDMEG.js +2217 -0
  16. package/dist/chunk-DRYCDMEG.js.map +7 -0
  17. package/dist/chunk-DVU44T77.js +855 -0
  18. package/dist/chunk-DVU44T77.js.map +7 -0
  19. package/dist/chunk-FL5KFNQH.js +640 -0
  20. package/dist/chunk-FL5KFNQH.js.map +7 -0
  21. package/dist/chunk-FOOS3T5L.js +117 -0
  22. package/dist/chunk-FOOS3T5L.js.map +7 -0
  23. package/dist/chunk-JYWURG5T.js +32 -0
  24. package/dist/chunk-JYWURG5T.js.map +7 -0
  25. package/dist/chunk-TLZUUFOE.js +276 -0
  26. package/dist/chunk-TLZUUFOE.js.map +7 -0
  27. package/dist/chunk-UCJ2WD4D.js +625 -0
  28. package/dist/chunk-UCJ2WD4D.js.map +7 -0
  29. package/dist/chunk-WVFGV5FT.js +205 -0
  30. package/dist/chunk-WVFGV5FT.js.map +7 -0
  31. package/dist/core.js +1098 -0
  32. package/dist/core.js.map +7 -0
  33. package/dist/editor.js +1661 -0
  34. package/dist/editor.js.map +7 -0
  35. package/dist/flexdesk.css +5797 -0
  36. package/dist/host.js +132 -0
  37. package/dist/host.js.map +7 -0
  38. package/dist/reset.css +46 -0
  39. package/dist/tile_registry-6WZPTUZV.js +28 -0
  40. package/dist/tile_registry-6WZPTUZV.js.map +7 -0
  41. package/dist/tiles.js +2068 -0
  42. package/dist/tiles.js.map +7 -0
  43. package/dist/tokens.css +137 -0
  44. package/dist/widgets.js +5438 -0
  45. package/dist/widgets.js.map +7 -0
  46. package/dist/wm.js +4830 -0
  47. package/dist/wm.js.map +7 -0
  48. package/package.json +80 -0
  49. package/src/charts/chart_types.js +699 -0
  50. package/src/charts/data_series_plot_window.js +761 -0
  51. package/src/charts/downsample.js +119 -0
  52. package/src/charts/plot_config.js +176 -0
  53. package/src/charts/plot_popout_window.js +1560 -0
  54. package/src/charts/plotly_wrapper.js +432 -0
  55. package/src/core/event_bus.js +230 -0
  56. package/src/core/logging.js +300 -0
  57. package/src/core/settings.js +813 -0
  58. package/src/core/state_guard.js +382 -0
  59. package/src/core/state_guard_config.js +309 -0
  60. package/src/core/state_machine.js +258 -0
  61. package/src/editor/editor_pane.js +264 -0
  62. package/src/editor/monaco_editor_factory.js +449 -0
  63. package/src/editor/monaco_loader.js +74 -0
  64. package/src/editor/notebook_search.js +460 -0
  65. package/src/editor/notebook_tab_bar.js +456 -0
  66. package/src/editor/notebook_undo_manager.js +108 -0
  67. package/src/editor/split_pane_container.js +197 -0
  68. package/src/help/help_modal.js +409 -0
  69. package/src/help/help_registry.js +65 -0
  70. package/src/host/host.js +69 -0
  71. package/src/host/pywebview_host.js +133 -0
  72. package/src/tiles/config_schema.js +256 -0
  73. package/src/tiles/layout_persistence.js +662 -0
  74. package/src/tiles/tile_base.js +785 -0
  75. package/src/tiles/tile_grid.js +905 -0
  76. package/src/tiles/tile_registry.js +136 -0
  77. package/src/tiling/command_palette.js +328 -0
  78. package/src/tiling/content_registry.js +110 -0
  79. package/src/tiling/desktops.js +133 -0
  80. package/src/tiling/entity_sources.js +179 -0
  81. package/src/tiling/keymap.js +156 -0
  82. package/src/tiling/kind_taxonomy.js +172 -0
  83. package/src/tiling/landing_table.js +717 -0
  84. package/src/tiling/loading_overlay.js +33 -0
  85. package/src/tiling/nav_panel.js +142 -0
  86. package/src/tiling/page_factory.js +215 -0
  87. package/src/tiling/panel_keys.js +145 -0
  88. package/src/tiling/shell.js +437 -0
  89. package/src/tiling/tile_breadcrumb.js +222 -0
  90. package/src/tiling/tile_renderer.js +459 -0
  91. package/src/tiling/tile_tab_menu.js +340 -0
  92. package/src/tiling/tile_tree.js +880 -0
  93. package/src/tiling/wm.js +1352 -0
  94. package/src/ui/base/component_base.js +30 -0
  95. package/src/ui/base/controller_base.js +48 -0
  96. package/src/ui/base/page_base.js +102 -0
  97. package/src/ui/components/about_dialog.js +224 -0
  98. package/src/ui/components/action_dropdown.js +389 -0
  99. package/src/ui/components/attribute_list_editor.js +652 -0
  100. package/src/ui/components/autocomplete_field.js +732 -0
  101. package/src/ui/components/computing_status_window.js +81 -0
  102. package/src/ui/components/confirm_dialog.js +307 -0
  103. package/src/ui/components/context_menu.js +87 -0
  104. package/src/ui/components/data_table.js +2228 -0
  105. package/src/ui/components/detail_header.js +177 -0
  106. package/src/ui/components/drag_reorder.js +0 -0
  107. package/src/ui/components/gallery_picker.js +147 -0
  108. package/src/ui/components/inline_renamer.js +129 -0
  109. package/src/ui/components/managed_window.js +804 -0
  110. package/src/ui/components/modal.js +512 -0
  111. package/src/ui/components/notification_history.js +264 -0
  112. package/src/ui/components/slide_out_panel.js +185 -0
  113. package/src/ui/components/slider_field.js +122 -0
  114. package/src/ui/components/sortable_list.js +309 -0
  115. package/src/ui/components/table_state_store.js +81 -0
  116. package/src/ui/components/toast.js +59 -0
  117. package/src/ui/components/tree_view.js +460 -0
  118. package/src/ui/components/window_taskbar.js +143 -0
  119. package/src/ui/controllers/panel_state_machine.js +634 -0
  120. package/src/ui/controllers/window_chrome_controller.js +659 -0
  121. package/src/ui/notification_center.js +576 -0
  122. package/src/ui/utils/overlay_scrollbar.js +492 -0
  123. package/src/ui/utils/raf_resize_observer.js +31 -0
  124. package/src/ui/utils/tooltip_service.js +406 -0
@@ -0,0 +1,2217 @@
1
+ import {
2
+ DataTable,
3
+ createRafResizeObserver
4
+ } from "./chunk-CT4YXXLP.js";
5
+ import {
6
+ ManagedWindow
7
+ } from "./chunk-UCJ2WD4D.js";
8
+
9
+ // src/charts/plotly_wrapper.js
10
+ var _plotlySrc = null;
11
+ function setPlotlySource(src) {
12
+ if (typeof src === "string" && src) _plotlySrc = src;
13
+ }
14
+ var _plotlyLoadPromise = null;
15
+ var _plotlyPatchesInstalled = false;
16
+ function _installPlotlyPatches() {
17
+ if (_plotlyPatchesInstalled) return;
18
+ _plotlyPatchesInstalled = true;
19
+ const targetDesc = Object.getOwnPropertyDescriptor(Event.prototype, "target");
20
+ if (targetDesc && targetDesc.get && !targetDesc.set) {
21
+ const origGetter = targetDesc.get;
22
+ Object.defineProperty(Event.prototype, "target", {
23
+ configurable: true,
24
+ enumerable: true,
25
+ get() {
26
+ return this._plotlyTarget ?? origGetter.call(this);
27
+ },
28
+ set(v) {
29
+ this._plotlyTarget = v;
30
+ }
31
+ });
32
+ }
33
+ const _origGetContext = HTMLCanvasElement.prototype.getContext;
34
+ HTMLCanvasElement.prototype.getContext = function(type, attrs) {
35
+ if (type === "2d") {
36
+ attrs = Object.assign({ willReadFrequently: true }, attrs);
37
+ }
38
+ return _origGetContext.call(this, type, attrs);
39
+ };
40
+ }
41
+ if (typeof HTMLCanvasElement !== "undefined") {
42
+ _installPlotlyPatches();
43
+ }
44
+ var DARK_THEME_LAYOUT = {
45
+ paper_bgcolor: "transparent",
46
+ plot_bgcolor: "transparent",
47
+ font: {
48
+ family: "Inter, -apple-system, BlinkMacSystemFont, sans-serif",
49
+ size: 11,
50
+ color: "#cccccc"
51
+ },
52
+ margin: { l: 50, r: 20, t: 30, b: 40 },
53
+ legend: {
54
+ bgcolor: "rgba(0,0,0,0)",
55
+ font: { color: "#cccccc" }
56
+ },
57
+ xaxis: {
58
+ gridcolor: "#333333",
59
+ linecolor: "#444444",
60
+ tickcolor: "#666666",
61
+ zerolinecolor: "#444444"
62
+ },
63
+ yaxis: {
64
+ gridcolor: "#333333",
65
+ linecolor: "#444444",
66
+ tickcolor: "#666666",
67
+ zerolinecolor: "#444444"
68
+ },
69
+ // Hover tooltip styling for dark theme
70
+ hoverlabel: {
71
+ bgcolor: "#1e2228",
72
+ bordercolor: "#444444",
73
+ font: {
74
+ family: "Inter, -apple-system, BlinkMacSystemFont, sans-serif",
75
+ size: 12,
76
+ color: "#e0e0e0"
77
+ }
78
+ },
79
+ // 3D scene defaults
80
+ scene: {
81
+ xaxis: {
82
+ gridcolor: "#333333",
83
+ linecolor: "#444444",
84
+ backgroundcolor: "rgba(0,0,0,0)"
85
+ },
86
+ yaxis: {
87
+ gridcolor: "#333333",
88
+ linecolor: "#444444",
89
+ backgroundcolor: "rgba(0,0,0,0)"
90
+ },
91
+ zaxis: {
92
+ gridcolor: "#333333",
93
+ linecolor: "#444444",
94
+ backgroundcolor: "rgba(0,0,0,0)"
95
+ },
96
+ bgcolor: "rgba(0,0,0,0)"
97
+ }
98
+ };
99
+ var DEFAULT_CONFIG = {
100
+ responsive: true,
101
+ displaylogo: false,
102
+ displayModeBar: false,
103
+ // Hide modebar completely
104
+ scrollZoom: true,
105
+ // Allow zoom with scroll wheel instead
106
+ toImageButtonOptions: {
107
+ format: "png",
108
+ filename: "ecosim_chart",
109
+ scale: 2
110
+ }
111
+ };
112
+ async function ensurePlotly() {
113
+ if (typeof Plotly !== "undefined") {
114
+ return Plotly;
115
+ }
116
+ if (_plotlyLoadPromise) {
117
+ return _plotlyLoadPromise;
118
+ }
119
+ if (!_plotlySrc) {
120
+ throw new Error(
121
+ '[twm/charts] Plotly is not loaded and no source is configured. Either load Plotly yourself (a <script> tag that sets window.Plotly), or call setPlotlySource("/path/to/plotly.min.js") before rendering a chart.'
122
+ );
123
+ }
124
+ _plotlyLoadPromise = new Promise((resolve, reject) => {
125
+ const savedDefine = window.define;
126
+ window.define = void 0;
127
+ const script = document.createElement("script");
128
+ script.src = _plotlySrc;
129
+ script.async = true;
130
+ script.onload = () => {
131
+ window.define = savedDefine;
132
+ if (typeof Plotly !== "undefined") {
133
+ console.log("[PlotlyWrapper] Plotly.js loaded successfully");
134
+ resolve(Plotly);
135
+ } else {
136
+ reject(new Error("Plotly.js loaded but Plotly global not found"));
137
+ }
138
+ };
139
+ script.onerror = () => {
140
+ window.define = savedDefine;
141
+ reject(new Error(`Failed to load Plotly.js from ${_plotlySrc} \u2014 call setPlotlySource() with the path to your copy.`));
142
+ };
143
+ document.head.appendChild(script);
144
+ });
145
+ return _plotlyLoadPromise;
146
+ }
147
+ async function createChart(container, data, layout = {}, config = {}) {
148
+ const Plotly2 = await ensurePlotly();
149
+ const mergedLayout = {
150
+ ...DARK_THEME_LAYOUT,
151
+ ...layout
152
+ };
153
+ const mergedConfig = {
154
+ ...DEFAULT_CONFIG,
155
+ ...config
156
+ };
157
+ return Plotly2.newPlot(container, data, mergedLayout, mergedConfig);
158
+ }
159
+ async function updateChart(container, data, layout = {}) {
160
+ const Plotly2 = await ensurePlotly();
161
+ const mergedLayout = {
162
+ ...DARK_THEME_LAYOUT,
163
+ ...layout
164
+ };
165
+ return Plotly2.react(container, data, mergedLayout);
166
+ }
167
+ async function extendTraces(container, update, traceIndices, maxPoints = null) {
168
+ const Plotly2 = await ensurePlotly();
169
+ if (maxPoints) {
170
+ return Plotly2.extendTraces(container, update, traceIndices, maxPoints);
171
+ }
172
+ return Plotly2.extendTraces(container, update, traceIndices);
173
+ }
174
+ async function relayout(container, layoutUpdate) {
175
+ const Plotly2 = await ensurePlotly();
176
+ return Plotly2.relayout(container, layoutUpdate);
177
+ }
178
+ async function restyle(container, styleUpdate, traceIndices) {
179
+ const Plotly2 = await ensurePlotly();
180
+ return Plotly2.restyle(container, styleUpdate, traceIndices);
181
+ }
182
+ async function purge(container) {
183
+ const Plotly2 = await ensurePlotly();
184
+ return Plotly2.purge(container);
185
+ }
186
+ function hasPlot(container) {
187
+ return container && container._fullLayout !== void 0;
188
+ }
189
+ async function resize(container) {
190
+ const Plotly2 = await ensurePlotly();
191
+ return Plotly2.Plots.resize(container);
192
+ }
193
+ function hexToRgba(hex, alpha = 1) {
194
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
195
+ if (!result) return hex;
196
+ const r = parseInt(result[1], 16);
197
+ const g = parseInt(result[2], 16);
198
+ const b = parseInt(result[3], 16);
199
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
200
+ }
201
+ var COLOR_PALETTE = [
202
+ "#268bd2",
203
+ // Solarized Blue
204
+ "#d33682",
205
+ // Solarized Magenta
206
+ "#cb4b16",
207
+ // Solarized Orange
208
+ "#2aa198",
209
+ // Solarized Cyan
210
+ "#6a5acd",
211
+ // SlateBlue/Purple
212
+ "#7a7d80",
213
+ // Gray
214
+ "#9fb6cf"
215
+ // Gray-Blue
216
+ ];
217
+ function getSeriesColor(index) {
218
+ return COLOR_PALETTE[index % COLOR_PALETTE.length];
219
+ }
220
+ async function createChartDeferred(container, traces, layout = {}, config = {}) {
221
+ const Plotly2 = await ensurePlotly();
222
+ const mergedLayout = {
223
+ ...DARK_THEME_LAYOUT,
224
+ ...layout
225
+ };
226
+ const mergedConfig = {
227
+ ...DEFAULT_CONFIG,
228
+ ...config
229
+ };
230
+ const w = container.offsetWidth;
231
+ const h = container.offsetHeight;
232
+ let resizeObserver = null;
233
+ const cleanup = () => {
234
+ if (resizeObserver) {
235
+ resizeObserver.disconnect();
236
+ resizeObserver = null;
237
+ }
238
+ };
239
+ if (w > 0 && h > 0) {
240
+ await Plotly2.newPlot(container, traces, mergedLayout, mergedConfig);
241
+ resizeObserver = createRafResizeObserver(() => {
242
+ if (hasPlot(container)) {
243
+ Plotly2.Plots.resize(container).catch(() => {
244
+ });
245
+ }
246
+ });
247
+ resizeObserver.observe(container);
248
+ return { element: container, cleanup };
249
+ }
250
+ return new Promise((resolve) => {
251
+ let rendered = false;
252
+ resizeObserver = createRafResizeObserver((entries) => {
253
+ for (const entry of entries) {
254
+ const { width, height } = entry.contentRect;
255
+ if (width > 0 && height > 0) {
256
+ if (!rendered) {
257
+ rendered = true;
258
+ Plotly2.newPlot(container, traces, mergedLayout, mergedConfig).then(() => {
259
+ resolve({ element: container, cleanup });
260
+ }).catch(() => {
261
+ resolve({ element: container, cleanup });
262
+ });
263
+ } else if (hasPlot(container)) {
264
+ Plotly2.Plots.resize(container).catch(() => {
265
+ });
266
+ }
267
+ }
268
+ }
269
+ });
270
+ resizeObserver.observe(container);
271
+ });
272
+ }
273
+
274
+ // src/charts/chart_types.js
275
+ var CHART_TYPES_2D = Object.freeze([
276
+ { value: "line", label: "Line" },
277
+ { value: "bar", label: "Bar" },
278
+ { value: "scatter", label: "Scatter" },
279
+ { value: "area", label: "Area" },
280
+ { value: "area-stacked", label: "Area (Stacked %)" },
281
+ { value: "phase", label: "Phase" }
282
+ ]);
283
+ var CHART_TYPES_3D = Object.freeze([
284
+ { value: "scatter3d", label: "3D Scatter" },
285
+ { value: "line3d", label: "3D Line" },
286
+ { value: "surface", label: "3D Surface" }
287
+ ]);
288
+ var ALL_CHART_TYPES = Object.freeze([...CHART_TYPES_2D, ...CHART_TYPES_3D]);
289
+ function is3DChart(chartType) {
290
+ return ["scatter3d", "line3d", "surface"].includes(chartType);
291
+ }
292
+ var LINE_DASH_MAP = {
293
+ solid: "solid",
294
+ dashed: "dash",
295
+ dotted: "dot"
296
+ };
297
+ var INTERPOLATION_MAP = {
298
+ linear: "linear",
299
+ step: "hv",
300
+ // horizontal-vertical step
301
+ cubic: "spline",
302
+ // smooth spline
303
+ auto: "linear"
304
+ };
305
+ function buildTrace(options) {
306
+ const {
307
+ chartType = "line",
308
+ x = [],
309
+ y = [],
310
+ z = null,
311
+ name = "Series",
312
+ color = null,
313
+ lineWidth = 2,
314
+ lineStyle = "solid",
315
+ interpolation = "linear",
316
+ yAxisId = "y",
317
+ showMarkers = false,
318
+ seriesIndex = 0,
319
+ stackGroup = null
320
+ } = options;
321
+ const seriesColor = color || getSeriesColor(seriesIndex);
322
+ let plotlyType, mode;
323
+ switch (chartType) {
324
+ case "line":
325
+ plotlyType = "scatter";
326
+ mode = showMarkers ? "lines+markers" : "lines";
327
+ break;
328
+ case "scatter":
329
+ plotlyType = "scatter";
330
+ mode = "markers";
331
+ break;
332
+ case "area":
333
+ case "area-stacked":
334
+ plotlyType = "scatter";
335
+ mode = showMarkers ? "lines+markers" : "lines";
336
+ break;
337
+ case "phase":
338
+ plotlyType = "scatter";
339
+ mode = "lines+markers";
340
+ break;
341
+ case "bar":
342
+ plotlyType = "bar";
343
+ mode = void 0;
344
+ break;
345
+ case "scatter3d":
346
+ plotlyType = "scatter3d";
347
+ mode = "markers";
348
+ break;
349
+ case "line3d":
350
+ plotlyType = "scatter3d";
351
+ mode = showMarkers ? "lines+markers" : "lines";
352
+ break;
353
+ case "surface":
354
+ plotlyType = "surface";
355
+ mode = void 0;
356
+ break;
357
+ default:
358
+ plotlyType = "scatter";
359
+ mode = showMarkers ? "lines+markers" : "lines";
360
+ }
361
+ const trace = {
362
+ type: plotlyType,
363
+ name,
364
+ x,
365
+ y
366
+ };
367
+ if (mode) {
368
+ trace.mode = mode;
369
+ }
370
+ if (z && is3DChart(chartType)) {
371
+ if (chartType === "surface") {
372
+ const zIs2D = Array.isArray(z) && z.length > 0 && Array.isArray(z[0]);
373
+ if (zIs2D) {
374
+ trace.z = z;
375
+ } else {
376
+ console.info("[chart_types] Surface chart with 1D z data - using mesh3d with Delaunay triangulation");
377
+ trace.type = "mesh3d";
378
+ trace.x = x;
379
+ trace.y = y;
380
+ trace.z = z;
381
+ trace.alphahull = 0;
382
+ trace.opacity = 0.8;
383
+ trace.color = seriesColor;
384
+ trace.flatshading = true;
385
+ trace.lighting = {
386
+ ambient: 0.6,
387
+ diffuse: 0.5,
388
+ specular: 0.2
389
+ };
390
+ return trace;
391
+ }
392
+ } else {
393
+ trace.z = z;
394
+ }
395
+ }
396
+ if (chartType === "phase") {
397
+ trace.line = {
398
+ color: seriesColor,
399
+ width: lineWidth,
400
+ dash: LINE_DASH_MAP[lineStyle] || "solid"
401
+ };
402
+ trace.marker = {
403
+ color: seriesColor,
404
+ size: 3
405
+ };
406
+ return trace;
407
+ }
408
+ if (["line", "area", "area-stacked", "line3d"].includes(chartType)) {
409
+ trace.line = {
410
+ color: seriesColor,
411
+ width: lineWidth,
412
+ dash: LINE_DASH_MAP[lineStyle] || "solid",
413
+ shape: INTERPOLATION_MAP[interpolation] || "linear"
414
+ };
415
+ }
416
+ if (["scatter", "scatter3d"].includes(chartType) || showMarkers) {
417
+ trace.marker = {
418
+ color: seriesColor,
419
+ size: chartType === "scatter3d" ? 4 : 6
420
+ };
421
+ }
422
+ if (chartType === "area-stacked") {
423
+ trace.stackgroup = yAxisId;
424
+ trace.groupnorm = "fraction";
425
+ trace.fillcolor = hexToRgba(seriesColor, 0.5);
426
+ trace.line = { ...trace.line || {}, width: 0.5, color: seriesColor };
427
+ trace.mode = "lines";
428
+ } else if (chartType === "area" && !stackGroup) {
429
+ trace.fill = "tozeroy";
430
+ trace.fillcolor = hexToRgba(seriesColor, 0.3);
431
+ } else if (stackGroup) {
432
+ trace.stackgroup = stackGroup;
433
+ trace.fillcolor = hexToRgba(seriesColor, 0.5);
434
+ trace.line = { ...trace.line || {}, width: 0.5, color: seriesColor };
435
+ trace.mode = "lines";
436
+ }
437
+ if (chartType === "bar") {
438
+ trace.marker = {
439
+ color: seriesColor
440
+ };
441
+ }
442
+ if (chartType === "surface") {
443
+ trace.colorscale = "Viridis";
444
+ trace.showscale = true;
445
+ }
446
+ if (!is3DChart(chartType) && yAxisId && yAxisId !== "y") {
447
+ trace.yaxis = yAxisId;
448
+ }
449
+ return trace;
450
+ }
451
+ function buildYAxisLayout(options) {
452
+ const {
453
+ axisId = "yaxis",
454
+ title = "",
455
+ position = "left",
456
+ scaleType = "linear",
457
+ min = null,
458
+ max = null,
459
+ step = null,
460
+ showGrid = true,
461
+ axisPosition = null
462
+ } = options;
463
+ const layout = {
464
+ title: { text: title },
465
+ type: scaleType === "log" ? "log" : "linear",
466
+ side: position,
467
+ gridcolor: "rgba(255,255,255,0.05)",
468
+ linecolor: "rgba(255,255,255,0.08)",
469
+ tickcolor: "rgba(255,255,255,0.3)",
470
+ zerolinecolor: "rgba(255,255,255,0.06)",
471
+ showgrid: showGrid,
472
+ automargin: true
473
+ // Auto-expand margin for tick labels
474
+ };
475
+ if (min !== null && max !== null) {
476
+ layout.autorange = false;
477
+ layout.range = [min, max];
478
+ } else if (min !== null || max !== null) {
479
+ layout.autorange = true;
480
+ layout.autorangeoptions = {};
481
+ if (min !== null) layout.autorangeoptions.minallowed = min;
482
+ if (max !== null) layout.autorangeoptions.maxallowed = max;
483
+ }
484
+ if (step !== null && step > 0) {
485
+ layout.dtick = step;
486
+ }
487
+ if (axisId !== "yaxis") {
488
+ layout.overlaying = "y";
489
+ layout.anchor = "free";
490
+ if (axisPosition !== null) {
491
+ layout.position = axisPosition;
492
+ } else {
493
+ layout.position = position === "left" ? 0 : 1;
494
+ }
495
+ }
496
+ return layout;
497
+ }
498
+ function buildXAxisLayout(options) {
499
+ const {
500
+ title = "Time",
501
+ scaleType = "linear",
502
+ min = null,
503
+ max = null
504
+ } = options;
505
+ const layout = {
506
+ title: { text: title },
507
+ type: scaleType === "log" ? "log" : "linear",
508
+ gridcolor: "rgba(255,255,255,0.05)",
509
+ linecolor: "rgba(255,255,255,0.08)",
510
+ tickcolor: "rgba(255,255,255,0.3)",
511
+ zerolinecolor: "rgba(255,255,255,0.06)",
512
+ automargin: true
513
+ };
514
+ if (min !== null || max !== null) {
515
+ layout.range = [min, max];
516
+ }
517
+ return layout;
518
+ }
519
+ function build3DSceneLayout(options) {
520
+ const {
521
+ xTitle = "X",
522
+ yTitle = "Y",
523
+ zTitle = "Z",
524
+ zScale = "linear",
525
+ zMin = null,
526
+ zMax = null,
527
+ zStep = null
528
+ } = options;
529
+ const zaxis = {
530
+ title: { text: zTitle },
531
+ type: zScale === "log" ? "log" : "linear",
532
+ gridcolor: "rgba(255,255,255,0.05)",
533
+ linecolor: "rgba(255,255,255,0.08)",
534
+ backgroundcolor: "rgba(0,0,0,0)"
535
+ };
536
+ if (zMin !== null || zMax !== null) {
537
+ zaxis.range = [zMin, zMax];
538
+ }
539
+ if (zStep !== null && zStep > 0) {
540
+ zaxis.dtick = zStep;
541
+ }
542
+ return {
543
+ xaxis: {
544
+ title: { text: xTitle },
545
+ gridcolor: "rgba(255,255,255,0.05)",
546
+ linecolor: "rgba(255,255,255,0.08)",
547
+ backgroundcolor: "rgba(0,0,0,0)"
548
+ },
549
+ yaxis: {
550
+ title: { text: yTitle },
551
+ gridcolor: "rgba(255,255,255,0.05)",
552
+ linecolor: "rgba(255,255,255,0.08)",
553
+ backgroundcolor: "rgba(0,0,0,0)"
554
+ },
555
+ zaxis,
556
+ bgcolor: "rgba(0,0,0,0)"
557
+ };
558
+ }
559
+ function buildLegendLayout(position) {
560
+ if (position === "hidden") {
561
+ return { showlegend: false };
562
+ }
563
+ const baseLayout = {
564
+ showlegend: true,
565
+ bgcolor: "rgba(0,0,0,0)",
566
+ font: { color: "#cccccc" }
567
+ };
568
+ switch (position) {
569
+ case "top":
570
+ return {
571
+ ...baseLayout,
572
+ legend: {
573
+ ...baseLayout,
574
+ orientation: "h",
575
+ y: 1.1,
576
+ yanchor: "bottom",
577
+ x: 0.5,
578
+ xanchor: "center"
579
+ }
580
+ };
581
+ case "bottom":
582
+ return {
583
+ ...baseLayout,
584
+ legend: {
585
+ ...baseLayout,
586
+ orientation: "h",
587
+ y: -0.15,
588
+ yanchor: "top",
589
+ x: 0.5,
590
+ xanchor: "center"
591
+ }
592
+ };
593
+ case "left":
594
+ return {
595
+ ...baseLayout,
596
+ legend: {
597
+ ...baseLayout,
598
+ orientation: "v",
599
+ x: -0.15,
600
+ xanchor: "right",
601
+ y: 0.5,
602
+ yanchor: "middle"
603
+ }
604
+ };
605
+ case "right":
606
+ default:
607
+ return {
608
+ ...baseLayout,
609
+ legend: {
610
+ ...baseLayout,
611
+ orientation: "v",
612
+ x: 1.02,
613
+ xanchor: "left",
614
+ y: 0.5,
615
+ yanchor: "middle"
616
+ }
617
+ };
618
+ }
619
+ }
620
+ function buildOverlayShapes(opts = {}) {
621
+ const { referenceLines = [], shadedBands = [], yRefByAxisId = {} } = opts;
622
+ const shapes = [];
623
+ const annotations = [];
624
+ const dashMap = { solid: "solid", dashed: "dash", dotted: "dot", dashdot: "dashdot" };
625
+ for (const band of shadedBands) {
626
+ if (band.x0 == null || band.x1 == null) continue;
627
+ shapes.push({
628
+ type: "rect",
629
+ xref: "x",
630
+ yref: "paper",
631
+ x0: band.x0,
632
+ x1: band.x1,
633
+ y0: 0,
634
+ y1: 1,
635
+ fillcolor: band.color || "rgba(255, 200, 0, 0.08)",
636
+ line: { width: 0 },
637
+ opacity: band.opacity ?? 1,
638
+ layer: "below"
639
+ });
640
+ if (band.label) {
641
+ annotations.push({
642
+ x: (band.x0 + band.x1) / 2,
643
+ y: band.labelY ?? 1,
644
+ xref: "x",
645
+ yref: "paper",
646
+ text: band.label,
647
+ showarrow: false,
648
+ font: { size: 9, color: band.labelColor || "rgba(255,255,255,0.55)" },
649
+ xanchor: "center",
650
+ yanchor: "bottom"
651
+ });
652
+ }
653
+ }
654
+ const firstYRef = Object.values(yRefByAxisId)[0] || "y";
655
+ for (const ref of referenceLines) {
656
+ if (ref.value == null || !Number.isFinite(ref.value)) continue;
657
+ const yRef = ref.yAxisId && yRefByAxisId[ref.yAxisId] || firstYRef;
658
+ const color = ref.color || "rgba(255, 120, 120, 0.75)";
659
+ shapes.push({
660
+ type: "line",
661
+ xref: "paper",
662
+ yref: yRef,
663
+ x0: 0,
664
+ x1: 1,
665
+ y0: ref.value,
666
+ y1: ref.value,
667
+ line: {
668
+ color,
669
+ width: ref.lineWidth ?? 1.5,
670
+ dash: dashMap[ref.lineStyle || "dashed"] || "dash"
671
+ },
672
+ layer: "above"
673
+ });
674
+ if (ref.label) {
675
+ annotations.push({
676
+ x: ref.labelX ?? 0.015,
677
+ y: ref.value,
678
+ xref: "paper",
679
+ yref: yRef,
680
+ text: ref.label,
681
+ showarrow: false,
682
+ font: { size: 10, color },
683
+ xanchor: "left",
684
+ yanchor: "bottom",
685
+ bgcolor: "rgba(20,22,26,0.6)",
686
+ borderpad: 2
687
+ });
688
+ }
689
+ }
690
+ return { shapes, annotations };
691
+ }
692
+ function buildFanBandTraces(options) {
693
+ const { x, upper, lower, color, opacity = 0.2, name = "" } = options;
694
+ const lowerTrace = {
695
+ type: "scatter",
696
+ x,
697
+ y: lower,
698
+ mode: "lines",
699
+ line: { width: 0 },
700
+ showlegend: false,
701
+ hoverinfo: "skip"
702
+ };
703
+ const upperTrace = {
704
+ type: "scatter",
705
+ x,
706
+ y: upper,
707
+ mode: "lines",
708
+ line: { width: 0 },
709
+ fill: "tonexty",
710
+ fillcolor: hexToRgba(color, opacity),
711
+ name: name || `${(opacity * 100).toFixed(0)}% Band`,
712
+ showlegend: !!name
713
+ };
714
+ return [lowerTrace, upperTrace];
715
+ }
716
+ function buildHistogramTrace(options) {
717
+ const { x, nbins = 20, color, name = "Distribution" } = options;
718
+ return {
719
+ type: "histogram",
720
+ x,
721
+ nbinsx: nbins,
722
+ marker: { color },
723
+ name
724
+ };
725
+ }
726
+ function buildBoxTrace(options) {
727
+ const { y, x = null, color, name = "" } = options;
728
+ const trace = {
729
+ type: "box",
730
+ y,
731
+ marker: { color },
732
+ name,
733
+ boxpoints: false
734
+ // Don't show individual points
735
+ };
736
+ if (x !== null) {
737
+ trace.x = Array.isArray(x) ? x : [x];
738
+ }
739
+ return trace;
740
+ }
741
+ function buildHeatmapTrace(options) {
742
+ const { z, x = null, y = null, colorscale = "RdBu" } = options;
743
+ const trace = {
744
+ type: "heatmap",
745
+ z,
746
+ colorscale,
747
+ showscale: true
748
+ };
749
+ if (x) trace.x = x;
750
+ if (y) trace.y = y;
751
+ return trace;
752
+ }
753
+
754
+ // src/charts/plot_config.js
755
+ var _uid = 0;
756
+ function uid(prefix = "p") {
757
+ return `${prefix}-${++_uid}-${Date.now().toString(36)}`;
758
+ }
759
+ function applyNanHandling(data, strategy, xData) {
760
+ const isMissing = (v) => v === null || v === void 0 || typeof v === "number" && !Number.isFinite(v);
761
+ if (strategy === "zero") return data.map((v) => isMissing(v) ? 0 : v);
762
+ if (strategy === "hold") {
763
+ const result = [];
764
+ let last = null;
765
+ for (const v of data) {
766
+ if (isMissing(v)) result.push(last);
767
+ else {
768
+ last = v;
769
+ result.push(v);
770
+ }
771
+ }
772
+ return result;
773
+ }
774
+ if (strategy === "interpolate") {
775
+ const result = [...data];
776
+ for (let i = 0; i < result.length; i++) {
777
+ if (!isMissing(result[i])) continue;
778
+ let left = -1, right = -1;
779
+ for (let j = i - 1; j >= 0; j--) {
780
+ if (!isMissing(result[j])) {
781
+ left = j;
782
+ break;
783
+ }
784
+ }
785
+ for (let j = i + 1; j < result.length; j++) {
786
+ if (!isMissing(result[j])) {
787
+ right = j;
788
+ break;
789
+ }
790
+ }
791
+ if (left >= 0 && right >= 0 && xData) {
792
+ const frac = (xData[i] - xData[left]) / (xData[right] - xData[left]);
793
+ result[i] = result[left] + frac * (result[right] - result[left]);
794
+ } else if (left >= 0) {
795
+ result[i] = result[left];
796
+ } else if (right >= 0) {
797
+ result[i] = result[right];
798
+ } else {
799
+ result[i] = null;
800
+ }
801
+ }
802
+ return result;
803
+ }
804
+ return data.map((v) => isMissing(v) ? null : v);
805
+ }
806
+ var PLOT_LAYOUTS = Object.freeze({
807
+ "1x1": { label: "Single", slots: 1, grid: { columns: "1fr", rows: "1fr" }, areas: [{ slot: 0, gridArea: "1/1/2/2" }] },
808
+ "1x2": { label: "Side by side", slots: 2, grid: { columns: "1fr 1fr", rows: "1fr" }, areas: [{ slot: 0, gridArea: "1/1/2/2" }, { slot: 1, gridArea: "1/2/2/3" }] },
809
+ "2x1": { label: "Stacked", slots: 2, grid: { columns: "1fr", rows: "1fr 1fr" }, areas: [{ slot: 0, gridArea: "1/1/2/2" }, { slot: 1, gridArea: "2/1/3/2" }] },
810
+ "2x2": { label: "2\xD72 Grid", slots: 4, grid: { columns: "1fr 1fr", rows: "1fr 1fr" }, areas: [{ slot: 0, gridArea: "1/1/2/2" }, { slot: 1, gridArea: "1/2/2/3" }, { slot: 2, gridArea: "2/1/3/2" }, { slot: 3, gridArea: "2/2/3/3" }] },
811
+ "1x3": { label: "Three columns", slots: 3, grid: { columns: "1fr 1fr 1fr", rows: "1fr" }, areas: [{ slot: 0, gridArea: "1/1/2/2" }, { slot: 1, gridArea: "1/2/2/3" }, { slot: 2, gridArea: "1/3/2/4" }] },
812
+ "wide-top": { label: "1 + 2", slots: 3, grid: { columns: "1fr 1fr", rows: "1fr 1fr" }, areas: [{ slot: 0, gridArea: "1/1/2/3" }, { slot: 1, gridArea: "2/1/3/2" }, { slot: 2, gridArea: "2/2/3/3" }] },
813
+ "wide-bottom": { label: "2 + 1", slots: 3, grid: { columns: "1fr 1fr", rows: "1fr 1fr" }, areas: [{ slot: 0, gridArea: "1/1/2/2" }, { slot: 1, gridArea: "1/2/2/3" }, { slot: 2, gridArea: "2/1/3/3" }] }
814
+ });
815
+ function makeDefaultSubplot(index) {
816
+ return {
817
+ id: uid("sp"),
818
+ displayName: "",
819
+ chartType: "line",
820
+ legendPosition: "hidden",
821
+ interpolation: "linear",
822
+ nanHandling: "gap",
823
+ showDataPoints: false,
824
+ showHpTrend: false,
825
+ showHpCycle: false,
826
+ hpLambda: 1600,
827
+ xAxis: { useTime: true, variable: "" },
828
+ zAxis: null,
829
+ yAxes: [{
830
+ id: uid("y"),
831
+ label: "",
832
+ scale: "linear",
833
+ position: "left",
834
+ min: null,
835
+ max: null,
836
+ step: null,
837
+ series: []
838
+ }]
839
+ };
840
+ }
841
+ function migrateData(data) {
842
+ if (data?.layout && Array.isArray(data?.subplots)) {
843
+ if ("caption" in data && !("description" in data)) {
844
+ data.description = data.caption;
845
+ data.caption = "";
846
+ }
847
+ return data;
848
+ }
849
+ const legacySingle = migrateSingleChart(data);
850
+ return {
851
+ layout: "1x1",
852
+ subplots: [{ id: uid("sp"), ...legacySingle }],
853
+ caption: "",
854
+ description: data?.caption ?? data?.description ?? "",
855
+ doc: data?.doc ?? ""
856
+ };
857
+ }
858
+ function migrateSingleChart(data) {
859
+ if (data?.yAxes) {
860
+ return {
861
+ displayName: data.displayName ?? "",
862
+ chartType: data.chartType ?? "line",
863
+ legendPosition: data.legendPosition ?? "hidden",
864
+ interpolation: data.interpolation ?? "linear",
865
+ nanHandling: data.nanHandling ?? "gap",
866
+ showDataPoints: data.showDataPoints ?? false,
867
+ yAxes: data.yAxes
868
+ };
869
+ }
870
+ const series = data?.series ?? data?.variables?.map((v, i) => ({
871
+ variable: v,
872
+ label: "",
873
+ color: getSeriesColor(i)
874
+ })) ?? [];
875
+ return {
876
+ displayName: data?.title ?? data?.displayName ?? "",
877
+ chartType: data?.chartType ?? data?.widgetType ?? "line",
878
+ legendPosition: data?.legendPosition ?? "hidden",
879
+ interpolation: data?.interpolation ?? "linear",
880
+ nanHandling: data?.nanHandling ?? "gap",
881
+ showDataPoints: data?.showDataPoints ?? false,
882
+ yAxes: [{
883
+ id: uid("y"),
884
+ label: data?.yLabel ?? "",
885
+ scale: "linear",
886
+ position: "left",
887
+ min: null,
888
+ max: null,
889
+ series: series.map((s, i) => ({
890
+ id: s.id ?? uid("s"),
891
+ variable: s.variable ?? "",
892
+ label: s.label ?? "",
893
+ color: s.color ?? getSeriesColor(i),
894
+ lineWidth: s.lineWidth ?? 2,
895
+ lineStyle: s.lineStyle ?? "solid",
896
+ stackGroup: s.stackGroup ?? null,
897
+ ...s.overlay ? { overlay: s.overlay } : {}
898
+ }))
899
+ }]
900
+ };
901
+ }
902
+
903
+ // src/charts/plot_popout_window.js
904
+ var PlotPopoutWindow = class {
905
+ /**
906
+ * @param {Object} options
907
+ * @param {string} options.id - Unique window identifier
908
+ * @param {string} options.title - Window title
909
+ * @param {Object} options.plotConfig - PlotCell format: { layout, subplots }
910
+ * @param {Function} options.getResults - Returns { time: number[], series: { [key]: number[] } }
911
+ * @param {Object} options.services - { eventBus, logger }
912
+ * @param {Object} [options.host] - A Host (ui/js/host/host.js). Its `dialogs`
913
+ * capability backs Export PNG/CSV; absent => Blob download fallback.
914
+ * @param {Function} [options.onClose] - Called when window closes
915
+ * @param {Object} [options.actuals] - ETL overlay data { seriesId: { years, values } }
916
+ * @param {Function} [options.configRenderer] - (container) => void — renders config panel
917
+ */
918
+ constructor({
919
+ id,
920
+ title,
921
+ plotConfig,
922
+ getResults,
923
+ services,
924
+ host,
925
+ onClose,
926
+ actuals,
927
+ configRenderer,
928
+ // The window used to listen for 'simulation:run:completed' and tell
929
+ // the user to "run simulation first". A chart window does not know
930
+ // that its data comes from a simulation; the app that opens it does.
931
+ refreshEvent = "data:changed",
932
+ emptyMessage = "No data yet.",
933
+ tableEmptyMessage = "No data yet."
934
+ }) {
935
+ this._id = id;
936
+ this._title = title || "Plot";
937
+ this._plotConfig = migrateData(plotConfig);
938
+ this._getResults = getResults;
939
+ this._services = services;
940
+ this._host = host || null;
941
+ this._onClose = onClose ?? (() => {
942
+ });
943
+ this._actuals = actuals ?? null;
944
+ this._configRenderer = configRenderer ?? null;
945
+ this.refreshEvent = refreshEvent;
946
+ this.emptyMessage = emptyMessage;
947
+ this.tableEmptyMessage = tableEmptyMessage;
948
+ this._window = null;
949
+ this._contentEl = null;
950
+ this._chartDivs = [];
951
+ this._gridEl = null;
952
+ this._activeTab = "plot";
953
+ this._eventDisposers = [];
954
+ this._resizeObserver = null;
955
+ this._chartCreated = false;
956
+ this._lastResults = null;
957
+ this._dataTable = null;
958
+ this._dataTableContainer = null;
959
+ this._tableHeaders = [];
960
+ this._tableRows = [];
961
+ this._plotTab = null;
962
+ this._dataTab = null;
963
+ this._configTab = null;
964
+ this._plotContent = null;
965
+ this._dataContent = null;
966
+ this._configContent = null;
967
+ this._toolbarActions = null;
968
+ this._zoomBtn = null;
969
+ this._panBtn = null;
970
+ this._hiddenSeries = /* @__PURE__ */ new Set();
971
+ this._allSeriesInfo = [];
972
+ this._allSeriesLabels = [];
973
+ this._seriesToggleBtn = null;
974
+ this._seriesTogglePanel = null;
975
+ this._seriesTogglePanelVisible = false;
976
+ this._lastTogglePanelSignature = null;
977
+ this._outsideClickHandler = null;
978
+ this._renderPending = false;
979
+ this._liveUpdate = true;
980
+ this._liveBtn = null;
981
+ }
982
+ get isVisible() {
983
+ return this._window?.isVisible ?? false;
984
+ }
985
+ async show() {
986
+ if (this._window?.isVisible) {
987
+ this._window.bringToFront();
988
+ return;
989
+ }
990
+ await ensurePlotly();
991
+ this.#createWindow();
992
+ this.#subscribeEvents();
993
+ this.#setupResizeObserver();
994
+ await this.#render();
995
+ }
996
+ bringToFront() {
997
+ this._window?.bringToFront?.();
998
+ }
999
+ close() {
1000
+ this._window?.close?.();
1001
+ }
1002
+ /** Update plot config and re-render. */
1003
+ updateConfig(plotConfig) {
1004
+ this._plotConfig = migrateData(plotConfig);
1005
+ this.#buildSubplotGrid();
1006
+ this.#scheduleRender();
1007
+ }
1008
+ /** Update actuals overlay data and re-render. */
1009
+ setActuals(actuals) {
1010
+ this._actuals = actuals;
1011
+ this.#scheduleRender();
1012
+ }
1013
+ // ── Window creation ─────────────────────────────────────────────────
1014
+ #createWindow() {
1015
+ this._contentEl = this.#buildContent();
1016
+ this._window = new ManagedWindow({
1017
+ id: `plot-window-${this._id}`,
1018
+ title: this._title,
1019
+ icon: "monitoring",
1020
+ content: this._contentEl,
1021
+ minWidth: 500,
1022
+ minHeight: 400,
1023
+ defaultWidth: 800,
1024
+ defaultHeight: 600,
1025
+ canMinimize: true,
1026
+ canMaximize: true,
1027
+ canResize: true,
1028
+ canDrag: true,
1029
+ onClose: () => {
1030
+ this.#dispose();
1031
+ this._onClose();
1032
+ }
1033
+ });
1034
+ this._window.show();
1035
+ }
1036
+ #buildContent() {
1037
+ const container = document.createElement("div");
1038
+ container.className = "twm-plot-window-container";
1039
+ const tabHeader = document.createElement("div");
1040
+ tabHeader.className = "twm-code-tabs-header";
1041
+ const tabs = document.createElement("div");
1042
+ tabs.className = "tabs twm-code-tabs";
1043
+ this._plotTab = document.createElement("button");
1044
+ this._plotTab.className = "twm-code-tab active";
1045
+ this._plotTab.type = "button";
1046
+ this._plotTab.textContent = "Plot";
1047
+ this._plotTab.addEventListener("click", () => this.#switchTab("plot"));
1048
+ this._dataTab = document.createElement("button");
1049
+ this._dataTab.className = "twm-code-tab";
1050
+ this._dataTab.type = "button";
1051
+ this._dataTab.textContent = "Data";
1052
+ this._dataTab.addEventListener("click", () => this.#switchTab("data"));
1053
+ tabs.appendChild(this._plotTab);
1054
+ tabs.appendChild(this._dataTab);
1055
+ if (this._configRenderer) {
1056
+ this._configTab = document.createElement("button");
1057
+ this._configTab.className = "twm-code-tab";
1058
+ this._configTab.type = "button";
1059
+ this._configTab.textContent = "Config";
1060
+ this._configTab.addEventListener("click", () => this.#switchTab("config"));
1061
+ tabs.appendChild(this._configTab);
1062
+ }
1063
+ this._toolbarActions = document.createElement("div");
1064
+ this._toolbarActions.className = "twm-plot-window-toolbar";
1065
+ const dragGroup = this.#toolbarGroup();
1066
+ this._zoomBtn = this.#toolbarBtn("search", "Zoom", () => this.#setDragMode("zoom"));
1067
+ this._zoomBtn.classList.add("twm-is-active");
1068
+ this._panBtn = this.#toolbarBtn("pan_tool", "Pan", () => this.#setDragMode("pan"));
1069
+ dragGroup.appendChild(this._zoomBtn);
1070
+ dragGroup.appendChild(this._panBtn);
1071
+ this._toolbarActions.appendChild(dragGroup);
1072
+ const zoomGroup = this.#toolbarGroup();
1073
+ zoomGroup.appendChild(this.#toolbarBtn("zoom_in", "Zoom in", () => this.#zoom(0.5)));
1074
+ zoomGroup.appendChild(this.#toolbarBtn("zoom_out", "Zoom out", () => this.#zoom(2)));
1075
+ zoomGroup.appendChild(this.#toolbarBtn("zoom_out_map", "Autoscale", () => this.#autoscale()));
1076
+ zoomGroup.appendChild(this.#toolbarBtn("home", "Reset axes", () => this.#autoscale()));
1077
+ this._toolbarActions.appendChild(zoomGroup);
1078
+ const spacer = document.createElement("div");
1079
+ spacer.style.flex = "1";
1080
+ this._toolbarActions.appendChild(spacer);
1081
+ const actionsGroup = this.#toolbarGroup();
1082
+ this._liveBtn = this.#toolbarBtn("stream", "Live update \u2014 click to lock", () => this.#toggleLiveUpdate());
1083
+ this._liveBtn.classList.add("twm-is-active");
1084
+ actionsGroup.appendChild(this._liveBtn);
1085
+ const toggleSeriesBtn = this.#toolbarBtn("visibility", "Toggle series visibility", () => this.#toggleSeriesPanel());
1086
+ this._seriesToggleBtn = toggleSeriesBtn;
1087
+ actionsGroup.appendChild(toggleSeriesBtn);
1088
+ actionsGroup.appendChild(this.#toolbarBtn("image", "Download plot as PNG", () => this.#downloadPNG()));
1089
+ actionsGroup.appendChild(this.#toolbarBtn("download", "Download data as CSV", () => this.#downloadCSV()));
1090
+ this._toolbarActions.appendChild(actionsGroup);
1091
+ tabHeader.appendChild(tabs);
1092
+ tabHeader.appendChild(this._toolbarActions);
1093
+ const content = document.createElement("div");
1094
+ content.className = "twm-plot-window-content";
1095
+ this._plotContent = document.createElement("div");
1096
+ this._plotContent.className = "twm-plot-tab-content active";
1097
+ this._plotContent.setAttribute("data-tab", "plot");
1098
+ this._plotContent.style.cssText = "display:flex; flex-direction:column;";
1099
+ this._gridEl = document.createElement("div");
1100
+ this._gridEl.className = "twm-plot-window-chart";
1101
+ this._plotContent.appendChild(this._gridEl);
1102
+ this.#buildSubplotGrid();
1103
+ this._dataContent = document.createElement("div");
1104
+ this._dataContent.className = "twm-plot-tab-content";
1105
+ this._dataContent.setAttribute("data-tab", "data");
1106
+ this._dataContent.style.cssText = "display:none; flex-direction:column;";
1107
+ this._dataTableContainer = document.createElement("div");
1108
+ this._dataTableContainer.style.cssText = "flex:1; min-height:0; display:flex; flex-direction:column;";
1109
+ this._dataContent.appendChild(this._dataTableContainer);
1110
+ content.appendChild(this._plotContent);
1111
+ content.appendChild(this._dataContent);
1112
+ if (this._configRenderer) {
1113
+ this._configContent = document.createElement("div");
1114
+ this._configContent.className = "twm-plot-tab-content";
1115
+ this._configContent.setAttribute("data-tab", "config");
1116
+ this._configContent.style.cssText = "display:none; flex-direction:column; overflow:auto; padding:8px 12px;";
1117
+ this._configRenderer(this._configContent);
1118
+ content.appendChild(this._configContent);
1119
+ }
1120
+ this._seriesTogglePanel = document.createElement("div");
1121
+ this._seriesTogglePanel.className = "twm-plot-series-toggle-panel";
1122
+ this._seriesTogglePanel.style.display = "none";
1123
+ container.appendChild(tabHeader);
1124
+ container.appendChild(content);
1125
+ container.appendChild(this._seriesTogglePanel);
1126
+ return container;
1127
+ }
1128
+ // ── Subplot grid ────────────────────────────────────────────────────
1129
+ #buildSubplotGrid() {
1130
+ if (!this._gridEl) return;
1131
+ for (const div of this._chartDivs) {
1132
+ if (div && window.Plotly) {
1133
+ try {
1134
+ purge(div);
1135
+ } catch (_) {
1136
+ }
1137
+ }
1138
+ }
1139
+ this._gridEl.innerHTML = "";
1140
+ this._chartDivs = [];
1141
+ const tmpl = PLOT_LAYOUTS[this._plotConfig.layout] || PLOT_LAYOUTS["1x1"];
1142
+ this._gridEl.style.display = "grid";
1143
+ this._gridEl.style.gridTemplateColumns = tmpl.grid.columns;
1144
+ this._gridEl.style.gridTemplateRows = tmpl.grid.rows;
1145
+ this._gridEl.style.gap = "4px";
1146
+ this._gridEl.style.height = "100%";
1147
+ this._gridEl.style.width = "100%";
1148
+ for (let i = 0; i < tmpl.slots; i++) {
1149
+ const div = document.createElement("div");
1150
+ div.className = "plot-tile-chart";
1151
+ div.style.gridArea = tmpl.areas[i].gridArea;
1152
+ div.style.minHeight = "0";
1153
+ this._gridEl.appendChild(div);
1154
+ this._chartDivs.push(div);
1155
+ }
1156
+ this._chartCreated = false;
1157
+ }
1158
+ // ── Toolbar helpers ─────────────────────────────────────────────────
1159
+ #toolbarGroup() {
1160
+ const g = document.createElement("div");
1161
+ g.className = "twm-plot-window-toolbar__group";
1162
+ return g;
1163
+ }
1164
+ #toolbarBtn(icon, tooltip, onClick) {
1165
+ const btn = document.createElement("button");
1166
+ btn.className = "twm-btn-icon twm-has-tooltip";
1167
+ btn.type = "button";
1168
+ btn.setAttribute("data-tooltip", tooltip);
1169
+ btn.innerHTML = `<span class="material-symbols-outlined">${icon}</span>`;
1170
+ if (onClick) btn.addEventListener("click", (e) => {
1171
+ e.stopPropagation();
1172
+ onClick();
1173
+ });
1174
+ return btn;
1175
+ }
1176
+ // ── Plotly toolbar actions ───────────────────────────────────────────
1177
+ #setDragMode(mode) {
1178
+ if (!this._chartCreated) return;
1179
+ for (const div of this._chartDivs) {
1180
+ if (div._fullLayout) {
1181
+ window.Plotly.relayout(div, { dragmode: mode }).catch(() => {
1182
+ });
1183
+ }
1184
+ }
1185
+ this._zoomBtn?.classList.toggle("twm-is-active", mode === "zoom");
1186
+ this._panBtn?.classList.toggle("twm-is-active", mode === "pan");
1187
+ }
1188
+ #zoom(factor) {
1189
+ for (const div of this._chartDivs) {
1190
+ if (!div._fullLayout) continue;
1191
+ const xa = div._fullLayout.xaxis;
1192
+ const ya = div._fullLayout.yaxis;
1193
+ if (!xa || !ya) continue;
1194
+ const xr = xa.range;
1195
+ const yr = ya.range;
1196
+ const xMid = (xr[0] + xr[1]) / 2;
1197
+ const yMid = (yr[0] + yr[1]) / 2;
1198
+ const xHalf = (xr[1] - xr[0]) / 2 * factor;
1199
+ const yHalf = (yr[1] - yr[0]) / 2 * factor;
1200
+ window.Plotly.relayout(div, {
1201
+ "xaxis.range": [xMid - xHalf, xMid + xHalf],
1202
+ "yaxis.range": [yMid - yHalf, yMid + yHalf]
1203
+ }).catch(() => {
1204
+ });
1205
+ }
1206
+ }
1207
+ #autoscale() {
1208
+ for (const div of this._chartDivs) {
1209
+ if (!div._fullLayout) continue;
1210
+ window.Plotly.relayout(div, {
1211
+ "xaxis.autorange": true,
1212
+ "yaxis.autorange": true
1213
+ }).catch(() => {
1214
+ });
1215
+ }
1216
+ }
1217
+ #setupResizeObserver() {
1218
+ if (!this._plotContent) return;
1219
+ this._resizeObserver = createRafResizeObserver(() => {
1220
+ if (this._activeTab === "plot" && this._chartCreated && window.Plotly) {
1221
+ for (const div of this._chartDivs) {
1222
+ if (div._fullLayout) {
1223
+ Plotly.Plots.resize(div).catch(() => {
1224
+ });
1225
+ }
1226
+ }
1227
+ }
1228
+ });
1229
+ this._resizeObserver.observe(this._plotContent);
1230
+ }
1231
+ #switchTab(tabName) {
1232
+ this._activeTab = tabName;
1233
+ this._plotTab.classList.toggle("active", tabName === "plot");
1234
+ this._dataTab.classList.toggle("active", tabName === "data");
1235
+ this._configTab?.classList.toggle("active", tabName === "config");
1236
+ if (this._toolbarActions) {
1237
+ this._toolbarActions.style.display = tabName === "plot" ? "" : "none";
1238
+ }
1239
+ this._plotContent.style.display = tabName === "plot" ? "flex" : "none";
1240
+ this._dataContent.style.display = tabName === "data" ? "flex" : "none";
1241
+ if (this._configContent) {
1242
+ this._configContent.style.display = tabName === "config" ? "flex" : "none";
1243
+ }
1244
+ if (tabName === "plot" && this._chartCreated && window.Plotly) {
1245
+ for (const div of this._chartDivs) {
1246
+ if (div._fullLayout) {
1247
+ Plotly.Plots.resize(div).catch(() => {
1248
+ });
1249
+ }
1250
+ }
1251
+ }
1252
+ if (tabName === "data") {
1253
+ this.#renderDataTable();
1254
+ }
1255
+ }
1256
+ // ── Series visibility toggle ────────────────────────────────────────
1257
+ #toggleSeriesPanel() {
1258
+ if (this._seriesTogglePanelVisible) {
1259
+ this.#hideSeriesPanel();
1260
+ } else {
1261
+ this.#showSeriesPanel();
1262
+ }
1263
+ }
1264
+ #showSeriesPanel() {
1265
+ if (!this._seriesTogglePanel || !this._seriesToggleBtn) return;
1266
+ const btnRect = this._seriesToggleBtn.getBoundingClientRect();
1267
+ const containerRect = this._contentEl.getBoundingClientRect();
1268
+ this._seriesTogglePanel.style.top = `${btnRect.bottom - containerRect.top + 4}px`;
1269
+ this._seriesTogglePanel.style.right = `${containerRect.right - btnRect.right}px`;
1270
+ this._seriesTogglePanel.style.display = "";
1271
+ this._seriesTogglePanelVisible = true;
1272
+ this._seriesToggleBtn.classList.add("twm-is-active");
1273
+ this.#rebuildSeriesToggleList();
1274
+ this._outsideClickHandler = (e) => {
1275
+ if (!this._seriesTogglePanel.contains(e.target) && !this._seriesToggleBtn.contains(e.target)) {
1276
+ this.#hideSeriesPanel();
1277
+ }
1278
+ };
1279
+ requestAnimationFrame(() => {
1280
+ document.addEventListener("pointerdown", this._outsideClickHandler, true);
1281
+ });
1282
+ }
1283
+ #hideSeriesPanel() {
1284
+ if (this._seriesTogglePanel) {
1285
+ this._seriesTogglePanel.style.display = "none";
1286
+ }
1287
+ this._seriesTogglePanelVisible = false;
1288
+ this._lastTogglePanelSignature = null;
1289
+ this._seriesToggleBtn?.classList.remove("twm-is-active");
1290
+ if (this._outsideClickHandler) {
1291
+ document.removeEventListener("pointerdown", this._outsideClickHandler, true);
1292
+ this._outsideClickHandler = null;
1293
+ }
1294
+ }
1295
+ #rebuildSeriesToggleList() {
1296
+ const panel = this._seriesTogglePanel;
1297
+ if (!panel) return;
1298
+ panel.innerHTML = "";
1299
+ const header = document.createElement("div");
1300
+ header.className = "twm-plot-series-toggle-panel__header";
1301
+ const title = document.createElement("span");
1302
+ title.className = "twm-plot-series-toggle-panel__title";
1303
+ title.textContent = "Series";
1304
+ header.appendChild(title);
1305
+ const headerActions = document.createElement("div");
1306
+ headerActions.className = "twm-plot-series-toggle-panel__header-actions";
1307
+ const showAllBtn = document.createElement("button");
1308
+ showAllBtn.className = "twm-plot-series-toggle-panel__action";
1309
+ showAllBtn.type = "button";
1310
+ showAllBtn.textContent = "All";
1311
+ showAllBtn.addEventListener("click", () => {
1312
+ this._hiddenSeries.clear();
1313
+ this.#rebuildSeriesToggleList();
1314
+ this.#scheduleRender();
1315
+ });
1316
+ const hideAllBtn = document.createElement("button");
1317
+ hideAllBtn.className = "twm-plot-series-toggle-panel__action";
1318
+ hideAllBtn.type = "button";
1319
+ hideAllBtn.textContent = "None";
1320
+ hideAllBtn.addEventListener("click", () => {
1321
+ for (const label of this._allSeriesLabels) {
1322
+ this._hiddenSeries.add(label);
1323
+ }
1324
+ this.#rebuildSeriesToggleList();
1325
+ this.#scheduleRender();
1326
+ });
1327
+ headerActions.appendChild(showAllBtn);
1328
+ headerActions.appendChild(hideAllBtn);
1329
+ header.appendChild(headerActions);
1330
+ panel.appendChild(header);
1331
+ const modelSeries = this._allSeriesInfo.filter((s) => !s.isOverlay);
1332
+ const overlaySeries = this._allSeriesInfo.filter((s) => s.isOverlay);
1333
+ const buildItem = ({ label, color, lineStyle }) => {
1334
+ const item = document.createElement("label");
1335
+ item.className = "twm-plot-series-toggle-panel__item";
1336
+ const cb = document.createElement("input");
1337
+ cb.type = "checkbox";
1338
+ cb.checked = !this._hiddenSeries.has(label);
1339
+ cb.addEventListener("change", () => {
1340
+ if (cb.checked) this._hiddenSeries.delete(label);
1341
+ else this._hiddenSeries.add(label);
1342
+ this.#scheduleRender();
1343
+ });
1344
+ const dashMap = { solid: "", dashed: "4,2", dotted: "1,2" };
1345
+ const indicator = document.createElement("div");
1346
+ indicator.className = "twm-plot-series-toggle-panel__line-indicator";
1347
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
1348
+ svg.setAttribute("viewBox", "0 0 28 14");
1349
+ const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
1350
+ line.setAttribute("x1", "3");
1351
+ line.setAttribute("y1", "7");
1352
+ line.setAttribute("x2", "25");
1353
+ line.setAttribute("y2", "7");
1354
+ line.setAttribute("stroke", color || "#888");
1355
+ line.setAttribute("stroke-width", "2");
1356
+ line.setAttribute("stroke-linecap", "round");
1357
+ const dash = dashMap[lineStyle] || "";
1358
+ if (dash) line.setAttribute("stroke-dasharray", dash);
1359
+ svg.appendChild(line);
1360
+ indicator.appendChild(svg);
1361
+ const text = document.createElement("span");
1362
+ text.className = "twm-plot-series-toggle-panel__label";
1363
+ text.textContent = label;
1364
+ text.title = label;
1365
+ item.appendChild(cb);
1366
+ item.appendChild(indicator);
1367
+ item.appendChild(text);
1368
+ return item;
1369
+ };
1370
+ const list = document.createElement("div");
1371
+ list.className = "twm-plot-series-toggle-panel__list";
1372
+ const subplotGroups = /* @__PURE__ */ new Map();
1373
+ for (const info of modelSeries) {
1374
+ if (!subplotGroups.has(info.subplotIndex)) {
1375
+ subplotGroups.set(info.subplotIndex, []);
1376
+ }
1377
+ subplotGroups.get(info.subplotIndex).push(info);
1378
+ }
1379
+ const multipleSubplots = subplotGroups.size > 1;
1380
+ for (const [spIdx, seriesList] of subplotGroups) {
1381
+ if (multipleSubplots) {
1382
+ const spName = seriesList[0]?.subplotName || `Subplot ${spIdx + 1}`;
1383
+ const spHeader = document.createElement("div");
1384
+ spHeader.className = "twm-plot-series-toggle-panel__axis-label";
1385
+ spHeader.textContent = spName;
1386
+ list.appendChild(spHeader);
1387
+ }
1388
+ for (const info of seriesList) {
1389
+ list.appendChild(buildItem(info));
1390
+ }
1391
+ }
1392
+ panel.appendChild(list);
1393
+ if (overlaySeries.length > 0) {
1394
+ const ovList = document.createElement("div");
1395
+ ovList.className = "twm-plot-series-toggle-panel__list";
1396
+ const ovHeader = document.createElement("div");
1397
+ ovHeader.className = "twm-plot-series-toggle-panel__axis-label";
1398
+ ovHeader.textContent = "Overlays";
1399
+ ovList.appendChild(ovHeader);
1400
+ for (const info of overlaySeries) {
1401
+ ovList.appendChild(buildItem(info));
1402
+ }
1403
+ panel.appendChild(ovList);
1404
+ }
1405
+ }
1406
+ // ── Event subscriptions ─────────────────────────────────────────────
1407
+ #subscribeEvents() {
1408
+ const { eventBus } = this._services;
1409
+ if (!eventBus) return;
1410
+ this._eventDisposers.push(
1411
+ eventBus.on("streaming:delta", () => {
1412
+ if (this._liveUpdate) this.#scheduleRender();
1413
+ })
1414
+ );
1415
+ this._eventDisposers.push(
1416
+ eventBus.on("streaming:complete", () => {
1417
+ if (this._liveUpdate) this.#scheduleRender();
1418
+ })
1419
+ );
1420
+ this._eventDisposers.push(
1421
+ eventBus.on(this.refreshEvent, () => {
1422
+ if (this._liveUpdate) this.#scheduleRender();
1423
+ })
1424
+ );
1425
+ }
1426
+ #toggleLiveUpdate() {
1427
+ this._liveUpdate = !this._liveUpdate;
1428
+ if (this._liveBtn) {
1429
+ this._liveBtn.classList.toggle("twm-is-active", this._liveUpdate);
1430
+ const icon = this._liveBtn.querySelector(".material-symbols-outlined");
1431
+ if (icon) icon.textContent = this._liveUpdate ? "stream" : "lock";
1432
+ this._liveBtn.title = this._liveUpdate ? "Live update \u2014 click to lock" : "Locked \u2014 click for live update";
1433
+ }
1434
+ if (this._liveUpdate) this.#scheduleRender();
1435
+ }
1436
+ #scheduleRender() {
1437
+ if (this._renderPending) return;
1438
+ this._renderPending = true;
1439
+ requestAnimationFrame(() => {
1440
+ this._renderPending = false;
1441
+ this.#render();
1442
+ });
1443
+ }
1444
+ // ── Rendering ───────────────────────────────────────────────────────
1445
+ async #render() {
1446
+ if (!this._window?.isVisible) return;
1447
+ const results = this._getResults?.();
1448
+ if (!results) return;
1449
+ this._lastResults = results;
1450
+ this.#collectSeriesInfo();
1451
+ this.#renderAllCharts(results);
1452
+ this.#updateTableData(results);
1453
+ if (this._activeTab === "data") {
1454
+ this.#renderDataTable();
1455
+ }
1456
+ }
1457
+ #collectSeriesInfo() {
1458
+ this._allSeriesInfo = [];
1459
+ this._allSeriesLabels = [];
1460
+ const subplots = this._plotConfig.subplots ?? [];
1461
+ for (let spIdx = 0; spIdx < subplots.length; spIdx++) {
1462
+ const sp = subplots[spIdx];
1463
+ for (const axis of sp.yAxes ?? []) {
1464
+ for (const s of axis.series ?? []) {
1465
+ const label = s.label || s.variable || "Series";
1466
+ this._allSeriesInfo.push({
1467
+ label,
1468
+ color: s.color || getSeriesColor(this._allSeriesInfo.length),
1469
+ lineStyle: s.lineStyle || "solid",
1470
+ subplotIndex: spIdx,
1471
+ subplotName: sp.displayName || `Subplot ${spIdx + 1}`
1472
+ });
1473
+ this._allSeriesLabels.push(label);
1474
+ if (s.overlay?.etlKey) {
1475
+ const ovLabel = s.overlay.label || `${label} (actual)`;
1476
+ this._allSeriesInfo.push({
1477
+ label: ovLabel,
1478
+ color: s.overlay.color || s.color || getSeriesColor(this._allSeriesInfo.length),
1479
+ lineStyle: s.overlay.lineStyle || "dashed",
1480
+ subplotIndex: spIdx,
1481
+ subplotName: sp.displayName || `Subplot ${spIdx + 1}`,
1482
+ isOverlay: true
1483
+ });
1484
+ this._allSeriesLabels.push(ovLabel);
1485
+ }
1486
+ }
1487
+ }
1488
+ }
1489
+ for (const label of this._hiddenSeries) {
1490
+ if (!this._allSeriesLabels.includes(label)) {
1491
+ this._hiddenSeries.delete(label);
1492
+ }
1493
+ }
1494
+ if (this._seriesTogglePanelVisible) {
1495
+ const newSig = this._allSeriesInfo.map((s) => `${s.subplotIndex}|${s.label}|${s.lineStyle}`).join("\0");
1496
+ if (newSig !== this._lastTogglePanelSignature) {
1497
+ this._lastTogglePanelSignature = newSig;
1498
+ this.#rebuildSeriesToggleList();
1499
+ }
1500
+ }
1501
+ if (this._seriesToggleBtn) {
1502
+ const icon = this._seriesToggleBtn.querySelector(".material-symbols-outlined");
1503
+ if (icon) {
1504
+ icon.textContent = this._hiddenSeries.size > 0 ? "visibility_off" : "visibility";
1505
+ }
1506
+ }
1507
+ }
1508
+ #renderAllCharts(results) {
1509
+ if (!window.Plotly) return;
1510
+ const subplots = this._plotConfig.subplots ?? [];
1511
+ for (let i = 0; i < this._chartDivs.length; i++) {
1512
+ const chartDiv = this._chartDivs[i];
1513
+ const sp = subplots[i];
1514
+ if (!chartDiv) continue;
1515
+ if (!sp) {
1516
+ chartDiv.innerHTML = '<div class="twm-plot-no-data" style="color:rgba(255,255,255,0.3);text-align:center;padding:16px;">No subplot configured.</div>';
1517
+ continue;
1518
+ }
1519
+ this.#renderSubplotChart(chartDiv, sp, results);
1520
+ }
1521
+ }
1522
+ async #renderSubplotChart(chartDiv, sp, results) {
1523
+ const { time, series } = results;
1524
+ const traces = [];
1525
+ const layoutAxes = {};
1526
+ const yRefByAxisId = {};
1527
+ let axisCount = 0;
1528
+ for (const axis of sp.yAxes ?? []) {
1529
+ axisCount++;
1530
+ const yAxisKey = axisCount === 1 ? "yaxis" : `yaxis${axisCount}`;
1531
+ const yRef = axisCount === 1 ? "y" : `y${axisCount}`;
1532
+ if (axis.id) yRefByAxisId[axis.id] = yRef;
1533
+ layoutAxes[yAxisKey] = buildYAxisLayout({
1534
+ axisId: yAxisKey,
1535
+ title: axis.label || "",
1536
+ position: axis.position || "left",
1537
+ scaleType: axis.scale || "linear",
1538
+ min: axis.min,
1539
+ max: axis.max,
1540
+ showGrid: (sp.yAxes ?? []).length === 1,
1541
+ axisPosition: axisCount > 1 ? axis.position === "right" ? 1 : 0 : null
1542
+ });
1543
+ for (const s of axis.series ?? []) {
1544
+ if (!s.variable) continue;
1545
+ const label = s.label || s.variable;
1546
+ if (this._hiddenSeries.has(label)) continue;
1547
+ let rawY = series?.[s._resultKey ?? s.variable];
1548
+ if (!rawY && s._resultKey) {
1549
+ rawY = series?.[s.variable];
1550
+ }
1551
+ if (!rawY) {
1552
+ const dotIdx = s.variable.indexOf(".");
1553
+ if (dotIdx >= 0) rawY = series?.[s.variable.slice(dotIdx + 1)];
1554
+ }
1555
+ if (!rawY) continue;
1556
+ const y = Array.isArray(rawY) ? applyNanHandling(rawY, sp.nanHandling ?? "gap", time) : rawY;
1557
+ const seriesTime = s._resultTime ?? time;
1558
+ traces.push(buildTrace({
1559
+ chartType: sp.chartType || "line",
1560
+ x: seriesTime,
1561
+ y,
1562
+ name: label,
1563
+ color: s.color,
1564
+ lineWidth: s.lineWidth ?? 2,
1565
+ lineStyle: s.lineStyle ?? "solid",
1566
+ interpolation: sp.interpolation ?? "linear",
1567
+ yAxisId: yRef,
1568
+ showMarkers: sp.showDataPoints ?? false,
1569
+ stackGroup: s.stackGroup ?? null
1570
+ }));
1571
+ if (s.overlay?.etlKey && this._actuals?.[s.id]) {
1572
+ const ovLabel = s.overlay.label || `${label} (actual)`;
1573
+ if (!this._hiddenSeries.has(ovLabel)) {
1574
+ const act = this._actuals[s.id];
1575
+ if (act?.years?.length) {
1576
+ const ov = s.overlay;
1577
+ const dashMap = { dashed: "dash", dotted: "dot", solid: "solid" };
1578
+ traces.push({
1579
+ type: "scatter",
1580
+ mode: ov.mode || "markers+lines",
1581
+ x: act.years,
1582
+ y: act.values,
1583
+ name: ov.label || `${label} (actual)`,
1584
+ yaxis: yRef === "y" ? void 0 : yRef,
1585
+ line: {
1586
+ color: ov.color || s.color,
1587
+ width: ov.lineWidth ?? 2,
1588
+ dash: dashMap[ov.lineStyle] || "dash"
1589
+ },
1590
+ marker: {
1591
+ color: ov.color || s.color,
1592
+ size: 4,
1593
+ symbol: "circle"
1594
+ },
1595
+ showlegend: true
1596
+ });
1597
+ }
1598
+ }
1599
+ }
1600
+ }
1601
+ }
1602
+ if ((sp.showHpTrend || sp.showHpCycle) && window.pywebview?.api?.hp_filter) {
1603
+ const hpLambda = sp.hpLambda ?? 1600;
1604
+ for (const trace of [...traces]) {
1605
+ if (trace.type && trace.type !== "scatter" && trace.type !== "scattergl") continue;
1606
+ try {
1607
+ const hpResult = await window.pywebview.api.hp_filter({
1608
+ data: trace.y,
1609
+ lambda: hpLambda
1610
+ });
1611
+ if (!hpResult?.ok) continue;
1612
+ const { trend, cycle } = hpResult;
1613
+ const origColor = trace.line?.color || trace.marker?.color || getSeriesColor(0);
1614
+ if (sp.showHpTrend && trend) {
1615
+ traces.push({
1616
+ x: trace.x,
1617
+ y: trend,
1618
+ type: "scatter",
1619
+ mode: "lines",
1620
+ name: `${trace.name} (HP Trend)`,
1621
+ yaxis: trace.yaxis || "y",
1622
+ line: { color: origColor, width: 2, dash: "dash" },
1623
+ showlegend: true
1624
+ });
1625
+ }
1626
+ if (sp.showHpCycle && cycle) {
1627
+ traces.push({
1628
+ x: trace.x,
1629
+ y: cycle,
1630
+ type: "scatter",
1631
+ mode: "lines",
1632
+ name: `${trace.name} (HP Cycle)`,
1633
+ yaxis: trace.yaxis || "y",
1634
+ line: { color: origColor, width: 1, dash: "dot" },
1635
+ showlegend: true
1636
+ });
1637
+ }
1638
+ } catch (_) {
1639
+ }
1640
+ }
1641
+ }
1642
+ if (traces.length === 0 && this._hiddenSeries.size === 0) {
1643
+ chartDiv.innerHTML = `<div class="twm-plot-no-data" style="color:rgba(255,255,255,0.3);text-align:center;padding:16px;font-size:11px;">${this.emptyMessage}</div>`;
1644
+ return;
1645
+ }
1646
+ const legendPos = sp.legendPosition === "hidden" ? "top" : sp.legendPosition ?? "top";
1647
+ const legend = buildLegendLayout(legendPos);
1648
+ const xRange = time?.length >= 2 ? [time[0], time[time.length - 1]] : void 0;
1649
+ const { shapes, annotations } = buildOverlayShapes({
1650
+ referenceLines: sp.referenceLines,
1651
+ shadedBands: sp.shadedBands,
1652
+ yRefByAxisId
1653
+ });
1654
+ const layout = {
1655
+ ...layoutAxes,
1656
+ ...shapes.length ? { shapes } : {},
1657
+ ...annotations.length ? { annotations } : {},
1658
+ xaxis: {
1659
+ gridcolor: "rgba(255,255,255,0.05)",
1660
+ linecolor: "rgba(255,255,255,0.08)",
1661
+ tickcolor: "rgba(255,255,255,0.3)",
1662
+ zerolinecolor: "rgba(255,255,255,0.06)",
1663
+ automargin: true,
1664
+ ...xRange ? { range: xRange } : {}
1665
+ },
1666
+ ...legend,
1667
+ paper_bgcolor: "transparent",
1668
+ plot_bgcolor: "transparent",
1669
+ font: { family: "Inter, -apple-system, BlinkMacSystemFont, sans-serif", color: "#cccccc", size: 11 },
1670
+ margin: { t: 10, r: 20, b: 40, l: 50 },
1671
+ hoverlabel: {
1672
+ bgcolor: "#1e2228",
1673
+ bordercolor: "#444444",
1674
+ font: { family: "Inter, -apple-system, BlinkMacSystemFont, sans-serif", color: "#cccccc" }
1675
+ },
1676
+ autosize: true,
1677
+ hovermode: "x unified"
1678
+ };
1679
+ if (!chartDiv.isConnected || chartDiv.offsetWidth === 0) return;
1680
+ try {
1681
+ await Plotly.react(chartDiv, traces, layout, { responsive: true, displayModeBar: false });
1682
+ this._chartCreated = true;
1683
+ } catch (_) {
1684
+ }
1685
+ }
1686
+ // ── Data table ──────────────────────────────────────────────────────
1687
+ #updateTableData(results) {
1688
+ if (!results) {
1689
+ this._tableHeaders = [];
1690
+ this._tableRows = [];
1691
+ return;
1692
+ }
1693
+ const headers = ["Time"];
1694
+ const seriesColumns = [];
1695
+ const { time, series } = results;
1696
+ const subplots = this._plotConfig.subplots ?? [];
1697
+ for (const sp of subplots) {
1698
+ for (const axis of sp.yAxes ?? []) {
1699
+ for (const s of axis.series ?? []) {
1700
+ if (!s.variable) continue;
1701
+ const label = s.label || s.variable;
1702
+ let rawY = series?.[s._resultKey ?? s.variable];
1703
+ if (!rawY && s._resultKey) {
1704
+ rawY = series?.[s.variable];
1705
+ }
1706
+ if (!rawY) {
1707
+ const dotIdx = s.variable.indexOf(".");
1708
+ if (dotIdx >= 0) rawY = series?.[s.variable.slice(dotIdx + 1)];
1709
+ }
1710
+ if (!rawY) continue;
1711
+ headers.push(label);
1712
+ seriesColumns.push(rawY);
1713
+ }
1714
+ }
1715
+ }
1716
+ const rows = [];
1717
+ for (let i = 0; i < time.length; i++) {
1718
+ const row = [time[i]];
1719
+ for (const col of seriesColumns) {
1720
+ row.push(Array.isArray(col) ? col[i] : null);
1721
+ }
1722
+ rows.push(row);
1723
+ }
1724
+ this._tableHeaders = headers;
1725
+ this._tableRows = rows;
1726
+ }
1727
+ #renderDataTable() {
1728
+ if (!this._dataTableContainer) return;
1729
+ if (!this._dataTable) {
1730
+ this._dataTable = new DataTable(this._dataTableContainer, {
1731
+ headers: this._tableHeaders,
1732
+ rows: this._tableRows,
1733
+ pageSize: 100,
1734
+ pagination: true,
1735
+ selectable: true,
1736
+ copyable: true,
1737
+ sortable: true,
1738
+ filterable: true,
1739
+ readonly: false,
1740
+ emptyMessage: this.tableEmptyMessage,
1741
+ services: this._services,
1742
+ host: this._host
1743
+ });
1744
+ this._dataTable.render();
1745
+ } else {
1746
+ this._dataTable.setData({
1747
+ headers: this._tableHeaders,
1748
+ rows: this._tableRows
1749
+ });
1750
+ }
1751
+ }
1752
+ // ── Downloads ────────────────────────────────────────────────────────
1753
+ async #downloadCSV() {
1754
+ if (this._tableRows.length === 0) {
1755
+ this.#notify("Download", "No data available to download.", "warn");
1756
+ return;
1757
+ }
1758
+ const filename = `${this._title.replace(/[^a-z0-9]/gi, "_")}_data.csv`;
1759
+ const lines = [this._tableHeaders.join(",")];
1760
+ for (const row of this._tableRows) {
1761
+ lines.push(row.map((val) => this.#csvEscape(val)).join(","));
1762
+ }
1763
+ const csv = lines.join("\n");
1764
+ const dialogs = this._host?.dialogs;
1765
+ let result = null;
1766
+ if (dialogs) {
1767
+ try {
1768
+ result = await dialogs.saveFile({ data: csv, filename, kind: "csv" });
1769
+ } catch (err) {
1770
+ console.error("[PlotPopoutWindow] CSV download failed:", err);
1771
+ this.#notify("Download", "Failed to save file", "error");
1772
+ return;
1773
+ }
1774
+ }
1775
+ if (result) {
1776
+ if (result.ok) {
1777
+ this.#notify("Download", `Saved ${this._tableRows.length} rows to ${result.path}`, "success");
1778
+ } else if (!result.cancelled) {
1779
+ this.#notify("Download", result.error || "Failed to save file", "error");
1780
+ }
1781
+ return;
1782
+ }
1783
+ const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
1784
+ const url = URL.createObjectURL(blob);
1785
+ const link = document.createElement("a");
1786
+ link.href = url;
1787
+ link.download = filename;
1788
+ link.click();
1789
+ URL.revokeObjectURL(url);
1790
+ this.#notify("Download", "Download started", "info");
1791
+ }
1792
+ #csvEscape(value) {
1793
+ if (value == null) return "";
1794
+ const str = String(value);
1795
+ if (str.includes(",") || str.includes('"') || str.includes("\n")) {
1796
+ return `"${str.replace(/"/g, '""')}"`;
1797
+ }
1798
+ return str;
1799
+ }
1800
+ async #downloadPNG() {
1801
+ const targetDiv = this._chartDivs.length === 1 ? this._chartDivs[0] : this._chartDivs[0];
1802
+ if (!targetDiv || !window.Plotly || !this._chartCreated) {
1803
+ this.#notify("Download", "Plot not ready", "warn");
1804
+ return;
1805
+ }
1806
+ const filename = `${this._title.replace(/[^a-z0-9]/gi, "_")}.png`;
1807
+ try {
1808
+ const pngDataUrl = await window.Plotly.toImage(targetDiv, {
1809
+ format: "png",
1810
+ width: 1920,
1811
+ height: 1080,
1812
+ scale: 2
1813
+ });
1814
+ const dialogs = this._host?.dialogs;
1815
+ const result = dialogs ? await dialogs.saveFile({ data: pngDataUrl.split(",")[1], filename, kind: "png" }) : null;
1816
+ if (result) {
1817
+ if (result.ok) {
1818
+ this.#notify("Download", `Plot saved to ${result.path}`, "success");
1819
+ } else if (!result.cancelled) {
1820
+ this.#notify("Download", result.error || "Failed to save image", "error");
1821
+ }
1822
+ } else {
1823
+ const link = document.createElement("a");
1824
+ link.href = pngDataUrl;
1825
+ link.download = filename;
1826
+ link.click();
1827
+ this.#notify("Download", "PNG download started", "info");
1828
+ }
1829
+ } catch (err) {
1830
+ this._services.logger?.warn?.("[PlotPopoutWindow] PNG download failed", err);
1831
+ this.#notify("Download", "Failed to generate PNG", "error");
1832
+ }
1833
+ }
1834
+ // ── Utilities ───────────────────────────────────────────────────────
1835
+ #notify(title, message, severity = "info") {
1836
+ this._services.eventBus?.emit?.("toast:show", {
1837
+ title,
1838
+ message,
1839
+ type: severity
1840
+ });
1841
+ }
1842
+ // ── Cleanup ─────────────────────────────────────────────────────────
1843
+ #dispose() {
1844
+ if (this._dataTable) {
1845
+ this._dataTable.dispose();
1846
+ this._dataTable = null;
1847
+ }
1848
+ this.#hideSeriesPanel();
1849
+ if (this._resizeObserver) {
1850
+ this._resizeObserver.disconnect();
1851
+ this._resizeObserver = null;
1852
+ }
1853
+ for (const disposer of this._eventDisposers) {
1854
+ try {
1855
+ disposer?.dispose?.();
1856
+ } catch (_) {
1857
+ }
1858
+ }
1859
+ this._eventDisposers = [];
1860
+ for (const div of this._chartDivs) {
1861
+ if (div && window.Plotly) {
1862
+ try {
1863
+ purge(div);
1864
+ } catch (_) {
1865
+ }
1866
+ }
1867
+ }
1868
+ this._window = null;
1869
+ this._contentEl = null;
1870
+ this._gridEl = null;
1871
+ this._chartDivs = [];
1872
+ this._plotTab = null;
1873
+ this._dataTab = null;
1874
+ this._plotContent = null;
1875
+ this._dataContent = null;
1876
+ this._dataTableContainer = null;
1877
+ this._toolbarActions = null;
1878
+ this._zoomBtn = null;
1879
+ this._panBtn = null;
1880
+ this._seriesToggleBtn = null;
1881
+ this._seriesTogglePanel = null;
1882
+ this._lastTogglePanelSignature = null;
1883
+ this._hiddenSeries = null;
1884
+ this._allSeriesInfo = null;
1885
+ this._allSeriesLabels = null;
1886
+ this._chartCreated = false;
1887
+ this._lastResults = null;
1888
+ }
1889
+ };
1890
+ var _openWindows = /* @__PURE__ */ new Map();
1891
+ async function openPlotPopoutWindow(options) {
1892
+ const { id } = options;
1893
+ let win = _openWindows.get(id);
1894
+ if (win?.isVisible) {
1895
+ win.bringToFront();
1896
+ return win;
1897
+ }
1898
+ win = new PlotPopoutWindow({
1899
+ ...options,
1900
+ onClose: () => _openWindows.delete(id)
1901
+ });
1902
+ _openWindows.set(id, win);
1903
+ await win.show();
1904
+ return win;
1905
+ }
1906
+ async function openRawTracesWindow({ id, title, traces, layout, frames, tableHeaders, tableRows, services, host }) {
1907
+ const existing = _openWindows.get(id);
1908
+ if (existing?.isVisible) {
1909
+ existing.bringToFront();
1910
+ return existing;
1911
+ }
1912
+ await ensurePlotly();
1913
+ let chartDiv = null;
1914
+ let chartCreated = false;
1915
+ let resizeObserver = null;
1916
+ let dataTable = null;
1917
+ let managedWindow = null;
1918
+ const container = document.createElement("div");
1919
+ container.className = "twm-plot-window-container";
1920
+ const tabHeader = document.createElement("div");
1921
+ tabHeader.className = "twm-code-tabs-header";
1922
+ const tabs = document.createElement("div");
1923
+ tabs.className = "tabs twm-code-tabs";
1924
+ const plotTab = document.createElement("button");
1925
+ plotTab.className = "twm-code-tab active";
1926
+ plotTab.type = "button";
1927
+ plotTab.textContent = "Plot";
1928
+ const dataTab = document.createElement("button");
1929
+ dataTab.className = "twm-code-tab";
1930
+ dataTab.type = "button";
1931
+ dataTab.textContent = "Data";
1932
+ tabs.appendChild(plotTab);
1933
+ tabs.appendChild(dataTab);
1934
+ const toolbar = document.createElement("div");
1935
+ toolbar.className = "twm-plot-window-toolbar";
1936
+ const spacer = document.createElement("div");
1937
+ spacer.style.flex = "1";
1938
+ toolbar.appendChild(spacer);
1939
+ const actionsGroup = document.createElement("div");
1940
+ actionsGroup.className = "twm-plot-window-toolbar__group";
1941
+ const mkBtn = (icon, tooltip, onClick) => {
1942
+ const btn = document.createElement("button");
1943
+ btn.className = "twm-btn-icon twm-has-tooltip";
1944
+ btn.type = "button";
1945
+ btn.setAttribute("data-tooltip", tooltip);
1946
+ btn.innerHTML = `<span class="material-symbols-outlined">${icon}</span>`;
1947
+ btn.addEventListener("click", (e) => {
1948
+ e.stopPropagation();
1949
+ onClick();
1950
+ });
1951
+ return btn;
1952
+ };
1953
+ actionsGroup.appendChild(mkBtn("image", "Download plot as PNG", downloadPNG));
1954
+ actionsGroup.appendChild(mkBtn("download", "Download data as CSV", downloadCSV));
1955
+ toolbar.appendChild(actionsGroup);
1956
+ tabHeader.appendChild(tabs);
1957
+ tabHeader.appendChild(toolbar);
1958
+ const content = document.createElement("div");
1959
+ content.className = "twm-plot-window-content";
1960
+ const plotContent = document.createElement("div");
1961
+ plotContent.className = "twm-plot-tab-content active";
1962
+ plotContent.setAttribute("data-tab", "plot");
1963
+ plotContent.style.cssText = "display:flex; flex-direction:column;";
1964
+ chartDiv = document.createElement("div");
1965
+ chartDiv.className = "twm-plot-window-chart";
1966
+ plotContent.appendChild(chartDiv);
1967
+ const dataContent = document.createElement("div");
1968
+ dataContent.className = "twm-plot-tab-content";
1969
+ dataContent.setAttribute("data-tab", "data");
1970
+ dataContent.style.cssText = "display:none; flex-direction:column;";
1971
+ const dataTableContainer = document.createElement("div");
1972
+ dataTableContainer.style.cssText = "flex:1; min-height:0; display:flex; flex-direction:column;";
1973
+ dataContent.appendChild(dataTableContainer);
1974
+ content.appendChild(plotContent);
1975
+ content.appendChild(dataContent);
1976
+ container.appendChild(tabHeader);
1977
+ container.appendChild(content);
1978
+ plotTab.addEventListener("click", () => {
1979
+ plotTab.classList.add("active");
1980
+ dataTab.classList.remove("active");
1981
+ toolbar.style.display = "";
1982
+ plotContent.style.display = "flex";
1983
+ dataContent.style.display = "none";
1984
+ if (chartCreated && window.Plotly && chartDiv) {
1985
+ try {
1986
+ window.Plotly.Plots.resize(chartDiv);
1987
+ } catch (_) {
1988
+ }
1989
+ }
1990
+ });
1991
+ dataTab.addEventListener("click", () => {
1992
+ dataTab.classList.add("active");
1993
+ plotTab.classList.remove("active");
1994
+ toolbar.style.display = "none";
1995
+ plotContent.style.display = "none";
1996
+ dataContent.style.display = "flex";
1997
+ if (!dataTable && tableHeaders?.length) {
1998
+ dataTable = new DataTable(dataTableContainer, {
1999
+ headers: tableHeaders,
2000
+ rows: tableRows || [],
2001
+ pagination: true,
2002
+ pageSize: 100,
2003
+ selectable: true,
2004
+ copyable: true,
2005
+ sortable: true,
2006
+ filterable: true,
2007
+ readonly: true,
2008
+ emptyMessage: "No data available",
2009
+ services,
2010
+ host
2011
+ });
2012
+ dataTable.render();
2013
+ }
2014
+ });
2015
+ function dispose() {
2016
+ if (resizeObserver) {
2017
+ resizeObserver.disconnect();
2018
+ resizeObserver = null;
2019
+ }
2020
+ if (dataTable) {
2021
+ dataTable.dispose();
2022
+ dataTable = null;
2023
+ }
2024
+ if (chartDiv && window.Plotly) {
2025
+ try {
2026
+ purge(chartDiv);
2027
+ } catch (_) {
2028
+ }
2029
+ }
2030
+ chartDiv = null;
2031
+ chartCreated = false;
2032
+ managedWindow = null;
2033
+ _openWindows.delete(id);
2034
+ }
2035
+ managedWindow = new ManagedWindow({
2036
+ id: `raw-plot-${id}`,
2037
+ title: title || "Chart",
2038
+ icon: "monitoring",
2039
+ content: container,
2040
+ minWidth: 500,
2041
+ minHeight: 400,
2042
+ defaultWidth: 800,
2043
+ defaultHeight: 600,
2044
+ canMinimize: true,
2045
+ canMaximize: true,
2046
+ canResize: true,
2047
+ canDrag: true,
2048
+ onClose: dispose
2049
+ });
2050
+ const handle = {
2051
+ get isVisible() {
2052
+ return managedWindow?.isVisible ?? false;
2053
+ },
2054
+ bringToFront() {
2055
+ managedWindow?.bringToFront?.();
2056
+ },
2057
+ close() {
2058
+ managedWindow?.close?.();
2059
+ }
2060
+ };
2061
+ _openWindows.set(id, handle);
2062
+ managedWindow.show();
2063
+ const defaultMargin = { l: 60, r: 30, t: 40, b: 60 };
2064
+ const plotLayout = {
2065
+ paper_bgcolor: "transparent",
2066
+ plot_bgcolor: "transparent",
2067
+ font: { family: "Inter, -apple-system, BlinkMacSystemFont, sans-serif", color: "#cccccc", size: 11 },
2068
+ hoverlabel: {
2069
+ bgcolor: "#1e2228",
2070
+ bordercolor: "#444444",
2071
+ font: { family: "Inter, -apple-system, BlinkMacSystemFont, sans-serif", color: "#cccccc" }
2072
+ },
2073
+ autosize: true,
2074
+ hovermode: "x unified",
2075
+ ...layout,
2076
+ margin: { ...defaultMargin, ...layout?.margin || {} }
2077
+ };
2078
+ await window.Plotly.newPlot(chartDiv, traces, plotLayout, {
2079
+ responsive: true,
2080
+ displayModeBar: true,
2081
+ displaylogo: false,
2082
+ modeBarButtonsToRemove: ["lasso2d", "select2d", "toImage"]
2083
+ });
2084
+ if (Array.isArray(frames) && frames.length) {
2085
+ try {
2086
+ await window.Plotly.addFrames(chartDiv, frames);
2087
+ } catch (err) {
2088
+ console.warn("[openRawTracesWindow] addFrames failed:", err);
2089
+ }
2090
+ }
2091
+ chartCreated = true;
2092
+ resizeObserver = createRafResizeObserver(() => {
2093
+ if (chartDiv && chartCreated && window.Plotly) {
2094
+ try {
2095
+ window.Plotly.Plots.resize(chartDiv);
2096
+ } catch (_) {
2097
+ }
2098
+ }
2099
+ });
2100
+ resizeObserver.observe(plotContent);
2101
+ function notify(ntitle, message, severity = "info") {
2102
+ services?.eventBus?.emit?.("toast:show", { title: ntitle, message, type: severity });
2103
+ }
2104
+ async function downloadPNG() {
2105
+ if (!chartDiv || !chartCreated || !window.Plotly) {
2106
+ notify("Download", "Plot not ready", "warn");
2107
+ return;
2108
+ }
2109
+ const filename = `${(title || "chart").replace(/[^a-z0-9]/gi, "_")}.png`;
2110
+ try {
2111
+ const dataUrl = await window.Plotly.toImage(chartDiv, {
2112
+ format: "png",
2113
+ width: 1920,
2114
+ height: 1080,
2115
+ scale: 2
2116
+ });
2117
+ const dialogs = host?.dialogs;
2118
+ const result = dialogs ? await dialogs.saveFile({ data: dataUrl.split(",")[1], filename, kind: "png" }) : null;
2119
+ if (result) {
2120
+ if (result.ok) notify("Download", `Plot saved to ${result.path}`, "success");
2121
+ else if (!result.cancelled) notify("Download", result.error || "Failed to save image", "error");
2122
+ } else {
2123
+ const link = document.createElement("a");
2124
+ link.href = dataUrl;
2125
+ link.download = filename;
2126
+ link.click();
2127
+ notify("Download", "PNG download started", "info");
2128
+ }
2129
+ } catch (err) {
2130
+ console.error("[openRawTracesWindow] PNG download failed:", err);
2131
+ notify("Download", "Failed to generate PNG", "error");
2132
+ }
2133
+ }
2134
+ async function downloadCSV() {
2135
+ if (!tableRows?.length) {
2136
+ notify("Download", "No data available to download", "warn");
2137
+ return;
2138
+ }
2139
+ const filename = `${(title || "data").replace(/[^a-z0-9]/gi, "_")}_data.csv`;
2140
+ const csvEscape = (val) => {
2141
+ if (val == null) return "";
2142
+ const str = String(val);
2143
+ return str.includes(",") || str.includes('"') || str.includes("\n") ? `"${str.replace(/"/g, '""')}"` : str;
2144
+ };
2145
+ const lines = [(tableHeaders || []).join(",")];
2146
+ for (const row of tableRows) lines.push(row.map(csvEscape).join(","));
2147
+ const csv = lines.join("\n");
2148
+ const dialogs = host?.dialogs;
2149
+ let result = null;
2150
+ if (dialogs) {
2151
+ try {
2152
+ result = await dialogs.saveFile({ data: csv, filename, kind: "csv" });
2153
+ } catch (err) {
2154
+ console.error("[openRawTracesWindow] CSV download failed:", err);
2155
+ notify("Download", "Failed to save file", "error");
2156
+ return;
2157
+ }
2158
+ }
2159
+ if (result) {
2160
+ if (result.ok) notify("Download", `Saved ${tableRows.length} rows to ${result.path}`, "success");
2161
+ else if (!result.cancelled) notify("Download", result.error || "Failed to save file", "error");
2162
+ } else {
2163
+ const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
2164
+ const url = URL.createObjectURL(blob);
2165
+ const link = document.createElement("a");
2166
+ link.href = url;
2167
+ link.download = filename;
2168
+ link.click();
2169
+ URL.revokeObjectURL(url);
2170
+ notify("Download", "Download started", "info");
2171
+ }
2172
+ }
2173
+ return handle;
2174
+ }
2175
+
2176
+ export {
2177
+ setPlotlySource,
2178
+ DARK_THEME_LAYOUT,
2179
+ DEFAULT_CONFIG,
2180
+ ensurePlotly,
2181
+ createChart,
2182
+ updateChart,
2183
+ extendTraces,
2184
+ relayout,
2185
+ restyle,
2186
+ purge,
2187
+ hasPlot,
2188
+ resize,
2189
+ hexToRgba,
2190
+ COLOR_PALETTE,
2191
+ getSeriesColor,
2192
+ createChartDeferred,
2193
+ CHART_TYPES_2D,
2194
+ CHART_TYPES_3D,
2195
+ ALL_CHART_TYPES,
2196
+ is3DChart,
2197
+ buildTrace,
2198
+ buildYAxisLayout,
2199
+ buildXAxisLayout,
2200
+ build3DSceneLayout,
2201
+ buildLegendLayout,
2202
+ buildOverlayShapes,
2203
+ buildFanBandTraces,
2204
+ buildHistogramTrace,
2205
+ buildBoxTrace,
2206
+ buildHeatmapTrace,
2207
+ uid,
2208
+ applyNanHandling,
2209
+ PLOT_LAYOUTS,
2210
+ makeDefaultSubplot,
2211
+ migrateData,
2212
+ migrateSingleChart,
2213
+ PlotPopoutWindow,
2214
+ openPlotPopoutWindow,
2215
+ openRawTracesWindow
2216
+ };
2217
+ //# sourceMappingURL=chunk-DRYCDMEG.js.map