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,699 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chart Types and Trace Configurations
|
|
3
|
+
*
|
|
4
|
+
* Defines trace configurations for Plotly.js and provides mapping
|
|
5
|
+
* from Chart.js options to Plotly equivalents.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getSeriesColor, hexToRgba } from './plotly_wrapper.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Available 2D chart types
|
|
12
|
+
*/
|
|
13
|
+
export const CHART_TYPES_2D = Object.freeze([
|
|
14
|
+
{ value: 'line', label: 'Line' },
|
|
15
|
+
{ value: 'bar', label: 'Bar' },
|
|
16
|
+
{ value: 'scatter', label: 'Scatter' },
|
|
17
|
+
{ value: 'area', label: 'Area' },
|
|
18
|
+
{ value: 'area-stacked', label: 'Area (Stacked %)' },
|
|
19
|
+
{ value: 'phase', label: 'Phase' },
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Available 3D chart types
|
|
24
|
+
*/
|
|
25
|
+
export const CHART_TYPES_3D = Object.freeze([
|
|
26
|
+
{ value: 'scatter3d', label: '3D Scatter' },
|
|
27
|
+
{ value: 'line3d', label: '3D Line' },
|
|
28
|
+
{ value: 'surface', label: '3D Surface' },
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* All chart types combined
|
|
33
|
+
*/
|
|
34
|
+
export const ALL_CHART_TYPES = Object.freeze([...CHART_TYPES_2D, ...CHART_TYPES_3D]);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if chart type is 3D
|
|
38
|
+
* @param {string} chartType
|
|
39
|
+
* @returns {boolean}
|
|
40
|
+
*/
|
|
41
|
+
export function is3DChart(chartType) {
|
|
42
|
+
return ['scatter3d', 'line3d', 'surface'].includes(chartType);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Line style mapping (Chart.js borderDash to Plotly dash)
|
|
47
|
+
*/
|
|
48
|
+
const LINE_DASH_MAP = {
|
|
49
|
+
solid: 'solid',
|
|
50
|
+
dashed: 'dash',
|
|
51
|
+
dotted: 'dot',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Interpolation mapping (Chart.js to Plotly line shape)
|
|
56
|
+
*/
|
|
57
|
+
const INTERPOLATION_MAP = {
|
|
58
|
+
linear: 'linear',
|
|
59
|
+
step: 'hv', // horizontal-vertical step
|
|
60
|
+
cubic: 'spline', // smooth spline
|
|
61
|
+
auto: 'linear',
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Build a Plotly trace from series configuration.
|
|
66
|
+
* @param {Object} options
|
|
67
|
+
* @param {string} options.chartType - Chart type (line, bar, scatter, area, scatter3d, etc.)
|
|
68
|
+
* @param {Array} options.x - X-axis data
|
|
69
|
+
* @param {Array} options.y - Y-axis data
|
|
70
|
+
* @param {Array} [options.z] - Z-axis data (for 3D charts)
|
|
71
|
+
* @param {string} options.name - Series name/label
|
|
72
|
+
* @param {string} [options.color] - Series color (hex)
|
|
73
|
+
* @param {number} [options.lineWidth] - Line width in pixels
|
|
74
|
+
* @param {string} [options.lineStyle] - Line style: solid, dashed, dotted
|
|
75
|
+
* @param {string} [options.interpolation] - Interpolation: linear, step, cubic
|
|
76
|
+
* @param {string} [options.yAxisId] - Y-axis identifier (y, y2, y3, etc.)
|
|
77
|
+
* @param {boolean} [options.showMarkers] - Show data point markers
|
|
78
|
+
* @param {number} [options.seriesIndex] - Index for default color selection
|
|
79
|
+
* @returns {Object} Plotly trace object
|
|
80
|
+
*/
|
|
81
|
+
export function buildTrace(options) {
|
|
82
|
+
const {
|
|
83
|
+
chartType = 'line',
|
|
84
|
+
x = [],
|
|
85
|
+
y = [],
|
|
86
|
+
z = null,
|
|
87
|
+
name = 'Series',
|
|
88
|
+
color = null,
|
|
89
|
+
lineWidth = 2,
|
|
90
|
+
lineStyle = 'solid',
|
|
91
|
+
interpolation = 'linear',
|
|
92
|
+
yAxisId = 'y',
|
|
93
|
+
showMarkers = false,
|
|
94
|
+
seriesIndex = 0,
|
|
95
|
+
stackGroup = null,
|
|
96
|
+
} = options;
|
|
97
|
+
|
|
98
|
+
const seriesColor = color || getSeriesColor(seriesIndex);
|
|
99
|
+
|
|
100
|
+
// Determine Plotly trace type and mode
|
|
101
|
+
let plotlyType, mode;
|
|
102
|
+
switch (chartType) {
|
|
103
|
+
case 'line':
|
|
104
|
+
plotlyType = 'scatter';
|
|
105
|
+
mode = showMarkers ? 'lines+markers' : 'lines';
|
|
106
|
+
break;
|
|
107
|
+
case 'scatter':
|
|
108
|
+
plotlyType = 'scatter';
|
|
109
|
+
mode = 'markers';
|
|
110
|
+
break;
|
|
111
|
+
case 'area':
|
|
112
|
+
case 'area-stacked':
|
|
113
|
+
plotlyType = 'scatter';
|
|
114
|
+
mode = showMarkers ? 'lines+markers' : 'lines';
|
|
115
|
+
break;
|
|
116
|
+
case 'phase':
|
|
117
|
+
plotlyType = 'scatter';
|
|
118
|
+
mode = 'lines+markers';
|
|
119
|
+
break;
|
|
120
|
+
case 'bar':
|
|
121
|
+
plotlyType = 'bar';
|
|
122
|
+
mode = undefined;
|
|
123
|
+
break;
|
|
124
|
+
case 'scatter3d':
|
|
125
|
+
plotlyType = 'scatter3d';
|
|
126
|
+
mode = 'markers';
|
|
127
|
+
break;
|
|
128
|
+
case 'line3d':
|
|
129
|
+
plotlyType = 'scatter3d';
|
|
130
|
+
mode = showMarkers ? 'lines+markers' : 'lines';
|
|
131
|
+
break;
|
|
132
|
+
case 'surface':
|
|
133
|
+
plotlyType = 'surface';
|
|
134
|
+
mode = undefined;
|
|
135
|
+
break;
|
|
136
|
+
default:
|
|
137
|
+
plotlyType = 'scatter';
|
|
138
|
+
mode = showMarkers ? 'lines+markers' : 'lines';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Build base trace
|
|
142
|
+
const trace = {
|
|
143
|
+
type: plotlyType,
|
|
144
|
+
name,
|
|
145
|
+
x,
|
|
146
|
+
y,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// Add mode if applicable
|
|
150
|
+
if (mode) {
|
|
151
|
+
trace.mode = mode;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Add z-axis data for 3D charts
|
|
155
|
+
if (z && is3DChart(chartType)) {
|
|
156
|
+
// Surface charts require z to be a 2D array
|
|
157
|
+
// If z is 1D (time series data), use mesh3d with Delaunay triangulation
|
|
158
|
+
if (chartType === 'surface') {
|
|
159
|
+
const zIs2D = Array.isArray(z) && z.length > 0 && Array.isArray(z[0]);
|
|
160
|
+
if (zIs2D) {
|
|
161
|
+
trace.z = z;
|
|
162
|
+
} else {
|
|
163
|
+
// Use mesh3d with Delaunay triangulation for 1D scattered points
|
|
164
|
+
console.info('[chart_types] Surface chart with 1D z data - using mesh3d with Delaunay triangulation');
|
|
165
|
+
trace.type = 'mesh3d';
|
|
166
|
+
trace.x = x;
|
|
167
|
+
trace.y = y;
|
|
168
|
+
trace.z = z;
|
|
169
|
+
trace.alphahull = 0; // Delaunay triangulation (convex hull = -1, 0 = Delaunay)
|
|
170
|
+
trace.opacity = 0.8;
|
|
171
|
+
trace.color = seriesColor;
|
|
172
|
+
trace.flatshading = true;
|
|
173
|
+
trace.lighting = {
|
|
174
|
+
ambient: 0.6,
|
|
175
|
+
diffuse: 0.5,
|
|
176
|
+
specular: 0.2,
|
|
177
|
+
};
|
|
178
|
+
// Skip surface-specific config below
|
|
179
|
+
return trace;
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
trace.z = z;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Phase chart: line with small markers
|
|
187
|
+
if (chartType === 'phase') {
|
|
188
|
+
trace.line = {
|
|
189
|
+
color: seriesColor,
|
|
190
|
+
width: lineWidth,
|
|
191
|
+
dash: LINE_DASH_MAP[lineStyle] || 'solid',
|
|
192
|
+
};
|
|
193
|
+
trace.marker = {
|
|
194
|
+
color: seriesColor,
|
|
195
|
+
size: 3,
|
|
196
|
+
};
|
|
197
|
+
return trace;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Line configuration (for line/area charts)
|
|
201
|
+
if (['line', 'area', 'area-stacked', 'line3d'].includes(chartType)) {
|
|
202
|
+
trace.line = {
|
|
203
|
+
color: seriesColor,
|
|
204
|
+
width: lineWidth,
|
|
205
|
+
dash: LINE_DASH_MAP[lineStyle] || 'solid',
|
|
206
|
+
shape: INTERPOLATION_MAP[interpolation] || 'linear',
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Marker configuration
|
|
211
|
+
if (['scatter', 'scatter3d'].includes(chartType) || showMarkers) {
|
|
212
|
+
trace.marker = {
|
|
213
|
+
color: seriesColor,
|
|
214
|
+
size: chartType === 'scatter3d' ? 4 : 6,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Normalized stacked area: each Y-axis is its own stack group, each summing to 1.
|
|
219
|
+
if (chartType === 'area-stacked') {
|
|
220
|
+
trace.stackgroup = yAxisId;
|
|
221
|
+
trace.groupnorm = 'fraction';
|
|
222
|
+
trace.fillcolor = hexToRgba(seriesColor, 0.5);
|
|
223
|
+
trace.line = { ...(trace.line || {}), width: 0.5, color: seriesColor };
|
|
224
|
+
trace.mode = 'lines';
|
|
225
|
+
}
|
|
226
|
+
// Fill for area charts (non-stacked)
|
|
227
|
+
else if (chartType === 'area' && !stackGroup) {
|
|
228
|
+
trace.fill = 'tozeroy';
|
|
229
|
+
trace.fillcolor = hexToRgba(seriesColor, 0.3);
|
|
230
|
+
}
|
|
231
|
+
// Stacked area via explicit stackGroup on any chart type
|
|
232
|
+
else if (stackGroup) {
|
|
233
|
+
trace.stackgroup = stackGroup;
|
|
234
|
+
trace.fillcolor = hexToRgba(seriesColor, 0.5);
|
|
235
|
+
trace.line = { ...(trace.line || {}), width: 0.5, color: seriesColor };
|
|
236
|
+
trace.mode = 'lines';
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Bar color
|
|
240
|
+
if (chartType === 'bar') {
|
|
241
|
+
trace.marker = {
|
|
242
|
+
color: seriesColor,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Surface colorscale
|
|
247
|
+
if (chartType === 'surface') {
|
|
248
|
+
trace.colorscale = 'Viridis';
|
|
249
|
+
trace.showscale = true;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Y-axis assignment (for multi-axis support)
|
|
253
|
+
if (!is3DChart(chartType) && yAxisId && yAxisId !== 'y') {
|
|
254
|
+
trace.yaxis = yAxisId;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return trace;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Build layout for Y-axis based on configuration.
|
|
262
|
+
* @param {Object} options
|
|
263
|
+
* @param {string} options.axisId - Axis identifier (y, y2, y3, etc.)
|
|
264
|
+
* @param {string} [options.title] - Axis title
|
|
265
|
+
* @param {string} [options.position] - Axis position: left or right
|
|
266
|
+
* @param {string} [options.scaleType] - Scale type: linear or log
|
|
267
|
+
* @param {number} [options.min] - Minimum value
|
|
268
|
+
* @param {number} [options.max] - Maximum value
|
|
269
|
+
* @param {number} [options.step] - Tick step size
|
|
270
|
+
* @param {boolean} [options.showGrid] - Show grid lines
|
|
271
|
+
* @param {number|null} [options.axisPosition] - Explicit position in normalized coords (0-1), null for default
|
|
272
|
+
* @returns {Object} Plotly axis layout object
|
|
273
|
+
*/
|
|
274
|
+
export function buildYAxisLayout(options) {
|
|
275
|
+
const {
|
|
276
|
+
axisId = 'yaxis',
|
|
277
|
+
title = '',
|
|
278
|
+
position = 'left',
|
|
279
|
+
scaleType = 'linear',
|
|
280
|
+
min = null,
|
|
281
|
+
max = null,
|
|
282
|
+
step = null,
|
|
283
|
+
showGrid = true,
|
|
284
|
+
axisPosition = null,
|
|
285
|
+
} = options;
|
|
286
|
+
|
|
287
|
+
const layout = {
|
|
288
|
+
title: { text: title },
|
|
289
|
+
type: scaleType === 'log' ? 'log' : 'linear',
|
|
290
|
+
side: position,
|
|
291
|
+
gridcolor: 'rgba(255,255,255,0.05)',
|
|
292
|
+
linecolor: 'rgba(255,255,255,0.08)',
|
|
293
|
+
tickcolor: 'rgba(255,255,255,0.3)',
|
|
294
|
+
zerolinecolor: 'rgba(255,255,255,0.06)',
|
|
295
|
+
showgrid: showGrid,
|
|
296
|
+
automargin: true, // Auto-expand margin for tick labels
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// Set range if min/max provided.
|
|
300
|
+
// Both bounds: fixed range (autorange off).
|
|
301
|
+
// One-sided: use autorangeoptions so Plotly auto-ranges within data but respects the cap/floor.
|
|
302
|
+
// Plotly does not accept null in a range array — [null, max] silently sets the lower bound to 0.
|
|
303
|
+
if (min !== null && max !== null) {
|
|
304
|
+
layout.autorange = false;
|
|
305
|
+
layout.range = [min, max];
|
|
306
|
+
} else if (min !== null || max !== null) {
|
|
307
|
+
layout.autorange = true;
|
|
308
|
+
layout.autorangeoptions = {};
|
|
309
|
+
if (min !== null) layout.autorangeoptions.minallowed = min;
|
|
310
|
+
if (max !== null) layout.autorangeoptions.maxallowed = max;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Set tick step if provided
|
|
314
|
+
if (step !== null && step > 0) {
|
|
315
|
+
layout.dtick = step;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// For secondary axes, overlay on the first y-axis
|
|
319
|
+
if (axisId !== 'yaxis') {
|
|
320
|
+
layout.overlaying = 'y';
|
|
321
|
+
layout.anchor = 'free';
|
|
322
|
+
|
|
323
|
+
// Use explicit position if provided (for proper multi-axis layout)
|
|
324
|
+
// axisPosition is in normalized figure coordinates (0-1)
|
|
325
|
+
if (axisPosition !== null) {
|
|
326
|
+
layout.position = axisPosition;
|
|
327
|
+
} else {
|
|
328
|
+
// Fallback: position at domain edge (0 for left, 1 for right)
|
|
329
|
+
layout.position = position === 'left' ? 0 : 1;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return layout;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Build X-axis layout.
|
|
338
|
+
* @param {Object} options
|
|
339
|
+
* @param {string} [options.title] - Axis title
|
|
340
|
+
* @param {string} [options.scaleType] - Scale type: linear or log
|
|
341
|
+
* @param {number} [options.min] - Minimum value
|
|
342
|
+
* @param {number} [options.max] - Maximum value
|
|
343
|
+
* @returns {Object} Plotly xaxis layout object
|
|
344
|
+
*/
|
|
345
|
+
export function buildXAxisLayout(options) {
|
|
346
|
+
const {
|
|
347
|
+
title = 'Time',
|
|
348
|
+
scaleType = 'linear',
|
|
349
|
+
min = null,
|
|
350
|
+
max = null,
|
|
351
|
+
} = options;
|
|
352
|
+
|
|
353
|
+
const layout = {
|
|
354
|
+
title: { text: title },
|
|
355
|
+
type: scaleType === 'log' ? 'log' : 'linear',
|
|
356
|
+
gridcolor: 'rgba(255,255,255,0.05)',
|
|
357
|
+
linecolor: 'rgba(255,255,255,0.08)',
|
|
358
|
+
tickcolor: 'rgba(255,255,255,0.3)',
|
|
359
|
+
zerolinecolor: 'rgba(255,255,255,0.06)',
|
|
360
|
+
automargin: true,
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
if (min !== null || max !== null) {
|
|
364
|
+
layout.range = [min, max];
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return layout;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Build 3D scene layout.
|
|
372
|
+
* @param {Object} options
|
|
373
|
+
* @param {string} [options.xTitle] - X-axis title
|
|
374
|
+
* @param {string} [options.yTitle] - Y-axis title
|
|
375
|
+
* @param {string} [options.zTitle] - Z-axis title
|
|
376
|
+
* @param {string} [options.zScale] - Z-axis scale type: 'linear' or 'log'
|
|
377
|
+
* @param {number} [options.zMin] - Z-axis minimum value
|
|
378
|
+
* @param {number} [options.zMax] - Z-axis maximum value
|
|
379
|
+
* @param {number} [options.zStep] - Z-axis tick step size
|
|
380
|
+
* @returns {Object} Plotly scene layout object
|
|
381
|
+
*/
|
|
382
|
+
export function build3DSceneLayout(options) {
|
|
383
|
+
const {
|
|
384
|
+
xTitle = 'X',
|
|
385
|
+
yTitle = 'Y',
|
|
386
|
+
zTitle = 'Z',
|
|
387
|
+
zScale = 'linear',
|
|
388
|
+
zMin = null,
|
|
389
|
+
zMax = null,
|
|
390
|
+
zStep = null,
|
|
391
|
+
} = options;
|
|
392
|
+
|
|
393
|
+
const zaxis = {
|
|
394
|
+
title: { text: zTitle },
|
|
395
|
+
type: zScale === 'log' ? 'log' : 'linear',
|
|
396
|
+
gridcolor: 'rgba(255,255,255,0.05)',
|
|
397
|
+
linecolor: 'rgba(255,255,255,0.08)',
|
|
398
|
+
backgroundcolor: 'rgba(0,0,0,0)',
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
// Set range if min/max provided
|
|
402
|
+
if (zMin !== null || zMax !== null) {
|
|
403
|
+
zaxis.range = [zMin, zMax];
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Set tick step if provided
|
|
407
|
+
if (zStep !== null && zStep > 0) {
|
|
408
|
+
zaxis.dtick = zStep;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return {
|
|
412
|
+
xaxis: {
|
|
413
|
+
title: { text: xTitle },
|
|
414
|
+
gridcolor: 'rgba(255,255,255,0.05)',
|
|
415
|
+
linecolor: 'rgba(255,255,255,0.08)',
|
|
416
|
+
backgroundcolor: 'rgba(0,0,0,0)',
|
|
417
|
+
},
|
|
418
|
+
yaxis: {
|
|
419
|
+
title: { text: yTitle },
|
|
420
|
+
gridcolor: 'rgba(255,255,255,0.05)',
|
|
421
|
+
linecolor: 'rgba(255,255,255,0.08)',
|
|
422
|
+
backgroundcolor: 'rgba(0,0,0,0)',
|
|
423
|
+
},
|
|
424
|
+
zaxis,
|
|
425
|
+
bgcolor: 'rgba(0,0,0,0)',
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Build legend layout.
|
|
431
|
+
* @param {string} position - Legend position: top, bottom, left, right, hidden
|
|
432
|
+
* @returns {Object} Plotly legend layout
|
|
433
|
+
*/
|
|
434
|
+
export function buildLegendLayout(position) {
|
|
435
|
+
if (position === 'hidden') {
|
|
436
|
+
return { showlegend: false };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const baseLayout = {
|
|
440
|
+
showlegend: true,
|
|
441
|
+
bgcolor: 'rgba(0,0,0,0)',
|
|
442
|
+
font: { color: '#cccccc' },
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
switch (position) {
|
|
446
|
+
case 'top':
|
|
447
|
+
return {
|
|
448
|
+
...baseLayout,
|
|
449
|
+
legend: {
|
|
450
|
+
...baseLayout,
|
|
451
|
+
orientation: 'h',
|
|
452
|
+
y: 1.1,
|
|
453
|
+
yanchor: 'bottom',
|
|
454
|
+
x: 0.5,
|
|
455
|
+
xanchor: 'center',
|
|
456
|
+
},
|
|
457
|
+
};
|
|
458
|
+
case 'bottom':
|
|
459
|
+
return {
|
|
460
|
+
...baseLayout,
|
|
461
|
+
legend: {
|
|
462
|
+
...baseLayout,
|
|
463
|
+
orientation: 'h',
|
|
464
|
+
y: -0.15,
|
|
465
|
+
yanchor: 'top',
|
|
466
|
+
x: 0.5,
|
|
467
|
+
xanchor: 'center',
|
|
468
|
+
},
|
|
469
|
+
};
|
|
470
|
+
case 'left':
|
|
471
|
+
return {
|
|
472
|
+
...baseLayout,
|
|
473
|
+
legend: {
|
|
474
|
+
...baseLayout,
|
|
475
|
+
orientation: 'v',
|
|
476
|
+
x: -0.15,
|
|
477
|
+
xanchor: 'right',
|
|
478
|
+
y: 0.5,
|
|
479
|
+
yanchor: 'middle',
|
|
480
|
+
},
|
|
481
|
+
};
|
|
482
|
+
case 'right':
|
|
483
|
+
default:
|
|
484
|
+
return {
|
|
485
|
+
...baseLayout,
|
|
486
|
+
legend: {
|
|
487
|
+
...baseLayout,
|
|
488
|
+
orientation: 'v',
|
|
489
|
+
x: 1.02,
|
|
490
|
+
xanchor: 'left',
|
|
491
|
+
y: 0.5,
|
|
492
|
+
yanchor: 'middle',
|
|
493
|
+
},
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Build Plotly layout shapes + annotations for reference lines and shaded x-bands.
|
|
500
|
+
*
|
|
501
|
+
* @param {Object} opts
|
|
502
|
+
* @param {Array<{yAxisId?:string, value:number, label?:string, color?:string, lineStyle?:string, lineWidth?:number, labelX?:number}>} [opts.referenceLines]
|
|
503
|
+
* Horizontal threshold lines drawn across the plot, anchored to a specific Y axis.
|
|
504
|
+
* `yAxisId` matches an axis id in sp.yAxes[].id; omitted → first axis.
|
|
505
|
+
* @param {Array<{x0:number, x1:number, label?:string, color?:string, opacity?:number, labelY?:number, labelColor?:string}>} [opts.shadedBands]
|
|
506
|
+
* Vertical stripes (rectangles spanning full y range) to mark policy/recession periods.
|
|
507
|
+
* @param {Object<string,string>} [opts.yRefByAxisId]
|
|
508
|
+
* Mapping from subplot yAxes[i].id → Plotly yref ('y', 'y2', ...).
|
|
509
|
+
* @returns {{shapes: Array, annotations: Array}}
|
|
510
|
+
*/
|
|
511
|
+
export function buildOverlayShapes(opts = {}) {
|
|
512
|
+
const { referenceLines = [], shadedBands = [], yRefByAxisId = {} } = opts;
|
|
513
|
+
const shapes = [];
|
|
514
|
+
const annotations = [];
|
|
515
|
+
const dashMap = { solid: 'solid', dashed: 'dash', dotted: 'dot', dashdot: 'dashdot' };
|
|
516
|
+
|
|
517
|
+
for (const band of shadedBands) {
|
|
518
|
+
if (band.x0 == null || band.x1 == null) continue;
|
|
519
|
+
shapes.push({
|
|
520
|
+
type: 'rect',
|
|
521
|
+
xref: 'x',
|
|
522
|
+
yref: 'paper',
|
|
523
|
+
x0: band.x0,
|
|
524
|
+
x1: band.x1,
|
|
525
|
+
y0: 0,
|
|
526
|
+
y1: 1,
|
|
527
|
+
fillcolor: band.color || 'rgba(255, 200, 0, 0.08)',
|
|
528
|
+
line: { width: 0 },
|
|
529
|
+
opacity: band.opacity ?? 1,
|
|
530
|
+
layer: 'below',
|
|
531
|
+
});
|
|
532
|
+
if (band.label) {
|
|
533
|
+
annotations.push({
|
|
534
|
+
x: (band.x0 + band.x1) / 2,
|
|
535
|
+
y: band.labelY ?? 1.0,
|
|
536
|
+
xref: 'x',
|
|
537
|
+
yref: 'paper',
|
|
538
|
+
text: band.label,
|
|
539
|
+
showarrow: false,
|
|
540
|
+
font: { size: 9, color: band.labelColor || 'rgba(255,255,255,0.55)' },
|
|
541
|
+
xanchor: 'center',
|
|
542
|
+
yanchor: 'bottom',
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const firstYRef = Object.values(yRefByAxisId)[0] || 'y';
|
|
548
|
+
for (const ref of referenceLines) {
|
|
549
|
+
if (ref.value == null || !Number.isFinite(ref.value)) continue;
|
|
550
|
+
const yRef = (ref.yAxisId && yRefByAxisId[ref.yAxisId]) || firstYRef;
|
|
551
|
+
const color = ref.color || 'rgba(255, 120, 120, 0.75)';
|
|
552
|
+
shapes.push({
|
|
553
|
+
type: 'line',
|
|
554
|
+
xref: 'paper',
|
|
555
|
+
yref: yRef,
|
|
556
|
+
x0: 0,
|
|
557
|
+
x1: 1,
|
|
558
|
+
y0: ref.value,
|
|
559
|
+
y1: ref.value,
|
|
560
|
+
line: {
|
|
561
|
+
color,
|
|
562
|
+
width: ref.lineWidth ?? 1.5,
|
|
563
|
+
dash: dashMap[ref.lineStyle || 'dashed'] || 'dash',
|
|
564
|
+
},
|
|
565
|
+
layer: 'above',
|
|
566
|
+
});
|
|
567
|
+
if (ref.label) {
|
|
568
|
+
annotations.push({
|
|
569
|
+
x: ref.labelX ?? 0.015,
|
|
570
|
+
y: ref.value,
|
|
571
|
+
xref: 'paper',
|
|
572
|
+
yref: yRef,
|
|
573
|
+
text: ref.label,
|
|
574
|
+
showarrow: false,
|
|
575
|
+
font: { size: 10, color },
|
|
576
|
+
xanchor: 'left',
|
|
577
|
+
yanchor: 'bottom',
|
|
578
|
+
bgcolor: 'rgba(20,22,26,0.6)',
|
|
579
|
+
borderpad: 2,
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return { shapes, annotations };
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Create a fan chart trace with confidence bands.
|
|
589
|
+
* @param {Object} options
|
|
590
|
+
* @param {Array} options.x - X-axis data (time)
|
|
591
|
+
* @param {Array} options.upper - Upper bound data
|
|
592
|
+
* @param {Array} options.lower - Lower bound data
|
|
593
|
+
* @param {string} options.color - Band color
|
|
594
|
+
* @param {number} options.opacity - Fill opacity
|
|
595
|
+
* @param {string} [options.name] - Band name for legend
|
|
596
|
+
* @returns {Array} Array of two traces (lower bound + fill to upper)
|
|
597
|
+
*/
|
|
598
|
+
export function buildFanBandTraces(options) {
|
|
599
|
+
const { x, upper, lower, color, opacity = 0.2, name = '' } = options;
|
|
600
|
+
|
|
601
|
+
// Lower bound line (invisible, for fill reference)
|
|
602
|
+
const lowerTrace = {
|
|
603
|
+
type: 'scatter',
|
|
604
|
+
x,
|
|
605
|
+
y: lower,
|
|
606
|
+
mode: 'lines',
|
|
607
|
+
line: { width: 0 },
|
|
608
|
+
showlegend: false,
|
|
609
|
+
hoverinfo: 'skip',
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
// Upper bound with fill to lower
|
|
613
|
+
const upperTrace = {
|
|
614
|
+
type: 'scatter',
|
|
615
|
+
x,
|
|
616
|
+
y: upper,
|
|
617
|
+
mode: 'lines',
|
|
618
|
+
line: { width: 0 },
|
|
619
|
+
fill: 'tonexty',
|
|
620
|
+
fillcolor: hexToRgba(color, opacity),
|
|
621
|
+
name: name || `${(opacity * 100).toFixed(0)}% Band`,
|
|
622
|
+
showlegend: !!name,
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
return [lowerTrace, upperTrace];
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Create a histogram trace.
|
|
630
|
+
* @param {Object} options
|
|
631
|
+
* @param {Array} options.x - Data values
|
|
632
|
+
* @param {number} options.nbins - Number of bins
|
|
633
|
+
* @param {string} options.color - Bar color
|
|
634
|
+
* @param {string} [options.name] - Series name
|
|
635
|
+
* @returns {Object} Plotly histogram trace
|
|
636
|
+
*/
|
|
637
|
+
export function buildHistogramTrace(options) {
|
|
638
|
+
const { x, nbins = 20, color, name = 'Distribution' } = options;
|
|
639
|
+
|
|
640
|
+
return {
|
|
641
|
+
type: 'histogram',
|
|
642
|
+
x,
|
|
643
|
+
nbinsx: nbins,
|
|
644
|
+
marker: { color },
|
|
645
|
+
name,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Create a box plot trace.
|
|
651
|
+
* @param {Object} options
|
|
652
|
+
* @param {Array} options.y - Data values
|
|
653
|
+
* @param {string} [options.x] - Category/time label
|
|
654
|
+
* @param {string} options.color - Box color
|
|
655
|
+
* @param {string} [options.name] - Series name
|
|
656
|
+
* @returns {Object} Plotly box trace
|
|
657
|
+
*/
|
|
658
|
+
export function buildBoxTrace(options) {
|
|
659
|
+
const { y, x = null, color, name = '' } = options;
|
|
660
|
+
|
|
661
|
+
const trace = {
|
|
662
|
+
type: 'box',
|
|
663
|
+
y,
|
|
664
|
+
marker: { color },
|
|
665
|
+
name,
|
|
666
|
+
boxpoints: false, // Don't show individual points
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
if (x !== null) {
|
|
670
|
+
trace.x = Array.isArray(x) ? x : [x];
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
return trace;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Create a heatmap trace.
|
|
678
|
+
* @param {Object} options
|
|
679
|
+
* @param {Array} options.z - 2D array of values
|
|
680
|
+
* @param {Array} [options.x] - X labels
|
|
681
|
+
* @param {Array} [options.y] - Y labels
|
|
682
|
+
* @param {string} [options.colorscale] - Plotly colorscale name
|
|
683
|
+
* @returns {Object} Plotly heatmap trace
|
|
684
|
+
*/
|
|
685
|
+
export function buildHeatmapTrace(options) {
|
|
686
|
+
const { z, x = null, y = null, colorscale = 'RdBu' } = options;
|
|
687
|
+
|
|
688
|
+
const trace = {
|
|
689
|
+
type: 'heatmap',
|
|
690
|
+
z,
|
|
691
|
+
colorscale,
|
|
692
|
+
showscale: true,
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
if (x) trace.x = x;
|
|
696
|
+
if (y) trace.y = y;
|
|
697
|
+
|
|
698
|
+
return trace;
|
|
699
|
+
}
|