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