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.
- package/LICENSE +21 -0
- package/README.md +195 -0
- package/THIRD_PARTY_LICENSES.md +53 -0
- package/css/base.css +3439 -0
- package/css/flexdesk.css +5797 -0
- package/css/overrides.css +2349 -0
- package/css/reset.css +46 -0
- package/css/tokens.css +137 -0
- package/dist/charts.js +780 -0
- package/dist/charts.js.map +7 -0
- package/dist/chunk-3PHCPZHT.js +222 -0
- package/dist/chunk-3PHCPZHT.js.map +7 -0
- package/dist/chunk-CT4YXXLP.js +1770 -0
- package/dist/chunk-CT4YXXLP.js.map +7 -0
- package/dist/chunk-DRYCDMEG.js +2217 -0
- package/dist/chunk-DRYCDMEG.js.map +7 -0
- package/dist/chunk-DVU44T77.js +855 -0
- package/dist/chunk-DVU44T77.js.map +7 -0
- package/dist/chunk-FL5KFNQH.js +640 -0
- package/dist/chunk-FL5KFNQH.js.map +7 -0
- package/dist/chunk-FOOS3T5L.js +117 -0
- package/dist/chunk-FOOS3T5L.js.map +7 -0
- package/dist/chunk-JYWURG5T.js +32 -0
- package/dist/chunk-JYWURG5T.js.map +7 -0
- package/dist/chunk-TLZUUFOE.js +276 -0
- package/dist/chunk-TLZUUFOE.js.map +7 -0
- package/dist/chunk-UCJ2WD4D.js +625 -0
- package/dist/chunk-UCJ2WD4D.js.map +7 -0
- package/dist/chunk-WVFGV5FT.js +205 -0
- package/dist/chunk-WVFGV5FT.js.map +7 -0
- package/dist/core.js +1098 -0
- package/dist/core.js.map +7 -0
- package/dist/editor.js +1661 -0
- package/dist/editor.js.map +7 -0
- package/dist/flexdesk.css +5797 -0
- package/dist/host.js +132 -0
- package/dist/host.js.map +7 -0
- package/dist/reset.css +46 -0
- package/dist/tile_registry-6WZPTUZV.js +28 -0
- package/dist/tile_registry-6WZPTUZV.js.map +7 -0
- package/dist/tiles.js +2068 -0
- package/dist/tiles.js.map +7 -0
- package/dist/tokens.css +137 -0
- package/dist/widgets.js +5438 -0
- package/dist/widgets.js.map +7 -0
- package/dist/wm.js +4830 -0
- package/dist/wm.js.map +7 -0
- package/package.json +80 -0
- package/src/charts/chart_types.js +699 -0
- package/src/charts/data_series_plot_window.js +761 -0
- package/src/charts/downsample.js +119 -0
- package/src/charts/plot_config.js +176 -0
- package/src/charts/plot_popout_window.js +1560 -0
- package/src/charts/plotly_wrapper.js +432 -0
- package/src/core/event_bus.js +230 -0
- package/src/core/logging.js +300 -0
- package/src/core/settings.js +813 -0
- package/src/core/state_guard.js +382 -0
- package/src/core/state_guard_config.js +309 -0
- package/src/core/state_machine.js +258 -0
- package/src/editor/editor_pane.js +264 -0
- package/src/editor/monaco_editor_factory.js +449 -0
- package/src/editor/monaco_loader.js +74 -0
- package/src/editor/notebook_search.js +460 -0
- package/src/editor/notebook_tab_bar.js +456 -0
- package/src/editor/notebook_undo_manager.js +108 -0
- package/src/editor/split_pane_container.js +197 -0
- package/src/help/help_modal.js +409 -0
- package/src/help/help_registry.js +65 -0
- package/src/host/host.js +69 -0
- package/src/host/pywebview_host.js +133 -0
- package/src/tiles/config_schema.js +256 -0
- package/src/tiles/layout_persistence.js +662 -0
- package/src/tiles/tile_base.js +785 -0
- package/src/tiles/tile_grid.js +905 -0
- package/src/tiles/tile_registry.js +136 -0
- package/src/tiling/command_palette.js +328 -0
- package/src/tiling/content_registry.js +110 -0
- package/src/tiling/desktops.js +133 -0
- package/src/tiling/entity_sources.js +179 -0
- package/src/tiling/keymap.js +156 -0
- package/src/tiling/kind_taxonomy.js +172 -0
- package/src/tiling/landing_table.js +717 -0
- package/src/tiling/loading_overlay.js +33 -0
- package/src/tiling/nav_panel.js +142 -0
- package/src/tiling/page_factory.js +215 -0
- package/src/tiling/panel_keys.js +145 -0
- package/src/tiling/shell.js +437 -0
- package/src/tiling/tile_breadcrumb.js +222 -0
- package/src/tiling/tile_renderer.js +459 -0
- package/src/tiling/tile_tab_menu.js +340 -0
- package/src/tiling/tile_tree.js +880 -0
- package/src/tiling/wm.js +1352 -0
- package/src/ui/base/component_base.js +30 -0
- package/src/ui/base/controller_base.js +48 -0
- package/src/ui/base/page_base.js +102 -0
- package/src/ui/components/about_dialog.js +224 -0
- package/src/ui/components/action_dropdown.js +389 -0
- package/src/ui/components/attribute_list_editor.js +652 -0
- package/src/ui/components/autocomplete_field.js +732 -0
- package/src/ui/components/computing_status_window.js +81 -0
- package/src/ui/components/confirm_dialog.js +307 -0
- package/src/ui/components/context_menu.js +87 -0
- package/src/ui/components/data_table.js +2228 -0
- package/src/ui/components/detail_header.js +177 -0
- package/src/ui/components/drag_reorder.js +0 -0
- package/src/ui/components/gallery_picker.js +147 -0
- package/src/ui/components/inline_renamer.js +129 -0
- package/src/ui/components/managed_window.js +804 -0
- package/src/ui/components/modal.js +512 -0
- package/src/ui/components/notification_history.js +264 -0
- package/src/ui/components/slide_out_panel.js +185 -0
- package/src/ui/components/slider_field.js +122 -0
- package/src/ui/components/sortable_list.js +309 -0
- package/src/ui/components/table_state_store.js +81 -0
- package/src/ui/components/toast.js +59 -0
- package/src/ui/components/tree_view.js +460 -0
- package/src/ui/components/window_taskbar.js +143 -0
- package/src/ui/controllers/panel_state_machine.js +634 -0
- package/src/ui/controllers/window_chrome_controller.js +659 -0
- package/src/ui/notification_center.js +576 -0
- package/src/ui/utils/overlay_scrollbar.js +492 -0
- package/src/ui/utils/raf_resize_observer.js +31 -0
- package/src/ui/utils/tooltip_service.js +406 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plotly.js Wrapper
|
|
3
|
+
*
|
|
4
|
+
* Provides lazy loading of Plotly.js and helper functions for chart operations.
|
|
5
|
+
* Supports streaming updates via Plotly.extendTraces() and Plotly.react().
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createRafResizeObserver } from '../ui/utils/raf_resize_observer.js';
|
|
9
|
+
|
|
10
|
+
// Where Plotly lives is the CONSUMER's business, not the library's.
|
|
11
|
+
//
|
|
12
|
+
// This was `const PLOTLY_LOCAL = 'vendor/plotly/plotly-2.35.2.min.js'` — a
|
|
13
|
+
// relative path into one particular application's directory tree. It happens to
|
|
14
|
+
// work for EcoAgent and would 404 for anyone else, and the failure would arrive
|
|
15
|
+
// as an empty chart with a console error, at runtime, only on the screens that
|
|
16
|
+
// plot something.
|
|
17
|
+
//
|
|
18
|
+
// Plotly stays a PEER dependency: 4.4 MB that @flexdesk/charts refuses to bundle.
|
|
19
|
+
//
|
|
20
|
+
// There is NO DEFAULT. Leaving one in place would only have meant leaving
|
|
21
|
+
// EcoAgent's exact versioned filename in the library — the same bug wearing a
|
|
22
|
+
// setter — and it would fail for everyone else as an empty chart and a console
|
|
23
|
+
// error, at runtime, on the screens that plot something.
|
|
24
|
+
//
|
|
25
|
+
// ensurePlotly() short-circuits when `window.Plotly` is already defined, which is
|
|
26
|
+
// how EcoAgent works: it preloads Plotly with a <script> tag in index.html and
|
|
27
|
+
// never reaches the loader below. A consumer that does not preload calls
|
|
28
|
+
// setPlotlySource() instead. A consumer that does neither now gets told so.
|
|
29
|
+
let _plotlySrc = null;
|
|
30
|
+
|
|
31
|
+
/** Point the loader at your copy of Plotly. Call before the first chart renders. */
|
|
32
|
+
export function setPlotlySource(src) {
|
|
33
|
+
if (typeof src === 'string' && src) _plotlySrc = src;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Loading promise for singleton pattern
|
|
37
|
+
let _plotlyLoadPromise = null;
|
|
38
|
+
|
|
39
|
+
// Workarounds for Plotly.js 2.35.x running in Chromium-based pywebview.
|
|
40
|
+
let _plotlyPatchesInstalled = false;
|
|
41
|
+
function _installPlotlyPatches() {
|
|
42
|
+
if (_plotlyPatchesInstalled) return;
|
|
43
|
+
_plotlyPatchesInstalled = true;
|
|
44
|
+
|
|
45
|
+
// Plotly's internal mousemove handler assigns event.target which is
|
|
46
|
+
// read-only in modern browsers. Replace the native getter with a
|
|
47
|
+
// getter/setter pair so the assignment succeeds silently.
|
|
48
|
+
const targetDesc = Object.getOwnPropertyDescriptor(Event.prototype, 'target');
|
|
49
|
+
if (targetDesc && targetDesc.get && !targetDesc.set) {
|
|
50
|
+
const origGetter = targetDesc.get;
|
|
51
|
+
Object.defineProperty(Event.prototype, 'target', {
|
|
52
|
+
configurable: true,
|
|
53
|
+
enumerable: true,
|
|
54
|
+
get() {
|
|
55
|
+
return this._plotlyTarget ?? origGetter.call(this);
|
|
56
|
+
},
|
|
57
|
+
set(v) {
|
|
58
|
+
this._plotlyTarget = v;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Plotly calls getImageData() repeatedly on 2D canvases without the
|
|
64
|
+
// willReadFrequently hint, triggering a Chromium performance warning.
|
|
65
|
+
// Patch getContext to inject the hint for all 2D contexts.
|
|
66
|
+
const _origGetContext = HTMLCanvasElement.prototype.getContext;
|
|
67
|
+
HTMLCanvasElement.prototype.getContext = function (type, attrs) {
|
|
68
|
+
if (type === '2d') {
|
|
69
|
+
attrs = Object.assign({ willReadFrequently: true }, attrs);
|
|
70
|
+
}
|
|
71
|
+
return _origGetContext.call(this, type, attrs);
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Run immediately on module import so the patches are active before any Plotly
|
|
76
|
+
// chart is created (including during workspace hydration) — but ONLY in a DOM.
|
|
77
|
+
//
|
|
78
|
+
// Unguarded, this monkey-patched HTMLCanvasElement.prototype at module scope, so
|
|
79
|
+
// `import '@flexdesk/charts'` threw `ReferenceError: HTMLCanvasElement is not defined`
|
|
80
|
+
// the instant anything imported it outside a browser. That is the third module in
|
|
81
|
+
// this library caught doing DOM work on import (see inline_renamer.js and
|
|
82
|
+
// tooltip_service.js), and all three were found by the same test: one that does
|
|
83
|
+
// nothing but import each entry point. A library whose entry point cannot be
|
|
84
|
+
// imported without a DOM cannot be unit-tested, server-rendered, or loaded in a
|
|
85
|
+
// worker.
|
|
86
|
+
if (typeof HTMLCanvasElement !== 'undefined') {
|
|
87
|
+
_installPlotlyPatches();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Dark theme layout defaults matching EcoSim UI
|
|
92
|
+
*/
|
|
93
|
+
export const DARK_THEME_LAYOUT = {
|
|
94
|
+
paper_bgcolor: 'transparent',
|
|
95
|
+
plot_bgcolor: 'transparent',
|
|
96
|
+
font: {
|
|
97
|
+
family: 'Inter, -apple-system, BlinkMacSystemFont, sans-serif',
|
|
98
|
+
size: 11,
|
|
99
|
+
color: '#cccccc'
|
|
100
|
+
},
|
|
101
|
+
margin: { l: 50, r: 20, t: 30, b: 40 },
|
|
102
|
+
legend: {
|
|
103
|
+
bgcolor: 'rgba(0,0,0,0)',
|
|
104
|
+
font: { color: '#cccccc' }
|
|
105
|
+
},
|
|
106
|
+
xaxis: {
|
|
107
|
+
gridcolor: '#333333',
|
|
108
|
+
linecolor: '#444444',
|
|
109
|
+
tickcolor: '#666666',
|
|
110
|
+
zerolinecolor: '#444444'
|
|
111
|
+
},
|
|
112
|
+
yaxis: {
|
|
113
|
+
gridcolor: '#333333',
|
|
114
|
+
linecolor: '#444444',
|
|
115
|
+
tickcolor: '#666666',
|
|
116
|
+
zerolinecolor: '#444444'
|
|
117
|
+
},
|
|
118
|
+
// Hover tooltip styling for dark theme
|
|
119
|
+
hoverlabel: {
|
|
120
|
+
bgcolor: '#1e2228',
|
|
121
|
+
bordercolor: '#444444',
|
|
122
|
+
font: {
|
|
123
|
+
family: 'Inter, -apple-system, BlinkMacSystemFont, sans-serif',
|
|
124
|
+
size: 12,
|
|
125
|
+
color: '#e0e0e0'
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
// 3D scene defaults
|
|
129
|
+
scene: {
|
|
130
|
+
xaxis: {
|
|
131
|
+
gridcolor: '#333333',
|
|
132
|
+
linecolor: '#444444',
|
|
133
|
+
backgroundcolor: 'rgba(0,0,0,0)'
|
|
134
|
+
},
|
|
135
|
+
yaxis: {
|
|
136
|
+
gridcolor: '#333333',
|
|
137
|
+
linecolor: '#444444',
|
|
138
|
+
backgroundcolor: 'rgba(0,0,0,0)'
|
|
139
|
+
},
|
|
140
|
+
zaxis: {
|
|
141
|
+
gridcolor: '#333333',
|
|
142
|
+
linecolor: '#444444',
|
|
143
|
+
backgroundcolor: 'rgba(0,0,0,0)'
|
|
144
|
+
},
|
|
145
|
+
bgcolor: 'rgba(0,0,0,0)'
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Default Plotly config options
|
|
151
|
+
*/
|
|
152
|
+
export const DEFAULT_CONFIG = {
|
|
153
|
+
responsive: true,
|
|
154
|
+
displaylogo: false,
|
|
155
|
+
displayModeBar: false, // Hide modebar completely
|
|
156
|
+
scrollZoom: true, // Allow zoom with scroll wheel instead
|
|
157
|
+
toImageButtonOptions: {
|
|
158
|
+
format: 'png',
|
|
159
|
+
filename: 'ecosim_chart',
|
|
160
|
+
scale: 2
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Load Plotly.js from CDN.
|
|
166
|
+
* Uses singleton pattern to avoid multiple loads.
|
|
167
|
+
* @returns {Promise<Plotly>} The Plotly library object
|
|
168
|
+
*/
|
|
169
|
+
export async function ensurePlotly() {
|
|
170
|
+
// Already loaded
|
|
171
|
+
if (typeof Plotly !== 'undefined') {
|
|
172
|
+
return Plotly;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Loading in progress
|
|
176
|
+
if (_plotlyLoadPromise) {
|
|
177
|
+
return _plotlyLoadPromise;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!_plotlySrc) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
'[twm/charts] Plotly is not loaded and no source is configured. '
|
|
183
|
+
+ 'Either load Plotly yourself (a <script> tag that sets window.Plotly), '
|
|
184
|
+
+ 'or call setPlotlySource("/path/to/plotly.min.js") before rendering a chart.');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Start loading — temporarily hide AMD define so Plotly sets window.Plotly
|
|
188
|
+
// instead of registering itself as an AMD module (conflicts with Monaco loader).
|
|
189
|
+
_plotlyLoadPromise = new Promise((resolve, reject) => {
|
|
190
|
+
const savedDefine = window.define;
|
|
191
|
+
window.define = undefined;
|
|
192
|
+
|
|
193
|
+
const script = document.createElement('script');
|
|
194
|
+
script.src = _plotlySrc;
|
|
195
|
+
script.async = true;
|
|
196
|
+
|
|
197
|
+
script.onload = () => {
|
|
198
|
+
window.define = savedDefine;
|
|
199
|
+
if (typeof Plotly !== 'undefined') {
|
|
200
|
+
console.log('[PlotlyWrapper] Plotly.js loaded successfully');
|
|
201
|
+
resolve(Plotly);
|
|
202
|
+
} else {
|
|
203
|
+
reject(new Error('Plotly.js loaded but Plotly global not found'));
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
script.onerror = () => {
|
|
208
|
+
window.define = savedDefine;
|
|
209
|
+
reject(new Error(`Failed to load Plotly.js from ${_plotlySrc} — `
|
|
210
|
+
+ 'call setPlotlySource() with the path to your copy.'));
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
document.head.appendChild(script);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
return _plotlyLoadPromise;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Create a new Plotly chart.
|
|
221
|
+
* @param {HTMLElement} container - Container element for the chart
|
|
222
|
+
* @param {Array} data - Array of trace objects
|
|
223
|
+
* @param {Object} layout - Layout configuration
|
|
224
|
+
* @param {Object} config - Plotly config options
|
|
225
|
+
* @returns {Promise<HTMLElement>} The chart element
|
|
226
|
+
*/
|
|
227
|
+
export async function createChart(container, data, layout = {}, config = {}) {
|
|
228
|
+
const Plotly = await ensurePlotly();
|
|
229
|
+
|
|
230
|
+
const mergedLayout = {
|
|
231
|
+
...DARK_THEME_LAYOUT,
|
|
232
|
+
...layout
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const mergedConfig = {
|
|
236
|
+
...DEFAULT_CONFIG,
|
|
237
|
+
...config
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
return Plotly.newPlot(container, data, mergedLayout, mergedConfig);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Update chart data and layout efficiently.
|
|
245
|
+
* Uses Plotly.react() which is optimized for updates.
|
|
246
|
+
* @param {HTMLElement} container - Chart container
|
|
247
|
+
* @param {Array} data - New trace data
|
|
248
|
+
* @param {Object} layout - Layout updates
|
|
249
|
+
*/
|
|
250
|
+
export async function updateChart(container, data, layout = {}) {
|
|
251
|
+
const Plotly = await ensurePlotly();
|
|
252
|
+
|
|
253
|
+
const mergedLayout = {
|
|
254
|
+
...DARK_THEME_LAYOUT,
|
|
255
|
+
...layout
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
return Plotly.react(container, data, mergedLayout);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Extend traces with new data points (for streaming).
|
|
263
|
+
* More efficient than full update for appending data.
|
|
264
|
+
* @param {HTMLElement} container - Chart container
|
|
265
|
+
* @param {Object} update - Data to extend: { x: [[...]], y: [[...]] }
|
|
266
|
+
* @param {Array<number>} traceIndices - Which traces to extend
|
|
267
|
+
* @param {number} maxPoints - Maximum points to keep (optional)
|
|
268
|
+
*/
|
|
269
|
+
export async function extendTraces(container, update, traceIndices, maxPoints = null) {
|
|
270
|
+
const Plotly = await ensurePlotly();
|
|
271
|
+
|
|
272
|
+
if (maxPoints) {
|
|
273
|
+
return Plotly.extendTraces(container, update, traceIndices, maxPoints);
|
|
274
|
+
}
|
|
275
|
+
return Plotly.extendTraces(container, update, traceIndices);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Relayout chart (update layout only, no data change).
|
|
280
|
+
* @param {HTMLElement} container - Chart container
|
|
281
|
+
* @param {Object} layoutUpdate - Layout properties to update
|
|
282
|
+
*/
|
|
283
|
+
export async function relayout(container, layoutUpdate) {
|
|
284
|
+
const Plotly = await ensurePlotly();
|
|
285
|
+
return Plotly.relayout(container, layoutUpdate);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Restyle traces (update trace properties without full redraw).
|
|
290
|
+
* @param {HTMLElement} container - Chart container
|
|
291
|
+
* @param {Object} styleUpdate - Style properties to update
|
|
292
|
+
* @param {Array<number>} traceIndices - Which traces to update
|
|
293
|
+
*/
|
|
294
|
+
export async function restyle(container, styleUpdate, traceIndices) {
|
|
295
|
+
const Plotly = await ensurePlotly();
|
|
296
|
+
return Plotly.restyle(container, styleUpdate, traceIndices);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Delete all traces and release resources.
|
|
301
|
+
* @param {HTMLElement} container - Chart container
|
|
302
|
+
*/
|
|
303
|
+
export async function purge(container) {
|
|
304
|
+
const Plotly = await ensurePlotly();
|
|
305
|
+
return Plotly.purge(container);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Check if container has a Plotly chart.
|
|
310
|
+
* @param {HTMLElement} container - Container element
|
|
311
|
+
* @returns {boolean}
|
|
312
|
+
*/
|
|
313
|
+
export function hasPlot(container) {
|
|
314
|
+
return container && container._fullLayout !== undefined;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Resize chart to fit container.
|
|
319
|
+
* Call this when container size changes.
|
|
320
|
+
* @param {HTMLElement} container - Chart container
|
|
321
|
+
*/
|
|
322
|
+
export async function resize(container) {
|
|
323
|
+
const Plotly = await ensurePlotly();
|
|
324
|
+
return Plotly.Plots.resize(container);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Convert hex color to rgba.
|
|
329
|
+
* @param {string} hex - Hex color code
|
|
330
|
+
* @param {number} alpha - Alpha value (0-1)
|
|
331
|
+
* @returns {string} rgba color string
|
|
332
|
+
*/
|
|
333
|
+
export function hexToRgba(hex, alpha = 1) {
|
|
334
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
335
|
+
if (!result) return hex;
|
|
336
|
+
const r = parseInt(result[1], 16);
|
|
337
|
+
const g = parseInt(result[2], 16);
|
|
338
|
+
const b = parseInt(result[3], 16);
|
|
339
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Solarized theme colors matching legacy plot_node.js
|
|
343
|
+
export const COLOR_PALETTE = [
|
|
344
|
+
'#268bd2', // Solarized Blue
|
|
345
|
+
'#d33682', // Solarized Magenta
|
|
346
|
+
'#cb4b16', // Solarized Orange
|
|
347
|
+
'#2aa198', // Solarized Cyan
|
|
348
|
+
'#6a5acd', // SlateBlue/Purple
|
|
349
|
+
'#7a7d80', // Gray
|
|
350
|
+
'#9fb6cf', // Gray-Blue
|
|
351
|
+
];
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Get color from palette by index.
|
|
355
|
+
* @param {number} index - Series index
|
|
356
|
+
* @returns {string} Color hex code
|
|
357
|
+
*/
|
|
358
|
+
export function getSeriesColor(index) {
|
|
359
|
+
return COLOR_PALETTE[index % COLOR_PALETTE.length];
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Create a Plotly chart with deferred rendering support.
|
|
364
|
+
* Handles zero-dimension containers by waiting for valid dimensions via ResizeObserver.
|
|
365
|
+
* @param {HTMLElement} container - Container element for the chart
|
|
366
|
+
* @param {Array} traces - Array of trace objects
|
|
367
|
+
* @param {Object} layout - Layout configuration
|
|
368
|
+
* @param {Object} config - Plotly config options
|
|
369
|
+
* @returns {Promise<{element: HTMLElement, cleanup: Function}>} Chart element and cleanup function
|
|
370
|
+
*/
|
|
371
|
+
export async function createChartDeferred(container, traces, layout = {}, config = {}) {
|
|
372
|
+
const Plotly = await ensurePlotly();
|
|
373
|
+
|
|
374
|
+
const mergedLayout = {
|
|
375
|
+
...DARK_THEME_LAYOUT,
|
|
376
|
+
...layout
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const mergedConfig = {
|
|
380
|
+
...DEFAULT_CONFIG,
|
|
381
|
+
...config
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const w = container.offsetWidth;
|
|
385
|
+
const h = container.offsetHeight;
|
|
386
|
+
|
|
387
|
+
// Cleanup function to return
|
|
388
|
+
let resizeObserver = null;
|
|
389
|
+
const cleanup = () => {
|
|
390
|
+
if (resizeObserver) {
|
|
391
|
+
resizeObserver.disconnect();
|
|
392
|
+
resizeObserver = null;
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
// If dimensions are valid, render immediately
|
|
397
|
+
if (w > 0 && h > 0) {
|
|
398
|
+
await Plotly.newPlot(container, traces, mergedLayout, mergedConfig);
|
|
399
|
+
// Still set up resize observer for ongoing resizes
|
|
400
|
+
resizeObserver = createRafResizeObserver(() => {
|
|
401
|
+
if (hasPlot(container)) {
|
|
402
|
+
Plotly.Plots.resize(container).catch(() => {});
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
resizeObserver.observe(container);
|
|
406
|
+
return { element: container, cleanup };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Dimensions are zero - wait for valid dimensions via ResizeObserver
|
|
410
|
+
return new Promise((resolve) => {
|
|
411
|
+
let rendered = false;
|
|
412
|
+
resizeObserver = createRafResizeObserver((entries) => {
|
|
413
|
+
for (const entry of entries) {
|
|
414
|
+
const { width, height } = entry.contentRect;
|
|
415
|
+
if (width > 0 && height > 0) {
|
|
416
|
+
if (!rendered) {
|
|
417
|
+
rendered = true;
|
|
418
|
+
Plotly.newPlot(container, traces, mergedLayout, mergedConfig).then(() => {
|
|
419
|
+
resolve({ element: container, cleanup });
|
|
420
|
+
}).catch(() => {
|
|
421
|
+
resolve({ element: container, cleanup });
|
|
422
|
+
});
|
|
423
|
+
} else if (hasPlot(container)) {
|
|
424
|
+
// Handle subsequent resizes
|
|
425
|
+
Plotly.Plots.resize(container).catch(() => {});
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
resizeObserver.observe(container);
|
|
431
|
+
});
|
|
432
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event Bus (js_new)
|
|
3
|
+
*
|
|
4
|
+
* Purpose
|
|
5
|
+
* -------
|
|
6
|
+
* Provide a deterministic, dependency-injected publish/subscribe hub for all front-end modules.
|
|
7
|
+
*
|
|
8
|
+
* Responsibilities
|
|
9
|
+
* - Maintain per-event listener registries with namespace awareness.
|
|
10
|
+
* - Support synchronous + async dispatch without DOM bridges or global singletons.
|
|
11
|
+
* - Surface diagnostics via the shared logging service instead of ad hoc console calls.
|
|
12
|
+
*
|
|
13
|
+
* Source Material
|
|
14
|
+
* - html/js/event_manager.js (core pub/sub semantics, minus DOM bridge + globals).
|
|
15
|
+
* - html/js/debug_utils.js (event history/diagnostics requirements).
|
|
16
|
+
*
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const PATTERN_TOKEN_RE = /[.*+?^${}()|[\]\\]/g;
|
|
20
|
+
const HAS_WILDCARD_RE = /[\*\?]/;
|
|
21
|
+
|
|
22
|
+
export class EventBus {
|
|
23
|
+
constructor({ logger, historyLimit = 200 } = {}) {
|
|
24
|
+
this.logger = logger;
|
|
25
|
+
this.historyLimit = historyLimit;
|
|
26
|
+
this.sequence = 0;
|
|
27
|
+
this.listeners = new Map(); // event -> Set<entry>
|
|
28
|
+
this.wildcardListeners = new Map(); // id -> entry (regex based)
|
|
29
|
+
this.listenerIndex = new Map(); // id -> entry
|
|
30
|
+
this.eventHistory = [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
on(eventName, handler, options = {}) {
|
|
34
|
+
if (typeof eventName !== 'string' || !eventName.trim()) {
|
|
35
|
+
throw new TypeError('eventName must be a non-empty string');
|
|
36
|
+
}
|
|
37
|
+
if (typeof handler !== 'function') {
|
|
38
|
+
throw new TypeError('handler must be a function');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const entry = {
|
|
42
|
+
id: ++this.sequence,
|
|
43
|
+
eventName,
|
|
44
|
+
handler,
|
|
45
|
+
namespace: options.namespace ?? null,
|
|
46
|
+
once: Boolean(options.once),
|
|
47
|
+
priority: options.priority ?? 0,
|
|
48
|
+
regex: null,
|
|
49
|
+
abortCleanup: null,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if (HAS_WILDCARD_RE.test(eventName)) {
|
|
53
|
+
entry.regex = this._compilePattern(eventName);
|
|
54
|
+
this.wildcardListeners.set(entry.id, entry);
|
|
55
|
+
} else {
|
|
56
|
+
if (!this.listeners.has(eventName)) {
|
|
57
|
+
this.listeners.set(eventName, new Set());
|
|
58
|
+
}
|
|
59
|
+
this.listeners.get(eventName).add(entry);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const useAbortSignal = typeof AbortSignal !== 'undefined' && options.signal instanceof AbortSignal;
|
|
63
|
+
if (useAbortSignal) {
|
|
64
|
+
if (options.signal.aborted) {
|
|
65
|
+
// Immediately unsubscribe if already aborted.
|
|
66
|
+
return { dispose: () => {}, id: entry.id };
|
|
67
|
+
}
|
|
68
|
+
const abortFn = () => this.off(entry.id);
|
|
69
|
+
options.signal.addEventListener('abort', abortFn, { once: true });
|
|
70
|
+
entry.abortCleanup = () => options.signal.removeEventListener('abort', abortFn);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
this.listenerIndex.set(entry.id, entry);
|
|
74
|
+
return {
|
|
75
|
+
id: entry.id,
|
|
76
|
+
dispose: () => this.off(entry.id),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
once(eventName, handler, options = {}) {
|
|
81
|
+
return this.on(eventName, handler, { ...options, once: true });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
off(target) {
|
|
85
|
+
const id = typeof target === 'number' ? target : target?.id;
|
|
86
|
+
if (!this.listenerIndex.has(id)) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
const entry = this.listenerIndex.get(id);
|
|
90
|
+
this.listenerIndex.delete(id);
|
|
91
|
+
|
|
92
|
+
if (entry.abortCleanup) {
|
|
93
|
+
try {
|
|
94
|
+
entry.abortCleanup();
|
|
95
|
+
} catch (err) {
|
|
96
|
+
this.logger?.warn?.('event-bus', 'Abort cleanup failed', err);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (entry.regex) {
|
|
101
|
+
this.wildcardListeners.delete(entry.id);
|
|
102
|
+
} else {
|
|
103
|
+
const bucket = this.listeners.get(entry.eventName);
|
|
104
|
+
if (bucket) {
|
|
105
|
+
bucket.delete(entry);
|
|
106
|
+
if (bucket.size === 0) {
|
|
107
|
+
this.listeners.delete(entry.eventName);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
emit(eventName, payload = undefined, options = {}) {
|
|
115
|
+
const context = this._createContext(eventName, options);
|
|
116
|
+
const listeners = this._collectListeners(eventName, context.namespace);
|
|
117
|
+
this._recordHistory(eventName, context.namespace, listeners.length);
|
|
118
|
+
return this._invokeListeners(listeners, payload, context);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
emitAsync(eventName, payload = undefined, options = {}) {
|
|
122
|
+
const context = this._createContext(eventName, options);
|
|
123
|
+
const listeners = this._collectListeners(eventName, context.namespace);
|
|
124
|
+
this._recordHistory(eventName, context.namespace, listeners.length);
|
|
125
|
+
return Promise.resolve().then(() => this._invokeListeners(listeners, payload, context));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
listenerCount(eventName) {
|
|
129
|
+
const direct = this.listeners.get(eventName)?.size ?? 0;
|
|
130
|
+
const wildcard = [...this.wildcardListeners.values()].filter((entry) => entry.regex.test(eventName)).length;
|
|
131
|
+
return direct + wildcard;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
getHistory() {
|
|
135
|
+
return [...this.eventHistory];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
clearHistory() {
|
|
139
|
+
this.eventHistory.length = 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
reset() {
|
|
143
|
+
this.listeners.clear();
|
|
144
|
+
this.wildcardListeners.clear();
|
|
145
|
+
this.listenerIndex.clear();
|
|
146
|
+
this.eventHistory.length = 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
_collectListeners(eventName, namespace) {
|
|
150
|
+
const matches = [];
|
|
151
|
+
const bucket = this.listeners.get(eventName);
|
|
152
|
+
if (bucket) {
|
|
153
|
+
matches.push(...bucket);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
for (const entry of this.wildcardListeners.values()) {
|
|
157
|
+
if (entry.regex.test(eventName)) {
|
|
158
|
+
matches.push(entry);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return matches
|
|
163
|
+
.filter((entry) => !entry.namespace || entry.namespace === namespace)
|
|
164
|
+
.sort((a, b) => {
|
|
165
|
+
if (a.priority === b.priority) {
|
|
166
|
+
return a.id - b.id;
|
|
167
|
+
}
|
|
168
|
+
return b.priority - a.priority;
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
_invokeListeners(listeners, payload, context) {
|
|
173
|
+
const results = [];
|
|
174
|
+
context.stopPropagation = () => {
|
|
175
|
+
context.__stopped = true;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
for (const entry of listeners) {
|
|
179
|
+
if (context.__stopped) {
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
const result = entry.handler(payload, context);
|
|
184
|
+
results.push(result);
|
|
185
|
+
if (entry.once) {
|
|
186
|
+
this.off(entry.id);
|
|
187
|
+
}
|
|
188
|
+
} catch (err) {
|
|
189
|
+
this._handleListenerError(entry, context, err);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return results;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
_createContext(eventName, options) {
|
|
196
|
+
return {
|
|
197
|
+
event: eventName,
|
|
198
|
+
namespace: options.namespace ?? null,
|
|
199
|
+
timestamp: Date.now(),
|
|
200
|
+
stopPropagation: () => {},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
_recordHistory(eventName, namespace, listenerCount) {
|
|
205
|
+
this.eventHistory.push({
|
|
206
|
+
event: eventName,
|
|
207
|
+
namespace,
|
|
208
|
+
timestamp: Date.now(),
|
|
209
|
+
listeners: listenerCount,
|
|
210
|
+
});
|
|
211
|
+
if (this.eventHistory.length > this.historyLimit) {
|
|
212
|
+
this.eventHistory.shift();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
_handleListenerError(entry, context, error) {
|
|
217
|
+
this.logger?.error?.('event-bus', 'Listener error', {
|
|
218
|
+
event: context.event,
|
|
219
|
+
namespace: context.namespace,
|
|
220
|
+
listenerId: entry.id,
|
|
221
|
+
error,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
_compilePattern(pattern) {
|
|
226
|
+
const escaped = pattern.replace(PATTERN_TOKEN_RE, '\\$&');
|
|
227
|
+
const regexSource = `^${escaped.replace(/\\\*/g, '.*').replace(/\\\?/g, '.')}$`;
|
|
228
|
+
return new RegExp(regexSource);
|
|
229
|
+
}
|
|
230
|
+
}
|