@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/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
+ }