flexdesk 0.16.1 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +93 -0
- package/LICENSE +0 -0
- package/README.md +2217 -42
- package/THIRD_PARTY_LICENSES.md +0 -0
- package/css/base.css +1834 -75
- package/css/flexdesk.css +3146 -164
- package/css/overrides.css +16 -0
- package/css/reset.css +0 -0
- package/css/tokens.css +0 -0
- package/dist/canvas.js +6 -239
- package/dist/canvas.js.map +3 -3
- package/dist/charts.js +709 -18
- package/dist/charts.js.map +4 -4
- package/dist/chunk-3PHCPZHT.js +0 -0
- package/dist/chunk-3PHCPZHT.js.map +0 -0
- package/dist/chunk-4A7SJC3T.js +503 -0
- package/dist/chunk-4A7SJC3T.js.map +7 -0
- package/dist/{chunk-6JSOVNID.js → chunk-CVMCZZX2.js} +635 -10
- package/dist/chunk-CVMCZZX2.js.map +7 -0
- package/dist/chunk-D3COLZZ2.js +1311 -0
- package/dist/chunk-D3COLZZ2.js.map +7 -0
- package/dist/chunk-FOOS3T5L.js +0 -0
- package/dist/chunk-FOOS3T5L.js.map +0 -0
- package/dist/{chunk-SN4LSOAR.js → chunk-GOQLR5WR.js} +452 -2
- package/dist/chunk-GOQLR5WR.js.map +7 -0
- package/dist/{chunk-FL5KFNQH.js → chunk-HNU6MM2B.js} +52 -17
- package/dist/chunk-HNU6MM2B.js.map +7 -0
- package/dist/chunk-HYJYYEPK.js +375 -0
- package/dist/chunk-HYJYYEPK.js.map +7 -0
- package/dist/chunk-JKZLNKG6.js +2041 -0
- package/dist/chunk-JKZLNKG6.js.map +7 -0
- package/dist/chunk-JYWURG5T.js +0 -0
- package/dist/chunk-JYWURG5T.js.map +0 -0
- package/dist/{chunk-RQM5Z2MU.js → chunk-MCIXFNVF.js} +633 -311
- package/dist/chunk-MCIXFNVF.js.map +7 -0
- package/dist/chunk-N7HC3ADV.js +30 -0
- package/dist/chunk-N7HC3ADV.js.map +7 -0
- package/dist/{chunk-FBGP5KN5.js → chunk-Q36EPT2R.js} +3268 -203
- package/dist/chunk-Q36EPT2R.js.map +7 -0
- package/dist/chunk-QWQJUDPD.js +610 -0
- package/dist/chunk-QWQJUDPD.js.map +7 -0
- package/dist/{chunk-NLTOY57M.js → chunk-UURZ37SD.js} +28 -48
- package/dist/chunk-UURZ37SD.js.map +7 -0
- package/dist/{chunk-B5Q53LAJ.js → chunk-UWO43BZU.js} +2 -2
- package/dist/chunk-UWO43BZU.js.map +7 -0
- package/dist/chunk-WVFGV5FT.js +0 -0
- package/dist/chunk-WVFGV5FT.js.map +0 -0
- package/dist/core.js +170 -118
- package/dist/core.js.map +2 -2
- package/dist/editor.js +156 -70
- package/dist/editor.js.map +2 -2
- package/dist/flexdesk.css +3146 -164
- package/dist/flow.js +2789 -306
- package/dist/flow.js.map +4 -4
- package/dist/host.js +109 -3
- package/dist/host.js.map +3 -3
- package/dist/reset.css +0 -0
- package/dist/tile_registry-6WZPTUZV.js +0 -0
- package/dist/tile_registry-6WZPTUZV.js.map +0 -0
- package/dist/tiles.js +657 -234
- package/dist/tiles.js.map +4 -4
- package/dist/tokens.css +0 -0
- package/dist/widgets.js +3252 -2124
- package/dist/widgets.js.map +4 -4
- package/dist/wm.js +1227 -347
- package/dist/wm.js.map +4 -4
- package/package.json +3 -1
- package/src/canvas/canvas_adapter.js +5 -0
- package/src/canvas/edge_router.js +0 -0
- package/src/canvas/graph/connector_router.js +0 -0
- package/src/canvas/graph/history_service.js +0 -0
- package/src/canvas/graph/node_platform.js +0 -0
- package/src/canvas/index.js +1 -0
- package/src/canvas/layered_layout.js +244 -0
- package/src/charts/chart_types.js +0 -0
- package/src/charts/data_series_plot_window.js +0 -0
- package/src/charts/downsample.js +2 -2
- package/src/charts/plot_config.js +53 -29
- package/src/charts/plot_config_editor.js +627 -0
- package/src/charts/plot_grid.js +449 -0
- package/src/charts/plot_popout_window.js +103 -317
- package/src/charts/plotly_wrapper.js +22 -2
- package/src/charts/series_transforms.js +206 -0
- package/src/core/event_bus.js +0 -0
- package/src/core/logging.js +1 -1
- package/src/core/settings.js +105 -19
- package/src/core/state_guard.js +93 -51
- package/src/core/state_guard_config.js +85 -74
- package/src/core/state_machine.js +0 -0
- package/src/editor/editor_pane.js +3 -3
- package/src/editor/monaco_editor_factory.js +124 -60
- package/src/editor/monaco_loader.js +103 -28
- package/src/editor/notebook_search.js +0 -0
- package/src/editor/notebook_tab_bar.js +38 -52
- package/src/editor/notebook_undo_manager.js +0 -0
- package/src/editor/split_pane_container.js +6 -3
- package/src/flow/canvas_editor/connections.js +90 -0
- package/src/flow/canvas_editor/editor.js +2351 -153
- package/src/flow/canvas_editor/index.js +2 -1
- package/src/flow/canvas_editor/layout.js +119 -12
- package/src/flow/canvas_editor/strings.js +80 -0
- package/src/flow/kit/always_before.js +0 -0
- package/src/flow/kit/catalogue.js +97 -3
- package/src/flow/kit/chip_input.js +0 -0
- package/src/flow/kit/dom.js +0 -0
- package/src/flow/kit/findings.js +0 -0
- package/src/flow/kit/history.js +0 -0
- package/src/flow/kit/index.js +3 -3
- package/src/flow/kit/keys.js +7 -1
- package/src/flow/kit/logic_graph.js +97 -15
- package/src/flow/kit/references.js +0 -0
- package/src/flow/kit/settings_panel.js +0 -0
- package/src/flow/kit/settings_schema.js +2 -1
- package/src/flow/kit/step_picker.js +0 -0
- package/src/flow/kit/strings.js +3 -1
- package/src/flow/kit/value_picker.js +0 -0
- package/src/flow/kit/widgets.js +86 -109
- package/src/flow/lanes/editor.js +136 -14
- package/src/flow/lanes/index.js +3 -1
- package/src/flow/lanes/layout.js +0 -0
- package/src/flow/lanes/pipeline.js +44 -2
- package/src/flow/lanes/preview.js +0 -0
- package/src/flow/lanes/strings.js +1 -0
- package/src/flow/outline/editor.js +0 -0
- package/src/flow/outline/index.js +0 -0
- package/src/flow/outline/mapping.js +0 -0
- package/src/flow/outline/menu.js +0 -0
- package/src/flow/outline/operations.js +0 -0
- package/src/flow/outline/recognise.js +0 -0
- package/src/flow/outline/strings.js +0 -0
- package/src/help/help_modal.js +168 -42
- package/src/help/help_registry.js +12 -1
- package/src/host/external_links.js +0 -0
- package/src/host/file_picker.js +117 -0
- package/src/host/host.js +10 -2
- package/src/host/photino_host.js +9 -0
- package/src/host/photino_host.md +21 -3
- package/src/host/pywebview_host.js +10 -0
- package/src/tiles/chart_tiles.js +53 -245
- package/src/tiles/config_schema.js +8 -0
- package/src/tiles/figure_tile.js +322 -0
- package/src/tiles/layout_persistence.js +3 -3
- package/src/tiles/list_tiles.js +7 -7
- package/src/tiles/scope_picker.js +0 -0
- package/src/tiles/tile_base.js +1 -1
- package/src/tiles/tile_config_form.js +442 -0
- package/src/tiles/tile_grid.js +2 -0
- package/src/tiles/tile_registry.js +0 -0
- package/src/tiling/bottom_panel_tabs.js +67 -99
- package/src/tiling/command_palette.js +64 -19
- package/src/tiling/content_registry.js +4 -1
- package/src/tiling/desktops.js +11 -1
- package/src/tiling/entity_sources.js +0 -0
- package/src/tiling/keymap.js +403 -128
- package/src/tiling/kind_taxonomy.js +78 -0
- package/src/tiling/landing_table.js +0 -0
- package/src/tiling/loading_overlay.js +0 -0
- package/src/tiling/nav_panel.js +59 -10
- package/src/tiling/page_factory.js +5 -5
- package/src/tiling/panel_availability.js +49 -0
- package/src/tiling/panel_keys.js +0 -0
- package/src/tiling/shell.js +114 -12
- package/src/tiling/tab_strip.js +0 -0
- package/src/tiling/tile_breadcrumb.js +0 -0
- package/src/tiling/tile_renderer.js +105 -9
- package/src/tiling/tile_tab_menu.js +0 -0
- package/src/tiling/tile_tree.js +0 -0
- package/src/tiling/wm.js +562 -30
- package/src/tiling/zoom.js +0 -0
- package/src/ui/base/component_base.js +0 -0
- package/src/ui/base/controller_base.js +0 -0
- package/src/ui/base/page_base.js +0 -0
- package/src/ui/components/about_dialog.js +0 -0
- package/src/ui/components/action_dropdown.js +30 -2
- package/src/ui/components/alert_log.js +0 -0
- package/src/ui/components/alerts_pane.js +0 -0
- package/src/ui/components/attribute_list_editor.js +0 -0
- package/src/ui/components/autocomplete_field.js +0 -0
- package/src/ui/components/cell_editor_host.js +211 -0
- package/src/ui/components/cell_grid.js +648 -0
- package/src/ui/components/cell_selection.js +220 -0
- package/src/ui/components/choice_controls.js +351 -0
- package/src/ui/components/color_field.js +211 -0
- package/src/ui/components/color_list_field.js +210 -0
- package/src/ui/components/column_fit.js +0 -0
- package/src/ui/components/column_groups.js +314 -0
- package/src/ui/components/column_order.js +142 -0
- package/src/ui/components/computing_status_window.js +98 -25
- package/src/ui/components/confirm_dialog.js +81 -38
- package/src/ui/components/context_menu.js +2 -2
- package/src/ui/components/conversation_view.js +440 -0
- package/src/ui/components/crosstab.js +289 -0
- package/src/ui/components/data_table.js +601 -62
- package/src/ui/components/detail_header.js +22 -11
- package/src/ui/components/document_outline.js +397 -0
- package/src/ui/components/drag_reorder.js +0 -0
- package/src/ui/components/fact_table.js +0 -0
- package/src/ui/components/fill_handle.js +322 -0
- package/src/ui/components/gallery_picker.js +19 -8
- package/src/ui/components/header_actions.js +55 -0
- package/src/ui/components/inline_renamer.js +0 -0
- package/src/ui/components/job_progress.js +51 -0
- package/src/ui/components/managed_window.js +27 -4
- package/src/ui/components/menu_bar.js +745 -0
- package/src/ui/components/modal.js +449 -75
- package/src/ui/components/notification_history.js +3 -3
- package/src/ui/components/progress.js +483 -0
- package/src/ui/components/record_list.js +0 -0
- package/src/ui/components/row_activation.js +0 -0
- package/src/ui/components/section_stack.js +298 -0
- package/src/ui/components/series.js +173 -0
- package/src/ui/components/settings_view.js +634 -0
- package/src/ui/components/slide_out_panel.js +43 -6
- package/src/ui/components/slider_field.js +275 -18
- package/src/ui/components/sortable_list.js +0 -0
- package/src/ui/components/staged_edits.js +343 -0
- package/src/ui/components/staged_history.js +172 -0
- package/src/ui/components/table_filter_chips.js +0 -0
- package/src/ui/components/table_state_store.js +0 -0
- package/src/ui/components/table_structure.js +182 -0
- package/src/ui/components/table_values.js +0 -0
- package/src/ui/components/tabs.js +497 -0
- package/src/ui/components/tag_input.js +215 -0
- package/src/ui/components/toast.js +25 -8
- package/src/ui/components/toggle_field.js +175 -0
- package/src/ui/components/tree_view.js +55 -34
- package/src/ui/components/window_taskbar.js +0 -0
- package/src/ui/components/wizard.js +563 -0
- package/src/ui/controllers/window_chrome_controller.js +2 -2
- package/src/ui/notification_center.js +375 -119
- package/src/ui/utils/auto_scrollbars.js +0 -0
- package/src/ui/utils/markdown.js +709 -0
- package/src/ui/utils/native_context_menu.js +0 -0
- package/src/ui/utils/overlay_scrollbar.js +16 -12
- package/src/ui/utils/raf_resize_observer.js +0 -0
- package/src/ui/utils/scroll_lock.js +0 -0
- package/src/ui/utils/strip_scroll.js +55 -0
- package/src/ui/utils/tooltip_service.js +0 -8
- package/dist/chunk-6JSOVNID.js.map +0 -7
- package/dist/chunk-B5Q53LAJ.js.map +0 -7
- package/dist/chunk-FBGP5KN5.js.map +0 -7
- package/dist/chunk-FL5KFNQH.js.map +0 -7
- package/dist/chunk-NLTOY57M.js.map +0 -7
- package/dist/chunk-NNFTETK3.js +0 -633
- package/dist/chunk-NNFTETK3.js.map +0 -7
- package/dist/chunk-O5OHMWBB.js +0 -284
- package/dist/chunk-O5OHMWBB.js.map +0 -7
- package/dist/chunk-RQM5Z2MU.js.map +0 -7
- package/dist/chunk-SN4LSOAR.js.map +0 -7
- package/dist/chunk-W6FBJLPW.js +0 -367
- package/dist/chunk-W6FBJLPW.js.map +0 -7
- package/src/ui/controllers/panel_state_machine.js +0 -634
|
@@ -13,11 +13,14 @@
|
|
|
13
13
|
* - Multi-subplot grid layouts (1x1, 1x2, 2x1, 2x2, 1x3, wide-top, wide-bottom)
|
|
14
14
|
* - Series visibility toggle panel
|
|
15
15
|
* - Zoom/pan toolbar with zoom in/out, autoscale, reset
|
|
16
|
-
* -
|
|
16
|
+
* - CSV and PNG export through the host's dialogs (else a browser download)
|
|
17
17
|
* - NotificationCenter integration for user feedback
|
|
18
18
|
* - Live streaming updates while the data source is running
|
|
19
|
-
* -
|
|
19
|
+
* - Derived series from injected transforms (`seriesTransforms`)
|
|
20
20
|
* - ETL actuals overlay support
|
|
21
|
+
*
|
|
22
|
+
* The figures are drawn by renderPlotGrid (plot_grid.js); this window adds the
|
|
23
|
+
* tabs, the toolbar, the series toggles and the data table around them.
|
|
21
24
|
*/
|
|
22
25
|
|
|
23
26
|
import { ManagedWindow } from '../ui/components/managed_window.js';
|
|
@@ -28,17 +31,8 @@ import {
|
|
|
28
31
|
purge,
|
|
29
32
|
getSeriesColor,
|
|
30
33
|
} from './plotly_wrapper.js';
|
|
31
|
-
import {
|
|
32
|
-
|
|
33
|
-
buildYAxisLayout,
|
|
34
|
-
buildLegendLayout,
|
|
35
|
-
buildOverlayShapes,
|
|
36
|
-
} from './chart_types.js';
|
|
37
|
-
import {
|
|
38
|
-
PLOT_LAYOUTS,
|
|
39
|
-
migrateData,
|
|
40
|
-
applyNanHandling,
|
|
41
|
-
} from './plot_config.js';
|
|
34
|
+
import { migrateData } from './plot_config.js';
|
|
35
|
+
import { renderPlotGrid, resolveSeriesData } from './plot_grid.js';
|
|
42
36
|
|
|
43
37
|
export class PlotPopoutWindow {
|
|
44
38
|
/**
|
|
@@ -52,14 +46,29 @@ export class PlotPopoutWindow {
|
|
|
52
46
|
* capability backs Export PNG/CSV; absent => Blob download fallback.
|
|
53
47
|
* @param {Function} [options.onClose] - Called when window closes
|
|
54
48
|
* @param {Object} [options.actuals] - ETL overlay data { seriesId: { years, values } }
|
|
55
|
-
* @param {Function} [options.configRenderer] - (container) =>
|
|
49
|
+
* @param {Function} [options.configRenderer] - `(container) => { dispose() } | void` —
|
|
50
|
+
* renders the Config tab. What it returns is disposed (`dispose()`) when the
|
|
51
|
+
* window closes, so an editor drawn there (renderPlotConfigEditor) goes with it.
|
|
52
|
+
* @param {Object} [options.seriesTransforms] - `{ [id]: transform }` (series_transforms.js);
|
|
53
|
+
* absent => no derived series are drawn
|
|
54
|
+
* @param {string} [options.refreshEvent='data:changed'] - Bus event after which the
|
|
55
|
+
* window re-reads `getResults()` and redraws.
|
|
56
|
+
* @param {{delta?: string|null, complete?: string|null}} [options.streamEvents] - Bus
|
|
57
|
+
* events a live data source emits while it runs (`delta`) and when it ends
|
|
58
|
+
* (`complete`); the window redraws on each while live update is on.
|
|
59
|
+
* There are no default names: without `streamEvents` (or with a name
|
|
60
|
+
* left out or given as null) nothing is listened for, and the window
|
|
61
|
+
* updates through `refreshEvent` and explicit calls only.
|
|
62
|
+
* @param {Function} [options.resolveSeries] - `(series, results, subplot) => { y, x? } | null`
|
|
63
|
+
* (plot_grid.js); used by the chart and the data table. Default resolveSeriesData.
|
|
56
64
|
*/
|
|
57
65
|
constructor({ id, title, plotConfig, getResults, services, host, onClose, actuals,
|
|
58
|
-
configRenderer,
|
|
66
|
+
configRenderer, seriesTransforms, resolveSeries = resolveSeriesData,
|
|
59
67
|
// The window used to listen for 'simulation:run:completed' and tell
|
|
60
68
|
// the user to "run simulation first". A chart window does not know
|
|
61
69
|
// that its data comes from a simulation; the app that opens it does.
|
|
62
70
|
refreshEvent = 'data:changed',
|
|
71
|
+
streamEvents = {},
|
|
63
72
|
emptyMessage = 'No data yet.',
|
|
64
73
|
tableEmptyMessage = 'No data yet.' }) {
|
|
65
74
|
this._id = id;
|
|
@@ -71,7 +80,11 @@ export class PlotPopoutWindow {
|
|
|
71
80
|
this._onClose = onClose ?? (() => {});
|
|
72
81
|
this._actuals = actuals ?? null;
|
|
73
82
|
this._configRenderer = configRenderer ?? null;
|
|
83
|
+
this._seriesTransforms = seriesTransforms ?? null;
|
|
84
|
+
this._resolveSeries = resolveSeries;
|
|
74
85
|
this.refreshEvent = refreshEvent;
|
|
86
|
+
this.streamEvents = Object.freeze({ delta: streamEvents?.delta ?? null,
|
|
87
|
+
complete: streamEvents?.complete ?? null });
|
|
75
88
|
// TWO messages, not one. The chart says "No data — run simulation first."
|
|
76
89
|
// and the data table underneath says "Run simulation to see data". Folding
|
|
77
90
|
// them into a single option would have quietly rewritten one of them, which
|
|
@@ -82,12 +95,10 @@ export class PlotPopoutWindow {
|
|
|
82
95
|
|
|
83
96
|
this._window = null;
|
|
84
97
|
this._contentEl = null;
|
|
85
|
-
this.
|
|
98
|
+
this._grid = null;
|
|
86
99
|
this._gridEl = null;
|
|
87
100
|
this._activeTab = 'plot';
|
|
88
101
|
this._eventDisposers = [];
|
|
89
|
-
this._resizeObserver = null;
|
|
90
|
-
this._chartCreated = false;
|
|
91
102
|
this._lastResults = null;
|
|
92
103
|
|
|
93
104
|
// Data table
|
|
@@ -103,6 +114,7 @@ export class PlotPopoutWindow {
|
|
|
103
114
|
this._plotContent = null;
|
|
104
115
|
this._dataContent = null;
|
|
105
116
|
this._configContent = null;
|
|
117
|
+
this._configHandle = null;
|
|
106
118
|
this._toolbarActions = null;
|
|
107
119
|
this._zoomBtn = null;
|
|
108
120
|
this._panBtn = null;
|
|
@@ -118,6 +130,8 @@ export class PlotPopoutWindow {
|
|
|
118
130
|
this._outsideClickHandler = null;
|
|
119
131
|
|
|
120
132
|
this._renderPending = false;
|
|
133
|
+
/** updateConfig() ran since the last #render(); applied even without results. */
|
|
134
|
+
this._configChanged = false;
|
|
121
135
|
|
|
122
136
|
// Live-update toggle: when true, window re-renders on streaming events
|
|
123
137
|
this._liveUpdate = true;
|
|
@@ -135,7 +149,6 @@ export class PlotPopoutWindow {
|
|
|
135
149
|
await ensurePlotly();
|
|
136
150
|
this.#createWindow();
|
|
137
151
|
this.#subscribeEvents();
|
|
138
|
-
this.#setupResizeObserver();
|
|
139
152
|
await this.#render();
|
|
140
153
|
}
|
|
141
154
|
|
|
@@ -146,7 +159,7 @@ export class PlotPopoutWindow {
|
|
|
146
159
|
/** Update plot config and re-render. */
|
|
147
160
|
updateConfig(plotConfig) {
|
|
148
161
|
this._plotConfig = migrateData(plotConfig);
|
|
149
|
-
this
|
|
162
|
+
this._configChanged = true;
|
|
150
163
|
this.#scheduleRender();
|
|
151
164
|
}
|
|
152
165
|
|
|
@@ -279,7 +292,20 @@ export class PlotPopoutWindow {
|
|
|
279
292
|
this._gridEl.className = 'twm-plot-window-chart';
|
|
280
293
|
this._plotContent.appendChild(this._gridEl);
|
|
281
294
|
|
|
282
|
-
this
|
|
295
|
+
this._grid = renderPlotGrid(this._gridEl, this._plotConfig, null, {
|
|
296
|
+
resolveSeries: this._resolveSeries,
|
|
297
|
+
overlays: this._actuals,
|
|
298
|
+
hiddenSeries: this._hiddenSeries,
|
|
299
|
+
seriesTransforms: this._seriesTransforms,
|
|
300
|
+
onTransformError: ({ transformId, traceName, error }) => this._services?.logger?.warn?.(
|
|
301
|
+
`[PlotPopoutWindow] transform "${transformId}" failed on "${traceName}"`, error),
|
|
302
|
+
// Popout windows have space — always show the legend.
|
|
303
|
+
alwaysShowLegend: true,
|
|
304
|
+
layout: { autosize: true, hovermode: 'x unified' },
|
|
305
|
+
emptyMessage: this.emptyMessage,
|
|
306
|
+
onError: ({ index, error }) => this._services?.logger?.warn?.(
|
|
307
|
+
`[PlotPopoutWindow] subplot ${index + 1} failed to draw`, error),
|
|
308
|
+
});
|
|
283
309
|
|
|
284
310
|
// Data tab content
|
|
285
311
|
this._dataContent = document.createElement('div');
|
|
@@ -300,7 +326,7 @@ export class PlotPopoutWindow {
|
|
|
300
326
|
this._configContent.className = 'twm-plot-tab-content';
|
|
301
327
|
this._configContent.setAttribute('data-tab', 'config');
|
|
302
328
|
this._configContent.style.cssText = 'display:none; flex-direction:column; overflow:auto; padding:8px 12px;';
|
|
303
|
-
this._configRenderer(this._configContent);
|
|
329
|
+
this._configHandle = this._configRenderer(this._configContent) ?? null;
|
|
304
330
|
content.appendChild(this._configContent);
|
|
305
331
|
}
|
|
306
332
|
|
|
@@ -316,40 +342,6 @@ export class PlotPopoutWindow {
|
|
|
316
342
|
return container;
|
|
317
343
|
}
|
|
318
344
|
|
|
319
|
-
// ── Subplot grid ────────────────────────────────────────────────────
|
|
320
|
-
|
|
321
|
-
#buildSubplotGrid() {
|
|
322
|
-
if (!this._gridEl) return;
|
|
323
|
-
|
|
324
|
-
// Purge existing charts
|
|
325
|
-
for (const div of this._chartDivs) {
|
|
326
|
-
if (div && window.Plotly) {
|
|
327
|
-
try { purge(div); } catch (_) {}
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
this._gridEl.innerHTML = '';
|
|
331
|
-
this._chartDivs = [];
|
|
332
|
-
|
|
333
|
-
const tmpl = PLOT_LAYOUTS[this._plotConfig.layout] || PLOT_LAYOUTS['1x1'];
|
|
334
|
-
this._gridEl.style.display = 'grid';
|
|
335
|
-
this._gridEl.style.gridTemplateColumns = tmpl.grid.columns;
|
|
336
|
-
this._gridEl.style.gridTemplateRows = tmpl.grid.rows;
|
|
337
|
-
this._gridEl.style.gap = '4px';
|
|
338
|
-
this._gridEl.style.height = '100%';
|
|
339
|
-
this._gridEl.style.width = '100%';
|
|
340
|
-
|
|
341
|
-
for (let i = 0; i < tmpl.slots; i++) {
|
|
342
|
-
const div = document.createElement('div');
|
|
343
|
-
div.className = 'plot-tile-chart';
|
|
344
|
-
div.style.gridArea = tmpl.areas[i].gridArea;
|
|
345
|
-
div.style.minHeight = '0';
|
|
346
|
-
this._gridEl.appendChild(div);
|
|
347
|
-
this._chartDivs.push(div);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
this._chartCreated = false;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
345
|
// ── Toolbar helpers ─────────────────────────────────────────────────
|
|
354
346
|
|
|
355
347
|
#toolbarGroup() {
|
|
@@ -370,9 +362,13 @@ export class PlotPopoutWindow {
|
|
|
370
362
|
|
|
371
363
|
// ── Plotly toolbar actions ───────────────────────────────────────────
|
|
372
364
|
|
|
365
|
+
get #charts() { return this._grid?.charts ?? []; }
|
|
366
|
+
/** The subplots that show a figure — the grid knows, however it drew them. */
|
|
367
|
+
get #plottedCharts() { return this._grid?.plotted ?? []; }
|
|
368
|
+
|
|
373
369
|
#setDragMode(mode) {
|
|
374
|
-
if (!this.
|
|
375
|
-
for (const div of this
|
|
370
|
+
if (!this.#plottedCharts.length) return;
|
|
371
|
+
for (const div of this.#charts) {
|
|
376
372
|
if (div._fullLayout) {
|
|
377
373
|
window.Plotly.relayout(div, { dragmode: mode }).catch(() => {});
|
|
378
374
|
}
|
|
@@ -382,7 +378,7 @@ export class PlotPopoutWindow {
|
|
|
382
378
|
}
|
|
383
379
|
|
|
384
380
|
#zoom(factor) {
|
|
385
|
-
for (const div of this
|
|
381
|
+
for (const div of this.#charts) {
|
|
386
382
|
if (!div._fullLayout) continue;
|
|
387
383
|
const xa = div._fullLayout.xaxis;
|
|
388
384
|
const ya = div._fullLayout.yaxis;
|
|
@@ -403,7 +399,7 @@ export class PlotPopoutWindow {
|
|
|
403
399
|
}
|
|
404
400
|
|
|
405
401
|
#autoscale() {
|
|
406
|
-
for (const div of this
|
|
402
|
+
for (const div of this.#charts) {
|
|
407
403
|
if (!div._fullLayout) continue;
|
|
408
404
|
window.Plotly.relayout(div, {
|
|
409
405
|
'xaxis.autorange': true,
|
|
@@ -412,20 +408,6 @@ export class PlotPopoutWindow {
|
|
|
412
408
|
}
|
|
413
409
|
}
|
|
414
410
|
|
|
415
|
-
#setupResizeObserver() {
|
|
416
|
-
if (!this._plotContent) return;
|
|
417
|
-
this._resizeObserver = createRafResizeObserver(() => {
|
|
418
|
-
if (this._activeTab === 'plot' && this._chartCreated && window.Plotly) {
|
|
419
|
-
for (const div of this._chartDivs) {
|
|
420
|
-
if (div._fullLayout) {
|
|
421
|
-
Plotly.Plots.resize(div).catch(() => {});
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
});
|
|
426
|
-
this._resizeObserver.observe(this._plotContent);
|
|
427
|
-
}
|
|
428
|
-
|
|
429
411
|
#switchTab(tabName) {
|
|
430
412
|
this._activeTab = tabName;
|
|
431
413
|
|
|
@@ -443,13 +425,7 @@ export class PlotPopoutWindow {
|
|
|
443
425
|
this._configContent.style.display = tabName === 'config' ? 'flex' : 'none';
|
|
444
426
|
}
|
|
445
427
|
|
|
446
|
-
if (tabName === 'plot'
|
|
447
|
-
for (const div of this._chartDivs) {
|
|
448
|
-
if (div._fullLayout) {
|
|
449
|
-
Plotly.Plots.resize(div).catch(() => {});
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
}
|
|
428
|
+
if (tabName === 'plot') this._grid?.resize();
|
|
453
429
|
|
|
454
430
|
if (tabName === 'data') {
|
|
455
431
|
this.#renderDataTable();
|
|
@@ -649,17 +625,16 @@ export class PlotPopoutWindow {
|
|
|
649
625
|
const { eventBus } = this._services;
|
|
650
626
|
if (!eventBus) return;
|
|
651
627
|
|
|
652
|
-
this.
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
// Final analytics arrive asynchronously after streaming:complete
|
|
628
|
+
const { delta, complete } = this.streamEvents;
|
|
629
|
+
for (const name of new Set([delta, complete])) {
|
|
630
|
+
if (!name) continue;
|
|
631
|
+
this._eventDisposers.push(
|
|
632
|
+
eventBus.on(name, () => {
|
|
633
|
+
if (this._liveUpdate) this.#scheduleRender();
|
|
634
|
+
})
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
// Final analytics arrive asynchronously after the stream's complete event
|
|
663
638
|
// (backend fetch + transform). Without this, the popout window may
|
|
664
639
|
// render stale data from a previous run when the dashboard tab
|
|
665
640
|
// didn't receive streaming deltas (e.g. user was on another tab).
|
|
@@ -699,7 +674,21 @@ export class PlotPopoutWindow {
|
|
|
699
674
|
if (!this._window?.isVisible) return;
|
|
700
675
|
|
|
701
676
|
const results = this._getResults?.();
|
|
702
|
-
|
|
677
|
+
const configChanged = this._configChanged;
|
|
678
|
+
this._configChanged = false;
|
|
679
|
+
if (!results) {
|
|
680
|
+
// No data yet: the figures already drawn stay, unless the layout or
|
|
681
|
+
// the subplots changed, in which case the grid is rebuilt for them.
|
|
682
|
+
if (configChanged) {
|
|
683
|
+
await this._grid.update({
|
|
684
|
+
plotConfig: this._plotConfig,
|
|
685
|
+
results: null,
|
|
686
|
+
overlays: this._actuals,
|
|
687
|
+
hiddenSeries: this._hiddenSeries,
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
703
692
|
|
|
704
693
|
this._lastResults = results;
|
|
705
694
|
|
|
@@ -707,7 +696,13 @@ export class PlotPopoutWindow {
|
|
|
707
696
|
this.#collectSeriesInfo();
|
|
708
697
|
|
|
709
698
|
// Render all subplot charts
|
|
710
|
-
this
|
|
699
|
+
await this._grid.update({
|
|
700
|
+
plotConfig: this._plotConfig,
|
|
701
|
+
results,
|
|
702
|
+
overlays: this._actuals,
|
|
703
|
+
hiddenSeries: this._hiddenSeries,
|
|
704
|
+
});
|
|
705
|
+
if (!this._window) return;
|
|
711
706
|
|
|
712
707
|
// Update table data
|
|
713
708
|
this.#updateTableData(results);
|
|
@@ -737,7 +732,7 @@ export class PlotPopoutWindow {
|
|
|
737
732
|
this._allSeriesLabels.push(label);
|
|
738
733
|
|
|
739
734
|
// Include overlay in toggle list
|
|
740
|
-
if (s.overlay
|
|
735
|
+
if (s.overlay) {
|
|
741
736
|
const ovLabel = s.overlay.label || `${label} (actual)`;
|
|
742
737
|
this._allSeriesInfo.push({
|
|
743
738
|
label: ovLabel,
|
|
@@ -778,204 +773,6 @@ export class PlotPopoutWindow {
|
|
|
778
773
|
}
|
|
779
774
|
}
|
|
780
775
|
|
|
781
|
-
#renderAllCharts(results) {
|
|
782
|
-
if (!window.Plotly) return;
|
|
783
|
-
|
|
784
|
-
const subplots = this._plotConfig.subplots ?? [];
|
|
785
|
-
for (let i = 0; i < this._chartDivs.length; i++) {
|
|
786
|
-
const chartDiv = this._chartDivs[i];
|
|
787
|
-
const sp = subplots[i];
|
|
788
|
-
if (!chartDiv) continue;
|
|
789
|
-
if (!sp) {
|
|
790
|
-
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>';
|
|
791
|
-
continue;
|
|
792
|
-
}
|
|
793
|
-
this.#renderSubplotChart(chartDiv, sp, results);
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
async #renderSubplotChart(chartDiv, sp, results) {
|
|
798
|
-
const { time, series } = results;
|
|
799
|
-
const traces = [];
|
|
800
|
-
const layoutAxes = {};
|
|
801
|
-
const yRefByAxisId = {};
|
|
802
|
-
let axisCount = 0;
|
|
803
|
-
|
|
804
|
-
for (const axis of sp.yAxes ?? []) {
|
|
805
|
-
axisCount++;
|
|
806
|
-
const yAxisKey = axisCount === 1 ? 'yaxis' : `yaxis${axisCount}`;
|
|
807
|
-
const yRef = axisCount === 1 ? 'y' : `y${axisCount}`;
|
|
808
|
-
if (axis.id) yRefByAxisId[axis.id] = yRef;
|
|
809
|
-
|
|
810
|
-
layoutAxes[yAxisKey] = buildYAxisLayout({
|
|
811
|
-
axisId: yAxisKey,
|
|
812
|
-
title: axis.label || '',
|
|
813
|
-
position: axis.position || 'left',
|
|
814
|
-
scaleType: axis.scale || 'linear',
|
|
815
|
-
min: axis.min,
|
|
816
|
-
max: axis.max,
|
|
817
|
-
showGrid: (sp.yAxes ?? []).length === 1,
|
|
818
|
-
axisPosition: axisCount > 1 ? (axis.position === 'right' ? 1 : 0) : null,
|
|
819
|
-
});
|
|
820
|
-
|
|
821
|
-
for (const s of axis.series ?? []) {
|
|
822
|
-
if (!s.variable) continue;
|
|
823
|
-
|
|
824
|
-
const label = s.label || s.variable;
|
|
825
|
-
|
|
826
|
-
// Skip hidden series
|
|
827
|
-
if (this._hiddenSeries.has(label)) continue;
|
|
828
|
-
|
|
829
|
-
// Resolve variable: try result key (paper multi-scenario), exact, then bare (without namespace prefix)
|
|
830
|
-
let rawY = series?.[s._resultKey ?? s.variable];
|
|
831
|
-
if (!rawY && s._resultKey) {
|
|
832
|
-
rawY = series?.[s.variable];
|
|
833
|
-
}
|
|
834
|
-
if (!rawY) {
|
|
835
|
-
const dotIdx = s.variable.indexOf('.');
|
|
836
|
-
if (dotIdx >= 0) rawY = series?.[s.variable.slice(dotIdx + 1)];
|
|
837
|
-
}
|
|
838
|
-
if (!rawY) continue;
|
|
839
|
-
|
|
840
|
-
const y = Array.isArray(rawY) ? applyNanHandling(rawY, sp.nanHandling ?? 'gap', time) : rawY;
|
|
841
|
-
const seriesTime = s._resultTime ?? time;
|
|
842
|
-
traces.push(buildTrace({
|
|
843
|
-
chartType: sp.chartType || 'line',
|
|
844
|
-
x: seriesTime,
|
|
845
|
-
y,
|
|
846
|
-
name: label,
|
|
847
|
-
color: s.color,
|
|
848
|
-
lineWidth: s.lineWidth ?? 2,
|
|
849
|
-
lineStyle: s.lineStyle ?? 'solid',
|
|
850
|
-
interpolation: sp.interpolation ?? 'linear',
|
|
851
|
-
yAxisId: yRef,
|
|
852
|
-
showMarkers: sp.showDataPoints ?? false,
|
|
853
|
-
stackGroup: s.stackGroup ?? null,
|
|
854
|
-
}));
|
|
855
|
-
|
|
856
|
-
// Per-series overlay trace (ETL actuals)
|
|
857
|
-
if (s.overlay?.etlKey && this._actuals?.[s.id]) {
|
|
858
|
-
const ovLabel = s.overlay.label || `${label} (actual)`;
|
|
859
|
-
if (!this._hiddenSeries.has(ovLabel)) {
|
|
860
|
-
const act = this._actuals[s.id];
|
|
861
|
-
if (act?.years?.length) {
|
|
862
|
-
const ov = s.overlay;
|
|
863
|
-
const dashMap = { dashed: 'dash', dotted: 'dot', solid: 'solid' };
|
|
864
|
-
traces.push({
|
|
865
|
-
type: 'scatter',
|
|
866
|
-
mode: ov.mode || 'markers+lines',
|
|
867
|
-
x: act.years,
|
|
868
|
-
y: act.values,
|
|
869
|
-
name: ov.label || `${label} (actual)`,
|
|
870
|
-
yaxis: yRef === 'y' ? undefined : yRef,
|
|
871
|
-
line: {
|
|
872
|
-
color: ov.color || s.color,
|
|
873
|
-
width: ov.lineWidth ?? 2,
|
|
874
|
-
dash: dashMap[ov.lineStyle] || 'dash',
|
|
875
|
-
},
|
|
876
|
-
marker: {
|
|
877
|
-
color: ov.color || s.color,
|
|
878
|
-
size: 4,
|
|
879
|
-
symbol: 'circle',
|
|
880
|
-
},
|
|
881
|
-
showlegend: true,
|
|
882
|
-
});
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
// HP filter traces
|
|
890
|
-
if ((sp.showHpTrend || sp.showHpCycle) && window.pywebview?.api?.hp_filter) {
|
|
891
|
-
const hpLambda = sp.hpLambda ?? 1600;
|
|
892
|
-
for (const trace of [...traces]) {
|
|
893
|
-
if (trace.type && trace.type !== 'scatter' && trace.type !== 'scattergl') continue;
|
|
894
|
-
try {
|
|
895
|
-
const hpResult = await window.pywebview.api.hp_filter({
|
|
896
|
-
data: trace.y,
|
|
897
|
-
lambda: hpLambda,
|
|
898
|
-
});
|
|
899
|
-
if (!hpResult?.ok) continue;
|
|
900
|
-
const { trend, cycle } = hpResult;
|
|
901
|
-
const origColor = trace.line?.color || trace.marker?.color || getSeriesColor(0);
|
|
902
|
-
if (sp.showHpTrend && trend) {
|
|
903
|
-
traces.push({
|
|
904
|
-
x: trace.x, y: trend, type: 'scatter', mode: 'lines',
|
|
905
|
-
name: `${trace.name} (HP Trend)`,
|
|
906
|
-
yaxis: trace.yaxis || 'y',
|
|
907
|
-
line: { color: origColor, width: 2, dash: 'dash' },
|
|
908
|
-
showlegend: true,
|
|
909
|
-
});
|
|
910
|
-
}
|
|
911
|
-
if (sp.showHpCycle && cycle) {
|
|
912
|
-
traces.push({
|
|
913
|
-
x: trace.x, y: cycle, type: 'scatter', mode: 'lines',
|
|
914
|
-
name: `${trace.name} (HP Cycle)`,
|
|
915
|
-
yaxis: trace.yaxis || 'y',
|
|
916
|
-
line: { color: origColor, width: 1, dash: 'dot' },
|
|
917
|
-
showlegend: true,
|
|
918
|
-
});
|
|
919
|
-
}
|
|
920
|
-
} catch (_) { /* ignore HP filter failures */ }
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
if (traces.length === 0 && this._hiddenSeries.size === 0) {
|
|
925
|
-
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>`;
|
|
926
|
-
return;
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
// Popout windows have space — always show legend
|
|
930
|
-
const legendPos = sp.legendPosition === 'hidden' ? 'top' : (sp.legendPosition ?? 'top');
|
|
931
|
-
const legend = buildLegendLayout(legendPos);
|
|
932
|
-
|
|
933
|
-
// Clip the x-axis to the data's time range so overlays don't run past t0–t1
|
|
934
|
-
const xRange = time?.length >= 2 ? [time[0], time[time.length - 1]] : undefined;
|
|
935
|
-
|
|
936
|
-
const { shapes, annotations } = buildOverlayShapes({
|
|
937
|
-
referenceLines: sp.referenceLines,
|
|
938
|
-
shadedBands: sp.shadedBands,
|
|
939
|
-
yRefByAxisId,
|
|
940
|
-
});
|
|
941
|
-
|
|
942
|
-
const layout = {
|
|
943
|
-
...layoutAxes,
|
|
944
|
-
...(shapes.length ? { shapes } : {}),
|
|
945
|
-
...(annotations.length ? { annotations } : {}),
|
|
946
|
-
xaxis: {
|
|
947
|
-
gridcolor: 'rgba(255,255,255,0.05)',
|
|
948
|
-
linecolor: 'rgba(255,255,255,0.08)',
|
|
949
|
-
tickcolor: 'rgba(255,255,255,0.3)',
|
|
950
|
-
zerolinecolor: 'rgba(255,255,255,0.06)',
|
|
951
|
-
automargin: true,
|
|
952
|
-
...(xRange ? { range: xRange } : {}),
|
|
953
|
-
},
|
|
954
|
-
...legend,
|
|
955
|
-
paper_bgcolor: 'transparent',
|
|
956
|
-
plot_bgcolor: 'transparent',
|
|
957
|
-
font: { family: 'Inter, -apple-system, BlinkMacSystemFont, sans-serif', color: '#cccccc', size: 11 },
|
|
958
|
-
margin: { t: 10, r: 20, b: 40, l: 50 },
|
|
959
|
-
hoverlabel: {
|
|
960
|
-
bgcolor: '#1e2228',
|
|
961
|
-
bordercolor: '#444444',
|
|
962
|
-
font: { family: 'Inter, -apple-system, BlinkMacSystemFont, sans-serif', color: '#cccccc' },
|
|
963
|
-
},
|
|
964
|
-
autosize: true,
|
|
965
|
-
hovermode: 'x unified',
|
|
966
|
-
};
|
|
967
|
-
|
|
968
|
-
// Guard: skip if div is detached or zero-size
|
|
969
|
-
if (!chartDiv.isConnected || chartDiv.offsetWidth === 0) return;
|
|
970
|
-
|
|
971
|
-
try {
|
|
972
|
-
await Plotly.react(chartDiv, traces, layout, { responsive: true, displayModeBar: false });
|
|
973
|
-
this._chartCreated = true;
|
|
974
|
-
} catch (_) {
|
|
975
|
-
// Plotly can throw if div is removed mid-render
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
|
|
979
776
|
// ── Data table ──────────────────────────────────────────────────────
|
|
980
777
|
|
|
981
778
|
#updateTableData(results) {
|
|
@@ -987,7 +784,7 @@ export class PlotPopoutWindow {
|
|
|
987
784
|
|
|
988
785
|
const headers = ['Time'];
|
|
989
786
|
const seriesColumns = [];
|
|
990
|
-
const { time
|
|
787
|
+
const { time } = results;
|
|
991
788
|
const subplots = this._plotConfig.subplots ?? [];
|
|
992
789
|
|
|
993
790
|
for (const sp of subplots) {
|
|
@@ -995,14 +792,7 @@ export class PlotPopoutWindow {
|
|
|
995
792
|
for (const s of axis.series ?? []) {
|
|
996
793
|
if (!s.variable) continue;
|
|
997
794
|
const label = s.label || s.variable;
|
|
998
|
-
|
|
999
|
-
if (!rawY && s._resultKey) {
|
|
1000
|
-
rawY = series?.[s.variable];
|
|
1001
|
-
}
|
|
1002
|
-
if (!rawY) {
|
|
1003
|
-
const dotIdx = s.variable.indexOf('.');
|
|
1004
|
-
if (dotIdx >= 0) rawY = series?.[s.variable.slice(dotIdx + 1)];
|
|
1005
|
-
}
|
|
795
|
+
const rawY = this._resolveSeries(s, results, sp)?.y;
|
|
1006
796
|
if (!rawY) continue;
|
|
1007
797
|
headers.push(label);
|
|
1008
798
|
seriesColumns.push(rawY);
|
|
@@ -1108,10 +898,9 @@ export class PlotPopoutWindow {
|
|
|
1108
898
|
}
|
|
1109
899
|
|
|
1110
900
|
async #downloadPNG() {
|
|
1111
|
-
//
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
if (!targetDiv || !window.Plotly || !this._chartCreated) {
|
|
901
|
+
// The first subplot that shows a figure.
|
|
902
|
+
const targetDiv = this.#plottedCharts[0];
|
|
903
|
+
if (!targetDiv || !window.Plotly) {
|
|
1115
904
|
this.#notify('Download', 'Plot not ready', 'warn');
|
|
1116
905
|
return;
|
|
1117
906
|
}
|
|
@@ -1170,26 +959,24 @@ export class PlotPopoutWindow {
|
|
|
1170
959
|
|
|
1171
960
|
this.#hideSeriesPanel();
|
|
1172
961
|
|
|
1173
|
-
if (this._resizeObserver) {
|
|
1174
|
-
this._resizeObserver.disconnect();
|
|
1175
|
-
this._resizeObserver = null;
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
962
|
for (const disposer of this._eventDisposers) {
|
|
1179
963
|
try { disposer?.dispose?.(); } catch (_) {}
|
|
1180
964
|
}
|
|
1181
965
|
this._eventDisposers = [];
|
|
1182
966
|
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
967
|
+
// What configRenderer returned (an editor with dropdowns in document.body,
|
|
968
|
+
// say) goes with the window.
|
|
969
|
+
try { this._configHandle?.dispose?.(); } catch (error) {
|
|
970
|
+
this._services?.logger?.warn?.('[PlotPopoutWindow] the config panel failed to dispose', error);
|
|
1187
971
|
}
|
|
972
|
+
this._configHandle = null;
|
|
973
|
+
|
|
974
|
+
this._grid?.dispose();
|
|
975
|
+
this._grid = null;
|
|
1188
976
|
|
|
1189
977
|
this._window = null;
|
|
1190
978
|
this._contentEl = null;
|
|
1191
979
|
this._gridEl = null;
|
|
1192
|
-
this._chartDivs = [];
|
|
1193
980
|
this._plotTab = null;
|
|
1194
981
|
this._dataTab = null;
|
|
1195
982
|
this._plotContent = null;
|
|
@@ -1204,7 +991,6 @@ export class PlotPopoutWindow {
|
|
|
1204
991
|
this._hiddenSeries = null;
|
|
1205
992
|
this._allSeriesInfo = null;
|
|
1206
993
|
this._allSeriesLabels = null;
|
|
1207
|
-
this._chartCreated = false;
|
|
1208
994
|
this._lastResults = null;
|
|
1209
995
|
}
|
|
1210
996
|
}
|
|
@@ -88,7 +88,7 @@ if (typeof HTMLCanvasElement !== 'undefined') {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
/**
|
|
91
|
-
* Dark theme layout defaults matching
|
|
91
|
+
* Dark theme layout defaults matching the shell's dark UI
|
|
92
92
|
*/
|
|
93
93
|
export const DARK_THEME_LAYOUT = {
|
|
94
94
|
paper_bgcolor: 'transparent',
|
|
@@ -146,6 +146,9 @@ export const DARK_THEME_LAYOUT = {
|
|
|
146
146
|
}
|
|
147
147
|
};
|
|
148
148
|
|
|
149
|
+
/** The PNG export's file name (without extension) when the embedder names none. */
|
|
150
|
+
export const DEFAULT_CHART_EXPORT_FILENAME = 'chart';
|
|
151
|
+
|
|
149
152
|
/**
|
|
150
153
|
* Default Plotly config options
|
|
151
154
|
*/
|
|
@@ -156,11 +159,28 @@ export const DEFAULT_CONFIG = {
|
|
|
156
159
|
scrollZoom: true, // Allow zoom with scroll wheel instead
|
|
157
160
|
toImageButtonOptions: {
|
|
158
161
|
format: 'png',
|
|
159
|
-
filename:
|
|
162
|
+
filename: DEFAULT_CHART_EXPORT_FILENAME,
|
|
160
163
|
scale: 2
|
|
161
164
|
}
|
|
162
165
|
};
|
|
163
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Name the file a chart's PNG export saves as (Plotly adds `.png`). Applies to
|
|
169
|
+
* every chart created afterwards through DEFAULT_CONFIG; a chart's own
|
|
170
|
+
* `config.toImageButtonOptions` still wins.
|
|
171
|
+
*
|
|
172
|
+
* @param {string} [filename=DEFAULT_CHART_EXPORT_FILENAME]
|
|
173
|
+
*/
|
|
174
|
+
export function setChartExportFilename(filename = DEFAULT_CHART_EXPORT_FILENAME) {
|
|
175
|
+
DEFAULT_CONFIG.toImageButtonOptions.filename =
|
|
176
|
+
(typeof filename === 'string' && filename) ? filename : DEFAULT_CHART_EXPORT_FILENAME;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** @returns {string} The PNG export's file name (without extension). */
|
|
180
|
+
export function getChartExportFilename() {
|
|
181
|
+
return DEFAULT_CONFIG.toImageButtonOptions.filename;
|
|
182
|
+
}
|
|
183
|
+
|
|
164
184
|
/**
|
|
165
185
|
* Load Plotly.js from CDN.
|
|
166
186
|
* Uses singleton pattern to avoid multiple loads.
|