@motionscript/charts 0.1.0-alpha.6 → 0.1.0-alpha.8

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 (66) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/NOTICE +15 -14
  3. package/dist/bar-chart/bar-chart.d.ts +46 -2
  4. package/dist/bar-chart/bar-chart.d.ts.map +1 -1
  5. package/dist/bar-chart/bar-chart.js +13 -5
  6. package/dist/bar-chart/bar-chart.js.map +1 -1
  7. package/dist/bar-chart/category-axis.d.ts +12 -1
  8. package/dist/bar-chart/category-axis.d.ts.map +1 -1
  9. package/dist/bar-chart/chart-bars.d.ts +15 -1
  10. package/dist/bar-chart/chart-bars.d.ts.map +1 -1
  11. package/dist/bar-chart/chart-bars.js +6 -7
  12. package/dist/bar-chart/chart-bars.js.map +1 -1
  13. package/dist/browser/index.js +1 -1
  14. package/dist/browser/index.js.map +3 -3
  15. package/dist/browser/manifest.json +1 -1
  16. package/dist/kit/chart-border.d.ts +3 -1
  17. package/dist/kit/chart-border.d.ts.map +1 -1
  18. package/dist/kit/chart-canvas.d.ts +10 -1
  19. package/dist/kit/chart-canvas.d.ts.map +1 -1
  20. package/dist/kit/chart-legend.d.ts +48 -1
  21. package/dist/kit/chart-legend.d.ts.map +1 -1
  22. package/dist/kit/vertical-axis.d.ts +13 -1
  23. package/dist/kit/vertical-axis.d.ts.map +1 -1
  24. package/dist/line-chart/chart-line.d.ts +46 -37
  25. package/dist/line-chart/chart-line.d.ts.map +1 -1
  26. package/dist/line-chart/chart-line.js +11 -1
  27. package/dist/line-chart/chart-line.js.map +1 -1
  28. package/dist/line-chart/chart-region.d.ts +21 -22
  29. package/dist/line-chart/chart-region.d.ts.map +1 -1
  30. package/dist/line-chart/chart-region.js +5 -1
  31. package/dist/line-chart/chart-region.js.map +1 -1
  32. package/dist/line-chart/horizontal-axis.d.ts +43 -25
  33. package/dist/line-chart/horizontal-axis.d.ts.map +1 -1
  34. package/dist/line-chart/horizontal-axis.js +10 -1
  35. package/dist/line-chart/horizontal-axis.js.map +1 -1
  36. package/dist/line-chart/line-chart.d.ts +47 -2
  37. package/dist/line-chart/line-chart.d.ts.map +1 -1
  38. package/dist/line-chart/line-chart.js +16 -7
  39. package/dist/line-chart/line-chart.js.map +1 -1
  40. package/dist/line-chart/x-scale.d.ts +3 -10
  41. package/dist/line-chart/x-scale.d.ts.map +1 -1
  42. package/dist/line-chart/x-scale.js +15 -4
  43. package/dist/line-chart/x-scale.js.map +1 -1
  44. package/dist/pie-chart/pie-chart.d.ts +35 -2
  45. package/dist/pie-chart/pie-chart.d.ts.map +1 -1
  46. package/dist/pie-chart/pie-chart.js +4 -1
  47. package/dist/pie-chart/pie-chart.js.map +1 -1
  48. package/dist/pie-chart/pie-legend.d.ts +31 -1
  49. package/dist/pie-chart/pie-legend.d.ts.map +1 -1
  50. package/dist/pie-chart/pie-ring.d.ts +19 -1
  51. package/dist/pie-chart/pie-ring.d.ts.map +1 -1
  52. package/dist/pie-chart/shared.d.ts +8 -2
  53. package/dist/pie-chart/shared.d.ts.map +1 -1
  54. package/dist/pie-chart/shared.js +10 -4
  55. package/dist/pie-chart/shared.js.map +1 -1
  56. package/package.json +5 -4
  57. package/src/bar-chart/bar-chart.ts +13 -5
  58. package/src/bar-chart/chart-bars.ts +6 -7
  59. package/src/line-chart/chart-line.ts +11 -36
  60. package/src/line-chart/chart-region.ts +5 -21
  61. package/src/line-chart/horizontal-axis.ts +10 -25
  62. package/src/line-chart/line-chart.ts +20 -7
  63. package/src/line-chart/x-scale.ts +18 -3
  64. package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  65. package/src/pie-chart/pie-chart.ts +4 -1
  66. package/src/pie-chart/shared.ts +10 -4
@@ -1 +1 @@
1
- {"version":3,"file":"shared.js","sourceRoot":"","sources":["../../src/pie-chart/shared.ts"],"names":[],"mappings":"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","sourcesContent":["/**\r\n * Types and pure helpers specific to the {@link PieChart} family: how rows\r\n * become slices, how slices become arcs on a ring, where the labels around it\r\n * end up, and the theme all of that is drawn from.\r\n *\r\n * The one chart in the family that draws **one mark per row** rather than one\r\n * per column, which is why almost nothing about its geometry is shared: there is\r\n * no scale, no axis and no baseline, only a circle divided by share. What it\r\n * *does* share — the legend, the theme leaf resolvers, the palette, the\r\n * percentage format — comes from `../kit/shared.ts`.\r\n *\r\n * Kept renderer-agnostic (no `Node` imports) so a host's inspector mapping\r\n * can build a partial {@link PieChartTheme} without\r\n * pulling the node classes in.\r\n */\r\n\r\nimport {\r\n lerpNumber,\r\n shadowOps,\r\n strokeOps,\r\n textOps,\r\n type DataRecord,\r\n type Fill,\r\n type Shadow,\r\n type ShadowResolved,\r\n type Stroke,\r\n type StrokeResolved,\r\n type TextStyle,\r\n type Vector2,\r\n} from \"@motionscript/sdk\"\r\n\r\nimport {\r\n CHART_PALETTE,\r\n DEFAULT_LEGEND,\r\n DEFAULT_SERIES_FILLS,\r\n SPOTLIGHT_DIM,\r\n lerpLegendConfig,\r\n lerpSeriesFills,\r\n num,\r\n resolveLegendConfig,\r\n resolveSeriesFills,\r\n text,\r\n type LegendConfig,\r\n type LegendTheme,\r\n} from \"../kit/shared\"\r\n\r\n/**\r\n * One **resolved** slice of a {@link PieChart}: its caption, its magnitude, and\r\n * the paint its wedge and legend swatch take.\r\n *\r\n * This is what the ring and the legend consume, not what a scene authors: the\r\n * chart takes tabular `data` plus two field names and resolves it into these, so\r\n * the parts stay unaware of where a slice's caption, magnitude and paint came\r\n * from.\r\n */\r\nexport interface PieSlice {\r\n /** Legend caption and label text for this slice. */\r\n category: string\r\n /** This slice's magnitude. Its wedge covers `value / (sum of all values)`. */\r\n value: number\r\n /** Wedge and swatch paint. */\r\n fill: Fill\r\n}\r\n\r\n/**\r\n * Resolve tabular rows into slices: one per row, captioned by the `category`\r\n * column, sized by the `value` column, painted from `palette` by row order\r\n * (cycling when the palette is shorter than the data).\r\n *\r\n * Paint comes from the palette rather than from the rows because a pie draws one\r\n * mark per *row* — unlike the line and bar charts, where a colour belongs to a\r\n * series (a whole column) and rides on the series entry instead. Keeping it out\r\n * of the rows is what lets a pie be fed straight from a CSV of plain numbers.\r\n */\r\nexport function resolvePieSlices(\r\n data: DataRecord[],\r\n category: string,\r\n value: string,\r\n palette: Fill[]\r\n): PieSlice[] {\r\n return data.map((row, i) => ({\r\n category: text(row, category),\r\n value: num(row, value),\r\n fill:\r\n palette.length > 0 ? palette[i % palette.length] : (\"white\" as Fill),\r\n }))\r\n}\r\n\r\n/** What a {@link PieChart}'s labels say. */\r\nexport type PieLabelMode =\r\n /**\r\n * The category and its percentage, two lines. Their order flips with the\r\n * placement: a **leader** label reads category over percentage, an **inside**\r\n * label reads the percentage big over a small category caption — inside a\r\n * wedge the reader already knows which slice they're on, so the magnitude is\r\n * the thing being read.\r\n */\r\n | \"both\"\r\n /** The category only. */\r\n | \"category\"\r\n /** The percentage only — the pick when a legend already names the categories. */\r\n | \"percent\"\r\n /** No labels and no leader lines at all. */\r\n | \"none\"\r\n\r\n/**\r\n * Where a slice's label sits.\r\n *\r\n * `\"inside\"` drops the leader line entirely and centres the label in the wedge's\r\n * own band, which costs no gutter — so the ring draws bigger — and costs the eye\r\n * no round trip. `\"leader\"` is the classic treatment, and what a sliver too\r\n * narrow to hold text needs.\r\n */\r\nexport type PieLabelPlacement = \"inside\" | \"leader\" | \"none\"\r\n\r\n/** Degrees → radians. */\r\nexport const DEG = Math.PI / 180\r\n\r\n/** A slice's plotted value: negatives and non-finite values count as 0. */\r\nexport function sliceValue(slice: PieSlice): number {\r\n return slice.value > 0 && Number.isFinite(slice.value) ? slice.value : 0\r\n}\r\n\r\n/** Sum of every slice's plotted value — the denominator every share is taken against. */\r\nexport function sliceTotal(slices: PieSlice[]): number {\r\n let sum = 0\r\n for (const slice of slices) sum += sliceValue(slice)\r\n return sum\r\n}\r\n\r\n/**\r\n * The point at radius `r` and angle `a` on a ring centred on the origin, in the\r\n * node's centred, **y-up** space. Angles run **clockwise from 12 o'clock**:\r\n * `a = 0` points straight up (+y), and increasing `a` sweeps toward +x then −y.\r\n */\r\nexport function ringPoint(r: number, a: number): Vector2 {\r\n return { x: r * Math.sin(a), y: r * Math.cos(a) }\r\n}\r\n\r\n/** One slice's resolved angular span on the ring. */\r\nexport interface PieArc {\r\n /** Index of the slice in the source list. */\r\n index: number\r\n /** Start angle, radians clockwise from 12 o'clock. */\r\n a0: number\r\n /** End angle, radians clockwise from 12 o'clock. */\r\n a1: number\r\n /** Bisector angle — where this slice's label and pop-out point. */\r\n mid: number\r\n /** This slice's share of the total, in `[0, 1]`. */\r\n fraction: number\r\n}\r\n\r\n/**\r\n * Resolve slices into per-slice arcs: each covers exactly its share of the\r\n * circle, walking clockwise from `startDegrees` (0 = 12 o'clock), with the arcs\r\n * abutting. Zero/negative slices are skipped entirely; an all-zero list yields\r\n * no arcs.\r\n *\r\n * Arcs are the *undivided* shares — the gap between neighbours is not carved out\r\n * here, because a fixed angular gap reads as a wedge: wide at the rim, pinched\r\n * shut at the hole. It's applied when the wedge is drawn instead, as a\r\n * constant-width inset off both ends of every arc, so a slice still covers its\r\n * true percentage of the circle.\r\n */\r\nexport function pieArcs(slices: PieSlice[], startDegrees: number): PieArc[] {\r\n const total = sliceTotal(slices)\r\n if (total <= 0) return []\r\n\r\n const arcs: PieArc[] = []\r\n let cursor = startDegrees * DEG\r\n slices.forEach((slice, index) => {\r\n const value = sliceValue(slice)\r\n if (value <= 0) return\r\n const fraction = value / total\r\n const a0 = cursor\r\n const a1 = a0 + fraction * Math.PI * 2\r\n cursor = a1\r\n arcs.push({ index, a0, a1, mid: (a0 + a1) / 2, fraction })\r\n })\r\n return arcs\r\n}\r\n\r\n/** A label competing for vertical space in one of the two label columns. */\r\nexport interface LabelSlot {\r\n /** Where the label would sit if nothing else were in the way. */\r\n ideal: number\r\n /** Where it ends up once the column has been de-conflicted. Written by {@link relaxLabels}. */\r\n y: number\r\n}\r\n\r\n/**\r\n * Spread one column of labels so no two overlap, keeping each as close to its\r\n * `ideal` y as possible and the whole column inside `[bottom, top]`.\r\n *\r\n * Two passes, the standard treatment. Walking top-to-bottom, each label takes\r\n * the highest slot at or below its ideal that still clears the label above it by\r\n * `minGap`; if that pushes the tail past `bottom`, a reverse pass lifts it back\r\n * up. When there are more labels than the column can hold, the reverse pass wins\r\n * and the crowding is shared evenly rather than dumped off the bottom edge.\r\n *\r\n * Mutates each slot's `y` and returns `slots` re-ordered top-to-bottom.\r\n */\r\nexport function relaxLabels<T extends LabelSlot>(\r\n slots: T[],\r\n minGap: number,\r\n top: number,\r\n bottom: number\r\n): T[] {\r\n slots.sort((a, b) => b.ideal - a.ideal)\r\n\r\n let ceiling = top\r\n for (const slot of slots) {\r\n slot.y = Math.min(slot.ideal, ceiling)\r\n ceiling = slot.y - minGap\r\n }\r\n\r\n let floor = bottom\r\n for (let i = slots.length - 1; i >= 0; i--) {\r\n slots[i].y = Math.max(slots[i].y, floor)\r\n floor = slots[i].y + minGap\r\n }\r\n\r\n return slots\r\n}\r\n\r\n// ---- Theme ---------------------------------------------------------------\r\n\r\n/** The ring's geometry and paint — everything about the wedges but their colour. */\r\nexport interface RingConfig {\r\n /**\r\n * Width of the channel between adjacent slices, in px, held constant all the\r\n * way across the band — the same clearance at the hole as at the rim. (An\r\n * angular gap would splay open toward the rim instead.)\r\n */\r\n gap: number\r\n /**\r\n * Radius of the hole punched through the middle. Below 1 it reads as a\r\n * fraction of the ring's outer radius (`0.6` leaves a band 40% of the radius\r\n * thick); 1 and up as an explicit pixel radius; `0` fills the middle in,\r\n * drawing solid pie wedges.\r\n */\r\n holeRadius: number\r\n /** Angle in degrees where the first slice begins, clockwise from 12 o'clock. */\r\n startAngle: number\r\n /**\r\n * How far out a selected slice's rim reaches, as a **multiple of the ring's\r\n * outer radius**. Being relative, the emphasis scales with the chart instead\r\n * of shrinking on a big one and swamping a small one. Clamped to `[1, 1.6]`.\r\n */\r\n selectedGrow: number\r\n /** Optional outline on every wedge. */\r\n stroke: Stroke\r\n /** Optional drop shadow on every wedge. */\r\n shadow: Shadow\r\n}\r\n\r\n/** How the ring's labels are set, and the leader lines that point at them. */\r\nexport interface LabelConfig {\r\n /** The category caption on a label. */\r\n textStyle: TextStyle\r\n /** The percentage on a label. */\r\n percentStyle: TextStyle\r\n /**\r\n * Ink for a label set **inside** its wedge, both lines. Separate because an\r\n * inside label sits on the wedge's own paint rather than on the background, so\r\n * it usually wants the opposite end of the value range from everything else.\r\n */\r\n insideStyle: TextStyle\r\n /** The leader line running from a wedge out to its label. */\r\n leaderStroke: Stroke\r\n}\r\n\r\n/** The summary set in the ring's hole. */\r\nexport interface CenterConfig {\r\n labelStyle: TextStyle\r\n captionStyle: TextStyle\r\n}\r\n\r\n/** A {@link PieChart}'s full, resolved visual theme. */\r\nexport interface PieChartConfig {\r\n /** Wedge and swatch paint, cycled by **row** index (`i % length`). */\r\n sliceFills: Fill[]\r\n ring: RingConfig\r\n label: LabelConfig\r\n center: CenterConfig\r\n legend: LegendConfig\r\n /** How far everything outside the selection fades while any slice is selected. */\r\n spotlight: { dimOpacity: number }\r\n}\r\n\r\n/** Author-facing partial theme — every branch optional and deep-merged. */\r\nexport type PieChartTheme = {\r\n sliceFills?: Fill[]\r\n ring?: Partial<RingConfig>\r\n label?: Partial<LabelConfig>\r\n center?: Partial<CenterConfig>\r\n legend?: LegendTheme\r\n spotlight?: Partial<PieChartConfig[\"spotlight\"]>\r\n}\r\n\r\nconst LABEL_TEXT_STYLE: TextStyle = {\r\n fontSize: 26,\r\n fontWeight: 600,\r\n textAlign: \"center\",\r\n fill: CHART_PALETTE.markerColor,\r\n}\r\n\r\n/** The pie chart's out-of-the-box look. */\r\nexport const DEFAULT_PIE_CHART_THEME: PieChartConfig = {\r\n sliceFills: DEFAULT_SERIES_FILLS,\r\n ring: {\r\n gap: 6,\r\n // A donut rather than a solid pie by default: the hole is the largest\r\n // uninterrupted area the chart owns and the one place a conclusion can be\r\n // set. `0` here would give that away before anyone had asked for it.\r\n holeRadius: 0.6,\r\n startAngle: 0,\r\n selectedGrow: 1.08,\r\n stroke: [],\r\n shadow: [],\r\n },\r\n label: {\r\n textStyle: LABEL_TEXT_STYLE,\r\n percentStyle: { ...LABEL_TEXT_STYLE, fontWeight: 700 },\r\n // Inside labels sit on the wedge's paint, which is a mid-tone from the\r\n // palette — so they lead with the panel colour rather than the axis ink.\r\n insideStyle: { ...LABEL_TEXT_STYLE, fontWeight: 800, fill: \"#15130e\" },\r\n leaderStroke: { weight: 2, fill: CHART_PALETTE.markerColor },\r\n },\r\n center: {\r\n labelStyle: {\r\n fontSize: 46,\r\n fontWeight: 800,\r\n textAlign: \"center\",\r\n fill: CHART_PALETTE.markerColor,\r\n },\r\n captionStyle: {\r\n fontSize: 22,\r\n fontWeight: 500,\r\n textAlign: \"center\",\r\n fill: CHART_PALETTE.markerColor,\r\n },\r\n },\r\n legend: DEFAULT_LEGEND,\r\n spotlight: { dimOpacity: SPOTLIGHT_DIM },\r\n}\r\n\r\nfunction resolveRingConfig(\r\n ring: Partial<RingConfig> | undefined,\r\n previous: RingConfig\r\n): RingConfig {\r\n return {\r\n gap: ring?.gap ?? previous.gap,\r\n holeRadius: ring?.holeRadius ?? previous.holeRadius,\r\n startAngle: ring?.startAngle ?? previous.startAngle,\r\n selectedGrow: ring?.selectedGrow ?? previous.selectedGrow,\r\n stroke: strokeOps.resolve(ring?.stroke ?? previous.stroke),\r\n shadow: shadowOps.resolve(ring?.shadow ?? previous.shadow),\r\n }\r\n}\r\n\r\nfunction lerpRingConfig(\r\n from: RingConfig,\r\n to: RingConfig,\r\n t: number\r\n): RingConfig {\r\n return {\r\n gap: lerpNumber(from.gap, to.gap, t),\r\n holeRadius: lerpNumber(from.holeRadius, to.holeRadius, t),\r\n startAngle: lerpNumber(from.startAngle, to.startAngle, t),\r\n selectedGrow: lerpNumber(from.selectedGrow, to.selectedGrow, t),\r\n stroke: strokeOps.lerp(\r\n from.stroke as StrokeResolved[],\r\n to.stroke as StrokeResolved[],\r\n t\r\n ),\r\n shadow: shadowOps.lerp(\r\n from.shadow as ShadowResolved[],\r\n to.shadow as ShadowResolved[],\r\n t\r\n ),\r\n }\r\n}\r\n\r\nfunction resolveLabelConfig(\r\n label: Partial<LabelConfig> | undefined,\r\n previous: LabelConfig\r\n): LabelConfig {\r\n return {\r\n textStyle: textOps.resolveStyle(label?.textStyle, previous.textStyle),\r\n percentStyle: textOps.resolveStyle(\r\n label?.percentStyle,\r\n previous.percentStyle\r\n ),\r\n insideStyle: textOps.resolveStyle(label?.insideStyle, previous.insideStyle),\r\n leaderStroke: strokeOps.resolve(\r\n label?.leaderStroke ?? previous.leaderStroke\r\n ),\r\n }\r\n}\r\n\r\nfunction lerpLabelConfig(\r\n from: LabelConfig,\r\n to: LabelConfig,\r\n t: number\r\n): LabelConfig {\r\n return {\r\n textStyle: textOps.lerpStyle(from.textStyle, to.textStyle, t),\r\n percentStyle: textOps.lerpStyle(from.percentStyle, to.percentStyle, t),\r\n insideStyle: textOps.lerpStyle(from.insideStyle, to.insideStyle, t),\r\n leaderStroke: strokeOps.lerp(\r\n from.leaderStroke as StrokeResolved[],\r\n to.leaderStroke as StrokeResolved[],\r\n t\r\n ),\r\n }\r\n}\r\n\r\nfunction resolveCenterConfig(\r\n center: Partial<CenterConfig> | undefined,\r\n previous: CenterConfig\r\n): CenterConfig {\r\n return {\r\n labelStyle: textOps.resolveStyle(center?.labelStyle, previous.labelStyle),\r\n captionStyle: textOps.resolveStyle(\r\n center?.captionStyle,\r\n previous.captionStyle\r\n ),\r\n }\r\n}\r\n\r\nfunction lerpCenterConfig(\r\n from: CenterConfig,\r\n to: CenterConfig,\r\n t: number\r\n): CenterConfig {\r\n return {\r\n labelStyle: textOps.lerpStyle(from.labelStyle, to.labelStyle, t),\r\n captionStyle: textOps.lerpStyle(from.captionStyle, to.captionStyle, t),\r\n }\r\n}\r\n\r\n/**\r\n * {@link PieChart}'s `theme` @property mapper: merges the author's partial\r\n * {@link PieChartTheme} onto `previous` (falling back to\r\n * {@link DEFAULT_PIE_CHART_THEME} on first resolution), fully resolving every\r\n * leaf so the tween below always has resolved data to interpolate.\r\n */\r\nexport function resolvePieChartTheme(\r\n theme: PieChartTheme | undefined,\r\n previous?: PieChartConfig\r\n): PieChartConfig {\r\n const base = previous ?? DEFAULT_PIE_CHART_THEME\r\n return {\r\n sliceFills: resolveSeriesFills(theme?.sliceFills, base.sliceFills),\r\n ring: resolveRingConfig(theme?.ring, base.ring),\r\n label: resolveLabelConfig(theme?.label, base.label),\r\n center: resolveCenterConfig(theme?.center, base.center),\r\n legend: resolveLegendConfig(theme?.legend, base.legend),\r\n spotlight: {\r\n dimOpacity: theme?.spotlight?.dimOpacity ?? base.spotlight.dimOpacity,\r\n },\r\n }\r\n}\r\n\r\n/** {@link PieChart}'s `theme` @property tween — leaf by leaf. */\r\nexport function lerpPieChartTheme(\r\n from: PieChartConfig,\r\n to: PieChartConfig,\r\n t: number\r\n): PieChartConfig {\r\n return {\r\n // A palette is a list of independent paints rather than one value, so it\r\n // interpolates pairwise by index; one that grows or shrinks holds the longer\r\n // side's tail, exactly as the cartesian charts' palettes do.\r\n sliceFills: lerpSeriesFills(from.sliceFills, to.sliceFills, t),\r\n ring: lerpRingConfig(from.ring, to.ring, t),\r\n label: lerpLabelConfig(from.label, to.label, t),\r\n center: lerpCenterConfig(from.center, to.center, t),\r\n legend: lerpLegendConfig(from.legend, to.legend, t),\r\n spotlight: {\r\n dimOpacity: lerpNumber(\r\n from.spotlight.dimOpacity,\r\n to.spotlight.dimOpacity,\r\n t\r\n ),\r\n },\r\n }\r\n}\r\n"]}
1
+ {"version":3,"file":"shared.js","sourceRoot":"","sources":["../../src/pie-chart/shared.ts"],"names":[],"mappings":"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","sourcesContent":["/**\r\n * Types and pure helpers specific to the {@link PieChart} family: how rows\r\n * become slices, how slices become arcs on a ring, where the labels around it\r\n * end up, and the theme all of that is drawn from.\r\n *\r\n * The one chart in the family that draws **one mark per row** rather than one\r\n * per column, which is why almost nothing about its geometry is shared: there is\r\n * no scale, no axis and no baseline, only a circle divided by share. What it\r\n * *does* share — the legend, the theme leaf resolvers, the palette, the\r\n * percentage format — comes from `../kit/shared.ts`.\r\n *\r\n * Kept renderer-agnostic (no `Node` imports) so a host's inspector mapping\r\n * can build a partial {@link PieChartTheme} without\r\n * pulling the node classes in.\r\n */\r\n\r\nimport {\r\n lerpNumber,\r\n shadowOps,\r\n strokeOps,\r\n textOps,\r\n type DataRecord,\r\n type Fill,\r\n type Shadow,\r\n type ShadowResolved,\r\n type Stroke,\r\n type StrokeResolved,\r\n type TextStyle,\r\n type Vector2,\r\n} from \"@motionscript/sdk\"\r\n\r\nimport {\r\n CHART_PALETTE,\r\n DEFAULT_LEGEND,\r\n DEFAULT_SERIES_FILLS,\r\n lerpLegendConfig,\r\n lerpSeriesFills,\r\n num,\r\n resolveLegendConfig,\r\n resolveSeriesFills,\r\n text,\r\n type LegendConfig,\r\n type LegendTheme,\r\n} from \"../kit/shared\"\r\n\r\n/**\r\n * One **resolved** slice of a {@link PieChart}: its caption, its magnitude, and\r\n * the paint its wedge and legend swatch take.\r\n *\r\n * This is what the ring and the legend consume, not what a scene authors: the\r\n * chart takes tabular `data` plus two field names and resolves it into these, so\r\n * the parts stay unaware of where a slice's caption, magnitude and paint came\r\n * from.\r\n */\r\nexport interface PieSlice {\r\n /** Legend caption and label text for this slice. */\r\n category: string\r\n /** This slice's magnitude. Its wedge covers `value / (sum of all values)`. */\r\n value: number\r\n /** Wedge and swatch paint. */\r\n fill: Fill\r\n}\r\n\r\n/**\r\n * Resolve tabular rows into slices: one per row, captioned by the `category`\r\n * column, sized by the `value` column, painted from `palette` by row order\r\n * (cycling when the palette is shorter than the data).\r\n *\r\n * Paint comes from the palette rather than from the rows because a pie draws one\r\n * mark per *row* — unlike the line and bar charts, where a colour belongs to a\r\n * series (a whole column) and rides on the series entry instead. Keeping it out\r\n * of the rows is what lets a pie be fed straight from a CSV of plain numbers.\r\n */\r\nexport function resolvePieSlices(\r\n data: DataRecord[],\r\n category: string,\r\n value: string,\r\n palette: Fill[]\r\n): PieSlice[] {\r\n return data.map((row, i) => ({\r\n category: text(row, category),\r\n value: num(row, value),\r\n fill:\r\n palette.length > 0 ? palette[i % palette.length] : (\"white\" as Fill),\r\n }))\r\n}\r\n\r\n/** What a {@link PieChart}'s labels say. */\r\nexport type PieLabelMode =\r\n /**\r\n * The category and its percentage, two lines. Their order flips with the\r\n * placement: a **leader** label reads category over percentage, an **inside**\r\n * label reads the percentage big over a small category caption — inside a\r\n * wedge the reader already knows which slice they're on, so the magnitude is\r\n * the thing being read.\r\n */\r\n | \"both\"\r\n /** The category only. */\r\n | \"category\"\r\n /** The percentage only — the pick when a legend already names the categories. */\r\n | \"percent\"\r\n /** No labels and no leader lines at all. */\r\n | \"none\"\r\n\r\n/**\r\n * Where a slice's label sits.\r\n *\r\n * `\"inside\"` drops the leader line entirely and centres the label in the wedge's\r\n * own band, which costs no gutter — so the ring draws bigger — and costs the eye\r\n * no round trip. `\"leader\"` is the classic treatment, and what a sliver too\r\n * narrow to hold text needs.\r\n */\r\nexport type PieLabelPlacement = \"inside\" | \"leader\" | \"none\"\r\n\r\n/** Degrees → radians. */\r\nexport const DEG = Math.PI / 180\r\n\r\n/** A slice's plotted value: negatives and non-finite values count as 0. */\r\nexport function sliceValue(slice: PieSlice): number {\r\n return slice.value > 0 && Number.isFinite(slice.value) ? slice.value : 0\r\n}\r\n\r\n/** Sum of every slice's plotted value — the denominator every share is taken against. */\r\nexport function sliceTotal(slices: PieSlice[]): number {\r\n let sum = 0\r\n for (const slice of slices) sum += sliceValue(slice)\r\n return sum\r\n}\r\n\r\n/**\r\n * The point at radius `r` and angle `a` on a ring centred on the origin, in the\r\n * node's centred, **y-up** space. Angles run **clockwise from 12 o'clock**:\r\n * `a = 0` points straight up (+y), and increasing `a` sweeps toward +x then −y.\r\n */\r\nexport function ringPoint(r: number, a: number): Vector2 {\r\n return { x: r * Math.sin(a), y: r * Math.cos(a) }\r\n}\r\n\r\n/** One slice's resolved angular span on the ring. */\r\nexport interface PieArc {\r\n /** Index of the slice in the source list. */\r\n index: number\r\n /** Start angle, radians clockwise from 12 o'clock. */\r\n a0: number\r\n /** End angle, radians clockwise from 12 o'clock. */\r\n a1: number\r\n /** Bisector angle — where this slice's label and pop-out point. */\r\n mid: number\r\n /** This slice's share of the total, in `[0, 1]`. */\r\n fraction: number\r\n}\r\n\r\n/**\r\n * Resolve slices into per-slice arcs: each covers exactly its share of the\r\n * circle, walking clockwise from `startDegrees` (0 = 12 o'clock), with the arcs\r\n * abutting. Zero/negative slices are skipped entirely; an all-zero list yields\r\n * no arcs.\r\n *\r\n * Arcs are the *undivided* shares — the gap between neighbours is not carved out\r\n * here, because a fixed angular gap reads as a wedge: wide at the rim, pinched\r\n * shut at the hole. It's applied when the wedge is drawn instead, as a\r\n * constant-width inset off both ends of every arc, so a slice still covers its\r\n * true percentage of the circle.\r\n */\r\nexport function pieArcs(slices: PieSlice[], startDegrees: number): PieArc[] {\r\n const total = sliceTotal(slices)\r\n if (total <= 0) return []\r\n\r\n const arcs: PieArc[] = []\r\n let cursor = startDegrees * DEG\r\n slices.forEach((slice, index) => {\r\n const value = sliceValue(slice)\r\n if (value <= 0) return\r\n const fraction = value / total\r\n const a0 = cursor\r\n const a1 = a0 + fraction * Math.PI * 2\r\n cursor = a1\r\n arcs.push({ index, a0, a1, mid: (a0 + a1) / 2, fraction })\r\n })\r\n return arcs\r\n}\r\n\r\n/** A label competing for vertical space in one of the two label columns. */\r\nexport interface LabelSlot {\r\n /** Where the label would sit if nothing else were in the way. */\r\n ideal: number\r\n /** Where it ends up once the column has been de-conflicted. Written by {@link relaxLabels}. */\r\n y: number\r\n}\r\n\r\n/**\r\n * Spread one column of labels so no two overlap, keeping each as close to its\r\n * `ideal` y as possible and the whole column inside `[bottom, top]`.\r\n *\r\n * Two passes, the standard treatment. Walking top-to-bottom, each label takes\r\n * the highest slot at or below its ideal that still clears the label above it by\r\n * `minGap`; if that pushes the tail past `bottom`, a reverse pass lifts it back\r\n * up. When there are more labels than the column can hold, the reverse pass wins\r\n * and the crowding is shared evenly rather than dumped off the bottom edge.\r\n *\r\n * Mutates each slot's `y` and returns `slots` re-ordered top-to-bottom.\r\n */\r\nexport function relaxLabels<T extends LabelSlot>(\r\n slots: T[],\r\n minGap: number,\r\n top: number,\r\n bottom: number\r\n): T[] {\r\n slots.sort((a, b) => b.ideal - a.ideal)\r\n\r\n let ceiling = top\r\n for (const slot of slots) {\r\n slot.y = Math.min(slot.ideal, ceiling)\r\n ceiling = slot.y - minGap\r\n }\r\n\r\n let floor = bottom\r\n for (let i = slots.length - 1; i >= 0; i--) {\r\n slots[i].y = Math.max(slots[i].y, floor)\r\n floor = slots[i].y + minGap\r\n }\r\n\r\n return slots\r\n}\r\n\r\n// ---- Theme ---------------------------------------------------------------\r\n\r\n/** The ring's geometry and paint — everything about the wedges but their colour. */\r\nexport interface RingConfig {\r\n /**\r\n * Width of the channel between adjacent slices, in px, held constant all the\r\n * way across the band — the same clearance at the hole as at the rim. (An\r\n * angular gap would splay open toward the rim instead.)\r\n */\r\n gap: number\r\n /**\r\n * Radius of the hole punched through the middle. Below 1 it reads as a\r\n * fraction of the ring's outer radius (`0.6` leaves a band 40% of the radius\r\n * thick); 1 and up as an explicit pixel radius; `0` fills the middle in,\r\n * drawing solid pie wedges.\r\n */\r\n holeRadius: number\r\n /** Angle in degrees where the first slice begins, clockwise from 12 o'clock. */\r\n startAngle: number\r\n /**\r\n * How far out a selected slice's rim reaches, as a **multiple of the ring's\r\n * outer radius**. Being relative, the emphasis scales with the chart instead\r\n * of shrinking on a big one and swamping a small one. Clamped to `[1, 1.6]`.\r\n */\r\n selectedGrow: number\r\n /** Optional outline on every wedge. */\r\n stroke: Stroke\r\n /** Optional drop shadow on every wedge. */\r\n shadow: Shadow\r\n}\r\n\r\n/** How the ring's labels are set, and the leader lines that point at them. */\r\nexport interface LabelConfig {\r\n /** The category caption on a label. */\r\n textStyle: TextStyle\r\n /** The percentage on a label. */\r\n percentStyle: TextStyle\r\n /**\r\n * Ink for a label set **inside** its wedge, both lines. Separate because an\r\n * inside label sits on the wedge's own paint rather than on the background, so\r\n * it usually wants the opposite end of the value range from everything else.\r\n */\r\n insideStyle: TextStyle\r\n /** The leader line running from a wedge out to its label. */\r\n leaderStroke: Stroke\r\n}\r\n\r\n/** The summary set in the ring's hole. */\r\nexport interface CenterConfig {\r\n labelStyle: TextStyle\r\n captionStyle: TextStyle\r\n}\r\n\r\n/** A {@link PieChart}'s full, resolved visual theme. */\r\nexport interface PieChartConfig {\r\n /** Wedge and swatch paint, cycled by **row** index (`i % length`). */\r\n sliceFills: Fill[]\r\n ring: RingConfig\r\n label: LabelConfig\r\n center: CenterConfig\r\n legend: LegendConfig\r\n /** How far everything outside the selection fades while any slice is selected. */\r\n spotlight: { dimOpacity: number }\r\n}\r\n\r\n/** Author-facing partial theme — every branch optional and deep-merged. */\r\nexport type PieChartTheme = {\r\n sliceFills?: Fill[]\r\n ring?: Partial<RingConfig>\r\n label?: Partial<LabelConfig>\r\n center?: Partial<CenterConfig>\r\n legend?: LegendTheme\r\n spotlight?: Partial<PieChartConfig[\"spotlight\"]>\r\n}\r\n\r\nconst LABEL_TEXT_STYLE: TextStyle = {\r\n fontSize: 26,\r\n fontWeight: 600,\r\n textAlign: \"center\",\r\n fill: CHART_PALETTE.markerColor,\r\n}\r\n\r\n/**\r\n * Opacity the other slices fade to under a selection. Fainter than the line and\r\n * bar charts' `SPOTLIGHT_DIM`: a ring's slices touch, so at that a selected\r\n * slice barely separates from its neighbours.\r\n */\r\nexport const PIE_SPOTLIGHT_DIM = 0.1\r\n\r\n/** The pie chart's out-of-the-box look. */\r\nexport const DEFAULT_PIE_CHART_THEME: PieChartConfig = {\r\n sliceFills: DEFAULT_SERIES_FILLS,\r\n ring: {\r\n gap: 6,\r\n // A donut rather than a solid pie by default: the hole is the largest\r\n // uninterrupted area the chart owns and the one place a conclusion can be\r\n // set. `0` here would give that away before anyone had asked for it.\r\n holeRadius: 0.6,\r\n startAngle: 0,\r\n selectedGrow: 1.08,\r\n stroke: [],\r\n shadow: [],\r\n },\r\n label: {\r\n textStyle: LABEL_TEXT_STYLE,\r\n percentStyle: { ...LABEL_TEXT_STYLE, fontWeight: 700 },\r\n // Inside labels sit on the wedge's paint, which is a mid-tone from the\r\n // palette — so they lead with the panel colour rather than the axis ink.\r\n insideStyle: { ...LABEL_TEXT_STYLE, fontWeight: 800, fill: \"#15130e\" },\r\n leaderStroke: { weight: 2, fill: CHART_PALETTE.markerColor },\r\n },\r\n center: {\r\n labelStyle: {\r\n fontSize: 46,\r\n fontWeight: 800,\r\n textAlign: \"center\",\r\n fill: CHART_PALETTE.markerColor,\r\n },\r\n captionStyle: {\r\n fontSize: 22,\r\n fontWeight: 500,\r\n textAlign: \"center\",\r\n fill: CHART_PALETTE.markerColor,\r\n },\r\n },\r\n legend: DEFAULT_LEGEND,\r\n spotlight: { dimOpacity: PIE_SPOTLIGHT_DIM },\r\n}\r\n\r\nfunction resolveRingConfig(\r\n ring: Partial<RingConfig> | undefined,\r\n previous: RingConfig\r\n): RingConfig {\r\n return {\r\n gap: ring?.gap ?? previous.gap,\r\n holeRadius: ring?.holeRadius ?? previous.holeRadius,\r\n startAngle: ring?.startAngle ?? previous.startAngle,\r\n selectedGrow: ring?.selectedGrow ?? previous.selectedGrow,\r\n stroke: strokeOps.resolve(ring?.stroke ?? previous.stroke),\r\n shadow: shadowOps.resolve(ring?.shadow ?? previous.shadow),\r\n }\r\n}\r\n\r\nfunction lerpRingConfig(\r\n from: RingConfig,\r\n to: RingConfig,\r\n t: number\r\n): RingConfig {\r\n return {\r\n gap: lerpNumber(from.gap, to.gap, t),\r\n holeRadius: lerpNumber(from.holeRadius, to.holeRadius, t),\r\n startAngle: lerpNumber(from.startAngle, to.startAngle, t),\r\n selectedGrow: lerpNumber(from.selectedGrow, to.selectedGrow, t),\r\n stroke: strokeOps.lerp(\r\n from.stroke as StrokeResolved[],\r\n to.stroke as StrokeResolved[],\r\n t\r\n ),\r\n shadow: shadowOps.lerp(\r\n from.shadow as ShadowResolved[],\r\n to.shadow as ShadowResolved[],\r\n t\r\n ),\r\n }\r\n}\r\n\r\nfunction resolveLabelConfig(\r\n label: Partial<LabelConfig> | undefined,\r\n previous: LabelConfig\r\n): LabelConfig {\r\n return {\r\n textStyle: textOps.resolveStyle(label?.textStyle, previous.textStyle),\r\n percentStyle: textOps.resolveStyle(\r\n label?.percentStyle,\r\n previous.percentStyle\r\n ),\r\n insideStyle: textOps.resolveStyle(label?.insideStyle, previous.insideStyle),\r\n leaderStroke: strokeOps.resolve(\r\n label?.leaderStroke ?? previous.leaderStroke\r\n ),\r\n }\r\n}\r\n\r\nfunction lerpLabelConfig(\r\n from: LabelConfig,\r\n to: LabelConfig,\r\n t: number\r\n): LabelConfig {\r\n return {\r\n textStyle: textOps.lerpStyle(from.textStyle, to.textStyle, t),\r\n percentStyle: textOps.lerpStyle(from.percentStyle, to.percentStyle, t),\r\n insideStyle: textOps.lerpStyle(from.insideStyle, to.insideStyle, t),\r\n leaderStroke: strokeOps.lerp(\r\n from.leaderStroke as StrokeResolved[],\r\n to.leaderStroke as StrokeResolved[],\r\n t\r\n ),\r\n }\r\n}\r\n\r\nfunction resolveCenterConfig(\r\n center: Partial<CenterConfig> | undefined,\r\n previous: CenterConfig\r\n): CenterConfig {\r\n return {\r\n labelStyle: textOps.resolveStyle(center?.labelStyle, previous.labelStyle),\r\n captionStyle: textOps.resolveStyle(\r\n center?.captionStyle,\r\n previous.captionStyle\r\n ),\r\n }\r\n}\r\n\r\nfunction lerpCenterConfig(\r\n from: CenterConfig,\r\n to: CenterConfig,\r\n t: number\r\n): CenterConfig {\r\n return {\r\n labelStyle: textOps.lerpStyle(from.labelStyle, to.labelStyle, t),\r\n captionStyle: textOps.lerpStyle(from.captionStyle, to.captionStyle, t),\r\n }\r\n}\r\n\r\n/**\r\n * {@link PieChart}'s `theme` property mapper: merges the author's partial\r\n * {@link PieChartTheme} onto `previous` (falling back to\r\n * {@link DEFAULT_PIE_CHART_THEME} on first resolution), fully resolving every\r\n * leaf so the tween below always has resolved data to interpolate.\r\n */\r\nexport function resolvePieChartTheme(\r\n theme: PieChartTheme | undefined,\r\n previous?: PieChartConfig\r\n): PieChartConfig {\r\n const base = previous ?? DEFAULT_PIE_CHART_THEME\r\n return {\r\n sliceFills: resolveSeriesFills(theme?.sliceFills, base.sliceFills),\r\n ring: resolveRingConfig(theme?.ring, base.ring),\r\n label: resolveLabelConfig(theme?.label, base.label),\r\n center: resolveCenterConfig(theme?.center, base.center),\r\n legend: resolveLegendConfig(theme?.legend, base.legend),\r\n spotlight: {\r\n dimOpacity: theme?.spotlight?.dimOpacity ?? base.spotlight.dimOpacity,\r\n },\r\n }\r\n}\r\n\r\n/** {@link PieChart}'s `theme` property tween — leaf by leaf. */\r\nexport function lerpPieChartTheme(\r\n from: PieChartConfig,\r\n to: PieChartConfig,\r\n t: number\r\n): PieChartConfig {\r\n return {\r\n // A palette is a list of independent paints rather than one value, so it\r\n // interpolates pairwise by index; one that grows or shrinks holds the longer\r\n // side's tail, exactly as the cartesian charts' palettes do.\r\n sliceFills: lerpSeriesFills(from.sliceFills, to.sliceFills, t),\r\n ring: lerpRingConfig(from.ring, to.ring, t),\r\n label: lerpLabelConfig(from.label, to.label, t),\r\n center: lerpCenterConfig(from.center, to.center, t),\r\n legend: lerpLegendConfig(from.legend, to.legend, t),\r\n spotlight: {\r\n dimOpacity: lerpNumber(\r\n from.spotlight.dimOpacity,\r\n to.spotlight.dimOpacity,\r\n t\r\n ),\r\n },\r\n }\r\n}\r\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@motionscript/charts",
3
- "version": "0.1.0-alpha.6",
3
+ "version": "0.1.0-alpha.8",
4
4
  "type": "module",
5
5
  "description": "Animated data charts for Motion Script: a line chart, a bar chart and a pie chart sharing one axis, legend and theme kit.",
6
6
  "repository": {
@@ -48,8 +48,9 @@
48
48
  "animation"
49
49
  ],
50
50
  "license": "Apache-2.0",
51
+ "author": "Motionscript",
51
52
  "peerDependencies": {
52
- "@motionscript/sdk": "^0.1.0-alpha.6"
53
+ "@motionscript/sdk": "^0.1.0-alpha.8"
53
54
  },
54
55
  "devDependencies": {
55
56
  "@types/node": "^25.6.2",
@@ -60,8 +61,8 @@
60
61
  "typescript": "^6.0.3",
61
62
  "typescript-eslint": "^8.48.0",
62
63
  "vitest": "^4.1.5",
63
- "@motionscript/core": "0.1.0-alpha.6",
64
- "@motionscript/sdk": "0.1.0-alpha.6"
64
+ "@motionscript/sdk": "0.1.0-alpha.8",
65
+ "@motionscript/core": "0.1.0-alpha.8"
65
66
  },
66
67
  "scripts": {
67
68
  "build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json --resolve-full-paths && node ../../../scripts/build-browser.mjs",
@@ -128,9 +128,17 @@ function drawnValue(s: BarChartValues, domain: AxisDomainConfig): AxisDomainConf
128
128
  return { ...domain, range, ticks: domain.ticks ?? markerValues(range, DEFAULT_MARKERS - 1) }
129
129
  }
130
130
 
131
+ /** The last domain worked out from each data array, and what else it was read from. */
132
+ const domainsDrawn = new WeakMap<DataRecord[], Omit<BarChartValues, "data"> & { readonly domain: BarChartDomain }>()
133
+
131
134
  /** The value axis as a bar chart draws it now, an auto range or tick row read back as itself. */
132
135
  export function barChartDomain(s: BarChartValues): BarChartDomain {
133
- return settleAxisDomain(s.valueAxis, (d) => drawnValue(s, d)) as BarChartDomain
136
+ const { data, series, stacked, valueAxis } = s
137
+ const kept = domainsDrawn.get(data)
138
+ if (kept && kept.series === series && kept.stacked === stacked && kept.valueAxis === valueAxis) return kept.domain
139
+ const domain = settleAxisDomain(valueAxis, (d) => drawnValue(s, d)) as BarChartDomain
140
+ domainsDrawn.set(data, { series, stacked, valueAxis, domain })
141
+ return domain
134
142
  }
135
143
 
136
144
  /**
@@ -334,6 +342,7 @@ export const BarChart = component2d({
334
342
  weights: (state) => state.series.map((s) => ((s.enabled ?? true) ? 1 : 0)),
335
343
  showing: (state) => state.series.map((s) => s.enabled ?? true),
336
344
  },
345
+ allowedChildren: [],
337
346
  children: (state) => {
338
347
  const theme = () => state.theme
339
348
  const domain = () => barChartDomain(state)
@@ -426,10 +435,7 @@ export const BarChart = component2d({
426
435
  height: 0,
427
436
  }) as unknown as Node2D
428
437
 
429
- return asParts([
430
- ...(state.children as unknown as Node2D[]),
431
- cartesianFrame({ axis: vAxis as unknown as Node2D, plot, gutter, below }),
432
- ])
438
+ return asParts([cartesianFrame({ axis: vAxis as unknown as Node2D, plot, gutter, below })])
433
439
  },
434
440
  commands: {
435
441
  /**
@@ -441,6 +447,8 @@ export const BarChart = component2d({
441
447
  */
442
448
  enter: {
443
449
  args: {},
450
+ // Held from the chart's arrival, so it never shows finished ahead of its draw.
451
+ entrance: true,
444
452
  easing: easeOut(),
445
453
  run: (timeline, _state, children) => {
446
454
  const parts = partsOf(children)
@@ -263,16 +263,15 @@ export const ChartBars = component2d({
263
263
  y: (ends.base + ends.end) / 2,
264
264
  width: bar.width,
265
265
  height,
266
- // This node's space is y-up, but the renderer's corner-radius keys are
267
- // labelled in canvas y-down space — so a column reaching *up* rounds
268
- // through the `bottom*` keys, and one reaching down through the `top*`
269
- // ones. Only the far end rounds, which is what makes a stack read as a
270
- // single capped column.
266
+ // Corner keys are visual (the renderer keeps them in y-up author space),
267
+ // so a column reaching *up* rounds its `top*` corners and one reaching
268
+ // down its `bottom*` ones. Only the far end rounds, which is what makes
269
+ // a stack read as a single capped column.
271
270
  cornerRadius:
272
271
  radius > 0
273
272
  ? ends.end >= ends.base
274
- ? { bottomLeft: radius, bottomRight: radius, topLeft: 0, topRight: 0 }
275
- : { topLeft: radius, topRight: radius, bottomLeft: 0, bottomRight: 0 }
273
+ ? { topLeft: radius, topRight: radius, bottomLeft: 0, bottomRight: 0 }
274
+ : { bottomLeft: radius, bottomRight: radius, topLeft: 0, topRight: 0 }
276
275
  : 0,
277
276
  })
278
277
  rect.shadow(state.barShadow)
@@ -6,7 +6,6 @@ import {
6
6
  type NodeOf,
7
7
  // Named so this component's emitted type reaches `Node2D` through the sdk, not core.
8
8
  type Node2D as _Node2D,
9
- type Node2DProps,
10
9
  type Stroke,
11
10
  type StrokeResolved,
12
11
  type Vector2,
@@ -23,40 +22,6 @@ import {
23
22
  type RegionBox,
24
23
  } from "./shared"
25
24
 
26
- export interface ChartLineProps extends Node2DProps {
27
- /**
28
- * The series polyline, already mapped into this node's local pixel space
29
- * (origin at centre, y-up) by the parent {@link LineChart}. Ordered along x.
30
- */
31
- points: Vector2[]
32
- /** Line colour + weight (+ optional dash/cap/join) — the per-series stroke. */
33
- stroke: Stroke
34
- /**
35
- * Reveal of the line in `[0, 1]`: 0 hides it, 1 traces it fully from its left
36
- * end to its right.
37
- *
38
- * Defaults to **1**, unlike the standalone chart this was ported from: in an
39
- * editor a node's resting state is what the paused editor canvas paints, so a
40
- * chart that only appeared once a command had run would edit blind. {@link
41
- * ChartLine.enter} drives it from 0 regardless of where it rests, so the
42
- * animation is unchanged.
43
- */
44
- progress: number
45
- /**
46
- * Whether this line is drawn at all. `false` hides it outright — the switch
47
- * the chart flips alongside its legend's per-series toggle.
48
- */
49
- enabled: boolean
50
- /**
51
- * A slice of the plot to spotlight, in this node's own pixel space. When set
52
- * to a real box, the stretches of the line *outside* it drop to
53
- * {@link ChartLineProps.dimOpacity} while the stretches inside stay full.
54
- */
55
- spotlight: RegionBox
56
- /** Opacity the out-of-region stretches fade to. */
57
- dimOpacity: number
58
- }
59
-
60
25
  /**
61
26
  * The prefix of `pts` revealed at fraction `p ∈ [0, 1]`, measuring the reveal
62
27
  * by *cumulative arc length* so the line traces at a steady pace regardless of
@@ -99,17 +64,27 @@ function tracePoints(pts: Vector2[], p: number): Vector2[] {
99
64
  * through pre-mapped pixel `points`, drawn as one {@link Graphics2D} command list.
100
65
  * Sized and positioned by the parent, which shares the plot's coordinate frame.
101
66
  *
102
- * Two signals drive it: {@link ChartLineProps.progress} traces the line in
67
+ * Two signals drive it: `progress` traces the line in
103
68
  * (animated by `enter`) and `enabled` shows or hides it wholesale.
104
69
  */
105
70
  export const ChartLine = component2d({
106
71
  key: "chartLine",
107
72
  state: {
73
+ /** The series polyline, already mapped into this node's local pixel space (origin at centre, y-up) by the parent {@link LineChart}. Ordered along x. */
108
74
  points: fresh<Vector2[]>(() => []),
75
+ /** Line colour + weight (+ optional dash/cap/join) — the per-series stroke. */
109
76
  stroke: Types.custom<Stroke, StrokeResolved[]>([], { resolve: strokeOps.resolve, lerp: strokeOps.lerp }),
77
+ /**
78
+ * Reveal of the line in `[0, 1]`: 0 hides it, 1 traces it fully from its left
79
+ * end to its right. Defaults to 1, so the paused editor canvas paints the line;
80
+ * `enter` drives it from 0 regardless of where it rests.
81
+ */
110
82
  progress: 1,
83
+ /** Whether this line is drawn at all: the switch the chart flips alongside its legend's per-series toggle. */
111
84
  enabled: true,
85
+ /** A slice of the plot to spotlight, in this node's own pixel space: outside it the line drops to `dimOpacity`. */
112
86
  spotlight: fresh<RegionBox>(() => EMPTY_REGION_BOX),
87
+ /** Opacity the out-of-region stretches fade to. */
113
88
  dimOpacity: SPOTLIGHT_DIM,
114
89
  },
115
90
  load: (loader, state) => loadPaints(loader, { strokes: [state.stroke] }),
@@ -9,7 +9,6 @@ import {
9
9
  type NodeOf,
10
10
  // Named so this component's emitted type reaches `Node2D` through the sdk, not core.
11
11
  type Node2D as _Node2D,
12
- type Node2DProps,
13
12
  type Stroke,
14
13
  type StrokeResolved,
15
14
  } from "@motionscript/sdk"
@@ -23,29 +22,10 @@ import {
23
22
  type RegionBox,
24
23
  } from "./shared"
25
24
 
26
- export interface ChartRegionProps extends Node2DProps {
27
- /**
28
- * The highlighted slice, already resolved into this node's local pixel space
29
- * (origin at centre, y-up) by the parent {@link LineChart}. A degenerate box
30
- * draws nothing.
31
- */
32
- box: RegionBox
33
- /** Dashed rule marking each *bounded* edge of the box. */
34
- stroke: Stroke
35
- /** Wash filling the box, under the series lines. */
36
- fill: Fill
37
- /**
38
- * Reveal of the annotation in `[0, 1]`: 0 hides it, 1 draws it fully. Driven
39
- * by the chart's own `regionReveal`, so the rules and the series' dimming move
40
- * as one.
41
- */
42
- reveal: number
43
- }
44
-
45
25
  /**
46
26
  * The **region call-out** of a {@link LineChart}: a washed rectangle over the
47
27
  * plot with a dashed rule down each edge the author actually bounded. Sized and
48
- * positioned entirely from the pre-mapped {@link ChartRegionProps.box} rather
28
+ * positioned entirely from the pre-mapped `box` rather
49
29
  * than its own box — the region already carries the plot geometry.
50
30
  *
51
31
  * It draws only the *marking*; fading the series outside the region is
@@ -59,9 +39,13 @@ export interface ChartRegionProps extends Node2DProps {
59
39
  export const ChartRegion = component2d({
60
40
  key: "chartRegion",
61
41
  state: {
42
+ /** The highlighted slice, already resolved into this node's local pixel space (origin at centre, y-up) by the parent {@link LineChart}. A degenerate box draws nothing. */
62
43
  box: fresh<RegionBox>(() => EMPTY_REGION_BOX),
44
+ /** Dashed rule marking each *bounded* edge of the box. */
63
45
  stroke: Types.custom<Stroke, StrokeResolved[]>(DEFAULT_CHART_THEME.region.stroke, { resolve: strokeOps.resolve, lerp: strokeOps.lerp }),
46
+ /** Wash filling the box, under the series lines. */
64
47
  fill: Types.custom<Fill, FillResolved[]>(DEFAULT_CHART_THEME.region.fill, { resolve: fillOps.resolve, lerp: fillOps.lerp }),
48
+ /** Reveal of the annotation in `[0, 1]`, driven by the chart's own `regionReveal` so the rules and the series' dimming move as one. */
65
49
  reveal: 0,
66
50
  },
67
51
  load: (loader, state) => loadPaints(loader, { fills: [state.fill], strokes: [state.stroke] }),
@@ -7,7 +7,6 @@ import {
7
7
  textOps,
8
8
  type NodeOf,
9
9
  type Node2D,
10
- type Node2DProps,
11
10
  } from "@motionscript/sdk";
12
11
 
13
12
  import { asParts, fresh } from "../kit/entries";
@@ -20,32 +19,9 @@ import {
20
19
  } from "../kit/shared";
21
20
  import { markerCaption } from "../kit/vertical-axis";
22
21
 
23
- export interface HorizontalAxisProps extends Node2DProps {
24
- /** `[min, max]` of the horizontal scale the markers are positioned within. */
25
- range: Range;
26
- /** Tick values (in data units) to draw along the axis. */
27
- markers: number[];
28
- /**
29
- * How each marker is captioned, one per entry of `markers` and in its order.
30
- * Empty (the default), or short, captions a marker with its bare numeric
31
- * reading; a chart whose x column names months or categories writes those.
32
- */
33
- captions: string[];
34
- /** Marker/caption text style, grid stroke, and gutters — see {@link AxisConfig}. */
35
- axis: AxisConfig;
36
- /**
37
- * Caption drawn centred in the gutter beneath the marker row. Empty string
38
- * (the default) hides it.
39
- */
40
- label: string;
41
- }
42
-
43
22
  /** How far a marker starts above its slot when it enters, in px. */
44
23
  const SLIDE = 24;
45
24
 
46
- /** How a tick value is captioned — see {@link HorizontalAxisProps.captions}. */
47
- export type MarkerLabel = (value: number) => string;
48
-
49
25
  /**
50
26
  * The horizontal axis of a {@link LineChart}: a **row of {@link Text} nodes**,
51
27
  * one per marker value, running left-to-right. Pure layout — the mirror of the
@@ -56,7 +32,7 @@ export type MarkerLabel = (value: number) => string;
56
32
  * `2026-03` sits three months along, not three slots along. That is what keeps
57
33
  * it distinct from a bar chart's bottom axis, which centres a caption on each
58
34
  * slot and has no scale at all — the two only look alike. What the values are
59
- * captioned *as* is the chart's to decide; see {@link HorizontalAxisProps.captions}.
35
+ * captioned *as* is the chart's to decide, in `captions`.
60
36
  *
61
37
  * `enter` staggers the markers in **left-to-right**: each starts lifted a
62
38
  * little above its slot and transparent, then fades and slides down into place.
@@ -64,10 +40,19 @@ export type MarkerLabel = (value: number) => string;
64
40
  export const HorizontalAxis = component2d({
65
41
  key: "horizontalAxis",
66
42
  state: {
43
+ /** `[min, max]` of the horizontal scale the markers are positioned within. */
67
44
  range: fresh<Range>(() => [0, 1]),
45
+ /** Tick values (in data units) to draw along the axis. */
68
46
  markers: fresh<number[]>(() => []),
47
+ /**
48
+ * How each marker is captioned, one per entry of `markers` and in its order.
49
+ * Empty (the default), or short, captions a marker with its bare numeric
50
+ * reading; a chart whose x column names months or categories writes those.
51
+ */
69
52
  captions: fresh<string[]>(() => []),
53
+ /** Marker/caption text style, grid stroke, and gutters — see {@link AxisConfig}. */
70
54
  axis: Types.custom<AxisConfig>(DEFAULT_X_AXIS),
55
+ /** Caption drawn centred in the gutter beneath the marker row; empty (the default) hides it. */
71
56
  label: "",
72
57
  arrivals: arrivalsEntry(),
73
58
  },
@@ -169,12 +169,25 @@ function drawnY(s: LineChartValues, domain: AxisDomainConfig): AxisDomainConfig
169
169
  return { ...domain, range, ticks: domain.ticks ?? markerValues(range, DEFAULT_MARKERS - 1) }
170
170
  }
171
171
 
172
+ /** The last domains worked out from each data array, and what else they were read from. */
173
+ const domainsDrawn = new WeakMap<
174
+ DataRecord[],
175
+ Pick<LineChartValues, "xField" | "xAxis" | "yAxis" | "series"> & { readonly domains: LineChartDomains }
176
+ >()
177
+
172
178
  /** Both axes as a line chart draws them now, an auto range or tick row read back as itself. */
173
179
  export function lineChartDomains(s: LineChartValues): LineChartDomains {
174
- return {
175
- x: settleAxisDomain(s.xAxis, (d) => drawnX(s, d)) as LineChartDomains["x"],
176
- y: settleAxisDomain(s.yAxis, (d) => drawnY(s, d)) as LineChartDomains["y"],
180
+ const { data, xField, xAxis, yAxis, series } = s
181
+ const kept = domainsDrawn.get(data)
182
+ if (kept && kept.xField === xField && kept.xAxis === xAxis && kept.yAxis === yAxis && kept.series === series) {
183
+ return kept.domains
184
+ }
185
+ const domains = {
186
+ x: settleAxisDomain(xAxis, (d) => drawnX(s, d)) as LineChartDomains["x"],
187
+ y: settleAxisDomain(yAxis, (d) => drawnY(s, d)) as LineChartDomains["y"],
177
188
  }
189
+ domainsDrawn.set(data, { xField, xAxis, yAxis, series, domains })
190
+ return domains
178
191
  }
179
192
 
180
193
  /**
@@ -333,6 +346,7 @@ export const LineChart = component2d({
333
346
  },
334
347
  // Its parts fill it; a node made of them would hug them.
335
348
  defaults: { width: "fill", height: "fill" },
349
+ allowedChildren: [],
336
350
  children: (state) => {
337
351
  const theme = () => state.theme
338
352
  const domains = () => lineChartDomains(state)
@@ -440,10 +454,7 @@ export const LineChart = component2d({
440
454
  height: 0,
441
455
  }) as unknown as Node2D
442
456
 
443
- return asParts([
444
- ...(state.children as unknown as Node2D[]),
445
- cartesianFrame({ axis: vAxis as unknown as Node2D, plot, gutter, below }),
446
- ])
457
+ return asParts([cartesianFrame({ axis: vAxis as unknown as Node2D, plot, gutter, below })])
447
458
  },
448
459
  commands: {
449
460
  /**
@@ -457,6 +468,8 @@ export const LineChart = component2d({
457
468
  */
458
469
  enter: {
459
470
  args: {},
471
+ // Held from the chart's arrival, so it never shows finished ahead of its draw.
472
+ entrance: true,
460
473
  easing: easeOut(),
461
474
  run: (timeline, state, children) => {
462
475
  const parts = partsOf(children)
@@ -304,15 +304,30 @@ function labelBySample(
304
304
  return i >= 0 ? captions[i] : fallback(value)
305
305
  }
306
306
 
307
+ /** Scales already resolved, by data array, then by field and pattern. */
308
+ const scales = new WeakMap<DataRecord[], Map<string, XScale>>()
309
+
307
310
  /**
308
311
  * How `field` positions the rows: the number it holds, the instant it names, or
309
312
  * failing both, the row's own place in the file.
310
313
  *
311
- * Resolved from the data on every read rather than cached on the node: it is
312
- * cheap (one pass to classify, one to position), and a cache keyed on a `data`
313
- * array that a rebuild replaces on every edit would miss every time anyway.
314
+ * Kept per `data` array: a chart's bound props ask a dozen times a frame, and
315
+ * parsing every row's instant each time was most of a frame.
314
316
  */
315
317
  export function resolveXScale(
318
+ data: DataRecord[],
319
+ field: string,
320
+ pattern?: string
321
+ ): XScale {
322
+ let byKey = scales.get(data)
323
+ if (!byKey) scales.set(data, (byKey = new Map()))
324
+ const key = JSON.stringify([field, pattern ?? null])
325
+ let scale = byKey.get(key)
326
+ if (!scale) byKey.set(key, (scale = xScaleOf(data, field, pattern)))
327
+ return scale
328
+ }
329
+
330
+ function xScaleOf(
316
331
  data: DataRecord[],
317
332
  field: string,
318
333
  /**
@@ -0,0 +1 @@
1
+ {"version":"4.1.9","results":[[":document.test.ts",{"duration":547.3535000000002,"failed":false}],[":pie-chart/pie-ring.test.ts",{"duration":115.20639999999958,"failed":false}],[":bar-chart/bar-chart.test.ts",{"duration":337.1051000000002,"failed":false}],[":line-chart/line-chart.test.ts",{"duration":256.0781999999999,"failed":false}],[":kit/theme.test.ts",{"duration":23.301500000000033,"failed":false}],[":pie-chart/pie-chart.test.ts",{"duration":243.7885000000001,"failed":false}],[":kit/chart-legend.test.ts",{"duration":107.26519999999982,"failed":false}],[":line-chart/x-scale.test.ts",{"duration":6.663699999999608,"failed":false}],[":kit/shared.test.ts",{"duration":8.266599999999926,"failed":false}],[":nodes.test.ts",{"duration":34.65560000000005,"failed":false}],[":line-chart/chart-line.test.ts",{"duration":25.551500000000033,"failed":false}],[":kit/vertical-axis.test.ts",{"duration":57.74340000000029,"failed":false}],[":line-chart/shared.test.ts",{"duration":8.284700000000157,"failed":false}],[":line-chart/horizontal-axis.test.ts",{"duration":45.18509999999969,"failed":false}],[":bar-chart/category-axis.test.ts",{"duration":43.834299999999985,"failed":false}],[":kit/chart-canvas.test.ts",{"duration":14.003599999999892,"failed":false}],[":bar-chart/shared.test.ts",{"duration":6.999300000000062,"failed":false}],[":pie-chart/shared.test.ts",{"duration":5.208399999999983,"failed":false}],[":kit/declare-paints.test.ts",{"duration":11.566000000000031,"failed":false}],[":tests/leaf.test.ts",{"duration":15.790100000000052,"failed":false}]]}
@@ -179,6 +179,7 @@ export const PieChart = component2d({
179
179
  height: "fill",
180
180
  pops: (state) => slicesOf(state).map(() => 0),
181
181
  },
182
+ allowedChildren: [],
182
183
  // The slice *structure* (one label and one legend row per row) is read once;
183
184
  // the slices themselves, their paint and the selection are bound, so a theme
184
185
  // tween re-paints and a `set` of `data` re-cuts the wedges.
@@ -216,7 +217,7 @@ export const PieChart = component2d({
216
217
  )
217
218
  }
218
219
  const row = new Rect({ flow: "horizontal", width: "fill", height: "fill", gap: state.gap, align: "center", children: parts })
219
- return asParts([...(state.children as unknown as Node2D[]), row as unknown as Node2D])
220
+ return asParts([row as unknown as Node2D])
220
221
  },
221
222
  commands: {
222
223
  /**
@@ -227,6 +228,8 @@ export const PieChart = component2d({
227
228
  */
228
229
  enter: {
229
230
  args: {},
231
+ // Held from the chart's arrival, so it never shows finished ahead of its draw.
232
+ entrance: true,
230
233
  easing: easeOut(),
231
234
  run: (timeline, _state, children) => {
232
235
  const parts = partsOf(children)
@@ -33,7 +33,6 @@ import {
33
33
  CHART_PALETTE,
34
34
  DEFAULT_LEGEND,
35
35
  DEFAULT_SERIES_FILLS,
36
- SPOTLIGHT_DIM,
37
36
  lerpLegendConfig,
38
37
  lerpSeriesFills,
39
38
  num,
@@ -306,6 +305,13 @@ const LABEL_TEXT_STYLE: TextStyle = {
306
305
  fill: CHART_PALETTE.markerColor,
307
306
  }
308
307
 
308
+ /**
309
+ * Opacity the other slices fade to under a selection. Fainter than the line and
310
+ * bar charts' `SPOTLIGHT_DIM`: a ring's slices touch, so at that a selected
311
+ * slice barely separates from its neighbours.
312
+ */
313
+ export const PIE_SPOTLIGHT_DIM = 0.1
314
+
309
315
  /** The pie chart's out-of-the-box look. */
310
316
  export const DEFAULT_PIE_CHART_THEME: PieChartConfig = {
311
317
  sliceFills: DEFAULT_SERIES_FILLS,
@@ -343,7 +349,7 @@ export const DEFAULT_PIE_CHART_THEME: PieChartConfig = {
343
349
  },
344
350
  },
345
351
  legend: DEFAULT_LEGEND,
346
- spotlight: { dimOpacity: SPOTLIGHT_DIM },
352
+ spotlight: { dimOpacity: PIE_SPOTLIGHT_DIM },
347
353
  }
348
354
 
349
355
  function resolveRingConfig(
@@ -442,7 +448,7 @@ function lerpCenterConfig(
442
448
  }
443
449
 
444
450
  /**
445
- * {@link PieChart}'s `theme` @property mapper: merges the author's partial
451
+ * {@link PieChart}'s `theme` property mapper: merges the author's partial
446
452
  * {@link PieChartTheme} onto `previous` (falling back to
447
453
  * {@link DEFAULT_PIE_CHART_THEME} on first resolution), fully resolving every
448
454
  * leaf so the tween below always has resolved data to interpolate.
@@ -464,7 +470,7 @@ export function resolvePieChartTheme(
464
470
  }
465
471
  }
466
472
 
467
- /** {@link PieChart}'s `theme` @property tween — leaf by leaf. */
473
+ /** {@link PieChart}'s `theme` property tween — leaf by leaf. */
468
474
  export function lerpPieChartTheme(
469
475
  from: PieChartConfig,
470
476
  to: PieChartConfig,