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.
Files changed (252) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/LICENSE +0 -0
  3. package/README.md +2217 -42
  4. package/THIRD_PARTY_LICENSES.md +0 -0
  5. package/css/base.css +1834 -75
  6. package/css/flexdesk.css +3146 -164
  7. package/css/overrides.css +16 -0
  8. package/css/reset.css +0 -0
  9. package/css/tokens.css +0 -0
  10. package/dist/canvas.js +6 -239
  11. package/dist/canvas.js.map +3 -3
  12. package/dist/charts.js +709 -18
  13. package/dist/charts.js.map +4 -4
  14. package/dist/chunk-3PHCPZHT.js +0 -0
  15. package/dist/chunk-3PHCPZHT.js.map +0 -0
  16. package/dist/chunk-4A7SJC3T.js +503 -0
  17. package/dist/chunk-4A7SJC3T.js.map +7 -0
  18. package/dist/{chunk-6JSOVNID.js → chunk-CVMCZZX2.js} +635 -10
  19. package/dist/chunk-CVMCZZX2.js.map +7 -0
  20. package/dist/chunk-D3COLZZ2.js +1311 -0
  21. package/dist/chunk-D3COLZZ2.js.map +7 -0
  22. package/dist/chunk-FOOS3T5L.js +0 -0
  23. package/dist/chunk-FOOS3T5L.js.map +0 -0
  24. package/dist/{chunk-SN4LSOAR.js → chunk-GOQLR5WR.js} +452 -2
  25. package/dist/chunk-GOQLR5WR.js.map +7 -0
  26. package/dist/{chunk-FL5KFNQH.js → chunk-HNU6MM2B.js} +52 -17
  27. package/dist/chunk-HNU6MM2B.js.map +7 -0
  28. package/dist/chunk-HYJYYEPK.js +375 -0
  29. package/dist/chunk-HYJYYEPK.js.map +7 -0
  30. package/dist/chunk-JKZLNKG6.js +2041 -0
  31. package/dist/chunk-JKZLNKG6.js.map +7 -0
  32. package/dist/chunk-JYWURG5T.js +0 -0
  33. package/dist/chunk-JYWURG5T.js.map +0 -0
  34. package/dist/{chunk-RQM5Z2MU.js → chunk-MCIXFNVF.js} +633 -311
  35. package/dist/chunk-MCIXFNVF.js.map +7 -0
  36. package/dist/chunk-N7HC3ADV.js +30 -0
  37. package/dist/chunk-N7HC3ADV.js.map +7 -0
  38. package/dist/{chunk-FBGP5KN5.js → chunk-Q36EPT2R.js} +3268 -203
  39. package/dist/chunk-Q36EPT2R.js.map +7 -0
  40. package/dist/chunk-QWQJUDPD.js +610 -0
  41. package/dist/chunk-QWQJUDPD.js.map +7 -0
  42. package/dist/{chunk-NLTOY57M.js → chunk-UURZ37SD.js} +28 -48
  43. package/dist/chunk-UURZ37SD.js.map +7 -0
  44. package/dist/{chunk-B5Q53LAJ.js → chunk-UWO43BZU.js} +2 -2
  45. package/dist/chunk-UWO43BZU.js.map +7 -0
  46. package/dist/chunk-WVFGV5FT.js +0 -0
  47. package/dist/chunk-WVFGV5FT.js.map +0 -0
  48. package/dist/core.js +170 -118
  49. package/dist/core.js.map +2 -2
  50. package/dist/editor.js +156 -70
  51. package/dist/editor.js.map +2 -2
  52. package/dist/flexdesk.css +3146 -164
  53. package/dist/flow.js +2789 -306
  54. package/dist/flow.js.map +4 -4
  55. package/dist/host.js +109 -3
  56. package/dist/host.js.map +3 -3
  57. package/dist/reset.css +0 -0
  58. package/dist/tile_registry-6WZPTUZV.js +0 -0
  59. package/dist/tile_registry-6WZPTUZV.js.map +0 -0
  60. package/dist/tiles.js +657 -234
  61. package/dist/tiles.js.map +4 -4
  62. package/dist/tokens.css +0 -0
  63. package/dist/widgets.js +3252 -2124
  64. package/dist/widgets.js.map +4 -4
  65. package/dist/wm.js +1227 -347
  66. package/dist/wm.js.map +4 -4
  67. package/package.json +3 -1
  68. package/src/canvas/canvas_adapter.js +5 -0
  69. package/src/canvas/edge_router.js +0 -0
  70. package/src/canvas/graph/connector_router.js +0 -0
  71. package/src/canvas/graph/history_service.js +0 -0
  72. package/src/canvas/graph/node_platform.js +0 -0
  73. package/src/canvas/index.js +1 -0
  74. package/src/canvas/layered_layout.js +244 -0
  75. package/src/charts/chart_types.js +0 -0
  76. package/src/charts/data_series_plot_window.js +0 -0
  77. package/src/charts/downsample.js +2 -2
  78. package/src/charts/plot_config.js +53 -29
  79. package/src/charts/plot_config_editor.js +627 -0
  80. package/src/charts/plot_grid.js +449 -0
  81. package/src/charts/plot_popout_window.js +103 -317
  82. package/src/charts/plotly_wrapper.js +22 -2
  83. package/src/charts/series_transforms.js +206 -0
  84. package/src/core/event_bus.js +0 -0
  85. package/src/core/logging.js +1 -1
  86. package/src/core/settings.js +105 -19
  87. package/src/core/state_guard.js +93 -51
  88. package/src/core/state_guard_config.js +85 -74
  89. package/src/core/state_machine.js +0 -0
  90. package/src/editor/editor_pane.js +3 -3
  91. package/src/editor/monaco_editor_factory.js +124 -60
  92. package/src/editor/monaco_loader.js +103 -28
  93. package/src/editor/notebook_search.js +0 -0
  94. package/src/editor/notebook_tab_bar.js +38 -52
  95. package/src/editor/notebook_undo_manager.js +0 -0
  96. package/src/editor/split_pane_container.js +6 -3
  97. package/src/flow/canvas_editor/connections.js +90 -0
  98. package/src/flow/canvas_editor/editor.js +2351 -153
  99. package/src/flow/canvas_editor/index.js +2 -1
  100. package/src/flow/canvas_editor/layout.js +119 -12
  101. package/src/flow/canvas_editor/strings.js +80 -0
  102. package/src/flow/kit/always_before.js +0 -0
  103. package/src/flow/kit/catalogue.js +97 -3
  104. package/src/flow/kit/chip_input.js +0 -0
  105. package/src/flow/kit/dom.js +0 -0
  106. package/src/flow/kit/findings.js +0 -0
  107. package/src/flow/kit/history.js +0 -0
  108. package/src/flow/kit/index.js +3 -3
  109. package/src/flow/kit/keys.js +7 -1
  110. package/src/flow/kit/logic_graph.js +97 -15
  111. package/src/flow/kit/references.js +0 -0
  112. package/src/flow/kit/settings_panel.js +0 -0
  113. package/src/flow/kit/settings_schema.js +2 -1
  114. package/src/flow/kit/step_picker.js +0 -0
  115. package/src/flow/kit/strings.js +3 -1
  116. package/src/flow/kit/value_picker.js +0 -0
  117. package/src/flow/kit/widgets.js +86 -109
  118. package/src/flow/lanes/editor.js +136 -14
  119. package/src/flow/lanes/index.js +3 -1
  120. package/src/flow/lanes/layout.js +0 -0
  121. package/src/flow/lanes/pipeline.js +44 -2
  122. package/src/flow/lanes/preview.js +0 -0
  123. package/src/flow/lanes/strings.js +1 -0
  124. package/src/flow/outline/editor.js +0 -0
  125. package/src/flow/outline/index.js +0 -0
  126. package/src/flow/outline/mapping.js +0 -0
  127. package/src/flow/outline/menu.js +0 -0
  128. package/src/flow/outline/operations.js +0 -0
  129. package/src/flow/outline/recognise.js +0 -0
  130. package/src/flow/outline/strings.js +0 -0
  131. package/src/help/help_modal.js +168 -42
  132. package/src/help/help_registry.js +12 -1
  133. package/src/host/external_links.js +0 -0
  134. package/src/host/file_picker.js +117 -0
  135. package/src/host/host.js +10 -2
  136. package/src/host/photino_host.js +9 -0
  137. package/src/host/photino_host.md +21 -3
  138. package/src/host/pywebview_host.js +10 -0
  139. package/src/tiles/chart_tiles.js +53 -245
  140. package/src/tiles/config_schema.js +8 -0
  141. package/src/tiles/figure_tile.js +322 -0
  142. package/src/tiles/layout_persistence.js +3 -3
  143. package/src/tiles/list_tiles.js +7 -7
  144. package/src/tiles/scope_picker.js +0 -0
  145. package/src/tiles/tile_base.js +1 -1
  146. package/src/tiles/tile_config_form.js +442 -0
  147. package/src/tiles/tile_grid.js +2 -0
  148. package/src/tiles/tile_registry.js +0 -0
  149. package/src/tiling/bottom_panel_tabs.js +67 -99
  150. package/src/tiling/command_palette.js +64 -19
  151. package/src/tiling/content_registry.js +4 -1
  152. package/src/tiling/desktops.js +11 -1
  153. package/src/tiling/entity_sources.js +0 -0
  154. package/src/tiling/keymap.js +403 -128
  155. package/src/tiling/kind_taxonomy.js +78 -0
  156. package/src/tiling/landing_table.js +0 -0
  157. package/src/tiling/loading_overlay.js +0 -0
  158. package/src/tiling/nav_panel.js +59 -10
  159. package/src/tiling/page_factory.js +5 -5
  160. package/src/tiling/panel_availability.js +49 -0
  161. package/src/tiling/panel_keys.js +0 -0
  162. package/src/tiling/shell.js +114 -12
  163. package/src/tiling/tab_strip.js +0 -0
  164. package/src/tiling/tile_breadcrumb.js +0 -0
  165. package/src/tiling/tile_renderer.js +105 -9
  166. package/src/tiling/tile_tab_menu.js +0 -0
  167. package/src/tiling/tile_tree.js +0 -0
  168. package/src/tiling/wm.js +562 -30
  169. package/src/tiling/zoom.js +0 -0
  170. package/src/ui/base/component_base.js +0 -0
  171. package/src/ui/base/controller_base.js +0 -0
  172. package/src/ui/base/page_base.js +0 -0
  173. package/src/ui/components/about_dialog.js +0 -0
  174. package/src/ui/components/action_dropdown.js +30 -2
  175. package/src/ui/components/alert_log.js +0 -0
  176. package/src/ui/components/alerts_pane.js +0 -0
  177. package/src/ui/components/attribute_list_editor.js +0 -0
  178. package/src/ui/components/autocomplete_field.js +0 -0
  179. package/src/ui/components/cell_editor_host.js +211 -0
  180. package/src/ui/components/cell_grid.js +648 -0
  181. package/src/ui/components/cell_selection.js +220 -0
  182. package/src/ui/components/choice_controls.js +351 -0
  183. package/src/ui/components/color_field.js +211 -0
  184. package/src/ui/components/color_list_field.js +210 -0
  185. package/src/ui/components/column_fit.js +0 -0
  186. package/src/ui/components/column_groups.js +314 -0
  187. package/src/ui/components/column_order.js +142 -0
  188. package/src/ui/components/computing_status_window.js +98 -25
  189. package/src/ui/components/confirm_dialog.js +81 -38
  190. package/src/ui/components/context_menu.js +2 -2
  191. package/src/ui/components/conversation_view.js +440 -0
  192. package/src/ui/components/crosstab.js +289 -0
  193. package/src/ui/components/data_table.js +601 -62
  194. package/src/ui/components/detail_header.js +22 -11
  195. package/src/ui/components/document_outline.js +397 -0
  196. package/src/ui/components/drag_reorder.js +0 -0
  197. package/src/ui/components/fact_table.js +0 -0
  198. package/src/ui/components/fill_handle.js +322 -0
  199. package/src/ui/components/gallery_picker.js +19 -8
  200. package/src/ui/components/header_actions.js +55 -0
  201. package/src/ui/components/inline_renamer.js +0 -0
  202. package/src/ui/components/job_progress.js +51 -0
  203. package/src/ui/components/managed_window.js +27 -4
  204. package/src/ui/components/menu_bar.js +745 -0
  205. package/src/ui/components/modal.js +449 -75
  206. package/src/ui/components/notification_history.js +3 -3
  207. package/src/ui/components/progress.js +483 -0
  208. package/src/ui/components/record_list.js +0 -0
  209. package/src/ui/components/row_activation.js +0 -0
  210. package/src/ui/components/section_stack.js +298 -0
  211. package/src/ui/components/series.js +173 -0
  212. package/src/ui/components/settings_view.js +634 -0
  213. package/src/ui/components/slide_out_panel.js +43 -6
  214. package/src/ui/components/slider_field.js +275 -18
  215. package/src/ui/components/sortable_list.js +0 -0
  216. package/src/ui/components/staged_edits.js +343 -0
  217. package/src/ui/components/staged_history.js +172 -0
  218. package/src/ui/components/table_filter_chips.js +0 -0
  219. package/src/ui/components/table_state_store.js +0 -0
  220. package/src/ui/components/table_structure.js +182 -0
  221. package/src/ui/components/table_values.js +0 -0
  222. package/src/ui/components/tabs.js +497 -0
  223. package/src/ui/components/tag_input.js +215 -0
  224. package/src/ui/components/toast.js +25 -8
  225. package/src/ui/components/toggle_field.js +175 -0
  226. package/src/ui/components/tree_view.js +55 -34
  227. package/src/ui/components/window_taskbar.js +0 -0
  228. package/src/ui/components/wizard.js +563 -0
  229. package/src/ui/controllers/window_chrome_controller.js +2 -2
  230. package/src/ui/notification_center.js +375 -119
  231. package/src/ui/utils/auto_scrollbars.js +0 -0
  232. package/src/ui/utils/markdown.js +709 -0
  233. package/src/ui/utils/native_context_menu.js +0 -0
  234. package/src/ui/utils/overlay_scrollbar.js +16 -12
  235. package/src/ui/utils/raf_resize_observer.js +0 -0
  236. package/src/ui/utils/scroll_lock.js +0 -0
  237. package/src/ui/utils/strip_scroll.js +55 -0
  238. package/src/ui/utils/tooltip_service.js +0 -8
  239. package/dist/chunk-6JSOVNID.js.map +0 -7
  240. package/dist/chunk-B5Q53LAJ.js.map +0 -7
  241. package/dist/chunk-FBGP5KN5.js.map +0 -7
  242. package/dist/chunk-FL5KFNQH.js.map +0 -7
  243. package/dist/chunk-NLTOY57M.js.map +0 -7
  244. package/dist/chunk-NNFTETK3.js +0 -633
  245. package/dist/chunk-NNFTETK3.js.map +0 -7
  246. package/dist/chunk-O5OHMWBB.js +0 -284
  247. package/dist/chunk-O5OHMWBB.js.map +0 -7
  248. package/dist/chunk-RQM5Z2MU.js.map +0 -7
  249. package/dist/chunk-SN4LSOAR.js.map +0 -7
  250. package/dist/chunk-W6FBJLPW.js +0 -367
  251. package/dist/chunk-W6FBJLPW.js.map +0 -7
  252. package/src/ui/controllers/panel_state_machine.js +0 -634
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  DataTable,
3
3
  createRafResizeObserver
4
- } from "./chunk-FBGP5KN5.js";
4
+ } from "./chunk-Q36EPT2R.js";
5
5
  import {
6
6
  ManagedWindow
7
- } from "./chunk-6JSOVNID.js";
7
+ } from "./chunk-CVMCZZX2.js";
8
8
 
9
9
  // src/charts/plotly_wrapper.js
10
10
  var _plotlySrc = null;
@@ -96,6 +96,7 @@ var DARK_THEME_LAYOUT = {
96
96
  bgcolor: "rgba(0,0,0,0)"
97
97
  }
98
98
  };
99
+ var DEFAULT_CHART_EXPORT_FILENAME = "chart";
99
100
  var DEFAULT_CONFIG = {
100
101
  responsive: true,
101
102
  displaylogo: false,
@@ -105,10 +106,16 @@ var DEFAULT_CONFIG = {
105
106
  // Allow zoom with scroll wheel instead
106
107
  toImageButtonOptions: {
107
108
  format: "png",
108
- filename: "ecosim_chart",
109
+ filename: DEFAULT_CHART_EXPORT_FILENAME,
109
110
  scale: 2
110
111
  }
111
112
  };
113
+ function setChartExportFilename(filename = DEFAULT_CHART_EXPORT_FILENAME) {
114
+ DEFAULT_CONFIG.toImageButtonOptions.filename = typeof filename === "string" && filename ? filename : DEFAULT_CHART_EXPORT_FILENAME;
115
+ }
116
+ function getChartExportFilename() {
117
+ return DEFAULT_CONFIG.toImageButtonOptions.filename;
118
+ }
112
119
  async function ensurePlotly() {
113
120
  if (typeof Plotly !== "undefined") {
114
121
  return Plotly;
@@ -1071,6 +1078,29 @@ var PLOT_LAYOUTS = Object.freeze({
1071
1078
  "wide-top": { label: "1 + 2", slots: 3, grid: { columns: "1fr 1fr", rows: "1fr 1fr" }, areas: [{ slot: 0, gridArea: "1/1/2/3" }, { slot: 1, gridArea: "2/1/3/2" }, { slot: 2, gridArea: "2/2/3/3" }] },
1072
1079
  "wide-bottom": { label: "2 + 1", slots: 3, grid: { columns: "1fr 1fr", rows: "1fr 1fr" }, areas: [{ slot: 0, gridArea: "1/1/2/2" }, { slot: 1, gridArea: "1/2/2/3" }, { slot: 2, gridArea: "2/1/3/3" }] }
1073
1080
  });
1081
+ function makeDefaultYAxis(index = 0) {
1082
+ return {
1083
+ id: uid("y"),
1084
+ label: "",
1085
+ scale: "linear",
1086
+ position: index % 2 === 0 ? "left" : "right",
1087
+ min: null,
1088
+ max: null,
1089
+ step: null,
1090
+ series: []
1091
+ };
1092
+ }
1093
+ function makeDefaultSeries(index = 0) {
1094
+ return {
1095
+ id: uid("s"),
1096
+ variable: "",
1097
+ label: "",
1098
+ color: getSeriesColor(index),
1099
+ lineWidth: 2,
1100
+ lineStyle: "solid",
1101
+ stackGroup: null
1102
+ };
1103
+ }
1074
1104
  function makeDefaultSubplot(index) {
1075
1105
  return {
1076
1106
  id: uid("sp"),
@@ -1080,21 +1110,9 @@ function makeDefaultSubplot(index) {
1080
1110
  interpolation: "linear",
1081
1111
  nanHandling: "gap",
1082
1112
  showDataPoints: false,
1083
- showHpTrend: false,
1084
- showHpCycle: false,
1085
- hpLambda: 1600,
1086
1113
  xAxis: { useTime: true, variable: "" },
1087
1114
  zAxis: null,
1088
- yAxes: [{
1089
- id: uid("y"),
1090
- label: "",
1091
- scale: "linear",
1092
- position: "left",
1093
- min: null,
1094
- max: null,
1095
- step: null,
1096
- series: []
1097
- }]
1115
+ yAxes: [makeDefaultYAxis(0)]
1098
1116
  };
1099
1117
  }
1100
1118
  function migrateData(data) {
@@ -1139,26 +1157,499 @@ function migrateSingleChart(data) {
1139
1157
  nanHandling: data?.nanHandling ?? "gap",
1140
1158
  showDataPoints: data?.showDataPoints ?? false,
1141
1159
  yAxes: [{
1142
- id: uid("y"),
1160
+ ...makeDefaultYAxis(0),
1143
1161
  label: data?.yLabel ?? "",
1144
- scale: "linear",
1145
- position: "left",
1146
- min: null,
1147
- max: null,
1148
- series: series.map((s, i) => ({
1149
- id: s.id ?? uid("s"),
1150
- variable: s.variable ?? "",
1151
- label: s.label ?? "",
1152
- color: s.color ?? getSeriesColor(i),
1153
- lineWidth: s.lineWidth ?? 2,
1154
- lineStyle: s.lineStyle ?? "solid",
1155
- stackGroup: s.stackGroup ?? null,
1156
- ...s.overlay ? { overlay: s.overlay } : {}
1157
- }))
1162
+ series: series.map((s, i) => {
1163
+ const d = makeDefaultSeries(i);
1164
+ return {
1165
+ id: s.id ?? d.id,
1166
+ variable: s.variable ?? d.variable,
1167
+ label: s.label ?? d.label,
1168
+ color: s.color ?? d.color,
1169
+ lineWidth: s.lineWidth ?? d.lineWidth,
1170
+ lineStyle: s.lineStyle ?? d.lineStyle,
1171
+ stackGroup: s.stackGroup ?? d.stackGroup,
1172
+ ...s.overlay ? { overlay: s.overlay } : {}
1173
+ };
1174
+ })
1158
1175
  }]
1159
1176
  };
1160
1177
  }
1161
1178
 
1179
+ // src/charts/series_transforms.js
1180
+ function resolveParams(transform, state) {
1181
+ const params = {};
1182
+ for (const [name, spec] of Object.entries(transform.params ?? {})) {
1183
+ params[name] = spec?.default;
1184
+ }
1185
+ return { ...params, ...state?.params ?? {} };
1186
+ }
1187
+ function enabledOutputs(transform, state) {
1188
+ return Object.keys(transform.outputs ?? {}).filter((name) => state?.outputs?.[name] === true);
1189
+ }
1190
+ function isEligible(trace) {
1191
+ const type = trace.type ?? "scatter";
1192
+ return (type === "scatter" || type === "scattergl") && Array.isArray(trace.y);
1193
+ }
1194
+ async function deriveSeriesTraces(traces, subplot, seriesTransforms, { onError } = {}) {
1195
+ const derived = [];
1196
+ if (!seriesTransforms) return derived;
1197
+ const sources = traces.filter(isEligible);
1198
+ for (const [transformId, transform] of Object.entries(seriesTransforms)) {
1199
+ const state = subplot?.transforms?.[transformId];
1200
+ const outputs = enabledOutputs(transform, state);
1201
+ if (!outputs.length || typeof transform?.run !== "function") continue;
1202
+ const params = resolveParams(transform, state);
1203
+ for (const source of sources) {
1204
+ let result;
1205
+ try {
1206
+ result = await transform.run(source.y, params);
1207
+ } catch (error) {
1208
+ onError?.({ transformId, traceName: source.name, error });
1209
+ continue;
1210
+ }
1211
+ if (!result || typeof result !== "object") {
1212
+ onError?.({
1213
+ transformId,
1214
+ traceName: source.name,
1215
+ error: new Error(`transform "${transformId}" resolved no outputs`)
1216
+ });
1217
+ continue;
1218
+ }
1219
+ const color = source.line?.color || source.marker?.color;
1220
+ for (const name of outputs) {
1221
+ const y = result[name];
1222
+ if (!Array.isArray(y)) continue;
1223
+ const spec = transform.outputs[name];
1224
+ derived.push({
1225
+ ...buildTrace({
1226
+ chartType: "line",
1227
+ x: source.x,
1228
+ y,
1229
+ name: `${source.name}${spec.suffix ?? ` (${spec.label ?? name})`}`,
1230
+ color,
1231
+ lineWidth: spec.lineWidth ?? 2,
1232
+ lineStyle: spec.lineStyle ?? "solid",
1233
+ yAxisId: source.yaxis || "y"
1234
+ }),
1235
+ showlegend: true
1236
+ });
1237
+ }
1238
+ }
1239
+ }
1240
+ return derived;
1241
+ }
1242
+ function renderSeriesTransformControls(container, subplot, seriesTransforms, { onChange } = {}) {
1243
+ const entries = Object.entries(seriesTransforms ?? {});
1244
+ if (!entries.length) return { dispose() {
1245
+ } };
1246
+ const root = document.createElement("div");
1247
+ root.className = "twm-series-transforms";
1248
+ const stateOf = (transformId) => {
1249
+ subplot.transforms ??= {};
1250
+ subplot.transforms[transformId] ??= {};
1251
+ return subplot.transforms[transformId];
1252
+ };
1253
+ for (const [transformId, transform] of entries) {
1254
+ const group = document.createElement("fieldset");
1255
+ group.className = "twm-series-transforms__group";
1256
+ const legend = document.createElement("legend");
1257
+ legend.className = "twm-series-transforms__label";
1258
+ legend.textContent = transform.label ?? transformId;
1259
+ group.appendChild(legend);
1260
+ const paramInputs = [];
1261
+ const syncDisabled = () => {
1262
+ const on = enabledOutputs(transform, subplot.transforms?.[transformId]).length > 0;
1263
+ for (const input of paramInputs) input.disabled = !on;
1264
+ };
1265
+ for (const [name, spec] of Object.entries(transform.outputs ?? {})) {
1266
+ const row = document.createElement("label");
1267
+ row.className = "twm-series-transforms__row";
1268
+ const box = document.createElement("input");
1269
+ box.type = "checkbox";
1270
+ box.checked = subplot.transforms?.[transformId]?.outputs?.[name] === true;
1271
+ box.addEventListener("change", () => {
1272
+ const state = stateOf(transformId);
1273
+ state.outputs = { ...state.outputs ?? {}, [name]: box.checked };
1274
+ syncDisabled();
1275
+ onChange?.();
1276
+ });
1277
+ const text = document.createElement("span");
1278
+ text.textContent = spec?.label ?? name;
1279
+ row.append(box, text);
1280
+ group.appendChild(row);
1281
+ }
1282
+ for (const [name, spec] of Object.entries(transform.params ?? {})) {
1283
+ const row = document.createElement("label");
1284
+ row.className = "twm-series-transforms__row";
1285
+ const text = document.createElement("span");
1286
+ text.textContent = spec?.label ?? name;
1287
+ const input = document.createElement("input");
1288
+ input.type = "number";
1289
+ input.className = "twm-series-transforms__param";
1290
+ for (const attr of ["min", "max", "step"]) {
1291
+ if (spec?.[attr] != null) input[attr] = String(spec[attr]);
1292
+ }
1293
+ const current = subplot.transforms?.[transformId]?.params?.[name] ?? spec?.default;
1294
+ input.value = current == null ? "" : String(current);
1295
+ input.addEventListener("change", () => {
1296
+ const value = parseFloat(input.value);
1297
+ const state = stateOf(transformId);
1298
+ const params = { ...state.params ?? {} };
1299
+ if (Number.isFinite(value)) params[name] = value;
1300
+ else delete params[name];
1301
+ state.params = params;
1302
+ onChange?.();
1303
+ });
1304
+ paramInputs.push(input);
1305
+ row.append(text, input);
1306
+ group.appendChild(row);
1307
+ }
1308
+ syncDisabled();
1309
+ root.appendChild(group);
1310
+ }
1311
+ container.appendChild(root);
1312
+ return { dispose() {
1313
+ root.remove();
1314
+ } };
1315
+ }
1316
+
1317
+ // src/charts/plot_grid.js
1318
+ var PLOT_GRID_THEME = Object.freeze({
1319
+ fontFamily: "Inter, -apple-system, BlinkMacSystemFont, sans-serif",
1320
+ fontColor: "#cccccc",
1321
+ fontSize: 11,
1322
+ paperBackground: "transparent",
1323
+ plotBackground: "transparent",
1324
+ gridColor: "rgba(255,255,255,0.05)",
1325
+ lineColor: "rgba(255,255,255,0.08)",
1326
+ tickColor: "rgba(255,255,255,0.3)",
1327
+ zeroLineColor: "rgba(255,255,255,0.06)",
1328
+ hoverBackground: "#1e2228",
1329
+ hoverBorder: "#444444"
1330
+ });
1331
+ var OVERLAY_DASH = { dashed: "dash", dotted: "dot", solid: "solid" };
1332
+ function resolveSeriesData(series, results) {
1333
+ const table = results?.series;
1334
+ const variable = series?.variable;
1335
+ if (!table || !variable) return null;
1336
+ let y = table[series._resultKey ?? variable];
1337
+ if (!y && series._resultKey) y = table[variable];
1338
+ if (!y) {
1339
+ const dot = variable.indexOf(".");
1340
+ if (dot >= 0) y = table[variable.slice(dot + 1)];
1341
+ }
1342
+ if (!y) return null;
1343
+ return series._resultTime ? { y, x: series._resultTime } : { y };
1344
+ }
1345
+ function renderPlotGrid(container, plotConfig, results, options = {}) {
1346
+ const opts = {
1347
+ resolveSeries: resolveSeriesData,
1348
+ overlays: null,
1349
+ seriesTransforms: null,
1350
+ onTransformError: null,
1351
+ derivedTraces: null,
1352
+ hiddenSeries: null,
1353
+ theme: null,
1354
+ alwaysShowLegend: false,
1355
+ layout: null,
1356
+ emptyMessage: "No data yet.",
1357
+ noSubplotMessage: "No subplot configured.",
1358
+ onError: null,
1359
+ ...options
1360
+ };
1361
+ let config = plotConfig;
1362
+ let data = results;
1363
+ let disposed = false;
1364
+ let cells = [];
1365
+ let cellsLayout = null;
1366
+ let tokens = [];
1367
+ let lastToken = 0;
1368
+ const deferred = /* @__PURE__ */ new Set();
1369
+ const grid = document.createElement("div");
1370
+ grid.className = "twm-plot-grid";
1371
+ container.appendChild(grid);
1372
+ const observer = createRafResizeObserver(() => resize2());
1373
+ observer.observe(grid);
1374
+ const plotted = /* @__PURE__ */ new Set();
1375
+ const drawn = /* @__PURE__ */ new Set();
1376
+ const hasPlot2 = (cell) => plotted.has(cell);
1377
+ const purgeCell = (cell) => {
1378
+ drawn.delete(cell);
1379
+ if (!plotted.delete(cell)) return;
1380
+ try {
1381
+ globalThis.Plotly?.purge(cell);
1382
+ } catch (_) {
1383
+ }
1384
+ };
1385
+ function buildCells() {
1386
+ for (const cell of cells) purgeCell(cell);
1387
+ grid.replaceChildren();
1388
+ const tmpl = PLOT_LAYOUTS[config?.layout] || PLOT_LAYOUTS["1x1"];
1389
+ grid.style.gridTemplateColumns = tmpl.grid.columns;
1390
+ grid.style.gridTemplateRows = tmpl.grid.rows;
1391
+ cells = tmpl.areas.map(({ gridArea }) => {
1392
+ const cell = document.createElement("div");
1393
+ cell.className = "twm-plot-grid__cell";
1394
+ cell.style.gridArea = gridArea;
1395
+ grid.appendChild(cell);
1396
+ return cell;
1397
+ });
1398
+ cellsLayout = config?.layout;
1399
+ tokens = cells.map(() => 0);
1400
+ deferred.clear();
1401
+ }
1402
+ function showMessage(cell, text) {
1403
+ purgeCell(cell);
1404
+ const msg = document.createElement("div");
1405
+ msg.className = "twm-plot-no-data";
1406
+ msg.textContent = text;
1407
+ cell.replaceChildren(msg);
1408
+ }
1409
+ const theme = () => ({ ...PLOT_GRID_THEME, ...opts.theme ?? {} });
1410
+ const isHidden = (name) => {
1411
+ const h = opts.hiddenSeries;
1412
+ if (!h) return false;
1413
+ return typeof h.has === "function" ? h.has(name) : h.includes(name);
1414
+ };
1415
+ const axisColours = (t) => ({
1416
+ gridcolor: t.gridColor,
1417
+ linecolor: t.lineColor,
1418
+ tickcolor: t.tickColor,
1419
+ zerolinecolor: t.zeroLineColor
1420
+ });
1421
+ async function buildFigure(sp, index) {
1422
+ if (!data) return null;
1423
+ const t = theme();
1424
+ const time = data.time ?? [];
1425
+ const traces = [];
1426
+ const layoutAxes = {};
1427
+ const yRefByAxisId = {};
1428
+ let ownCount = 0;
1429
+ let hiddenCount = 0;
1430
+ const useX = sp.xAxis && sp.xAxis.useTime === false && sp.xAxis.variable;
1431
+ const xData = useX ? opts.resolveSeries({ variable: sp.xAxis.variable }, data, sp)?.y ?? time : time;
1432
+ const yAxes = sp.yAxes ?? [];
1433
+ yAxes.forEach((axis, a) => {
1434
+ const n = a + 1;
1435
+ const yAxisKey = n === 1 ? "yaxis" : `yaxis${n}`;
1436
+ const yRef = n === 1 ? "y" : `y${n}`;
1437
+ if (axis.id) yRefByAxisId[axis.id] = yRef;
1438
+ layoutAxes[yAxisKey] = {
1439
+ ...buildYAxisLayout({
1440
+ axisId: yAxisKey,
1441
+ title: axis.label || "",
1442
+ position: axis.position || "left",
1443
+ scaleType: axis.scale || "linear",
1444
+ min: axis.min ?? null,
1445
+ max: axis.max ?? null,
1446
+ step: axis.step ?? null,
1447
+ showGrid: yAxes.length === 1,
1448
+ axisPosition: n > 1 ? axis.position === "right" ? 1 : 0 : null
1449
+ }),
1450
+ ...axisColours(t)
1451
+ };
1452
+ for (const s of axis.series ?? []) {
1453
+ if (!s.variable) continue;
1454
+ const label = s.label || s.variable;
1455
+ ownCount++;
1456
+ if (isHidden(label)) {
1457
+ hiddenCount++;
1458
+ continue;
1459
+ }
1460
+ const resolved = opts.resolveSeries(s, data, sp);
1461
+ if (!resolved?.y) continue;
1462
+ const x = resolved.x ?? xData;
1463
+ const y = Array.isArray(resolved.y) ? applyNanHandling(resolved.y, sp.nanHandling ?? "gap", x) : resolved.y;
1464
+ traces.push(buildTrace({
1465
+ chartType: sp.chartType || "line",
1466
+ x,
1467
+ y,
1468
+ name: label,
1469
+ color: s.color,
1470
+ lineWidth: s.lineWidth ?? 2,
1471
+ lineStyle: s.lineStyle ?? "solid",
1472
+ interpolation: sp.interpolation ?? "linear",
1473
+ yAxisId: yRef,
1474
+ showMarkers: sp.showDataPoints ?? false,
1475
+ stackGroup: s.stackGroup ?? null
1476
+ }));
1477
+ const overlay = s.overlay ? opts.overlays?.[s.id] : null;
1478
+ if (!overlay?.years?.length) continue;
1479
+ const ov = s.overlay;
1480
+ const ovLabel = ov.label || `${label} (actual)`;
1481
+ if (isHidden(ovLabel)) continue;
1482
+ const ovTrace = {
1483
+ type: "scatter",
1484
+ mode: ov.mode || "markers+lines",
1485
+ x: overlay.years,
1486
+ y: overlay.values,
1487
+ name: ovLabel,
1488
+ yaxis: yRef === "y" ? void 0 : yRef,
1489
+ line: {
1490
+ color: ov.color || s.color,
1491
+ width: ov.lineWidth ?? 2,
1492
+ dash: OVERLAY_DASH[ov.lineStyle] || "dash"
1493
+ },
1494
+ marker: { color: ov.color || s.color, size: 4, symbol: "circle" },
1495
+ showlegend: true
1496
+ };
1497
+ if ((sp.chartType || "line") === "area-stacked") {
1498
+ ovTrace.stackgroup = `overlay-${yRef}`;
1499
+ ovTrace.groupnorm = "fraction";
1500
+ ovTrace.fill = "none";
1501
+ ovTrace.mode = "lines";
1502
+ }
1503
+ traces.push(ovTrace);
1504
+ }
1505
+ });
1506
+ traces.push(...await deriveSeriesTraces(
1507
+ traces,
1508
+ sp,
1509
+ opts.seriesTransforms,
1510
+ { onError: opts.onTransformError ?? void 0 }
1511
+ ));
1512
+ if (opts.derivedTraces) {
1513
+ const extra = await opts.derivedTraces(traces, sp, { results: data, index });
1514
+ if (Array.isArray(extra)) traces.push(...extra);
1515
+ }
1516
+ if (traces.length === 0 && !(ownCount > 0 && hiddenCount === ownCount)) return null;
1517
+ const legendPos = opts.alwaysShowLegend && (sp.legendPosition ?? "hidden") === "hidden" ? "top" : sp.legendPosition ?? "hidden";
1518
+ const xRange = !useX && time.length >= 2 ? [time[0], time[time.length - 1]] : void 0;
1519
+ const { shapes, annotations } = buildOverlayShapes({
1520
+ referenceLines: sp.referenceLines,
1521
+ shadedBands: sp.shadedBands,
1522
+ yRefByAxisId
1523
+ });
1524
+ const font = { family: t.fontFamily, color: t.fontColor };
1525
+ const layout = {
1526
+ ...layoutAxes,
1527
+ ...shapes.length ? { shapes } : {},
1528
+ ...annotations.length ? { annotations } : {},
1529
+ xaxis: {
1530
+ ...useX ? { title: { text: sp.xAxis.label || sp.xAxis.variable, font: { size: t.fontSize } } } : {},
1531
+ ...axisColours(t),
1532
+ automargin: true,
1533
+ ...xRange ? { range: xRange } : {}
1534
+ },
1535
+ ...buildLegendLayout(legendPos),
1536
+ paper_bgcolor: t.paperBackground,
1537
+ plot_bgcolor: t.plotBackground,
1538
+ font: { ...font, size: t.fontSize },
1539
+ margin: { t: 10, r: 20, b: 40, l: 50 },
1540
+ hoverlabel: { bgcolor: t.hoverBackground, bordercolor: t.hoverBorder, font },
1541
+ ...opts.layout ?? {}
1542
+ };
1543
+ return { traces, layout };
1544
+ }
1545
+ const hasSize = (cell) => cell.isConnected && cell.offsetWidth > 0 && cell.offsetHeight > 0;
1546
+ async function drawCell(index) {
1547
+ const cell = cells[index];
1548
+ const token = ++lastToken;
1549
+ tokens[index] = token;
1550
+ const current = () => !disposed && cells[index] === cell && tokens[index] === token;
1551
+ const sp = config?.subplots?.[index];
1552
+ if (!sp) {
1553
+ showMessage(cell, opts.noSubplotMessage);
1554
+ deferred.delete(index);
1555
+ return false;
1556
+ }
1557
+ const figure = await buildFigure(sp, index);
1558
+ if (!current()) return false;
1559
+ if (!figure) {
1560
+ showMessage(cell, opts.emptyMessage);
1561
+ deferred.delete(index);
1562
+ return false;
1563
+ }
1564
+ if (!hasSize(cell)) {
1565
+ deferred.add(index);
1566
+ return false;
1567
+ }
1568
+ deferred.delete(index);
1569
+ const Plotly2 = await ensurePlotly();
1570
+ if (!current()) return false;
1571
+ if (!hasPlot2(cell)) cell.replaceChildren();
1572
+ plotted.add(cell);
1573
+ try {
1574
+ await Plotly2.react(cell, figure.traces, figure.layout, { responsive: true, displayModeBar: false });
1575
+ if (plotted.has(cell)) drawn.add(cell);
1576
+ return true;
1577
+ } catch (_) {
1578
+ drawn.delete(cell);
1579
+ return false;
1580
+ }
1581
+ }
1582
+ function drawCellOrFail(index) {
1583
+ const cell = cells[index];
1584
+ const pending = drawCell(index);
1585
+ const token = tokens[index];
1586
+ return pending.catch((error) => {
1587
+ if (disposed) return false;
1588
+ if (cells[index] === cell && tokens[index] === token) {
1589
+ deferred.delete(index);
1590
+ showMessage(cell, opts.emptyMessage);
1591
+ }
1592
+ opts.onError?.({ index, error });
1593
+ return false;
1594
+ });
1595
+ }
1596
+ async function drawAll() {
1597
+ if (disposed) return false;
1598
+ if (cellsLayout !== config?.layout || !cells.length) buildCells();
1599
+ const results2 = await Promise.all(cells.map((_, i) => drawCellOrFail(i)));
1600
+ return results2.some(Boolean);
1601
+ }
1602
+ function update(patch = {}) {
1603
+ if (disposed) return Promise.resolve(false);
1604
+ if ("plotConfig" in patch) config = patch.plotConfig;
1605
+ if ("results" in patch) data = patch.results;
1606
+ if ("overlays" in patch) opts.overlays = patch.overlays;
1607
+ if ("hiddenSeries" in patch) opts.hiddenSeries = patch.hiddenSeries;
1608
+ return drawAll();
1609
+ }
1610
+ function resize2() {
1611
+ if (disposed) return;
1612
+ for (const index of [...deferred]) {
1613
+ if (!hasSize(cells[index])) continue;
1614
+ drawCellOrFail(index);
1615
+ }
1616
+ const Plotly2 = globalThis.Plotly;
1617
+ if (!Plotly2?.Plots?.resize) return;
1618
+ for (const cell of cells) {
1619
+ if (hasPlot2(cell) && hasSize(cell)) {
1620
+ Promise.resolve().then(() => Plotly2.Plots.resize(cell)).catch(() => {
1621
+ });
1622
+ }
1623
+ }
1624
+ }
1625
+ function dispose() {
1626
+ if (disposed) return;
1627
+ disposed = true;
1628
+ observer.disconnect();
1629
+ for (const cell of cells) purgeCell(cell);
1630
+ cells = [];
1631
+ deferred.clear();
1632
+ grid.remove();
1633
+ }
1634
+ drawAll();
1635
+ return {
1636
+ update,
1637
+ resize: resize2,
1638
+ dispose,
1639
+ /** The cells, one per grid slot, in layout order. */
1640
+ get charts() {
1641
+ return [...cells];
1642
+ },
1643
+ /**
1644
+ * The cells that show a figure now, in layout order — however it got
1645
+ * there: an update, or a deferred draw on a resize.
1646
+ */
1647
+ get plotted() {
1648
+ return cells.filter((cell) => drawn.has(cell));
1649
+ }
1650
+ };
1651
+ }
1652
+
1162
1653
  // src/charts/plot_popout_window.js
1163
1654
  var PlotPopoutWindow = class {
1164
1655
  /**
@@ -1172,7 +1663,21 @@ var PlotPopoutWindow = class {
1172
1663
  * capability backs Export PNG/CSV; absent => Blob download fallback.
1173
1664
  * @param {Function} [options.onClose] - Called when window closes
1174
1665
  * @param {Object} [options.actuals] - ETL overlay data { seriesId: { years, values } }
1175
- * @param {Function} [options.configRenderer] - (container) => void — renders config panel
1666
+ * @param {Function} [options.configRenderer] - `(container) => { dispose() } | void` —
1667
+ * renders the Config tab. What it returns is disposed (`dispose()`) when the
1668
+ * window closes, so an editor drawn there (renderPlotConfigEditor) goes with it.
1669
+ * @param {Object} [options.seriesTransforms] - `{ [id]: transform }` (series_transforms.js);
1670
+ * absent => no derived series are drawn
1671
+ * @param {string} [options.refreshEvent='data:changed'] - Bus event after which the
1672
+ * window re-reads `getResults()` and redraws.
1673
+ * @param {{delta?: string|null, complete?: string|null}} [options.streamEvents] - Bus
1674
+ * events a live data source emits while it runs (`delta`) and when it ends
1675
+ * (`complete`); the window redraws on each while live update is on.
1676
+ * There are no default names: without `streamEvents` (or with a name
1677
+ * left out or given as null) nothing is listened for, and the window
1678
+ * updates through `refreshEvent` and explicit calls only.
1679
+ * @param {Function} [options.resolveSeries] - `(series, results, subplot) => { y, x? } | null`
1680
+ * (plot_grid.js); used by the chart and the data table. Default resolveSeriesData.
1176
1681
  */
1177
1682
  constructor({
1178
1683
  id,
@@ -1184,10 +1689,13 @@ var PlotPopoutWindow = class {
1184
1689
  onClose,
1185
1690
  actuals,
1186
1691
  configRenderer,
1692
+ seriesTransforms,
1693
+ resolveSeries = resolveSeriesData,
1187
1694
  // The window used to listen for 'simulation:run:completed' and tell
1188
1695
  // the user to "run simulation first". A chart window does not know
1189
1696
  // that its data comes from a simulation; the app that opens it does.
1190
1697
  refreshEvent = "data:changed",
1698
+ streamEvents = {},
1191
1699
  emptyMessage = "No data yet.",
1192
1700
  tableEmptyMessage = "No data yet."
1193
1701
  }) {
@@ -1201,17 +1709,21 @@ var PlotPopoutWindow = class {
1201
1709
  });
1202
1710
  this._actuals = actuals ?? null;
1203
1711
  this._configRenderer = configRenderer ?? null;
1712
+ this._seriesTransforms = seriesTransforms ?? null;
1713
+ this._resolveSeries = resolveSeries;
1204
1714
  this.refreshEvent = refreshEvent;
1715
+ this.streamEvents = Object.freeze({
1716
+ delta: streamEvents?.delta ?? null,
1717
+ complete: streamEvents?.complete ?? null
1718
+ });
1205
1719
  this.emptyMessage = emptyMessage;
1206
1720
  this.tableEmptyMessage = tableEmptyMessage;
1207
1721
  this._window = null;
1208
1722
  this._contentEl = null;
1209
- this._chartDivs = [];
1723
+ this._grid = null;
1210
1724
  this._gridEl = null;
1211
1725
  this._activeTab = "plot";
1212
1726
  this._eventDisposers = [];
1213
- this._resizeObserver = null;
1214
- this._chartCreated = false;
1215
1727
  this._lastResults = null;
1216
1728
  this._dataTable = null;
1217
1729
  this._dataTableContainer = null;
@@ -1223,6 +1735,7 @@ var PlotPopoutWindow = class {
1223
1735
  this._plotContent = null;
1224
1736
  this._dataContent = null;
1225
1737
  this._configContent = null;
1738
+ this._configHandle = null;
1226
1739
  this._toolbarActions = null;
1227
1740
  this._zoomBtn = null;
1228
1741
  this._panBtn = null;
@@ -1235,6 +1748,7 @@ var PlotPopoutWindow = class {
1235
1748
  this._lastTogglePanelSignature = null;
1236
1749
  this._outsideClickHandler = null;
1237
1750
  this._renderPending = false;
1751
+ this._configChanged = false;
1238
1752
  this._liveUpdate = true;
1239
1753
  this._liveBtn = null;
1240
1754
  }
@@ -1249,7 +1763,6 @@ var PlotPopoutWindow = class {
1249
1763
  await ensurePlotly();
1250
1764
  this.#createWindow();
1251
1765
  this.#subscribeEvents();
1252
- this.#setupResizeObserver();
1253
1766
  await this.#render();
1254
1767
  }
1255
1768
  bringToFront() {
@@ -1261,7 +1774,7 @@ var PlotPopoutWindow = class {
1261
1774
  /** Update plot config and re-render. */
1262
1775
  updateConfig(plotConfig) {
1263
1776
  this._plotConfig = migrateData(plotConfig);
1264
- this.#buildSubplotGrid();
1777
+ this._configChanged = true;
1265
1778
  this.#scheduleRender();
1266
1779
  }
1267
1780
  /** Update actuals overlay data and re-render. */
@@ -1358,7 +1871,24 @@ var PlotPopoutWindow = class {
1358
1871
  this._gridEl = document.createElement("div");
1359
1872
  this._gridEl.className = "twm-plot-window-chart";
1360
1873
  this._plotContent.appendChild(this._gridEl);
1361
- this.#buildSubplotGrid();
1874
+ this._grid = renderPlotGrid(this._gridEl, this._plotConfig, null, {
1875
+ resolveSeries: this._resolveSeries,
1876
+ overlays: this._actuals,
1877
+ hiddenSeries: this._hiddenSeries,
1878
+ seriesTransforms: this._seriesTransforms,
1879
+ onTransformError: ({ transformId, traceName, error }) => this._services?.logger?.warn?.(
1880
+ `[PlotPopoutWindow] transform "${transformId}" failed on "${traceName}"`,
1881
+ error
1882
+ ),
1883
+ // Popout windows have space — always show the legend.
1884
+ alwaysShowLegend: true,
1885
+ layout: { autosize: true, hovermode: "x unified" },
1886
+ emptyMessage: this.emptyMessage,
1887
+ onError: ({ index, error }) => this._services?.logger?.warn?.(
1888
+ `[PlotPopoutWindow] subplot ${index + 1} failed to draw`,
1889
+ error
1890
+ )
1891
+ });
1362
1892
  this._dataContent = document.createElement("div");
1363
1893
  this._dataContent.className = "twm-plot-tab-content";
1364
1894
  this._dataContent.setAttribute("data-tab", "data");
@@ -1373,7 +1903,7 @@ var PlotPopoutWindow = class {
1373
1903
  this._configContent.className = "twm-plot-tab-content";
1374
1904
  this._configContent.setAttribute("data-tab", "config");
1375
1905
  this._configContent.style.cssText = "display:none; flex-direction:column; overflow:auto; padding:8px 12px;";
1376
- this._configRenderer(this._configContent);
1906
+ this._configHandle = this._configRenderer(this._configContent) ?? null;
1377
1907
  content.appendChild(this._configContent);
1378
1908
  }
1379
1909
  this._seriesTogglePanel = document.createElement("div");
@@ -1384,36 +1914,6 @@ var PlotPopoutWindow = class {
1384
1914
  container.appendChild(this._seriesTogglePanel);
1385
1915
  return container;
1386
1916
  }
1387
- // ── Subplot grid ────────────────────────────────────────────────────
1388
- #buildSubplotGrid() {
1389
- if (!this._gridEl) return;
1390
- for (const div of this._chartDivs) {
1391
- if (div && window.Plotly) {
1392
- try {
1393
- purge(div);
1394
- } catch (_) {
1395
- }
1396
- }
1397
- }
1398
- this._gridEl.innerHTML = "";
1399
- this._chartDivs = [];
1400
- const tmpl = PLOT_LAYOUTS[this._plotConfig.layout] || PLOT_LAYOUTS["1x1"];
1401
- this._gridEl.style.display = "grid";
1402
- this._gridEl.style.gridTemplateColumns = tmpl.grid.columns;
1403
- this._gridEl.style.gridTemplateRows = tmpl.grid.rows;
1404
- this._gridEl.style.gap = "4px";
1405
- this._gridEl.style.height = "100%";
1406
- this._gridEl.style.width = "100%";
1407
- for (let i = 0; i < tmpl.slots; i++) {
1408
- const div = document.createElement("div");
1409
- div.className = "plot-tile-chart";
1410
- div.style.gridArea = tmpl.areas[i].gridArea;
1411
- div.style.minHeight = "0";
1412
- this._gridEl.appendChild(div);
1413
- this._chartDivs.push(div);
1414
- }
1415
- this._chartCreated = false;
1416
- }
1417
1917
  // ── Toolbar helpers ─────────────────────────────────────────────────
1418
1918
  #toolbarGroup() {
1419
1919
  const g = document.createElement("div");
@@ -1433,9 +1933,16 @@ var PlotPopoutWindow = class {
1433
1933
  return btn;
1434
1934
  }
1435
1935
  // ── Plotly toolbar actions ───────────────────────────────────────────
1936
+ get #charts() {
1937
+ return this._grid?.charts ?? [];
1938
+ }
1939
+ /** The subplots that show a figure — the grid knows, however it drew them. */
1940
+ get #plottedCharts() {
1941
+ return this._grid?.plotted ?? [];
1942
+ }
1436
1943
  #setDragMode(mode) {
1437
- if (!this._chartCreated) return;
1438
- for (const div of this._chartDivs) {
1944
+ if (!this.#plottedCharts.length) return;
1945
+ for (const div of this.#charts) {
1439
1946
  if (div._fullLayout) {
1440
1947
  window.Plotly.relayout(div, { dragmode: mode }).catch(() => {
1441
1948
  });
@@ -1445,7 +1952,7 @@ var PlotPopoutWindow = class {
1445
1952
  this._panBtn?.classList.toggle("twm-is-active", mode === "pan");
1446
1953
  }
1447
1954
  #zoom(factor) {
1448
- for (const div of this._chartDivs) {
1955
+ for (const div of this.#charts) {
1449
1956
  if (!div._fullLayout) continue;
1450
1957
  const xa = div._fullLayout.xaxis;
1451
1958
  const ya = div._fullLayout.yaxis;
@@ -1464,7 +1971,7 @@ var PlotPopoutWindow = class {
1464
1971
  }
1465
1972
  }
1466
1973
  #autoscale() {
1467
- for (const div of this._chartDivs) {
1974
+ for (const div of this.#charts) {
1468
1975
  if (!div._fullLayout) continue;
1469
1976
  window.Plotly.relayout(div, {
1470
1977
  "xaxis.autorange": true,
@@ -1473,20 +1980,6 @@ var PlotPopoutWindow = class {
1473
1980
  });
1474
1981
  }
1475
1982
  }
1476
- #setupResizeObserver() {
1477
- if (!this._plotContent) return;
1478
- this._resizeObserver = createRafResizeObserver(() => {
1479
- if (this._activeTab === "plot" && this._chartCreated && window.Plotly) {
1480
- for (const div of this._chartDivs) {
1481
- if (div._fullLayout) {
1482
- Plotly.Plots.resize(div).catch(() => {
1483
- });
1484
- }
1485
- }
1486
- }
1487
- });
1488
- this._resizeObserver.observe(this._plotContent);
1489
- }
1490
1983
  #switchTab(tabName) {
1491
1984
  this._activeTab = tabName;
1492
1985
  this._plotTab.classList.toggle("active", tabName === "plot");
@@ -1500,14 +1993,7 @@ var PlotPopoutWindow = class {
1500
1993
  if (this._configContent) {
1501
1994
  this._configContent.style.display = tabName === "config" ? "flex" : "none";
1502
1995
  }
1503
- if (tabName === "plot" && this._chartCreated && window.Plotly) {
1504
- for (const div of this._chartDivs) {
1505
- if (div._fullLayout) {
1506
- Plotly.Plots.resize(div).catch(() => {
1507
- });
1508
- }
1509
- }
1510
- }
1996
+ if (tabName === "plot") this._grid?.resize();
1511
1997
  if (tabName === "data") {
1512
1998
  this.#renderDataTable();
1513
1999
  }
@@ -1666,16 +2152,15 @@ var PlotPopoutWindow = class {
1666
2152
  #subscribeEvents() {
1667
2153
  const { eventBus } = this._services;
1668
2154
  if (!eventBus) return;
1669
- this._eventDisposers.push(
1670
- eventBus.on("streaming:delta", () => {
1671
- if (this._liveUpdate) this.#scheduleRender();
1672
- })
1673
- );
1674
- this._eventDisposers.push(
1675
- eventBus.on("streaming:complete", () => {
1676
- if (this._liveUpdate) this.#scheduleRender();
1677
- })
1678
- );
2155
+ const { delta, complete } = this.streamEvents;
2156
+ for (const name of /* @__PURE__ */ new Set([delta, complete])) {
2157
+ if (!name) continue;
2158
+ this._eventDisposers.push(
2159
+ eventBus.on(name, () => {
2160
+ if (this._liveUpdate) this.#scheduleRender();
2161
+ })
2162
+ );
2163
+ }
1679
2164
  this._eventDisposers.push(
1680
2165
  eventBus.on(this.refreshEvent, () => {
1681
2166
  if (this._liveUpdate) this.#scheduleRender();
@@ -1704,10 +2189,28 @@ var PlotPopoutWindow = class {
1704
2189
  async #render() {
1705
2190
  if (!this._window?.isVisible) return;
1706
2191
  const results = this._getResults?.();
1707
- if (!results) return;
2192
+ const configChanged = this._configChanged;
2193
+ this._configChanged = false;
2194
+ if (!results) {
2195
+ if (configChanged) {
2196
+ await this._grid.update({
2197
+ plotConfig: this._plotConfig,
2198
+ results: null,
2199
+ overlays: this._actuals,
2200
+ hiddenSeries: this._hiddenSeries
2201
+ });
2202
+ }
2203
+ return;
2204
+ }
1708
2205
  this._lastResults = results;
1709
2206
  this.#collectSeriesInfo();
1710
- this.#renderAllCharts(results);
2207
+ await this._grid.update({
2208
+ plotConfig: this._plotConfig,
2209
+ results,
2210
+ overlays: this._actuals,
2211
+ hiddenSeries: this._hiddenSeries
2212
+ });
2213
+ if (!this._window) return;
1711
2214
  this.#updateTableData(results);
1712
2215
  if (this._activeTab === "data") {
1713
2216
  this.#renderDataTable();
@@ -1730,7 +2233,7 @@ var PlotPopoutWindow = class {
1730
2233
  subplotName: sp.displayName || `Subplot ${spIdx + 1}`
1731
2234
  });
1732
2235
  this._allSeriesLabels.push(label);
1733
- if (s.overlay?.etlKey) {
2236
+ if (s.overlay) {
1734
2237
  const ovLabel = s.overlay.label || `${label} (actual)`;
1735
2238
  this._allSeriesInfo.push({
1736
2239
  label: ovLabel,
@@ -1764,184 +2267,6 @@ var PlotPopoutWindow = class {
1764
2267
  }
1765
2268
  }
1766
2269
  }
1767
- #renderAllCharts(results) {
1768
- if (!window.Plotly) return;
1769
- const subplots = this._plotConfig.subplots ?? [];
1770
- for (let i = 0; i < this._chartDivs.length; i++) {
1771
- const chartDiv = this._chartDivs[i];
1772
- const sp = subplots[i];
1773
- if (!chartDiv) continue;
1774
- if (!sp) {
1775
- 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>';
1776
- continue;
1777
- }
1778
- this.#renderSubplotChart(chartDiv, sp, results);
1779
- }
1780
- }
1781
- async #renderSubplotChart(chartDiv, sp, results) {
1782
- const { time, series } = results;
1783
- const traces = [];
1784
- const layoutAxes = {};
1785
- const yRefByAxisId = {};
1786
- let axisCount = 0;
1787
- for (const axis of sp.yAxes ?? []) {
1788
- axisCount++;
1789
- const yAxisKey = axisCount === 1 ? "yaxis" : `yaxis${axisCount}`;
1790
- const yRef = axisCount === 1 ? "y" : `y${axisCount}`;
1791
- if (axis.id) yRefByAxisId[axis.id] = yRef;
1792
- layoutAxes[yAxisKey] = buildYAxisLayout({
1793
- axisId: yAxisKey,
1794
- title: axis.label || "",
1795
- position: axis.position || "left",
1796
- scaleType: axis.scale || "linear",
1797
- min: axis.min,
1798
- max: axis.max,
1799
- showGrid: (sp.yAxes ?? []).length === 1,
1800
- axisPosition: axisCount > 1 ? axis.position === "right" ? 1 : 0 : null
1801
- });
1802
- for (const s of axis.series ?? []) {
1803
- if (!s.variable) continue;
1804
- const label = s.label || s.variable;
1805
- if (this._hiddenSeries.has(label)) continue;
1806
- let rawY = series?.[s._resultKey ?? s.variable];
1807
- if (!rawY && s._resultKey) {
1808
- rawY = series?.[s.variable];
1809
- }
1810
- if (!rawY) {
1811
- const dotIdx = s.variable.indexOf(".");
1812
- if (dotIdx >= 0) rawY = series?.[s.variable.slice(dotIdx + 1)];
1813
- }
1814
- if (!rawY) continue;
1815
- const y = Array.isArray(rawY) ? applyNanHandling(rawY, sp.nanHandling ?? "gap", time) : rawY;
1816
- const seriesTime = s._resultTime ?? time;
1817
- traces.push(buildTrace({
1818
- chartType: sp.chartType || "line",
1819
- x: seriesTime,
1820
- y,
1821
- name: label,
1822
- color: s.color,
1823
- lineWidth: s.lineWidth ?? 2,
1824
- lineStyle: s.lineStyle ?? "solid",
1825
- interpolation: sp.interpolation ?? "linear",
1826
- yAxisId: yRef,
1827
- showMarkers: sp.showDataPoints ?? false,
1828
- stackGroup: s.stackGroup ?? null
1829
- }));
1830
- if (s.overlay?.etlKey && this._actuals?.[s.id]) {
1831
- const ovLabel = s.overlay.label || `${label} (actual)`;
1832
- if (!this._hiddenSeries.has(ovLabel)) {
1833
- const act = this._actuals[s.id];
1834
- if (act?.years?.length) {
1835
- const ov = s.overlay;
1836
- const dashMap = { dashed: "dash", dotted: "dot", solid: "solid" };
1837
- traces.push({
1838
- type: "scatter",
1839
- mode: ov.mode || "markers+lines",
1840
- x: act.years,
1841
- y: act.values,
1842
- name: ov.label || `${label} (actual)`,
1843
- yaxis: yRef === "y" ? void 0 : yRef,
1844
- line: {
1845
- color: ov.color || s.color,
1846
- width: ov.lineWidth ?? 2,
1847
- dash: dashMap[ov.lineStyle] || "dash"
1848
- },
1849
- marker: {
1850
- color: ov.color || s.color,
1851
- size: 4,
1852
- symbol: "circle"
1853
- },
1854
- showlegend: true
1855
- });
1856
- }
1857
- }
1858
- }
1859
- }
1860
- }
1861
- if ((sp.showHpTrend || sp.showHpCycle) && window.pywebview?.api?.hp_filter) {
1862
- const hpLambda = sp.hpLambda ?? 1600;
1863
- for (const trace of [...traces]) {
1864
- if (trace.type && trace.type !== "scatter" && trace.type !== "scattergl") continue;
1865
- try {
1866
- const hpResult = await window.pywebview.api.hp_filter({
1867
- data: trace.y,
1868
- lambda: hpLambda
1869
- });
1870
- if (!hpResult?.ok) continue;
1871
- const { trend, cycle } = hpResult;
1872
- const origColor = trace.line?.color || trace.marker?.color || getSeriesColor(0);
1873
- if (sp.showHpTrend && trend) {
1874
- traces.push({
1875
- x: trace.x,
1876
- y: trend,
1877
- type: "scatter",
1878
- mode: "lines",
1879
- name: `${trace.name} (HP Trend)`,
1880
- yaxis: trace.yaxis || "y",
1881
- line: { color: origColor, width: 2, dash: "dash" },
1882
- showlegend: true
1883
- });
1884
- }
1885
- if (sp.showHpCycle && cycle) {
1886
- traces.push({
1887
- x: trace.x,
1888
- y: cycle,
1889
- type: "scatter",
1890
- mode: "lines",
1891
- name: `${trace.name} (HP Cycle)`,
1892
- yaxis: trace.yaxis || "y",
1893
- line: { color: origColor, width: 1, dash: "dot" },
1894
- showlegend: true
1895
- });
1896
- }
1897
- } catch (_) {
1898
- }
1899
- }
1900
- }
1901
- if (traces.length === 0 && this._hiddenSeries.size === 0) {
1902
- 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>`;
1903
- return;
1904
- }
1905
- const legendPos = sp.legendPosition === "hidden" ? "top" : sp.legendPosition ?? "top";
1906
- const legend = buildLegendLayout(legendPos);
1907
- const xRange = time?.length >= 2 ? [time[0], time[time.length - 1]] : void 0;
1908
- const { shapes, annotations } = buildOverlayShapes({
1909
- referenceLines: sp.referenceLines,
1910
- shadedBands: sp.shadedBands,
1911
- yRefByAxisId
1912
- });
1913
- const layout = {
1914
- ...layoutAxes,
1915
- ...shapes.length ? { shapes } : {},
1916
- ...annotations.length ? { annotations } : {},
1917
- xaxis: {
1918
- gridcolor: "rgba(255,255,255,0.05)",
1919
- linecolor: "rgba(255,255,255,0.08)",
1920
- tickcolor: "rgba(255,255,255,0.3)",
1921
- zerolinecolor: "rgba(255,255,255,0.06)",
1922
- automargin: true,
1923
- ...xRange ? { range: xRange } : {}
1924
- },
1925
- ...legend,
1926
- paper_bgcolor: "transparent",
1927
- plot_bgcolor: "transparent",
1928
- font: { family: "Inter, -apple-system, BlinkMacSystemFont, sans-serif", color: "#cccccc", size: 11 },
1929
- margin: { t: 10, r: 20, b: 40, l: 50 },
1930
- hoverlabel: {
1931
- bgcolor: "#1e2228",
1932
- bordercolor: "#444444",
1933
- font: { family: "Inter, -apple-system, BlinkMacSystemFont, sans-serif", color: "#cccccc" }
1934
- },
1935
- autosize: true,
1936
- hovermode: "x unified"
1937
- };
1938
- if (!chartDiv.isConnected || chartDiv.offsetWidth === 0) return;
1939
- try {
1940
- await Plotly.react(chartDiv, traces, layout, { responsive: true, displayModeBar: false });
1941
- this._chartCreated = true;
1942
- } catch (_) {
1943
- }
1944
- }
1945
2270
  // ── Data table ──────────────────────────────────────────────────────
1946
2271
  #updateTableData(results) {
1947
2272
  if (!results) {
@@ -1951,21 +2276,14 @@ var PlotPopoutWindow = class {
1951
2276
  }
1952
2277
  const headers = ["Time"];
1953
2278
  const seriesColumns = [];
1954
- const { time, series } = results;
2279
+ const { time } = results;
1955
2280
  const subplots = this._plotConfig.subplots ?? [];
1956
2281
  for (const sp of subplots) {
1957
2282
  for (const axis of sp.yAxes ?? []) {
1958
2283
  for (const s of axis.series ?? []) {
1959
2284
  if (!s.variable) continue;
1960
2285
  const label = s.label || s.variable;
1961
- let rawY = series?.[s._resultKey ?? s.variable];
1962
- if (!rawY && s._resultKey) {
1963
- rawY = series?.[s.variable];
1964
- }
1965
- if (!rawY) {
1966
- const dotIdx = s.variable.indexOf(".");
1967
- if (dotIdx >= 0) rawY = series?.[s.variable.slice(dotIdx + 1)];
1968
- }
2286
+ const rawY = this._resolveSeries(s, results, sp)?.y;
1969
2287
  if (!rawY) continue;
1970
2288
  headers.push(label);
1971
2289
  seriesColumns.push(rawY);
@@ -2057,8 +2375,8 @@ var PlotPopoutWindow = class {
2057
2375
  return str;
2058
2376
  }
2059
2377
  async #downloadPNG() {
2060
- const targetDiv = this._chartDivs.length === 1 ? this._chartDivs[0] : this._chartDivs[0];
2061
- if (!targetDiv || !window.Plotly || !this._chartCreated) {
2378
+ const targetDiv = this.#plottedCharts[0];
2379
+ if (!targetDiv || !window.Plotly) {
2062
2380
  this.#notify("Download", "Plot not ready", "warn");
2063
2381
  return;
2064
2382
  }
@@ -2105,10 +2423,6 @@ var PlotPopoutWindow = class {
2105
2423
  this._dataTable = null;
2106
2424
  }
2107
2425
  this.#hideSeriesPanel();
2108
- if (this._resizeObserver) {
2109
- this._resizeObserver.disconnect();
2110
- this._resizeObserver = null;
2111
- }
2112
2426
  for (const disposer of this._eventDisposers) {
2113
2427
  try {
2114
2428
  disposer?.dispose?.();
@@ -2116,18 +2430,17 @@ var PlotPopoutWindow = class {
2116
2430
  }
2117
2431
  }
2118
2432
  this._eventDisposers = [];
2119
- for (const div of this._chartDivs) {
2120
- if (div && window.Plotly) {
2121
- try {
2122
- purge(div);
2123
- } catch (_) {
2124
- }
2125
- }
2433
+ try {
2434
+ this._configHandle?.dispose?.();
2435
+ } catch (error) {
2436
+ this._services?.logger?.warn?.("[PlotPopoutWindow] the config panel failed to dispose", error);
2126
2437
  }
2438
+ this._configHandle = null;
2439
+ this._grid?.dispose();
2440
+ this._grid = null;
2127
2441
  this._window = null;
2128
2442
  this._contentEl = null;
2129
2443
  this._gridEl = null;
2130
- this._chartDivs = [];
2131
2444
  this._plotTab = null;
2132
2445
  this._dataTab = null;
2133
2446
  this._plotContent = null;
@@ -2142,7 +2455,6 @@ var PlotPopoutWindow = class {
2142
2455
  this._hiddenSeries = null;
2143
2456
  this._allSeriesInfo = null;
2144
2457
  this._allSeriesLabels = null;
2145
- this._chartCreated = false;
2146
2458
  this._lastResults = null;
2147
2459
  }
2148
2460
  };
@@ -2435,7 +2747,10 @@ async function openRawTracesWindow({ id, title, traces, layout, frames, tableHea
2435
2747
  export {
2436
2748
  setPlotlySource,
2437
2749
  DARK_THEME_LAYOUT,
2750
+ DEFAULT_CHART_EXPORT_FILENAME,
2438
2751
  DEFAULT_CONFIG,
2752
+ setChartExportFilename,
2753
+ getChartExportFilename,
2439
2754
  ensurePlotly,
2440
2755
  createChart,
2441
2756
  updateChart,
@@ -2470,11 +2785,18 @@ export {
2470
2785
  uid,
2471
2786
  applyNanHandling,
2472
2787
  PLOT_LAYOUTS,
2788
+ makeDefaultYAxis,
2789
+ makeDefaultSeries,
2473
2790
  makeDefaultSubplot,
2474
2791
  migrateData,
2475
2792
  migrateSingleChart,
2793
+ deriveSeriesTraces,
2794
+ renderSeriesTransformControls,
2795
+ PLOT_GRID_THEME,
2796
+ resolveSeriesData,
2797
+ renderPlotGrid,
2476
2798
  PlotPopoutWindow,
2477
2799
  openPlotPopoutWindow,
2478
2800
  openRawTracesWindow
2479
2801
  };
2480
- //# sourceMappingURL=chunk-RQM5Z2MU.js.map
2802
+ //# sourceMappingURL=chunk-MCIXFNVF.js.map