@pixerate/editor 0.164.0 → 0.166.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 (146) 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 +60 -2
  14. package/dist/chart/controller.d.ts +60 -2
  15. package/dist/chart/index.cjs +657 -163
  16. package/dist/chart/index.d.cts +3 -0
  17. package/dist/chart/index.d.ts +3 -0
  18. package/dist/chart/index.js +512 -18
  19. package/dist/chart/layout-families.d.cts +14 -0
  20. package/dist/chart/layout-families.d.ts +14 -0
  21. package/dist/chart/layout-shared.d.cts +5 -0
  22. package/dist/chart/layout-shared.d.ts +5 -0
  23. package/dist/chart/links.d.cts +34 -0
  24. package/dist/chart/links.d.ts +34 -0
  25. package/dist/chart/mermaid.d.cts +9 -6
  26. package/dist/chart/mermaid.d.ts +9 -6
  27. package/dist/chart/race.d.cts +26 -0
  28. package/dist/chart/race.d.ts +26 -0
  29. package/dist/chart/schemas.d.cts +5 -0
  30. package/dist/chart/schemas.d.ts +5 -0
  31. package/dist/chart/spec.d.cts +3 -2
  32. package/dist/chart/spec.d.ts +3 -2
  33. package/dist/chart/style.d.cts +33 -0
  34. package/dist/chart/style.d.ts +33 -0
  35. package/dist/chart/types.d.cts +28 -2
  36. package/dist/chart/types.d.ts +28 -2
  37. package/dist/{chunk-ZVNAMTFO.cjs → chunk-3FSUJIXN.cjs} +18 -174
  38. package/dist/{chunk-RQCMSEM5.cjs → chunk-4QUUHTGJ.cjs} +2 -2
  39. package/dist/{chunk-CIPIESX4.cjs → chunk-4ZC5QJE2.cjs} +3 -3
  40. package/dist/{chunk-S6X2QRVU.cjs → chunk-5UEFBM2M.cjs} +1 -138
  41. package/dist/{chunk-5FNGLV2D.js → chunk-5UNQDC3Q.js} +18 -3
  42. package/dist/{chunk-YSAGBIRW.cjs → chunk-6FYIFGBW.cjs} +16 -16
  43. package/dist/{chunk-QYZGBVNC.js → chunk-7272D3K2.js} +10 -8
  44. package/dist/{chunk-462WGGXX.cjs → chunk-AMX23CSN.cjs} +27 -16
  45. package/dist/{chunk-3VLOOO3G.js → chunk-BDIM6NVW.js} +1 -1
  46. package/dist/{chunk-R3P2M7HU.cjs → chunk-BPX4Z3N3.cjs} +355 -207
  47. package/dist/{chunk-D54JHQK2.cjs → chunk-CHLV7UFP.cjs} +51 -51
  48. package/dist/{chunk-OXERWBKE.js → chunk-DFCAR7AW.js} +3 -3
  49. package/dist/{chunk-GZ2FM3ZB.js → chunk-DKNEQX7T.js} +6 -162
  50. package/dist/{chunk-JCXMJUQO.cjs → chunk-EGZM52EO.cjs} +3 -3
  51. package/dist/{chunk-ES3UEVUT.cjs → chunk-EUIDTZNQ.cjs} +112 -110
  52. package/dist/{chunk-H3OEM3Q2.js → chunk-F6R67I3A.js} +1 -1
  53. package/dist/{chunk-3NEZJCBY.js → chunk-FR432ROA.js} +349 -201
  54. package/dist/chunk-HIDFL5LQ.js +484 -0
  55. package/dist/{chunk-22KLZZGH.cjs → chunk-HXOBOHH2.cjs} +8 -8
  56. package/dist/{chunk-MKTDELCD.js → chunk-I5MRJW7B.js} +1 -1
  57. package/dist/{chunk-SDHZG5TF.js → chunk-ILP77UD6.js} +160 -2
  58. package/dist/{chunk-Z4P2RBMJ.js → chunk-ITANXMZD.js} +177 -3
  59. package/dist/chunk-KGRZMHZB.js +204 -0
  60. package/dist/{chunk-SET6KJOP.js → chunk-LPIKXPBM.js} +1 -1
  61. package/dist/{chunk-BQWGN5WH.js → chunk-M2TPMYJE.js} +1 -1
  62. package/dist/{chunk-GTELFJRE.cjs → chunk-M5GLQQWG.cjs} +2 -2
  63. package/dist/{chunk-4VU4CP7R.js → chunk-MHLU6GRJ.js} +1 -138
  64. package/dist/{chunk-NZD2MZJC.cjs → chunk-MZFZSVUJ.cjs} +38 -23
  65. package/dist/{chunk-DAPQGA55.js → chunk-NQQDGGXY.js} +2 -2
  66. package/dist/chunk-O6OGNDND.js +164 -0
  67. package/dist/{chunk-PA3IMPPS.cjs → chunk-OVYQAYGH.cjs} +81 -81
  68. package/dist/{chunk-YXOW456B.js → chunk-PPEDZ5WR.js} +1 -1
  69. package/dist/{chunk-24M7VD6M.cjs → chunk-S4EIAIEW.cjs} +7 -7
  70. package/dist/{chunk-M4MIZPQI.cjs → chunk-SJ2CPTQC.cjs} +189 -15
  71. package/dist/{chunk-YGG7WDWT.cjs → chunk-SZWVMB3B.cjs} +24 -18
  72. package/dist/chunk-TFF2GTGN.cjs +153 -0
  73. package/dist/chunk-TTCJPX4U.cjs +204 -0
  74. package/dist/{chunk-SBQVEBSU.cjs → chunk-UUUTEVX6.cjs} +242 -68
  75. package/dist/{chunk-WI7QJA7A.cjs → chunk-UXETV2NC.cjs} +161 -3
  76. package/dist/{chunk-GKNEJ6EN.cjs → chunk-V5L2G22R.cjs} +321 -107
  77. package/dist/{chunk-TALQONZ5.js → chunk-VB2CY6EG.js} +3 -3
  78. package/dist/{chunk-QY4KUDMY.js → chunk-VRXXAXUT.js} +2 -2
  79. package/dist/chunk-VU6WQSMB.cjs +164 -0
  80. package/dist/{chunk-NHLYK3AX.js → chunk-XBQX76CT.js} +182 -8
  81. package/dist/{chunk-5AARIS3R.cjs → chunk-XCDGM7TD.cjs} +37 -37
  82. package/dist/{chunk-POR54B55.js → chunk-XNU45TYA.js} +7 -1
  83. package/dist/{chunk-EVXQNQND.js → chunk-XZB3TJ6K.js} +16 -5
  84. package/dist/chunk-YBAPT7NL.js +153 -0
  85. package/dist/{chunk-SZA42R4N.js → chunk-YUFTVNLX.js} +230 -16
  86. package/dist/chunk-Z2UQYMPS.cjs +484 -0
  87. package/dist/color/index.cjs +5 -3
  88. package/dist/color/index.js +12 -10
  89. package/dist/diagram/index.cjs +55 -243
  90. package/dist/diagram/index.js +12 -200
  91. package/dist/export/index.cjs +7 -6
  92. package/dist/export/index.js +6 -5
  93. package/dist/extensions/index.cjs +12 -9
  94. package/dist/extensions/index.js +11 -8
  95. package/dist/gradient/index.cjs +29 -27
  96. package/dist/gradient/index.js +6 -4
  97. package/dist/image-editor/index.cjs +644 -643
  98. package/dist/image-editor/index.js +7 -6
  99. package/dist/index.cjs +21 -18
  100. package/dist/index.js +14 -11
  101. package/dist/paint-editor/index.cjs +19 -18
  102. package/dist/paint-editor/index.js +7 -6
  103. package/dist/parameters/index.cjs +2 -2
  104. package/dist/parameters/index.js +1 -1
  105. package/dist/persistence/assetWrites.d.cts +22 -0
  106. package/dist/persistence/assetWrites.d.ts +22 -0
  107. package/dist/persistence/assets.d.cts +71 -0
  108. package/dist/persistence/assets.d.ts +71 -0
  109. package/dist/persistence/documentStore.d.cts +17 -1
  110. package/dist/persistence/documentStore.d.ts +17 -1
  111. package/dist/persistence/documentTools.d.cts +5 -1
  112. package/dist/persistence/documentTools.d.ts +5 -1
  113. package/dist/persistence/editorAutosave.d.cts +7 -0
  114. package/dist/persistence/editorAutosave.d.ts +7 -0
  115. package/dist/persistence/index.cjs +141 -21
  116. package/dist/persistence/index.js +130 -10
  117. package/dist/persistence/storage.d.cts +13 -1
  118. package/dist/persistence/storage.d.ts +13 -1
  119. package/dist/spreadsheet/index.cjs +3 -3
  120. package/dist/spreadsheet/index.js +2 -2
  121. package/dist/tokens/index.cjs +11 -3
  122. package/dist/tokens/index.d.cts +1 -0
  123. package/dist/tokens/index.d.ts +1 -0
  124. package/dist/tokens/index.js +10 -2
  125. package/dist/tokens/style.d.cts +36 -0
  126. package/dist/tokens/style.d.ts +36 -0
  127. package/dist/tools/index.cjs +15 -3
  128. package/dist/tools/index.d.cts +2 -0
  129. package/dist/tools/index.d.ts +2 -0
  130. package/dist/tools/index.js +14 -2
  131. package/dist/tools/style-tools.d.cts +19 -0
  132. package/dist/tools/style-tools.d.ts +19 -0
  133. package/dist/tools/style.d.cts +212 -0
  134. package/dist/tools/style.d.ts +212 -0
  135. package/dist/tools/view.d.cts +6 -0
  136. package/dist/tools/view.d.ts +6 -0
  137. package/dist/typography/index.cjs +8 -6
  138. package/dist/typography/index.js +5 -3
  139. package/dist/vector/index.cjs +953 -952
  140. package/dist/vector/index.js +7 -6
  141. package/dist/video-editor/index.cjs +8 -7
  142. package/dist/video-editor/index.js +6 -5
  143. package/package.json +1 -1
  144. package/skills/pixerate-chart/SKILL.md +20 -2
  145. package/dist/chunk-ERAGQL2N.cjs +0 -68
  146. package/dist/chunk-KMU6QJB7.js +0 -68
@@ -253,6 +253,12 @@ var ToolRegistryView = class {
253
253
  return set.tools.some((t) => this.enabledTools.has(t)) ? "some" : "none";
254
254
  };
255
255
  const listedCount = () => this.listed(this.catalogue()).length;
256
+ const styleOf = async (set) => {
257
+ const domain = set.skill?.replace(/^pixerate-/, "");
258
+ if (!discovery.styles || !domain || !discovery.styles.domains.some((d) => d.id === domain)) return void 0;
259
+ const style = await discovery.styles.resolve(domain);
260
+ return style.skills.length ? { domain, skills: style.skills.length, layers: [...new Set(style.skills.map((s) => s.layer))] } : void 0;
261
+ };
256
262
  const tools = [
257
263
  {
258
264
  name: "list_toolsets",
@@ -260,16 +266,18 @@ var ToolRegistryView = class {
260
266
  description: "Lists the groups of editor tools available here (one per open editor or feature): name, title, what it covers, its agent skill, how many tools it has and whether they are in your tool list (`enabled`: all, some or none). Start here, then call search_tools or enable_tools.",
261
267
  annotations: READ_ONLY,
262
268
  inputSchema: { type: "object", properties: {}, additionalProperties: false },
263
- handler: () => {
269
+ handler: async () => {
264
270
  const cat = this.catalogue();
271
+ const styles = await Promise.all(cat.sets.map(styleOf));
265
272
  return {
266
- toolsets: cat.sets.map((set) => ({
273
+ toolsets: cat.sets.map((set, i) => ({
267
274
  name: set.name,
268
275
  ...set.title ? { title: set.title } : {},
269
276
  ...set.description ? { description: set.description } : {},
270
277
  ...set.skill ? { skill: set.skill } : {},
271
278
  tools: set.tools.length,
272
- enabled: enabledState(set)
279
+ enabled: enabledState(set),
280
+ ...styles[i] ? { style: styles[i] } : {}
273
281
  })),
274
282
  ...skills.length > 0 ? { skills: skills.map((s) => s.name) } : {}
275
283
  };
@@ -312,9 +320,12 @@ var ToolRegistryView = class {
312
320
  description: "Adds tools to your tool list: single tools by name (from search_tools) and/or whole toolsets (from list_toolsets). Returns the names added and how many tools are listed now. Prefer single tools over large toolsets. Unknown names fail and change nothing.",
313
321
  annotations: LISTING,
314
322
  inputSchema: SELECTION_SCHEMA,
315
- handler: (args) => {
323
+ handler: async (args) => {
316
324
  const added = this.enableTools(args);
317
- return { added, listed: listedCount() };
325
+ const sets = this.catalogue().sets.filter((set) => set.tools.some((t) => added.includes(t)));
326
+ const domains = (await Promise.all(sets.map(styleOf))).filter((s) => s !== void 0).map((s) => s.domain);
327
+ const style = [...new Set(domains)].map((d) => `House style applies: call get_style with domain "${d}" before making changes.`);
328
+ return { added, listed: listedCount(), ...style.length ? { style } : {} };
318
329
  }
319
330
  },
320
331
  {
@@ -0,0 +1,153 @@
1
+ import {
2
+ contrastLevel,
3
+ contrastRatio,
4
+ linearSrgbToDisplayP3,
5
+ linearToSrgb,
6
+ oklabToOklch,
7
+ oklabToRgb,
8
+ oklchToOklab,
9
+ parseColor,
10
+ rgbToHsl,
11
+ rgbToOklab,
12
+ srgbToLinear
13
+ } from "./chunk-MHLU6GRJ.js";
14
+
15
+ // src/color/gamut.ts
16
+ var clamp01 = (v) => Math.min(1, Math.max(0, v));
17
+ function inSrgbGamut(c, epsilon = 1e-5) {
18
+ return c.r >= -epsilon && c.r <= 1 + epsilon && c.g >= -epsilon && c.g <= 1 + epsilon && c.b >= -epsilon && c.b <= 1 + epsilon;
19
+ }
20
+ function deltaEOK(a, b) {
21
+ const la = rgbToOklab(a);
22
+ const lb = rgbToOklab(b);
23
+ return Math.hypot(la.l - lb.l, la.a - lb.a, la.b - lb.b);
24
+ }
25
+ function toSrgbGamut(c) {
26
+ const lch = oklabToOklch(rgbToOklab(c));
27
+ if (lch.l >= 1) return { r: 1, g: 1, b: 1, a: c.a };
28
+ if (lch.l <= 0) return { r: 0, g: 0, b: 0, a: c.a };
29
+ if (inSrgbGamut(c)) {
30
+ return { r: clamp01(c.r), g: clamp01(c.g), b: clamp01(c.b), a: c.a };
31
+ }
32
+ let low = 0;
33
+ let high = lch.c;
34
+ let clippedCandidate = { r: clamp01(c.r), g: clamp01(c.g), b: clamp01(c.b), a: c.a };
35
+ const JND = 0.02;
36
+ const EPS = 1e-4;
37
+ for (let i = 0; i < 30; i++) {
38
+ const mid = (low + high) / 2;
39
+ const candidate = oklabToRgb(oklchToOklab({ l: lch.l, c: mid, h: lch.h, alpha: c.a }));
40
+ const clipped = {
41
+ r: clamp01(candidate.r),
42
+ g: clamp01(candidate.g),
43
+ b: clamp01(candidate.b),
44
+ a: c.a
45
+ };
46
+ clippedCandidate = clipped;
47
+ const dE = deltaEOK(candidate, clipped);
48
+ if (dE < JND) {
49
+ if (JND - dE < EPS) {
50
+ return clipped;
51
+ }
52
+ low = mid;
53
+ } else {
54
+ high = mid;
55
+ }
56
+ if (high - low < 1e-4) {
57
+ break;
58
+ }
59
+ }
60
+ return clippedCandidate;
61
+ }
62
+
63
+ // src/color/format.ts
64
+ var clamp012 = (v) => Math.min(1, Math.max(0, v));
65
+ var byte = (v) => Math.round(clamp012(v) * 255);
66
+ var hex2 = (n) => n.toString(16).padStart(2, "0");
67
+ var round2 = (n) => (Math.round(n * 100) / 100).toString();
68
+ function formatHex(c) {
69
+ const mapped = toSrgbGamut(c);
70
+ const head = `#${hex2(byte(mapped.r))}${hex2(byte(mapped.g))}${hex2(byte(mapped.b))}`;
71
+ const alpha = byte(mapped.a);
72
+ return alpha === 255 ? head : head + hex2(alpha);
73
+ }
74
+ function formatColor(c, format) {
75
+ switch (format) {
76
+ case "hex":
77
+ return formatHex(c);
78
+ case "rgb": {
79
+ const inGamut = toSrgbGamut(c);
80
+ const r = byte(inGamut.r);
81
+ const g = byte(inGamut.g);
82
+ const b = byte(inGamut.b);
83
+ return c.a < 1 ? `rgb(${r} ${g} ${b} / ${round2(c.a)})` : `rgb(${r} ${g} ${b})`;
84
+ }
85
+ case "hsl": {
86
+ const hsl = rgbToHsl(toSrgbGamut(c));
87
+ const h = (Math.round(hsl.h * 100) / 100).toString();
88
+ const s = (Math.round(hsl.s * 1e4) / 100).toString();
89
+ const l = (Math.round(hsl.l * 1e4) / 100).toString();
90
+ return c.a < 1 ? `hsl(${h} ${s}% ${l}% / ${round2(c.a)})` : `hsl(${h} ${s}% ${l}%)`;
91
+ }
92
+ case "oklch": {
93
+ const lch = oklabToOklch(rgbToOklab(c));
94
+ const l = (lch.l * 100).toFixed(2);
95
+ const ch = lch.c.toFixed(4);
96
+ const h = lch.h.toFixed(2);
97
+ return c.a < 1 ? `oklch(${l}% ${ch} ${h} / ${round2(c.a)})` : `oklch(${l}% ${ch} ${h})`;
98
+ }
99
+ case "oklab": {
100
+ const lab = rgbToOklab(c);
101
+ const l = (lab.l * 100).toFixed(2);
102
+ const a = lab.a.toFixed(4);
103
+ const b = lab.b.toFixed(4);
104
+ return c.a < 1 ? `oklab(${l}% ${a} ${b} / ${round2(c.a)})` : `oklab(${l}% ${a} ${b})`;
105
+ }
106
+ case "p3": {
107
+ const linSrgb = {
108
+ r: srgbToLinear(c.r),
109
+ g: srgbToLinear(c.g),
110
+ b: srgbToLinear(c.b),
111
+ a: c.a
112
+ };
113
+ const linP3 = linearSrgbToDisplayP3(linSrgb);
114
+ const r = linearToSrgb(linP3.r).toFixed(4);
115
+ const g = linearToSrgb(linP3.g).toFixed(4);
116
+ const b = linearToSrgb(linP3.b).toFixed(4);
117
+ return c.a < 1 ? `color(display-p3 ${r} ${g} ${b} / ${round2(c.a)})` : `color(display-p3 ${r} ${g} ${b})`;
118
+ }
119
+ }
120
+ }
121
+ function normalizeColor(input) {
122
+ const parsed = parseColor(input);
123
+ return parsed ? formatHex(parsed) : null;
124
+ }
125
+
126
+ // src/color/suggest.ts
127
+ function suggestAccessibleForeground(foreground, background, options = {}) {
128
+ const large = options.large ?? false;
129
+ const lch = oklabToOklch(rgbToOklab(foreground));
130
+ const asHex = (l) => parseColor(formatHex(toSrgbGamut(oklabToRgb(oklchToOklab({ ...lch, l })))));
131
+ for (let d = 0.01; d <= 1; d += 0.01) {
132
+ const downCand = asHex(Math.max(0, lch.l - d));
133
+ const upCand = asHex(Math.min(1, lch.l + d));
134
+ const downRatio = contrastRatio(downCand, background);
135
+ const upRatio = contrastRatio(upCand, background);
136
+ const downPass = contrastLevel(downRatio, { large }) !== "fail";
137
+ const upPass = contrastLevel(upRatio, { large }) !== "fail";
138
+ if (downPass && upPass) return formatHex(downRatio >= upRatio ? downCand : upCand);
139
+ if (downPass) return formatHex(downCand);
140
+ if (upPass) return formatHex(upCand);
141
+ }
142
+ return void 0;
143
+ }
144
+
145
+ export {
146
+ inSrgbGamut,
147
+ deltaEOK,
148
+ toSrgbGamut,
149
+ formatHex,
150
+ formatColor,
151
+ normalizeColor,
152
+ suggestAccessibleForeground
153
+ };
@@ -1,5 +1,10 @@
1
+ import {
2
+ parseSankey,
3
+ parseTreemap
4
+ } from "./chunk-KGRZMHZB.js";
1
5
  import {
2
6
  chartMovingAverage,
7
+ chartRaceRanking,
3
8
  chartSupportsTrendlines,
4
9
  fitChartTrendline,
5
10
  formatChartNumber,
@@ -7,20 +12,23 @@ import {
7
12
  r2,
8
13
  sameChartSelection,
9
14
  summarizeChartSeries
10
- } from "./chunk-Z4P2RBMJ.js";
15
+ } from "./chunk-ITANXMZD.js";
11
16
  import {
12
17
  CHART_TYPE_LABELS,
13
18
  DEFAULT_CHART_PALETTE,
14
19
  DEFAULT_CHART_THEME,
15
20
  chartDataFromTable,
16
21
  chartFamily,
22
+ chartLinksFromTable,
17
23
  chartSeriesColor,
18
24
  chartSliceColor,
19
25
  chartSpecColorsByCategory,
20
26
  chartTreeDepth,
21
27
  chartTreeFromRows,
28
+ chartTreeFromSeries,
22
29
  chartTreeFromTable,
23
30
  chartTreeToRows,
31
+ chartTreeTotal,
24
32
  checkChartColors,
25
33
  cloneChartSeries,
26
34
  cloneChartSpec,
@@ -28,6 +36,7 @@ import {
28
36
  defaultChartAxis,
29
37
  describeChartErrorBars,
30
38
  findChartTreeNode,
39
+ normalizeChartLinks,
31
40
  normalizeChartReferenceBand,
32
41
  normalizeChartReferenceLine,
33
42
  normalizeChartSpec,
@@ -36,7 +45,7 @@ import {
36
45
  parseChartValue,
37
46
  shadeChartColor,
38
47
  uniqueChartSeriesId
39
- } from "./chunk-3NEZJCBY.js";
48
+ } from "./chunk-FR432ROA.js";
40
49
  import {
41
50
  parseCsv,
42
51
  parseTsv
@@ -71,9 +80,14 @@ function splitOutside(text, sep) {
71
80
  out.push(cur);
72
81
  return out;
73
82
  }
83
+ var csvField = (text) => /[",\n]|^\s|\s$/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
84
+ var treemapName = (name) => !name.includes('"') ? `"${name}"` : !name.includes("'") ? `'${name}'` : `"${name.replace(/"/g, "'")}"`;
85
+ var yamlText = (text) => /^[\w][\w .,!?()/&+-]*$/.test(text) && !/:\s/.test(text) ? text : JSON.stringify(text);
74
86
  function chartMermaidFamily(spec) {
75
87
  const family = chartFamily(spec);
76
- if (family === "pie" || family === "funnel" || family === "treemap" || family === "parliament" || family === "sunburst") return "pie";
88
+ if (family === "sankey" && spec.links?.length) return "sankey";
89
+ if (family === "treemap" || family === "sunburst") return "treemap";
90
+ if (family === "pie" || family === "funnel" || family === "parliament") return "pie";
77
91
  if (family === "radar") return "radar";
78
92
  return "xychart";
79
93
  }
@@ -114,7 +128,27 @@ function chartToMermaid(spec, options = {}) {
114
128
  const visible = spec.series.filter((s) => !s.hidden);
115
129
  const lines = [];
116
130
  const v = (x) => x === null || !Number.isFinite(x) ? "0" : num(x);
117
- if (family === "pie") {
131
+ const front = [];
132
+ if (family === "sankey") {
133
+ if (spec.title) front.push("---", `title: ${yamlText(spec.title)}`, "---");
134
+ lines.push("sankey-beta");
135
+ for (const l of spec.links) lines.push(`${csvField(l.source)},${csvField(l.target)},${num(l.value)}`);
136
+ } else if (family === "treemap") {
137
+ lines.push("treemap-beta");
138
+ if (spec.title) lines.push(`title ${spec.title}`);
139
+ const first = visible[0] ?? spec.series[0];
140
+ const tree = spec.tree.length ? spec.tree : chartTreeFromSeries(spec.categories, first?.values ?? []);
141
+ const write = (nodes, depth) => {
142
+ for (const n of nodes) {
143
+ const pad = ind.repeat(depth);
144
+ if (n.children) {
145
+ lines.push(`${pad}${treemapName(n.name)}`);
146
+ write(n.children, depth + 1);
147
+ } else lines.push(`${pad}${treemapName(n.name)}: ${num(n.value ?? 0)}`);
148
+ }
149
+ };
150
+ write(tree, 1);
151
+ } else if (family === "pie") {
118
152
  const s = visible[0] ?? spec.series[0];
119
153
  lines.push(`pie${spec.dataLabels ? " showData" : ""}`);
120
154
  if (spec.title) lines.push(`${ind}title ${spec.title}`);
@@ -160,13 +194,13 @@ function chartToMermaid(spec, options = {}) {
160
194
  lines.push(`${ind}${kind} ${quote(s.name)} [${s.values.map(v).join(", ")}]`);
161
195
  }
162
196
  }
163
- let text = `${lines.join("\n")}
197
+ let text = `${[...front, ...lines].join("\n")}
164
198
  `;
165
199
  if (options.meta !== false) {
166
200
  const meta = metaFor(spec, text);
167
201
  if (meta) {
168
202
  const [header, ...rest] = lines;
169
- text = `${[header, `${CHART_MERMAID_META_PREFIX}${JSON.stringify({ v: 1, ...meta })}`, ...rest].join("\n")}
203
+ text = `${[...front, header, `${CHART_MERMAID_META_PREFIX}${JSON.stringify({ v: 1, ...meta })}`, ...rest].join("\n")}
170
204
  `;
171
205
  }
172
206
  }
@@ -229,7 +263,9 @@ function chartFromMermaid(text, options = {}) {
229
263
  showData = !!m[1];
230
264
  if (m[2]) frontTitle = m[2].trim();
231
265
  } 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)}".` });
266
+ else if (/^sankey(?:-beta)?\s*$/i.test(t)) family = "sankey";
267
+ else if (/^treemap(?:-beta)?\s*$/i.test(t)) family = "treemap";
268
+ 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
269
  headerLine = i;
234
270
  break;
235
271
  }
@@ -245,6 +281,7 @@ function chartFromMermaid(text, options = {}) {
245
281
  let graticule = "circle";
246
282
  const counts = { bar: 0, line: 0 };
247
283
  const lineIssue = (i, message) => issues.push({ path: `line ${i + 1}`, line: i + 1, message: `Line ${i + 1}: ${message}` });
284
+ const whole = family === "sankey" || family === "treemap";
248
285
  for (let i = headerLine + 1; family && i < all.length; i++) {
249
286
  const raw = all[i];
250
287
  const t = raw.trim();
@@ -260,7 +297,7 @@ function chartFromMermaid(text, options = {}) {
260
297
  }
261
298
  continue;
262
299
  }
263
- if (t.startsWith("%%")) continue;
300
+ if (t.startsWith("%%") || whole) continue;
264
301
  const isLast = i === all.length - 1 || all.slice(i + 1).every((l) => !l.trim());
265
302
  let m;
266
303
  if (m = /^title\s+(.+)$/i.exec(t)) {
@@ -362,7 +399,8 @@ function chartFromMermaid(text, options = {}) {
362
399
  if (!(partial && isLast)) lineIssue(i, `could not read "${t.slice(0, 60)}"; it was skipped.`);
363
400
  }
364
401
  let input;
365
- if (family === "pie") {
402
+ if (whole) input = readWholeMermaid(family, all, headerLine, partial, title, issues);
403
+ else if (family === "pie") {
366
404
  input = {
367
405
  type: "pie",
368
406
  title,
@@ -398,18 +436,54 @@ function chartFromMermaid(text, options = {}) {
398
436
  }
399
437
  }
400
438
  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)." });
439
+ 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
440
  return { spec, issues: [...issues, ...specIssues], family, hasMeta: !!meta };
403
441
  }
442
+ function readWholeMermaid(family, all, headerLine, partial, title, issues) {
443
+ const body = all.slice(headerLine).map((l) => l.trim().startsWith(CHART_MERMAID_META_PREFIX.trim()) ? "%%" : l);
444
+ const parse = (lines) => family === "sankey" ? parseSankey(lines.join("\n")) : parseTreemap(lines.join("\n"));
445
+ let read = parse(body);
446
+ if ("error" in read && partial && body.length > 1) read = parse(body.slice(0, -1));
447
+ const lineOf = (n) => n === void 0 ? void 0 : n + headerLine;
448
+ if ("error" in read) {
449
+ const line = lineOf(read.line);
450
+ if (!(partial && body.length <= 2)) issues.push({ path: family, ...line ? { line } : {}, message: line ? `Line ${line}: ${read.error}` : read.error });
451
+ return { type: family === "sankey" ? "sankey" : "treemap", title };
452
+ }
453
+ if (family === "sankey") {
454
+ const data2 = read.data;
455
+ return { type: "sankey", title, links: data2.links, sankeyNodes: data2.nodes, series: [{ id: "series-1", name: "Values", values: [] }] };
456
+ }
457
+ const data = read.data;
458
+ let items = data.items;
459
+ let name = data.title ?? title;
460
+ if (items.length === 1 && items[0].children?.length) {
461
+ name ||= items[0].name;
462
+ items = items[0].children;
463
+ }
464
+ const toNode = (item) => item.children?.length ? { name: item.name, children: item.children.map(toNode) } : { name: item.name, value: item.value ?? 0 };
465
+ const tree = items.map(toNode);
466
+ const leaf = (n) => !n.children;
467
+ const flatOrOneLevel = tree.every(leaf) || tree.every((n) => n.children?.every(leaf));
468
+ return { type: flatOrOneLevel ? "treemap" : "sunburst", title: name, tree, categories: tree.map((n) => n.name), series: [{ id: "series-1", name: "Values", values: tree.map(chartTreeTotal) }] };
469
+ }
404
470
  function findMermaidCharts(text) {
405
471
  const lines = text.replace(/^/, "").split(/\r?\n/);
406
472
  const found = [];
407
473
  const familyOf = (body) => {
408
- const header = body.split(/\r?\n/).find((l) => l.trim() && !l.trim().startsWith("%%") && l.trim() !== "---");
474
+ let rows = body.split(/\r?\n/);
475
+ const firstRow = rows.findIndex((l) => l.trim());
476
+ if (rows[firstRow]?.trim() === "---") {
477
+ const end = rows.findIndex((l, i) => i > firstRow && l.trim() === "---");
478
+ if (end > 0) rows = rows.slice(end + 1);
479
+ }
480
+ const header = rows.find((l) => l.trim() && !l.trim().startsWith("%%"));
409
481
  if (!header) return null;
410
482
  if (/^\s*xychart(-beta)?\b/.test(header)) return "xychart";
411
483
  if (/^\s*pie\b/.test(header)) return "pie";
412
484
  if (/^\s*radar(-beta)?\b/.test(header)) return "radar";
485
+ if (/^\s*sankey(-beta)?\s*$/.test(header)) return "sankey";
486
+ if (/^\s*treemap(-beta)?\s*$/.test(header)) return "treemap";
413
487
  return null;
414
488
  };
415
489
  for (let i = 0; i < lines.length; i++) {
@@ -476,6 +550,21 @@ function describeChart(spec, series = spec.series.filter((s) => !s.hidden).map((
476
550
  }
477
551
  return parts.join(" ");
478
552
  }
553
+ if (family === "barrace") {
554
+ const n = spec.categories.length;
555
+ const first = chartRaceRanking(spec, 0)[0];
556
+ const last = chartRaceRanking(spec, n - 1)[0];
557
+ const lead = (e, at) => e ? `${e.name} leads at ${at} with ${fmt(e.value)}` : "";
558
+ return `${head} of ${series.length} names over ${n} time points (${spec.categories[0]} to ${spec.categories[n - 1]}), showing the top ${Math.min(spec.raceTopN, series.length)}. ${lead(first, spec.categories[0])}; ${lead(last, spec.categories[n - 1])}.`;
559
+ }
560
+ if (family === "sankey") {
561
+ if (!spec.links.length) return `${head} with no flows yet.`;
562
+ const total = spec.links.reduce((t, l) => t + l.value, 0);
563
+ const largest = [...spec.links].sort((a, b) => b.value - a.value)[0];
564
+ const sources = spec.sankeyNodes.filter((n) => !spec.links.some((l) => l.target === n));
565
+ const sinks = spec.sankeyNodes.filter((n) => !spec.links.some((l) => l.source === n));
566
+ 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)}.`;
567
+ }
479
568
  if (family === "parliament") {
480
569
  const s = spec.series.find((x) => !x.hidden);
481
570
  const seats = spec.categories.map((c, i) => ({ c, n: Math.max(0, Math.round(s.values[i] ?? 0)) }));
@@ -695,6 +784,17 @@ function sameChartJson(a, b) {
695
784
  if (keys.length !== Object.keys(bo).filter((k) => bo[k] !== void 0).length) return false;
696
785
  return keys.every((k) => Object.prototype.hasOwnProperty.call(bo, k) && sameChartJson(ao[k], bo[k]));
697
786
  }
787
+ var defaultRaceTicker = {
788
+ now: () => typeof performance !== "undefined" ? performance.now() : Date.now(),
789
+ next: (step) => {
790
+ if (typeof requestAnimationFrame === "function") {
791
+ const id2 = requestAnimationFrame(() => step());
792
+ return () => cancelAnimationFrame(id2);
793
+ }
794
+ const id = setTimeout(step, 16);
795
+ return () => clearTimeout(id);
796
+ }
797
+ };
698
798
  var fail = (message, issues) => ({ ok: false, message, ...issues ? { issues } : {} });
699
799
  var ChartController = class {
700
800
  history;
@@ -707,12 +807,16 @@ var ChartController = class {
707
807
  revision = 0;
708
808
  applying = false;
709
809
  onChange;
810
+ raceTicker;
811
+ raceFrame = null;
812
+ raceStop = null;
710
813
  listeners = /* @__PURE__ */ new Set();
711
814
  unsubscribeHistory;
712
815
  state;
713
816
  constructor(options = {}) {
714
817
  this.history = options.history ?? new HistoryManager();
715
818
  this.onChange = options.onChange;
819
+ this.raceTicker = options.raceTicker ?? defaultRaceTicker;
716
820
  const { spec, issues } = normalizeChartSpec(options.spec ?? {});
717
821
  this.spec = spec;
718
822
  this.issues = issues;
@@ -762,6 +866,8 @@ var ChartController = class {
762
866
  canRedo: h.canRedo,
763
867
  undoLabel: h.undoName,
764
868
  redoLabel: h.redoName,
869
+ raceFrame: this.raceFrame === null ? null : Math.min(this.raceFrame, Math.max(0, this.spec.categories.length - 1)),
870
+ racePlaying: this.raceStop !== null,
765
871
  revision: this.revision
766
872
  };
767
873
  }
@@ -910,7 +1016,7 @@ var ChartController = class {
910
1016
  * to the categories; all-or-nothing like `update`.
911
1017
  */
912
1018
  setData(data, options = {}) {
913
- const clear = (data.categories || data.series) && this.spec.tree.length ? { tree: [] } : {};
1019
+ const clear = (data.categories || data.series) && (this.spec.tree.length || this.spec.links.length) ? { tree: [], links: [] } : {};
914
1020
  return this.update({ ...data.categories ? { categories: data.categories } : {}, ...data.series ? { series: data.series } : {}, ...clear }, { label: options.label ?? "Edit chart data" });
915
1021
  }
916
1022
  /**
@@ -1153,7 +1259,7 @@ var ChartController = class {
1153
1259
  const old = byName.get(s.name);
1154
1260
  return { ...s, ...old?.color ? { color: old.color } : {}, ...old?.type ? { type: old.type } : {}, ...old ? { id: old.id } : {} };
1155
1261
  });
1156
- const { spec } = normalizeChartSpec({ ...this.spec, categories: data.categories, series, tree: [] });
1262
+ const { spec } = normalizeChartSpec({ ...this.spec, categories: data.categories, series, tree: [], links: [] });
1157
1263
  this.commit("Import data", spec);
1158
1264
  if (data.issues.length) {
1159
1265
  this.issues = data.issues;
@@ -1170,8 +1276,12 @@ var ChartController = class {
1170
1276
  // -------------------------------------------------------------------------
1171
1277
  // Grouped data (tree)
1172
1278
  // -------------------------------------------------------------------------
1173
- /** A refusal while the data is a tree (cell edits would be overwritten by its totals), else null. */
1279
+ /** A refusal while the data is a tree or flows (cell edits would be overwritten by their totals), else null. */
1174
1280
  treeLocked() {
1281
+ if (this.spec.links.length)
1282
+ return this.reject(
1283
+ "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."
1284
+ );
1175
1285
  if (!this.spec.tree.length) return null;
1176
1286
  return this.reject(
1177
1287
  "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 +1292,7 @@ var ChartController = class {
1182
1292
  const issues = [];
1183
1293
  const next = normalizeChartTree(tree, issues);
1184
1294
  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") });
1295
+ return this.update({ tree: next, links: [] }, { label: options.label ?? (next.length ? "Replace grouped data" : "Ungroup data") });
1186
1296
  }
1187
1297
  /**
1188
1298
  * Replaces the grouped data from rows of names and a value (group, …, item, value), as the
@@ -1208,7 +1318,7 @@ var ChartController = class {
1208
1318
  const next = chartTreeFromTable(rows, options, issues);
1209
1319
  if (issues.length) return this.reject(issues.map((i) => i.message).join(" "), issues);
1210
1320
  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" });
1321
+ return this.update({ tree: next, links: [] }, { label: "Import grouped data" });
1212
1322
  }
1213
1323
  nodeAt(path) {
1214
1324
  const node = findChartTreeNode(this.spec.tree, path);
@@ -1281,6 +1391,56 @@ var ChartController = class {
1281
1391
  return this.update({ tree }, { label: `Delete "${node.name}"` });
1282
1392
  }
1283
1393
  // -------------------------------------------------------------------------
1394
+ // Flows (links)
1395
+ // -------------------------------------------------------------------------
1396
+ /**
1397
+ * Replaces the flows (and, when given, the node order). An empty list goes back to the
1398
+ * table, keeping how much passed through each node as values. One undo step, all-or-nothing.
1399
+ */
1400
+ setLinks(links, options = {}) {
1401
+ const issues = [];
1402
+ const next = normalizeChartLinks(links, issues);
1403
+ if (issues.length) return this.reject(issues.map((i) => i.message).join(" "), issues);
1404
+ const used = new Set(next.flatMap((l) => [l.source, l.target]));
1405
+ const sankeyNodes = options.nodes ? [...options.nodes] : this.spec.sankeyNodes.filter((n) => used.has(n));
1406
+ return this.update({ links: next, tree: [], sankeyNodes }, { label: options.label ?? (next.length ? "Replace flows" : "Remove flows") });
1407
+ }
1408
+ /** Replaces the data with flows from a table of source, target and value columns. */
1409
+ importLinksTable(rows, options = {}) {
1410
+ const raw = chartLinksFromTable(rows, options);
1411
+ if (!raw.length) return this.reject('There are no flows to import: expected rows of source, target and value, e.g. "Website,Signup,120".');
1412
+ return this.setLinks(raw, { label: "Import flows" });
1413
+ }
1414
+ badLink(index) {
1415
+ const n = this.spec.links.length;
1416
+ return fail(`There is no flow ${index}; ${n ? `expected 0 to ${n - 1}` : "the chart has none yet"}.`);
1417
+ }
1418
+ /** Adds a flow and returns its index. */
1419
+ addLink(link) {
1420
+ const result = this.setLinks([...this.spec.links, link], { label: `Add flow ${String(link?.source ?? "")} \u2192 ${String(link?.target ?? "")}` });
1421
+ if (!result.ok) return result;
1422
+ return { ok: true, index: this.spec.links.length - 1 };
1423
+ }
1424
+ /** Changes a flow's source, target, value or colour (`color: null` goes back to the default). */
1425
+ updateLink(index, patch) {
1426
+ const current = this.spec.links[index];
1427
+ if (!current) return this.badLink(index);
1428
+ const merged = { ...current, ...patch };
1429
+ if (patch.color === null) delete merged.color;
1430
+ return this.setLinks(
1431
+ this.spec.links.map((l, i) => i === index ? merged : l),
1432
+ { label: "Change flow" }
1433
+ );
1434
+ }
1435
+ removeLink(index) {
1436
+ const current = this.spec.links[index];
1437
+ if (!current) return this.badLink(index);
1438
+ return this.setLinks(
1439
+ this.spec.links.filter((_, i) => i !== index),
1440
+ { label: `Delete flow ${current.source} \u2192 ${current.target}` }
1441
+ );
1442
+ }
1443
+ // -------------------------------------------------------------------------
1284
1444
  // Annotations
1285
1445
  // -------------------------------------------------------------------------
1286
1446
  /** Sets the note written under the chart ("" removes it). */
@@ -1516,6 +1676,55 @@ var ChartController = class {
1516
1676
  // -------------------------------------------------------------------------
1517
1677
  // Selection and gestures
1518
1678
  // -------------------------------------------------------------------------
1679
+ /**
1680
+ * Shows a bar race at a moment: a category index, fractional between time points (`null`:
1681
+ * the last one). View state like the selection, so not an undo step. Stops playback.
1682
+ */
1683
+ setRaceFrame(frame) {
1684
+ const n = this.spec.categories.length;
1685
+ if (frame !== null && !(Number.isFinite(frame) && frame >= 0 && frame <= Math.max(0, n - 1)))
1686
+ return fail(n ? `The race has time points 0 to ${n - 1}${n > 1 ? ` ("${this.spec.categories[0]}" to "${this.spec.categories[n - 1]}")` : ""}; got ${frame}.` : "The chart has no time points (categories) yet.");
1687
+ this.stopRace();
1688
+ if (frame === this.raceFrame) return { ok: true };
1689
+ this.raceFrame = frame;
1690
+ this.notify(false);
1691
+ return { ok: true };
1692
+ }
1693
+ /**
1694
+ * Plays the bar race from the moment shown (from the start when it is at the end), moving
1695
+ * one time point every `msPerStep` (default 1000) until the last. Not an undo step.
1696
+ */
1697
+ playRace(options = {}) {
1698
+ const last = this.spec.categories.length - 1;
1699
+ if (last < 1) return fail("A race needs at least two time points (categories) to play.");
1700
+ this.stopRace();
1701
+ const per = Math.max(50, options.msPerStep ?? 1e3);
1702
+ const from = this.raceFrame === null || this.raceFrame >= last ? 0 : this.raceFrame;
1703
+ const start = this.raceTicker.now();
1704
+ const step = () => {
1705
+ const at = Math.min(last, from + (this.raceTicker.now() - start) / per);
1706
+ this.raceFrame = at;
1707
+ if (at >= last) {
1708
+ this.raceStop = null;
1709
+ this.raceFrame = null;
1710
+ } else this.raceStop = this.raceTicker.next(step);
1711
+ this.notify(false);
1712
+ };
1713
+ this.raceFrame = from;
1714
+ this.raceStop = this.raceTicker.next(step);
1715
+ this.notify(false);
1716
+ return { ok: true };
1717
+ }
1718
+ /** Pauses bar race playback where it is. */
1719
+ pauseRace() {
1720
+ if (!this.raceStop) return;
1721
+ this.stopRace();
1722
+ this.notify(false);
1723
+ }
1724
+ stopRace() {
1725
+ this.raceStop?.();
1726
+ this.raceStop = null;
1727
+ }
1519
1728
  /** Selects a chart element (view state; not an undo step). */
1520
1729
  select(selection) {
1521
1730
  const next = this.validSelection(selection, this.spec);
@@ -1638,6 +1847,7 @@ var ChartController = class {
1638
1847
  }
1639
1848
  /** Stops listening to the history (reconnected by the next `subscribe`). */
1640
1849
  destroy() {
1850
+ this.stopRace();
1641
1851
  this.unsubscribeHistory?.();
1642
1852
  this.unsubscribeHistory = void 0;
1643
1853
  this.listeners.clear();
@@ -1673,8 +1883,12 @@ function updateLabel(patch) {
1673
1883
  parliamentArc: "Change hemicycle sweep",
1674
1884
  parliamentMajority: "Change majority",
1675
1885
  parliamentLabel: "Edit seat caption",
1886
+ raceTopN: "Change bars shown",
1676
1887
  sunburstLabel: "Edit centre caption",
1677
1888
  tree: "Replace grouped data",
1889
+ links: "Replace flows",
1890
+ sankeyNodes: "Reorder nodes",
1891
+ sankeyLinkColor: "Change flow colours",
1678
1892
  radarTicks: "Change radar rings",
1679
1893
  radarGraticule: "Change radar grid",
1680
1894
  legend: "Change legend",