@advui/charts 0.5.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/LICENSE +49 -0
- package/README.md +41 -0
- package/dist/charts/ChartFrame.d.ts +75 -0
- package/dist/charts/ChartFrame.d.ts.map +1 -0
- package/dist/charts/ChartFrame.js +100 -0
- package/dist/charts/ChartFrame.js.map +1 -0
- package/dist/charts/LinePlot.d.ts +32 -0
- package/dist/charts/LinePlot.d.ts.map +1 -0
- package/dist/charts/LinePlot.js +93 -0
- package/dist/charts/LinePlot.js.map +1 -0
- package/dist/charts/cartesian.d.ts +35 -0
- package/dist/charts/cartesian.d.ts.map +1 -0
- package/dist/charts/cartesian.js +23 -0
- package/dist/charts/cartesian.js.map +1 -0
- package/dist/charts/format.d.ts +7 -0
- package/dist/charts/format.d.ts.map +1 -0
- package/dist/charts/format.js +12 -0
- package/dist/charts/format.js.map +1 -0
- package/dist/charts/index.d.ts +7 -0
- package/dist/charts/index.d.ts.map +1 -0
- package/dist/charts/index.js +7 -0
- package/dist/charts/index.js.map +1 -0
- package/dist/charts/scale.d.ts +20 -0
- package/dist/charts/scale.d.ts.map +1 -0
- package/dist/charts/scale.js +75 -0
- package/dist/charts/scale.js.map +1 -0
- package/dist/charts/svg.d.ts +51 -0
- package/dist/charts/svg.d.ts.map +1 -0
- package/dist/charts/svg.js +13 -0
- package/dist/charts/svg.js.map +1 -0
- package/dist/charts/svg.native.d.ts +50 -0
- package/dist/charts/svg.native.d.ts.map +1 -0
- package/dist/charts/svg.native.js +14 -0
- package/dist/charts/svg.native.js.map +1 -0
- package/dist/components/area-chart/AreaChart.d.ts +11 -0
- package/dist/components/area-chart/AreaChart.d.ts.map +1 -0
- package/dist/components/area-chart/AreaChart.js +10 -0
- package/dist/components/area-chart/AreaChart.js.map +1 -0
- package/dist/components/area-chart/index.d.ts +2 -0
- package/dist/components/area-chart/index.d.ts.map +1 -0
- package/dist/components/area-chart/index.js +2 -0
- package/dist/components/area-chart/index.js.map +1 -0
- package/dist/components/bar-chart/BarChart.d.ts +31 -0
- package/dist/components/bar-chart/BarChart.d.ts.map +1 -0
- package/dist/components/bar-chart/BarChart.js +114 -0
- package/dist/components/bar-chart/BarChart.js.map +1 -0
- package/dist/components/bar-chart/index.d.ts +2 -0
- package/dist/components/bar-chart/index.d.ts.map +1 -0
- package/dist/components/bar-chart/index.js +2 -0
- package/dist/components/bar-chart/index.js.map +1 -0
- package/dist/components/line-chart/LineChart.d.ts +8 -0
- package/dist/components/line-chart/LineChart.d.ts.map +1 -0
- package/dist/components/line-chart/LineChart.js +10 -0
- package/dist/components/line-chart/LineChart.js.map +1 -0
- package/dist/components/line-chart/index.d.ts +2 -0
- package/dist/components/line-chart/index.d.ts.map +1 -0
- package/dist/components/line-chart/index.js +2 -0
- package/dist/components/line-chart/index.js.map +1 -0
- package/dist/components/pie-chart/PieChart.d.ts +37 -0
- package/dist/components/pie-chart/PieChart.d.ts.map +1 -0
- package/dist/components/pie-chart/PieChart.js +89 -0
- package/dist/components/pie-chart/PieChart.js.map +1 -0
- package/dist/components/pie-chart/index.d.ts +2 -0
- package/dist/components/pie-chart/index.d.ts.map +1 -0
- package/dist/components/pie-chart/index.js +2 -0
- package/dist/components/pie-chart/index.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/package.json +93 -0
- package/src/charts/ChartFrame.tsx +303 -0
- package/src/charts/LinePlot.tsx +216 -0
- package/src/charts/cartesian.tsx +99 -0
- package/src/charts/format.ts +14 -0
- package/src/charts/index.ts +16 -0
- package/src/charts/scale.ts +91 -0
- package/src/charts/svg.native.tsx +62 -0
- package/src/charts/svg.tsx +57 -0
- package/src/components/area-chart/AreaChart.tsx +14 -0
- package/src/components/area-chart/index.ts +1 -0
- package/src/components/bar-chart/BarChart.tsx +300 -0
- package/src/components/bar-chart/index.ts +1 -0
- package/src/components/line-chart/LineChart.tsx +11 -0
- package/src/components/line-chart/index.ts +1 -0
- package/src/components/pie-chart/PieChart.tsx +192 -0
- package/src/components/pie-chart/index.ts +1 -0
- package/src/index.ts +6 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useIconColor } from '@advui/icons';
|
|
3
|
+
import { ChartFrame, G, Path, Svg, SvgText, arcPath, formatFull, useChartColors, useChartInk, } from '../../charts';
|
|
4
|
+
const percent = (part, total) => total > 0 ? `${Math.round((part / total) * 1000) / 10}%` : '0%';
|
|
5
|
+
/**
|
|
6
|
+
* Part-to-whole at a glance, for up to six parts. Each slice is named in the
|
|
7
|
+
* legend with its share; the tooltip (hover, tap or arrow keys) and the
|
|
8
|
+
* table view give the exact values. For close values, a bar chart reads better.
|
|
9
|
+
*/
|
|
10
|
+
export function PieChart({ title, description, data, variant = 'donut', maxSlices = 6, height = 240, width, valueFormatter = formatFull, text: textProp, labels, }) {
|
|
11
|
+
const text = {
|
|
12
|
+
label: 'Label',
|
|
13
|
+
value: 'Value',
|
|
14
|
+
share: 'Share',
|
|
15
|
+
other: 'Other',
|
|
16
|
+
total: 'Total',
|
|
17
|
+
...textProp,
|
|
18
|
+
};
|
|
19
|
+
const colors = useChartColors();
|
|
20
|
+
const ink = useChartInk('$background');
|
|
21
|
+
const foreground = useIconColor('$foreground') ?? 'currentColor';
|
|
22
|
+
// Largest first; past the limit, the tail folds into one "Other" slice.
|
|
23
|
+
const positive = data.filter((d) => d.value > 0).sort((a, b) => b.value - a.value);
|
|
24
|
+
const slices = positive.length > maxSlices
|
|
25
|
+
? [
|
|
26
|
+
...positive.slice(0, maxSlices - 1),
|
|
27
|
+
{
|
|
28
|
+
label: text.other,
|
|
29
|
+
value: positive.slice(maxSlices - 1).reduce((sum, d) => sum + d.value, 0),
|
|
30
|
+
},
|
|
31
|
+
]
|
|
32
|
+
: positive;
|
|
33
|
+
const folded = positive.length > maxSlices;
|
|
34
|
+
const total = slices.reduce((sum, d) => sum + d.value, 0);
|
|
35
|
+
const colorOf = (i) => (folded && i === slices.length - 1 ? ink.axis : colors[i % 8]);
|
|
36
|
+
let angle = 0;
|
|
37
|
+
const arcs = slices.map((slice, i) => {
|
|
38
|
+
const start = angle;
|
|
39
|
+
angle += total > 0 ? (slice.value / total) * Math.PI * 2 : 0;
|
|
40
|
+
return { ...slice, start, end: angle, color: colorOf(i) };
|
|
41
|
+
});
|
|
42
|
+
const geometry = (w) => {
|
|
43
|
+
const outer = Math.min(w, height) / 2 - 8;
|
|
44
|
+
return { cx: w / 2, cy: height / 2, outer, inner: variant === 'donut' ? outer * 0.62 : 0 };
|
|
45
|
+
};
|
|
46
|
+
return (_jsx(ChartFrame, { title: title, description: description, legend: arcs.map((arc) => ({
|
|
47
|
+
label: arc.label,
|
|
48
|
+
color: arc.color,
|
|
49
|
+
shape: 'square',
|
|
50
|
+
detail: percent(arc.value, total),
|
|
51
|
+
})), height: height, width: width, count: arcs.length, hitTest: (x, y, w) => {
|
|
52
|
+
const { cx, cy, outer, inner } = geometry(w);
|
|
53
|
+
const dx = x - cx;
|
|
54
|
+
const dy = y - cy;
|
|
55
|
+
const r = Math.hypot(dx, dy);
|
|
56
|
+
if (r > outer + 8 || r < inner)
|
|
57
|
+
return null;
|
|
58
|
+
const a = (Math.atan2(dx, -dy) + Math.PI * 2) % (Math.PI * 2);
|
|
59
|
+
const found = arcs.findIndex((arc) => a >= arc.start && a < arc.end);
|
|
60
|
+
return found >= 0 ? found : null;
|
|
61
|
+
}, anchor: (i, w) => {
|
|
62
|
+
const { cx, outer } = geometry(w);
|
|
63
|
+
const mid = (arcs[i].start + arcs[i].end) / 2;
|
|
64
|
+
return { x: cx + outer * Math.sin(mid), y: 0 };
|
|
65
|
+
}, tooltip: (i) => ({
|
|
66
|
+
title: arcs[i].label,
|
|
67
|
+
rows: [
|
|
68
|
+
{
|
|
69
|
+
label: percent(arcs[i].value, total),
|
|
70
|
+
value: valueFormatter(arcs[i].value),
|
|
71
|
+
color: arcs[i].color,
|
|
72
|
+
shape: 'square',
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
}), table: {
|
|
76
|
+
columns: [text.label, text.value, text.share],
|
|
77
|
+
rows: [
|
|
78
|
+
...arcs.map((arc) => [arc.label, valueFormatter(arc.value), percent(arc.value, total)]),
|
|
79
|
+
[text.total, valueFormatter(total), '100%'],
|
|
80
|
+
],
|
|
81
|
+
numeric: [false, true, true],
|
|
82
|
+
}, labels: labels, children: (w, active) => {
|
|
83
|
+
const { cx, cy, outer, inner } = geometry(w);
|
|
84
|
+
// A 2px gap between slices, as angle at the outer edge.
|
|
85
|
+
const gap = arcs.length > 1 ? 2 / outer : 0;
|
|
86
|
+
return (_jsxs(Svg, { width: w, height: height, children: [arcs.map((arc, i) => (_jsx(G, { opacity: active == null || active === i ? 1 : 0.55, children: _jsx(Path, { d: arcPath(cx, cy, active === i ? outer + 4 : outer, inner, arc.start + gap / 2, Math.max(arc.start + gap / 2, arc.end - gap / 2)), fill: arc.color }) }, arc.label))), variant === 'donut' ? (_jsxs(_Fragment, { children: [_jsx(SvgText, { x: cx, y: cy - 6, fill: foreground, fontSize: 20, fontWeight: "600", textAnchor: "middle", children: valueFormatter(total) }), _jsx(SvgText, { x: cx, y: cy + 16, fill: ink.text, fontSize: 12, textAnchor: "middle", children: text.total })] })) : null] }));
|
|
87
|
+
} }));
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=PieChart.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PieChart.js","sourceRoot":"","sources":["../../../src/components/pie-chart/PieChart.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/pie-chart/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/components/pie-chart/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAqC,MAAM,YAAY,CAAA"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,cAAc,yBAAyB,CAAA;AACvC,cAAc,wBAAwB,CAAA;AACtC,cAAc,yBAAyB,CAAA;AACvC,cAAc,wBAAwB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Chart components. The shared internals in ./charts (frame, legend, tooltip,
|
|
2
|
+
// table view, scales and the SVG layer) are not part of the public API.
|
|
3
|
+
export * from './components/area-chart';
|
|
4
|
+
export * from './components/bar-chart';
|
|
5
|
+
export * from './components/line-chart';
|
|
6
|
+
export * from './components/pie-chart';
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,wEAAwE;AACxE,cAAc,yBAAyB,CAAA;AACvC,cAAc,wBAAwB,CAAA;AACtC,cAAc,yBAAyB,CAAA;AACvC,cAAc,wBAAwB,CAAA"}
|
package/package.json
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@advui/charts",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Bar, line, area and pie charts for Adv UI: SVG on web, react-native-svg on iOS and Android, with a legend, tooltips and a table view.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/zinmin232/advui.git",
|
|
9
|
+
"directory": "packages/charts"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/zinmin232/advui/tree/main/packages/charts#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/zinmin232/advui/issues"
|
|
14
|
+
},
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"react-native": "./src/index.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"default": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./package.json": "./package.json"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"src",
|
|
29
|
+
"!src/**/*.test.*",
|
|
30
|
+
"!src/**/examples",
|
|
31
|
+
"!src/**/*.meta.ts"
|
|
32
|
+
],
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public",
|
|
35
|
+
"react-native": "./dist/index.js"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@advui/icons": "0.5.0"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@advui/core": "^0.5.0",
|
|
42
|
+
"@advui/data": "^0.5.0",
|
|
43
|
+
"react": ">=19",
|
|
44
|
+
"react-native": "*",
|
|
45
|
+
"react-native-svg": ">=15",
|
|
46
|
+
"tamagui": "^2.7.7"
|
|
47
|
+
},
|
|
48
|
+
"peerDependenciesMeta": {
|
|
49
|
+
"react-native-svg": {
|
|
50
|
+
"optional": true
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@jest/globals": "^29.7.0",
|
|
55
|
+
"@testing-library/jest-dom": "^7.0.1",
|
|
56
|
+
"@testing-library/react": "^16.3.3",
|
|
57
|
+
"@testing-library/react-native": "^14.0.1",
|
|
58
|
+
"@testing-library/user-event": "^14.6.7",
|
|
59
|
+
"@types/node": "^22.15.0",
|
|
60
|
+
"@types/react": "~19.2.2",
|
|
61
|
+
"@types/react-dom": "~19.2.2",
|
|
62
|
+
"@advui/data": "0.5.0",
|
|
63
|
+
"@advui/eslint-config": "0.1.0",
|
|
64
|
+
"@advui/tsconfig": "0.1.0",
|
|
65
|
+
"@vitejs/plugin-react": "^6.1.1",
|
|
66
|
+
"babel-preset-expo": "~57.0.13",
|
|
67
|
+
"expo": "~57.0.25",
|
|
68
|
+
"happy-dom": "^20.14.5",
|
|
69
|
+
"jest": "^29.7.0",
|
|
70
|
+
"jest-expo": "~57.0.5",
|
|
71
|
+
"react": "19.2.3",
|
|
72
|
+
"react-dom": "19.2.3",
|
|
73
|
+
"react-native": "0.86.3",
|
|
74
|
+
"react-native-safe-area-context": "~5.7.0",
|
|
75
|
+
"react-native-svg": "15.15.4",
|
|
76
|
+
"react-native-web": "~0.21.0",
|
|
77
|
+
"tamagui": "2.7.7",
|
|
78
|
+
"test-renderer": "~1.2.0",
|
|
79
|
+
"vite": "^8.3.1",
|
|
80
|
+
"vitest": "^5.0.2",
|
|
81
|
+
"@advui/core": "0.5.0",
|
|
82
|
+
"@advui/theme": "0.5.0"
|
|
83
|
+
},
|
|
84
|
+
"scripts": {
|
|
85
|
+
"build": "tsc -p tsconfig.build.json",
|
|
86
|
+
"typecheck": "tsc --noEmit",
|
|
87
|
+
"lint": "eslint .",
|
|
88
|
+
"test": "vitest run",
|
|
89
|
+
"test:watch": "vitest",
|
|
90
|
+
"clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
91
|
+
"test:native": "jest --config jest.native.config.js"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { useIconColor } from '@advui/icons'
|
|
2
|
+
import { type ReactNode, useId, useRef, useState } from 'react'
|
|
3
|
+
import { type GetProps, View, VisuallyHidden, XStack, isWeb } from 'tamagui'
|
|
4
|
+
import { Button, Text } from '@advui/core'
|
|
5
|
+
import { Table } from '@advui/data'
|
|
6
|
+
|
|
7
|
+
/** One series (a line, a set of bars): the data field it reads and its name. */
|
|
8
|
+
export interface ChartSeries {
|
|
9
|
+
key: string
|
|
10
|
+
label: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ChartLabels {
|
|
14
|
+
showTable: string
|
|
15
|
+
hideTable: string
|
|
16
|
+
/** Read before the arrow-key hint on the focused plot. */
|
|
17
|
+
keyboardHint: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const defaultChartLabels: ChartLabels = {
|
|
21
|
+
showTable: 'Show table',
|
|
22
|
+
hideTable: 'Hide table',
|
|
23
|
+
keyboardHint: 'Use the arrow keys to read each value.',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The chart palette, in its fixed order, as resolved colors. */
|
|
27
|
+
export function useChartColors(): string[] {
|
|
28
|
+
// Hooks in a fixed order: one per slot.
|
|
29
|
+
return [
|
|
30
|
+
useIconColor('$chart1'),
|
|
31
|
+
useIconColor('$chart2'),
|
|
32
|
+
useIconColor('$chart3'),
|
|
33
|
+
useIconColor('$chart4'),
|
|
34
|
+
useIconColor('$chart5'),
|
|
35
|
+
useIconColor('$chart6'),
|
|
36
|
+
useIconColor('$chart7'),
|
|
37
|
+
useIconColor('$chart8'),
|
|
38
|
+
].map((color) => color ?? 'currentColor')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Axis text, gridlines and the surface, as resolved colors. */
|
|
42
|
+
export function useChartInk(surface: string) {
|
|
43
|
+
return {
|
|
44
|
+
text: useIconColor('$mutedForeground') ?? 'currentColor',
|
|
45
|
+
grid: useIconColor('$border') ?? 'currentColor',
|
|
46
|
+
axis: useIconColor('$borderStrong') ?? 'currentColor',
|
|
47
|
+
surface: useIconColor(surface) ?? 'transparent',
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface LegendItem {
|
|
52
|
+
label: string
|
|
53
|
+
color: string
|
|
54
|
+
/** Mirrors the mark: a line for line charts, a square for bars, areas and slices. */
|
|
55
|
+
shape: 'line' | 'square'
|
|
56
|
+
/** Extra text after the label, e.g. a slice's share. */
|
|
57
|
+
detail?: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface TooltipContent {
|
|
61
|
+
title: string
|
|
62
|
+
rows: { label: string; value: string; color: string; shape: LegendItem['shape'] }[]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface ChartFrameProps {
|
|
66
|
+
/** Names the chart (its accessible name and heading). */
|
|
67
|
+
title: string
|
|
68
|
+
description?: string
|
|
69
|
+
legend: LegendItem[]
|
|
70
|
+
/** Plot height in pixels, including the axes. */
|
|
71
|
+
height: number
|
|
72
|
+
/** Fixed plot width; by default the chart fills its container. */
|
|
73
|
+
width?: number
|
|
74
|
+
/** Number of positions the arrow keys step through. */
|
|
75
|
+
count: number
|
|
76
|
+
/** The position under a point in the plot, or null. */
|
|
77
|
+
hitTest: (x: number, y: number, width: number) => number | null
|
|
78
|
+
tooltip: (index: number) => TooltipContent | null
|
|
79
|
+
/** Where the tooltip points, in plot coordinates. */
|
|
80
|
+
anchor: (index: number, width: number) => { x: number; y: number }
|
|
81
|
+
/** Draws the plot at the measured width; `active` is the hovered position. */
|
|
82
|
+
children: (width: number, active: number | null) => ReactNode
|
|
83
|
+
table: { columns: string[]; rows: string[][]; numeric: boolean[] }
|
|
84
|
+
labels?: Partial<ChartLabels>
|
|
85
|
+
frameProps?: GetProps<typeof View>
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The frame every chart shares: title, legend, a measured plot with a
|
|
90
|
+
* tooltip (hover, tap or arrow keys), and a table view of the same data.
|
|
91
|
+
*/
|
|
92
|
+
export function ChartFrame({
|
|
93
|
+
title,
|
|
94
|
+
description,
|
|
95
|
+
legend,
|
|
96
|
+
height,
|
|
97
|
+
width: fixedWidth,
|
|
98
|
+
count,
|
|
99
|
+
hitTest,
|
|
100
|
+
tooltip,
|
|
101
|
+
anchor,
|
|
102
|
+
children,
|
|
103
|
+
table,
|
|
104
|
+
labels: labelsProp,
|
|
105
|
+
frameProps,
|
|
106
|
+
}: ChartFrameProps) {
|
|
107
|
+
const labels = { ...defaultChartLabels, ...labelsProp }
|
|
108
|
+
const [measured, setMeasured] = useState(0)
|
|
109
|
+
const width = fixedWidth ?? measured
|
|
110
|
+
const [active, setActive] = useState<number | null>(null)
|
|
111
|
+
const [showTable, setShowTable] = useState(false)
|
|
112
|
+
const titleId = useId()
|
|
113
|
+
const tableId = useId()
|
|
114
|
+
const plotRef = useRef<HTMLElement | null>(null)
|
|
115
|
+
const content = active != null ? tooltip(active) : null
|
|
116
|
+
|
|
117
|
+
const pointAt = (clientX: number, clientY: number) => {
|
|
118
|
+
const rect = plotRef.current?.getBoundingClientRect()
|
|
119
|
+
if (!rect) return
|
|
120
|
+
setActive(hitTest(clientX - rect.left, clientY - rect.top, width))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const onKeyDown = (event: { key: string; preventDefault: () => void }) => {
|
|
124
|
+
const last = count - 1
|
|
125
|
+
const moves: Record<string, number> = {
|
|
126
|
+
ArrowRight: Math.min(last, (active ?? -1) + 1),
|
|
127
|
+
ArrowDown: Math.min(last, (active ?? -1) + 1),
|
|
128
|
+
ArrowLeft: Math.max(0, (active ?? count) - 1),
|
|
129
|
+
ArrowUp: Math.max(0, (active ?? count) - 1),
|
|
130
|
+
Home: 0,
|
|
131
|
+
End: last,
|
|
132
|
+
}
|
|
133
|
+
if (event.key in moves && count > 0) {
|
|
134
|
+
event.preventDefault()
|
|
135
|
+
setActive(moves[event.key]!)
|
|
136
|
+
} else if (event.key === 'Escape') setActive(null)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const point = active != null && width > 0 ? anchor(active, width) : null
|
|
140
|
+
const summary = content
|
|
141
|
+
? `${content.title}: ${content.rows.map((r) => `${r.label} ${r.value}`).join(', ')}`
|
|
142
|
+
: ''
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<View
|
|
146
|
+
render="figure"
|
|
147
|
+
margin={0}
|
|
148
|
+
gap="$3"
|
|
149
|
+
width="100%"
|
|
150
|
+
aria-labelledby={titleId}
|
|
151
|
+
{...frameProps}
|
|
152
|
+
>
|
|
153
|
+
<View gap="$1">
|
|
154
|
+
<Text id={titleId} size="sm" weight="semibold">
|
|
155
|
+
{title}
|
|
156
|
+
</Text>
|
|
157
|
+
{description ? (
|
|
158
|
+
<Text size="xs" tone="muted">
|
|
159
|
+
{description}
|
|
160
|
+
</Text>
|
|
161
|
+
) : null}
|
|
162
|
+
</View>
|
|
163
|
+
|
|
164
|
+
{legend.length > 0 ? (
|
|
165
|
+
<XStack flexWrap="wrap" columnGap="$4" rowGap="$1.5">
|
|
166
|
+
{legend.map((item) => (
|
|
167
|
+
<XStack key={item.label} alignItems="center" gap="$1.5">
|
|
168
|
+
<View
|
|
169
|
+
aria-hidden
|
|
170
|
+
backgroundColor={item.color as never}
|
|
171
|
+
width={item.shape === 'line' ? '$3' : '$2.5'}
|
|
172
|
+
height={item.shape === 'line' ? 2 : '$2.5'}
|
|
173
|
+
borderRadius={item.shape === 'line' ? '$full' : '$xs'}
|
|
174
|
+
/>
|
|
175
|
+
<Text size="xs" tone="muted">
|
|
176
|
+
{item.label}
|
|
177
|
+
{item.detail ? ` · ${item.detail}` : ''}
|
|
178
|
+
</Text>
|
|
179
|
+
</XStack>
|
|
180
|
+
))}
|
|
181
|
+
</XStack>
|
|
182
|
+
) : null}
|
|
183
|
+
|
|
184
|
+
<View
|
|
185
|
+
ref={plotRef as never}
|
|
186
|
+
position="relative"
|
|
187
|
+
height={height}
|
|
188
|
+
width={fixedWidth ?? '100%'}
|
|
189
|
+
onLayout={(event: { nativeEvent: { layout: { width: number } } }) =>
|
|
190
|
+
setMeasured(Math.floor(event.nativeEvent.layout.width))
|
|
191
|
+
}
|
|
192
|
+
// The plot is one focusable image; the arrow keys read it point by point.
|
|
193
|
+
role="img"
|
|
194
|
+
aria-label={`${title}. ${description ? `${description}. ` : ''}${labels.keyboardHint}`}
|
|
195
|
+
{...(isWeb
|
|
196
|
+
? {
|
|
197
|
+
tabIndex: 0,
|
|
198
|
+
onKeyDown,
|
|
199
|
+
onBlur: () => setActive(null),
|
|
200
|
+
onMouseMove: (event: { clientX: number; clientY: number }) =>
|
|
201
|
+
pointAt(event.clientX, event.clientY),
|
|
202
|
+
onMouseLeave: () => setActive(null),
|
|
203
|
+
}
|
|
204
|
+
: {
|
|
205
|
+
accessible: true,
|
|
206
|
+
// Native has no live region here: the reading becomes the plot's value.
|
|
207
|
+
...(summary && { accessibilityValue: { text: summary } }),
|
|
208
|
+
onPress: (event: { nativeEvent: { locationX: number; locationY: number } }) =>
|
|
209
|
+
setActive(hitTest(event.nativeEvent.locationX, event.nativeEvent.locationY, width)),
|
|
210
|
+
})}
|
|
211
|
+
borderRadius="$sm"
|
|
212
|
+
focusVisibleStyle={{
|
|
213
|
+
outlineColor: '$ring',
|
|
214
|
+
outlineStyle: 'solid',
|
|
215
|
+
outlineWidth: 2,
|
|
216
|
+
outlineOffset: 4,
|
|
217
|
+
}}
|
|
218
|
+
>
|
|
219
|
+
{width > 0 ? children(width, active) : null}
|
|
220
|
+
{content && point ? (
|
|
221
|
+
<View
|
|
222
|
+
aria-hidden
|
|
223
|
+
pointerEvents="none"
|
|
224
|
+
position="absolute"
|
|
225
|
+
top={0}
|
|
226
|
+
{...(point.x < width / 2 ? { left: point.x + 12 } : { right: width - point.x + 12 })}
|
|
227
|
+
backgroundColor="$popover"
|
|
228
|
+
borderWidth={1}
|
|
229
|
+
borderColor="$border"
|
|
230
|
+
borderRadius="$md"
|
|
231
|
+
paddingHorizontal="$2.5"
|
|
232
|
+
paddingVertical="$2"
|
|
233
|
+
gap="$1"
|
|
234
|
+
minWidth="$32"
|
|
235
|
+
zIndex={1}
|
|
236
|
+
>
|
|
237
|
+
<Text size="xs" tone="muted">
|
|
238
|
+
{content.title}
|
|
239
|
+
</Text>
|
|
240
|
+
{content.rows.map((row) => (
|
|
241
|
+
<XStack key={row.label} alignItems="center" gap="$2">
|
|
242
|
+
<View
|
|
243
|
+
backgroundColor={row.color as never}
|
|
244
|
+
width="$2.5"
|
|
245
|
+
height={row.shape === 'line' ? 2 : '$2.5'}
|
|
246
|
+
borderRadius={row.shape === 'line' ? '$full' : '$xs'}
|
|
247
|
+
/>
|
|
248
|
+
<Text size="sm" weight="semibold">
|
|
249
|
+
{row.value}
|
|
250
|
+
</Text>
|
|
251
|
+
<Text size="xs" tone="muted" flexShrink={1}>
|
|
252
|
+
{row.label}
|
|
253
|
+
</Text>
|
|
254
|
+
</XStack>
|
|
255
|
+
))}
|
|
256
|
+
</View>
|
|
257
|
+
) : null}
|
|
258
|
+
</View>
|
|
259
|
+
{/* Outside the plot: an img's contents are hidden from assistive technology. */}
|
|
260
|
+
{isWeb ? <VisuallyHidden aria-live="polite">{summary}</VisuallyHidden> : null}
|
|
261
|
+
|
|
262
|
+
<View alignItems="flex-start" gap="$2">
|
|
263
|
+
<Button
|
|
264
|
+
size="sm"
|
|
265
|
+
variant="ghost"
|
|
266
|
+
// Lines the label up with the title above.
|
|
267
|
+
marginLeft="$-3"
|
|
268
|
+
aria-expanded={showTable}
|
|
269
|
+
{...(isWeb && { 'aria-controls': tableId })}
|
|
270
|
+
onPress={() => setShowTable(!showTable)}
|
|
271
|
+
>
|
|
272
|
+
{showTable ? labels.hideTable : labels.showTable}
|
|
273
|
+
</Button>
|
|
274
|
+
{showTable ? (
|
|
275
|
+
<View id={tableId} width="100%">
|
|
276
|
+
<Table aria-label={title} size="sm" minWidth={Math.max(320, table.columns.length * 96)}>
|
|
277
|
+
<Table.Header>
|
|
278
|
+
<Table.Row>
|
|
279
|
+
{table.columns.map((column, i) => (
|
|
280
|
+
<Table.Head key={column} align={table.numeric[i] ? 'end' : 'start'}>
|
|
281
|
+
{column}
|
|
282
|
+
</Table.Head>
|
|
283
|
+
))}
|
|
284
|
+
</Table.Row>
|
|
285
|
+
</Table.Header>
|
|
286
|
+
<Table.Body>
|
|
287
|
+
{table.rows.map((row, r) => (
|
|
288
|
+
<Table.Row key={r}>
|
|
289
|
+
{row.map((cell, i) => (
|
|
290
|
+
<Table.Cell key={i} align={table.numeric[i] ? 'end' : 'start'}>
|
|
291
|
+
{cell}
|
|
292
|
+
</Table.Cell>
|
|
293
|
+
))}
|
|
294
|
+
</Table.Row>
|
|
295
|
+
))}
|
|
296
|
+
</Table.Body>
|
|
297
|
+
</Table>
|
|
298
|
+
</View>
|
|
299
|
+
) : null}
|
|
300
|
+
</View>
|
|
301
|
+
</View>
|
|
302
|
+
)
|
|
303
|
+
}
|