@rokkit/chart 1.3.15 → 1.4.1

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.
Files changed (47) hide show
  1. package/dist/Plot.svelte +25 -9
  2. package/dist/PlotState.svelte.d.ts +1 -0
  3. package/dist/PlotState.svelte.js +19 -8
  4. package/dist/Spark.svelte +165 -0
  5. package/dist/Spark.svelte.d.ts +31 -0
  6. package/dist/SparkState.svelte.d.ts +138 -0
  7. package/dist/SparkState.svelte.js +331 -0
  8. package/dist/Sparkline.svelte +62 -237
  9. package/dist/Sparkline.svelte.d.ts +2 -0
  10. package/dist/charts/RadarChart.svelte +106 -0
  11. package/dist/charts/RadarChart.svelte.d.ts +43 -0
  12. package/dist/geoms/Area.svelte +24 -9
  13. package/dist/geoms/Area.svelte.d.ts +2 -0
  14. package/dist/geoms/Bar.svelte +9 -16
  15. package/dist/geoms/Highlight.svelte +61 -40
  16. package/dist/geoms/Line.svelte +10 -9
  17. package/dist/geoms/Point.svelte +9 -15
  18. package/dist/geoms/Radar.svelte +352 -0
  19. package/dist/geoms/Radar.svelte.d.ts +42 -0
  20. package/dist/geoms/Trend.svelte +46 -23
  21. package/dist/geoms/lib/aesthetics.d.ts +14 -0
  22. package/dist/geoms/lib/aesthetics.js +21 -0
  23. package/dist/geoms/lib/areas.d.ts +9 -2
  24. package/dist/geoms/lib/areas.js +53 -8
  25. package/dist/geoms/lib/marks/area.d.ts +3 -0
  26. package/dist/geoms/lib/marks/area.js +11 -2
  27. package/dist/geoms/lib/marks/radar.d.ts +68 -0
  28. package/dist/geoms/lib/marks/radar.js +153 -0
  29. package/dist/index.d.ts +6 -0
  30. package/dist/index.js +7 -1
  31. package/dist/lib/brewing/marks/boxes.js +19 -30
  32. package/dist/lib/brewing/polar.d.ts +444 -0
  33. package/dist/lib/brewing/polar.js +645 -0
  34. package/dist/lib/highlight.js +84 -33
  35. package/dist/lib/plot/scales.js +8 -5
  36. package/dist/lib/preset.d.ts +1 -0
  37. package/dist/lib/preset.js +2 -1
  38. package/dist/lib/select.d.ts +25 -11
  39. package/dist/lib/select.js +14 -7
  40. package/dist/lib/trend.js +55 -39
  41. package/package.json +1 -1
  42. package/dist/examples/BarChartExample.svelte +0 -78
  43. package/dist/examples/BarChartExample.svelte.d.ts +0 -18
  44. package/dist/lib/brewing/types.d.ts +0 -162
  45. package/dist/lib/brewing/types.js +0 -73
  46. package/dist/lib/xscale.d.ts +0 -11
  47. package/dist/lib/xscale.js +0 -31
@@ -0,0 +1,444 @@
1
+ /**
2
+ * Resolves the `axes` prop of a Radar chart to a normalised, ordered list.
3
+ *
4
+ * When `axes` is supplied it is the sole source of truth for both membership and
5
+ * order: axes it names but the data lacks are kept anyway (the geom renders an empty
6
+ * spoke for them — better a visible gap than a silently reshaped polygon), and data
7
+ * axes it doesn't name are dropped, with a dev warning, since silently adding an
8
+ * un-requested spoke would change the shape just as surely as silently reordering one.
9
+ *
10
+ * When `axes` is omitted entirely we fall back to first-appearance order in the data.
11
+ * That fallback exists so a bare `<Radar axis="metric" value="score" />` still renders
12
+ * something, but because radar shape is order-sensitive we warn every time it's used —
13
+ * the resulting order depends on incidental upstream row order, not an analytical
14
+ * choice, and should not be relied on for anything the caller shows to someone else.
15
+ *
16
+ * @param {(string|AxisSpec)[]|undefined} axes
17
+ * @param {Object[]} data
18
+ * @param {string} axisField - the data field naming which axis a row belongs to
19
+ * @returns {ResolvedAxis[]}
20
+ */
21
+ export function resolveAxes(axes: (string | AxisSpec)[] | undefined, data: Object[], axisField: string): ResolvedAxis[];
22
+ /**
23
+ * Angle in degrees for each axis — first axis at the top, proceeding clockwise.
24
+ *
25
+ * Each axis owns a wedge proportional to its weight and sits at that wedge's
26
+ * MIDPOINT, so the wedge is symmetric around its spoke. Midpoint placement alone
27
+ * would put axis 0 half a sector past the top, so axis 0's half-wedge is
28
+ * subtracted to rotate the whole diagram back.
29
+ *
30
+ * At equal weights this reduces exactly to -90 + i*360/n. A test asserts that for
31
+ * n=3..6, because an earlier draft of this formula omitted the rotation term and was
32
+ * off by 180/n for every n — nothing sat at the top.
33
+ *
34
+ * @param {number[]} weights - one per axis; non-positive or non-finite treated as 1
35
+ * @returns {number[]} degrees, -90 = top
36
+ */
37
+ export function anglesFor(weights: number[]): number[];
38
+ /**
39
+ * Value domain per axis for a radar/spider chart.
40
+ *
41
+ * A radar's most-cited honesty problem is that per-axis domains are usually INFERRED from
42
+ * whichever rows happen to be present: filter a comparator series out of `data` and every
43
+ * remaining series' radius moves, because the domain that normalises it just shrank — the
44
+ * rendered shape becomes a property of the query, not of the entity being plotted. A
45
+ * DECLARED `AxisSpec.domain` fixes this by being used verbatim regardless of which rows are
46
+ * in `data`, so removing a comparator moves nothing. That is why declaring `domain` is the
47
+ * documented recommendation rather than an advanced option — this module's spec asserts the
48
+ * stability directly: the same series computes identical domains with and without a
49
+ * much-larger comparator series under a declared domain, and DIFFERENT domains under an
50
+ * inferred one, so the distinction is provably real rather than assumed.
51
+ *
52
+ * Inferred domains (no declared `domain`) are `[0, max]` for non-negative data. An axis whose
53
+ * values include a negative infers `[min, max]` instead of clamping the low end to 0 —
54
+ * clamping would silently delete the part of the shape that dips below centre, which is a
55
+ * worse failure than an occasionally-asymmetric domain. Non-finite values (a stray string, a
56
+ * missing field) are filtered out of the min/max computation entirely, so they cannot poison
57
+ * a domain to `NaN`.
58
+ *
59
+ * `opts.sharedDomain` collapses every undeclared axis onto one `[min, max]` computed across
60
+ * ALL axes' values (still applying the negatives-extend rule globally, so one negative
61
+ * anywhere widens every shared-mode axis down), for callers who want one consistent scale
62
+ * across mixed axes instead of a per-axis one. A declared `domain` still wins per-axis even
63
+ * when `sharedDomain` is on — declaring an axis is a stronger, more specific statement than a
64
+ * chart-wide default.
65
+ *
66
+ * An axis with no data at all (named in `axes` but absent from every row) gets the finite,
67
+ * degenerate domain `[0, 0]` rather than `NaN` — enough for a caller to render an empty spoke
68
+ * without the radius math blowing up.
69
+ *
70
+ * @param {ResolvedAxis[]} axes
71
+ * @param {Object[]} data
72
+ * @param {{ x: string, y: string }} channels - `x` names the axis field, `y` the value field
73
+ * @param {{ sharedDomain?: boolean }} [opts]
74
+ * @returns {[number, number][]} one domain per axis, positionally aligned with `axes`
75
+ */
76
+ export function domainsFor(axes: ResolvedAxis[], data: Object[], channels: {
77
+ x: string;
78
+ y: string;
79
+ }, opts?: {
80
+ sharedDomain?: boolean;
81
+ }): [number, number][];
82
+ /**
83
+ * Maps a value to a radius, in pixels, for one axis of a radar/spider chart.
84
+ *
85
+ * The centre is `domain.min`, NOT zero: on a domain of `[-5, 10]` with `R = 100`, a value of 0
86
+ * sits at `R * 5/15 ≈ 33.33`, not at the centre. Centring on 0 unconditionally would silently
87
+ * misplace every axis whose domain doesn't start at zero — this follows `domainsFor`'s own
88
+ * inferred-negative-domain rule (`[min, max]`, never clamped to 0): whatever `domainsFor` decided
89
+ * an axis's low end is, THAT is the value drawn at the centre, not literal zero.
90
+ *
91
+ * `transform` is `'linear'` (`R * ratio`) or `'sqrt'` (`R * sqrt(ratio)`) — see
92
+ * `resolveRadiusTransform` for why a caller would ever want the latter. Anything other than the
93
+ * literal string `'sqrt'` is treated as `'linear'`.
94
+ *
95
+ * Two failure modes are handled explicitly rather than left to produce `NaN`:
96
+ *
97
+ * - A zero-width domain (`min === max`, e.g. the `[0, 0]` `domainsFor` returns for an axis
98
+ * absent from every row) would divide by zero. Rather than `NaN`, it resolves to `0` — the
99
+ * centre — continuing the promise `domainsFor`'s own doc comment already makes for that case:
100
+ * "enough for a caller to render an empty spoke without the radius math blowing up."
101
+ * - A non-finite `value` (missing field, a stray string that slipped past upstream filtering)
102
+ * returns `null` rather than being coerced to some position. Plotting it anyway — at the
103
+ * centre, say — would silently reshape the polygon exactly the way `resolveAxes` already
104
+ * refuses to do for axis membership ("better a visible gap than a silently reshaped polygon").
105
+ * The geom is expected to break the line at a `null` vertex rather than draw a false one.
106
+ *
107
+ * A finite value outside `[min, max]` is clamped into `[0, R]` rather than left to overshoot the
108
+ * ring (linear) or hand a negative ratio to `sqrt`, which has no real root and would produce
109
+ * `NaN`. `domainsFor` treats an out-of-domain value as "the caller/renderer problem, not
110
+ * domainsFor's" — `radiusFor` *is* that renderer, so this is where the clamp belongs.
111
+ *
112
+ * @param {number} value
113
+ * @param {[number, number]} domain
114
+ * @param {number} R - outer radius in pixels
115
+ * @param {'linear'|'sqrt'} transform
116
+ * @returns {number|null} radius in pixels, or `null` for a non-finite value
117
+ */
118
+ export function radiusFor(value: number, domain: [number, number], R: number, transform: "linear" | "sqrt"): number | null;
119
+ /**
120
+ * Picks the radius transform for a radar chart, given its axis weights.
121
+ *
122
+ * A wedge's area is ~½θr². `anglesFor` already makes θ (a wedge's angular width) proportional to
123
+ * weight for a weighted radar. If radius stayed linear in value, a heavily-weighted axis would get
124
+ * both a wider wedge AND a radius growing at the ordinary linear rate for the same value — the two
125
+ * compound, so swept area grows faster than `weight × value`, stacking on top of radar's already
126
+ * well-known area exaggeration (a 2x difference in a linear radius already reads as ~4x in swept
127
+ * area). Making radius track `√value` cancels exactly one factor of that compounding: with
128
+ * `r² ∝ value`, area becomes `∝ θ * value ∝ weight * value` — the honest claim a weighted radar is
129
+ * making about its data in the first place. (This module's spec asserts that proportionality
130
+ * numerically rather than arguing it, since it is the entire reason this branch exists.)
131
+ *
132
+ * At equal weights every wedge is the same width regardless of transform, so there is nothing to
133
+ * compensate for — `'linear'` is used, matching conventional radar (whose area exaggeration is a
134
+ * documented, accepted limitation, not something this module tries to fix).
135
+ *
136
+ * `requested` lets a caller pin the transform explicitly; an explicit `'linear'` or `'sqrt'`
137
+ * always wins over the weight-derived choice, so a consumer who has deliberately chosen one never
138
+ * has it silently swapped out from under them the next time someone adds an unequal weight to the
139
+ * axes list.
140
+ *
141
+ * Weights are normalised the same way `anglesFor` normalises them (non-positive or non-finite
142
+ * treated as 1) before the equality check, so this function's answer always matches the wedges
143
+ * `anglesFor` actually draws for the same `weights` array.
144
+ *
145
+ * @param {number[]} weights - one per axis, the same array `anglesFor` would receive
146
+ * @param {'linear'|'sqrt'|'auto'} [requested] - defaults to `'auto'`
147
+ * @returns {'linear'|'sqrt'}
148
+ */
149
+ export function resolveRadiusTransform(weights: number[], requested?: "linear" | "sqrt" | "auto"): "linear" | "sqrt";
150
+ /**
151
+ * @typedef {Object} Vertex
152
+ * @property {string} axisKey - the axis this vertex belongs to
153
+ * @property {number} value - the plotted value; the average when the `(series, axis)` cell had
154
+ * more than one row
155
+ * @property {number} angle - degrees, taken from `angles` at this axis's position
156
+ * @property {number} radius - pixels, from `radiusFor`; never `null` here because `value` is
157
+ * always finite by construction (non-finite readings are filtered out before averaging)
158
+ * @property {Record<string, unknown>} row - one of the original rows for this cell, chosen so `===` identity with
159
+ * the container's `data` array survives averaging
160
+ */
161
+ /**
162
+ * Turns rows into one polygon's worth of vertices per series, positionally aligned with `axes`.
163
+ *
164
+ * A row belongs to the `(series, axis)` cell identified by `channels.color` (or the single
165
+ * implicit series `undefined`, when no series channel is given at all) crossed with
166
+ * `channels.x`. Two situations a naive per-cell lookup would mishandle are resolved explicitly,
167
+ * for reasons that matter more than they look like they should:
168
+ *
169
+ * DUPLICATE cells — more than one row landing on the same `(series, axis)` pair — are averaged
170
+ * HERE, inside this module, rather than by routing through the generic `stat`/`applyGeomStat`
171
+ * machinery the rest of the package uses for aggregation. `applyGeomStat` calls
172
+ * `groupDataByKeys`, which builds a FRESH row object containing only the group-by keys plus the
173
+ * summarised value — a new allocation that fails `===` identity against anything in the
174
+ * container's `data` array. Every geom in this package computes its `onselect` index as
175
+ * `plotState.data.indexOf(row)`, so an aggregated row would report `index: -1` and silently drop
176
+ * every field that wasn't a channel. That is not a rare edge case for radar: long-format
177
+ * `(series × axis)` data duplicating a cell is the single most common data-shape mistake a radar
178
+ * consumer will make, so aggregation cannot be allowed to degrade interactivity the way a generic
179
+ * stat would. The fix: average the values, but keep a reference to one of the ORIGINAL row
180
+ * objects (the first one encountered) as the vertex's `row`, so identity survives. A repeated
181
+ * `(series, axis)` pair also `console.warn`s in dev, matching this file's existing warn
182
+ * register — axis is a small, fixed enum rather than a repeated-measurement field, so a duplicate
183
+ * is almost always a mistake rather than intentional multiple sampling.
184
+ *
185
+ * MISSING cells — a `(series, axis)` pair with no matching row at all, or one whose only
186
+ * matching row(s) all have a non-finite value — resolve to `null`, never to a value of `0`. Zero
187
+ * is a real, meaningful position on almost every axis (the centre, per `radiusFor`'s own
188
+ * contract), so defaulting a gap to it would silently invent a score and render as a real low
189
+ * value rather than as missing data — the same "better a visible gap than a silently reshaped
190
+ * polygon" principle `resolveAxes` already applies to axis membership. The geom is expected to
191
+ * break the polygon's outline at a `null` vertex instead of drawing a line through it.
192
+ *
193
+ * The geometry arguments travel as one bag rather than six positionals, matching `ringsFor`'s
194
+ * `(axes, { R, rings, domains, transform })` shape. At the call site that is the difference
195
+ * between `verticesFor(data, axes, angles, domains, 100, 'linear', channels)` — where the reader
196
+ * has to count commas to learn what `100` and `'linear'` mean — and naming each one.
197
+ *
198
+ * @param {Object[]} data
199
+ * @param {ResolvedAxis[]} axes - from `resolveAxes`; vertex order follows this array
200
+ * @param {{ x: string, y: string, color?: string }} channels - `x` names the axis field, `y` the
201
+ * value field, `color` the series field — mirroring `fill ?? color`'s role as the series
202
+ * channel in every other geom. Omitting it entirely means one implicit series.
203
+ * @param {Object} geometry
204
+ * @param {number[]} geometry.angles - from `anglesFor(axes.map(a => a.weight))`, aligned with `axes`
205
+ * @param {[number, number][]} geometry.domains - from `domainsFor`, aligned with `axes`
206
+ * @param {number} geometry.R - outer radius in pixels, forwarded to `radiusFor`
207
+ * @param {'linear'|'sqrt'} geometry.transform - forwarded to `radiusFor`
208
+ * @returns {Map<unknown, (Vertex|null)[]>} one entry per distinct series value (or the single key
209
+ * `undefined` when `channels.color` is omitted), each holding one vertex — or `null` for a
210
+ * gap — per axis, in `axes` order
211
+ */
212
+ export function verticesFor(data: Object[], axes: ResolvedAxis[], channels: {
213
+ x: string;
214
+ y: string;
215
+ color?: string;
216
+ }, { angles, domains, R, transform }: {
217
+ angles: number[];
218
+ domains: [number, number][];
219
+ R: number;
220
+ transform: "linear" | "sqrt";
221
+ }): Map<unknown, (Vertex | null)[]>;
222
+ /**
223
+ * Concentric grid rings for a radar chart — one shared set of circles, evenly spaced in
224
+ * RADIUS, that every spoke is drawn against.
225
+ *
226
+ * Ring *i* sits at `(i / ringCount) * R`. This follows directly from `radiusFor`'s own linear
227
+ * mapping between radius and normalised position: since every spoke shares one `R` and the
228
+ * grid is one set of concentric circles (not one grid per axis), the only spacing that keeps
229
+ * "ring 2 is twice as far out as ring 1" true on EVERY spoke simultaneously is spacing by
230
+ * radius. Spacing the rings evenly in VALUE instead would only coincide with this for a
231
+ * domain that starts at 0 (see the spec: a `[20, 120]`-style domain exposes the divergence,
232
+ * because splitting an unanchored domain into equal value steps and converting each through
233
+ * `radiusFor` on ITS OWN axis does not reduce to `(i/ringCount) * R` the way it would for a
234
+ * zero-anchored domain).
235
+ *
236
+ * Ring COUNT has to be one number for the whole grid, because there is only one grid, drawn
237
+ * once. When every `AxisSpec` declares the same `ticks`, that count is used verbatim and ring
238
+ * *k* genuinely means "k of n" on every spoke. When they disagree, honouring any one axis's
239
+ * count would silently mislabel every other axis's rings, so this falls back to `opts.rings`
240
+ * (default 4) and warns — the geom cannot draw a different number of rings per spoke.
241
+ *
242
+ * Ring LABELS are resolved per axis (each ring has one label per axis, since the same radius
243
+ * means a different value on each spoke): a declared `tickLabels` entry wins outright — it is
244
+ * the ordinal vocabulary of a Likert-style axis and has no "value" to compute at all. Absent
245
+ * that, the ring's value on that axis (via `valueAtRing`) is run through the axis's `format`
246
+ * if one is given, else left as the bare number. A DECLARED domain makes this value a stable,
247
+ * meaningful tick; an INFERRED one (no `domain` supplied anywhere for this axis) makes it only
248
+ * an approximation of "if the current rows were the whole story."
249
+ *
250
+ * @param {ResolvedAxis[]} axes
251
+ * @param {{ R: number, rings?: number, domains?: [number, number][], transform?: 'linear'|'sqrt' }} opts
252
+ * @returns {Ring[]}
253
+ */
254
+ export function ringsFor(axes: ResolvedAxis[], opts?: {
255
+ R: number;
256
+ rings?: number;
257
+ domains?: [number, number][];
258
+ transform?: "linear" | "sqrt";
259
+ }): Ring[];
260
+ /**
261
+ * Zero-reference marker, per axis — a dashed ring segment (in the geom, not here) at the
262
+ * radius where the value 0 actually sits on that spoke.
263
+ *
264
+ * `radiusFor` centres each spoke on `domain.min`, not on 0, because `domainsFor` lets a
265
+ * negative value extend a domain rather than clamping it. That is honest about the data but
266
+ * has a real cost: the hub means a DIFFERENT value on every spoke whose domain doesn't start
267
+ * at 0, so a reader has no way to tell where "nothing" is just by looking at the centre. An
268
+ * all-zeros row would then bow OUTWARD to wherever 0 lands on each axis instead of collapsing
269
+ * to a point at the centre — the opposite of how a radar's most basic case is meant to read.
270
+ *
271
+ * So: any axis whose domain CONTAINS zero (`min <= 0 <= max`) but does not START there
272
+ * (`min !== 0`) gets a marker at `radiusFor(0, domain, R, transform)`. Two axes need none:
273
+ *
274
+ * - `min === 0` — the hub already is zero, nothing to mark.
275
+ * - the domain EXCLUDES zero entirely (`min > 0` or `max < 0`) — there is no honest position
276
+ * for "zero" ON this spoke at all. `radiusFor` would clamp value `0` to whichever end of
277
+ * `[0, R]` is nearest, which is exactly the position `domain.min` already occupies — drawing
278
+ * a "this is zero" marker there would misrepresent `domain.min` as zero, which is worse than
279
+ * drawing nothing. This module returns `null` for that case rather than a clamped radius.
280
+ *
281
+ * @param {ResolvedAxis[]} axes
282
+ * @param {[number, number][]} domains - aligned with `axes`, from `domainsFor`
283
+ * @param {number} R
284
+ * @param {'linear'|'sqrt'} transform
285
+ * @returns {(number|null)[]} one entry per axis, positionally aligned with `axes`
286
+ */
287
+ export function zeroRingFor(axes: ResolvedAxis[], domains: [number, number][], R: number, transform: "linear" | "sqrt"): (number | null)[];
288
+ /**
289
+ * @typedef {Object} RadarLayout
290
+ * @property {ResolvedAxis[]} axes
291
+ * @property {number[]} angles - from `anglesFor`, aligned with `axes`
292
+ * @property {[number, number][]} domains - from `domainsFor`, aligned with `axes`
293
+ * @property {Map<unknown, (import('./polar.js').Vertex|null)[]>} series - from `verticesFor`
294
+ * @property {Ring[]} rings - from `ringsFor`
295
+ * @property {(number|null)[]} zeroRings - from `zeroRingFor`, aligned with `axes`
296
+ * @property {number} radius - the outer radius, `R`, every other measurement is relative to
297
+ * @property {'linear'|'sqrt'} transform - resolved via `resolveRadiusTransform`
298
+ */
299
+ /**
300
+ * Composes every piece of this module into the one call a `Radar` geom actually needs — the
301
+ * public entry point, and the reason the rest of the module is allowed to stay a set of small,
302
+ * separately-testable functions instead of one large one.
303
+ *
304
+ * This function makes the ordering dependency between those functions explicit, because it is
305
+ * real, not incidental: `axes` must exist before weights can be read off it for `anglesFor`/
306
+ * `resolveRadiusTransform`; domains must exist before `verticesFor` (which places vertices) or
307
+ * `zeroRingFor` (which needs to know each axis's centre) can run; and the resolved `transform`
308
+ * must be shared between `verticesFor` and `ringsFor`/`zeroRingFor` so a ring drawn at "80% of
309
+ * R" and a vertex plotted at "80% of R" agree on what value that actually represents.
310
+ *
311
+ * `channels` follows the same `{ x, y, color? }` convention as `domainsFor`/`verticesFor` — `x`
312
+ * names the axis field, `y` the value field, `color` the series field. `opts.axes` is the
313
+ * `Radar` component's own `axes` prop (bare strings, full `AxisSpec`s, or a mix — see
314
+ * `resolveAxes`); `opts.R` defaults to `100` so this function is independently callable (by
315
+ * tests, tooling, anything that isn't a geom mid-render) without having to invent a pixel
316
+ * radius — a real `Radar.svelte` is expected to pass its actual computed outer radius instead.
317
+ *
318
+ * @param {Object[]} data
319
+ * @param {{ x: string, y: string, color?: string }} channels
320
+ * @param {{ axes?: (string|AxisSpec)[], sharedDomain?: boolean, rings?: number,
321
+ * radiusScale?: 'linear'|'sqrt'|'auto', R?: number }} [opts]
322
+ * @returns {RadarLayout}
323
+ */
324
+ export function buildRadarLayout(data: Object[], channels: {
325
+ x: string;
326
+ y: string;
327
+ color?: string;
328
+ }, opts?: {
329
+ axes?: (string | AxisSpec)[];
330
+ sharedDomain?: boolean;
331
+ rings?: number;
332
+ radiusScale?: "linear" | "sqrt" | "auto";
333
+ R?: number;
334
+ }): RadarLayout;
335
+ export type AxisSpec = {
336
+ /**
337
+ * - field name in the data this axis reads from
338
+ */
339
+ key: string;
340
+ /**
341
+ * - display label; defaults to `key`
342
+ */
343
+ label?: string | undefined;
344
+ /**
345
+ * - appended to the label in parentheses for `displayLabel`
346
+ */
347
+ unit?: string | undefined;
348
+ /**
349
+ * - explicit value domain; otherwise inferred later
350
+ */
351
+ domain?: [number, number] | undefined;
352
+ /**
353
+ * - number of tick rings to draw
354
+ */
355
+ ticks?: number | undefined;
356
+ /**
357
+ * - custom labels for each tick ring
358
+ */
359
+ tickLabels?: string[] | undefined;
360
+ /**
361
+ * - value formatter for ticks/tooltips
362
+ */
363
+ format?: Function | undefined;
364
+ /**
365
+ * - relative angular share of the circle; defaults to 1
366
+ */
367
+ weight?: number | undefined;
368
+ };
369
+ export type ResolvedAxis = {
370
+ key: string;
371
+ label: string;
372
+ displayLabel: string;
373
+ domain?: [number, number] | undefined;
374
+ ticks?: number | undefined;
375
+ tickLabels?: string[] | undefined;
376
+ format?: Function | undefined;
377
+ weight: number;
378
+ };
379
+ export type Vertex = {
380
+ /**
381
+ * - the axis this vertex belongs to
382
+ */
383
+ axisKey: string;
384
+ /**
385
+ * - the plotted value; the average when the `(series, axis)` cell had
386
+ * more than one row
387
+ */
388
+ value: number;
389
+ /**
390
+ * - degrees, taken from `angles` at this axis's position
391
+ */
392
+ angle: number;
393
+ /**
394
+ * - pixels, from `radiusFor`; never `null` here because `value` is
395
+ * always finite by construction (non-finite readings are filtered out before averaging)
396
+ */
397
+ radius: number;
398
+ /**
399
+ * - one of the original rows for this cell, chosen so `===` identity with
400
+ * the container's `data` array survives averaging
401
+ */
402
+ row: Record<string, unknown>;
403
+ };
404
+ export type Ring = {
405
+ /**
406
+ * - pixels from centre
407
+ */
408
+ radius: number;
409
+ /**
410
+ * - one label per axis, positionally aligned with `axes`
411
+ */
412
+ labels: (string | number)[];
413
+ };
414
+ export type RadarLayout = {
415
+ axes: ResolvedAxis[];
416
+ /**
417
+ * - from `anglesFor`, aligned with `axes`
418
+ */
419
+ angles: number[];
420
+ /**
421
+ * - from `domainsFor`, aligned with `axes`
422
+ */
423
+ domains: [number, number][];
424
+ /**
425
+ * - from `verticesFor`
426
+ */
427
+ series: Map<unknown, (import("./polar.js").Vertex | null)[]>;
428
+ /**
429
+ * - from `ringsFor`
430
+ */
431
+ rings: Ring[];
432
+ /**
433
+ * - from `zeroRingFor`, aligned with `axes`
434
+ */
435
+ zeroRings: (number | null)[];
436
+ /**
437
+ * - the outer radius, `R`, every other measurement is relative to
438
+ */
439
+ radius: number;
440
+ /**
441
+ * - resolved via `resolveRadiusTransform`
442
+ */
443
+ transform: "linear" | "sqrt";
444
+ };