@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,216 @@
|
|
|
1
|
+
import { CategoryLabels, ValueGrid, cartesianPadding } from './cartesian'
|
|
2
|
+
import {
|
|
3
|
+
ChartFrame,
|
|
4
|
+
type ChartLabels,
|
|
5
|
+
type ChartSeries,
|
|
6
|
+
useChartColors,
|
|
7
|
+
useChartInk,
|
|
8
|
+
} from './ChartFrame'
|
|
9
|
+
import { formatFull, numberAt } from './format'
|
|
10
|
+
import { linearScale, niceTicks } from './scale'
|
|
11
|
+
import { Circle, G, Line, Path, Svg } from './svg'
|
|
12
|
+
|
|
13
|
+
export interface LinePlotProps {
|
|
14
|
+
title: string
|
|
15
|
+
description?: string
|
|
16
|
+
/** One object per x position, e.g. `{ month: 'Jan', reached: 1200 }`. */
|
|
17
|
+
data: Record<string, unknown>[]
|
|
18
|
+
/** The field holding each row's x label. */
|
|
19
|
+
index: string
|
|
20
|
+
/** Heading of the x column in the table view. Default: `index`. */
|
|
21
|
+
indexLabel?: string
|
|
22
|
+
/** The numeric fields to plot, in color order. */
|
|
23
|
+
series: ChartSeries[]
|
|
24
|
+
/** Height in pixels, including the axes. Default: 240. */
|
|
25
|
+
height?: number
|
|
26
|
+
width?: number
|
|
27
|
+
/** Formats values in the tooltip and table. Axis ticks are compact (1.2K). */
|
|
28
|
+
valueFormatter?: (value: number) => string
|
|
29
|
+
/** Plot area always starts at zero. Default: true for areas, false for lines. */
|
|
30
|
+
startAtZero?: boolean
|
|
31
|
+
/**
|
|
32
|
+
* Color of the surface the chart sits on, for the ring around markers.
|
|
33
|
+
* Default: `$background`; use `$card` inside a Card.
|
|
34
|
+
*/
|
|
35
|
+
surface?: string
|
|
36
|
+
labels?: Partial<ChartLabels>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Shared by LineChart and AreaChart. */
|
|
40
|
+
export function LinePlot({
|
|
41
|
+
title,
|
|
42
|
+
description,
|
|
43
|
+
data,
|
|
44
|
+
index,
|
|
45
|
+
indexLabel = index,
|
|
46
|
+
series,
|
|
47
|
+
height = 240,
|
|
48
|
+
width,
|
|
49
|
+
valueFormatter = formatFull,
|
|
50
|
+
startAtZero,
|
|
51
|
+
surface = '$background',
|
|
52
|
+
labels,
|
|
53
|
+
area = false,
|
|
54
|
+
stacked = false,
|
|
55
|
+
}: LinePlotProps & { area?: boolean; stacked?: boolean }) {
|
|
56
|
+
const colors = useChartColors()
|
|
57
|
+
const ink = useChartInk(surface)
|
|
58
|
+
const categories = data.map((row) => String(row[index] ?? ''))
|
|
59
|
+
const values = data.map((row) => series.map((s) => numberAt(row, s.key)))
|
|
60
|
+
|
|
61
|
+
// Stacked areas draw each series on top of the ones before it.
|
|
62
|
+
const tops = values.map((row) => {
|
|
63
|
+
let sum = 0
|
|
64
|
+
return row.map((v) => (v == null ? null : stacked ? (sum += v) : v))
|
|
65
|
+
})
|
|
66
|
+
const all = tops.flat().filter((v): v is number => v != null)
|
|
67
|
+
const zero = startAtZero ?? area
|
|
68
|
+
const ticks = niceTicks(
|
|
69
|
+
zero ? Math.min(0, ...all) : Math.min(...all),
|
|
70
|
+
zero ? Math.max(0, ...all) : Math.max(...all),
|
|
71
|
+
)
|
|
72
|
+
const pad = cartesianPadding(ticks)
|
|
73
|
+
const n = data.length
|
|
74
|
+
|
|
75
|
+
const scales = (w: number) => {
|
|
76
|
+
const step = (w - pad.left - pad.right) / Math.max(1, n)
|
|
77
|
+
return {
|
|
78
|
+
step,
|
|
79
|
+
x: (i: number) => pad.left + (i + 0.5) * step,
|
|
80
|
+
y: linearScale([ticks[0]!, ticks.at(-1)!], [height - pad.bottom, pad.top]),
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const format = (value: number | null) => (value == null ? '–' : valueFormatter(value))
|
|
85
|
+
const shape = area ? ('square' as const) : ('line' as const)
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<ChartFrame
|
|
89
|
+
title={title}
|
|
90
|
+
description={description}
|
|
91
|
+
legend={
|
|
92
|
+
series.length > 1
|
|
93
|
+
? series.map((s, i) => ({ label: s.label, color: colors[i % 8]!, shape }))
|
|
94
|
+
: []
|
|
95
|
+
}
|
|
96
|
+
height={height}
|
|
97
|
+
width={width}
|
|
98
|
+
count={n}
|
|
99
|
+
hitTest={(x, _y, w) => {
|
|
100
|
+
const { step } = scales(w)
|
|
101
|
+
const i = Math.round((x - pad.left) / step - 0.5)
|
|
102
|
+
return Math.max(0, Math.min(n - 1, i))
|
|
103
|
+
}}
|
|
104
|
+
anchor={(i, w) => ({ x: scales(w).x(i), y: pad.top })}
|
|
105
|
+
tooltip={(i) => ({
|
|
106
|
+
title: categories[i]!,
|
|
107
|
+
rows: series.map((s, j) => ({
|
|
108
|
+
label: s.label,
|
|
109
|
+
value: format(values[i]![j]!),
|
|
110
|
+
color: colors[j % 8]!,
|
|
111
|
+
shape,
|
|
112
|
+
})),
|
|
113
|
+
})}
|
|
114
|
+
table={{
|
|
115
|
+
columns: [indexLabel, ...series.map((s) => s.label)],
|
|
116
|
+
rows: data.map((_, i) => [categories[i]!, ...values[i]!.map(format)]),
|
|
117
|
+
numeric: [false, ...series.map(() => true)],
|
|
118
|
+
}}
|
|
119
|
+
labels={labels}
|
|
120
|
+
>
|
|
121
|
+
{(w, active) => {
|
|
122
|
+
const { x, y, step } = scales(w)
|
|
123
|
+
const base = (i: number, j: number) =>
|
|
124
|
+
stacked && j > 0 ? (tops[i]![j - 1] ?? 0) : Math.max(ticks[0]!, 0)
|
|
125
|
+
return (
|
|
126
|
+
<Svg width={w} height={height}>
|
|
127
|
+
<ValueGrid ticks={ticks} y={y} left={pad.left} right={w - pad.right} ink={ink} />
|
|
128
|
+
<CategoryLabels
|
|
129
|
+
labels={categories}
|
|
130
|
+
x={x}
|
|
131
|
+
y={height - pad.bottom + 4}
|
|
132
|
+
available={step}
|
|
133
|
+
ink={ink}
|
|
134
|
+
/>
|
|
135
|
+
{active != null ? (
|
|
136
|
+
<Line
|
|
137
|
+
x1={x(active)}
|
|
138
|
+
x2={x(active)}
|
|
139
|
+
y1={pad.top}
|
|
140
|
+
y2={height - pad.bottom}
|
|
141
|
+
stroke={ink.axis}
|
|
142
|
+
strokeWidth={1}
|
|
143
|
+
/>
|
|
144
|
+
) : null}
|
|
145
|
+
{series.map((s, j) => {
|
|
146
|
+
const color = colors[j % 8]!
|
|
147
|
+
// A missing value breaks the line instead of dropping to zero.
|
|
148
|
+
const runs: number[][] = [[]]
|
|
149
|
+
tops.forEach((row, i) => {
|
|
150
|
+
if (row[j] == null) runs.push([])
|
|
151
|
+
else runs.at(-1)!.push(i)
|
|
152
|
+
})
|
|
153
|
+
return (
|
|
154
|
+
<G key={s.key}>
|
|
155
|
+
{runs
|
|
156
|
+
.filter((run) => run.length > 0)
|
|
157
|
+
.map((run) => {
|
|
158
|
+
const line = run
|
|
159
|
+
.map((i, k) => `${k === 0 ? 'M' : 'L'}${x(i)},${y(tops[i]![j]!)}`)
|
|
160
|
+
.join('')
|
|
161
|
+
const fill = area
|
|
162
|
+
? `${line}${[...run]
|
|
163
|
+
.reverse()
|
|
164
|
+
.map((i) => `L${x(i)},${y(base(i, j))}`)
|
|
165
|
+
.join('')}Z`
|
|
166
|
+
: null
|
|
167
|
+
// A lone value between gaps has no line to draw: show it as a dot.
|
|
168
|
+
if (run.length === 1) {
|
|
169
|
+
const i = run[0]!
|
|
170
|
+
return <Circle key={i} cx={x(i)} cy={y(tops[i]![j]!)} r={3} fill={color} />
|
|
171
|
+
}
|
|
172
|
+
return (
|
|
173
|
+
<G key={run[0]}>
|
|
174
|
+
{fill ? <Path d={fill} fill={color} fillOpacity={0.1} /> : null}
|
|
175
|
+
<Path
|
|
176
|
+
d={line}
|
|
177
|
+
fill="none"
|
|
178
|
+
stroke={color}
|
|
179
|
+
strokeWidth={2}
|
|
180
|
+
strokeLinejoin="round"
|
|
181
|
+
strokeLinecap="round"
|
|
182
|
+
/>
|
|
183
|
+
</G>
|
|
184
|
+
)
|
|
185
|
+
})}
|
|
186
|
+
</G>
|
|
187
|
+
)
|
|
188
|
+
})}
|
|
189
|
+
{/* End markers, and every series' marker at the hovered position, ringed in the surface color. */}
|
|
190
|
+
{series.map((s, j) => {
|
|
191
|
+
const color = colors[j % 8]!
|
|
192
|
+
const lastIndex = tops.reduce<number>(
|
|
193
|
+
(last, row, i) => (row[j] != null ? i : last),
|
|
194
|
+
-1,
|
|
195
|
+
)
|
|
196
|
+
const points = new Set([lastIndex, ...(active != null ? [active] : [])])
|
|
197
|
+
return [...points]
|
|
198
|
+
.filter((i) => i >= 0 && tops[i]?.[j] != null)
|
|
199
|
+
.map((i) => (
|
|
200
|
+
<Circle
|
|
201
|
+
key={`${s.key}-${i}`}
|
|
202
|
+
cx={x(i)}
|
|
203
|
+
cy={y(tops[i]![j]!)}
|
|
204
|
+
r={4}
|
|
205
|
+
fill={color}
|
|
206
|
+
stroke={ink.surface}
|
|
207
|
+
strokeWidth={2}
|
|
208
|
+
/>
|
|
209
|
+
))
|
|
210
|
+
})}
|
|
211
|
+
</Svg>
|
|
212
|
+
)
|
|
213
|
+
}}
|
|
214
|
+
</ChartFrame>
|
|
215
|
+
)
|
|
216
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Line, SvgText } from './svg'
|
|
2
|
+
import { formatCompact } from './format'
|
|
3
|
+
|
|
4
|
+
/** Space around the plot for axis labels. */
|
|
5
|
+
export function cartesianPadding(
|
|
6
|
+
ticks: number[],
|
|
7
|
+
format: (value: number) => string = formatCompact,
|
|
8
|
+
) {
|
|
9
|
+
// About 7px per character of the widest tick label at 12px.
|
|
10
|
+
const labelWidth = Math.max(...ticks.map((t) => format(t).length)) * 7
|
|
11
|
+
return { top: 8, right: 8, bottom: 24, left: labelWidth + 12 }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Horizontal gridlines with value labels on the left. */
|
|
15
|
+
export function ValueGrid({
|
|
16
|
+
ticks,
|
|
17
|
+
y,
|
|
18
|
+
left,
|
|
19
|
+
right,
|
|
20
|
+
ink,
|
|
21
|
+
format = formatCompact,
|
|
22
|
+
}: {
|
|
23
|
+
ticks: number[]
|
|
24
|
+
y: (value: number) => number
|
|
25
|
+
left: number
|
|
26
|
+
right: number
|
|
27
|
+
ink: { text: string; grid: string; axis: string }
|
|
28
|
+
format?: (value: number) => string
|
|
29
|
+
}) {
|
|
30
|
+
return (
|
|
31
|
+
<>
|
|
32
|
+
{ticks.map((tick) => (
|
|
33
|
+
<Line
|
|
34
|
+
key={`grid-${tick}`}
|
|
35
|
+
x1={left}
|
|
36
|
+
x2={right}
|
|
37
|
+
y1={y(tick)}
|
|
38
|
+
y2={y(tick)}
|
|
39
|
+
stroke={tick === 0 ? ink.axis : ink.grid}
|
|
40
|
+
strokeWidth={1}
|
|
41
|
+
/>
|
|
42
|
+
))}
|
|
43
|
+
{ticks.map((tick) => (
|
|
44
|
+
<SvgText
|
|
45
|
+
key={`label-${tick}`}
|
|
46
|
+
x={left - 8}
|
|
47
|
+
y={y(tick)}
|
|
48
|
+
dy="0.35em"
|
|
49
|
+
fill={ink.text}
|
|
50
|
+
fontSize={12}
|
|
51
|
+
textAnchor="end"
|
|
52
|
+
>
|
|
53
|
+
{format(tick)}
|
|
54
|
+
</SvgText>
|
|
55
|
+
))}
|
|
56
|
+
</>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Category labels under the plot. When they would overlap, only every nth
|
|
62
|
+
* is drawn (the first and the rest in steps), so none collide.
|
|
63
|
+
*/
|
|
64
|
+
export function CategoryLabels({
|
|
65
|
+
labels,
|
|
66
|
+
x,
|
|
67
|
+
y,
|
|
68
|
+
available,
|
|
69
|
+
ink,
|
|
70
|
+
}: {
|
|
71
|
+
labels: string[]
|
|
72
|
+
x: (index: number) => number
|
|
73
|
+
y: number
|
|
74
|
+
/** Width each label may use before thinning kicks in. */
|
|
75
|
+
available: number
|
|
76
|
+
ink: { text: string }
|
|
77
|
+
}) {
|
|
78
|
+
const widest = Math.max(1, ...labels.map((l) => l.length)) * 7 + 8
|
|
79
|
+
const step = Math.max(1, Math.ceil(widest / Math.max(1, available)))
|
|
80
|
+
return (
|
|
81
|
+
<>
|
|
82
|
+
{labels.map((label, index) =>
|
|
83
|
+
index % step === 0 ? (
|
|
84
|
+
<SvgText
|
|
85
|
+
key={`${label}-${index}`}
|
|
86
|
+
x={x(index)}
|
|
87
|
+
y={y}
|
|
88
|
+
dy="0.9em"
|
|
89
|
+
fill={ink.text}
|
|
90
|
+
fontSize={12}
|
|
91
|
+
textAnchor="middle"
|
|
92
|
+
>
|
|
93
|
+
{label}
|
|
94
|
+
</SvgText>
|
|
95
|
+
) : null,
|
|
96
|
+
)}
|
|
97
|
+
</>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const compact = new Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 1 })
|
|
2
|
+
const full = new Intl.NumberFormat('en-US', { maximumFractionDigits: 2 })
|
|
3
|
+
|
|
4
|
+
/** Axis ticks: 1.2K, 3.4M. */
|
|
5
|
+
export const formatCompact = (value: number) => compact.format(value)
|
|
6
|
+
|
|
7
|
+
/** Tooltips and the table: 1,234.5. */
|
|
8
|
+
export const formatFull = (value: number) => full.format(value)
|
|
9
|
+
|
|
10
|
+
/** Reads a numeric field; anything else counts as missing. */
|
|
11
|
+
export function numberAt(row: Record<string, unknown>, key: string): number | null {
|
|
12
|
+
const value = row[key]
|
|
13
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null
|
|
14
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export {
|
|
2
|
+
ChartFrame,
|
|
3
|
+
defaultChartLabels,
|
|
4
|
+
useChartColors,
|
|
5
|
+
useChartInk,
|
|
6
|
+
type ChartFrameProps,
|
|
7
|
+
type ChartLabels,
|
|
8
|
+
type ChartSeries,
|
|
9
|
+
type LegendItem,
|
|
10
|
+
type TooltipContent,
|
|
11
|
+
} from './ChartFrame'
|
|
12
|
+
export { CategoryLabels, ValueGrid, cartesianPadding } from './cartesian'
|
|
13
|
+
export { LinePlot, type LinePlotProps } from './LinePlot'
|
|
14
|
+
export { formatCompact, formatFull, numberAt } from './format'
|
|
15
|
+
export { arcPath, bandScale, barPath, linearScale, niceTicks } from './scale'
|
|
16
|
+
export { Circle, G, Line, Path, Rect, Svg, SvgText } from './svg'
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/** Clean, round tick values ("nice numbers") covering [min, max]. */
|
|
2
|
+
export function niceTicks(min: number, max: number, count = 5): number[] {
|
|
3
|
+
if (!Number.isFinite(min) || !Number.isFinite(max)) return [0]
|
|
4
|
+
if (min === max) {
|
|
5
|
+
if (min === 0) return [0, 1]
|
|
6
|
+
;[min, max] = min > 0 ? [0, min] : [min, 0]
|
|
7
|
+
}
|
|
8
|
+
const step = niceStep((max - min) / Math.max(1, count - 1))
|
|
9
|
+
const start = Math.floor(min / step) * step
|
|
10
|
+
const end = Math.ceil(max / step) * step
|
|
11
|
+
const ticks: number[] = []
|
|
12
|
+
// Rounding keeps 0.1 + 0.2 style float noise out of the labels.
|
|
13
|
+
for (let v = start; v <= end + step / 2; v += step) ticks.push(Number(v.toPrecision(12)))
|
|
14
|
+
return ticks
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function niceStep(raw: number): number {
|
|
18
|
+
const power = 10 ** Math.floor(Math.log10(raw))
|
|
19
|
+
const fraction = raw / power
|
|
20
|
+
const nice =
|
|
21
|
+
fraction <= 1 ? 1 : fraction <= 2 ? 2 : fraction <= 2.5 ? 2.5 : fraction <= 5 ? 5 : 10
|
|
22
|
+
return nice * power
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Maps a value in [d0, d1] to [r0, r1]. */
|
|
26
|
+
export function linearScale([d0, d1]: [number, number], [r0, r1]: [number, number]) {
|
|
27
|
+
const span = d1 - d0 || 1
|
|
28
|
+
return (value: number) => r0 + ((value - d0) / span) * (r1 - r0)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Splits [start, end] into `count` equal bands; `band(i)` is the start of
|
|
33
|
+
* band i and `bandwidth` its size.
|
|
34
|
+
*/
|
|
35
|
+
export function bandScale(count: number, [start, end]: [number, number]) {
|
|
36
|
+
const bandwidth = (end - start) / Math.max(1, count)
|
|
37
|
+
return { band: (index: number) => start + index * bandwidth, bandwidth }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A rectangle with rounded corners on its data end only (the end away from
|
|
42
|
+
* the baseline), as an SVG path. `direction` is where the data end points.
|
|
43
|
+
*/
|
|
44
|
+
export function barPath(
|
|
45
|
+
x: number,
|
|
46
|
+
y: number,
|
|
47
|
+
width: number,
|
|
48
|
+
height: number,
|
|
49
|
+
radius: number,
|
|
50
|
+
direction: 'up' | 'down' | 'right' | 'left',
|
|
51
|
+
): string {
|
|
52
|
+
if (width <= 0 || height <= 0) return ''
|
|
53
|
+
const r = Math.max(0, Math.min(radius, width / 2, height / 2))
|
|
54
|
+
const x2 = x + width
|
|
55
|
+
const y2 = y + height
|
|
56
|
+
switch (direction) {
|
|
57
|
+
case 'up':
|
|
58
|
+
return `M${x},${y2}V${y + r}Q${x},${y} ${x + r},${y}H${x2 - r}Q${x2},${y} ${x2},${y + r}V${y2}Z`
|
|
59
|
+
case 'down':
|
|
60
|
+
return `M${x},${y}V${y2 - r}Q${x},${y2} ${x + r},${y2}H${x2 - r}Q${x2},${y2} ${x2},${y2 - r}V${y}Z`
|
|
61
|
+
case 'right':
|
|
62
|
+
return `M${x},${y}H${x2 - r}Q${x2},${y} ${x2},${y + r}V${y2 - r}Q${x2},${y2} ${x2 - r},${y2}H${x}Z`
|
|
63
|
+
case 'left':
|
|
64
|
+
return `M${x2},${y}H${x + r}Q${x},${y} ${x},${y + r}V${y2 - r}Q${x},${y2} ${x + r},${y2}H${x2}Z`
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A pie or donut slice as an SVG path; angles in radians, 0 at 12 o'clock. */
|
|
69
|
+
export function arcPath(
|
|
70
|
+
cx: number,
|
|
71
|
+
cy: number,
|
|
72
|
+
outer: number,
|
|
73
|
+
inner: number,
|
|
74
|
+
start: number,
|
|
75
|
+
end: number,
|
|
76
|
+
): string {
|
|
77
|
+
const point = (radius: number, angle: number) =>
|
|
78
|
+
`${cx + radius * Math.sin(angle)},${cy - radius * Math.cos(angle)}`
|
|
79
|
+
const large = end - start > Math.PI ? 1 : 0
|
|
80
|
+
// A full circle cannot be one arc: split it in two.
|
|
81
|
+
if (end - start >= Math.PI * 2 - 1e-6) {
|
|
82
|
+
const mid = start + Math.PI
|
|
83
|
+
return inner > 0
|
|
84
|
+
? `M${point(outer, start)}A${outer},${outer} 0 1 1 ${point(outer, mid)}A${outer},${outer} 0 1 1 ${point(outer, start)}` +
|
|
85
|
+
`M${point(inner, start)}A${inner},${inner} 0 1 0 ${point(inner, mid)}A${inner},${inner} 0 1 0 ${point(inner, start)}Z`
|
|
86
|
+
: `M${point(outer, start)}A${outer},${outer} 0 1 1 ${point(outer, mid)}A${outer},${outer} 0 1 1 ${point(outer, start)}Z`
|
|
87
|
+
}
|
|
88
|
+
return inner > 0
|
|
89
|
+
? `M${point(outer, start)}A${outer},${outer} 0 ${large} 1 ${point(outer, end)}L${point(inner, end)}A${inner},${inner} 0 ${large} 0 ${point(inner, start)}Z`
|
|
90
|
+
: `M${cx},${cy}L${point(outer, start)}A${outer},${outer} 0 ${large} 1 ${point(outer, end)}Z`
|
|
91
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import NativeSvg, {
|
|
3
|
+
Circle as NativeCircle,
|
|
4
|
+
G as NativeG,
|
|
5
|
+
Line as NativeLine,
|
|
6
|
+
Path as NativePath,
|
|
7
|
+
Rect as NativeRect,
|
|
8
|
+
Text as NativeText,
|
|
9
|
+
} from 'react-native-svg'
|
|
10
|
+
|
|
11
|
+
// Native: the web file's components, mapped onto react-native-svg.
|
|
12
|
+
|
|
13
|
+
type Common = { children?: ReactNode; opacity?: number }
|
|
14
|
+
type Paint = { fill?: string; fillOpacity?: number; stroke?: string; strokeWidth?: number }
|
|
15
|
+
|
|
16
|
+
export function Svg({ width, height, children }: Common & { width: number; height: number }) {
|
|
17
|
+
return (
|
|
18
|
+
<NativeSvg
|
|
19
|
+
width={width}
|
|
20
|
+
height={height}
|
|
21
|
+
viewBox={`0 0 ${width} ${height}`}
|
|
22
|
+
accessible={false}
|
|
23
|
+
importantForAccessibility="no-hide-descendants"
|
|
24
|
+
>
|
|
25
|
+
{children}
|
|
26
|
+
</NativeSvg>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const G = ({ children, opacity }: Common) => <NativeG opacity={opacity}>{children}</NativeG>
|
|
31
|
+
|
|
32
|
+
export const Rect = (props: Paint & { x: number; y: number; width: number; height: number }) => (
|
|
33
|
+
<NativeRect {...props} />
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
export const Path = (
|
|
37
|
+
props: Paint & { d: string; strokeLinejoin?: 'round'; strokeLinecap?: 'round' },
|
|
38
|
+
) => <NativePath {...props} />
|
|
39
|
+
|
|
40
|
+
export const Line = (props: Paint & { x1: number; y1: number; x2: number; y2: number }) => (
|
|
41
|
+
<NativeLine {...props} />
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
export const Circle = (props: Paint & { cx: number; cy: number; r: number }) => (
|
|
45
|
+
<NativeCircle {...props} />
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
export function SvgText({
|
|
49
|
+
children,
|
|
50
|
+
...props
|
|
51
|
+
}: {
|
|
52
|
+
children: ReactNode
|
|
53
|
+
x: number
|
|
54
|
+
y: number
|
|
55
|
+
fill: string
|
|
56
|
+
fontSize: number
|
|
57
|
+
fontWeight?: string
|
|
58
|
+
textAnchor?: 'start' | 'middle' | 'end'
|
|
59
|
+
dy?: string
|
|
60
|
+
}) {
|
|
61
|
+
return <NativeText {...(props as object)}>{children}</NativeText>
|
|
62
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
// Web: plain DOM SVG, so web bundles do not need react-native-svg. The
|
|
4
|
+
// native file maps the same components onto react-native-svg.
|
|
5
|
+
|
|
6
|
+
type Common = { children?: ReactNode; opacity?: number }
|
|
7
|
+
type Paint = { fill?: string; fillOpacity?: number; stroke?: string; strokeWidth?: number }
|
|
8
|
+
|
|
9
|
+
export function Svg({ width, height, children }: Common & { width: number; height: number }) {
|
|
10
|
+
return (
|
|
11
|
+
<svg
|
|
12
|
+
width={width}
|
|
13
|
+
height={height}
|
|
14
|
+
viewBox={`0 0 ${width} ${height}`}
|
|
15
|
+
style={{ display: 'block', overflow: 'visible', fontFamily: 'inherit' }}
|
|
16
|
+
aria-hidden
|
|
17
|
+
focusable="false"
|
|
18
|
+
>
|
|
19
|
+
{children}
|
|
20
|
+
</svg>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const G = ({ children, opacity }: Common) => <g opacity={opacity}>{children}</g>
|
|
25
|
+
|
|
26
|
+
export const Rect = (props: Paint & { x: number; y: number; width: number; height: number }) => (
|
|
27
|
+
<rect {...props} />
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
export const Path = (
|
|
31
|
+
props: Paint & { d: string; strokeLinejoin?: 'round'; strokeLinecap?: 'round' },
|
|
32
|
+
) => <path {...props} />
|
|
33
|
+
|
|
34
|
+
export const Line = (props: Paint & { x1: number; y1: number; x2: number; y2: number }) => (
|
|
35
|
+
<line {...props} />
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
export const Circle = (props: Paint & { cx: number; cy: number; r: number }) => (
|
|
39
|
+
<circle {...props} />
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
export function SvgText({
|
|
43
|
+
children,
|
|
44
|
+
...props
|
|
45
|
+
}: {
|
|
46
|
+
children: ReactNode
|
|
47
|
+
x: number
|
|
48
|
+
y: number
|
|
49
|
+
fill: string
|
|
50
|
+
fontSize: number
|
|
51
|
+
fontWeight?: string
|
|
52
|
+
textAnchor?: 'start' | 'middle' | 'end'
|
|
53
|
+
/** Vertical nudge: 0.35em centers text on y. */
|
|
54
|
+
dy?: string
|
|
55
|
+
}) {
|
|
56
|
+
return <text {...props}>{children}</text>
|
|
57
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { LinePlot, type LinePlotProps } from '../../charts'
|
|
2
|
+
|
|
3
|
+
export interface AreaChartProps extends LinePlotProps {
|
|
4
|
+
/** Stacks the series, so the top edge is their total (part-to-whole over time). */
|
|
5
|
+
stacked?: boolean
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A line chart with a light wash under each line, from zero. Best for one
|
|
10
|
+
* series, or for stacked totals over time.
|
|
11
|
+
*/
|
|
12
|
+
export function AreaChart({ stacked = false, ...props }: AreaChartProps) {
|
|
13
|
+
return <LinePlot {...props} area stacked={stacked} />
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { AreaChart, type AreaChartProps } from './AreaChart'
|