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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flexdesk",
3
- "version": "0.16.1",
3
+ "version": "0.17.0",
4
4
  "description": "A tiling window manager and widget set for desktop-class web applications. Zero runtime dependencies.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -20,6 +20,7 @@
20
20
  "src",
21
21
  "css",
22
22
  "README.md",
23
+ "CHANGELOG.md",
23
24
  "LICENSE",
24
25
  "THIRD_PARTY_LICENSES.md"
25
26
  ],
@@ -42,6 +43,7 @@
42
43
  },
43
44
  "./css": "./dist/flexdesk.css",
44
45
  "./css/tokens": "./css/tokens.css",
46
+ "./css/reset": "./dist/reset.css",
45
47
  "./charts": {
46
48
  "import": "./dist/charts.js",
47
49
  "development": "./charts.js"
@@ -43,6 +43,11 @@ const COARSE_GRID_EVERY = 5;
43
43
  const SUB_PIXEL = 0.5;
44
44
 
45
45
  export class CanvasAdapter {
46
+ /** The zoom limits every `setViewport`, `zoomAt` and `fit` clamps to, for an
47
+ * embedder whose own zoom controls must stop where the kernel stops. */
48
+ static MIN_SCALE = MIN_SCALE;
49
+ static MAX_SCALE = MAX_SCALE;
50
+
46
51
  /**
47
52
  * @param {object} o
48
53
  * @param {HTMLElement} o.container the box the canvas fills (it needs a height)
File without changes
File without changes
File without changes
File without changes
@@ -11,6 +11,7 @@ export { CanvasAdapter } from './canvas_adapter.js';
11
11
  export {
12
12
  OBSTACLE_ROUTING_NODE_LIMIT, routeEdge, footAnchor, crowsFoot, cardinality, selfLoop, returnEdge, arrange,
13
13
  } from './edge_router.js';
14
+ export { layeredLayout } from './layered_layout.js';
14
15
  export { NodePlatform } from './graph/node_platform.js';
15
16
  export { ConnectorRouter, CONNECTOR_STRINGS } from './graph/connector_router.js';
16
17
  export { HistoryService } from './graph/history_service.js';
@@ -0,0 +1,244 @@
1
+ /**
2
+ * A LAYERED LAYOUT for any directed graph — PURE: no DOM, runs under plain node.
3
+ *
4
+ * layeredLayout(nodes, edges, {rank, sizeOf, columnGap, rowGap, componentGap})
5
+ * → Map of node id → {x, y}, the top-left corner of each node's box, from (0, 0)
6
+ *
7
+ * The kernel's `arrange` stays what Tables' diagrams were built on (an edge
8
+ * from child to parent, columns sorted by label). This one reads an edge the
9
+ * way a diagram is read — `{from, to}` puts `from` LEFT of `to` — and, in
10
+ * order:
11
+ *
12
+ * 1. **separate components.** Nodes no line joins are laid out on their own
13
+ * and stacked top to bottom, the largest first, `componentGap` apart, so
14
+ * two of them never overlap.
15
+ * 2. **cycles broken.** A depth-first walk in the order the nodes are given
16
+ * turns each line back to a node still on the walk round; nothing hangs.
17
+ * 3. **longest-path columns.** A node sits one column right of the furthest
18
+ * of the nodes that lead to it.
19
+ * 4. **rank hints.** `rank(node)` may answer `'source'` (the first column) or
20
+ * `'sink'` (the last column of its component); a line INTO a source or OUT
21
+ * of a sink then pulls nothing. Anything else is no hint.
22
+ * 5. **crossing reduction.** Barycentre sweeps, down the columns and back,
23
+ * each node to the mean place of its neighbours in the column it is swept
24
+ * from (a node with none keeps its place, the sort is stable). The order
25
+ * with the FEWEST crossings between neighbouring columns is kept — the
26
+ * order the nodes were given included, so a sweep never makes it worse.
27
+ * 6. **measured sizes.** `sizeOf(node)` → `{width, height}` (default: the
28
+ * node's own `width`/`height`, else 160 × 80). A column is as wide as its
29
+ * widest node, `columnGap` before the next; a column's nodes are
30
+ * `rowGap` apart and the column is centred on its component's height.
31
+ *
32
+ * Unknown ends, a node's line to itself and a repeated line are ignored. A
33
+ * second node with an id already seen is left out.
34
+ */
35
+
36
+ const DEFAULT_SIZE = Object.freeze({ width: 160, height: 80 });
37
+
38
+ const defaultSizeOf = (node) => ({ width: Number(node?.width) || DEFAULT_SIZE.width,
39
+ height: Number(node?.height) || DEFAULT_SIZE.height });
40
+
41
+ /** Crossings between two neighbouring columns: the pairs of lines whose ends come in opposite orders. */
42
+ function crossingsBetween(pairs) {
43
+ let n = 0;
44
+ for (let i = 0; i < pairs.length; i++) {
45
+ const [a1, b1] = pairs[i];
46
+ for (let j = i + 1; j < pairs.length; j++) {
47
+ const [a2, b2] = pairs[j];
48
+ if ((a1 - a2) * (b1 - b2) < 0) n++;
49
+ }
50
+ }
51
+ return n;
52
+ }
53
+
54
+ /**
55
+ * @param {{id: string}[]} nodes
56
+ * @param {{from: string, to: string}[]} edges `from` is laid out left of `to`
57
+ * @param {object} [options]
58
+ * @param {(node: object) => ('source'|'sink'|null|undefined)} [options.rank]
59
+ * @param {(node: object) => {width: number, height: number}} [options.sizeOf]
60
+ * @param {number} [options.columnGap=64] between the widest node of a column and the next column
61
+ * @param {number} [options.rowGap=32] between two nodes of a column
62
+ * @param {number} [options.componentGap=80] between two components, stacked top to bottom
63
+ * @param {number} [options.passes=8] barycentre sweeps (each one down and back)
64
+ * @returns {Map<string, {x: number, y: number}>}
65
+ */
66
+ export function layeredLayout(nodes, edges, { rank = null, sizeOf = null, columnGap = 64, rowGap = 32,
67
+ componentGap = 80, passes = 8 } = {}) {
68
+ const list = [];
69
+ const index = new Map();
70
+ for (const node of Array.isArray(nodes) ? nodes : []) {
71
+ if (!node || node.id === undefined || node.id === null || index.has(String(node.id))) continue;
72
+ index.set(String(node.id), list.length);
73
+ list.push(node);
74
+ }
75
+ const count = list.length;
76
+ const positions = new Map();
77
+ if (!count) return positions;
78
+
79
+ const hint = list.map((n) => {
80
+ const r = typeof rank === 'function' ? rank(n) : null;
81
+ return r === 'source' || r === 'sink' ? r : null;
82
+ });
83
+ const measure = typeof sizeOf === 'function' ? sizeOf : defaultSizeOf;
84
+ const size = list.map((n) => {
85
+ const s = measure(n) || {};
86
+ return { width: Math.max(0, Number(s.width) || 0), height: Math.max(0, Number(s.height) || 0) };
87
+ });
88
+
89
+ // ── the lines, deduplicated, both ways ─────────────────────────────
90
+ const out = Array.from({ length: count }, () => []);
91
+ const near = Array.from({ length: count }, () => []);
92
+ const seen = new Set();
93
+ const lines = [];
94
+ for (const e of Array.isArray(edges) ? edges : []) {
95
+ const u = index.get(String(e?.from));
96
+ const v = index.get(String(e?.to));
97
+ if (u === undefined || v === undefined || u === v) continue;
98
+ const key = `${u}>${v}`;
99
+ if (seen.has(key)) continue;
100
+ seen.add(key);
101
+ out[u].push(v);
102
+ lines.push([u, v]);
103
+ if (!seen.has(`${u}~${v}`)) {
104
+ seen.add(`${u}~${v}`); seen.add(`${v}~${u}`);
105
+ near[u].push(v); near[v].push(u);
106
+ }
107
+ }
108
+
109
+ // ── 1. components, the largest first, then in the order given ─────
110
+ const componentOf = new Int32Array(count).fill(-1);
111
+ const components = [];
112
+ for (let s = 0; s < count; s++) {
113
+ if (componentOf[s] >= 0) continue;
114
+ const members = [];
115
+ const stack = [s];
116
+ componentOf[s] = components.length;
117
+ while (stack.length) {
118
+ const u = stack.pop();
119
+ members.push(u);
120
+ for (const v of near[u]) if (componentOf[v] < 0) { componentOf[v] = components.length; stack.push(v); }
121
+ }
122
+ members.sort((a, b) => a - b);
123
+ components.push(members);
124
+ }
125
+ const ordered = components.map((m, i) => ({ m, i })).sort((a, b) => (b.m.length - a.m.length) || (a.i - b.i));
126
+ const linesOf = components.map(() => []);
127
+ for (const line of lines) linesOf[componentOf[line[0]]].push(line);
128
+
129
+ // ── 2. cycles broken: a line back onto the walk is turned round ────
130
+ const reversed = new Set();
131
+ const state = new Uint8Array(count); // 0 new, 1 on the walk, 2 done
132
+ for (let s = 0; s < count; s++) {
133
+ if (state[s]) continue;
134
+ const stack = [[s, 0]];
135
+ state[s] = 1;
136
+ while (stack.length) {
137
+ const top = stack[stack.length - 1];
138
+ const [u, k] = top;
139
+ if (k < out[u].length) {
140
+ top[1] = k + 1;
141
+ const v = out[u][k];
142
+ if (state[v] === 1) reversed.add(`${u}>${v}`);
143
+ else if (state[v] === 0) { state[v] = 1; stack.push([v, 0]); }
144
+ } else {
145
+ state[u] = 2;
146
+ stack.pop();
147
+ }
148
+ }
149
+ }
150
+
151
+ // The lines that decide columns: acyclic, none into a source or out of a sink.
152
+ const preds = Array.from({ length: count }, () => []);
153
+ const succs = Array.from({ length: count }, () => []);
154
+ for (const [u0, v0] of lines) {
155
+ const [u, v] = reversed.has(`${u0}>${v0}`) ? [v0, u0] : [u0, v0];
156
+ if (hint[v] === 'source' || hint[u] === 'sink') continue;
157
+ preds[v].push(u);
158
+ succs[u].push(v);
159
+ }
160
+
161
+ // ── 3. longest path, in topological order ──────────────────────────
162
+ const layer = new Int32Array(count);
163
+ const waiting = preds.map((p) => p.length);
164
+ const queue = [];
165
+ for (let u = 0; u < count; u++) if (!waiting[u]) queue.push(u);
166
+ for (let q = 0; q < queue.length; q++) {
167
+ const u = queue[q];
168
+ for (const v of succs[u]) {
169
+ if (layer[u] + 1 > layer[v]) layer[v] = layer[u] + 1;
170
+ if (--waiting[v] === 0) queue.push(v);
171
+ }
172
+ }
173
+
174
+ let top = 0;
175
+ for (const { m, i: c } of ordered) {
176
+ // ── 4. rank hints: sources first, sinks last, no empty column ─
177
+ let last = -1;
178
+ for (const u of m) if (hint[u] !== 'sink') last = Math.max(last, hint[u] === 'source' ? 0 : layer[u]);
179
+ for (const u of m) {
180
+ if (hint[u] === 'source') layer[u] = 0;
181
+ else if (hint[u] === 'sink') layer[u] = last + 1;
182
+ }
183
+ const used = [...new Set(m.map((u) => layer[u]))].sort((a, b) => a - b);
184
+ const renumber = new Map(used.map((l, i) => [l, i]));
185
+ const columns = used.map(() => []);
186
+ for (const u of m) { layer[u] = renumber.get(layer[u]); columns[layer[u]].push(u); }
187
+
188
+ // ── 5. crossing reduction ──────────────────────────────────────
189
+ const place = new Map();
190
+ const remember = () => { for (const c of columns) c.forEach((u, i) => place.set(u, i)); };
191
+ remember();
192
+ const between = columns.map(() => []);
193
+ for (const [u, v] of linesOf[c]) {
194
+ const a = Math.min(layer[u], layer[v]);
195
+ if (Math.abs(layer[u] - layer[v]) === 1) between[a].push(layer[u] < layer[v] ? [u, v] : [v, u]);
196
+ }
197
+ const crossings = () => {
198
+ let n = 0;
199
+ for (let l = 0; l + 1 < columns.length; l++) {
200
+ n += crossingsBetween(between[l].map(([u, v]) => [place.get(u), place.get(v)]));
201
+ }
202
+ return n;
203
+ };
204
+ const sweep = (l, from) => {
205
+ const column = columns[l];
206
+ const centre = new Map();
207
+ for (const u of column) {
208
+ let sum = 0;
209
+ let k = 0;
210
+ for (const v of near[u]) {
211
+ if (from(layer[v])) { sum += place.get(v); k++; }
212
+ }
213
+ centre.set(u, k ? sum / k : place.get(u));
214
+ }
215
+ column.sort((a, b) => (centre.get(a) - centre.get(b)) || (place.get(a) - place.get(b)));
216
+ column.forEach((u, i) => place.set(u, i));
217
+ };
218
+ let best = crossings();
219
+ let bestColumns = columns.map((c) => [...c]);
220
+ for (let p = 0; p < passes && best > 0; p++) {
221
+ for (let l = 1; l < columns.length; l++) sweep(l, (x) => x < l);
222
+ for (let l = columns.length - 2; l >= 0; l--) sweep(l, (x) => x > l);
223
+ const now = crossings();
224
+ if (now < best) { best = now; bestColumns = columns.map((c) => [...c]); }
225
+ }
226
+
227
+ // ── 6. measured sizes ─────────────────────────────────────────
228
+ const widths = bestColumns.map((c) => Math.max(0, ...c.map((u) => size[u].width)));
229
+ const heights = bestColumns.map((c) => c.reduce((h, u) => h + size[u].height, 0)
230
+ + rowGap * Math.max(0, c.length - 1));
231
+ const tallest = Math.max(0, ...heights);
232
+ let x = 0;
233
+ bestColumns.forEach((c, l) => {
234
+ let y = top + (tallest - heights[l]) / 2;
235
+ for (const u of c) {
236
+ positions.set(String(list[u].id), { x, y });
237
+ y += size[u].height + rowGap;
238
+ }
239
+ x += widths[l] + columnGap;
240
+ });
241
+ top += tallest + componentGap;
242
+ }
243
+ return positions;
244
+ }
File without changes
File without changes
@@ -7,8 +7,8 @@
7
7
  * These helpers cap the points actually drawn while preserving the visual
8
8
  * shape.
9
9
  *
10
- * Two strategies, mirroring EcoSim (App/downsampler.py = LTTB,
11
- * streaming/server.py = stride decimation):
10
+ * Two strategies, the usual pair on the server side as well (LTTB for one
11
+ * line, stride decimation for a stream):
12
12
  *
13
13
  * • strideDownsampleShared — one set of indices applied to the shared x
14
14
  * axis and EVERY series. Cheap, gap-safe (nulls keep their slot), and
@@ -78,6 +78,40 @@ const PLOT_LAYOUTS = Object.freeze({
78
78
  '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' }] },
79
79
  });
80
80
 
81
+ /**
82
+ * A new y axis with no series. Axis `index` 0, 2, 4, ... sits on the left,
83
+ * 1, 3, 5, ... on the right.
84
+ * @param {number} [index=0] - its place among the subplot's y axes
85
+ */
86
+ function makeDefaultYAxis(index = 0) {
87
+ return {
88
+ id: uid('y'),
89
+ label: '',
90
+ scale: 'linear',
91
+ position: index % 2 === 0 ? 'left' : 'right',
92
+ min: null,
93
+ max: null,
94
+ step: null,
95
+ series: [],
96
+ };
97
+ }
98
+
99
+ /**
100
+ * A new series with no variable, coloured by its place in the subplot.
101
+ * @param {number} [index=0] - how many series the subplot already has
102
+ */
103
+ function makeDefaultSeries(index = 0) {
104
+ return {
105
+ id: uid('s'),
106
+ variable: '',
107
+ label: '',
108
+ color: getSeriesColor(index),
109
+ lineWidth: 2,
110
+ lineStyle: 'solid',
111
+ stackGroup: null,
112
+ };
113
+ }
114
+
81
115
  function makeDefaultSubplot(index) {
82
116
  return {
83
117
  id: uid('sp'),
@@ -87,21 +121,9 @@ function makeDefaultSubplot(index) {
87
121
  interpolation: 'linear',
88
122
  nanHandling: 'gap',
89
123
  showDataPoints: false,
90
- showHpTrend: false,
91
- showHpCycle: false,
92
- hpLambda: 1600,
93
124
  xAxis: { useTime: true, variable: '' },
94
125
  zAxis: null,
95
- yAxes: [{
96
- id: uid('y'),
97
- label: '',
98
- scale: 'linear',
99
- position: 'left',
100
- min: null,
101
- max: null,
102
- step: null,
103
- series: [],
104
- }],
126
+ yAxes: [makeDefaultYAxis(0)],
105
127
  };
106
128
  }
107
129
 
@@ -153,24 +175,26 @@ function migrateSingleChart(data) {
153
175
  nanHandling: data?.nanHandling ?? 'gap',
154
176
  showDataPoints: data?.showDataPoints ?? false,
155
177
  yAxes: [{
156
- id: uid('y'),
178
+ ...makeDefaultYAxis(0),
157
179
  label: data?.yLabel ?? '',
158
- scale: 'linear',
159
- position: 'left',
160
- min: null,
161
- max: null,
162
- series: series.map((s, i) => ({
163
- id: s.id ?? uid('s'),
164
- variable: s.variable ?? '',
165
- label: s.label ?? '',
166
- color: s.color ?? getSeriesColor(i),
167
- lineWidth: s.lineWidth ?? 2,
168
- lineStyle: s.lineStyle ?? 'solid',
169
- stackGroup: s.stackGroup ?? null,
170
- ...(s.overlay ? { overlay: s.overlay } : {}),
171
- })),
180
+ series: series.map((s, i) => {
181
+ const d = makeDefaultSeries(i);
182
+ return {
183
+ id: s.id ?? d.id,
184
+ variable: s.variable ?? d.variable,
185
+ label: s.label ?? d.label,
186
+ color: s.color ?? d.color,
187
+ lineWidth: s.lineWidth ?? d.lineWidth,
188
+ lineStyle: s.lineStyle ?? d.lineStyle,
189
+ stackGroup: s.stackGroup ?? d.stackGroup,
190
+ ...(s.overlay ? { overlay: s.overlay } : {}),
191
+ };
192
+ }),
172
193
  }],
173
194
  };
174
195
  }
175
196
 
176
- export { uid, applyNanHandling, PLOT_LAYOUTS, makeDefaultSubplot, migrateData, migrateSingleChart };
197
+ export {
198
+ uid, applyNanHandling, PLOT_LAYOUTS, makeDefaultSubplot, makeDefaultYAxis, makeDefaultSeries,
199
+ migrateData, migrateSingleChart,
200
+ };