@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
@@ -2,6 +2,9 @@ import {
2
2
  CATEGORY_REF,
3
3
  CHART_UPDATE_PROPERTIES,
4
4
  HEIGHT,
5
+ LINK_INDEX,
6
+ LINK_PROPS,
7
+ RACE_TIME,
5
8
  REFERENCE_BAND_PATCH,
6
9
  REFERENCE_BAND_PROPS,
7
10
  REFERENCE_INDEX,
@@ -14,7 +17,7 @@ import {
14
17
  TRENDLINE_PROPS,
15
18
  VALUE,
16
19
  WIDTH
17
- } from "../chunk-5FNGLV2D.js";
20
+ } from "../chunk-5UNQDC3Q.js";
18
21
  import {
19
22
  CHART_OOXML_CONTENT_TYPE,
20
23
  CHART_OOXML_NS,
@@ -22,7 +25,7 @@ import {
22
25
  chartFormatToCode,
23
26
  chartFromOoxml,
24
27
  chartToOoxml
25
- } from "../chunk-POR54B55.js";
28
+ } from "../chunk-XNU45TYA.js";
26
29
  import {
27
30
  CHART_COLOR_SCHEMES,
28
31
  CHART_LAYOUT_PRESETS,
@@ -44,7 +47,8 @@ import {
44
47
  monochromeChartPalette,
45
48
  sparklineToSvg,
46
49
  summarizeChart
47
- } from "../chunk-SZA42R4N.js";
50
+ } from "../chunk-YUFTVNLX.js";
51
+ import "../chunk-KGRZMHZB.js";
48
52
  import {
49
53
  CHART_DATE_STEPS,
50
54
  MAX_CHART_DATA_LABELS,
@@ -56,6 +60,9 @@ import {
56
60
  chartHistogram,
57
61
  chartMovingAverage,
58
62
  chartQuantile,
63
+ chartRaceMoment,
64
+ chartRaceRanking,
65
+ chartRaceValueAt,
59
66
  chartScaleFromPixel,
60
67
  chartScaleToPixel,
61
68
  chartSerialToDate,
@@ -84,7 +91,7 @@ import {
84
91
  suggestChartReferences,
85
92
  summarizeChartSeries,
86
93
  trendNumberText
87
- } from "../chunk-Z4P2RBMJ.js";
94
+ } from "../chunk-ITANXMZD.js";
88
95
  import {
89
96
  CHART_AXIS_KINDS,
90
97
  CHART_AXIS_SCALES,
@@ -102,6 +109,7 @@ import {
102
109
  CHART_PALETTE_SHADES,
103
110
  CHART_RADAR_GRATICULES,
104
111
  CHART_REFERENCE_AXES,
112
+ CHART_SANKEY_LINK_COLORS,
105
113
  CHART_SERIES_TYPES,
106
114
  CHART_STACKINGS,
107
115
  CHART_TRENDLINE_TYPES,
@@ -110,8 +118,10 @@ import {
110
118
  CSS_VARIABLE_CHART_THEME,
111
119
  DEFAULT_CHART_PALETTE,
112
120
  DEFAULT_CHART_THEME,
121
+ MAX_CHART_LINKS,
113
122
  MAX_CHART_TREE_DEPTH,
114
123
  MAX_CHART_TREE_NODES,
124
+ addChartLink,
115
125
  addChartTreeRow,
116
126
  chartColorDistance,
117
127
  chartColorInputValue,
@@ -123,10 +133,13 @@ import {
123
133
  chartErrorBarsOfType,
124
134
  chartFamily,
125
135
  chartLikertColor,
136
+ chartLinksFromTable,
126
137
  chartLuminance,
127
138
  chartPointColor,
128
139
  chartRgbToHex,
129
140
  chartRgbToOklab,
141
+ chartSankeyNodeValues,
142
+ chartSankeyNodes,
130
143
  chartSeriesColor,
131
144
  chartSeriesType,
132
145
  chartSliceColor,
@@ -155,6 +168,7 @@ import {
155
168
  isXyChart,
156
169
  normalizeChartAxis,
157
170
  normalizeChartErrorBars,
171
+ normalizeChartLinks,
158
172
  normalizeChartReferenceBand,
159
173
  normalizeChartReferenceLine,
160
174
  normalizeChartSpec,
@@ -165,12 +179,14 @@ import {
165
179
  parseChartColor,
166
180
  parseChartValue,
167
181
  removeChartTreeRow,
182
+ setChartLinkField,
168
183
  setChartTreeRowName,
169
184
  setChartTreeRowValue,
170
185
  shadeChartColor,
171
186
  simulateChartVision,
172
187
  uniqueChartSeriesId
173
- } from "../chunk-3NEZJCBY.js";
188
+ } from "../chunk-FR432ROA.js";
189
+ import "../chunk-O6OGNDND.js";
174
190
  import "../chunk-II2WIF5Z.js";
175
191
  import {
176
192
  parseCsv
@@ -188,11 +204,12 @@ import {
188
204
  } from "../chunk-CIQIM2IP.js";
189
205
  import {
190
206
  defineEditorDocument
191
- } from "../chunk-NHLYK3AX.js";
207
+ } from "../chunk-XBQX76CT.js";
192
208
  import "../chunk-HF5QZS7Z.js";
193
209
  import "../chunk-RNSANXPT.js";
194
- import "../chunk-QYZGBVNC.js";
195
- import "../chunk-4VU4CP7R.js";
210
+ import "../chunk-7272D3K2.js";
211
+ import "../chunk-YBAPT7NL.js";
212
+ import "../chunk-MHLU6GRJ.js";
196
213
  import "../chunk-6ONMQ6WF.js";
197
214
 
198
215
  // src/chart/raster.ts
@@ -383,6 +400,7 @@ function recommendForShape(shape) {
383
400
  add("area", "Area", "Shows change over time with volume.");
384
401
  add("bar", "Column", "Compares each period.");
385
402
  if (shape.series > 1) add("area", "Stacked area", "Shows how the parts add up over time.", "stacked");
403
+ if (shape.series >= 5 && shape.points >= 3) add("barrace", "Bar race", "Many series over time: an animated ranking shows who leads when.");
386
404
  return out;
387
405
  }
388
406
  if (shape.series === 1) {
@@ -417,6 +435,7 @@ function recommendForShape(shape) {
417
435
  add("bar", "Stacked column", "Shows how the series add up.", "stacked");
418
436
  add("line", "Line", "Shows the series in order.");
419
437
  if (shape.points >= 3 && shape.series <= 6) add("radar", "Radar", "Compares several measures across the series.");
438
+ if (shape.nonNegative && shape.series >= 5 && shape.points >= 3) add("barrace", "Bar race", "Many series across ordered categories (such as years): an animated ranking shows who leads when.");
420
439
  if (shape.nonNegative && shape.series >= 3 && shape.series <= 7) add("likert", "Likert", "Survey answers per question (columns from most negative to most positive): shows agreement against disagreement.");
421
440
  }
422
441
  return out;
@@ -462,6 +481,12 @@ function createChartCommands(controller, options = {}) {
462
481
  if (index < 0 || index >= categories.length || !Number.isInteger(index)) fail(`There is no category ${JSON.stringify(ref)}; expected an index from 0 to ${categories.length - 1} or one of the labels.`);
463
482
  return index;
464
483
  };
484
+ const raceFrame = (time) => {
485
+ const cats = controller.getSpec().categories;
486
+ const frame = typeof time === "number" ? time : cats.indexOf(time);
487
+ if (!(Number.isFinite(frame) && frame >= 0 && frame <= cats.length - 1)) fail(`There is no time point ${JSON.stringify(time)}; expected an index from 0 to ${Math.max(0, cats.length - 1)} or a category label.`);
488
+ return frame;
489
+ };
465
490
  const object = (properties, required) => ({ type: "object", properties, ...required ? { required } : {}, additionalProperties: false });
466
491
  return [
467
492
  {
@@ -800,6 +825,131 @@ function createChartCommands(controller, options = {}) {
800
825
  return { groups: controller.getSpec().tree.map((n) => ({ name: n.name, total: chartTreeTotal(n) })) };
801
826
  }
802
827
  },
828
+ {
829
+ ...base,
830
+ id: `${prefix}setLinks`,
831
+ label: "Replace Flows\u2026",
832
+ keywords: ["chart", "sankey", "flow", "links", "funnel", "budget"],
833
+ description: "Replaces the data with flows between named nodes for a Sankey diagram; [] goes back to plain data.",
834
+ params: object(
835
+ {
836
+ links: { type: "array", items: { type: "object", properties: LINK_PROPS, required: ["source", "target", "value"], additionalProperties: false }, description: "All the flows; [] removes them." },
837
+ nodes: { type: "array", items: { type: "string" }, description: "Node names in stacking order." }
838
+ },
839
+ ["links"]
840
+ ),
841
+ run: (_ctx, params) => {
842
+ check(controller.setLinks(params.links, { nodes: params.nodes }));
843
+ return { links: controller.getSpec().links, nodes: controller.getSpec().sankeyNodes };
844
+ }
845
+ },
846
+ {
847
+ ...base,
848
+ id: `${prefix}importLinksCsv`,
849
+ label: "Import Flows from CSV\u2026",
850
+ keywords: ["chart", "sankey", "flow", "csv", "import"],
851
+ description: "Replaces the data with flows from CSV / TSV rows of source, target and value.",
852
+ params: object(
853
+ {
854
+ csv: { type: "string", description: "CSV or TSV text." },
855
+ header: { type: ["boolean", "string"], enum: [true, false, "auto"], description: "The first row holds column names (default auto)." }
856
+ },
857
+ ["csv"]
858
+ ),
859
+ run: (_ctx, params) => {
860
+ check(controller.importLinksTable(parseCsv(params.csv, { delimiter: params.csv.includes(" ") ? " " : "," }), { header: params.header }));
861
+ return { links: controller.getSpec().links };
862
+ }
863
+ },
864
+ {
865
+ ...base,
866
+ id: `${prefix}addLink`,
867
+ label: "Add Flow\u2026",
868
+ keywords: ["chart", "sankey", "flow", "link", "add"],
869
+ description: "Adds a flow between two nodes. Returns its index.",
870
+ params: object(LINK_PROPS, ["source", "target", "value"]),
871
+ run: (_ctx, params) => {
872
+ const result = controller.addLink(params);
873
+ check(result);
874
+ return { index: result.index };
875
+ }
876
+ },
877
+ {
878
+ ...base,
879
+ id: `${prefix}updateLink`,
880
+ label: "Change Flow\u2026",
881
+ keywords: ["chart", "sankey", "flow", "link", "value"],
882
+ description: "Changes a flow by index (color null for the default).",
883
+ params: object({ index: LINK_INDEX, ...LINK_PROPS, color: { type: ["string", "null"], description: "CSS colour, or null for the default." } }, ["index"]),
884
+ run: (_ctx, params) => {
885
+ const { index, ...patch } = params;
886
+ check(controller.updateLink(index, patch));
887
+ return { link: controller.getSpec().links[index] };
888
+ }
889
+ },
890
+ {
891
+ ...base,
892
+ id: `${prefix}removeLink`,
893
+ label: "Delete Flow",
894
+ keywords: ["chart", "sankey", "flow", "link", "remove"],
895
+ description: "Deletes a flow by index.",
896
+ params: object({ index: LINK_INDEX }, ["index"]),
897
+ run: (_ctx, params) => {
898
+ check(controller.removeLink(params.index));
899
+ return { links: controller.getSpec().links };
900
+ }
901
+ },
902
+ {
903
+ ...base,
904
+ id: `${prefix}raceRanking`,
905
+ label: "Bar Race Ranking",
906
+ readOnly: true,
907
+ keywords: ["chart", "race", "ranking", "leader", "time"],
908
+ description: "Returns the top names of a bar race at a moment (default: the one shown).",
909
+ params: object({ time: RACE_TIME }),
910
+ run: (_ctx, params = {}) => {
911
+ const spec = controller.getSpec();
912
+ const frame = params?.time !== void 0 ? raceFrame(params.time) : controller.getState().raceFrame ?? spec.categories.length - 1;
913
+ return { frame, ranking: chartRaceRanking(spec, frame).slice(0, spec.raceTopN).map((e) => ({ rank: e.rank + 1, series: e.seriesId, name: e.name, value: e.value })) };
914
+ }
915
+ },
916
+ {
917
+ ...base,
918
+ id: `${prefix}setRaceFrame`,
919
+ label: "Show Bar Race Moment\u2026",
920
+ keywords: ["chart", "race", "frame", "time", "scrub"],
921
+ description: "Shows the bar race at a time point (null for the last) and stops playback. View state, not an undo step.",
922
+ params: object({ time: { ...RACE_TIME, type: ["string", "number", "null"], description: `${RACE_TIME.description} null shows the last time point.` } }, ["time"]),
923
+ run: (_ctx, params) => {
924
+ const frame = params.time === null ? null : raceFrame(params.time);
925
+ check(controller.setRaceFrame(frame));
926
+ return { frame };
927
+ }
928
+ },
929
+ {
930
+ ...base,
931
+ id: `${prefix}playRace`,
932
+ label: "Play Bar Race",
933
+ keywords: ["chart", "race", "play", "animate"],
934
+ description: "Plays the bar race from the moment shown, one time point every msPerStep ms.",
935
+ params: object({ msPerStep: { type: "number", minimum: 50, maximum: 1e4, description: "Milliseconds per time point (default 1000)." } }),
936
+ run: (_ctx, params = {}) => {
937
+ check(controller.playRace({ msPerStep: params?.msPerStep }));
938
+ return { playing: true };
939
+ }
940
+ },
941
+ {
942
+ ...base,
943
+ id: `${prefix}pauseRace`,
944
+ label: "Pause Bar Race",
945
+ keywords: ["chart", "race", "pause", "stop"],
946
+ description: "Pauses bar race playback where it is.",
947
+ enabled: () => controller.getState().racePlaying || "The race is not playing",
948
+ run: () => {
949
+ controller.pauseRace();
950
+ return { frame: controller.getState().raceFrame };
951
+ }
952
+ },
803
953
  {
804
954
  ...base,
805
955
  id: `${prefix}applyPreset`,
@@ -851,11 +1001,11 @@ function createChartCommands(controller, options = {}) {
851
1001
  id: `${prefix}importMermaid`,
852
1002
  label: "Import Mermaid Chart\u2026",
853
1003
  keywords: ["chart", "mermaid", "markdown", "xychart", "pie", "radar", "import"],
854
- description: "Replaces the chart with Mermaid xychart-beta, pie or radar-beta text (one undo step). Unreadable lines are listed with line numbers.",
1004
+ description: "Replaces the chart with Mermaid xychart-beta, pie, radar-beta, sankey-beta or treemap-beta text (one undo step). Unreadable lines are listed with line numbers.",
855
1005
  params: object({ mermaid: { type: "string", description: "Mermaid text." } }, ["mermaid"]),
856
1006
  run: (_ctx, params) => {
857
1007
  const read = chartFromMermaid(params.mermaid);
858
- if (!read.family) fail(read.issues.map((i) => i.message).join(" ") || "Expected Mermaid starting with xychart-beta, pie or radar-beta.");
1008
+ if (!read.family) fail(read.issues.map((i) => i.message).join(" ") || "Expected Mermaid starting with xychart-beta, pie, radar-beta, sankey-beta or treemap-beta.");
859
1009
  check(controller.update({ ...read.spec }, { label: "Import Mermaid chart" }));
860
1010
  return { spec: controller.toJSON(), issues: read.issues };
861
1011
  }
@@ -1366,6 +1516,11 @@ function planChartToolPlayback(tool, args) {
1366
1516
  case "add_chart_tree_node":
1367
1517
  case "update_chart_tree_node":
1368
1518
  case "remove_chart_tree_node":
1519
+ case "set_chart_links":
1520
+ case "import_chart_links_csv":
1521
+ case "add_chart_link":
1522
+ case "update_chart_link":
1523
+ case "remove_chart_link":
1369
1524
  case "import_chart_csv":
1370
1525
  case "import_chart_mermaid":
1371
1526
  case "import_chart_ooxml":
@@ -1392,6 +1547,10 @@ function planChartToolPlayback(tool, args) {
1392
1547
  return click(control("series.callout"));
1393
1548
  case "highlight_chart":
1394
1549
  return click(control("highlight"));
1550
+ case "set_chart_race_frame":
1551
+ return click(control("raceFrame"));
1552
+ case "play_chart_race":
1553
+ return click({ kind: "action", id: "race-play" });
1395
1554
  case "render_chart":
1396
1555
  return [{ type: "target", ref: { kind: "action", id: "download-png" } }];
1397
1556
  case "undo_chart":
@@ -1456,6 +1615,23 @@ function createChartToolProvider(controller, options = {}) {
1456
1615
  const depth = chartTreeDepth(spec().tree);
1457
1616
  return [[...Array.from({ length: depth }, (_, k) => k === 0 ? "Group" : k === depth - 1 ? "Item" : `Level ${k + 1}`), "Value"], ...rows.map((r) => [...r.path, ...Array(depth - r.path.length).fill(""), r.value])];
1458
1617
  };
1618
+ const raceFrameOf = (time) => {
1619
+ const cats = spec().categories;
1620
+ const frame = typeof time === "number" ? time : cats.indexOf(time);
1621
+ if (!(Number.isFinite(frame) && frame >= 0 && frame <= cats.length - 1))
1622
+ fail2(`There is no time point ${JSON.stringify(time)}; expected an index from 0 to ${Math.max(0, cats.length - 1)} or one of ${cats.slice(0, 20).map((c) => JSON.stringify(c)).join(", ")}.`);
1623
+ return frame;
1624
+ };
1625
+ const raceAt = (frame) => {
1626
+ const s = spec();
1627
+ const at = frame ?? s.categories.length - 1;
1628
+ return {
1629
+ frame: at,
1630
+ time: s.categories[Math.round(at)] ?? null,
1631
+ ranking: chartRaceRanking(s, at).slice(0, s.raceTopN).map((e) => ({ rank: e.rank + 1, series: e.seriesId, name: e.name, value: e.value }))
1632
+ };
1633
+ };
1634
+ const linksSummary = () => ({ links: spec().links, nodes: spec().sankeyNodes.map((name, i) => ({ name, value: chartSankeyNodeValues(spec().links, spec().sankeyNodes)[i] })) });
1459
1635
  const treeSummary = () => ({ tree: spec().tree, groups: spec().tree.map((n) => ({ name: n.name, total: chartTreeTotal(n) })), depth: chartTreeDepth(spec().tree) });
1460
1636
  const Parser = () => options.DOMParser ?? globalThis.DOMParser;
1461
1637
  const getChart = {
@@ -1486,14 +1662,16 @@ function createChartToolProvider(controller, options = {}) {
1486
1662
  properties: {
1487
1663
  width: WIDTH,
1488
1664
  height: HEIGHT,
1489
- format: { type: "string", enum: ["svg", "png"], description: "svg (default): the SVG document as text. png: an image to look at (only when the host can rasterize; costs image tokens)." }
1665
+ format: { type: "string", enum: ["svg", "png"], description: "svg (default): the SVG document as text. png: an image to look at (only when the host can rasterize; costs image tokens)." },
1666
+ time: { ...RACE_TIME, description: `Bar race only: the moment to draw (default: the one shown). ${RACE_TIME.description}` }
1490
1667
  },
1491
1668
  additionalProperties: false
1492
1669
  },
1493
1670
  annotations: { title: "Render the chart", ...READ },
1494
- handler: async ({ width = size.width, height = size.height, format = "svg" }) => {
1671
+ handler: async ({ width = size.width, height = size.height, format = "svg", time }) => {
1495
1672
  const json = controller.toJSON();
1496
- const scene = layoutChart(json, { width, height, selection: null });
1673
+ const raceFrame = time !== void 0 ? raceFrameOf(time) : controller.getState().raceFrame;
1674
+ const scene = layoutChart(json, { width, height, selection: null, raceFrame });
1497
1675
  const svg = chartSceneToSvg(scene, { ariaLabel: json.title || void 0, description: describeChart(json) });
1498
1676
  const warnings = scene.warnings;
1499
1677
  if (format === "png") {
@@ -1566,6 +1744,14 @@ function createChartToolProvider(controller, options = {}) {
1566
1744
  return { trendline: line, coefficients: fit.coefficients, r2: fit.r2, equation: fit.equation, points: fit.points };
1567
1745
  }
1568
1746
  };
1747
+ const raceRanking = {
1748
+ name: "get_chart_race_ranking",
1749
+ title: "Read a bar race ranking",
1750
+ description: "Returns who leads a bar race at a moment without changing what is shown: the top raceTopN names with their rank, series id and value (eased between time points). Defaults to the moment shown. Works for any chart, reading categories as time points and series as names.",
1751
+ inputSchema: { type: "object", properties: { time: RACE_TIME }, additionalProperties: false },
1752
+ annotations: { title: "Read a bar race ranking", ...READ },
1753
+ handler: ({ time }) => raceAt(time !== void 0 ? raceFrameOf(time) : controller.getState().raceFrame)
1754
+ };
1569
1755
  const listPresets = {
1570
1756
  name: "list_chart_presets",
1571
1757
  title: "List chart presets",
@@ -1580,7 +1766,7 @@ function createChartToolProvider(controller, options = {}) {
1580
1766
  const exportMermaid = {
1581
1767
  name: "export_chart_mermaid",
1582
1768
  title: "Export chart as Mermaid",
1583
- description: 'Returns the chart as Mermaid text (xychart-beta, pie or radar-beta) for Markdown or chat. With meta (default true) one "%% @pixerate {\u2026}" comment keeps what Mermaid cannot express, so import_chart_mermaid gets the chart back exactly.',
1769
+ description: 'Returns the chart as Mermaid text for Markdown or chat: xychart-beta, pie or radar-beta, sankey-beta for Sankey diagrams with flows, treemap-beta for treemaps and sunbursts (with their levels). With meta (default true) one "%% @pixerate {\u2026}" comment keeps what Mermaid cannot express, so import_chart_mermaid gets the chart back exactly.',
1584
1770
  inputSchema: { type: "object", properties: { meta: { type: "boolean", description: "Include the round-trip comment (default true)." } }, additionalProperties: false },
1585
1771
  annotations: { title: "Export chart as Mermaid", ...READ },
1586
1772
  handler: ({ meta = true }) => ({ mermaid: chartToMermaid(controller.toJSON(), { meta }) })
@@ -1767,13 +1953,13 @@ function createChartToolProvider(controller, options = {}) {
1767
1953
  const importMermaid = {
1768
1954
  name: "import_chart_mermaid",
1769
1955
  title: "Import a Mermaid chart",
1770
- description: 'Replaces the chart with Mermaid xychart-beta, pie or radar-beta text as one undo step (a "%% @pixerate" comment from export_chart_mermaid restores everything else). Lines that cannot be read are skipped and listed with their line numbers. Returns the spec.',
1956
+ description: 'Replaces the chart with Mermaid xychart-beta, pie, radar-beta, sankey-beta or treemap-beta text as one undo step (a "%% @pixerate" comment from export_chart_mermaid restores everything else). Lines that cannot be read are skipped and listed with their line numbers. Returns the spec.',
1771
1957
  inputSchema: { type: "object", properties: { mermaid: { type: "string", description: "Mermaid text, optionally inside a ```mermaid fence." } }, required: ["mermaid"], additionalProperties: false },
1772
1958
  annotations: { title: "Import a Mermaid chart", ...DESTRUCTIVE },
1773
1959
  handler: ({ mermaid }) => {
1774
1960
  const fenced = /```(?:mermaid)?\s*\n([\s\S]*?)```/.exec(mermaid);
1775
1961
  const read = chartFromMermaid(fenced ? fenced[1] : mermaid);
1776
- if (!read.family) fail2(read.issues.map((i) => i.message).join(" ") || "Expected Mermaid starting with xychart-beta, pie or radar-beta.");
1962
+ if (!read.family) fail2(read.issues.map((i) => i.message).join(" ") || "Expected Mermaid starting with xychart-beta, pie, radar-beta, sankey-beta or treemap-beta.");
1777
1963
  resolve(controller.update({ ...read.spec }, { label: "Import Mermaid chart" }));
1778
1964
  return { spec: controller.toJSON(), issues: read.issues };
1779
1965
  }
@@ -1982,6 +2168,105 @@ function createChartToolProvider(controller, options = {}) {
1982
2168
  return { groups: treeSummary().groups };
1983
2169
  }
1984
2170
  };
2171
+ const setLinks = {
2172
+ name: "set_chart_links",
2173
+ title: "Replace the flows",
2174
+ description: `Replaces the chart's data with flows between named nodes for a Sankey diagram, as one undo step: each {"source", "target", "value"} with a value above 0. Flows only go forwards (no loops) and a source-target pair appears once. The nodes become the categories and how much passes through each the first series. nodes (optional) sets the stacking order. [] goes back to plain data. Returns the flows and each node's throughput.`,
2175
+ inputSchema: {
2176
+ type: "object",
2177
+ properties: {
2178
+ links: { type: "array", items: { type: "object", properties: LINK_PROPS, required: ["source", "target", "value"], additionalProperties: false }, description: "All the flows; [] removes them." },
2179
+ nodes: { type: "array", items: { type: "string" }, description: "Node names in stacking order (default: as the flows first name them)." }
2180
+ },
2181
+ required: ["links"],
2182
+ additionalProperties: false
2183
+ },
2184
+ annotations: { title: "Replace the flows", ...WRITE_IDEMPOTENT },
2185
+ handler: ({ links, nodes }) => {
2186
+ resolve(controller.setLinks(links, { nodes }));
2187
+ return linksSummary();
2188
+ }
2189
+ };
2190
+ const importLinksCsv = {
2191
+ name: "import_chart_links_csv",
2192
+ title: "Import flows from CSV",
2193
+ description: "Replaces the data with flows from CSV or TSV rows of source, target and value (as Mermaid sankey-beta and spreadsheets write them). One undo step. Returns the flows and each node's throughput.",
2194
+ inputSchema: {
2195
+ type: "object",
2196
+ properties: {
2197
+ csv: { type: "string", description: 'CSV or TSV text, e.g. "Website,Signup,120\\nSignup,Paid,30".' },
2198
+ header: { type: ["boolean", "string"], enum: [true, false, "auto"], description: "The first row holds column names (default auto: when its third cell is not a number)." }
2199
+ },
2200
+ required: ["csv"],
2201
+ additionalProperties: false
2202
+ },
2203
+ annotations: { title: "Import flows from CSV", ...WRITE },
2204
+ handler: ({ csv, header }) => {
2205
+ resolve(controller.importLinksTable(parseCsv(csv, { delimiter: csv.includes(" ") ? " " : "," }), { header }));
2206
+ return linksSummary();
2207
+ }
2208
+ };
2209
+ const addLink = {
2210
+ name: "add_chart_link",
2211
+ title: "Add a flow",
2212
+ description: "Adds a flow from one node to another (new names add nodes). Refused when the pair exists already or it would close a loop. Returns its index and the throughput of its two nodes.",
2213
+ inputSchema: { type: "object", properties: LINK_PROPS, required: ["source", "target", "value"], additionalProperties: false },
2214
+ annotations: { title: "Add a flow", ...WRITE },
2215
+ handler: (args) => {
2216
+ const result = resolve(controller.addLink(args));
2217
+ const s = spec();
2218
+ const through = (n) => s.series[0]?.values[s.sankeyNodes.indexOf(n)] ?? 0;
2219
+ const link = s.links[result.index];
2220
+ return { index: result.index, link, nodes: [link.source, link.target].map((name) => ({ name, value: through(name) })) };
2221
+ }
2222
+ };
2223
+ const updateLink = {
2224
+ name: "update_chart_link",
2225
+ title: "Change a flow",
2226
+ description: "Changes a flow by index: its source, target, value or colour (null goes back to the default). Checked like a new flow. Returns the flow as stored.",
2227
+ inputSchema: { type: "object", properties: { index: LINK_INDEX, ...LINK_PROPS, color: { type: ["string", "null"], description: "CSS colour, or null for the default." } }, required: ["index"], additionalProperties: false },
2228
+ annotations: { title: "Change a flow", ...WRITE_IDEMPOTENT },
2229
+ handler: ({ index, ...patch }) => {
2230
+ if (!Object.keys(patch).length) fail2(`Nothing to change: pass source, target, value or color with the index, e.g. {"index": ${index}, "value": 40}.`);
2231
+ resolve(controller.updateLink(index, patch));
2232
+ return { index, link: spec().links[index] };
2233
+ }
2234
+ };
2235
+ const removeLink = {
2236
+ name: "remove_chart_link",
2237
+ title: "Delete a flow",
2238
+ description: "Deletes a flow by index; the ones after it move up one, and nodes no flow uses any more go. Returns the flows left.",
2239
+ inputSchema: { type: "object", properties: { index: LINK_INDEX }, required: ["index"], additionalProperties: false },
2240
+ annotations: { title: "Delete a flow", ...DESTRUCTIVE },
2241
+ handler: ({ index }) => {
2242
+ resolve(controller.removeLink(index));
2243
+ return { links: spec().links };
2244
+ }
2245
+ };
2246
+ const setRaceFrame = {
2247
+ name: "set_chart_race_frame",
2248
+ title: "Show a bar race moment",
2249
+ description: "Shows the bar race at a time point (or between two), and stops playback. This is view state like the selection: it is not an undo step and is not saved in the chart. Returns the moment and the ranking shown.",
2250
+ inputSchema: { type: "object", properties: { time: { ...RACE_TIME, type: ["string", "number", "null"], description: `${RACE_TIME.description} null shows the last time point.` } }, required: ["time"], additionalProperties: false },
2251
+ annotations: { title: "Show a bar race moment", ...WRITE_IDEMPOTENT },
2252
+ handler: ({ time }) => {
2253
+ const frame = time === null ? null : raceFrameOf(time);
2254
+ resolve(controller.setRaceFrame(frame));
2255
+ return raceAt(frame);
2256
+ }
2257
+ };
2258
+ const playRace = {
2259
+ name: "play_chart_race",
2260
+ title: "Play the bar race",
2261
+ description: "Starts playing the bar race for the user from the moment shown (from the start when it is at the end), one time point every msPerStep ms, and returns at once. set_chart_race_frame stops it. Nothing in the chart changes.",
2262
+ inputSchema: { type: "object", properties: { msPerStep: { type: "number", minimum: 50, maximum: 1e4, description: "Milliseconds per time point (default 1000)." } }, additionalProperties: false },
2263
+ annotations: { title: "Play the bar race", ...WRITE },
2264
+ handler: ({ msPerStep }) => {
2265
+ resolve(controller.playRace({ msPerStep }));
2266
+ const n = spec().categories.length;
2267
+ return { playing: true, from: controller.getState().raceFrame ?? 0, durationMs: Math.round((n - 1 - (controller.getState().raceFrame ?? 0)) * (msPerStep ?? 1e3)) };
2268
+ }
2269
+ };
1985
2270
  const applyPreset = {
1986
2271
  name: "apply_chart_preset",
1987
2272
  title: "Apply a chart style",
@@ -2147,7 +2432,7 @@ function createChartToolProvider(controller, options = {}) {
2147
2432
  return { redone, ...redone ? { label } : {} };
2148
2433
  }
2149
2434
  };
2150
- const reads = [getChart, renderChart, checkColors, recommend, getTrendline, listPresets, exportMermaid, exportOoxml];
2435
+ const reads = [getChart, renderChart, checkColors, recommend, getTrendline, raceRanking, listPresets, exportMermaid, exportOoxml];
2151
2436
  const writes = [
2152
2437
  updateChart,
2153
2438
  setData,
@@ -2174,6 +2459,13 @@ function createChartToolProvider(controller, options = {}) {
2174
2459
  addTreeNode,
2175
2460
  updateTreeNode,
2176
2461
  removeTreeNode,
2462
+ setLinks,
2463
+ importLinksCsv,
2464
+ addLink,
2465
+ updateLink,
2466
+ removeLink,
2467
+ setRaceFrame,
2468
+ playRace,
2177
2469
  applyPreset,
2178
2470
  transpose,
2179
2471
  select,
@@ -2186,7 +2478,7 @@ function createChartToolProvider(controller, options = {}) {
2186
2478
  id: options.id ?? "chart",
2187
2479
  namespace: options.namespace,
2188
2480
  title: "Chart editor",
2189
- description: "One chart: its spec, data and values, series and categories, trendlines, reference lines and bands, callouts, highlight, grouped data and presets, CSV, Mermaid and OOXML import, rendering and colour checks.",
2481
+ description: "One chart: its spec, data and values, series and categories, trendlines, reference lines and bands, callouts, highlight, grouped data, flows, bar race playback and presets, CSV, Mermaid and OOXML import, rendering and colour checks.",
2190
2482
  skill: "pixerate-chart",
2191
2483
  tools: () => {
2192
2484
  if (readOnlyReason()) return reads;
@@ -2669,6 +2961,194 @@ function createChartEditorDocument(controller) {
2669
2961
  }
2670
2962
  });
2671
2963
  }
2964
+
2965
+ // src/chart/style.ts
2966
+ var CHART_TYPES3 = ["bar", "line", "area", "scatter", "bubble", "pie", "donut", "radar", "waterfall", "histogram", "box", "funnel", "treemap"];
2967
+ var COLOR = { type: "string", minLength: 1, description: 'A CSS colour (hex or rgb()), or a { "$token": ... } reference.' };
2968
+ function chartColors(spec) {
2969
+ const out = [];
2970
+ const visible = spec.series.map((s, i) => ({ s, i })).filter(({ s }) => !s.hidden);
2971
+ if (chartColorsByCategory(spec.type)) {
2972
+ const first = visible[0];
2973
+ spec.categories.forEach((name, index) => {
2974
+ const color = first?.s.pointColors?.[index] || chartSliceColor(spec, index);
2975
+ out.push({ color, label: `Slice "${name}"`, target: first ? { kind: "point", seriesId: first.s.id, index } : { kind: "category", index } });
2976
+ });
2977
+ return out;
2978
+ }
2979
+ for (const { s, i } of visible) {
2980
+ out.push({ color: chartSeriesColor(spec, i), label: `Series "${s.name}"`, target: { kind: "series", seriesId: s.id } });
2981
+ s.pointColors?.forEach((c, index) => {
2982
+ if (c) out.push({ color: c, label: `Point ${index + 1} of "${s.name}"`, target: { kind: "point", seriesId: s.id, index } });
2983
+ });
2984
+ }
2985
+ return out;
2986
+ }
2987
+ var paletteOf = (profile) => {
2988
+ const palette = profile.palette;
2989
+ if (!palette?.length) throw new Error("the profile has no palette.");
2990
+ return palette;
2991
+ };
2992
+ var paletteOnly = {
2993
+ id: "palette-only",
2994
+ description: "Every series, slice and point colour comes from the house palette (or a lighter or darker shade the chart cycles to once the palette runs out).",
2995
+ optionsSchema: {
2996
+ type: "object",
2997
+ properties: { tolerance: { type: "number", minimum: 0, maximum: 1, description: "Largest OKLab distance still counted as the same colour (default 0.02)." } },
2998
+ additionalProperties: false
2999
+ },
3000
+ run(spec, profile, { tolerance = 0.02 }) {
3001
+ const palette = paletteOf(profile);
3002
+ const allowed = palette.flatMap((c) => CHART_PALETTE_SHADES.map((t) => shadeChartColor(c, t))).map((c) => ({ c, rgb: parseChartColor(c) }));
3003
+ return chartColors(spec).flatMap(({ color, label, target }) => {
3004
+ const rgb = parseChartColor(color);
3005
+ if (!rgb) return [];
3006
+ let best = { c: palette[0], d: Infinity };
3007
+ for (const a of allowed) {
3008
+ if (!a.rgb) continue;
3009
+ const d = chartColorDistance(rgb, a.rgb);
3010
+ if (d < best.d) best = { c: a.c, d };
3011
+ }
3012
+ if (best.d <= tolerance) return [];
3013
+ const nearest = palette.map((c) => ({ c, d: parseChartColor(c) ? chartColorDistance(rgb, parseChartColor(c)) : Infinity })).sort((a, b) => a.d - b.d)[0].c;
3014
+ return [{ severity: "error", message: `${label} uses ${color}, which is not in the house palette; use ${nearest} or another palette colour (${palette.join(", ")}).`, target }];
3015
+ });
3016
+ }
3017
+ };
3018
+ var distinguishable = {
3019
+ id: "distinguishable",
3020
+ description: "Series (or slice) colours can be told apart with normal vision and with protanopia, deuteranopia and tritanopia.",
3021
+ optionsSchema: {
3022
+ type: "object",
3023
+ properties: { minDistance: { type: "number", minimum: 0, maximum: 1, description: "Smallest OKLab distance between two colours after simulation (default 0.08)." } },
3024
+ additionalProperties: false
3025
+ },
3026
+ run(spec, _profile, { minDistance = 0.08 }) {
3027
+ const colors = chartColors(spec).filter((c) => c.target.kind !== "point" || chartColorsByCategory(spec.type));
3028
+ const result = checkChartColors(colors.map((c) => c.color), { minDistance, minContrast: 0 });
3029
+ const findings = [];
3030
+ for (const [vision, pair] of Object.entries(result.closest)) {
3031
+ if (!pair || pair.distance >= minDistance) continue;
3032
+ const who = vision === "normal" ? "" : ` with ${vision}`;
3033
+ findings.push({
3034
+ severity: "warning",
3035
+ message: `${colors[pair.a].label} (${pair.colors[0]}) and ${colors[pair.b].label} (${pair.colors[1]}) are hard to tell apart${who} (distance ${pair.distance}, want ${minDistance}); give the second another palette colour.`,
3036
+ target: colors[pair.b].target
3037
+ });
3038
+ }
3039
+ return findings;
3040
+ }
3041
+ };
3042
+ var minContrast = {
3043
+ id: "min-contrast",
3044
+ description: "Marks stand out from the background: each series or slice colour against the profile theme's background (or surface, default white).",
3045
+ optionsSchema: {
3046
+ type: "object",
3047
+ properties: { ratio: { type: "number", minimum: 1, maximum: 21, description: "Smallest contrast ratio (default 3, the WCAG ratio for graphics)." } },
3048
+ additionalProperties: false
3049
+ },
3050
+ run(spec, profile, { ratio = 3 }) {
3051
+ const theme = profile.theme ?? {};
3052
+ const background = theme.background && theme.background !== "transparent" ? theme.background : theme.surface ?? "#ffffff";
3053
+ return chartColors(spec).flatMap(({ color, label, target }) => {
3054
+ const r = chartContrastRatio(color, background);
3055
+ if (r === null || r >= ratio) return [];
3056
+ return [{ severity: "warning", message: `${label} (${color}) has contrast ${Math.round(r * 100) / 100}:1 against the background ${background}; want at least ${ratio}:1, so use a darker or lighter palette colour.`, target }];
3057
+ });
3058
+ }
3059
+ };
3060
+ var allowedType = {
3061
+ id: "allowed-type",
3062
+ description: "The chart is one of the chart types the house style allows.",
3063
+ optionsSchema: {
3064
+ type: "object",
3065
+ properties: { types: { type: "array", items: { type: "string", enum: [...CHART_TYPES3] }, minItems: 1, description: "Allowed chart types." } },
3066
+ required: ["types"],
3067
+ additionalProperties: false
3068
+ },
3069
+ run(spec, _profile, { types }) {
3070
+ const allowed = types;
3071
+ if (allowed.includes(spec.type)) return [];
3072
+ return [{ severity: "error", message: `This is a ${spec.type} chart; the house style allows ${allowed.join(", ")}. Change the type with update_chart.`, target: { kind: "chart" } }];
3073
+ }
3074
+ };
3075
+ var chartStyleDomain = {
3076
+ id: "chart",
3077
+ title: "Charts",
3078
+ description: "The house palette and theme for charts, which colour schemes and layouts are allowed, defaults for new charts, and the checks to run.",
3079
+ profileSchema: {
3080
+ type: "object",
3081
+ properties: {
3082
+ palette: { type: "array", items: COLOR, minItems: 1, description: "Series colours in order." },
3083
+ colorSchemes: { type: "array", items: { type: "string", enum: CHART_COLOR_SCHEMES.map((s) => s.id) }, description: "Colour scheme ids allowed besides the palette (empty: none)." },
3084
+ layouts: { type: "array", items: { type: "string", enum: CHART_LAYOUT_PRESETS.map((p) => p.id) }, description: "Layout preset ids allowed." },
3085
+ theme: {
3086
+ type: "object",
3087
+ properties: {
3088
+ background: COLOR,
3089
+ surface: COLOR,
3090
+ text: COLOR,
3091
+ mutedText: COLOR,
3092
+ grid: COLOR,
3093
+ axis: COLOR,
3094
+ positive: COLOR,
3095
+ negative: COLOR,
3096
+ fontFamily: { type: "string", minLength: 1, description: "CSS font family." },
3097
+ fontSize: { type: "number", minimum: 6, maximum: 48, description: "Body text size in px." },
3098
+ titleSize: { type: "number", minimum: 6, maximum: 72, description: "Title size in px." }
3099
+ },
3100
+ additionalProperties: false,
3101
+ description: "Colours and type to draw charts with."
3102
+ },
3103
+ defaults: {
3104
+ type: "object",
3105
+ properties: {
3106
+ dataLabels: { type: "boolean", description: "Value labels on." },
3107
+ points: { type: "boolean", description: "Dots on lines." },
3108
+ curve: { type: "string", enum: ["linear", "smooth", "step"], description: "Line interpolation." },
3109
+ stacking: { type: "string", enum: ["none", "stacked", "percent"], description: "Stacking." },
3110
+ orientation: { type: "string", enum: ["vertical", "horizontal"], description: "Bar direction." },
3111
+ labelPosition: { type: "string", enum: ["auto", "inside", "outside", "center"], description: "Bar label position." }
3112
+ },
3113
+ additionalProperties: false,
3114
+ description: "Spec values new charts start from."
3115
+ }
3116
+ },
3117
+ additionalProperties: false
3118
+ },
3119
+ checks: [paletteOnly, distinguishable, minContrast, allowedType]
3120
+ };
3121
+ function chartStylePatch(profile) {
3122
+ const p = profile;
3123
+ return { ...p.defaults ?? {}, ...p.palette?.length ? { palette: [...p.palette] } : {} };
3124
+ }
3125
+ function createChartStyleToolProvider(controller, styles, options = {}) {
3126
+ if (!styles.domains.some((d) => d.id === "chart")) throw new Error('The style resolver has no "chart" domain; add chartStyleDomain to its domains.');
3127
+ const check2 = {
3128
+ name: "check_chart_style",
3129
+ title: "Check chart house style",
3130
+ description: 'Checks the chart against the house style (see get_style with domain "chart"): runs the checks its profile turns on (palette-only, distinguishable, min-contrast, allowed-type). Returns findings with a severity, a sentence saying how to fix it, a target selection (series id or point) for update_chart_series or update_chart, and the layer that asked for it; plus the checks that ran and the ones skipped with the reason. Check after a batch of edits and again after fixing.',
3131
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
3132
+ annotations: { title: "Check chart house style", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
3133
+ handler: async () => {
3134
+ const { style, ...report } = await styles.check("chart", controller.getSpec());
3135
+ return {
3136
+ ...report,
3137
+ ok: report.findings.length === 0,
3138
+ ...style.skills.length ? {} : { note: "No house style applies to charts." },
3139
+ ...style.issues.length ? { issues: style.issues } : {}
3140
+ };
3141
+ }
3142
+ };
3143
+ return {
3144
+ id: options.id ?? "chart-style",
3145
+ namespace: options.namespace ?? "chart",
3146
+ title: "Chart house style",
3147
+ description: "Check a chart against the house style.",
3148
+ skill: "pixerate-chart",
3149
+ tools: [check2]
3150
+ };
3151
+ }
2672
3152
  export {
2673
3153
  CHART_AXIS_KINDS,
2674
3154
  CHART_AXIS_SCALES,
@@ -2695,6 +3175,7 @@ export {
2695
3175
  CHART_RADAR_GRATICULES,
2696
3176
  CHART_RASTER_MAX_SIZE,
2697
3177
  CHART_REFERENCE_AXES,
3178
+ CHART_SANKEY_LINK_COLORS,
2698
3179
  CHART_SERIES_TYPES,
2699
3180
  CHART_STACKINGS,
2700
3181
  CHART_TRENDLINE_TYPES,
@@ -2709,11 +3190,13 @@ export {
2709
3190
  DEFAULT_CHART_THEME,
2710
3191
  MAX_CHART_DATA_LABELS,
2711
3192
  MAX_CHART_LINE_POINTS,
3193
+ MAX_CHART_LINKS,
2712
3194
  MAX_CHART_MARKERS,
2713
3195
  MAX_CHART_TREE_DEPTH,
2714
3196
  MAX_CHART_TREE_NODES,
2715
3197
  OfficeChartsError,
2716
3198
  RENDER_CHART_INSTRUCTIONS,
3199
+ addChartLink,
2717
3200
  addChartTreeRow,
2718
3201
  analyzeChartTable,
2719
3202
  blendChartScenes,
@@ -2739,6 +3222,7 @@ export {
2739
3222
  chartHistogram,
2740
3223
  chartHitTest,
2741
3224
  chartLikertColor,
3225
+ chartLinksFromTable,
2742
3226
  chartLuminance,
2743
3227
  chartMermaidFamily,
2744
3228
  chartMovingAverage,
@@ -2747,9 +3231,14 @@ export {
2747
3231
  chartPointRef,
2748
3232
  chartPresetPatch,
2749
3233
  chartQuantile,
3234
+ chartRaceMoment,
3235
+ chartRaceRanking,
3236
+ chartRaceValueAt,
2750
3237
  chartRasterSize,
2751
3238
  chartRgbToHex,
2752
3239
  chartRgbToOklab,
3240
+ chartSankeyNodeValues,
3241
+ chartSankeyNodes,
2753
3242
  chartScaleFromPixel,
2754
3243
  chartScaleToPixel,
2755
3244
  chartSceneToDomSpec,
@@ -2760,6 +3249,8 @@ export {
2760
3249
  chartSliceColor,
2761
3250
  chartSpecColorsByCategory,
2762
3251
  chartSquarify,
3252
+ chartStyleDomain,
3253
+ chartStylePatch,
2763
3254
  chartSupportsTrendlines,
2764
3255
  chartTextBaseline,
2765
3256
  chartTextOn,
@@ -2789,6 +3280,7 @@ export {
2789
3280
  createChartEditorDocument,
2790
3281
  createChartRasterizer,
2791
3282
  createChartSpec,
3283
+ createChartStyleToolProvider,
2792
3284
  createChartToolProvider,
2793
3285
  createInstantChartClock,
2794
3286
  createOfficeChartToolProvider,
@@ -2827,6 +3319,7 @@ export {
2827
3319
  niceStep,
2828
3320
  normalizeChartAxis,
2829
3321
  normalizeChartErrorBars,
3322
+ normalizeChartLinks,
2830
3323
  normalizeChartReferenceBand,
2831
3324
  normalizeChartReferenceLine,
2832
3325
  normalizeChartSpec,
@@ -2847,6 +3340,7 @@ export {
2847
3340
  removeChartTreeRow,
2848
3341
  resolveChartSvgPaints,
2849
3342
  sameChartSelection,
3343
+ setChartLinkField,
2850
3344
  setChartTreeRowName,
2851
3345
  setChartTreeRowValue,
2852
3346
  shadeChartColor,