@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.
- package/README.md +178 -155
- package/bin/motion-kit.mjs +3 -3
- package/dist/auto/index.d.ts +6 -3
- package/dist/auto/index.js +8 -2
- package/dist/auto/index.js.map +1 -1
- package/dist/{chunk-EY4BJKC4.js → chunk-NFZIX3IA.js} +2 -2
- package/dist/chunk-RBEKR5UZ.js +3660 -0
- package/dist/chunk-RBEKR5UZ.js.map +1 -0
- package/dist/{chunk-CU6SXW3E.js → chunk-SDQ776LG.js} +1596 -485
- package/dist/chunk-SDQ776LG.js.map +1 -0
- package/dist/css/components/add-to-cart.css +72 -0
- package/dist/css/components/bidirectional-slider.css +168 -0
- package/dist/css/components/chart.css +7 -0
- package/dist/css/components/data-table.css +121 -26
- package/dist/css/components/filter-builder.css +10 -15
- package/dist/css/components/heatmap-chart.css +398 -0
- package/dist/css/components/hover-card.css +8 -2
- package/dist/css/components/image-preview.css +90 -2
- package/dist/css/components/json-viewer.css +33 -1
- package/dist/css/components/nav.css +102 -102
- package/dist/css/components/orbit-menu.css +130 -0
- package/dist/css/components/pagination.css +10 -5
- package/dist/css/components/pie-chart.css +397 -0
- package/dist/css/components/pill-button.css +135 -0
- package/dist/css/components/radar-chart.css +377 -0
- package/dist/css/components/radial-chart.css +412 -0
- package/dist/css/components/sankey-chart.css +365 -0
- package/dist/css/components/scroll-island.css +114 -0
- package/dist/css/components/select.css +136 -0
- package/dist/css/components/toolbar.css +122 -0
- package/dist/css/components/tree-view.css +79 -0
- package/dist/css/components/voice-dots.css +70 -0
- package/dist/elements/index.d.ts +180 -7
- package/dist/elements/index.js +293 -7
- package/dist/elements/index.js.map +1 -1
- package/dist/{engine-Cn6IhE3z.d.ts → engine-5zRIssTO.d.ts} +11 -1
- package/dist/index.css +1849 -168
- package/dist/index.d.ts +4 -4
- package/dist/index.js +33 -3
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.ts +117 -11
- package/dist/react/index.js +320 -13
- package/dist/react/index.js.map +1 -1
- package/dist/{reduced-BBUusld6.d.ts → reduced-DQwVXQYb.d.ts} +1 -1
- package/dist/{reveal-DEvp6U6T.d.ts → reveal-DRpNO4KP.d.ts} +109 -4
- package/dist/store-Da3zSh8Z.d.ts +457 -0
- package/package.json +9 -16
- package/registry.json +1096 -948
- package/src/auto/auto-init.ts +6 -0
- package/src/components/add-to-cart/README.md +49 -0
- package/src/components/add-to-cart/add-to-cart.css +71 -0
- package/src/components/add-to-cart/element.ts +20 -0
- package/src/components/add-to-cart/engine.ts +206 -0
- package/src/components/add-to-cart/react.tsx +33 -0
- package/src/components/bidirectional-slider/README.md +46 -0
- package/src/components/bidirectional-slider/bidirectional-slider.css +167 -0
- package/src/components/bidirectional-slider/element.ts +42 -0
- package/src/components/bidirectional-slider/engine.ts +289 -0
- package/src/components/bidirectional-slider/react.tsx +45 -0
- package/src/components/chart/README.md +49 -24
- package/src/components/chart/chart.css +7 -0
- package/src/components/chart/element.ts +4 -1
- package/src/components/chart/engine.ts +153 -34
- package/src/components/chart/parts.ts +237 -0
- package/src/components/chart/react.tsx +2 -2
- package/src/components/chart/use-chart.ts +55 -0
- package/src/components/data-table/README.md +3 -3
- package/src/components/data-table/data-table.css +121 -26
- package/src/components/data-table/engine.ts +81 -29
- package/src/components/editable-table/engine.ts +67 -11
- package/src/components/filter-builder/engine.ts +33 -2
- package/src/components/filter-builder/filter-builder.css +10 -15
- package/src/components/heatmap-chart/README.md +48 -0
- package/src/components/heatmap-chart/element.ts +21 -0
- package/src/components/heatmap-chart/engine.ts +281 -0
- package/src/components/heatmap-chart/heatmap-chart.css +93 -0
- package/src/components/heatmap-chart/react.tsx +14 -0
- package/src/components/hover-card/engine.ts +30 -1
- package/src/components/hover-card/hover-card.css +8 -2
- package/src/components/image-preview/element.ts +1 -1
- package/src/components/image-preview/engine.ts +95 -15
- package/src/components/image-preview/image-preview.css +90 -2
- package/src/components/image-preview/react.tsx +3 -3
- package/src/components/json-viewer/engine.ts +11 -2
- package/src/components/json-viewer/json-viewer.css +33 -1
- package/src/components/nav/README.md +12 -10
- package/src/components/nav/element.ts +5 -5
- package/src/components/nav/engine.ts +21 -14
- package/src/components/nav/nav.css +102 -102
- package/src/components/nav/react.tsx +4 -4
- package/src/components/orbit-menu/README.md +46 -0
- package/src/components/orbit-menu/element.ts +27 -0
- package/src/components/orbit-menu/engine.ts +179 -0
- package/src/components/orbit-menu/orbit-menu.css +129 -0
- package/src/components/orbit-menu/react.tsx +44 -0
- package/src/components/pagination/README.md +1 -1
- package/src/components/pagination/engine.ts +50 -4
- package/src/components/pagination/pagination.css +10 -5
- package/src/components/pie-chart/README.md +48 -0
- package/src/components/pie-chart/element.ts +30 -0
- package/src/components/pie-chart/engine.ts +283 -0
- package/src/components/pie-chart/pie-chart.css +92 -0
- package/src/components/pie-chart/react.tsx +14 -0
- package/src/components/pill-button/README.md +46 -0
- package/src/components/pill-button/element.ts +16 -0
- package/src/components/pill-button/engine.ts +77 -0
- package/src/components/pill-button/pill-button.css +134 -0
- package/src/components/pill-button/react.tsx +32 -0
- package/src/components/radar-chart/README.md +48 -0
- package/src/components/radar-chart/element.ts +30 -0
- package/src/components/radar-chart/engine.ts +327 -0
- package/src/components/radar-chart/radar-chart.css +72 -0
- package/src/components/radar-chart/react.tsx +14 -0
- package/src/components/radial-chart/README.md +58 -0
- package/src/components/radial-chart/element.ts +30 -0
- package/src/components/radial-chart/engine.ts +317 -0
- package/src/components/radial-chart/radial-chart.css +107 -0
- package/src/components/radial-chart/react.tsx +14 -0
- package/src/components/sankey-chart/README.md +48 -0
- package/src/components/sankey-chart/element.ts +31 -0
- package/src/components/sankey-chart/engine.ts +401 -0
- package/src/components/sankey-chart/react.tsx +14 -0
- package/src/components/sankey-chart/sankey-chart.css +60 -0
- package/src/components/scroll-island/README.md +44 -0
- package/src/components/scroll-island/element.ts +16 -0
- package/src/components/scroll-island/engine.ts +274 -0
- package/src/components/scroll-island/react.tsx +26 -0
- package/src/components/scroll-island/scroll-island.css +113 -0
- package/src/components/select/README.md +34 -0
- package/src/components/select/element.ts +26 -0
- package/src/components/select/engine.ts +267 -0
- package/src/components/select/react.tsx +33 -0
- package/src/components/select/select.css +135 -0
- package/src/components/toolbar/README.md +46 -0
- package/src/components/toolbar/element.ts +30 -0
- package/src/components/toolbar/engine.ts +230 -0
- package/src/components/toolbar/react.tsx +34 -0
- package/src/components/toolbar/toolbar.css +121 -0
- package/src/components/tree-view/element.ts +1 -1
- package/src/components/tree-view/engine.ts +174 -0
- package/src/components/tree-view/react.tsx +7 -3
- package/src/components/tree-view/tree-view.css +79 -0
- package/src/components/voice-dots/README.md +48 -0
- package/src/components/voice-dots/element.ts +45 -0
- package/src/components/voice-dots/engine.ts +243 -0
- package/src/components/voice-dots/react.tsx +52 -0
- package/src/components/voice-dots/voice-dots.css +69 -0
- package/src/elements/index.ts +39 -0
- package/src/index.ts +16 -0
- package/src/motion/liquid.ts +14 -4
- package/src/react/index.ts +14 -1
- package/dist/chunk-6P4P7NU6.js +0 -1401
- package/dist/chunk-6P4P7NU6.js.map +0 -1
- package/dist/chunk-CU6SXW3E.js.map +0 -1
- package/dist/store-DjkTBX7f.d.ts +0 -195
- /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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
385
|
-
const
|
|
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.
|
|
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
|
|
425
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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 (
|
|
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
|
|
458
|
-
const
|
|
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:
|
|
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 =
|
|
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) ?
|
|
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
|
-
|
|
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
|
|
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:
|
|
679
|
-
duration:
|
|
680
|
-
ease:
|
|
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 => ({
|