@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,300 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CategoryLabels,
|
|
3
|
+
ChartFrame,
|
|
4
|
+
type ChartLabels,
|
|
5
|
+
type ChartSeries,
|
|
6
|
+
G,
|
|
7
|
+
Line,
|
|
8
|
+
Path,
|
|
9
|
+
Svg,
|
|
10
|
+
SvgText,
|
|
11
|
+
ValueGrid,
|
|
12
|
+
bandScale,
|
|
13
|
+
barPath,
|
|
14
|
+
cartesianPadding,
|
|
15
|
+
formatCompact,
|
|
16
|
+
formatFull,
|
|
17
|
+
linearScale,
|
|
18
|
+
niceTicks,
|
|
19
|
+
numberAt,
|
|
20
|
+
useChartColors,
|
|
21
|
+
useChartInk,
|
|
22
|
+
} from '../../charts'
|
|
23
|
+
|
|
24
|
+
const MAX_BAR = 24
|
|
25
|
+
const GAP = 2
|
|
26
|
+
const RADIUS = 4
|
|
27
|
+
|
|
28
|
+
export interface BarChartProps {
|
|
29
|
+
/** Names the chart: its heading and accessible name. */
|
|
30
|
+
title: string
|
|
31
|
+
description?: string
|
|
32
|
+
/** One object per category, e.g. `{ month: 'Jan', health: 120, wash: 80 }`. */
|
|
33
|
+
data: Record<string, unknown>[]
|
|
34
|
+
/** The field holding each row's category name. */
|
|
35
|
+
index: string
|
|
36
|
+
/** Heading of the category column in the table view. Default: `index`. */
|
|
37
|
+
indexLabel?: string
|
|
38
|
+
/** The numeric fields to plot, in color order. */
|
|
39
|
+
series: ChartSeries[]
|
|
40
|
+
/** `vertical` draws columns; `horizontal` bars suit long category names. */
|
|
41
|
+
layout?: 'vertical' | 'horizontal'
|
|
42
|
+
/** Stacks the series in one bar per category (part-to-whole). */
|
|
43
|
+
stacked?: boolean
|
|
44
|
+
/** Height in pixels. Default: 240, or 36 per category for horizontal bars. */
|
|
45
|
+
height?: number
|
|
46
|
+
/** Fixed width; by default the chart fills its container. */
|
|
47
|
+
width?: number
|
|
48
|
+
/** Formats values in the tooltip and table. Axis ticks are always compact (1.2K). */
|
|
49
|
+
valueFormatter?: (value: number) => string
|
|
50
|
+
labels?: Partial<ChartLabels>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Compares values across categories as columns or bars, grouped or stacked.
|
|
55
|
+
* Includes a legend, a tooltip (hover, tap or arrow keys) and a table view.
|
|
56
|
+
*/
|
|
57
|
+
export function BarChart({
|
|
58
|
+
title,
|
|
59
|
+
description,
|
|
60
|
+
data,
|
|
61
|
+
index,
|
|
62
|
+
indexLabel = index,
|
|
63
|
+
series,
|
|
64
|
+
layout = 'vertical',
|
|
65
|
+
stacked = false,
|
|
66
|
+
height: heightProp,
|
|
67
|
+
width,
|
|
68
|
+
valueFormatter = formatFull,
|
|
69
|
+
labels,
|
|
70
|
+
}: BarChartProps) {
|
|
71
|
+
const colors = useChartColors()
|
|
72
|
+
const ink = useChartInk('$background')
|
|
73
|
+
const horizontal = layout === 'horizontal'
|
|
74
|
+
const height = heightProp ?? (horizontal ? Math.max(120, data.length * 36 + 32) : 240)
|
|
75
|
+
const categories = data.map((row) => String(row[index] ?? ''))
|
|
76
|
+
const values = data.map((row) => series.map((s) => numberAt(row, s.key)))
|
|
77
|
+
|
|
78
|
+
// Stacks sum positives up and negatives down; groups need each value.
|
|
79
|
+
const extents = values.flatMap((row) =>
|
|
80
|
+
stacked
|
|
81
|
+
? [
|
|
82
|
+
row.reduce<number>((sum, v) => sum + Math.max(0, v ?? 0), 0),
|
|
83
|
+
row.reduce<number>((sum, v) => sum + Math.min(0, v ?? 0), 0),
|
|
84
|
+
]
|
|
85
|
+
: row.map((v) => v ?? 0),
|
|
86
|
+
)
|
|
87
|
+
const ticks = niceTicks(Math.min(0, ...extents), Math.max(0, ...extents))
|
|
88
|
+
const domain: [number, number] = [ticks[0]!, ticks.at(-1)!]
|
|
89
|
+
|
|
90
|
+
const categoryWidth = horizontal
|
|
91
|
+
? Math.min(160, Math.max(...categories.map((c) => c.length), 1) * 7 + 12)
|
|
92
|
+
: 0
|
|
93
|
+
const pad = horizontal
|
|
94
|
+
? { top: 8, right: 16, bottom: 24, left: categoryWidth }
|
|
95
|
+
: cartesianPadding(ticks)
|
|
96
|
+
|
|
97
|
+
const layoutAt = (w: number) => {
|
|
98
|
+
const valueScale = horizontal
|
|
99
|
+
? linearScale(domain, [pad.left, w - pad.right])
|
|
100
|
+
: linearScale(domain, [height - pad.bottom, pad.top])
|
|
101
|
+
const band = horizontal
|
|
102
|
+
? bandScale(data.length, [pad.top, height - pad.bottom])
|
|
103
|
+
: bandScale(data.length, [pad.left, w - pad.right])
|
|
104
|
+
const k = stacked ? 1 : series.length
|
|
105
|
+
// Bars never fill their slot: at most 24px, and the rest of the band is air.
|
|
106
|
+
const thickness = Math.max(2, Math.min(MAX_BAR, (band.bandwidth * 0.7 - GAP * (k - 1)) / k))
|
|
107
|
+
const group = k * thickness + (k - 1) * GAP
|
|
108
|
+
return { valueScale, band, thickness, group }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const hitTest = (x: number, y: number, w: number) => {
|
|
112
|
+
const { band } = layoutAt(w)
|
|
113
|
+
const position = horizontal ? y : x
|
|
114
|
+
const i = Math.floor((position - (horizontal ? pad.top : pad.left)) / band.bandwidth)
|
|
115
|
+
return i >= 0 && i < data.length ? i : null
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const format = (value: number | null) => (value == null ? '–' : valueFormatter(value))
|
|
119
|
+
const legend =
|
|
120
|
+
series.length > 1
|
|
121
|
+
? series.map((s, i) => ({ label: s.label, color: colors[i % 8]!, shape: 'square' as const }))
|
|
122
|
+
: []
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<ChartFrame
|
|
126
|
+
title={title}
|
|
127
|
+
description={description}
|
|
128
|
+
legend={legend}
|
|
129
|
+
height={height}
|
|
130
|
+
width={width}
|
|
131
|
+
count={data.length}
|
|
132
|
+
hitTest={hitTest}
|
|
133
|
+
anchor={(i, w) => {
|
|
134
|
+
const { band } = layoutAt(w)
|
|
135
|
+
const center = band.band(i) + band.bandwidth / 2
|
|
136
|
+
return horizontal ? { x: w - pad.right, y: center } : { x: center, y: pad.top }
|
|
137
|
+
}}
|
|
138
|
+
tooltip={(i) => ({
|
|
139
|
+
title: categories[i]!,
|
|
140
|
+
rows: series.map((s, j) => ({
|
|
141
|
+
label: s.label,
|
|
142
|
+
value: format(values[i]![j]!),
|
|
143
|
+
color: colors[j % 8]!,
|
|
144
|
+
shape: 'square',
|
|
145
|
+
})),
|
|
146
|
+
})}
|
|
147
|
+
table={{
|
|
148
|
+
columns: [indexLabel, ...series.map((s) => s.label)],
|
|
149
|
+
rows: data.map((_, i) => [categories[i]!, ...values[i]!.map(format)]),
|
|
150
|
+
numeric: [false, ...series.map(() => true)],
|
|
151
|
+
}}
|
|
152
|
+
labels={labels}
|
|
153
|
+
>
|
|
154
|
+
{(w, active) => {
|
|
155
|
+
const { valueScale, band, thickness, group } = layoutAt(w)
|
|
156
|
+
const zero = valueScale(0)
|
|
157
|
+
return (
|
|
158
|
+
<Svg width={w} height={height}>
|
|
159
|
+
{horizontal ? (
|
|
160
|
+
<>
|
|
161
|
+
{ticks.map((tick) => (
|
|
162
|
+
<Line
|
|
163
|
+
key={`grid-${tick}`}
|
|
164
|
+
x1={valueScale(tick)}
|
|
165
|
+
x2={valueScale(tick)}
|
|
166
|
+
y1={pad.top}
|
|
167
|
+
y2={height - pad.bottom}
|
|
168
|
+
stroke={tick === 0 ? ink.axis : ink.grid}
|
|
169
|
+
strokeWidth={1}
|
|
170
|
+
/>
|
|
171
|
+
))}
|
|
172
|
+
{ticks.map((tick) => (
|
|
173
|
+
<SvgText
|
|
174
|
+
key={`tick-${tick}`}
|
|
175
|
+
x={valueScale(tick)}
|
|
176
|
+
y={height - pad.bottom + 4}
|
|
177
|
+
dy="0.9em"
|
|
178
|
+
fill={ink.text}
|
|
179
|
+
fontSize={12}
|
|
180
|
+
textAnchor="middle"
|
|
181
|
+
>
|
|
182
|
+
{formatCompact(tick)}
|
|
183
|
+
</SvgText>
|
|
184
|
+
))}
|
|
185
|
+
{categories.map((category, i) => (
|
|
186
|
+
<SvgText
|
|
187
|
+
key={`cat-${i}`}
|
|
188
|
+
x={pad.left - 8}
|
|
189
|
+
y={band.band(i) + band.bandwidth / 2}
|
|
190
|
+
dy="0.35em"
|
|
191
|
+
fill={ink.text}
|
|
192
|
+
fontSize={12}
|
|
193
|
+
textAnchor="end"
|
|
194
|
+
>
|
|
195
|
+
{truncate(category, Math.floor((pad.left - 12) / 7))}
|
|
196
|
+
</SvgText>
|
|
197
|
+
))}
|
|
198
|
+
</>
|
|
199
|
+
) : (
|
|
200
|
+
<>
|
|
201
|
+
<ValueGrid
|
|
202
|
+
ticks={ticks}
|
|
203
|
+
y={valueScale}
|
|
204
|
+
left={pad.left}
|
|
205
|
+
right={w - pad.right}
|
|
206
|
+
ink={ink}
|
|
207
|
+
/>
|
|
208
|
+
<CategoryLabels
|
|
209
|
+
labels={categories}
|
|
210
|
+
x={(i) => band.band(i) + band.bandwidth / 2}
|
|
211
|
+
y={height - pad.bottom + 4}
|
|
212
|
+
available={band.bandwidth}
|
|
213
|
+
ink={ink}
|
|
214
|
+
/>
|
|
215
|
+
</>
|
|
216
|
+
)}
|
|
217
|
+
{values.map((row, i) => {
|
|
218
|
+
const start = band.band(i) + (band.bandwidth - group) / 2
|
|
219
|
+
let positive = 0
|
|
220
|
+
let negative = 0
|
|
221
|
+
// In a stack, only the outermost segment on each side gets the rounded end.
|
|
222
|
+
const lastPositive = row.reduce<number>((last, v, j) => ((v ?? 0) > 0 ? j : last), -1)
|
|
223
|
+
const lastNegative = row.reduce<number>((last, v, j) => ((v ?? 0) < 0 ? j : last), -1)
|
|
224
|
+
return (
|
|
225
|
+
<G key={i} opacity={active == null || active === i ? 1 : 0.55}>
|
|
226
|
+
{row.map((value, j) => {
|
|
227
|
+
if (value == null || value === 0) return null
|
|
228
|
+
const offset = stacked ? start : start + j * (thickness + GAP)
|
|
229
|
+
const from = stacked ? (value > 0 ? positive : negative) : 0
|
|
230
|
+
const to = from + value
|
|
231
|
+
if (stacked) {
|
|
232
|
+
if (value > 0) positive = to
|
|
233
|
+
else negative = to
|
|
234
|
+
}
|
|
235
|
+
const a = valueScale(from)
|
|
236
|
+
const b = valueScale(to)
|
|
237
|
+
// A 2px gap separates a stacked segment from the one on its baseline side.
|
|
238
|
+
const inset = stacked && from !== 0 ? GAP : 0
|
|
239
|
+
const rounded = !stacked || j === (value > 0 ? lastPositive : lastNegative)
|
|
240
|
+
const radius = rounded ? RADIUS : 0
|
|
241
|
+
const d = horizontal
|
|
242
|
+
? value > 0
|
|
243
|
+
? barPath(
|
|
244
|
+
Math.min(a, b) + inset,
|
|
245
|
+
offset,
|
|
246
|
+
Math.abs(b - a) - inset,
|
|
247
|
+
thickness,
|
|
248
|
+
radius,
|
|
249
|
+
'right',
|
|
250
|
+
)
|
|
251
|
+
: barPath(
|
|
252
|
+
Math.min(a, b),
|
|
253
|
+
offset,
|
|
254
|
+
Math.abs(b - a) - inset,
|
|
255
|
+
thickness,
|
|
256
|
+
radius,
|
|
257
|
+
'left',
|
|
258
|
+
)
|
|
259
|
+
: value > 0
|
|
260
|
+
? barPath(
|
|
261
|
+
offset,
|
|
262
|
+
Math.min(a, b),
|
|
263
|
+
thickness,
|
|
264
|
+
Math.abs(b - a) - inset,
|
|
265
|
+
radius,
|
|
266
|
+
'up',
|
|
267
|
+
)
|
|
268
|
+
: barPath(
|
|
269
|
+
offset,
|
|
270
|
+
Math.min(a, b) + inset,
|
|
271
|
+
thickness,
|
|
272
|
+
Math.abs(b - a) - inset,
|
|
273
|
+
radius,
|
|
274
|
+
'down',
|
|
275
|
+
)
|
|
276
|
+
return <Path key={j} d={d} fill={colors[j % 8]} />
|
|
277
|
+
})}
|
|
278
|
+
</G>
|
|
279
|
+
)
|
|
280
|
+
})}
|
|
281
|
+
{horizontal ? null : (
|
|
282
|
+
<Line
|
|
283
|
+
x1={pad.left}
|
|
284
|
+
x2={w - pad.right}
|
|
285
|
+
y1={zero}
|
|
286
|
+
y2={zero}
|
|
287
|
+
stroke={ink.axis}
|
|
288
|
+
strokeWidth={1}
|
|
289
|
+
/>
|
|
290
|
+
)}
|
|
291
|
+
</Svg>
|
|
292
|
+
)
|
|
293
|
+
}}
|
|
294
|
+
</ChartFrame>
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function truncate(text: string, max: number) {
|
|
299
|
+
return text.length > max ? `${text.slice(0, Math.max(1, max - 1))}…` : text
|
|
300
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { BarChart, type BarChartProps } from './BarChart'
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { LinePlot, type LinePlotProps } from '../../charts'
|
|
2
|
+
|
|
3
|
+
export type LineChartProps = LinePlotProps
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Shows change over an ordered axis (months, years) as 2px lines, with a
|
|
7
|
+
* crosshair tooltip (hover, tap or arrow keys), a legend and a table view.
|
|
8
|
+
*/
|
|
9
|
+
export function LineChart(props: LineChartProps) {
|
|
10
|
+
return <LinePlot {...props} />
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { LineChart, type LineChartProps } from './LineChart'
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { useIconColor } from '@advui/icons'
|
|
2
|
+
import {
|
|
3
|
+
ChartFrame,
|
|
4
|
+
type ChartLabels,
|
|
5
|
+
G,
|
|
6
|
+
Path,
|
|
7
|
+
Svg,
|
|
8
|
+
SvgText,
|
|
9
|
+
arcPath,
|
|
10
|
+
formatFull,
|
|
11
|
+
useChartColors,
|
|
12
|
+
useChartInk,
|
|
13
|
+
} from '../../charts'
|
|
14
|
+
|
|
15
|
+
export interface PieSlice {
|
|
16
|
+
label: string
|
|
17
|
+
value: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PieChartProps {
|
|
21
|
+
/** Names the chart: its heading and accessible name. */
|
|
22
|
+
title: string
|
|
23
|
+
description?: string
|
|
24
|
+
/** The parts of the whole. Negative and zero values are left out. */
|
|
25
|
+
data: PieSlice[]
|
|
26
|
+
/** `donut` leaves room for the total in the middle. */
|
|
27
|
+
variant?: 'donut' | 'pie'
|
|
28
|
+
/** More slices than this fold the smallest into "Other". Default: 6. */
|
|
29
|
+
maxSlices?: number
|
|
30
|
+
/** Height in pixels. Default: 240. */
|
|
31
|
+
height?: number
|
|
32
|
+
width?: number
|
|
33
|
+
/** Formats values in the legend, tooltip and table. */
|
|
34
|
+
valueFormatter?: (value: number) => string
|
|
35
|
+
/** Headings for the table and the folded slice, for translation. */
|
|
36
|
+
text?: { label?: string; value?: string; share?: string; other?: string; total?: string }
|
|
37
|
+
labels?: Partial<ChartLabels>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const percent = (part: number, total: number) =>
|
|
41
|
+
total > 0 ? `${Math.round((part / total) * 1000) / 10}%` : '0%'
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Part-to-whole at a glance, for up to six parts. Each slice is named in the
|
|
45
|
+
* legend with its share; the tooltip (hover, tap or arrow keys) and the
|
|
46
|
+
* table view give the exact values. For close values, a bar chart reads better.
|
|
47
|
+
*/
|
|
48
|
+
export function PieChart({
|
|
49
|
+
title,
|
|
50
|
+
description,
|
|
51
|
+
data,
|
|
52
|
+
variant = 'donut',
|
|
53
|
+
maxSlices = 6,
|
|
54
|
+
height = 240,
|
|
55
|
+
width,
|
|
56
|
+
valueFormatter = formatFull,
|
|
57
|
+
text: textProp,
|
|
58
|
+
labels,
|
|
59
|
+
}: PieChartProps) {
|
|
60
|
+
const text = {
|
|
61
|
+
label: 'Label',
|
|
62
|
+
value: 'Value',
|
|
63
|
+
share: 'Share',
|
|
64
|
+
other: 'Other',
|
|
65
|
+
total: 'Total',
|
|
66
|
+
...textProp,
|
|
67
|
+
}
|
|
68
|
+
const colors = useChartColors()
|
|
69
|
+
const ink = useChartInk('$background')
|
|
70
|
+
const foreground = useIconColor('$foreground') ?? 'currentColor'
|
|
71
|
+
|
|
72
|
+
// Largest first; past the limit, the tail folds into one "Other" slice.
|
|
73
|
+
const positive = data.filter((d) => d.value > 0).sort((a, b) => b.value - a.value)
|
|
74
|
+
const slices =
|
|
75
|
+
positive.length > maxSlices
|
|
76
|
+
? [
|
|
77
|
+
...positive.slice(0, maxSlices - 1),
|
|
78
|
+
{
|
|
79
|
+
label: text.other,
|
|
80
|
+
value: positive.slice(maxSlices - 1).reduce((sum, d) => sum + d.value, 0),
|
|
81
|
+
},
|
|
82
|
+
]
|
|
83
|
+
: positive
|
|
84
|
+
const folded = positive.length > maxSlices
|
|
85
|
+
const total = slices.reduce((sum, d) => sum + d.value, 0)
|
|
86
|
+
const colorOf = (i: number) => (folded && i === slices.length - 1 ? ink.axis : colors[i % 8]!)
|
|
87
|
+
|
|
88
|
+
let angle = 0
|
|
89
|
+
const arcs = slices.map((slice, i) => {
|
|
90
|
+
const start = angle
|
|
91
|
+
angle += total > 0 ? (slice.value / total) * Math.PI * 2 : 0
|
|
92
|
+
return { ...slice, start, end: angle, color: colorOf(i) }
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const geometry = (w: number) => {
|
|
96
|
+
const outer = Math.min(w, height) / 2 - 8
|
|
97
|
+
return { cx: w / 2, cy: height / 2, outer, inner: variant === 'donut' ? outer * 0.62 : 0 }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<ChartFrame
|
|
102
|
+
title={title}
|
|
103
|
+
description={description}
|
|
104
|
+
legend={arcs.map((arc) => ({
|
|
105
|
+
label: arc.label,
|
|
106
|
+
color: arc.color,
|
|
107
|
+
shape: 'square',
|
|
108
|
+
detail: percent(arc.value, total),
|
|
109
|
+
}))}
|
|
110
|
+
height={height}
|
|
111
|
+
width={width}
|
|
112
|
+
count={arcs.length}
|
|
113
|
+
hitTest={(x, y, w) => {
|
|
114
|
+
const { cx, cy, outer, inner } = geometry(w)
|
|
115
|
+
const dx = x - cx
|
|
116
|
+
const dy = y - cy
|
|
117
|
+
const r = Math.hypot(dx, dy)
|
|
118
|
+
if (r > outer + 8 || r < inner) return null
|
|
119
|
+
const a = (Math.atan2(dx, -dy) + Math.PI * 2) % (Math.PI * 2)
|
|
120
|
+
const found = arcs.findIndex((arc) => a >= arc.start && a < arc.end)
|
|
121
|
+
return found >= 0 ? found : null
|
|
122
|
+
}}
|
|
123
|
+
anchor={(i, w) => {
|
|
124
|
+
const { cx, outer } = geometry(w)
|
|
125
|
+
const mid = (arcs[i]!.start + arcs[i]!.end) / 2
|
|
126
|
+
return { x: cx + outer * Math.sin(mid), y: 0 }
|
|
127
|
+
}}
|
|
128
|
+
tooltip={(i) => ({
|
|
129
|
+
title: arcs[i]!.label,
|
|
130
|
+
rows: [
|
|
131
|
+
{
|
|
132
|
+
label: percent(arcs[i]!.value, total),
|
|
133
|
+
value: valueFormatter(arcs[i]!.value),
|
|
134
|
+
color: arcs[i]!.color,
|
|
135
|
+
shape: 'square',
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
})}
|
|
139
|
+
table={{
|
|
140
|
+
columns: [text.label, text.value, text.share],
|
|
141
|
+
rows: [
|
|
142
|
+
...arcs.map((arc) => [arc.label, valueFormatter(arc.value), percent(arc.value, total)]),
|
|
143
|
+
[text.total, valueFormatter(total), '100%'],
|
|
144
|
+
],
|
|
145
|
+
numeric: [false, true, true],
|
|
146
|
+
}}
|
|
147
|
+
labels={labels}
|
|
148
|
+
>
|
|
149
|
+
{(w, active) => {
|
|
150
|
+
const { cx, cy, outer, inner } = geometry(w)
|
|
151
|
+
// A 2px gap between slices, as angle at the outer edge.
|
|
152
|
+
const gap = arcs.length > 1 ? 2 / outer : 0
|
|
153
|
+
return (
|
|
154
|
+
<Svg width={w} height={height}>
|
|
155
|
+
{arcs.map((arc, i) => (
|
|
156
|
+
<G key={arc.label} opacity={active == null || active === i ? 1 : 0.55}>
|
|
157
|
+
<Path
|
|
158
|
+
d={arcPath(
|
|
159
|
+
cx,
|
|
160
|
+
cy,
|
|
161
|
+
active === i ? outer + 4 : outer,
|
|
162
|
+
inner,
|
|
163
|
+
arc.start + gap / 2,
|
|
164
|
+
Math.max(arc.start + gap / 2, arc.end - gap / 2),
|
|
165
|
+
)}
|
|
166
|
+
fill={arc.color}
|
|
167
|
+
/>
|
|
168
|
+
</G>
|
|
169
|
+
))}
|
|
170
|
+
{variant === 'donut' ? (
|
|
171
|
+
<>
|
|
172
|
+
<SvgText
|
|
173
|
+
x={cx}
|
|
174
|
+
y={cy - 6}
|
|
175
|
+
fill={foreground}
|
|
176
|
+
fontSize={20}
|
|
177
|
+
fontWeight="600"
|
|
178
|
+
textAnchor="middle"
|
|
179
|
+
>
|
|
180
|
+
{valueFormatter(total)}
|
|
181
|
+
</SvgText>
|
|
182
|
+
<SvgText x={cx} y={cy + 16} fill={ink.text} fontSize={12} textAnchor="middle">
|
|
183
|
+
{text.total}
|
|
184
|
+
</SvgText>
|
|
185
|
+
</>
|
|
186
|
+
) : null}
|
|
187
|
+
</Svg>
|
|
188
|
+
)
|
|
189
|
+
}}
|
|
190
|
+
</ChartFrame>
|
|
191
|
+
)
|
|
192
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { PieChart, type PieChartProps, type PieSlice } from './PieChart'
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
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'
|