@barefootjs/chart 0.1.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/dist/chart-container.d.ts +10 -0
- package/dist/chart-container.d.ts.map +1 -0
- package/dist/context.d.ts +10 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2254 -0
- package/dist/types.d.ts +235 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils/arcs.d.ts +44 -0
- package/dist/utils/arcs.d.ts.map +1 -0
- package/dist/utils/areas.d.ts +23 -0
- package/dist/utils/areas.d.ts.map +1 -0
- package/dist/utils/classes.d.ts +24 -0
- package/dist/utils/classes.d.ts.map +1 -0
- package/dist/utils/lines.d.ts +18 -0
- package/dist/utils/lines.d.ts.map +1 -0
- package/dist/utils/radar.d.ts +25 -0
- package/dist/utils/radar.d.ts.map +1 -0
- package/dist/utils/scales.d.ts +12 -0
- package/dist/utils/scales.d.ts.map +1 -0
- package/package.json +55 -0
- package/src/chart-container.ts +17 -0
- package/src/context.ts +14 -0
- package/src/index.ts +75 -0
- package/src/types.ts +270 -0
- package/src/utils/arcs.ts +149 -0
- package/src/utils/areas.ts +69 -0
- package/src/utils/classes.ts +24 -0
- package/src/utils/lines.ts +61 -0
- package/src/utils/radar.ts +53 -0
- package/src/utils/scales.ts +64 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import type { ScaleBand, ScaleLinear, ScalePoint } from 'd3-scale'
|
|
2
|
+
|
|
3
|
+
/** Color and label configuration for chart data series */
|
|
4
|
+
export type ChartConfig = Record<
|
|
5
|
+
string,
|
|
6
|
+
{
|
|
7
|
+
label: string
|
|
8
|
+
color: string
|
|
9
|
+
}
|
|
10
|
+
>
|
|
11
|
+
|
|
12
|
+
/** Registration info for a bar series */
|
|
13
|
+
export interface BarRegistration {
|
|
14
|
+
dataKey: string
|
|
15
|
+
fill: string
|
|
16
|
+
radius: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Props for ChartContainer */
|
|
20
|
+
export interface ChartContainerProps {
|
|
21
|
+
config: ChartConfig
|
|
22
|
+
className?: string
|
|
23
|
+
children?: unknown
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Props for BarChart */
|
|
27
|
+
export interface BarChartProps {
|
|
28
|
+
data: Record<string, unknown>[]
|
|
29
|
+
children?: unknown
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Props for Bar */
|
|
33
|
+
export interface BarProps {
|
|
34
|
+
dataKey: string
|
|
35
|
+
fill?: string
|
|
36
|
+
radius?: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Props for CartesianGrid */
|
|
40
|
+
export interface CartesianGridProps {
|
|
41
|
+
vertical?: boolean
|
|
42
|
+
horizontal?: boolean
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Props for XAxis */
|
|
46
|
+
export interface XAxisProps {
|
|
47
|
+
dataKey: string
|
|
48
|
+
tickFormatter?: (value: string) => string
|
|
49
|
+
hide?: boolean
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Props for YAxis */
|
|
53
|
+
export interface YAxisProps {
|
|
54
|
+
hide?: boolean
|
|
55
|
+
tickFormatter?: (value: number) => string
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Props for LineChart */
|
|
59
|
+
export interface LineChartProps {
|
|
60
|
+
data: Record<string, unknown>[]
|
|
61
|
+
children?: unknown
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Props for Line */
|
|
65
|
+
export interface LineProps {
|
|
66
|
+
dataKey: string
|
|
67
|
+
stroke?: string
|
|
68
|
+
strokeWidth?: number
|
|
69
|
+
type?: 'linear' | 'monotone'
|
|
70
|
+
dot?: boolean
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Props for ChartTooltip */
|
|
74
|
+
export interface ChartTooltipProps {
|
|
75
|
+
labelFormatter?: (label: string) => string
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Registration info for a radial bar series */
|
|
79
|
+
export interface RadialBarRegistration {
|
|
80
|
+
dataKey: string
|
|
81
|
+
fill: string
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Registration info for a pie slice */
|
|
85
|
+
export interface PieRegistration {
|
|
86
|
+
dataKey: string
|
|
87
|
+
fill: string
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Props for RadialChart */
|
|
91
|
+
export interface RadialChartProps {
|
|
92
|
+
data: Record<string, unknown>[]
|
|
93
|
+
innerRadius?: number
|
|
94
|
+
outerRadius?: number
|
|
95
|
+
startAngle?: number
|
|
96
|
+
endAngle?: number
|
|
97
|
+
children?: unknown
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Props for RadialBar */
|
|
101
|
+
export interface RadialBarProps {
|
|
102
|
+
dataKey: string
|
|
103
|
+
fill?: string
|
|
104
|
+
stackId?: string
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Props for RadialChartLabel */
|
|
108
|
+
export interface RadialChartLabelProps {
|
|
109
|
+
children?: unknown
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Context value shared between RadialChart and its children */
|
|
113
|
+
export interface RadialChartContextValue {
|
|
114
|
+
svgGroup: () => SVGGElement | null
|
|
115
|
+
container: () => HTMLElement | null
|
|
116
|
+
data: () => Record<string, unknown>[]
|
|
117
|
+
innerRadius: () => number
|
|
118
|
+
outerRadius: () => number
|
|
119
|
+
startAngle: () => number
|
|
120
|
+
endAngle: () => number
|
|
121
|
+
config: () => ChartConfig
|
|
122
|
+
centerX: () => number
|
|
123
|
+
centerY: () => number
|
|
124
|
+
radialBars: () => RadialBarRegistration[]
|
|
125
|
+
registerRadialBar: (bar: RadialBarRegistration) => void
|
|
126
|
+
unregisterRadialBar: (dataKey: string) => void
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Props for PieChart */
|
|
130
|
+
export interface PieChartProps {
|
|
131
|
+
data: Record<string, unknown>[]
|
|
132
|
+
children?: unknown
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Props for Pie */
|
|
136
|
+
export interface PieProps {
|
|
137
|
+
dataKey: string
|
|
138
|
+
nameKey: string
|
|
139
|
+
fill?: string
|
|
140
|
+
innerRadius?: number
|
|
141
|
+
outerRadius?: number
|
|
142
|
+
paddingAngle?: number
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Props for PieTooltip */
|
|
146
|
+
export interface PieTooltipProps {
|
|
147
|
+
labelFormatter?: (label: string) => string
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Context value shared between PieChart and its children */
|
|
151
|
+
export interface PieChartContextValue {
|
|
152
|
+
svgGroup: () => SVGGElement | null
|
|
153
|
+
container: () => HTMLElement | null
|
|
154
|
+
data: () => Record<string, unknown>[]
|
|
155
|
+
width: () => number
|
|
156
|
+
height: () => number
|
|
157
|
+
config: () => ChartConfig
|
|
158
|
+
pies: () => PieRegistration[]
|
|
159
|
+
registerPie: (pie: PieRegistration) => void
|
|
160
|
+
unregisterPie: (dataKey: string) => void
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Context value shared between BarChart and its children */
|
|
164
|
+
export interface BarChartContextValue {
|
|
165
|
+
svgGroup: () => SVGGElement | null
|
|
166
|
+
container: () => HTMLElement | null
|
|
167
|
+
data: () => Record<string, unknown>[]
|
|
168
|
+
xDataKey: () => string
|
|
169
|
+
xScale: () => ScaleBand<string> | null
|
|
170
|
+
yScale: () => ScaleLinear<number, number> | null
|
|
171
|
+
innerWidth: () => number
|
|
172
|
+
innerHeight: () => number
|
|
173
|
+
config: () => ChartConfig
|
|
174
|
+
bars: () => BarRegistration[]
|
|
175
|
+
registerBar: (bar: BarRegistration) => void
|
|
176
|
+
unregisterBar: (dataKey: string) => void
|
|
177
|
+
setXDataKey: (key: string) => void
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Registration info for a radar series */
|
|
181
|
+
export interface RadarRegistration {
|
|
182
|
+
dataKey: string
|
|
183
|
+
fill: string
|
|
184
|
+
fillOpacity: number
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Registration info for an area series */
|
|
188
|
+
export interface AreaRegistration {
|
|
189
|
+
dataKey: string
|
|
190
|
+
fill: string
|
|
191
|
+
stroke: string
|
|
192
|
+
fillOpacity: number
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Props for RadarChart */
|
|
196
|
+
export interface RadarChartProps {
|
|
197
|
+
data: Record<string, unknown>[]
|
|
198
|
+
children?: unknown
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Props for AreaChart */
|
|
202
|
+
export interface AreaChartProps {
|
|
203
|
+
data: Record<string, unknown>[]
|
|
204
|
+
children?: unknown
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Props for Radar */
|
|
208
|
+
export interface RadarProps {
|
|
209
|
+
dataKey: string
|
|
210
|
+
fill?: string
|
|
211
|
+
fillOpacity?: number
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Props for PolarGrid */
|
|
215
|
+
export interface PolarGridProps {
|
|
216
|
+
gridType?: 'polygon' | 'circle'
|
|
217
|
+
show?: boolean
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Props for PolarAngleAxis */
|
|
221
|
+
export interface PolarAngleAxisProps {
|
|
222
|
+
dataKey: string
|
|
223
|
+
tickFormatter?: (value: string) => string
|
|
224
|
+
hide?: boolean
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Props for RadarTooltip */
|
|
228
|
+
export interface RadarTooltipProps {
|
|
229
|
+
labelFormatter?: (label: string) => string
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Context value shared between RadarChart and its children */
|
|
233
|
+
export interface RadarChartContextValue {
|
|
234
|
+
svgGroup: () => SVGGElement | null
|
|
235
|
+
container: () => HTMLElement | null
|
|
236
|
+
data: () => Record<string, unknown>[]
|
|
237
|
+
dataKey: () => string
|
|
238
|
+
radius: () => number
|
|
239
|
+
radialScale: () => ScaleLinear<number, number> | null
|
|
240
|
+
config: () => ChartConfig
|
|
241
|
+
radars: () => RadarRegistration[]
|
|
242
|
+
registerRadar: (radar: RadarRegistration) => void
|
|
243
|
+
unregisterRadar: (dataKey: string) => void
|
|
244
|
+
setDataKey: (key: string) => void
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Props for Area */
|
|
248
|
+
export interface AreaProps {
|
|
249
|
+
dataKey: string
|
|
250
|
+
fill?: string
|
|
251
|
+
stroke?: string
|
|
252
|
+
fillOpacity?: number
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Context value shared between AreaChart and its children */
|
|
256
|
+
export interface AreaChartContextValue {
|
|
257
|
+
svgGroup: () => SVGGElement | null
|
|
258
|
+
container: () => HTMLElement | null
|
|
259
|
+
data: () => Record<string, unknown>[]
|
|
260
|
+
xDataKey: () => string
|
|
261
|
+
xScale: () => ScalePoint<string> | null
|
|
262
|
+
yScale: () => ScaleLinear<number, number> | null
|
|
263
|
+
innerWidth: () => number
|
|
264
|
+
innerHeight: () => number
|
|
265
|
+
config: () => ChartConfig
|
|
266
|
+
areas: () => AreaRegistration[]
|
|
267
|
+
registerArea: (area: AreaRegistration) => void
|
|
268
|
+
unregisterArea: (dataKey: string) => void
|
|
269
|
+
setXDataKey: (key: string) => void
|
|
270
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { arc as d3Arc, pie as d3Pie } from 'd3-shape'
|
|
2
|
+
import type { PieArcDatum } from 'd3-shape'
|
|
3
|
+
import { scaleLinear } from 'd3-scale'
|
|
4
|
+
import { max } from 'd3-array'
|
|
5
|
+
|
|
6
|
+
export interface PieSliceSpec {
|
|
7
|
+
/** Slice path generated by `d3-shape`'s `arc()`. */
|
|
8
|
+
d: string
|
|
9
|
+
/** Resolved fill — datum override, then ChartConfig color, then fallback. */
|
|
10
|
+
fill: string
|
|
11
|
+
/** Datum's name used for tooltip lookup and as the stable map key. */
|
|
12
|
+
name: string
|
|
13
|
+
/** Numeric value used to size the slice. */
|
|
14
|
+
value: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface RadialBarArcSpec {
|
|
18
|
+
/** Background-track path (full sweep, dim). */
|
|
19
|
+
trackD: string | null
|
|
20
|
+
/** Foreground value path (proportional to data value). */
|
|
21
|
+
arcD: string | null
|
|
22
|
+
/** Numeric value used to size the arc. */
|
|
23
|
+
value: number
|
|
24
|
+
/** Optional per-datum fill override (when datum.fill is set). */
|
|
25
|
+
itemFill?: string
|
|
26
|
+
/** Index in the input data, used as a stable identifier for SVG. */
|
|
27
|
+
index: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Build the arc geometry for a single radial-bar series. Each input datum
|
|
32
|
+
* becomes a concentric ring within `[innerR, outerR]`, with the value arc
|
|
33
|
+
* sweeping from `startDeg` (top) to a fraction of the dataset max.
|
|
34
|
+
*
|
|
35
|
+
* Pure data — caller renders one `<path d={track.trackD}>` plus one
|
|
36
|
+
* `<path d={track.arcD}>` per spec, keeping the d3-shape dependency inside
|
|
37
|
+
* `@barefootjs/chart` and out of consumer bundles.
|
|
38
|
+
*/
|
|
39
|
+
export function buildRadialBarArcs(
|
|
40
|
+
data: Record<string, unknown>[],
|
|
41
|
+
dataKey: string,
|
|
42
|
+
innerR: number,
|
|
43
|
+
outerR: number,
|
|
44
|
+
startDeg: number,
|
|
45
|
+
endDeg: number,
|
|
46
|
+
): RadialBarArcSpec[] {
|
|
47
|
+
if (data.length === 0) return []
|
|
48
|
+
|
|
49
|
+
// SVG arc convention: 0° = top, clockwise. d3-shape uses 0 = right, ccw,
|
|
50
|
+
// so subtract 90° before converting to radians.
|
|
51
|
+
const startRad = (startDeg - 90) * (Math.PI / 180)
|
|
52
|
+
const endRad = (endDeg - 90) * (Math.PI / 180)
|
|
53
|
+
|
|
54
|
+
const maxValue =
|
|
55
|
+
max(data, (d) => {
|
|
56
|
+
const v = d[dataKey]
|
|
57
|
+
return typeof v === 'number' ? v : 0
|
|
58
|
+
}) ?? 1
|
|
59
|
+
|
|
60
|
+
const angleScale = scaleLinear().domain([0, maxValue]).range([startRad, endRad])
|
|
61
|
+
|
|
62
|
+
const ringCount = data.length
|
|
63
|
+
const ringThickness = (outerR - innerR) / ringCount
|
|
64
|
+
const ringPadding = Math.max(1, ringThickness * 0.1)
|
|
65
|
+
|
|
66
|
+
const arcGenerator = d3Arc<{
|
|
67
|
+
innerR: number
|
|
68
|
+
outerR: number
|
|
69
|
+
startAngle: number
|
|
70
|
+
endAngle: number
|
|
71
|
+
}>()
|
|
72
|
+
.innerRadius((d) => d.innerR)
|
|
73
|
+
.outerRadius((d) => d.outerR)
|
|
74
|
+
.startAngle((d) => d.startAngle)
|
|
75
|
+
.endAngle((d) => d.endAngle)
|
|
76
|
+
.cornerRadius(4)
|
|
77
|
+
|
|
78
|
+
return data.map((datum, i) => {
|
|
79
|
+
const value = Number(datum[dataKey]) || 0
|
|
80
|
+
const rInner = innerR + i * ringThickness + ringPadding / 2
|
|
81
|
+
const rOuter = innerR + (i + 1) * ringThickness - ringPadding / 2
|
|
82
|
+
const itemFill = datum.fill as string | undefined
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
trackD: arcGenerator({
|
|
86
|
+
innerR: rInner,
|
|
87
|
+
outerR: rOuter,
|
|
88
|
+
startAngle: startRad,
|
|
89
|
+
endAngle: endRad,
|
|
90
|
+
}),
|
|
91
|
+
arcD: arcGenerator({
|
|
92
|
+
innerR: rInner,
|
|
93
|
+
outerR: rOuter,
|
|
94
|
+
startAngle: startRad,
|
|
95
|
+
endAngle: angleScale(value),
|
|
96
|
+
}),
|
|
97
|
+
value,
|
|
98
|
+
itemFill,
|
|
99
|
+
index: i,
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Build the slice geometry for a single pie/donut series. Each input datum
|
|
106
|
+
* becomes one path with its `d` string generated from `d3-shape`'s `arc()`.
|
|
107
|
+
*
|
|
108
|
+
* Pure data — caller renders one `<path d={slice.d}>` per spec, keeping the
|
|
109
|
+
* d3-shape dependency inside `@barefootjs/chart` and out of consumer bundles.
|
|
110
|
+
*/
|
|
111
|
+
export function buildPieSlices(
|
|
112
|
+
data: Record<string, unknown>[],
|
|
113
|
+
dataKey: string,
|
|
114
|
+
nameKey: string,
|
|
115
|
+
config: Record<string, { label: string; color: string }>,
|
|
116
|
+
width: number,
|
|
117
|
+
height: number,
|
|
118
|
+
innerRadiusRatio: number,
|
|
119
|
+
outerRadiusRatio: number,
|
|
120
|
+
paddingAngleDeg: number,
|
|
121
|
+
): PieSliceSpec[] {
|
|
122
|
+
if (data.length === 0) return []
|
|
123
|
+
|
|
124
|
+
const radius = Math.min(width, height) / 2
|
|
125
|
+
|
|
126
|
+
const pieData = data.map((d) => {
|
|
127
|
+
const name = String(d[nameKey] ?? '')
|
|
128
|
+
const value = Number(d[dataKey]) || 0
|
|
129
|
+
const configEntry = config[name]
|
|
130
|
+
const fill = configEntry?.color ?? 'currentColor'
|
|
131
|
+
return { name, value, fill }
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
const pieLayout = d3Pie<{ name: string; value: number; fill: string }>()
|
|
135
|
+
.value((d) => d.value)
|
|
136
|
+
.padAngle((paddingAngleDeg * Math.PI) / 180)
|
|
137
|
+
.sort(null)
|
|
138
|
+
|
|
139
|
+
const arcGenerator = d3Arc<PieArcDatum<{ name: string; value: number; fill: string }>>()
|
|
140
|
+
.innerRadius(radius * innerRadiusRatio)
|
|
141
|
+
.outerRadius(radius * outerRadiusRatio)
|
|
142
|
+
|
|
143
|
+
return pieLayout(pieData).map((arcDatum) => ({
|
|
144
|
+
d: arcGenerator(arcDatum) ?? '',
|
|
145
|
+
fill: arcDatum.data.fill,
|
|
146
|
+
name: arcDatum.data.name,
|
|
147
|
+
value: arcDatum.data.value,
|
|
148
|
+
}))
|
|
149
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { area as d3Area, curveLinear } from 'd3-shape'
|
|
2
|
+
import type { ScalePoint, ScaleLinear } from 'd3-scale'
|
|
3
|
+
|
|
4
|
+
export interface AreaPaths {
|
|
5
|
+
/** Filled area path from the bottom of the chart up to each y value. */
|
|
6
|
+
area: string
|
|
7
|
+
/** Stroke-only path along the top of the area (degenerate area where y0 === y1). */
|
|
8
|
+
line: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface AreaDot {
|
|
12
|
+
key: string
|
|
13
|
+
cx: number
|
|
14
|
+
cy: number
|
|
15
|
+
xValue: string
|
|
16
|
+
yValue: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Build the SVG `d` attributes for an area series. Pure helper — d3-shape
|
|
21
|
+
* stays inside `@barefootjs/chart` so consumer bundles avoid pulling in bare
|
|
22
|
+
* specifiers the browser import map does not resolve.
|
|
23
|
+
*/
|
|
24
|
+
export function buildAreaPaths(
|
|
25
|
+
data: Record<string, unknown>[],
|
|
26
|
+
xKey: string,
|
|
27
|
+
yKey: string,
|
|
28
|
+
xs: ScalePoint<string>,
|
|
29
|
+
ys: ScaleLinear<number, number>,
|
|
30
|
+
innerHeight: number,
|
|
31
|
+
): AreaPaths {
|
|
32
|
+
const fillGen = d3Area<Record<string, unknown>>()
|
|
33
|
+
.x((d) => xs(String(d[xKey])) ?? 0)
|
|
34
|
+
.y0(innerHeight)
|
|
35
|
+
.y1((d) => ys(Number(d[yKey]) || 0))
|
|
36
|
+
.curve(curveLinear)
|
|
37
|
+
|
|
38
|
+
const lineGen = d3Area<Record<string, unknown>>()
|
|
39
|
+
.x((d) => xs(String(d[xKey])) ?? 0)
|
|
40
|
+
.y0((d) => ys(Number(d[yKey]) || 0))
|
|
41
|
+
.y1((d) => ys(Number(d[yKey]) || 0))
|
|
42
|
+
.curve(curveLinear)
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
area: fillGen(data) ?? '',
|
|
46
|
+
line: lineGen(data) ?? '',
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Build the per-point geometry used to render invisible hover-target dots. */
|
|
51
|
+
export function buildAreaDots(
|
|
52
|
+
data: Record<string, unknown>[],
|
|
53
|
+
xKey: string,
|
|
54
|
+
yKey: string,
|
|
55
|
+
xs: ScalePoint<string>,
|
|
56
|
+
ys: ScaleLinear<number, number>,
|
|
57
|
+
): AreaDot[] {
|
|
58
|
+
return data.map((datum) => {
|
|
59
|
+
const xValue = String(datum[xKey])
|
|
60
|
+
const yValue = Number(datum[yKey]) || 0
|
|
61
|
+
return {
|
|
62
|
+
key: `${yKey}-${xValue}`,
|
|
63
|
+
cx: xs(xValue) ?? 0,
|
|
64
|
+
cy: ys(yValue),
|
|
65
|
+
xValue,
|
|
66
|
+
yValue,
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable CSS class names for chart primitives.
|
|
3
|
+
*
|
|
4
|
+
* Exported as constants so consumers can reference them via
|
|
5
|
+
* `<g className={CHART_CLASS_X_AXIS}>` in `"use client"` files. The compiler's
|
|
6
|
+
* `cssLayerPrefixer` only rewrites locally-declared constants and static
|
|
7
|
+
* className literals; imported identifiers are left alone, which keeps the
|
|
8
|
+
* names un-prefixed for the e2e test selectors (`.chart-x-axis text`, ...).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const CHART_CLASS_GRID = 'chart-grid'
|
|
12
|
+
export const CHART_CLASS_X_AXIS = 'chart-x-axis'
|
|
13
|
+
export const CHART_CLASS_Y_AXIS = 'chart-y-axis'
|
|
14
|
+
export const CHART_CLASS_POLAR_GRID = 'chart-polar-grid'
|
|
15
|
+
export const CHART_CLASS_POLAR_ANGLE_AXIS = 'chart-polar-angle-axis'
|
|
16
|
+
export const CHART_CLASS_RADIAL_BAR = 'chart-radial-bar'
|
|
17
|
+
export const CHART_CLASS_RADIAL_LABEL = 'chart-radial-label'
|
|
18
|
+
export const CHART_CLASS_BAR = 'chart-bar'
|
|
19
|
+
export const CHART_CLASS_LINE = 'chart-line'
|
|
20
|
+
export const CHART_CLASS_AREA = 'chart-area'
|
|
21
|
+
export const CHART_CLASS_AREA_DOT = 'chart-area-dot'
|
|
22
|
+
export const CHART_CLASS_RADAR = 'chart-radar'
|
|
23
|
+
export const CHART_CLASS_PIE = 'chart-pie'
|
|
24
|
+
export const CHART_CLASS_TOOLTIP = 'chart-tooltip'
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { line as d3Line, curveMonotoneX, curveLinear } from 'd3-shape'
|
|
2
|
+
import type { ScaleBand } from 'd3-scale'
|
|
3
|
+
import type { ScaleLinear } from 'd3-scale'
|
|
4
|
+
|
|
5
|
+
export interface LinePoint {
|
|
6
|
+
key: string
|
|
7
|
+
cx: number
|
|
8
|
+
cy: number
|
|
9
|
+
xValue: string
|
|
10
|
+
yValue: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Build the SVG `d` attribute for a line series. Pure helper — d3-shape stays
|
|
15
|
+
* inside `@barefootjs/chart` so consumer bundles avoid pulling in bare
|
|
16
|
+
* specifiers the browser import map does not resolve.
|
|
17
|
+
*/
|
|
18
|
+
export function buildLinePath(
|
|
19
|
+
data: Record<string, unknown>[],
|
|
20
|
+
xKey: string,
|
|
21
|
+
yKey: string,
|
|
22
|
+
xs: ScaleBand<string>,
|
|
23
|
+
ys: ScaleLinear<number, number>,
|
|
24
|
+
type: 'linear' | 'monotone',
|
|
25
|
+
): string {
|
|
26
|
+
const bandwidth = xs.bandwidth()
|
|
27
|
+
const points: [number, number][] = data.map((datum) => {
|
|
28
|
+
const xValue = String(datum[xKey])
|
|
29
|
+
const yValue = Number(datum[yKey]) || 0
|
|
30
|
+
return [(xs(xValue) ?? 0) + bandwidth / 2, ys(yValue)]
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const generator = d3Line<[number, number]>()
|
|
34
|
+
.x((d) => d[0])
|
|
35
|
+
.y((d) => d[1])
|
|
36
|
+
generator.curve(type === 'monotone' ? curveMonotoneX : curveLinear)
|
|
37
|
+
|
|
38
|
+
return generator(points) ?? ''
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Build the per-point geometry used to render hover/visual dots. */
|
|
42
|
+
export function buildLinePoints(
|
|
43
|
+
data: Record<string, unknown>[],
|
|
44
|
+
xKey: string,
|
|
45
|
+
yKey: string,
|
|
46
|
+
xs: ScaleBand<string>,
|
|
47
|
+
ys: ScaleLinear<number, number>,
|
|
48
|
+
): LinePoint[] {
|
|
49
|
+
const bandwidth = xs.bandwidth()
|
|
50
|
+
return data.map((datum) => {
|
|
51
|
+
const xValue = String(datum[xKey])
|
|
52
|
+
const yValue = Number(datum[yKey]) || 0
|
|
53
|
+
return {
|
|
54
|
+
key: `${yKey}-${xValue}`,
|
|
55
|
+
cx: (xs(xValue) ?? 0) + bandwidth / 2,
|
|
56
|
+
cy: ys(yValue),
|
|
57
|
+
xValue,
|
|
58
|
+
yValue,
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ScaleLinear } from 'd3-scale'
|
|
2
|
+
|
|
3
|
+
export interface RadarVertex {
|
|
4
|
+
/** Stable identifier — combines axis index and label for keyed mapArray. */
|
|
5
|
+
key: string
|
|
6
|
+
/** Polar angle in radians; 0 points up, increases clockwise. */
|
|
7
|
+
angle: number
|
|
8
|
+
/** Polar radius from the chart centre. */
|
|
9
|
+
r: number
|
|
10
|
+
/** Cartesian x coordinate (radius * cos(angle)). */
|
|
11
|
+
x: number
|
|
12
|
+
/** Cartesian y coordinate (radius * sin(angle)). */
|
|
13
|
+
y: number
|
|
14
|
+
/** Axis label drawn from `data[i][axisKey]`. */
|
|
15
|
+
label: string
|
|
16
|
+
/** Numeric series value drawn from `data[i][dataKey]`. */
|
|
17
|
+
value: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Compute the per-axis vertices for a radar series. Each input datum becomes
|
|
22
|
+
* one vertex placed at angle = `(2π / n) * i - π/2` and radius = `radialScale(value)`.
|
|
23
|
+
*/
|
|
24
|
+
export function buildRadarVertices(
|
|
25
|
+
data: Record<string, unknown>[],
|
|
26
|
+
dataKey: string,
|
|
27
|
+
axisKey: string,
|
|
28
|
+
radialScale: ScaleLinear<number, number>,
|
|
29
|
+
): RadarVertex[] {
|
|
30
|
+
const n = data.length
|
|
31
|
+
if (n === 0) return []
|
|
32
|
+
const angleStep = (2 * Math.PI) / n
|
|
33
|
+
return data.map((datum, i) => {
|
|
34
|
+
const value = Number(datum[dataKey]) || 0
|
|
35
|
+
const label = String(datum[axisKey])
|
|
36
|
+
const angle = angleStep * i - Math.PI / 2
|
|
37
|
+
const r = radialScale(value)
|
|
38
|
+
return {
|
|
39
|
+
key: `${i}-${label}`,
|
|
40
|
+
angle,
|
|
41
|
+
r,
|
|
42
|
+
x: r * Math.cos(angle),
|
|
43
|
+
y: r * Math.sin(angle),
|
|
44
|
+
label,
|
|
45
|
+
value,
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Convert vertices into the `points` attribute of a `<polygon>`. */
|
|
51
|
+
export function buildRadarPolygonPoints(vertices: RadarVertex[]): string {
|
|
52
|
+
return vertices.map((v) => `${v.x},${v.y}`).join(' ')
|
|
53
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { scaleBand, scaleLinear, scalePoint, type ScaleBand, type ScaleLinear, type ScalePoint } from 'd3-scale'
|
|
2
|
+
import { max } from 'd3-array'
|
|
3
|
+
|
|
4
|
+
export function createBandScale(
|
|
5
|
+
data: Record<string, unknown>[],
|
|
6
|
+
dataKey: string,
|
|
7
|
+
width: number,
|
|
8
|
+
): ScaleBand<string> {
|
|
9
|
+
return scaleBand<string>()
|
|
10
|
+
.domain(data.map((d) => String(d[dataKey])))
|
|
11
|
+
.range([0, width])
|
|
12
|
+
.padding(0.2)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createLinearScale(
|
|
16
|
+
data: Record<string, unknown>[],
|
|
17
|
+
dataKeys: string[],
|
|
18
|
+
height: number,
|
|
19
|
+
): ScaleLinear<number, number> {
|
|
20
|
+
const maxValue =
|
|
21
|
+
max(data, (d) =>
|
|
22
|
+
max(dataKeys, (key) => {
|
|
23
|
+
const v = d[key]
|
|
24
|
+
return typeof v === 'number' ? v : 0
|
|
25
|
+
}),
|
|
26
|
+
) ?? 0
|
|
27
|
+
return scaleLinear()
|
|
28
|
+
.domain([0, maxValue])
|
|
29
|
+
.nice()
|
|
30
|
+
.range([height, 0])
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function createPointScale(
|
|
34
|
+
data: Record<string, unknown>[],
|
|
35
|
+
dataKey: string,
|
|
36
|
+
width: number,
|
|
37
|
+
): ScalePoint<string> {
|
|
38
|
+
return scalePoint<string>()
|
|
39
|
+
.domain(data.map((d) => String(d[dataKey])))
|
|
40
|
+
.range([0, width])
|
|
41
|
+
.padding(0.5)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Linear scale spanning `[0, max(data values across dataKeys)]`, mapped to
|
|
46
|
+
* `[0, radius]`. Used by RadarChart to convert numeric series values into
|
|
47
|
+
* polar radii. Returns null when no dataKeys are supplied so callers can
|
|
48
|
+
* cheaply gate downstream rendering on registration completing.
|
|
49
|
+
*/
|
|
50
|
+
export function createRadarRadialScale(
|
|
51
|
+
data: Record<string, unknown>[],
|
|
52
|
+
dataKeys: string[],
|
|
53
|
+
radius: number,
|
|
54
|
+
): ScaleLinear<number, number> | null {
|
|
55
|
+
if (dataKeys.length === 0) return null
|
|
56
|
+
const maxValue =
|
|
57
|
+
max(data, (d) =>
|
|
58
|
+
max(dataKeys, (key) => {
|
|
59
|
+
const v = d[key]
|
|
60
|
+
return typeof v === 'number' ? v : 0
|
|
61
|
+
}),
|
|
62
|
+
) ?? 0
|
|
63
|
+
return scaleLinear<number, number>().domain([0, maxValue]).nice().range([0, radius])
|
|
64
|
+
}
|