flexdesk 0.16.0 → 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 +2261 -42
- package/THIRD_PARTY_LICENSES.md +0 -0
- package/css/base.css +1834 -75
- package/css/flexdesk.css +3373 -238
- 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-UWO43BZU.js +111 -0
- 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 +3373 -238
- 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 +1131 -253
- package/dist/tiles.js.map +4 -4
- package/dist/tokens.css +0 -0
- package/dist/widgets.js +3254 -2112
- package/dist/widgets.js.map +4 -4
- package/dist/wm.js +1227 -345
- 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 +422 -0
- package/src/tiles/scope_picker.js +96 -0
- package/src/tiles/tile_base.js +12 -1
- package/src/tiles/tile_config_form.js +442 -0
- package/src/tiles/tile_grid.js +38 -1
- 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 +26 -4
- 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-CWPMLX67.js +0 -475
- package/dist/chunk-CWPMLX67.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/src/ui/controllers/panel_state_machine.js +0 -634
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* figure_tile.js — a tile that draws ONE Plotly figure, and owns the lifecycle
|
|
3
|
+
* of drawing it.
|
|
4
|
+
*
|
|
5
|
+
* A subclass says WHAT to draw and nothing else. It overrides one method:
|
|
6
|
+
*
|
|
7
|
+
* class SpreadTile extends FigureTile {
|
|
8
|
+
* static TYPE = 'myapp.spread';
|
|
9
|
+
* static TITLE = 'Spread';
|
|
10
|
+
* figure(data) {
|
|
11
|
+
* if (!data?.values?.length) return null; // the empty state
|
|
12
|
+
* return {
|
|
13
|
+
* traces: [buildHistogramTrace({ x: data.values })],
|
|
14
|
+
* layout: { margin: { l: 48, r: 16, t: 8, b: 36 } },
|
|
15
|
+
* };
|
|
16
|
+
* }
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* `figure(data)` answers `{traces, layout?, config?, frames?}` or `null`, and is
|
|
20
|
+
* synchronous: it computes, it does not wait. Everything else is the base's:
|
|
21
|
+
*
|
|
22
|
+
* - it awaits `ensurePlotly()`, so a page that loads Plotly lazily through
|
|
23
|
+
* `setPlotlySource` draws on first view instead of skipping the draw;
|
|
24
|
+
* - it draws through `createChart` (the theme defaults, `scrollZoom` off — a
|
|
25
|
+
* tile sits on a scrolling board — and the mode bar hidden; `config` from
|
|
26
|
+
* the figure goes over those), then adds `frames` when there are any;
|
|
27
|
+
* - a draw that finishes after a newer one, or after the tile went empty,
|
|
28
|
+
* loading, errored or disposed, is discarded and its figure purged;
|
|
29
|
+
* - `null` is the empty state (`static EMPTY_MESSAGE`). A subclass that wants
|
|
30
|
+
* to say WHY calls `this.showEmpty(message)` itself and returns `null`;
|
|
31
|
+
* - a `figure()` that throws, or a draw that fails (Plotly absent included),
|
|
32
|
+
* is the error state plus `tile:render-failed {tileId, message}` on the
|
|
33
|
+
* event bus — never an unhandled rejection;
|
|
34
|
+
* - one relayout when a resize settles (`onResize`), none while drawing;
|
|
35
|
+
* - Download PNG in the ⋯ menu, through the host's `dialogs.saveFile` when the
|
|
36
|
+
* grid was given a host, else a browser download;
|
|
37
|
+
* - Expand opens `openRawTracesWindow` with the last figure drawn;
|
|
38
|
+
* - `onPointClick({customdata, point, tile})` on a click on a mark.
|
|
39
|
+
*
|
|
40
|
+
* `menuItems()` here EXTENDS `TileBase.menuItems()` with Download PNG; a
|
|
41
|
+
* subclass extends these the same way (`[...super.menuItems(), …]`) and routes
|
|
42
|
+
* its own actions through `_onMenuAction`, calling `super` for the rest.
|
|
43
|
+
*
|
|
44
|
+
* ══ EVERY STATE CHANGE INVALIDATES THE FIGURE ═══════════════════════════
|
|
45
|
+
*
|
|
46
|
+
* `showEmpty`, `showLoading` and `showError` replace the content with
|
|
47
|
+
* `innerHTML`, which detaches the Plotly div without telling Plotly. So each of
|
|
48
|
+
* them here first purges the figure, drops the reference, marks it not drawn,
|
|
49
|
+
* and bumps `_drawSeq` — and a draw still in flight finds itself stale and
|
|
50
|
+
* purges its own figure instead of marking it drawn.
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
import { TileBase } from './tile_base.js';
|
|
54
|
+
import { createChart, ensurePlotly, purge, resize as resizePlot } from '../charts/plotly_wrapper.js';
|
|
55
|
+
|
|
56
|
+
/** What every tile's figure is drawn with; a figure's own `config` goes over it. */
|
|
57
|
+
const FIGURE_CONFIG = Object.freeze({
|
|
58
|
+
scrollZoom: false,
|
|
59
|
+
displayModeBar: false,
|
|
60
|
+
responsive: true,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const DOWNLOAD_PNG = Object.freeze({ action: 'download-png', label: 'Download PNG', icon: 'image' });
|
|
64
|
+
|
|
65
|
+
export class FigureTile extends TileBase {
|
|
66
|
+
static TYPE = 'flexdesk.figure';
|
|
67
|
+
static TITLE = 'Figure';
|
|
68
|
+
static EXPANDABLE = true;
|
|
69
|
+
/** The empty state's text when `figure()` answers `null`. */
|
|
70
|
+
static EMPTY_MESSAGE = 'No data to show.';
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {Object} options - TileBase's, plus:
|
|
74
|
+
* @param {Function} [options.onPointClick] - `({customdata, point, tile})`
|
|
75
|
+
* on a click on a mark. Also assignable later as `tile.onPointClick`.
|
|
76
|
+
*/
|
|
77
|
+
constructor(options = {}) {
|
|
78
|
+
super(options);
|
|
79
|
+
this.onPointClick = typeof options.onPointClick === 'function' ? options.onPointClick : null;
|
|
80
|
+
this._plotEl = null;
|
|
81
|
+
this._drawn = false;
|
|
82
|
+
this._drawSeq = 0;
|
|
83
|
+
/** The figure on screen, once drawn; what Expand opens. */
|
|
84
|
+
this._figure = null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The one override: what to draw for `data`.
|
|
89
|
+
* @param {*} _data
|
|
90
|
+
* @returns {{traces: Array, layout?: Object, config?: Object, frames?: Array}|null}
|
|
91
|
+
*/
|
|
92
|
+
figure(_data) {
|
|
93
|
+
throw new Error(`${this.constructor.name}.figure() must be implemented by the subclass`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Hand this tile its data. Unlike `update()`, an empty or null value is
|
|
98
|
+
* drawn — as the empty state — rather than ignored.
|
|
99
|
+
*/
|
|
100
|
+
setData(data) {
|
|
101
|
+
if (this._disposed) return;
|
|
102
|
+
this.data = data ?? null;
|
|
103
|
+
if (this.contentElement) this.render(this.data);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Kick off the draw and return. Deliberately not `async`: nothing awaits
|
|
108
|
+
* `render`, so a returned promise would be a rejection nobody catches.
|
|
109
|
+
*/
|
|
110
|
+
render(data) {
|
|
111
|
+
this._draw(data);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** One relayout when a resize settles; nothing while a draw is in flight. */
|
|
115
|
+
onResize() {
|
|
116
|
+
if (!this._plotEl || !this._drawn) return;
|
|
117
|
+
resizePlot(this._plotEl).catch(() => {});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
menuItems() {
|
|
121
|
+
return [...super.menuItems(), ...this._figureMenuItems()];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The entries this class adds to the menu: Download PNG. For a subclass
|
|
125
|
+
* that wants them WITHOUT `TileBase`'s own. @protected */
|
|
126
|
+
_figureMenuItems() {
|
|
127
|
+
return [{ ...DOWNLOAD_PNG }];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
_onMenuAction(action) {
|
|
131
|
+
if (action === DOWNLOAD_PNG.action) this._downloadPng();
|
|
132
|
+
else super._onMenuAction(action);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The last figure drawn, for the expand window; `null` while there is none. */
|
|
136
|
+
getExpandData() {
|
|
137
|
+
const fig = this._figure;
|
|
138
|
+
if (!fig) return null;
|
|
139
|
+
return {
|
|
140
|
+
title: this.config.title || this.constructor.TITLE,
|
|
141
|
+
// Copies: Plotly writes onto the trace and layout objects it draws.
|
|
142
|
+
traces: fig.traces.map((t) => ({ ...t })),
|
|
143
|
+
layout: { ...(fig.layout || {}) },
|
|
144
|
+
frames: Array.isArray(fig.frames) ? fig.frames : undefined,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── the three states: each one invalidates the figure ───────────────
|
|
149
|
+
|
|
150
|
+
showLoading() {
|
|
151
|
+
this._invalidate();
|
|
152
|
+
super.showLoading();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
showEmpty(message) {
|
|
156
|
+
this._invalidate();
|
|
157
|
+
super.showEmpty(message);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
showError(message) {
|
|
161
|
+
this._invalidate();
|
|
162
|
+
super.showError(message);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Release the figure BEFORE the element goes — `TileBase.dispose` nulls it. */
|
|
166
|
+
dispose() {
|
|
167
|
+
this._invalidate();
|
|
168
|
+
super.dispose();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Any draw in flight is now stale, and the figure on screen is released. */
|
|
172
|
+
_invalidate() {
|
|
173
|
+
this._drawSeq += 1;
|
|
174
|
+
this._releasePlot();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Hand the current figure back to Plotly and forget it. Idempotent, and
|
|
178
|
+
* not awaited: a rejected purge (Plotly absent, already purged) must not
|
|
179
|
+
* surface as an unhandled rejection. */
|
|
180
|
+
_releasePlot() {
|
|
181
|
+
const el = this._plotEl;
|
|
182
|
+
this._plotEl = null;
|
|
183
|
+
this._drawn = false;
|
|
184
|
+
this._figure = null;
|
|
185
|
+
if (el) purge(el).catch(() => {});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The nodes the content holds while `plotEl` draws `fig`: the plot alone
|
|
190
|
+
* here. A subclass adds what sits around the figure (a note, a caption).
|
|
191
|
+
* @param {HTMLElement} plotEl
|
|
192
|
+
* @param {Object} _fig - what `figure()` answered
|
|
193
|
+
* @returns {HTMLElement[]}
|
|
194
|
+
* @protected
|
|
195
|
+
*/
|
|
196
|
+
_figureContent(plotEl, _fig) {
|
|
197
|
+
return [plotEl];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── the draw ────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
async _draw(data) {
|
|
203
|
+
this._drawSeq += 1;
|
|
204
|
+
const seq = this._drawSeq;
|
|
205
|
+
const content = this.contentElement;
|
|
206
|
+
if (!content) return;
|
|
207
|
+
const stale = () => this._disposed || seq !== this._drawSeq;
|
|
208
|
+
|
|
209
|
+
// Every path below replaces the content, so the figure there now is
|
|
210
|
+
// about to be orphaned whichever way this goes.
|
|
211
|
+
this._releasePlot();
|
|
212
|
+
|
|
213
|
+
let fig;
|
|
214
|
+
try {
|
|
215
|
+
fig = this.figure(data);
|
|
216
|
+
if (fig != null && (typeof fig !== 'object' || !Array.isArray(fig.traces))) {
|
|
217
|
+
throw new Error('figure() must answer {traces, layout} or null');
|
|
218
|
+
}
|
|
219
|
+
} catch (err) {
|
|
220
|
+
this._failed(err);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (!fig) {
|
|
224
|
+
// A subclass that already said why (showEmpty) has bumped the sequence.
|
|
225
|
+
if (!stale()) this.showEmpty(this.constructor.EMPTY_MESSAGE);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const host = document.createElement('div');
|
|
230
|
+
host.className = 'fd-figure-tile';
|
|
231
|
+
content.replaceChildren(...this._figureContent(host, fig));
|
|
232
|
+
this._plotEl = host;
|
|
233
|
+
this._drawn = false;
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
const P = await ensurePlotly();
|
|
237
|
+
// Data that changed while Plotly was loading: draw only the latest.
|
|
238
|
+
if (stale()) return;
|
|
239
|
+
await createChart(host, fig.traces, fig.layout || {}, { ...FIGURE_CONFIG, ...(fig.config || {}) });
|
|
240
|
+
if (Array.isArray(fig.frames) && fig.frames.length && !stale()) {
|
|
241
|
+
await P.addFrames(host, fig.frames);
|
|
242
|
+
}
|
|
243
|
+
} catch (err) {
|
|
244
|
+
if (stale()) return;
|
|
245
|
+
this._failed(err);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (stale()) {
|
|
249
|
+
// A newer draw or a state change has already replaced the content;
|
|
250
|
+
// this figure is detached, and purging it releases its listeners.
|
|
251
|
+
purge(host).catch(() => {});
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
this._drawn = true;
|
|
255
|
+
this._figure = fig;
|
|
256
|
+
this._wireClick(host);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Plotly's click, reduced to the point's own `customdata`. A pie's point
|
|
260
|
+
* carries its slice index as `pointNumber` (older builds: `pointIndex`),
|
|
261
|
+
* and its `customdata` is read off the trace by that index. */
|
|
262
|
+
_wireClick(host) {
|
|
263
|
+
if (typeof host.on !== 'function') return;
|
|
264
|
+
host.on('plotly_click', (ev) => {
|
|
265
|
+
const point = ev?.points?.[0];
|
|
266
|
+
if (!point || typeof this.onPointClick !== 'function') return;
|
|
267
|
+
const trace = point.data || {};
|
|
268
|
+
let customdata = point.customdata;
|
|
269
|
+
if (trace.type === 'pie' || customdata === undefined) {
|
|
270
|
+
const i = point.pointNumber ?? point.pointIndex;
|
|
271
|
+
if (Array.isArray(trace.customdata) && i != null) customdata = trace.customdata[i];
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
this.onPointClick({ customdata, point, tile: this });
|
|
275
|
+
} catch (err) {
|
|
276
|
+
console.error(`[${this.constructor.name}] onPointClick threw:`, err);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async _downloadPng() {
|
|
282
|
+
if (!this._plotEl || !this._drawn) return;
|
|
283
|
+
try {
|
|
284
|
+
const P = await ensurePlotly();
|
|
285
|
+
const uri = await P.toImage(this._plotEl, { format: 'png', scale: 2 });
|
|
286
|
+
const filename = `${slug(this.config.title || this.constructor.TITLE)}.png`;
|
|
287
|
+
// The native Save dialog when the host has one (a desktop window).
|
|
288
|
+
// `saveFile` answers null when there is no dialog behind it, which
|
|
289
|
+
// means "download it yourself", not a failed save.
|
|
290
|
+
const dialogs = this.host?.dialogs;
|
|
291
|
+
const result = dialogs
|
|
292
|
+
? await dialogs.saveFile({ data: uri.split(',')[1], filename, kind: 'png' })
|
|
293
|
+
: null;
|
|
294
|
+
if (result) {
|
|
295
|
+
if (result.ok) this._notify('Download', `Chart saved to ${result.path}`, 'success');
|
|
296
|
+
else if (!result.cancelled) this._notify('Download failed', result.error || 'The image could not be saved', 'error');
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const a = document.createElement('a');
|
|
300
|
+
a.href = uri;
|
|
301
|
+
a.download = filename;
|
|
302
|
+
a.click();
|
|
303
|
+
} catch (err) {
|
|
304
|
+
this._notify('Download failed', `The image could not be made: ${err?.message || err}`, 'error');
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
_failed(err) {
|
|
309
|
+
if (this._disposed) return;
|
|
310
|
+
this.showError(`This chart could not be drawn: ${err?.message || err}`);
|
|
311
|
+
this.eventBus?.emit?.('tile:render-failed', {
|
|
312
|
+
tileId: this.id,
|
|
313
|
+
message: err?.message || String(err),
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function slug(text) {
|
|
319
|
+
return String(text).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'chart';
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export default FigureTile;
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Three things used to make this un-shippable, and all three are now injected:
|
|
5
5
|
*
|
|
6
|
-
* 1. It wrote to `localStorage` under
|
|
7
|
-
*
|
|
6
|
+
* 1. It wrote to `localStorage` under two keys named after one application
|
|
7
|
+
* — a LIBRARY scribbling on one application's storage
|
|
8
8
|
* keys. The keys are the consumer's now, and EcoAgent passes its existing ones,
|
|
9
9
|
* so not a single saved layout is orphaned.
|
|
10
10
|
*
|
|
@@ -639,7 +639,7 @@ let _options = {};
|
|
|
639
639
|
* called first won. That is a trap with teeth: if ANY code path had reached the
|
|
640
640
|
* store before the dashboard did, the singleton would have been built with the
|
|
641
641
|
* library's generic defaults, and the app would have read
|
|
642
|
-
* 'twm.tiles.layouts' instead of '
|
|
642
|
+
* 'twm.tiles.layouts' instead of the app's own key — an empty store. Every
|
|
643
643
|
* saved dashboard a user had ever built would have silently vanished, with no
|
|
644
644
|
* error, and no captured view would show it because the pixel gate runs on a
|
|
645
645
|
* fresh template that has no saved layouts to lose.
|