@yewdiel21/motion-kit 0.14.0 → 0.16.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 (156) hide show
  1. package/README.md +178 -155
  2. package/bin/motion-kit.mjs +3 -3
  3. package/dist/auto/index.d.ts +6 -3
  4. package/dist/auto/index.js +8 -2
  5. package/dist/auto/index.js.map +1 -1
  6. package/dist/{chunk-EY4BJKC4.js → chunk-NFZIX3IA.js} +2 -2
  7. package/dist/chunk-RBEKR5UZ.js +3660 -0
  8. package/dist/chunk-RBEKR5UZ.js.map +1 -0
  9. package/dist/{chunk-CU6SXW3E.js → chunk-SDQ776LG.js} +1596 -485
  10. package/dist/chunk-SDQ776LG.js.map +1 -0
  11. package/dist/css/components/add-to-cart.css +72 -0
  12. package/dist/css/components/bidirectional-slider.css +168 -0
  13. package/dist/css/components/chart.css +7 -0
  14. package/dist/css/components/data-table.css +121 -26
  15. package/dist/css/components/filter-builder.css +10 -15
  16. package/dist/css/components/heatmap-chart.css +398 -0
  17. package/dist/css/components/hover-card.css +8 -2
  18. package/dist/css/components/image-preview.css +90 -2
  19. package/dist/css/components/json-viewer.css +33 -1
  20. package/dist/css/components/nav.css +102 -102
  21. package/dist/css/components/orbit-menu.css +130 -0
  22. package/dist/css/components/pagination.css +10 -5
  23. package/dist/css/components/pie-chart.css +397 -0
  24. package/dist/css/components/pill-button.css +135 -0
  25. package/dist/css/components/radar-chart.css +377 -0
  26. package/dist/css/components/radial-chart.css +412 -0
  27. package/dist/css/components/sankey-chart.css +365 -0
  28. package/dist/css/components/scroll-island.css +114 -0
  29. package/dist/css/components/select.css +136 -0
  30. package/dist/css/components/toolbar.css +122 -0
  31. package/dist/css/components/tree-view.css +79 -0
  32. package/dist/css/components/voice-dots.css +70 -0
  33. package/dist/elements/index.d.ts +180 -7
  34. package/dist/elements/index.js +293 -7
  35. package/dist/elements/index.js.map +1 -1
  36. package/dist/{engine-Cn6IhE3z.d.ts → engine-5zRIssTO.d.ts} +11 -1
  37. package/dist/index.css +1849 -168
  38. package/dist/index.d.ts +4 -4
  39. package/dist/index.js +33 -3
  40. package/dist/index.js.map +1 -1
  41. package/dist/react/index.d.ts +117 -11
  42. package/dist/react/index.js +320 -13
  43. package/dist/react/index.js.map +1 -1
  44. package/dist/{reduced-BBUusld6.d.ts → reduced-DQwVXQYb.d.ts} +1 -1
  45. package/dist/{reveal-DEvp6U6T.d.ts → reveal-DRpNO4KP.d.ts} +109 -4
  46. package/dist/store-Da3zSh8Z.d.ts +457 -0
  47. package/package.json +9 -16
  48. package/registry.json +1096 -948
  49. package/src/auto/auto-init.ts +6 -0
  50. package/src/components/add-to-cart/README.md +49 -0
  51. package/src/components/add-to-cart/add-to-cart.css +71 -0
  52. package/src/components/add-to-cart/element.ts +20 -0
  53. package/src/components/add-to-cart/engine.ts +206 -0
  54. package/src/components/add-to-cart/react.tsx +33 -0
  55. package/src/components/bidirectional-slider/README.md +46 -0
  56. package/src/components/bidirectional-slider/bidirectional-slider.css +167 -0
  57. package/src/components/bidirectional-slider/element.ts +42 -0
  58. package/src/components/bidirectional-slider/engine.ts +289 -0
  59. package/src/components/bidirectional-slider/react.tsx +45 -0
  60. package/src/components/chart/README.md +49 -24
  61. package/src/components/chart/chart.css +7 -0
  62. package/src/components/chart/element.ts +4 -1
  63. package/src/components/chart/engine.ts +153 -34
  64. package/src/components/chart/parts.ts +237 -0
  65. package/src/components/chart/react.tsx +2 -2
  66. package/src/components/chart/use-chart.ts +55 -0
  67. package/src/components/data-table/README.md +3 -3
  68. package/src/components/data-table/data-table.css +121 -26
  69. package/src/components/data-table/engine.ts +81 -29
  70. package/src/components/editable-table/engine.ts +67 -11
  71. package/src/components/filter-builder/engine.ts +33 -2
  72. package/src/components/filter-builder/filter-builder.css +10 -15
  73. package/src/components/heatmap-chart/README.md +48 -0
  74. package/src/components/heatmap-chart/element.ts +21 -0
  75. package/src/components/heatmap-chart/engine.ts +281 -0
  76. package/src/components/heatmap-chart/heatmap-chart.css +93 -0
  77. package/src/components/heatmap-chart/react.tsx +14 -0
  78. package/src/components/hover-card/engine.ts +30 -1
  79. package/src/components/hover-card/hover-card.css +8 -2
  80. package/src/components/image-preview/element.ts +1 -1
  81. package/src/components/image-preview/engine.ts +95 -15
  82. package/src/components/image-preview/image-preview.css +90 -2
  83. package/src/components/image-preview/react.tsx +3 -3
  84. package/src/components/json-viewer/engine.ts +11 -2
  85. package/src/components/json-viewer/json-viewer.css +33 -1
  86. package/src/components/nav/README.md +12 -10
  87. package/src/components/nav/element.ts +5 -5
  88. package/src/components/nav/engine.ts +21 -14
  89. package/src/components/nav/nav.css +102 -102
  90. package/src/components/nav/react.tsx +4 -4
  91. package/src/components/orbit-menu/README.md +46 -0
  92. package/src/components/orbit-menu/element.ts +27 -0
  93. package/src/components/orbit-menu/engine.ts +179 -0
  94. package/src/components/orbit-menu/orbit-menu.css +129 -0
  95. package/src/components/orbit-menu/react.tsx +44 -0
  96. package/src/components/pagination/README.md +1 -1
  97. package/src/components/pagination/engine.ts +50 -4
  98. package/src/components/pagination/pagination.css +10 -5
  99. package/src/components/pie-chart/README.md +48 -0
  100. package/src/components/pie-chart/element.ts +30 -0
  101. package/src/components/pie-chart/engine.ts +283 -0
  102. package/src/components/pie-chart/pie-chart.css +92 -0
  103. package/src/components/pie-chart/react.tsx +14 -0
  104. package/src/components/pill-button/README.md +46 -0
  105. package/src/components/pill-button/element.ts +16 -0
  106. package/src/components/pill-button/engine.ts +77 -0
  107. package/src/components/pill-button/pill-button.css +134 -0
  108. package/src/components/pill-button/react.tsx +32 -0
  109. package/src/components/radar-chart/README.md +48 -0
  110. package/src/components/radar-chart/element.ts +30 -0
  111. package/src/components/radar-chart/engine.ts +327 -0
  112. package/src/components/radar-chart/radar-chart.css +72 -0
  113. package/src/components/radar-chart/react.tsx +14 -0
  114. package/src/components/radial-chart/README.md +58 -0
  115. package/src/components/radial-chart/element.ts +30 -0
  116. package/src/components/radial-chart/engine.ts +317 -0
  117. package/src/components/radial-chart/radial-chart.css +107 -0
  118. package/src/components/radial-chart/react.tsx +14 -0
  119. package/src/components/sankey-chart/README.md +48 -0
  120. package/src/components/sankey-chart/element.ts +31 -0
  121. package/src/components/sankey-chart/engine.ts +401 -0
  122. package/src/components/sankey-chart/react.tsx +14 -0
  123. package/src/components/sankey-chart/sankey-chart.css +60 -0
  124. package/src/components/scroll-island/README.md +44 -0
  125. package/src/components/scroll-island/element.ts +16 -0
  126. package/src/components/scroll-island/engine.ts +274 -0
  127. package/src/components/scroll-island/react.tsx +26 -0
  128. package/src/components/scroll-island/scroll-island.css +113 -0
  129. package/src/components/select/README.md +34 -0
  130. package/src/components/select/element.ts +26 -0
  131. package/src/components/select/engine.ts +267 -0
  132. package/src/components/select/react.tsx +33 -0
  133. package/src/components/select/select.css +135 -0
  134. package/src/components/toolbar/README.md +46 -0
  135. package/src/components/toolbar/element.ts +30 -0
  136. package/src/components/toolbar/engine.ts +230 -0
  137. package/src/components/toolbar/react.tsx +34 -0
  138. package/src/components/toolbar/toolbar.css +121 -0
  139. package/src/components/tree-view/element.ts +1 -1
  140. package/src/components/tree-view/engine.ts +174 -0
  141. package/src/components/tree-view/react.tsx +7 -3
  142. package/src/components/tree-view/tree-view.css +79 -0
  143. package/src/components/voice-dots/README.md +48 -0
  144. package/src/components/voice-dots/element.ts +45 -0
  145. package/src/components/voice-dots/engine.ts +243 -0
  146. package/src/components/voice-dots/react.tsx +52 -0
  147. package/src/components/voice-dots/voice-dots.css +69 -0
  148. package/src/elements/index.ts +39 -0
  149. package/src/index.ts +16 -0
  150. package/src/motion/liquid.ts +14 -4
  151. package/src/react/index.ts +14 -1
  152. package/dist/chunk-6P4P7NU6.js +0 -1401
  153. package/dist/chunk-6P4P7NU6.js.map +0 -1
  154. package/dist/chunk-CU6SXW3E.js.map +0 -1
  155. package/dist/store-DjkTBX7f.d.ts +0 -195
  156. /package/dist/{chunk-EY4BJKC4.js.map → chunk-NFZIX3IA.js.map} +0 -0
@@ -18,7 +18,7 @@ import { inert, warnOnce, type Instance } from '../../core/types';
18
18
  * table carries every value for screen readers. Resizes with its container.
19
19
  */
20
20
 
21
- export type ChartType = 'line' | 'area' | 'bar';
21
+ export type ChartType = 'line' | 'area' | 'bar' | 'composed';
22
22
 
23
23
  export interface ChartPoint {
24
24
  label: string;
@@ -29,6 +29,10 @@ export interface ChartSeries {
29
29
  name: string;
30
30
  /** One value per label; null is a gap (no data). */
31
31
  values: Array<number | null>;
32
+ /** Per-series mark in a composed chart (default line). */
33
+ type?: 'line' | 'area' | 'bar';
34
+ /** An area's lower boundary; values is the upper boundary. Nulls leave gaps. */
35
+ lowerValues?: Array<number | null>;
32
36
  /** Any CSS colour; default: monochrome for one series, else the --mk-chart-N slot. */
33
37
  color?: string;
34
38
  /** Lines: an SVG dash pattern, e.g. '5 5' (a pace or goal) or '1 6' (a projection). */
@@ -43,6 +47,16 @@ export interface ChartOptions {
43
47
  points?: ChartPoint[];
44
48
  /** Bars only: stack the series. */
45
49
  stacked?: boolean;
50
+ /** Rounded ends (default) or a full capsule, including stacked columns. */
51
+ barStyle?: 'rounded' | 'pill';
52
+ /** Show the available capacity behind each column. */
53
+ barTrack?: boolean;
54
+ /** Solid (default), gradient, or a gradient selection over muted striped columns. */
55
+ barFill?: 'solid' | 'gradient' | 'spotlight';
56
+ /** Maximum column width in px (default 24; pill demos use 40). */
57
+ barWidth?: number;
58
+ /** Smooth monotone curves preserve the data's extrema (default linear). */
59
+ curve?: 'linear' | 'smooth';
46
60
  formatValue?: (value: number) => string;
47
61
  /** Accessible name (and table caption). */
48
62
  label?: string;
@@ -98,6 +112,25 @@ function barPath(x: number, width: number, base: number, end: number, rounded: b
98
112
  return `M${round(x)},${round(base)}V${round(end + dir * r)}Q${round(x)},${round(end)} ${round(x + r)},${round(end)}H${round(x + width - r)}Q${round(x + width)},${round(end)} ${round(x + width)},${round(end + dir * r)}V${round(base)}Z`;
99
113
  }
100
114
 
115
+ function pillPath(x: number, width: number, a: number, b: number) {
116
+ const top = Math.min(a, b), bottom = Math.max(a, b);
117
+ const r = Math.min(width / 2, (bottom - top) / 2);
118
+ return `M${round(x + r)},${round(top)}H${round(x + width - r)}Q${round(x + width)},${round(top)} ${round(x + width)},${round(top + r)}V${round(bottom - r)}Q${round(x + width)},${round(bottom)} ${round(x + width - r)},${round(bottom)}H${round(x + r)}Q${round(x)},${round(bottom)} ${round(x)},${round(bottom - r)}V${round(top + r)}Q${round(x)},${round(top)} ${round(x + r)},${round(top)}Z`;
119
+ }
120
+
121
+ /** Cubic Hermite interpolation with bounded tangents: no invented peaks between samples. */
122
+ function linePath(points: number[][], smooth: boolean) {
123
+ if (!points.length) return '';
124
+ let path = `M${round(points[0][0])},${round(points[0][1])}`;
125
+ const slopes = points.slice(1).map((point, i) => (point[1] - points[i][1]) / (point[0] - points[i][0] || 1));
126
+ const tangents = points.map((_, i) => i === 0 ? slopes[0] : i === points.length - 1 ? slopes[i - 1] : slopes[i - 1] * slopes[i] <= 0 ? 0 : 2 * slopes[i - 1] * slopes[i] / (slopes[i - 1] + slopes[i]));
127
+ for (let i = 1; i < points.length; i++) {
128
+ const [x0, y0] = points[i - 1], [x, y] = points[i], dx = (x - x0) / 3;
129
+ path += smooth ? `C${round(x0 + dx)},${round(y0 + tangents[i - 1] * dx)} ${round(x - dx)},${round(y - tangents[i] * dx)} ${round(x)},${round(y)}` : `L${round(x)},${round(y)}`;
130
+ }
131
+ return path;
132
+ }
133
+
101
134
  interface Model {
102
135
  labels: string[];
103
136
  series: Array<ChartSeries & { color: string }>;
@@ -116,6 +149,8 @@ function modelOf(options: ChartOptions): Model {
116
149
  values: Array.from({ length: count }, (_, j) => (finite(series.values[j]) ? series.values[j]! : null)),
117
150
  color: series.color ?? (mono ? 'var(--mk-chart-mono)' : i < SLOTS ? `var(--mk-chart-${i + 1})` : 'var(--mk-chart-other)'),
118
151
  dash: series.dash,
152
+ type: series.type,
153
+ lowerValues: series.lowerValues ? Array.from({ length: count }, (_, j) => finite(series.lowerValues![j]) ? series.lowerValues![j]! : null) : undefined,
119
154
  }));
120
155
  return { labels: fullLabels, series };
121
156
  }
@@ -171,7 +206,7 @@ export function createChart(root: HTMLElement | null, initial: ChartOptions = {}
171
206
  let cursor: SVGGElement | null = null;
172
207
  let band: SVGRectElement | null = null;
173
208
  /** What is drawn now: values and domain (they trail the model while gliding). */
174
- let shown: { values: Values; lo: number; hi: number; type: ChartType; stacked: boolean } | null = null;
209
+ let shown: { values: Values; lower: Values; lo: number; hi: number; type: ChartType; stacked: boolean } | null = null;
175
210
  let glide: gsap.core.Tween | null = null;
176
211
  const effects = new Set<gsap.core.Animation>();
177
212
  const keep = (animation: gsap.core.Animation) => {
@@ -190,7 +225,9 @@ export function createChart(root: HTMLElement | null, initial: ChartOptions = {}
190
225
  if (options.formatValue) return options.formatValue(value);
191
226
  return new Intl.NumberFormat(undefined, { maximumFractionDigits: 2 }).format(value);
192
227
  };
193
- const type = (): ChartType => (options.type === 'bar' || options.type === 'area' ? options.type : 'line');
228
+ const type = (): ChartType => (options.type === 'bar' || options.type === 'area' || options.type === 'composed' ? options.type : 'line');
229
+ const seriesType = (index: number) => type() === 'composed' ? model.series[index]!.type ?? 'line' : type();
230
+ const hasBars = () => model.series.some((_, index) => seriesType(index) === 'bar');
194
231
  const stacked = () => type() === 'bar' && Boolean(options.stacked) && model.series.length > 1;
195
232
  const hasData = () => model.labels.length > 0 && model.series.some((series) => series.values.some(finite));
196
233
 
@@ -206,6 +243,13 @@ export function createChart(root: HTMLElement | null, initial: ChartOptions = {}
206
243
  return { up, down };
207
244
  };
208
245
  const targetValues = (): Values => model.series.map((series) => series.values);
246
+ const lowerValues = (): Values => model.series.map((series) => series.lowerValues ?? []);
247
+ const rangeText = (series: Model['series'][number], index: number) => {
248
+ const value = series.values[index];
249
+ const lower = series.lowerValues?.[index];
250
+ if (!finite(value)) return '—';
251
+ return finite(lower) ? `${format(lower)} – ${format(value)}` : format(value);
252
+ };
209
253
  const totals = (index: number) => totalsOf(targetValues(), index);
210
254
 
211
255
  const domainOf = (values: Values) => {
@@ -224,6 +268,9 @@ export function createChart(root: HTMLElement | null, initial: ChartOptions = {}
224
268
  lo = Math.min(lo, value);
225
269
  }
226
270
  }
271
+ for (const list of lowerValues()) {
272
+ if (finite(list[i])) { lo = Math.min(lo, list[i]!); hi = Math.max(hi, list[i]!); }
273
+ }
227
274
  });
228
275
  if (hi === lo) hi = lo + 1;
229
276
  const step = niceStep((hi - lo) / 4);
@@ -272,7 +319,7 @@ export function createChart(root: HTMLElement | null, initial: ChartOptions = {}
272
319
  for (const series of model.series) {
273
320
  const td = document.createElement('td');
274
321
  const value = series.values[i];
275
- td.textContent = finite(value) ? format(value) : '—';
322
+ td.textContent = rangeText(series, i);
276
323
  row.append(td);
277
324
  }
278
325
  if (stacked()) {
@@ -291,11 +338,11 @@ export function createChart(root: HTMLElement | null, initial: ChartOptions = {}
291
338
  const show = options.legend ?? model.series.length > 1;
292
339
  legend.hidden = !show || !hasData();
293
340
  if (legend.hidden) return;
294
- for (const series of model.series) {
341
+ for (const [index, series] of model.series.entries()) {
295
342
  const item = document.createElement('li');
296
343
  const swatch = document.createElement('span');
297
344
  swatch.dataset.mkChartSwatch = '';
298
- swatch.dataset.mkType = type();
345
+ swatch.dataset.mkType = seriesType(index);
299
346
  swatch.style.background = series.color;
300
347
  swatch.style.color = series.color;
301
348
  if (series.dash) swatch.dataset.mkDash = '';
@@ -314,19 +361,19 @@ export function createChart(root: HTMLElement | null, initial: ChartOptions = {}
314
361
  };
315
362
 
316
363
  /** Draw `values` on the domain lo…hi (called every frame while gliding). */
317
- const paint = (values: Values, lo: number, hi: number, step: number) => {
364
+ const paint = (values: Values, lo: number, hi: number, step: number, lower: Values = lowerValues()) => {
318
365
  canvas.replaceChildren();
319
366
  const width = Math.max(120, Math.round(plot.clientWidth || root.clientWidth || 600));
320
367
  const height = Math.max(80, Math.round(finite(options.height) ? options.height : 240));
321
368
  canvas.setAttribute('viewBox', `0 0 ${width} ${height}`);
322
369
  canvas.setAttribute('width', String(width));
323
370
  canvas.setAttribute('height', String(height));
324
- const bar = type() === 'bar';
371
+ const bar = hasBars();
325
372
  const axis = Boolean(options.axis);
326
373
  const ticks: number[] = [];
327
374
  for (let t = lo; t <= hi + step / 2; t += step) ticks.push(Number(t.toPrecision(12)));
328
375
  const tickText = ticks.map(format);
329
- const withEnd = !bar && options.endLabel !== false && lastPoint(values[0]) >= 0;
376
+ const withEnd = seriesType(0) !== 'bar' && options.endLabel !== false && lastPoint(values[0]) >= 0;
330
377
  const left = axis ? Math.min(96, Math.max(28, Math.max(...tickText.map((text) => text.length)) * 7 + 12)) : 4;
331
378
  const top = 16;
332
379
  const right = withEnd ? Math.min(120, Math.max(48, format(values[0]![lastPoint(values[0])]!).length * 7.5 + 28)) : 8;
@@ -339,6 +386,7 @@ export function createChart(root: HTMLElement | null, initial: ChartOptions = {}
339
386
  const slot = plotW / n;
340
387
  const xs = model.labels.map((_, i) => (bar ? left + slot * (i + 0.5) : n === 1 ? left + plotW / 2 : left + (plotW * i) / (n - 1)));
341
388
  geometry = { width, height, top, bottom: top + plotH, left, right: width - right, xs, y, base };
389
+ shown = { values, lower, lo, hi, type: type(), stacked: stacked() };
342
390
 
343
391
  // Baseline (and, with `axis`, hairlines with their values).
344
392
  const grid = svg('g', { 'data-mk-part': 'grid' });
@@ -381,11 +429,48 @@ export function createChart(root: HTMLElement | null, initial: ChartOptions = {}
381
429
  const marks = svg('g', { 'data-mk-part': 'series' });
382
430
  const defs = svg('defs');
383
431
  if (bar) {
384
- const groups = stacked() ? 1 : model.series.length;
385
- const barWidth = Math.max(2, Math.min(BAR_MAX, (slot * 0.62 - GAP * (groups - 1)) / groups));
432
+ const barIndices = model.series.flatMap((_, index) => seriesType(index) === 'bar' ? [index] : []);
433
+ const groups = stacked() ? 1 : barIndices.length;
434
+ const maxWidth = finite(options.barWidth) ? Math.max(2, Math.min(96, options.barWidth)) : BAR_MAX;
435
+ const barWidth = Math.max(2, Math.min(maxWidth, (slot * 0.62 - GAP * (groups - 1)) / groups));
436
+ const pill = options.barStyle === 'pill';
437
+ const trackLayer = svg('g', { 'data-mk-part': 'bar-tracks' });
438
+ canvas.append(trackLayer);
439
+ if (options.barFill && options.barFill !== 'solid') {
440
+ model.series.forEach((series, k) => {
441
+ const gradient = svg('linearGradient', { id: `${uid}-bar-${k}`, x1: 0, y1: 0, x2: 0, y2: 1 });
442
+ for (const [offset, opacity] of [['0', '1'], ['1', '0.4']]) {
443
+ const stop = svg('stop', { offset }); stop.style.stopColor = series.color; stop.style.stopOpacity = opacity; gradient.append(stop);
444
+ }
445
+ defs.append(gradient);
446
+ });
447
+ const hatch = svg('pattern', { id: `${uid}-hatch`, patternUnits: 'userSpaceOnUse', width: 6, height: 6, patternTransform: 'rotate(35)' });
448
+ const ground = svg('rect', { width: 6, height: 6 }); ground.style.fill = 'var(--mk-chart-track)';
449
+ const stripe = svg('line', { x1: 0, x2: 0, y1: 0, y2: 6 }); stripe.style.stroke = 'var(--mk-chart-grid)'; stripe.style.strokeWidth = '2';
450
+ hatch.append(ground, stripe); defs.append(hatch);
451
+ }
452
+ const fillBar = (path: SVGPathElement, k: number, index: number) => {
453
+ const fill = options.barFill && options.barFill !== 'solid' ? `url(#${uid}-bar-${k})` : model.series[k]!.color;
454
+ path.style.setProperty('--mk-bar-fill', fill);
455
+ path.style.fill = options.barFill === 'spotlight' && index !== (selected ?? n - 1) ? `url(#${uid}-hatch)` : fill;
456
+ };
457
+ const addTrack = (x: number) => {
458
+ if (!options.barTrack) return;
459
+ const path = svg('path', { d: pillPath(x, barWidth, top, base), 'data-mk-part': 'bar-track' });
460
+ trackLayer.append(path);
461
+ };
386
462
  const totalsLayer = svg('g', { 'data-mk-part': 'totals' });
387
463
  model.labels.forEach((_, i) => {
388
464
  if (stacked()) {
465
+ const clipId = `${uid}-stack-${i}`;
466
+ if (pill) {
467
+ const sum = totalsOf(values, i);
468
+ const clip = svg('clipPath', { id: clipId });
469
+ if (sum.up) clip.append(svg('path', { d: pillPath(xs[i]! - barWidth / 2, barWidth, base, y(sum.up)) }));
470
+ if (sum.down) clip.append(svg('path', { d: pillPath(xs[i]! - barWidth / 2, barWidth, base, y(sum.down)) }));
471
+ defs.append(clip);
472
+ }
473
+ addTrack(xs[i]! - barWidth / 2);
389
474
  let up = 0;
390
475
  let down = 0;
391
476
  const outer = { up: -1, down: -1 };
@@ -405,8 +490,9 @@ export function createChart(root: HTMLElement | null, initial: ChartOptions = {}
405
490
  const start = y(from) + (from !== 0 ? (value > 0 ? -GAP : GAP) : 0);
406
491
  const end = y(to);
407
492
  if ((value > 0 && end >= start) || (value < 0 && end <= start)) return;
408
- const path = svg('path', { d: barPath(xs[i]! - barWidth / 2, barWidth, start, end, k === (value > 0 ? outer.up : outer.down)), 'data-mk-index': i, 'data-mk-series': k });
409
- path.style.fill = model.series[k]!.color;
493
+ const path = svg('path', { d: barPath(xs[i]! - barWidth / 2, barWidth, start, end, !pill && k === (value > 0 ? outer.up : outer.down)), 'data-mk-part': 'bar', 'data-mk-index': i, 'data-mk-series': k });
494
+ if (pill) path.setAttribute('clip-path', `url(#${clipId})`);
495
+ fillBar(path, k, i);
410
496
  marks.append(path);
411
497
  });
412
498
  // The stack total above it, when the columns have room.
@@ -419,30 +505,35 @@ export function createChart(root: HTMLElement | null, initial: ChartOptions = {}
419
505
  } else {
420
506
  const groupWidth = groups * barWidth + GAP * (groups - 1);
421
507
  values.forEach((list, k) => {
508
+ if (seriesType(k) !== 'bar') return;
422
509
  const value = list[i];
510
+ const x = xs[i]! - groupWidth / 2 + barIndices.indexOf(k) * (barWidth + GAP);
511
+ addTrack(x);
423
512
  if (!finite(value) || value === 0) return;
424
- const x = xs[i]! - groupWidth / 2 + k * (barWidth + GAP);
425
- const path = svg('path', { d: barPath(x, barWidth, base, y(value), true), 'data-mk-index': i, 'data-mk-series': k });
426
- path.style.fill = model.series[k]!.color;
513
+ const path = svg('path', { d: pill ? pillPath(x, barWidth, base, y(value)) : barPath(x, barWidth, base, y(value), true), 'data-mk-part': 'bar', 'data-mk-index': i, 'data-mk-series': k });
514
+ fillBar(path, k, i);
427
515
  marks.append(path);
428
516
  });
429
517
  }
430
518
  });
431
519
  if (totalsLayer.childNodes.length) canvas.append(totalsLayer);
432
- } else {
520
+ }
521
+ if (model.series.some((_, index) => seriesType(index) !== 'bar')) {
433
522
  values.forEach((list, k) => {
523
+ if (seriesType(k) === 'bar') return;
434
524
  const series = model.series[k]!;
435
- const runs: Array<Array<[number, number]>> = [];
436
- let run: Array<[number, number]> = [];
525
+ const runs: Array<Array<[number, number, number]>> = [];
526
+ let run: Array<[number, number, number]> = [];
437
527
  list.forEach((value, i) => {
438
- if (finite(value)) run.push([xs[i]!, y(value)]);
528
+ const bottomValue = lower[k]?.[i];
529
+ if (finite(value) && (!series.lowerValues || finite(bottomValue))) run.push([xs[i]!, y(value), finite(bottomValue) ? y(bottomValue) : base]);
439
530
  else if (run.length) {
440
531
  runs.push(run);
441
532
  run = [];
442
533
  }
443
534
  });
444
535
  if (run.length) runs.push(run);
445
- if (type() === 'area') {
536
+ if (seriesType(k) === 'area') {
446
537
  const gradient = svg('linearGradient', { id: `${uid}-fill-${k}`, x1: 0, x2: 0, y1: 0, y2: 1 });
447
538
  const stop = (offset: string, opacity: string) => {
448
539
  const element = svg('stop', { offset });
@@ -454,8 +545,10 @@ export function createChart(root: HTMLElement | null, initial: ChartOptions = {}
454
545
  defs.append(gradient);
455
546
  for (const points of runs) {
456
547
  if (points.length < 2) continue;
457
- const d = `M${points.map(([px, py]) => `${round(px)},${round(py)}`).join('L')}L${round(points[points.length - 1]![0])},${round(base)}L${round(points[0]![0])},${round(base)}Z`;
458
- const area = svg('path', { d, 'data-mk-part': 'area' });
548
+ const upper = linePath(points, options.curve === 'smooth');
549
+ const bottom = linePath([...points].reverse().map(([px, , py]) => [px, py]), options.curve === 'smooth');
550
+ const d = `${upper}${bottom.replace(/^M/, 'L')}Z`;
551
+ const area = svg('path', { d, 'data-mk-part': 'area', 'data-mk-series': k });
459
552
  area.style.fill = `url(#${uid}-fill-${k})`;
460
553
  marks.append(area);
461
554
  }
@@ -468,7 +561,7 @@ export function createChart(root: HTMLElement | null, initial: ChartOptions = {}
468
561
  marks.append(dot);
469
562
  continue;
470
563
  }
471
- const line = svg('path', { d: `M${points.map(([px, py]) => `${round(px)},${round(py)}`).join('L')}`, 'data-mk-part': 'line' });
564
+ const line = svg('path', { d: linePath(points, options.curve === 'smooth'), 'data-mk-part': 'line' });
472
565
  line.style.stroke = series.color;
473
566
  // Dotted patterns (a first dash of 1 or less) draw round dots; dashes stay thin.
474
567
  const dotted = Boolean(series.dash) && Number(series.dash!.trim().split(/[\s,]+/)[0]) <= 1;
@@ -479,7 +572,7 @@ export function createChart(root: HTMLElement | null, initial: ChartOptions = {}
479
572
  });
480
573
  // The last value of the first series: a ringed point and its pill.
481
574
  const last = lastPoint(values[0]);
482
- if (last >= 0 && !model.series[0]!.dash) {
575
+ if (last >= 0 && seriesType(0) !== 'bar' && !model.series[0]!.dash) {
483
576
  const point = svg('circle', { cx: round(xs[last]!), cy: round(y(values[0]![last]!)), r: 5, 'data-mk-part': 'end' });
484
577
  point.style.fill = model.series[0]!.color;
485
578
  marks.append(point);
@@ -508,6 +601,11 @@ export function createChart(root: HTMLElement | null, initial: ChartOptions = {}
508
601
  cursor.replaceChildren();
509
602
  if (band) band.style.display = 'none';
510
603
  for (const mark of canvas.querySelectorAll('[data-mk-index]')) mark.removeAttribute('data-mk-dim');
604
+ for (const mark of canvas.querySelectorAll<SVGPathElement>('[data-mk-part="bar"]')) {
605
+ const index = Number(mark.dataset.mkIndex);
606
+ mark.toggleAttribute('data-mk-active', index === selected);
607
+ if (options.barFill === 'spotlight') mark.style.fill = index === (selected ?? model.labels.length - 1) ? mark.style.getPropertyValue('--mk-bar-fill') : `url(#${uid}-hatch)`;
608
+ }
511
609
  if (selected === null || !hasData()) {
512
610
  if (!tooltip.hidden && !reduced()) {
513
611
  gsap.killTweensOf(tooltip, 'opacity,scale');
@@ -532,6 +630,20 @@ export function createChart(root: HTMLElement | null, initial: ChartOptions = {}
532
630
  for (const mark of canvas.querySelectorAll<SVGElement>('[data-mk-index]')) if (mark.getAttribute('data-mk-index') !== String(i)) mark.setAttribute('data-mk-dim', '');
533
631
  const { up, down } = totals(i);
534
632
  anchorY = stacked() ? geometry.y(up || down) : Math.min(...model.series.map((series) => (finite(series.values[i]) ? geometry.y(series.values[i]!) : geometry.bottom)));
633
+ if (options.barStyle === 'pill') {
634
+ const amount = stacked() ? up + down : model.series[0]?.values[i];
635
+ if (finite(amount)) {
636
+ const label = format(amount), width = Math.max(40, label.length * 7 + 18);
637
+ const bx = Math.max(0, Math.min(geometry.width - width, x - width / 2));
638
+ const by = Math.max(0, anchorY - 32);
639
+ const group = svg('g', { 'data-mk-part': 'bar-value' });
640
+ group.append(svg('rect', { x: round(bx), y: round(by), width, height: 24, rx: 12 }));
641
+ const text = svg('text', { x: round(bx + width / 2), y: round(by + 12), 'text-anchor': 'middle', 'dominant-baseline': 'middle' });
642
+ text.textContent = label; group.append(text); cursor.append(group);
643
+ const marker = svg('circle', { cx: round(x), cy: round(anchorY), r: 4, 'data-mk-part': 'marker' }); marker.style.fill = model.series[0]?.color ?? 'var(--mk-text)'; cursor.append(marker);
644
+ if (fresh && !reduced()) keep(gsap.fromTo(group, { opacity: 0, y: 6 }, { opacity: 1, y: 0, duration: 0.3, ease: EASE.pop }));
645
+ }
646
+ }
535
647
  } else {
536
648
  cursor.append(svg('line', { x1: round(x), x2: round(x), y1: geometry.top, y2: geometry.bottom, 'data-mk-part': 'guide' }));
537
649
  model.series.forEach((series) => {
@@ -564,10 +676,10 @@ export function createChart(root: HTMLElement | null, initial: ChartOptions = {}
564
676
  const name = document.createElement('span');
565
677
  name.textContent = series.name;
566
678
  const amount = document.createElement('b');
567
- amount.textContent = finite(value) ? format(value) : '—';
679
+ amount.textContent = rangeText(series, i);
568
680
  row.append(swatch, name, amount);
569
681
  tooltip.append(row);
570
- rows.push(`${series.name ? `${series.name}: ` : ''}${finite(value) ? format(value) : 'sin dato'}`);
682
+ rows.push(`${series.name ? `${series.name}: ` : ''}${finite(value) ? rangeText(series, i) : 'sin dato'}`);
571
683
  }
572
684
  if (stacked()) {
573
685
  const { up, down } = totals(i);
@@ -621,6 +733,7 @@ export function createChart(root: HTMLElement | null, initial: ChartOptions = {}
621
733
  glide?.kill();
622
734
  canvas.setAttribute('aria-label', describe());
623
735
  scope.attr(root, 'data-mk-chart-type', type());
736
+ scope.attr(root, 'data-mk-bar-style', options.barStyle ?? 'rounded');
624
737
  renderTable();
625
738
  renderLegend();
626
739
  const ok = hasData();
@@ -640,7 +753,7 @@ export function createChart(root: HTMLElement | null, initial: ChartOptions = {}
640
753
  const target = targetValues();
641
754
  const domain = domainOf(target);
642
755
  const finish = () => {
643
- shown = { values: target, lo: domain.lo, hi: domain.hi, type: type(), stacked: stacked() };
756
+ shown = { values: target, lower: lowerValues(), lo: domain.lo, hi: domain.hi, type: type(), stacked: stacked() };
644
757
  paint(target, domain.lo, domain.hi, domain.step);
645
758
  paintSelection(false);
646
759
  };
@@ -660,7 +773,11 @@ export function createChart(root: HTMLElement | null, initial: ChartOptions = {}
660
773
  const before = from.values[k]![i];
661
774
  return finite(value) && finite(before) ? before + (value - before) * t : value;
662
775
  }));
663
- paint(values, from.lo + (domain.lo - from.lo) * t, from.hi + (domain.hi - from.hi) * t, domain.step);
776
+ const lower = lowerValues().map((list, k) => list.map((value, i) => {
777
+ const before = from.lower[k]?.[i];
778
+ return finite(value) && finite(before) ? before + (value - before) * t : value;
779
+ }));
780
+ paint(values, from.lo + (domain.lo - from.lo) * t, from.hi + (domain.hi - from.hi) * t, domain.step, lower);
664
781
  paintSelection(false);
665
782
  },
666
783
  onComplete: () => {
@@ -673,12 +790,14 @@ export function createChart(root: HTMLElement | null, initial: ChartOptions = {}
673
790
  // First appearance: bars grow from the baseline, lines wipe in from the left.
674
791
  finish();
675
792
  if (type() === 'bar') {
676
- const state = { t: 0 };
793
+ const stagger = Math.min(0.045, 0.22 / Math.max(1, model.labels.length - 1));
794
+ const length = 0.7 + stagger * (model.labels.length - 1);
795
+ const state = { t: 0 }, ease = gsap.parseEase(EASE.swatch);
677
796
  glide = gsap.to(state, {
678
- t: 1,
679
- duration: 0.7,
680
- ease: EASE.swatch,
681
- onUpdate: () => paint(target.map((list) => list.map((value) => (finite(value) ? value * state.t : value))), domain.lo, domain.hi, domain.step),
797
+ t: length,
798
+ duration: length,
799
+ ease: 'none',
800
+ onUpdate: () => paint(target.map((list) => list.map((value, i) => (finite(value) ? value * Math.max(0, ease(Math.min(1, Math.max(0, (state.t - i * stagger) / 0.7)))) : value))), domain.lo, domain.hi, domain.step),
682
801
  onComplete: () => {
683
802
  finish();
684
803
  settle();
@@ -0,0 +1,237 @@
1
+ import { gsap, EASE } from '../../core/gsap';
2
+ import { reduced } from '../../core/reduced';
3
+ import type { Scope } from '../../core/scope';
4
+
5
+ /*
6
+ * The parts the round and flow charts (pie, radar, radial, sankey, heatmap)
7
+ * share with chart: the same plot, inverse-glass tooltip, outlined legend,
8
+ * screen-reader table and colour slots, so they read as one family and use
9
+ * chart.css. Each chart draws its own marks.
10
+ */
11
+
12
+ export const SVG_NS = 'http://www.w3.org/2000/svg';
13
+ export const SLOTS = 8;
14
+
15
+ export const svg = <K extends keyof SVGElementTagNameMap>(tag: K, attrs: Record<string, string | number> = {}) => {
16
+ const element = document.createElementNS(SVG_NS, tag);
17
+ for (const [name, value] of Object.entries(attrs)) element.setAttribute(name, String(value));
18
+ return element;
19
+ };
20
+ export const finite = (value: unknown): value is number => typeof value === 'number' && Number.isFinite(value);
21
+ export const round = (n: number) => Math.round(n * 100) / 100;
22
+
23
+ /** The colour of slot `i`: monochrome alone, --mk-chart-N for up to 8, a neutral past them. */
24
+ export const slotColor = (i: number, total = SLOTS) => (total === 1 ? 'var(--mk-chart-mono)' : i < SLOTS ? `var(--mk-chart-${i + 1})` : 'var(--mk-chart-other)');
25
+
26
+ export const formatNumber = (value: number) => new Intl.NumberFormat(undefined, { maximumFractionDigits: 2 }).format(value);
27
+
28
+ /** A point on a circle: angle 0 is 12 o'clock, clockwise. */
29
+ export const polar = (cx: number, cy: number, r: number, angle: number) => [cx + r * Math.sin(angle), cy - r * Math.cos(angle)] as const;
30
+
31
+ /** An annular sector (a pie slice when `inner` is 0) from angle `a0` to `a1`. */
32
+ export function arcPath(cx: number, cy: number, outer: number, inner: number, a0: number, a1: number) {
33
+ const sweep = Math.min(a1 - a0, Math.PI * 2 - 1e-4);
34
+ const end = a0 + sweep;
35
+ const large = sweep > Math.PI ? 1 : 0;
36
+ const [x0, y0] = polar(cx, cy, outer, a0);
37
+ const [x1, y1] = polar(cx, cy, outer, end);
38
+ if (inner <= 0) return `M${round(cx)},${round(cy)}L${round(x0)},${round(y0)}A${round(outer)},${round(outer)} 0 ${large} 1 ${round(x1)},${round(y1)}Z`;
39
+ const [x2, y2] = polar(cx, cy, inner, end);
40
+ const [x3, y3] = polar(cx, cy, inner, a0);
41
+ return `M${round(x0)},${round(y0)}A${round(outer)},${round(outer)} 0 ${large} 1 ${round(x1)},${round(y1)}L${round(x2)},${round(y2)}A${round(inner)},${round(inner)} 0 ${large} 0 ${round(x3)},${round(y3)}Z`;
42
+ }
43
+
44
+ export interface TipRow {
45
+ name: string;
46
+ color?: string;
47
+ value: string;
48
+ }
49
+
50
+ export interface ChartFrame {
51
+ plot: HTMLDivElement;
52
+ canvas: SVGSVGElement;
53
+ tooltip: HTMLDivElement;
54
+ empty: HTMLParagraphElement;
55
+ legend: HTMLUListElement;
56
+ table: HTMLTableElement;
57
+ live: HTMLSpanElement;
58
+ /** Fill and show the tooltip near (x, y) in plot px; null hides it. */
59
+ tip(at: { x: number; y: number; title: string; rows: TipRow[] } | null): void;
60
+ /** One outlined pill per item; `type` picks the swatch's shape. */
61
+ fillLegend(items: Array<{ name: string; color: string }>, show: boolean, type?: string): HTMLLIElement[];
62
+ /** The values for screen readers: a caption, the column heads and the rows. */
63
+ fillTable(caption: string, head: string[], rows: string[][]): void;
64
+ keep(animation: gsap.core.Animation | null | undefined): void;
65
+ }
66
+
67
+ /** The plot, tooltip, legend and table of a chart, removed by the scope. The root gets .mk-chart for chart.css. */
68
+ export function chartFrame(root: HTMLElement, scope: Scope, kind: string): ChartFrame {
69
+ if (!root.classList.contains('mk-chart')) {
70
+ root.classList.add('mk-chart');
71
+ scope.add(() => {
72
+ root.classList.remove('mk-chart');
73
+ if (!root.classList.length) root.removeAttribute('class');
74
+ });
75
+ }
76
+ scope.attr(root, 'data-mk-chart-type', kind);
77
+ const made = <K extends keyof HTMLElementTagNameMap>(tag: K, parent: Element = root) => {
78
+ const element = document.createElement(tag);
79
+ parent.append(element);
80
+ scope.add(() => element.remove());
81
+ return element;
82
+ };
83
+ const plot = made('div');
84
+ plot.dataset.mkChartPlot = '';
85
+ const canvas = svg('svg');
86
+ canvas.dataset.mkChartSvg = '';
87
+ canvas.setAttribute('role', 'img');
88
+ canvas.setAttribute('tabindex', '0');
89
+ canvas.setAttribute('focusable', 'true');
90
+ plot.append(canvas);
91
+ const tooltip = made('div', plot);
92
+ tooltip.dataset.mkChartTooltip = '';
93
+ tooltip.setAttribute('aria-hidden', 'true');
94
+ tooltip.hidden = true;
95
+ const empty = made('p');
96
+ empty.dataset.mkChartEmpty = '';
97
+ empty.hidden = true;
98
+ const legend = made('ul');
99
+ legend.dataset.mkChartLegend = '';
100
+ const table = made('table');
101
+ table.dataset.mkChartTable = '';
102
+ table.className = 'mk-sr-only';
103
+ const live = made('span');
104
+ live.className = 'mk-sr-only';
105
+ live.setAttribute('role', 'status');
106
+ scope.attr(root, 'data-mk-ready', '');
107
+
108
+ const effects = new Set<gsap.core.Animation>();
109
+ const keep = (animation: gsap.core.Animation | null | undefined) => {
110
+ if (!animation) return;
111
+ effects.add(animation);
112
+ // Finished ones leave as new ones arrive: the animation's own onComplete stays untouched.
113
+ for (const effect of effects) if (effect.totalProgress() === 1) effects.delete(effect);
114
+ };
115
+ scope.add(() => {
116
+ for (const effect of effects) effect.kill();
117
+ effects.clear();
118
+ gsap.killTweensOf(tooltip);
119
+ });
120
+
121
+ const tip: ChartFrame['tip'] = (at) => {
122
+ if (!at) {
123
+ if (!tooltip.hidden && !reduced()) {
124
+ gsap.killTweensOf(tooltip, 'opacity,scale');
125
+ keep(gsap.to(tooltip, { opacity: 0, scale: 0.94, duration: 0.15, ease: 'power2.in', onComplete: () => {
126
+ tooltip.hidden = true;
127
+ } }));
128
+ } else tooltip.hidden = true;
129
+ return;
130
+ }
131
+ tooltip.replaceChildren();
132
+ const title = document.createElement('strong');
133
+ title.textContent = at.title;
134
+ tooltip.append(title);
135
+ for (const each of at.rows) {
136
+ const row = document.createElement('span');
137
+ row.dataset.mkChartRow = '';
138
+ const swatch = document.createElement('i');
139
+ if (!each.color || /--mk-(text|chart-mono|muted)/.test(each.color)) swatch.dataset.mkMono = '';
140
+ else swatch.style.background = each.color;
141
+ const name = document.createElement('span');
142
+ name.textContent = each.name;
143
+ const amount = document.createElement('b');
144
+ amount.textContent = each.value;
145
+ row.append(swatch, name, amount);
146
+ tooltip.append(row);
147
+ }
148
+ const appearing = tooltip.hidden;
149
+ tooltip.hidden = false;
150
+ const width = plot.clientWidth || 300;
151
+ const height = plot.clientHeight || 240;
152
+ const tipW = tooltip.offsetWidth || 160;
153
+ const tipH = tooltip.offsetHeight || 60;
154
+ // Beside the point, towards the middle of the plot, inside it.
155
+ const left = round(Math.max(0, Math.min(width - tipW, at.x > width / 2 ? at.x - 16 - tipW : at.x + 16)));
156
+ const top = round(Math.max(0, Math.min(height - tipH, at.y - tipH - 12)));
157
+ if (appearing || reduced()) {
158
+ gsap.killTweensOf(tooltip);
159
+ gsap.set(tooltip, { x: left, y: top });
160
+ if (!reduced()) keep(gsap.fromTo(tooltip, { opacity: 0, scale: 0.9 }, { opacity: 1, scale: 1, duration: 0.25, ease: EASE.pop }));
161
+ else gsap.set(tooltip, { opacity: 1, scale: 1 });
162
+ } else {
163
+ gsap.killTweensOf(tooltip, 'opacity,scale');
164
+ gsap.set(tooltip, { opacity: 1, scale: 1 });
165
+ keep(gsap.to(tooltip, { x: left, y: top, duration: 0.25, ease: 'power3.out', overwrite: 'auto' }));
166
+ }
167
+ };
168
+
169
+ const fillLegend: ChartFrame['fillLegend'] = (items, show, type = 'bar') => {
170
+ legend.replaceChildren();
171
+ legend.hidden = !show || !items.length;
172
+ if (legend.hidden) return [];
173
+ return items.map((item) => {
174
+ const li = document.createElement('li');
175
+ const swatch = document.createElement('span');
176
+ swatch.dataset.mkChartSwatch = '';
177
+ swatch.dataset.mkType = type;
178
+ swatch.style.background = item.color;
179
+ swatch.style.color = item.color;
180
+ swatch.setAttribute('aria-hidden', 'true');
181
+ const name = document.createElement('span');
182
+ name.textContent = item.name;
183
+ li.append(swatch, name);
184
+ legend.append(li);
185
+ return li;
186
+ });
187
+ };
188
+
189
+ const fillTable: ChartFrame['fillTable'] = (caption, head, rows) => {
190
+ table.replaceChildren();
191
+ const cap = document.createElement('caption');
192
+ cap.textContent = caption;
193
+ const thead = document.createElement('thead');
194
+ const headRow = document.createElement('tr');
195
+ for (const text of head) {
196
+ const th = document.createElement('th');
197
+ th.scope = 'col';
198
+ th.textContent = text;
199
+ headRow.append(th);
200
+ }
201
+ thead.append(headRow);
202
+ const body = document.createElement('tbody');
203
+ for (const cells of rows) {
204
+ const row = document.createElement('tr');
205
+ cells.forEach((text, i) => {
206
+ const cell = document.createElement(i ? 'td' : 'th');
207
+ if (!i) (cell as HTMLTableCellElement).scope = 'row';
208
+ cell.textContent = text;
209
+ row.append(cell);
210
+ });
211
+ body.append(row);
212
+ }
213
+ table.append(cap, thead, body);
214
+ };
215
+
216
+ return { plot, canvas, tooltip, empty, legend, table, live, tip, fillLegend, fillTable, keep };
217
+ }
218
+
219
+ /** Calls `draw` again when the root changes width (on the next frame, once). */
220
+ export function onWidth(root: HTMLElement, scope: Scope, draw: () => void) {
221
+ if (typeof ResizeObserver !== 'function') return;
222
+ let width = 0;
223
+ let frame = 0;
224
+ const observer = scope.observe(
225
+ new ResizeObserver(() => {
226
+ const next = Math.round(root.clientWidth);
227
+ if (!next || next === width) return;
228
+ const first = !width;
229
+ width = next;
230
+ if (first) return;
231
+ cancelAnimationFrame(frame);
232
+ frame = requestAnimationFrame(draw);
233
+ }),
234
+ );
235
+ observer.observe(root);
236
+ scope.add(() => cancelAnimationFrame(frame));
237
+ }
@@ -11,13 +11,13 @@ export interface ChartProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onSele
11
11
  * <Chart type="bar" stacked labels={weeks} series={[{ name: 'Ana', values }]} formatValue={money} label="Sesiones por semana" />.
12
12
  * Data changes redraw in place; the selection survives when its point still exists.
13
13
  */
14
- export function Chart({ type, labels, series, points, stacked, formatValue, label, height, emptyLabel, legend, tooltip, onSelect, instanceRef, className, ...props }: ChartProps) {
14
+ export function Chart({ type, labels, series, points, stacked, barStyle, barTrack, barFill, barWidth, curve, formatValue, label, height, emptyLabel, legend, tooltip, axis, endLabel, onSelect, instanceRef, className, ...props }: ChartProps) {
15
15
  const ref = useRef<HTMLDivElement>(null);
16
16
  const instance = useRef<ChartInstance | null>(null);
17
17
  const format = useLatest(formatValue);
18
18
  const select = useLatest(onSelect);
19
19
  const formatted = formatValue !== undefined;
20
- const data = { type, labels, series, points, stacked, label, height, emptyLabel, legend, tooltip };
20
+ const data = { type, labels, series, points, stacked, barStyle, barTrack, barFill, barWidth, curve, label, height, emptyLabel, legend, tooltip, axis, endLabel };
21
21
  const key = JSON.stringify(data);
22
22
  const latest = useLatest(data);
23
23
  const options = (): ChartOptions => ({