@pixerate/editor 0.165.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 (46) hide show
  1. package/dist/agent/index.cjs +34 -34
  2. package/dist/agent/index.js +5 -5
  3. package/dist/canvas/index.cjs +6 -6
  4. package/dist/canvas/index.js +2 -2
  5. package/dist/chart/controller.d.cts +29 -0
  6. package/dist/chart/controller.d.ts +29 -0
  7. package/dist/chart/index.cjs +301 -179
  8. package/dist/chart/index.d.cts +1 -0
  9. package/dist/chart/index.d.ts +1 -0
  10. package/dist/chart/index.js +132 -10
  11. package/dist/chart/layout-families.d.cts +7 -0
  12. package/dist/chart/layout-families.d.ts +7 -0
  13. package/dist/chart/layout-shared.d.cts +5 -0
  14. package/dist/chart/layout-shared.d.ts +5 -0
  15. package/dist/chart/race.d.cts +26 -0
  16. package/dist/chart/race.d.ts +26 -0
  17. package/dist/chart/schemas.d.cts +2 -0
  18. package/dist/chart/schemas.d.ts +2 -0
  19. package/dist/chart/spec.d.cts +1 -1
  20. package/dist/chart/spec.d.ts +1 -1
  21. package/dist/chart/types.d.cts +6 -2
  22. package/dist/chart/types.d.ts +6 -2
  23. package/dist/{chunk-FEU2XVQH.js → chunk-5UNQDC3Q.js} +5 -2
  24. package/dist/{chunk-64VWWR5U.cjs → chunk-6FYIFGBW.cjs} +16 -16
  25. package/dist/{chunk-UVXTOZ2B.cjs → chunk-BPX4Z3N3.cjs} +12 -3
  26. package/dist/{chunk-MKXK6E4O.cjs → chunk-CHLV7UFP.cjs} +51 -51
  27. package/dist/{chunk-X5W645RI.js → chunk-DFCAR7AW.js} +3 -3
  28. package/dist/{chunk-2ZRN43IN.js → chunk-FR432ROA.js} +12 -3
  29. package/dist/{chunk-ODFCS7HF.cjs → chunk-HXOBOHH2.cjs} +8 -8
  30. package/dist/{chunk-V5GSY3NC.js → chunk-ITANXMZD.js} +78 -2
  31. package/dist/{chunk-TVQW26AV.cjs → chunk-MZFZSVUJ.cjs} +26 -23
  32. package/dist/{chunk-N7NBGT2Y.js → chunk-NQQDGGXY.js} +2 -2
  33. package/dist/{chunk-Q25WAFTD.cjs → chunk-SZWVMB3B.cjs} +21 -18
  34. package/dist/{chunk-QO2T6KA5.cjs → chunk-UUUTEVX6.cjs} +116 -40
  35. package/dist/{chunk-IMRXC7OA.cjs → chunk-V5L2G22R.cjs} +153 -77
  36. package/dist/{chunk-2J5VLJHC.js → chunk-VB2CY6EG.js} +3 -3
  37. package/dist/{chunk-CRFSQWKQ.js → chunk-XNU45TYA.js} +4 -1
  38. package/dist/{chunk-FRBRLKNQ.js → chunk-YUFTVNLX.js} +78 -2
  39. package/dist/export/index.cjs +4 -4
  40. package/dist/export/index.js +3 -3
  41. package/dist/extensions/index.cjs +7 -7
  42. package/dist/extensions/index.js +6 -6
  43. package/dist/index.cjs +13 -13
  44. package/dist/index.js +7 -7
  45. package/package.json +1 -1
  46. package/skills/pixerate-chart/SKILL.md +20 -2
@@ -2,6 +2,7 @@ export * from './types.cjs';
2
2
  export * from './spec.cjs';
3
3
  export * from './tree.cjs';
4
4
  export * from './links.cjs';
5
+ export * from './race.cjs';
5
6
  export * from './scale.cjs';
6
7
  export * from './numfmt.cjs';
7
8
  export * from './color.cjs';
@@ -2,6 +2,7 @@ export * from './types.js';
2
2
  export * from './spec.js';
3
3
  export * from './tree.js';
4
4
  export * from './links.js';
5
+ export * from './race.js';
5
6
  export * from './scale.js';
6
7
  export * from './numfmt.js';
7
8
  export * from './color.js';
@@ -4,6 +4,7 @@ import {
4
4
  HEIGHT,
5
5
  LINK_INDEX,
6
6
  LINK_PROPS,
7
+ RACE_TIME,
7
8
  REFERENCE_BAND_PATCH,
8
9
  REFERENCE_BAND_PROPS,
9
10
  REFERENCE_INDEX,
@@ -16,7 +17,7 @@ import {
16
17
  TRENDLINE_PROPS,
17
18
  VALUE,
18
19
  WIDTH
19
- } from "../chunk-FEU2XVQH.js";
20
+ } from "../chunk-5UNQDC3Q.js";
20
21
  import {
21
22
  CHART_OOXML_CONTENT_TYPE,
22
23
  CHART_OOXML_NS,
@@ -24,7 +25,7 @@ import {
24
25
  chartFormatToCode,
25
26
  chartFromOoxml,
26
27
  chartToOoxml
27
- } from "../chunk-CRFSQWKQ.js";
28
+ } from "../chunk-XNU45TYA.js";
28
29
  import {
29
30
  CHART_COLOR_SCHEMES,
30
31
  CHART_LAYOUT_PRESETS,
@@ -46,7 +47,7 @@ import {
46
47
  monochromeChartPalette,
47
48
  sparklineToSvg,
48
49
  summarizeChart
49
- } from "../chunk-FRBRLKNQ.js";
50
+ } from "../chunk-YUFTVNLX.js";
50
51
  import "../chunk-KGRZMHZB.js";
51
52
  import {
52
53
  CHART_DATE_STEPS,
@@ -59,6 +60,9 @@ import {
59
60
  chartHistogram,
60
61
  chartMovingAverage,
61
62
  chartQuantile,
63
+ chartRaceMoment,
64
+ chartRaceRanking,
65
+ chartRaceValueAt,
62
66
  chartScaleFromPixel,
63
67
  chartScaleToPixel,
64
68
  chartSerialToDate,
@@ -87,7 +91,7 @@ import {
87
91
  suggestChartReferences,
88
92
  summarizeChartSeries,
89
93
  trendNumberText
90
- } from "../chunk-V5GSY3NC.js";
94
+ } from "../chunk-ITANXMZD.js";
91
95
  import {
92
96
  CHART_AXIS_KINDS,
93
97
  CHART_AXIS_SCALES,
@@ -181,7 +185,7 @@ import {
181
185
  shadeChartColor,
182
186
  simulateChartVision,
183
187
  uniqueChartSeriesId
184
- } from "../chunk-2ZRN43IN.js";
188
+ } from "../chunk-FR432ROA.js";
185
189
  import "../chunk-O6OGNDND.js";
186
190
  import "../chunk-II2WIF5Z.js";
187
191
  import {
@@ -396,6 +400,7 @@ function recommendForShape(shape) {
396
400
  add("area", "Area", "Shows change over time with volume.");
397
401
  add("bar", "Column", "Compares each period.");
398
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.");
399
404
  return out;
400
405
  }
401
406
  if (shape.series === 1) {
@@ -430,6 +435,7 @@ function recommendForShape(shape) {
430
435
  add("bar", "Stacked column", "Shows how the series add up.", "stacked");
431
436
  add("line", "Line", "Shows the series in order.");
432
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.");
433
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.");
434
440
  }
435
441
  return out;
@@ -475,6 +481,12 @@ function createChartCommands(controller, options = {}) {
475
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.`);
476
482
  return index;
477
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
+ };
478
490
  const object = (properties, required) => ({ type: "object", properties, ...required ? { required } : {}, additionalProperties: false });
479
491
  return [
480
492
  {
@@ -887,6 +899,57 @@ function createChartCommands(controller, options = {}) {
887
899
  return { links: controller.getSpec().links };
888
900
  }
889
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
+ },
890
953
  {
891
954
  ...base,
892
955
  id: `${prefix}applyPreset`,
@@ -1484,6 +1547,10 @@ function planChartToolPlayback(tool, args) {
1484
1547
  return click(control("series.callout"));
1485
1548
  case "highlight_chart":
1486
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" });
1487
1554
  case "render_chart":
1488
1555
  return [{ type: "target", ref: { kind: "action", id: "download-png" } }];
1489
1556
  case "undo_chart":
@@ -1548,6 +1615,22 @@ function createChartToolProvider(controller, options = {}) {
1548
1615
  const depth = chartTreeDepth(spec().tree);
1549
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])];
1550
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
+ };
1551
1634
  const linksSummary = () => ({ links: spec().links, nodes: spec().sankeyNodes.map((name, i) => ({ name, value: chartSankeyNodeValues(spec().links, spec().sankeyNodes)[i] })) });
1552
1635
  const treeSummary = () => ({ tree: spec().tree, groups: spec().tree.map((n) => ({ name: n.name, total: chartTreeTotal(n) })), depth: chartTreeDepth(spec().tree) });
1553
1636
  const Parser = () => options.DOMParser ?? globalThis.DOMParser;
@@ -1579,14 +1662,16 @@ function createChartToolProvider(controller, options = {}) {
1579
1662
  properties: {
1580
1663
  width: WIDTH,
1581
1664
  height: HEIGHT,
1582
- 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}` }
1583
1667
  },
1584
1668
  additionalProperties: false
1585
1669
  },
1586
1670
  annotations: { title: "Render the chart", ...READ },
1587
- handler: async ({ width = size.width, height = size.height, format = "svg" }) => {
1671
+ handler: async ({ width = size.width, height = size.height, format = "svg", time }) => {
1588
1672
  const json = controller.toJSON();
1589
- 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 });
1590
1675
  const svg = chartSceneToSvg(scene, { ariaLabel: json.title || void 0, description: describeChart(json) });
1591
1676
  const warnings = scene.warnings;
1592
1677
  if (format === "png") {
@@ -1659,6 +1744,14 @@ function createChartToolProvider(controller, options = {}) {
1659
1744
  return { trendline: line, coefficients: fit.coefficients, r2: fit.r2, equation: fit.equation, points: fit.points };
1660
1745
  }
1661
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
+ };
1662
1755
  const listPresets = {
1663
1756
  name: "list_chart_presets",
1664
1757
  title: "List chart presets",
@@ -2150,6 +2243,30 @@ function createChartToolProvider(controller, options = {}) {
2150
2243
  return { links: spec().links };
2151
2244
  }
2152
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
+ };
2153
2270
  const applyPreset = {
2154
2271
  name: "apply_chart_preset",
2155
2272
  title: "Apply a chart style",
@@ -2315,7 +2432,7 @@ function createChartToolProvider(controller, options = {}) {
2315
2432
  return { redone, ...redone ? { label } : {} };
2316
2433
  }
2317
2434
  };
2318
- const reads = [getChart, renderChart, checkColors, recommend, getTrendline, listPresets, exportMermaid, exportOoxml];
2435
+ const reads = [getChart, renderChart, checkColors, recommend, getTrendline, raceRanking, listPresets, exportMermaid, exportOoxml];
2319
2436
  const writes = [
2320
2437
  updateChart,
2321
2438
  setData,
@@ -2347,6 +2464,8 @@ function createChartToolProvider(controller, options = {}) {
2347
2464
  addLink,
2348
2465
  updateLink,
2349
2466
  removeLink,
2467
+ setRaceFrame,
2468
+ playRace,
2350
2469
  applyPreset,
2351
2470
  transpose,
2352
2471
  select,
@@ -2359,7 +2478,7 @@ function createChartToolProvider(controller, options = {}) {
2359
2478
  id: options.id ?? "chart",
2360
2479
  namespace: options.namespace,
2361
2480
  title: "Chart editor",
2362
- description: "One chart: its spec, data and values, series and categories, trendlines, reference lines and bands, callouts, highlight, grouped data, flows 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.",
2363
2482
  skill: "pixerate-chart",
2364
2483
  tools: () => {
2365
2484
  if (readOnlyReason()) return reads;
@@ -3112,6 +3231,9 @@ export {
3112
3231
  chartPointRef,
3113
3232
  chartPresetPatch,
3114
3233
  chartQuantile,
3234
+ chartRaceMoment,
3235
+ chartRaceRanking,
3236
+ chartRaceValueAt,
3115
3237
  chartRasterSize,
3116
3238
  chartRgbToHex,
3117
3239
  chartRgbToOklab,
@@ -45,3 +45,10 @@ export declare function layoutSunburst(ctx: LayoutCtx, area: ChartRect, series:
45
45
  * in the order of the nodes at their other end, so they cross as little as the order allows.
46
46
  */
47
47
  export declare function layoutSankey(ctx: LayoutCtx, area: ChartRect, series: ChartSeries | undefined): FamilyResult;
48
+ /**
49
+ * Bar race: the top `raceTopN` names at one moment (`frame`, a fractional category index;
50
+ * default the last time point) as ranked horizontal bars. Values ease between time points and
51
+ * places slide from one ranking to the next, so drawing frame after frame (or blending scenes,
52
+ * which match bars by series) animates the race. The time point is written large in a corner.
53
+ */
54
+ export declare function layoutBarRace(ctx: LayoutCtx, area: ChartRect, frame: number | null | undefined): FamilyResult;
@@ -45,3 +45,10 @@ export declare function layoutSunburst(ctx: LayoutCtx, area: ChartRect, series:
45
45
  * in the order of the nodes at their other end, so they cross as little as the order allows.
46
46
  */
47
47
  export declare function layoutSankey(ctx: LayoutCtx, area: ChartRect, series: ChartSeries | undefined): FamilyResult;
48
+ /**
49
+ * Bar race: the top `raceTopN` names at one moment (`frame`, a fractional category index;
50
+ * default the last time point) as ranked horizontal bars. Values ease between time points and
51
+ * places slide from one ranking to the next, so drawing frame after frame (or blending scenes,
52
+ * which match bars by series) animates the race. The time point is written large in a corner.
53
+ */
54
+ export declare function layoutBarRace(ctx: LayoutCtx, area: ChartRect, frame: number | null | undefined): FamilyResult;
@@ -34,6 +34,11 @@ export interface ChartLayoutOptions {
34
34
  seriesId: string;
35
35
  index: number;
36
36
  } | null;
37
+ /**
38
+ * Bar race: the moment to draw, as a category index that may be fractional (2.5 is halfway
39
+ * between the third and fourth time points). Default: the last time point.
40
+ */
41
+ raceFrame?: number | null;
37
42
  }
38
43
  /** A DOM-free text width estimate (average glyph ≈ 0.56 em, bold ≈ 0.6 em). */
39
44
  export declare const estimateChartTextWidth: ChartTextMeasure;
@@ -34,6 +34,11 @@ export interface ChartLayoutOptions {
34
34
  seriesId: string;
35
35
  index: number;
36
36
  } | null;
37
+ /**
38
+ * Bar race: the moment to draw, as a category index that may be fractional (2.5 is halfway
39
+ * between the third and fourth time points). Default: the last time point.
40
+ */
41
+ raceFrame?: number | null;
37
42
  }
38
43
  /** A DOM-free text width estimate (average glyph ≈ 0.56 em, bold ≈ 0.6 em). */
39
44
  export declare const estimateChartTextWidth: ChartTextMeasure;
@@ -0,0 +1,26 @@
1
+ import type { ChartSeries, ChartSpec } from './types.cjs';
2
+ /** A series' value at time point `index`: its own, else the last value before it (0 before the first). */
3
+ export declare function chartRaceValueAt(series: Pick<ChartSeries, 'values'>, index: number): number;
4
+ /** The moment `frame` stands for: the time points either side and how far between them (0–1). */
5
+ export declare function chartRaceMoment(count: number, frame: number | null | undefined): {
6
+ from: number;
7
+ to: number;
8
+ t: number;
9
+ at: number;
10
+ };
11
+ export interface ChartRaceEntry {
12
+ seriesId: string;
13
+ name: string;
14
+ /** The value at the moment (between time points, eased from one to the next). */
15
+ value: number;
16
+ /** 0-based place at the moment, fractional while two names swap. */
17
+ position: number;
18
+ /** 0-based place at the nearest time point. */
19
+ rank: number;
20
+ }
21
+ /**
22
+ * Every visible series at a moment, best first: its value between the two time points and its
23
+ * position, which slides from its rank at one time point to its rank at the next so bars swap
24
+ * smoothly. Ties keep the series order.
25
+ */
26
+ export declare function chartRaceRanking(spec: Pick<ChartSpec, 'series' | 'categories'>, frame: number | null | undefined): ChartRaceEntry[];
@@ -0,0 +1,26 @@
1
+ import type { ChartSeries, ChartSpec } from './types.js';
2
+ /** A series' value at time point `index`: its own, else the last value before it (0 before the first). */
3
+ export declare function chartRaceValueAt(series: Pick<ChartSeries, 'values'>, index: number): number;
4
+ /** The moment `frame` stands for: the time points either side and how far between them (0–1). */
5
+ export declare function chartRaceMoment(count: number, frame: number | null | undefined): {
6
+ from: number;
7
+ to: number;
8
+ t: number;
9
+ at: number;
10
+ };
11
+ export interface ChartRaceEntry {
12
+ seriesId: string;
13
+ name: string;
14
+ /** The value at the moment (between time points, eased from one to the next). */
15
+ value: number;
16
+ /** 0-based place at the moment, fractional while two names swap. */
17
+ position: number;
18
+ /** 0-based place at the nearest time point. */
19
+ rank: number;
20
+ }
21
+ /**
22
+ * Every visible series at a moment, best first: its value between the two time points and its
23
+ * position, which slides from its rank at one time point to its rank at the next so bars swap
24
+ * smoothly. Ties keep the series order.
25
+ */
26
+ export declare function chartRaceRanking(spec: Pick<ChartSpec, 'series' | 'categories'>, frame: number | null | undefined): ChartRaceEntry[];
@@ -22,6 +22,8 @@ export declare const TREE_DEFS: {
22
22
  export declare const TREE_PATH: JsonSchema;
23
23
  /** One Sankey flow's fields; source, target and value are required when adding. */
24
24
  export declare const LINK_PROPS: Record<string, JsonSchema>;
25
+ /** A moment of a bar race: a time point by label, or a category index (fractional between time points). */
26
+ export declare const RACE_TIME: JsonSchema;
25
27
  export declare const LINK_INDEX: JsonSchema;
26
28
  export declare const ERROR_BARS: (what: string) => JsonSchema;
27
29
  export declare const SERIES_PROPS: Record<string, JsonSchema>;
@@ -22,6 +22,8 @@ export declare const TREE_DEFS: {
22
22
  export declare const TREE_PATH: JsonSchema;
23
23
  /** One Sankey flow's fields; source, target and value are required when adding. */
24
24
  export declare const LINK_PROPS: Record<string, JsonSchema>;
25
+ /** A moment of a bar race: a time point by label, or a category index (fractional between time points). */
26
+ export declare const RACE_TIME: JsonSchema;
25
27
  export declare const LINK_INDEX: JsonSchema;
26
28
  export declare const ERROR_BARS: (what: string) => JsonSchema;
27
29
  export declare const SERIES_PROPS: Record<string, JsonSchema>;
@@ -19,7 +19,7 @@ export declare const CHART_NUMBER_STYLES: readonly NonNullable<ChartNumberFormat
19
19
  /** Labels for pickers, shared by the React and Svelte components. */
20
20
  export declare const CHART_TYPE_LABELS: Readonly<Record<ChartType, string>>;
21
21
  /** How a chart type is laid out. */
22
- export type ChartFamily = 'cartesian' | 'xy' | 'pie' | 'radar' | 'waterfall' | 'histogram' | 'box' | 'funnel' | 'treemap' | 'slope' | 'likert' | 'parliament' | 'sunburst' | 'sankey';
22
+ export type ChartFamily = 'cartesian' | 'xy' | 'pie' | 'radar' | 'waterfall' | 'histogram' | 'box' | 'funnel' | 'treemap' | 'slope' | 'likert' | 'parliament' | 'sunburst' | 'sankey' | 'barrace';
23
23
  /** A colour-blind-friendly categorical palette (Okabe–Ito order, tuned for light and dark backgrounds). */
24
24
  export declare const DEFAULT_CHART_PALETTE: readonly string[];
25
25
  export declare const DEFAULT_CHART_THEME: ChartTheme;
@@ -19,7 +19,7 @@ export declare const CHART_NUMBER_STYLES: readonly NonNullable<ChartNumberFormat
19
19
  /** Labels for pickers, shared by the React and Svelte components. */
20
20
  export declare const CHART_TYPE_LABELS: Readonly<Record<ChartType, string>>;
21
21
  /** How a chart type is laid out. */
22
- export type ChartFamily = 'cartesian' | 'xy' | 'pie' | 'radar' | 'waterfall' | 'histogram' | 'box' | 'funnel' | 'treemap' | 'slope' | 'likert' | 'parliament' | 'sunburst' | 'sankey';
22
+ export type ChartFamily = 'cartesian' | 'xy' | 'pie' | 'radar' | 'waterfall' | 'histogram' | 'box' | 'funnel' | 'treemap' | 'slope' | 'likert' | 'parliament' | 'sunburst' | 'sankey' | 'barrace';
23
23
  /** A colour-blind-friendly categorical palette (Okabe–Ito order, tuned for light and dark backgrounds). */
24
24
  export declare const DEFAULT_CHART_PALETTE: readonly string[];
25
25
  export declare const DEFAULT_CHART_THEME: ChartTheme;
@@ -17,8 +17,10 @@
17
17
  * - `sunburst` draws `tree` as rings from the centre out (or, without a tree, the first series
18
18
  * as one ring); a `treemap` with a `tree` nests each group's rectangles inside it.
19
19
  * - `sankey` draws `links` as flows between columns of nodes.
20
+ * - `barrace` reads each category as a time point and each series as a name, and draws the
21
+ * top `raceTopN` names at one moment as ranked horizontal bars (an animated ranking).
20
22
  */
21
- export type ChartType = 'bar' | 'line' | 'area' | 'scatter' | 'bubble' | 'pie' | 'donut' | 'radar' | 'waterfall' | 'histogram' | 'box' | 'funnel' | 'treemap' | 'slope' | 'likert' | 'parliament' | 'sunburst' | 'sankey';
23
+ export type ChartType = 'bar' | 'line' | 'area' | 'scatter' | 'bubble' | 'pie' | 'donut' | 'radar' | 'waterfall' | 'histogram' | 'box' | 'funnel' | 'treemap' | 'slope' | 'likert' | 'parliament' | 'sunburst' | 'sankey' | 'barrace';
22
24
  /** How a series is drawn in a cartesian chart. */
23
25
  export type ChartSeriesType = 'bar' | 'line' | 'area' | 'scatter';
24
26
  /** One data value; `null` is a gap (missing value). */
@@ -299,6 +301,8 @@ export interface ChartSpec {
299
301
  sankeyNodes: string[];
300
302
  /** Sankey: what colours each flow. */
301
303
  sankeyLinkColor: ChartSankeyLinkColor;
304
+ /** Bar race: how many bars each moment shows, 1–30. */
305
+ raceTopN: number;
302
306
  }
303
307
  /** What a click selects. */
304
308
  export type ChartSelection = {
@@ -354,7 +358,7 @@ export interface ChartRect {
354
358
  height: number;
355
359
  }
356
360
  /** What a mark is, for styling and tests. */
357
- export type ChartMarkRole = 'background' | 'title' | 'subtitle' | 'grid' | 'axis' | 'tick' | 'tick-label' | 'axis-title' | 'bar' | 'line' | 'area' | 'point' | 'slice' | 'cell' | 'box' | 'whisker' | 'outlier' | 'connector' | 'trendline' | 'trendline-label' | 'error-bar' | 'data-label' | 'legend-swatch' | 'legend-label' | 'drag-readout' | 'reference-line' | 'reference-band' | 'reference-label' | 'callout' | 'node' | 'link' | 'note' | 'selection' | 'empty';
361
+ export type ChartMarkRole = 'background' | 'title' | 'subtitle' | 'grid' | 'axis' | 'tick' | 'tick-label' | 'axis-title' | 'bar' | 'line' | 'area' | 'point' | 'slice' | 'cell' | 'box' | 'whisker' | 'outlier' | 'connector' | 'trendline' | 'trendline-label' | 'error-bar' | 'data-label' | 'legend-swatch' | 'legend-label' | 'drag-readout' | 'reference-line' | 'reference-band' | 'reference-label' | 'callout' | 'node' | 'race-time' | 'link' | 'note' | 'selection' | 'empty';
358
362
  interface ChartMarkBase {
359
363
  /** Unique within the scene (a stable key for frameworks). */
360
364
  key: string;
@@ -17,8 +17,10 @@
17
17
  * - `sunburst` draws `tree` as rings from the centre out (or, without a tree, the first series
18
18
  * as one ring); a `treemap` with a `tree` nests each group's rectangles inside it.
19
19
  * - `sankey` draws `links` as flows between columns of nodes.
20
+ * - `barrace` reads each category as a time point and each series as a name, and draws the
21
+ * top `raceTopN` names at one moment as ranked horizontal bars (an animated ranking).
20
22
  */
21
- export type ChartType = 'bar' | 'line' | 'area' | 'scatter' | 'bubble' | 'pie' | 'donut' | 'radar' | 'waterfall' | 'histogram' | 'box' | 'funnel' | 'treemap' | 'slope' | 'likert' | 'parliament' | 'sunburst' | 'sankey';
23
+ export type ChartType = 'bar' | 'line' | 'area' | 'scatter' | 'bubble' | 'pie' | 'donut' | 'radar' | 'waterfall' | 'histogram' | 'box' | 'funnel' | 'treemap' | 'slope' | 'likert' | 'parliament' | 'sunburst' | 'sankey' | 'barrace';
22
24
  /** How a series is drawn in a cartesian chart. */
23
25
  export type ChartSeriesType = 'bar' | 'line' | 'area' | 'scatter';
24
26
  /** One data value; `null` is a gap (missing value). */
@@ -299,6 +301,8 @@ export interface ChartSpec {
299
301
  sankeyNodes: string[];
300
302
  /** Sankey: what colours each flow. */
301
303
  sankeyLinkColor: ChartSankeyLinkColor;
304
+ /** Bar race: how many bars each moment shows, 1–30. */
305
+ raceTopN: number;
302
306
  }
303
307
  /** What a click selects. */
304
308
  export type ChartSelection = {
@@ -354,7 +358,7 @@ export interface ChartRect {
354
358
  height: number;
355
359
  }
356
360
  /** What a mark is, for styling and tests. */
357
- export type ChartMarkRole = 'background' | 'title' | 'subtitle' | 'grid' | 'axis' | 'tick' | 'tick-label' | 'axis-title' | 'bar' | 'line' | 'area' | 'point' | 'slice' | 'cell' | 'box' | 'whisker' | 'outlier' | 'connector' | 'trendline' | 'trendline-label' | 'error-bar' | 'data-label' | 'legend-swatch' | 'legend-label' | 'drag-readout' | 'reference-line' | 'reference-band' | 'reference-label' | 'callout' | 'node' | 'link' | 'note' | 'selection' | 'empty';
361
+ export type ChartMarkRole = 'background' | 'title' | 'subtitle' | 'grid' | 'axis' | 'tick' | 'tick-label' | 'axis-title' | 'bar' | 'line' | 'area' | 'point' | 'slice' | 'cell' | 'box' | 'whisker' | 'outlier' | 'connector' | 'trendline' | 'trendline-label' | 'error-bar' | 'data-label' | 'legend-swatch' | 'legend-label' | 'drag-readout' | 'reference-line' | 'reference-band' | 'reference-label' | 'callout' | 'node' | 'race-time' | 'link' | 'note' | 'selection' | 'empty';
358
362
  interface ChartMarkBase {
359
363
  /** Unique within the scene (a stable key for frameworks). */
360
364
  key: string;
@@ -18,7 +18,7 @@ import {
18
18
  CHART_STACKINGS,
19
19
  CHART_TRENDLINE_TYPES,
20
20
  CHART_TYPES
21
- } from "./chunk-2ZRN43IN.js";
21
+ } from "./chunk-FR432ROA.js";
22
22
 
23
23
  // src/chart/schemas.ts
24
24
  var VALUE = { type: ["number", "string", "null"], description: 'A number, numeric text ("1,200", "12%", "$40", "(3)") or null for a gap.' };
@@ -119,6 +119,7 @@ var LINK_PROPS = {
119
119
  value: { type: "number", exclusiveMinimum: 0, description: "How much flows, above 0 (its width)." },
120
120
  color: { type: "string", description: "CSS colour (default: by sankeyLinkColor)." }
121
121
  };
122
+ var RACE_TIME = { type: ["string", "number"], description: 'Time point label (a category, e.g. "2023") or index from 0; an index may be fractional, 2.5 being halfway between the third and fourth time points.' };
122
123
  var LINK_INDEX = { type: "integer", minimum: 0, description: "Flow index (0 = first), as get_chart lists them in spec.links." };
123
124
  var ERROR_BARS = (what) => ({
124
125
  type: ["object", "null"],
@@ -157,7 +158,7 @@ var SERIES_PROPS = {
157
158
  xErrorBars: ERROR_BARS("Error bars along the x axis (scatter and bubble charts with numeric x values; amounts in x units).")
158
159
  };
159
160
  var CHART_UPDATE_PROPERTIES = {
160
- type: { type: "string", enum: [...CHART_TYPES], description: "Chart type. slope joins each category from the first series (before) to the second (after); likert reads each category as a question and the series as answers from most negative to most positive; parliament draws the first series as seats; sunburst draws the grouped data (set_chart_tree) as rings, and a treemap with grouped data nests its groups; sankey draws the flows (set_chart_links)." },
161
+ type: { type: "string", enum: [...CHART_TYPES], description: "Chart type. slope joins each category from the first series (before) to the second (after); likert reads each category as a question and the series as answers from most negative to most positive; parliament draws the first series as seats; sunburst draws the grouped data (set_chart_tree) as rings, and a treemap with grouped data nests its groups; sankey draws the flows (set_chart_links); barrace reads categories as time points and series as names, and animates their ranking." },
161
162
  title: { type: "string", description: 'Title ("" for none).' },
162
163
  subtitle: { type: "string", description: 'Subtitle ("" for none).' },
163
164
  xAxis: AXIS("Category axis (or numeric x axis of scatter / bubble charts)."),
@@ -195,6 +196,7 @@ var CHART_UPDATE_PROPERTIES = {
195
196
  parliamentLabel: { type: "string", description: 'Parliament: caption under the seat total ("" writes "seats").' },
196
197
  sunburstLabel: { type: "string", description: 'Sunburst: caption under the total in the centre ("" for none).' },
197
198
  sankeyLinkColor: { type: "string", enum: [...CHART_SANKEY_LINK_COLORS], description: "Sankey: colour each flow by its source node (default), its target node, or grey." },
199
+ raceTopN: { type: "integer", minimum: 1, maximum: 30, description: "Bar race: how many bars each moment shows (default 10)." },
198
200
  sankeyNodes: { type: "array", items: { type: "string" }, description: "Sankey: node names in the order to stack them within their columns (reorder to untangle crossing flows); nodes left out follow in the order the flows name them." }
199
201
  };
200
202
 
@@ -213,6 +215,7 @@ export {
213
215
  TREE_DEFS,
214
216
  TREE_PATH,
215
217
  LINK_PROPS,
218
+ RACE_TIME,
216
219
  LINK_INDEX,
217
220
  SERIES_PROPS,
218
221
  CHART_UPDATE_PROPERTIES