@pixerate/editor 0.164.0 → 0.165.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 (141) hide show
  1. package/dist/agent/index.cjs +54 -39
  2. package/dist/agent/index.js +24 -9
  3. package/dist/animation/index.cjs +6 -5
  4. package/dist/animation/index.js +5 -4
  5. package/dist/animation-editor/index.cjs +65 -64
  6. package/dist/animation-editor/index.js +8 -7
  7. package/dist/bridge/cli.cjs +3 -3
  8. package/dist/bridge/cli.js +2 -2
  9. package/dist/bridge/index.cjs +3 -3
  10. package/dist/bridge/index.js +2 -2
  11. package/dist/canvas/index.cjs +89 -87
  12. package/dist/canvas/index.js +11 -9
  13. package/dist/chart/controller.d.cts +31 -2
  14. package/dist/chart/controller.d.ts +31 -2
  15. package/dist/chart/index.cjs +504 -132
  16. package/dist/chart/index.d.cts +2 -0
  17. package/dist/chart/index.d.ts +2 -0
  18. package/dist/chart/index.js +386 -14
  19. package/dist/chart/layout-families.d.cts +7 -0
  20. package/dist/chart/layout-families.d.ts +7 -0
  21. package/dist/chart/links.d.cts +34 -0
  22. package/dist/chart/links.d.ts +34 -0
  23. package/dist/chart/mermaid.d.cts +9 -6
  24. package/dist/chart/mermaid.d.ts +9 -6
  25. package/dist/chart/schemas.d.cts +3 -0
  26. package/dist/chart/schemas.d.ts +3 -0
  27. package/dist/chart/spec.d.cts +3 -2
  28. package/dist/chart/spec.d.ts +3 -2
  29. package/dist/chart/style.d.cts +33 -0
  30. package/dist/chart/style.d.ts +33 -0
  31. package/dist/chart/types.d.cts +24 -2
  32. package/dist/chart/types.d.ts +24 -2
  33. package/dist/{chunk-TALQONZ5.js → chunk-2J5VLJHC.js} +3 -3
  34. package/dist/{chunk-3NEZJCBY.js → chunk-2ZRN43IN.js} +340 -201
  35. package/dist/{chunk-ZVNAMTFO.cjs → chunk-3FSUJIXN.cjs} +18 -174
  36. package/dist/{chunk-RQCMSEM5.cjs → chunk-4QUUHTGJ.cjs} +2 -2
  37. package/dist/{chunk-CIPIESX4.cjs → chunk-4ZC5QJE2.cjs} +3 -3
  38. package/dist/{chunk-S6X2QRVU.cjs → chunk-5UEFBM2M.cjs} +1 -138
  39. package/dist/{chunk-YSAGBIRW.cjs → chunk-64VWWR5U.cjs} +16 -16
  40. package/dist/{chunk-QYZGBVNC.js → chunk-7272D3K2.js} +10 -8
  41. package/dist/{chunk-462WGGXX.cjs → chunk-AMX23CSN.cjs} +27 -16
  42. package/dist/{chunk-3VLOOO3G.js → chunk-BDIM6NVW.js} +1 -1
  43. package/dist/{chunk-POR54B55.js → chunk-CRFSQWKQ.js} +4 -1
  44. package/dist/{chunk-GZ2FM3ZB.js → chunk-DKNEQX7T.js} +6 -162
  45. package/dist/{chunk-JCXMJUQO.cjs → chunk-EGZM52EO.cjs} +3 -3
  46. package/dist/{chunk-ES3UEVUT.cjs → chunk-EUIDTZNQ.cjs} +112 -110
  47. package/dist/{chunk-H3OEM3Q2.js → chunk-F6R67I3A.js} +1 -1
  48. package/dist/{chunk-5FNGLV2D.js → chunk-FEU2XVQH.js} +15 -3
  49. package/dist/{chunk-SZA42R4N.js → chunk-FRBRLKNQ.js} +154 -16
  50. package/dist/chunk-HIDFL5LQ.js +484 -0
  51. package/dist/{chunk-MKTDELCD.js → chunk-I5MRJW7B.js} +1 -1
  52. package/dist/{chunk-SDHZG5TF.js → chunk-ILP77UD6.js} +160 -2
  53. package/dist/{chunk-GKNEJ6EN.cjs → chunk-IMRXC7OA.cjs} +241 -103
  54. package/dist/chunk-KGRZMHZB.js +204 -0
  55. package/dist/{chunk-SET6KJOP.js → chunk-LPIKXPBM.js} +1 -1
  56. package/dist/{chunk-BQWGN5WH.js → chunk-M2TPMYJE.js} +1 -1
  57. package/dist/{chunk-GTELFJRE.cjs → chunk-M5GLQQWG.cjs} +2 -2
  58. package/dist/{chunk-4VU4CP7R.js → chunk-MHLU6GRJ.js} +1 -138
  59. package/dist/{chunk-D54JHQK2.cjs → chunk-MKXK6E4O.cjs} +51 -51
  60. package/dist/{chunk-DAPQGA55.js → chunk-N7NBGT2Y.js} +2 -2
  61. package/dist/chunk-O6OGNDND.js +164 -0
  62. package/dist/{chunk-22KLZZGH.cjs → chunk-ODFCS7HF.cjs} +8 -8
  63. package/dist/{chunk-PA3IMPPS.cjs → chunk-OVYQAYGH.cjs} +81 -81
  64. package/dist/{chunk-YXOW456B.js → chunk-PPEDZ5WR.js} +1 -1
  65. package/dist/{chunk-YGG7WDWT.cjs → chunk-Q25WAFTD.cjs} +21 -18
  66. package/dist/{chunk-SBQVEBSU.cjs → chunk-QO2T6KA5.cjs} +165 -67
  67. package/dist/{chunk-24M7VD6M.cjs → chunk-S4EIAIEW.cjs} +7 -7
  68. package/dist/{chunk-M4MIZPQI.cjs → chunk-SJ2CPTQC.cjs} +189 -15
  69. package/dist/chunk-TFF2GTGN.cjs +153 -0
  70. package/dist/chunk-TTCJPX4U.cjs +204 -0
  71. package/dist/{chunk-NZD2MZJC.cjs → chunk-TVQW26AV.cjs} +35 -23
  72. package/dist/{chunk-R3P2M7HU.cjs → chunk-UVXTOZ2B.cjs} +346 -207
  73. package/dist/{chunk-WI7QJA7A.cjs → chunk-UXETV2NC.cjs} +161 -3
  74. package/dist/{chunk-Z4P2RBMJ.js → chunk-V5GSY3NC.js} +101 -3
  75. package/dist/{chunk-QY4KUDMY.js → chunk-VRXXAXUT.js} +2 -2
  76. package/dist/chunk-VU6WQSMB.cjs +164 -0
  77. package/dist/{chunk-OXERWBKE.js → chunk-X5W645RI.js} +3 -3
  78. package/dist/{chunk-NHLYK3AX.js → chunk-XBQX76CT.js} +182 -8
  79. package/dist/{chunk-5AARIS3R.cjs → chunk-XCDGM7TD.cjs} +37 -37
  80. package/dist/{chunk-EVXQNQND.js → chunk-XZB3TJ6K.js} +16 -5
  81. package/dist/chunk-YBAPT7NL.js +153 -0
  82. package/dist/chunk-Z2UQYMPS.cjs +484 -0
  83. package/dist/color/index.cjs +5 -3
  84. package/dist/color/index.js +12 -10
  85. package/dist/diagram/index.cjs +55 -243
  86. package/dist/diagram/index.js +12 -200
  87. package/dist/export/index.cjs +7 -6
  88. package/dist/export/index.js +6 -5
  89. package/dist/extensions/index.cjs +12 -9
  90. package/dist/extensions/index.js +11 -8
  91. package/dist/gradient/index.cjs +29 -27
  92. package/dist/gradient/index.js +6 -4
  93. package/dist/image-editor/index.cjs +644 -643
  94. package/dist/image-editor/index.js +7 -6
  95. package/dist/index.cjs +21 -18
  96. package/dist/index.js +14 -11
  97. package/dist/paint-editor/index.cjs +19 -18
  98. package/dist/paint-editor/index.js +7 -6
  99. package/dist/parameters/index.cjs +2 -2
  100. package/dist/parameters/index.js +1 -1
  101. package/dist/persistence/assetWrites.d.cts +22 -0
  102. package/dist/persistence/assetWrites.d.ts +22 -0
  103. package/dist/persistence/assets.d.cts +71 -0
  104. package/dist/persistence/assets.d.ts +71 -0
  105. package/dist/persistence/documentStore.d.cts +17 -1
  106. package/dist/persistence/documentStore.d.ts +17 -1
  107. package/dist/persistence/documentTools.d.cts +5 -1
  108. package/dist/persistence/documentTools.d.ts +5 -1
  109. package/dist/persistence/editorAutosave.d.cts +7 -0
  110. package/dist/persistence/editorAutosave.d.ts +7 -0
  111. package/dist/persistence/index.cjs +141 -21
  112. package/dist/persistence/index.js +130 -10
  113. package/dist/persistence/storage.d.cts +13 -1
  114. package/dist/persistence/storage.d.ts +13 -1
  115. package/dist/spreadsheet/index.cjs +3 -3
  116. package/dist/spreadsheet/index.js +2 -2
  117. package/dist/tokens/index.cjs +11 -3
  118. package/dist/tokens/index.d.cts +1 -0
  119. package/dist/tokens/index.d.ts +1 -0
  120. package/dist/tokens/index.js +10 -2
  121. package/dist/tokens/style.d.cts +36 -0
  122. package/dist/tokens/style.d.ts +36 -0
  123. package/dist/tools/index.cjs +15 -3
  124. package/dist/tools/index.d.cts +2 -0
  125. package/dist/tools/index.d.ts +2 -0
  126. package/dist/tools/index.js +14 -2
  127. package/dist/tools/style-tools.d.cts +19 -0
  128. package/dist/tools/style-tools.d.ts +19 -0
  129. package/dist/tools/style.d.cts +212 -0
  130. package/dist/tools/style.d.ts +212 -0
  131. package/dist/tools/view.d.cts +6 -0
  132. package/dist/tools/view.d.ts +6 -0
  133. package/dist/typography/index.cjs +8 -6
  134. package/dist/typography/index.js +5 -3
  135. package/dist/vector/index.cjs +953 -952
  136. package/dist/vector/index.js +7 -6
  137. package/dist/video-editor/index.cjs +8 -7
  138. package/dist/video-editor/index.js +6 -5
  139. package/package.json +1 -1
  140. package/dist/chunk-ERAGQL2N.cjs +0 -68
  141. package/dist/chunk-KMU6QJB7.js +0 -68
@@ -1,3 +1,7 @@
1
+ import {
2
+ parseSankey,
3
+ parseTreemap
4
+ } from "./chunk-KGRZMHZB.js";
1
5
  import {
2
6
  chartMovingAverage,
3
7
  chartSupportsTrendlines,
@@ -7,20 +11,23 @@ import {
7
11
  r2,
8
12
  sameChartSelection,
9
13
  summarizeChartSeries
10
- } from "./chunk-Z4P2RBMJ.js";
14
+ } from "./chunk-V5GSY3NC.js";
11
15
  import {
12
16
  CHART_TYPE_LABELS,
13
17
  DEFAULT_CHART_PALETTE,
14
18
  DEFAULT_CHART_THEME,
15
19
  chartDataFromTable,
16
20
  chartFamily,
21
+ chartLinksFromTable,
17
22
  chartSeriesColor,
18
23
  chartSliceColor,
19
24
  chartSpecColorsByCategory,
20
25
  chartTreeDepth,
21
26
  chartTreeFromRows,
27
+ chartTreeFromSeries,
22
28
  chartTreeFromTable,
23
29
  chartTreeToRows,
30
+ chartTreeTotal,
24
31
  checkChartColors,
25
32
  cloneChartSeries,
26
33
  cloneChartSpec,
@@ -28,6 +35,7 @@ import {
28
35
  defaultChartAxis,
29
36
  describeChartErrorBars,
30
37
  findChartTreeNode,
38
+ normalizeChartLinks,
31
39
  normalizeChartReferenceBand,
32
40
  normalizeChartReferenceLine,
33
41
  normalizeChartSpec,
@@ -36,7 +44,7 @@ import {
36
44
  parseChartValue,
37
45
  shadeChartColor,
38
46
  uniqueChartSeriesId
39
- } from "./chunk-3NEZJCBY.js";
47
+ } from "./chunk-2ZRN43IN.js";
40
48
  import {
41
49
  parseCsv,
42
50
  parseTsv
@@ -71,9 +79,14 @@ function splitOutside(text, sep) {
71
79
  out.push(cur);
72
80
  return out;
73
81
  }
82
+ var csvField = (text) => /[",\n]|^\s|\s$/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
83
+ var treemapName = (name) => !name.includes('"') ? `"${name}"` : !name.includes("'") ? `'${name}'` : `"${name.replace(/"/g, "'")}"`;
84
+ var yamlText = (text) => /^[\w][\w .,!?()/&+-]*$/.test(text) && !/:\s/.test(text) ? text : JSON.stringify(text);
74
85
  function chartMermaidFamily(spec) {
75
86
  const family = chartFamily(spec);
76
- if (family === "pie" || family === "funnel" || family === "treemap" || family === "parliament" || family === "sunburst") return "pie";
87
+ if (family === "sankey" && spec.links?.length) return "sankey";
88
+ if (family === "treemap" || family === "sunburst") return "treemap";
89
+ if (family === "pie" || family === "funnel" || family === "parliament") return "pie";
77
90
  if (family === "radar") return "radar";
78
91
  return "xychart";
79
92
  }
@@ -114,7 +127,27 @@ function chartToMermaid(spec, options = {}) {
114
127
  const visible = spec.series.filter((s) => !s.hidden);
115
128
  const lines = [];
116
129
  const v = (x) => x === null || !Number.isFinite(x) ? "0" : num(x);
117
- if (family === "pie") {
130
+ const front = [];
131
+ if (family === "sankey") {
132
+ if (spec.title) front.push("---", `title: ${yamlText(spec.title)}`, "---");
133
+ lines.push("sankey-beta");
134
+ for (const l of spec.links) lines.push(`${csvField(l.source)},${csvField(l.target)},${num(l.value)}`);
135
+ } else if (family === "treemap") {
136
+ lines.push("treemap-beta");
137
+ if (spec.title) lines.push(`title ${spec.title}`);
138
+ const first = visible[0] ?? spec.series[0];
139
+ const tree = spec.tree.length ? spec.tree : chartTreeFromSeries(spec.categories, first?.values ?? []);
140
+ const write = (nodes, depth) => {
141
+ for (const n of nodes) {
142
+ const pad = ind.repeat(depth);
143
+ if (n.children) {
144
+ lines.push(`${pad}${treemapName(n.name)}`);
145
+ write(n.children, depth + 1);
146
+ } else lines.push(`${pad}${treemapName(n.name)}: ${num(n.value ?? 0)}`);
147
+ }
148
+ };
149
+ write(tree, 1);
150
+ } else if (family === "pie") {
118
151
  const s = visible[0] ?? spec.series[0];
119
152
  lines.push(`pie${spec.dataLabels ? " showData" : ""}`);
120
153
  if (spec.title) lines.push(`${ind}title ${spec.title}`);
@@ -160,13 +193,13 @@ function chartToMermaid(spec, options = {}) {
160
193
  lines.push(`${ind}${kind} ${quote(s.name)} [${s.values.map(v).join(", ")}]`);
161
194
  }
162
195
  }
163
- let text = `${lines.join("\n")}
196
+ let text = `${[...front, ...lines].join("\n")}
164
197
  `;
165
198
  if (options.meta !== false) {
166
199
  const meta = metaFor(spec, text);
167
200
  if (meta) {
168
201
  const [header, ...rest] = lines;
169
- text = `${[header, `${CHART_MERMAID_META_PREFIX}${JSON.stringify({ v: 1, ...meta })}`, ...rest].join("\n")}
202
+ text = `${[...front, header, `${CHART_MERMAID_META_PREFIX}${JSON.stringify({ v: 1, ...meta })}`, ...rest].join("\n")}
170
203
  `;
171
204
  }
172
205
  }
@@ -229,7 +262,9 @@ function chartFromMermaid(text, options = {}) {
229
262
  showData = !!m[1];
230
263
  if (m[2]) frontTitle = m[2].trim();
231
264
  } else if (/^radar(?:-beta)?\s*$/i.test(t)) family = "radar";
232
- else issues.push({ path: "header", line: i + 1, message: `Line ${i + 1}: expected a chart header (xychart-beta, pie or radar-beta), got "${t.slice(0, 40)}".` });
265
+ else if (/^sankey(?:-beta)?\s*$/i.test(t)) family = "sankey";
266
+ else if (/^treemap(?:-beta)?\s*$/i.test(t)) family = "treemap";
267
+ else issues.push({ path: "header", line: i + 1, message: `Line ${i + 1}: expected a chart header (xychart-beta, pie, radar-beta, sankey-beta or treemap-beta), got "${t.slice(0, 40)}".` });
233
268
  headerLine = i;
234
269
  break;
235
270
  }
@@ -245,6 +280,7 @@ function chartFromMermaid(text, options = {}) {
245
280
  let graticule = "circle";
246
281
  const counts = { bar: 0, line: 0 };
247
282
  const lineIssue = (i, message) => issues.push({ path: `line ${i + 1}`, line: i + 1, message: `Line ${i + 1}: ${message}` });
283
+ const whole = family === "sankey" || family === "treemap";
248
284
  for (let i = headerLine + 1; family && i < all.length; i++) {
249
285
  const raw = all[i];
250
286
  const t = raw.trim();
@@ -260,7 +296,7 @@ function chartFromMermaid(text, options = {}) {
260
296
  }
261
297
  continue;
262
298
  }
263
- if (t.startsWith("%%")) continue;
299
+ if (t.startsWith("%%") || whole) continue;
264
300
  const isLast = i === all.length - 1 || all.slice(i + 1).every((l) => !l.trim());
265
301
  let m;
266
302
  if (m = /^title\s+(.+)$/i.exec(t)) {
@@ -362,7 +398,8 @@ function chartFromMermaid(text, options = {}) {
362
398
  if (!(partial && isLast)) lineIssue(i, `could not read "${t.slice(0, 60)}"; it was skipped.`);
363
399
  }
364
400
  let input;
365
- if (family === "pie") {
401
+ if (whole) input = readWholeMermaid(family, all, headerLine, partial, title, issues);
402
+ else if (family === "pie") {
366
403
  input = {
367
404
  type: "pie",
368
405
  title,
@@ -398,18 +435,54 @@ function chartFromMermaid(text, options = {}) {
398
435
  }
399
436
  }
400
437
  const { spec, issues: specIssues } = normalizeChartSpec(input);
401
- if (!family && !partial && !issues.length) issues.push({ path: "header", message: "The text is empty: expected a chart header (xychart-beta, pie or radar-beta)." });
438
+ if (!family && !partial && !issues.length) issues.push({ path: "header", message: "The text is empty: expected a chart header (xychart-beta, pie, radar-beta, sankey-beta or treemap-beta)." });
402
439
  return { spec, issues: [...issues, ...specIssues], family, hasMeta: !!meta };
403
440
  }
441
+ function readWholeMermaid(family, all, headerLine, partial, title, issues) {
442
+ const body = all.slice(headerLine).map((l) => l.trim().startsWith(CHART_MERMAID_META_PREFIX.trim()) ? "%%" : l);
443
+ const parse = (lines) => family === "sankey" ? parseSankey(lines.join("\n")) : parseTreemap(lines.join("\n"));
444
+ let read = parse(body);
445
+ if ("error" in read && partial && body.length > 1) read = parse(body.slice(0, -1));
446
+ const lineOf = (n) => n === void 0 ? void 0 : n + headerLine;
447
+ if ("error" in read) {
448
+ const line = lineOf(read.line);
449
+ if (!(partial && body.length <= 2)) issues.push({ path: family, ...line ? { line } : {}, message: line ? `Line ${line}: ${read.error}` : read.error });
450
+ return { type: family === "sankey" ? "sankey" : "treemap", title };
451
+ }
452
+ if (family === "sankey") {
453
+ const data2 = read.data;
454
+ return { type: "sankey", title, links: data2.links, sankeyNodes: data2.nodes, series: [{ id: "series-1", name: "Values", values: [] }] };
455
+ }
456
+ const data = read.data;
457
+ let items = data.items;
458
+ let name = data.title ?? title;
459
+ if (items.length === 1 && items[0].children?.length) {
460
+ name ||= items[0].name;
461
+ items = items[0].children;
462
+ }
463
+ const toNode = (item) => item.children?.length ? { name: item.name, children: item.children.map(toNode) } : { name: item.name, value: item.value ?? 0 };
464
+ const tree = items.map(toNode);
465
+ const leaf = (n) => !n.children;
466
+ const flatOrOneLevel = tree.every(leaf) || tree.every((n) => n.children?.every(leaf));
467
+ return { type: flatOrOneLevel ? "treemap" : "sunburst", title: name, tree, categories: tree.map((n) => n.name), series: [{ id: "series-1", name: "Values", values: tree.map(chartTreeTotal) }] };
468
+ }
404
469
  function findMermaidCharts(text) {
405
470
  const lines = text.replace(/^/, "").split(/\r?\n/);
406
471
  const found = [];
407
472
  const familyOf = (body) => {
408
- const header = body.split(/\r?\n/).find((l) => l.trim() && !l.trim().startsWith("%%") && l.trim() !== "---");
473
+ let rows = body.split(/\r?\n/);
474
+ const firstRow = rows.findIndex((l) => l.trim());
475
+ if (rows[firstRow]?.trim() === "---") {
476
+ const end = rows.findIndex((l, i) => i > firstRow && l.trim() === "---");
477
+ if (end > 0) rows = rows.slice(end + 1);
478
+ }
479
+ const header = rows.find((l) => l.trim() && !l.trim().startsWith("%%"));
409
480
  if (!header) return null;
410
481
  if (/^\s*xychart(-beta)?\b/.test(header)) return "xychart";
411
482
  if (/^\s*pie\b/.test(header)) return "pie";
412
483
  if (/^\s*radar(-beta)?\b/.test(header)) return "radar";
484
+ if (/^\s*sankey(-beta)?\s*$/.test(header)) return "sankey";
485
+ if (/^\s*treemap(-beta)?\s*$/.test(header)) return "treemap";
413
486
  return null;
414
487
  };
415
488
  for (let i = 0; i < lines.length; i++) {
@@ -476,6 +549,14 @@ function describeChart(spec, series = spec.series.filter((s) => !s.hidden).map((
476
549
  }
477
550
  return parts.join(" ");
478
551
  }
552
+ if (family === "sankey") {
553
+ if (!spec.links.length) return `${head} with no flows yet.`;
554
+ const total = spec.links.reduce((t, l) => t + l.value, 0);
555
+ const largest = [...spec.links].sort((a, b) => b.value - a.value)[0];
556
+ const sources = spec.sankeyNodes.filter((n) => !spec.links.some((l) => l.target === n));
557
+ const sinks = spec.sankeyNodes.filter((n) => !spec.links.some((l) => l.source === n));
558
+ return `${head} of ${spec.links.length} flows between ${spec.sankeyNodes.length} nodes, from ${sources.join(", ")} to ${sinks.join(", ")}. The largest flow is ${largest.source} \u2192 ${largest.target} (${fmt(largest.value)}); all flows add up to ${fmt(total)}.`;
559
+ }
479
560
  if (family === "parliament") {
480
561
  const s = spec.series.find((x) => !x.hidden);
481
562
  const seats = spec.categories.map((c, i) => ({ c, n: Math.max(0, Math.round(s.values[i] ?? 0)) }));
@@ -910,7 +991,7 @@ var ChartController = class {
910
991
  * to the categories; all-or-nothing like `update`.
911
992
  */
912
993
  setData(data, options = {}) {
913
- const clear = (data.categories || data.series) && this.spec.tree.length ? { tree: [] } : {};
994
+ const clear = (data.categories || data.series) && (this.spec.tree.length || this.spec.links.length) ? { tree: [], links: [] } : {};
914
995
  return this.update({ ...data.categories ? { categories: data.categories } : {}, ...data.series ? { series: data.series } : {}, ...clear }, { label: options.label ?? "Edit chart data" });
915
996
  }
916
997
  /**
@@ -1153,7 +1234,7 @@ var ChartController = class {
1153
1234
  const old = byName.get(s.name);
1154
1235
  return { ...s, ...old?.color ? { color: old.color } : {}, ...old?.type ? { type: old.type } : {}, ...old ? { id: old.id } : {} };
1155
1236
  });
1156
- const { spec } = normalizeChartSpec({ ...this.spec, categories: data.categories, series, tree: [] });
1237
+ const { spec } = normalizeChartSpec({ ...this.spec, categories: data.categories, series, tree: [], links: [] });
1157
1238
  this.commit("Import data", spec);
1158
1239
  if (data.issues.length) {
1159
1240
  this.issues = data.issues;
@@ -1170,8 +1251,12 @@ var ChartController = class {
1170
1251
  // -------------------------------------------------------------------------
1171
1252
  // Grouped data (tree)
1172
1253
  // -------------------------------------------------------------------------
1173
- /** A refusal while the data is a tree (cell edits would be overwritten by its totals), else null. */
1254
+ /** A refusal while the data is a tree or flows (cell edits would be overwritten by their totals), else null. */
1174
1255
  treeLocked() {
1256
+ if (this.spec.links.length)
1257
+ return this.reject(
1258
+ "This chart's data is flows (links): its categories are the nodes and its values how much passes through each. Change it with setLinks / the flow edits (set_chart_links, add_chart_link, update_chart_link, remove_chart_link), or replace it with new data."
1259
+ );
1175
1260
  if (!this.spec.tree.length) return null;
1176
1261
  return this.reject(
1177
1262
  "This chart's data is grouped (a tree): its categories and values are the top-level groups and their totals. Change it with setTree / the tree node edits (set_chart_tree, add_chart_tree_node, update_chart_tree_node, remove_chart_tree_node), or replace it with new data."
@@ -1182,7 +1267,7 @@ var ChartController = class {
1182
1267
  const issues = [];
1183
1268
  const next = normalizeChartTree(tree, issues);
1184
1269
  if (issues.length) return this.reject(issues.map((i) => i.message).join(" "), issues);
1185
- return this.update({ tree: next }, { label: options.label ?? (next.length ? "Replace grouped data" : "Ungroup data") });
1270
+ return this.update({ tree: next, links: [] }, { label: options.label ?? (next.length ? "Replace grouped data" : "Ungroup data") });
1186
1271
  }
1187
1272
  /**
1188
1273
  * Replaces the grouped data from rows of names and a value (group, …, item, value), as the
@@ -1208,7 +1293,7 @@ var ChartController = class {
1208
1293
  const next = chartTreeFromTable(rows, options, issues);
1209
1294
  if (issues.length) return this.reject(issues.map((i) => i.message).join(" "), issues);
1210
1295
  if (!next.length) return this.reject('There is no grouped data to import: expected rows of names followed by a value, e.g. "Europe,France,40".');
1211
- return this.update({ tree: next }, { label: "Import grouped data" });
1296
+ return this.update({ tree: next, links: [] }, { label: "Import grouped data" });
1212
1297
  }
1213
1298
  nodeAt(path) {
1214
1299
  const node = findChartTreeNode(this.spec.tree, path);
@@ -1281,6 +1366,56 @@ var ChartController = class {
1281
1366
  return this.update({ tree }, { label: `Delete "${node.name}"` });
1282
1367
  }
1283
1368
  // -------------------------------------------------------------------------
1369
+ // Flows (links)
1370
+ // -------------------------------------------------------------------------
1371
+ /**
1372
+ * Replaces the flows (and, when given, the node order). An empty list goes back to the
1373
+ * table, keeping how much passed through each node as values. One undo step, all-or-nothing.
1374
+ */
1375
+ setLinks(links, options = {}) {
1376
+ const issues = [];
1377
+ const next = normalizeChartLinks(links, issues);
1378
+ if (issues.length) return this.reject(issues.map((i) => i.message).join(" "), issues);
1379
+ const used = new Set(next.flatMap((l) => [l.source, l.target]));
1380
+ const sankeyNodes = options.nodes ? [...options.nodes] : this.spec.sankeyNodes.filter((n) => used.has(n));
1381
+ return this.update({ links: next, tree: [], sankeyNodes }, { label: options.label ?? (next.length ? "Replace flows" : "Remove flows") });
1382
+ }
1383
+ /** Replaces the data with flows from a table of source, target and value columns. */
1384
+ importLinksTable(rows, options = {}) {
1385
+ const raw = chartLinksFromTable(rows, options);
1386
+ if (!raw.length) return this.reject('There are no flows to import: expected rows of source, target and value, e.g. "Website,Signup,120".');
1387
+ return this.setLinks(raw, { label: "Import flows" });
1388
+ }
1389
+ badLink(index) {
1390
+ const n = this.spec.links.length;
1391
+ return fail(`There is no flow ${index}; ${n ? `expected 0 to ${n - 1}` : "the chart has none yet"}.`);
1392
+ }
1393
+ /** Adds a flow and returns its index. */
1394
+ addLink(link) {
1395
+ const result = this.setLinks([...this.spec.links, link], { label: `Add flow ${String(link?.source ?? "")} \u2192 ${String(link?.target ?? "")}` });
1396
+ if (!result.ok) return result;
1397
+ return { ok: true, index: this.spec.links.length - 1 };
1398
+ }
1399
+ /** Changes a flow's source, target, value or colour (`color: null` goes back to the default). */
1400
+ updateLink(index, patch) {
1401
+ const current = this.spec.links[index];
1402
+ if (!current) return this.badLink(index);
1403
+ const merged = { ...current, ...patch };
1404
+ if (patch.color === null) delete merged.color;
1405
+ return this.setLinks(
1406
+ this.spec.links.map((l, i) => i === index ? merged : l),
1407
+ { label: "Change flow" }
1408
+ );
1409
+ }
1410
+ removeLink(index) {
1411
+ const current = this.spec.links[index];
1412
+ if (!current) return this.badLink(index);
1413
+ return this.setLinks(
1414
+ this.spec.links.filter((_, i) => i !== index),
1415
+ { label: `Delete flow ${current.source} \u2192 ${current.target}` }
1416
+ );
1417
+ }
1418
+ // -------------------------------------------------------------------------
1284
1419
  // Annotations
1285
1420
  // -------------------------------------------------------------------------
1286
1421
  /** Sets the note written under the chart ("" removes it). */
@@ -1675,6 +1810,9 @@ function updateLabel(patch) {
1675
1810
  parliamentLabel: "Edit seat caption",
1676
1811
  sunburstLabel: "Edit centre caption",
1677
1812
  tree: "Replace grouped data",
1813
+ links: "Replace flows",
1814
+ sankeyNodes: "Reorder nodes",
1815
+ sankeyLinkColor: "Change flow colours",
1678
1816
  radarTicks: "Change radar rings",
1679
1817
  radarGraticule: "Change radar grid",
1680
1818
  legend: "Change legend",