flexdesk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +195 -0
  3. package/THIRD_PARTY_LICENSES.md +53 -0
  4. package/css/base.css +3439 -0
  5. package/css/flexdesk.css +5797 -0
  6. package/css/overrides.css +2349 -0
  7. package/css/reset.css +46 -0
  8. package/css/tokens.css +137 -0
  9. package/dist/charts.js +780 -0
  10. package/dist/charts.js.map +7 -0
  11. package/dist/chunk-3PHCPZHT.js +222 -0
  12. package/dist/chunk-3PHCPZHT.js.map +7 -0
  13. package/dist/chunk-CT4YXXLP.js +1770 -0
  14. package/dist/chunk-CT4YXXLP.js.map +7 -0
  15. package/dist/chunk-DRYCDMEG.js +2217 -0
  16. package/dist/chunk-DRYCDMEG.js.map +7 -0
  17. package/dist/chunk-DVU44T77.js +855 -0
  18. package/dist/chunk-DVU44T77.js.map +7 -0
  19. package/dist/chunk-FL5KFNQH.js +640 -0
  20. package/dist/chunk-FL5KFNQH.js.map +7 -0
  21. package/dist/chunk-FOOS3T5L.js +117 -0
  22. package/dist/chunk-FOOS3T5L.js.map +7 -0
  23. package/dist/chunk-JYWURG5T.js +32 -0
  24. package/dist/chunk-JYWURG5T.js.map +7 -0
  25. package/dist/chunk-TLZUUFOE.js +276 -0
  26. package/dist/chunk-TLZUUFOE.js.map +7 -0
  27. package/dist/chunk-UCJ2WD4D.js +625 -0
  28. package/dist/chunk-UCJ2WD4D.js.map +7 -0
  29. package/dist/chunk-WVFGV5FT.js +205 -0
  30. package/dist/chunk-WVFGV5FT.js.map +7 -0
  31. package/dist/core.js +1098 -0
  32. package/dist/core.js.map +7 -0
  33. package/dist/editor.js +1661 -0
  34. package/dist/editor.js.map +7 -0
  35. package/dist/flexdesk.css +5797 -0
  36. package/dist/host.js +132 -0
  37. package/dist/host.js.map +7 -0
  38. package/dist/reset.css +46 -0
  39. package/dist/tile_registry-6WZPTUZV.js +28 -0
  40. package/dist/tile_registry-6WZPTUZV.js.map +7 -0
  41. package/dist/tiles.js +2068 -0
  42. package/dist/tiles.js.map +7 -0
  43. package/dist/tokens.css +137 -0
  44. package/dist/widgets.js +5438 -0
  45. package/dist/widgets.js.map +7 -0
  46. package/dist/wm.js +4830 -0
  47. package/dist/wm.js.map +7 -0
  48. package/package.json +80 -0
  49. package/src/charts/chart_types.js +699 -0
  50. package/src/charts/data_series_plot_window.js +761 -0
  51. package/src/charts/downsample.js +119 -0
  52. package/src/charts/plot_config.js +176 -0
  53. package/src/charts/plot_popout_window.js +1560 -0
  54. package/src/charts/plotly_wrapper.js +432 -0
  55. package/src/core/event_bus.js +230 -0
  56. package/src/core/logging.js +300 -0
  57. package/src/core/settings.js +813 -0
  58. package/src/core/state_guard.js +382 -0
  59. package/src/core/state_guard_config.js +309 -0
  60. package/src/core/state_machine.js +258 -0
  61. package/src/editor/editor_pane.js +264 -0
  62. package/src/editor/monaco_editor_factory.js +449 -0
  63. package/src/editor/monaco_loader.js +74 -0
  64. package/src/editor/notebook_search.js +460 -0
  65. package/src/editor/notebook_tab_bar.js +456 -0
  66. package/src/editor/notebook_undo_manager.js +108 -0
  67. package/src/editor/split_pane_container.js +197 -0
  68. package/src/help/help_modal.js +409 -0
  69. package/src/help/help_registry.js +65 -0
  70. package/src/host/host.js +69 -0
  71. package/src/host/pywebview_host.js +133 -0
  72. package/src/tiles/config_schema.js +256 -0
  73. package/src/tiles/layout_persistence.js +662 -0
  74. package/src/tiles/tile_base.js +785 -0
  75. package/src/tiles/tile_grid.js +905 -0
  76. package/src/tiles/tile_registry.js +136 -0
  77. package/src/tiling/command_palette.js +328 -0
  78. package/src/tiling/content_registry.js +110 -0
  79. package/src/tiling/desktops.js +133 -0
  80. package/src/tiling/entity_sources.js +179 -0
  81. package/src/tiling/keymap.js +156 -0
  82. package/src/tiling/kind_taxonomy.js +172 -0
  83. package/src/tiling/landing_table.js +717 -0
  84. package/src/tiling/loading_overlay.js +33 -0
  85. package/src/tiling/nav_panel.js +142 -0
  86. package/src/tiling/page_factory.js +215 -0
  87. package/src/tiling/panel_keys.js +145 -0
  88. package/src/tiling/shell.js +437 -0
  89. package/src/tiling/tile_breadcrumb.js +222 -0
  90. package/src/tiling/tile_renderer.js +459 -0
  91. package/src/tiling/tile_tab_menu.js +340 -0
  92. package/src/tiling/tile_tree.js +880 -0
  93. package/src/tiling/wm.js +1352 -0
  94. package/src/ui/base/component_base.js +30 -0
  95. package/src/ui/base/controller_base.js +48 -0
  96. package/src/ui/base/page_base.js +102 -0
  97. package/src/ui/components/about_dialog.js +224 -0
  98. package/src/ui/components/action_dropdown.js +389 -0
  99. package/src/ui/components/attribute_list_editor.js +652 -0
  100. package/src/ui/components/autocomplete_field.js +732 -0
  101. package/src/ui/components/computing_status_window.js +81 -0
  102. package/src/ui/components/confirm_dialog.js +307 -0
  103. package/src/ui/components/context_menu.js +87 -0
  104. package/src/ui/components/data_table.js +2228 -0
  105. package/src/ui/components/detail_header.js +177 -0
  106. package/src/ui/components/drag_reorder.js +0 -0
  107. package/src/ui/components/gallery_picker.js +147 -0
  108. package/src/ui/components/inline_renamer.js +129 -0
  109. package/src/ui/components/managed_window.js +804 -0
  110. package/src/ui/components/modal.js +512 -0
  111. package/src/ui/components/notification_history.js +264 -0
  112. package/src/ui/components/slide_out_panel.js +185 -0
  113. package/src/ui/components/slider_field.js +122 -0
  114. package/src/ui/components/sortable_list.js +309 -0
  115. package/src/ui/components/table_state_store.js +81 -0
  116. package/src/ui/components/toast.js +59 -0
  117. package/src/ui/components/tree_view.js +460 -0
  118. package/src/ui/components/window_taskbar.js +143 -0
  119. package/src/ui/controllers/panel_state_machine.js +634 -0
  120. package/src/ui/controllers/window_chrome_controller.js +659 -0
  121. package/src/ui/notification_center.js +576 -0
  122. package/src/ui/utils/overlay_scrollbar.js +492 -0
  123. package/src/ui/utils/raf_resize_observer.js +31 -0
  124. package/src/ui/utils/tooltip_service.js +406 -0
package/dist/charts.js ADDED
@@ -0,0 +1,780 @@
1
+ import {
2
+ ALL_CHART_TYPES,
3
+ CHART_TYPES_2D,
4
+ CHART_TYPES_3D,
5
+ COLOR_PALETTE,
6
+ DARK_THEME_LAYOUT,
7
+ DEFAULT_CONFIG,
8
+ PLOT_LAYOUTS,
9
+ PlotPopoutWindow,
10
+ applyNanHandling,
11
+ build3DSceneLayout,
12
+ buildBoxTrace,
13
+ buildFanBandTraces,
14
+ buildHeatmapTrace,
15
+ buildHistogramTrace,
16
+ buildLegendLayout,
17
+ buildOverlayShapes,
18
+ buildTrace,
19
+ buildXAxisLayout,
20
+ buildYAxisLayout,
21
+ createChart,
22
+ createChartDeferred,
23
+ ensurePlotly,
24
+ extendTraces,
25
+ getSeriesColor,
26
+ hasPlot,
27
+ hexToRgba,
28
+ is3DChart,
29
+ makeDefaultSubplot,
30
+ migrateData,
31
+ migrateSingleChart,
32
+ openPlotPopoutWindow,
33
+ openRawTracesWindow,
34
+ purge,
35
+ relayout,
36
+ resize,
37
+ restyle,
38
+ setPlotlySource,
39
+ uid,
40
+ updateChart
41
+ } from "./chunk-DRYCDMEG.js";
42
+ import {
43
+ createRafResizeObserver
44
+ } from "./chunk-CT4YXXLP.js";
45
+ import {
46
+ ManagedWindow
47
+ } from "./chunk-UCJ2WD4D.js";
48
+ import "./chunk-FL5KFNQH.js";
49
+ import "./chunk-JYWURG5T.js";
50
+
51
+ // src/charts/data_series_plot_window.js
52
+ var defaultClassifyHeader = (header) => {
53
+ const lower = String(header ?? "").trim().toLowerCase();
54
+ return lower === "t" || lower === "time" || lower === "step" || lower === "period" ? "time" : "indicator";
55
+ };
56
+ var defaultLabelHeader = (header) => header;
57
+ var INTERPOLATION_MODES = ["linear", "step", "cubic"];
58
+ var CHART_TYPES = ["line", "scatter", "bar", "area", "area-stacked"];
59
+ var LEGEND_POSITIONS = ["top", "bottom", "right", "hidden"];
60
+ var MAX_AUTO_COLUMNS = 3;
61
+ var DataSeriesPlotWindow = class _DataSeriesPlotWindow {
62
+ /**
63
+ * @param {Object} options
64
+ * @param {string} options.seriesId
65
+ * @param {string} options.seriesName
66
+ * @param {string} options.datasetName
67
+ * @param {string[]} options.headers - Column headers from the series
68
+ * @param {Object} options.dataHub - DataHub service for fetching data
69
+ * @param {Object} options.services - { eventBus, logger }
70
+ * @param {Object} [options.host] - A Host (ui/js/host/host.js). Its `dialogs`
71
+ * capability backs Export PNG/CSV; absent => Blob download fallback.
72
+ * @param {Function} options.getPlotConfig - (seriesId) => config | null
73
+ * @param {Function} options.savePlotConfig - (seriesId, config) => void
74
+ */
75
+ constructor({
76
+ seriesId,
77
+ seriesName,
78
+ datasetName,
79
+ headers,
80
+ dataHub,
81
+ services,
82
+ host,
83
+ getPlotConfig,
84
+ savePlotConfig,
85
+ classifyHeader = defaultClassifyHeader,
86
+ labelHeader = defaultLabelHeader
87
+ }) {
88
+ this.classifyHeader = classifyHeader;
89
+ this.labelHeader = labelHeader;
90
+ this.seriesId = seriesId;
91
+ this.seriesName = seriesName;
92
+ this.datasetName = datasetName;
93
+ this.headers = headers;
94
+ this.dataHub = dataHub;
95
+ this.services = services;
96
+ this._host = host || null;
97
+ this.getPlotConfig = getPlotConfig;
98
+ this.savePlotConfig = savePlotConfig;
99
+ this.window = null;
100
+ this.contentEl = null;
101
+ this.plotDiv = null;
102
+ this._resizeObserver = null;
103
+ this._chartCreated = false;
104
+ this._config = null;
105
+ this._fullData = null;
106
+ this._numericColumns = null;
107
+ this._loading = false;
108
+ }
109
+ async show() {
110
+ if (this.window?.isVisible) {
111
+ this.window.bringToFront();
112
+ return;
113
+ }
114
+ await ensurePlotly();
115
+ await this.#fetchData();
116
+ this._numericColumns = new Set(this.headers.filter((h) => this.#isNumericColumn(h)));
117
+ const saved = this.getPlotConfig?.(this.seriesId);
118
+ if (saved) {
119
+ this._config = { ...saved };
120
+ this._config.yColumns = (saved.yColumns || []).filter((c) => this._numericColumns.has(c.header));
121
+ } else {
122
+ this._config = _DataSeriesPlotWindow.createDefaultConfig(this.headers, this._numericColumns);
123
+ }
124
+ this.#createWindow();
125
+ this.#setupResizeObserver();
126
+ await this.#renderPlot();
127
+ }
128
+ close() {
129
+ if (this.window?.isVisible) {
130
+ this.window.close();
131
+ }
132
+ }
133
+ // ── Window creation ──────────────────────────────────────────────────
134
+ #createWindow() {
135
+ this.contentEl = this.#buildContent();
136
+ const title = this.datasetName ? `${this.datasetName} \xB7 ${this.seriesName}` : this.seriesName || "Series Plot";
137
+ this.window = new ManagedWindow({
138
+ id: `data-series-plot-${this.seriesId}`,
139
+ title,
140
+ icon: "monitoring",
141
+ content: this.contentEl,
142
+ minWidth: 600,
143
+ minHeight: 400,
144
+ defaultWidth: 900,
145
+ defaultHeight: 550,
146
+ canMinimize: true,
147
+ canMaximize: true,
148
+ canResize: true,
149
+ canDrag: true,
150
+ onClose: () => this.#dispose()
151
+ });
152
+ this.window.show();
153
+ }
154
+ #buildContent() {
155
+ const container = document.createElement("div");
156
+ container.className = "twm-data-series-plot-container";
157
+ const actionsBar = document.createElement("div");
158
+ actionsBar.className = "twm-data-series-plot-actions";
159
+ const pngBtn = document.createElement("button");
160
+ pngBtn.className = "twm-btn-icon twm-has-tooltip";
161
+ pngBtn.type = "button";
162
+ pngBtn.setAttribute("data-tooltip", "Download plot as PNG");
163
+ pngBtn.innerHTML = '<span class="material-symbols-outlined">image</span>';
164
+ pngBtn.addEventListener("click", () => this.#downloadPNG());
165
+ const csvBtn = document.createElement("button");
166
+ csvBtn.className = "twm-btn-icon twm-has-tooltip";
167
+ csvBtn.type = "button";
168
+ csvBtn.setAttribute("data-tooltip", "Download data as CSV");
169
+ csvBtn.innerHTML = '<span class="material-symbols-outlined">download</span>';
170
+ csvBtn.addEventListener("click", () => this.#downloadCSV());
171
+ actionsBar.appendChild(pngBtn);
172
+ actionsBar.appendChild(csvBtn);
173
+ const chartWrap = document.createElement("div");
174
+ chartWrap.className = "twm-data-series-plot-body";
175
+ this.plotDiv = document.createElement("div");
176
+ this.plotDiv.className = "twm-data-series-plot-chart";
177
+ chartWrap.appendChild(this.plotDiv);
178
+ const configPanel = this.#buildConfigPanel();
179
+ chartWrap.appendChild(configPanel);
180
+ container.appendChild(actionsBar);
181
+ container.appendChild(chartWrap);
182
+ return container;
183
+ }
184
+ // ── Config panel ─────────────────────────────────────────────────────
185
+ #buildConfigPanel() {
186
+ const panel = document.createElement("div");
187
+ panel.className = "twm-data-series-plot-config";
188
+ panel.appendChild(this.#buildSection("X Axis", this.#buildXColumnSelect()));
189
+ this._yColumnsList = document.createElement("div");
190
+ this._yColumnsList.className = "twm-data-series-plot-config__columns";
191
+ this.#populateYColumns();
192
+ panel.appendChild(this.#buildSection("Y Axis", this._yColumnsList));
193
+ panel.appendChild(this.#buildSection("Chart Type", this.#buildSelect(
194
+ CHART_TYPES,
195
+ this._config.chartType,
196
+ (val) => {
197
+ this._config.chartType = val;
198
+ this.#onConfigChanged();
199
+ }
200
+ )));
201
+ panel.appendChild(this.#buildSection("Interpolation", this.#buildSelect(
202
+ INTERPOLATION_MODES,
203
+ this._config.interpolation,
204
+ (val) => {
205
+ this._config.interpolation = val;
206
+ this.#onConfigChanged();
207
+ }
208
+ )));
209
+ panel.appendChild(this.#buildSection("Legend", this.#buildSelect(
210
+ LEGEND_POSITIONS,
211
+ this._config.legendPosition,
212
+ (val) => {
213
+ this._config.legendPosition = val;
214
+ this.#onConfigChanged();
215
+ }
216
+ )));
217
+ panel.appendChild(this.#buildSection("", this.#buildCheckbox(
218
+ "Show data points",
219
+ this._config.showDataPoints,
220
+ (checked) => {
221
+ this._config.showDataPoints = checked;
222
+ this.#onConfigChanged();
223
+ }
224
+ )));
225
+ return panel;
226
+ }
227
+ #buildSection(label, content) {
228
+ const section = document.createElement("div");
229
+ section.className = "twm-data-series-plot-config__section";
230
+ if (label) {
231
+ const labelEl = document.createElement("div");
232
+ labelEl.className = "twm-data-series-plot-config__label";
233
+ labelEl.textContent = label;
234
+ section.appendChild(labelEl);
235
+ }
236
+ if (content instanceof HTMLElement) {
237
+ section.appendChild(content);
238
+ }
239
+ return section;
240
+ }
241
+ #buildXColumnSelect() {
242
+ const select = document.createElement("select");
243
+ for (const header of this.headers) {
244
+ if (!this._numericColumns.has(header)) continue;
245
+ const opt = document.createElement("option");
246
+ opt.value = header;
247
+ opt.textContent = this.labelHeader(header);
248
+ if (header === this._config.xColumn) opt.selected = true;
249
+ select.appendChild(opt);
250
+ }
251
+ select.addEventListener("change", () => {
252
+ this._config.xColumn = select.value;
253
+ this.#onConfigChanged();
254
+ });
255
+ return select;
256
+ }
257
+ #populateYColumns() {
258
+ const list = this._yColumnsList;
259
+ list.innerHTML = "";
260
+ const selectedSet = new Set(this._config.yColumns.map((c) => c.header));
261
+ for (const header of this.headers) {
262
+ if (header === this._config.xColumn) continue;
263
+ if (!this._numericColumns.has(header)) continue;
264
+ const item = document.createElement("label");
265
+ item.className = "twm-data-series-plot-config__column-item";
266
+ const cb = document.createElement("input");
267
+ cb.type = "checkbox";
268
+ cb.checked = selectedSet.has(header);
269
+ cb.addEventListener("change", () => this.#toggleYColumn(header, cb.checked));
270
+ const colorSwatch = document.createElement("input");
271
+ colorSwatch.type = "color";
272
+ colorSwatch.className = "twm-data-series-plot-config__color-swatch";
273
+ const existing = this._config.yColumns.find((c) => c.header === header);
274
+ colorSwatch.value = existing?.color || this.#nextColor(this.headers.indexOf(header));
275
+ colorSwatch.addEventListener("input", () => this.#updateColumnColor(header, colorSwatch.value));
276
+ const label = document.createElement("span");
277
+ label.textContent = this.labelHeader(header);
278
+ label.title = header;
279
+ item.appendChild(cb);
280
+ item.appendChild(colorSwatch);
281
+ item.appendChild(label);
282
+ list.appendChild(item);
283
+ }
284
+ }
285
+ #toggleYColumn(header, checked) {
286
+ if (checked) {
287
+ if (!this._config.yColumns.find((c) => c.header === header)) {
288
+ const colorSwatch = this._yColumnsList.querySelector(
289
+ `label:has(span[title="${header}"]) input[type="color"]`
290
+ );
291
+ this._config.yColumns.push({
292
+ header,
293
+ color: colorSwatch?.value || this.#nextColor(this.headers.indexOf(header))
294
+ });
295
+ }
296
+ } else {
297
+ this._config.yColumns = this._config.yColumns.filter((c) => c.header !== header);
298
+ }
299
+ this.#onConfigChanged();
300
+ }
301
+ #updateColumnColor(header, color) {
302
+ const col = this._config.yColumns.find((c) => c.header === header);
303
+ if (col) {
304
+ col.color = color;
305
+ this.#onConfigChanged();
306
+ }
307
+ }
308
+ #nextColor(index) {
309
+ return getSeriesColor(index);
310
+ }
311
+ #buildSelect(options, value, onChange) {
312
+ const select = document.createElement("select");
313
+ for (const opt of options) {
314
+ const el = document.createElement("option");
315
+ el.value = opt;
316
+ el.textContent = opt.charAt(0).toUpperCase() + opt.slice(1);
317
+ if (opt === value) el.selected = true;
318
+ select.appendChild(el);
319
+ }
320
+ select.addEventListener("change", () => onChange(select.value));
321
+ return select;
322
+ }
323
+ #buildCheckbox(label, checked, onChange) {
324
+ const wrapper = document.createElement("label");
325
+ wrapper.className = "twm-data-series-plot-config__column-item";
326
+ const cb = document.createElement("input");
327
+ cb.type = "checkbox";
328
+ cb.checked = checked;
329
+ cb.addEventListener("change", () => onChange(cb.checked));
330
+ const span = document.createElement("span");
331
+ span.textContent = label;
332
+ wrapper.appendChild(cb);
333
+ wrapper.appendChild(span);
334
+ return wrapper;
335
+ }
336
+ // ── Data fetching ────────────────────────────────────────────────────
337
+ async #fetchData() {
338
+ if (this._loading) return;
339
+ this._loading = true;
340
+ try {
341
+ const response = await this.dataHub.fetchSeriesById(this.seriesId, { limit: null, offset: 0 });
342
+ if (response?.ok) {
343
+ this._fullData = {
344
+ headers: response.headers || [],
345
+ rows: response.rows || []
346
+ };
347
+ } else {
348
+ this.#notify("Plot", response?.error || "Failed to load series data", "error");
349
+ }
350
+ } catch (err) {
351
+ this.services.logger?.warn?.("[DataSeriesPlotWindow] Fetch failed", err);
352
+ this.#notify("Plot", "Failed to load data", "error");
353
+ } finally {
354
+ this._loading = false;
355
+ }
356
+ }
357
+ #isNumericColumn(header) {
358
+ if (!this._fullData) return false;
359
+ const data = this.#extractColumnData(header);
360
+ let numericCount = 0;
361
+ let sampleCount = 0;
362
+ for (const v of data) {
363
+ if (v == null || v === "") continue;
364
+ sampleCount++;
365
+ if (Number.isFinite(Number(v))) numericCount++;
366
+ if (sampleCount >= 20) break;
367
+ }
368
+ return sampleCount > 0 && numericCount / sampleCount >= 0.8;
369
+ }
370
+ // ── Chart rendering ──────────────────────────────────────────────────
371
+ #extractColumnData(columnHeader) {
372
+ if (!this._fullData) return [];
373
+ const { headers, rows } = this._fullData;
374
+ const idx = headers.indexOf(columnHeader);
375
+ if (idx < 0) return [];
376
+ const TIME_NAMES = /* @__PURE__ */ new Set(["time", "tDisp", "year", "date", "timestamp"]);
377
+ const firstIsTime = TIME_NAMES.has(headers[0]);
378
+ const colOffset = firstIsTime ? 1 : 0;
379
+ return rows.map((row) => {
380
+ if (Array.isArray(row)) return row[idx];
381
+ if (row && typeof row === "object") {
382
+ if (TIME_NAMES.has(columnHeader) && row.tDisp !== void 0) return row.tDisp;
383
+ if (Array.isArray(row.cols)) return row.cols[idx - colOffset] ?? "";
384
+ return row[columnHeader] ?? "";
385
+ }
386
+ return "";
387
+ }).map((v) => {
388
+ const n = Number(v);
389
+ return Number.isFinite(n) ? n : v;
390
+ });
391
+ }
392
+ async #renderPlot() {
393
+ if (!this.plotDiv || !window.Plotly || !this._fullData) return;
394
+ const cfg = this._config;
395
+ const xData = this.#extractColumnData(cfg.xColumn);
396
+ if (xData.length === 0 || cfg.yColumns.length === 0) {
397
+ if (this._chartCreated) {
398
+ try {
399
+ purge(this.plotDiv);
400
+ } catch (e) {
401
+ }
402
+ this._chartCreated = false;
403
+ }
404
+ this.plotDiv.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100%;color:#888;font-size:13px;">Select columns to plot</div>';
405
+ return;
406
+ }
407
+ if (!this._chartCreated) {
408
+ this.plotDiv.innerHTML = "";
409
+ }
410
+ const traces = [];
411
+ for (let i = 0; i < cfg.yColumns.length; i++) {
412
+ const col = cfg.yColumns[i];
413
+ const yData = this.#extractColumnData(col.header);
414
+ traces.push(buildTrace({
415
+ chartType: cfg.chartType,
416
+ x: xData,
417
+ y: yData,
418
+ name: this.labelHeader(col.header),
419
+ color: col.color,
420
+ lineWidth: 2,
421
+ lineStyle: "solid",
422
+ interpolation: cfg.interpolation,
423
+ yAxisId: "y",
424
+ showMarkers: cfg.showDataPoints,
425
+ seriesIndex: i
426
+ }));
427
+ }
428
+ const legendAtTop = cfg.legendPosition === "top";
429
+ const legendAtBottom = cfg.legendPosition === "bottom";
430
+ const layout = {
431
+ ...DARK_THEME_LAYOUT,
432
+ autosize: true,
433
+ margin: {
434
+ l: 60,
435
+ r: 40,
436
+ b: legendAtBottom ? 80 : 50,
437
+ t: legendAtTop ? 60 : 30
438
+ }
439
+ };
440
+ delete layout.scene;
441
+ layout.xaxis = {
442
+ ...buildXAxisLayout({ title: this.labelHeader(cfg.xColumn) })
443
+ };
444
+ layout.hovermode = "x unified";
445
+ layout.xaxis.showspikes = true;
446
+ layout.xaxis.spikemode = "across";
447
+ layout.xaxis.spikethickness = 1;
448
+ layout.xaxis.spikecolor = "#666666";
449
+ layout.xaxis.spikedash = "dot";
450
+ layout.yaxis = buildYAxisLayout({
451
+ axisId: "yaxis",
452
+ title: "",
453
+ position: "left",
454
+ scaleType: "linear",
455
+ showGrid: true
456
+ });
457
+ const legendConfig = buildLegendLayout(cfg.legendPosition);
458
+ if (legendConfig.legend) {
459
+ layout.legend = legendConfig.legend;
460
+ }
461
+ layout.showlegend = legendConfig.showlegend !== false;
462
+ const plotConfig = {
463
+ ...DEFAULT_CONFIG,
464
+ responsive: true,
465
+ displayModeBar: true,
466
+ modeBarButtonsToRemove: ["lasso2d", "select2d", "toImage"]
467
+ };
468
+ try {
469
+ await window.Plotly.newPlot(this.plotDiv, traces, layout, plotConfig);
470
+ this._chartCreated = true;
471
+ requestAnimationFrame(() => {
472
+ try {
473
+ window.Plotly.Plots.resize(this.plotDiv);
474
+ } catch (e) {
475
+ }
476
+ });
477
+ } catch (e) {
478
+ console.error("[DataSeriesPlotWindow] Chart render error:", e);
479
+ }
480
+ }
481
+ // ── Config change handler ────────────────────────────────────────────
482
+ #onConfigChanged() {
483
+ this.#renderPlot();
484
+ this.savePlotConfig?.(this.seriesId, { ...this._config, yColumns: [...this._config.yColumns] });
485
+ }
486
+ // ── Resize observer ──────────────────────────────────────────────────
487
+ #setupResizeObserver() {
488
+ if (!this.plotDiv) return;
489
+ this._resizeObserver = createRafResizeObserver(() => {
490
+ if (this.plotDiv && window.Plotly && this._chartCreated) {
491
+ try {
492
+ window.Plotly.Plots.resize(this.plotDiv);
493
+ } catch (e) {
494
+ }
495
+ }
496
+ });
497
+ this._resizeObserver.observe(this.plotDiv);
498
+ }
499
+ // ── Downloads ────────────────────────────────────────────────────────
500
+ async #downloadPNG() {
501
+ if (!this.plotDiv || !window.Plotly || !this._chartCreated) {
502
+ this.#notify("Download", "Plot not ready", "warn");
503
+ return;
504
+ }
505
+ const filename = `${(this.seriesName || "series").replace(/[^a-z0-9]/gi, "_")}.png`;
506
+ try {
507
+ const pngDataUrl = await window.Plotly.toImage(this.plotDiv, {
508
+ format: "png",
509
+ width: 1920,
510
+ height: 1080,
511
+ scale: 2
512
+ });
513
+ const dialogs = this._host?.dialogs;
514
+ const result = dialogs ? await dialogs.saveFile({ data: pngDataUrl.split(",")[1], filename, kind: "png" }) : null;
515
+ if (result) {
516
+ if (result.ok) {
517
+ this.#notify("Download", `Plot saved to ${result.path}`, "success");
518
+ } else if (!result.cancelled) {
519
+ this.#notify("Download", result.error || "Failed to save image", "error");
520
+ }
521
+ } else {
522
+ const link = document.createElement("a");
523
+ link.href = pngDataUrl;
524
+ link.download = filename;
525
+ link.click();
526
+ }
527
+ } catch (err) {
528
+ this.services.logger?.warn?.("[DataSeriesPlotWindow] PNG download failed", err);
529
+ this.#notify("Download", "Failed to generate PNG", "error");
530
+ }
531
+ }
532
+ async #downloadCSV() {
533
+ if (!this._fullData || !this._fullData.rows.length) {
534
+ this.#notify("Download", "No data available", "warn");
535
+ return;
536
+ }
537
+ const cfg = this._config;
538
+ const columns = [cfg.xColumn, ...cfg.yColumns.map((c) => c.header)];
539
+ const headers = columns.map((h) => this.labelHeader(h));
540
+ const columnData = columns.map((col) => this.#extractColumnData(col));
541
+ const rowCount = this._fullData.rows.length;
542
+ const lines = [headers.join(",")];
543
+ for (let r = 0; r < rowCount; r++) {
544
+ const vals = columnData.map((data) => this.#csvEscape(data[r]));
545
+ lines.push(vals.join(","));
546
+ }
547
+ const csv = lines.join("\n");
548
+ const filename = `${(this.seriesName || "series").replace(/[^a-z0-9]/gi, "_")}_data.csv`;
549
+ const dialogs = this._host?.dialogs;
550
+ let result = null;
551
+ if (dialogs) {
552
+ try {
553
+ result = await dialogs.saveFile({ data: csv, filename, kind: "csv" });
554
+ } catch (err) {
555
+ this.#notify("Download", "Failed to save file", "error");
556
+ return;
557
+ }
558
+ }
559
+ if (result) {
560
+ if (result.ok) {
561
+ this.#notify("Download", `Saved to ${result.path}`, "success");
562
+ } else if (!result.cancelled) {
563
+ this.#notify("Download", result.error || "Failed to save file", "error");
564
+ }
565
+ } else {
566
+ const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
567
+ const url = URL.createObjectURL(blob);
568
+ const link = document.createElement("a");
569
+ link.href = url;
570
+ link.download = filename;
571
+ link.click();
572
+ URL.revokeObjectURL(url);
573
+ }
574
+ }
575
+ #csvEscape(value) {
576
+ if (value == null) return "";
577
+ const str = String(value);
578
+ if (/[",\n]/.test(str)) return `"${str.replace(/"/g, '""')}"`;
579
+ return str;
580
+ }
581
+ // ── Notifications ────────────────────────────────────────────────────
582
+ #notify(title, message, severity = "info") {
583
+ this.services.eventBus?.emit?.("toast:show", { title, message, type: severity });
584
+ }
585
+ // ── Disposal ─────────────────────────────────────────────────────────
586
+ #dispose() {
587
+ if (this._resizeObserver) {
588
+ this._resizeObserver.disconnect();
589
+ this._resizeObserver = null;
590
+ }
591
+ if (this.plotDiv && window.Plotly) {
592
+ try {
593
+ purge(this.plotDiv);
594
+ } catch (e) {
595
+ }
596
+ }
597
+ _openPlotWindows.delete(this.seriesId);
598
+ this.window = null;
599
+ this.contentEl = null;
600
+ this.plotDiv = null;
601
+ this._yColumnsList = null;
602
+ this._chartCreated = false;
603
+ this._fullData = null;
604
+ this._numericColumns = null;
605
+ this._config = null;
606
+ }
607
+ // ── Default config factory ───────────────────────────────────────────
608
+ static createDefaultConfig(headers, numericColumns) {
609
+ const isNumeric = numericColumns instanceof Set ? (h) => numericColumns.has(h) : () => true;
610
+ let xColumn = "";
611
+ for (const h of headers) {
612
+ if (isNumeric(h) && this.classifyHeader(h) === "time") {
613
+ xColumn = h;
614
+ break;
615
+ }
616
+ }
617
+ if (!xColumn) {
618
+ for (const h of headers) {
619
+ if (isNumeric(h)) {
620
+ xColumn = h;
621
+ break;
622
+ }
623
+ }
624
+ }
625
+ const yColumns = [];
626
+ for (const h of headers) {
627
+ if (h === xColumn) continue;
628
+ if (!isNumeric(h)) continue;
629
+ if (yColumns.length >= MAX_AUTO_COLUMNS) break;
630
+ yColumns.push({ header: h, color: getSeriesColor(yColumns.length) });
631
+ }
632
+ return {
633
+ xColumn,
634
+ yColumns,
635
+ chartType: "line",
636
+ showDataPoints: false,
637
+ interpolation: "linear",
638
+ legendPosition: "top"
639
+ };
640
+ }
641
+ };
642
+ var _openPlotWindows = /* @__PURE__ */ new Map();
643
+ async function openDataSeriesPlotWindow(options) {
644
+ const { seriesId } = options;
645
+ let win = _openPlotWindows.get(seriesId);
646
+ if (win?.window?.isVisible) {
647
+ win.window.bringToFront();
648
+ return win;
649
+ }
650
+ win = new DataSeriesPlotWindow(options);
651
+ _openPlotWindows.set(seriesId, win);
652
+ await win.show();
653
+ return win;
654
+ }
655
+ function closeDataSeriesPlotWindow(seriesId) {
656
+ const win = _openPlotWindows.get(seriesId);
657
+ if (win) {
658
+ win.close();
659
+ _openPlotWindows.delete(seriesId);
660
+ }
661
+ }
662
+
663
+ // src/charts/downsample.js
664
+ function strideIndices(n, targetPoints) {
665
+ if (!Number.isFinite(n) || n <= 0) return null;
666
+ const target = Math.max(2, Math.floor(targetPoints) || 0);
667
+ if (n <= target) return null;
668
+ const step = (n - 1) / (target - 1);
669
+ const idx = new Array(target);
670
+ for (let i = 0; i < target; i++) idx[i] = Math.round(i * step);
671
+ idx[target - 1] = n - 1;
672
+ let w = 0;
673
+ for (let r = 0; r < idx.length; r++) {
674
+ if (w === 0 || idx[r] > idx[w - 1]) idx[w++] = idx[r];
675
+ }
676
+ idx.length = w;
677
+ return idx;
678
+ }
679
+ function strideDownsampleShared(time, series, targetPoints) {
680
+ const idx = strideIndices(time?.length ?? 0, targetPoints);
681
+ if (!idx) return { time, series };
682
+ const dsTime = idx.map((i) => time[i]);
683
+ const dsSeries = {};
684
+ for (const [name, arr] of Object.entries(series || {})) {
685
+ if (!Array.isArray(arr)) {
686
+ dsSeries[name] = arr;
687
+ continue;
688
+ }
689
+ dsSeries[name] = idx.map((i) => i < arr.length ? arr[i] : null);
690
+ }
691
+ return { time: dsTime, series: dsSeries };
692
+ }
693
+ function lttb(x, y, threshold) {
694
+ const n = Math.min(x?.length ?? 0, y?.length ?? 0);
695
+ const target = Math.floor(threshold) || 0;
696
+ if (target <= 0 || target >= n) return { x: x.slice(0, n), y: y.slice(0, n) };
697
+ if (target < 3) return { x: [x[0], x[n - 1]], y: [y[0], y[n - 1]] };
698
+ const num = (v) => Number.isFinite(v) ? v : 0;
699
+ const outX = [x[0]];
700
+ const outY = [y[0]];
701
+ const bucketSize = (n - 2) / (target - 2);
702
+ let a = 0;
703
+ for (let i = 0; i < target - 2; i++) {
704
+ let nextStart = Math.floor((i + 1) * bucketSize) + 1;
705
+ let nextEnd = Math.min(Math.floor((i + 2) * bucketSize) + 1, n);
706
+ if (nextEnd <= nextStart) nextEnd = Math.min(nextStart + 1, n);
707
+ let avgX = 0, avgY = 0;
708
+ const count = nextEnd - nextStart;
709
+ for (let j = nextStart; j < nextEnd; j++) {
710
+ avgX += x[j];
711
+ avgY += num(y[j]);
712
+ }
713
+ avgX /= count;
714
+ avgY /= count;
715
+ const curStart = Math.floor(i * bucketSize) + 1;
716
+ const curEnd = Math.min(Math.floor((i + 1) * bucketSize) + 1, n);
717
+ const ax = x[a], ay = num(y[a]);
718
+ let maxArea = -1, chosen = curStart;
719
+ for (let j = curStart; j < curEnd; j++) {
720
+ const area = Math.abs((ax - avgX) * (num(y[j]) - ay) - (ax - x[j]) * (avgY - ay));
721
+ if (area > maxArea) {
722
+ maxArea = area;
723
+ chosen = j;
724
+ }
725
+ }
726
+ outX.push(x[chosen]);
727
+ outY.push(y[chosen]);
728
+ a = chosen;
729
+ }
730
+ outX.push(x[n - 1]);
731
+ outY.push(y[n - 1]);
732
+ return { x: outX, y: outY };
733
+ }
734
+ export {
735
+ ALL_CHART_TYPES,
736
+ CHART_TYPES_2D,
737
+ CHART_TYPES_3D,
738
+ COLOR_PALETTE,
739
+ DARK_THEME_LAYOUT,
740
+ DEFAULT_CONFIG,
741
+ PLOT_LAYOUTS,
742
+ PlotPopoutWindow,
743
+ applyNanHandling,
744
+ build3DSceneLayout,
745
+ buildBoxTrace,
746
+ buildFanBandTraces,
747
+ buildHeatmapTrace,
748
+ buildHistogramTrace,
749
+ buildLegendLayout,
750
+ buildOverlayShapes,
751
+ buildTrace,
752
+ buildXAxisLayout,
753
+ buildYAxisLayout,
754
+ closeDataSeriesPlotWindow,
755
+ createChart,
756
+ createChartDeferred,
757
+ ensurePlotly,
758
+ extendTraces,
759
+ getSeriesColor,
760
+ hasPlot,
761
+ hexToRgba,
762
+ is3DChart,
763
+ lttb,
764
+ makeDefaultSubplot,
765
+ migrateData,
766
+ migrateSingleChart,
767
+ openDataSeriesPlotWindow,
768
+ openPlotPopoutWindow,
769
+ openRawTracesWindow,
770
+ purge,
771
+ relayout,
772
+ resize,
773
+ restyle,
774
+ setPlotlySource,
775
+ strideDownsampleShared,
776
+ strideIndices,
777
+ uid,
778
+ updateChart
779
+ };
780
+ //# sourceMappingURL=charts.js.map