@rokkit/chart 1.3.15 → 1.4.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.
Files changed (44) hide show
  1. package/dist/Plot.svelte +25 -9
  2. package/dist/PlotState.svelte.d.ts +1 -0
  3. package/dist/PlotState.svelte.js +8 -1
  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 +59 -232
  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 +17 -2
  13. package/dist/geoms/Area.svelte.d.ts +2 -0
  14. package/dist/geoms/Bar.svelte +2 -9
  15. package/dist/geoms/Highlight.svelte +2 -7
  16. package/dist/geoms/Line.svelte +2 -8
  17. package/dist/geoms/Point.svelte +2 -8
  18. package/dist/geoms/Radar.svelte +352 -0
  19. package/dist/geoms/Radar.svelte.d.ts +42 -0
  20. package/dist/geoms/lib/aesthetics.d.ts +14 -0
  21. package/dist/geoms/lib/aesthetics.js +21 -0
  22. package/dist/geoms/lib/areas.d.ts +9 -2
  23. package/dist/geoms/lib/areas.js +53 -8
  24. package/dist/geoms/lib/marks/area.d.ts +3 -0
  25. package/dist/geoms/lib/marks/area.js +11 -2
  26. package/dist/geoms/lib/marks/radar.d.ts +68 -0
  27. package/dist/geoms/lib/marks/radar.js +153 -0
  28. package/dist/index.d.ts +6 -0
  29. package/dist/index.js +7 -1
  30. package/dist/lib/brewing/marks/boxes.js +19 -30
  31. package/dist/lib/brewing/polar.d.ts +433 -0
  32. package/dist/lib/brewing/polar.js +584 -0
  33. package/dist/lib/plot/scales.js +8 -5
  34. package/dist/lib/preset.d.ts +1 -0
  35. package/dist/lib/preset.js +2 -1
  36. package/dist/lib/select.d.ts +3 -3
  37. package/dist/lib/select.js +1 -1
  38. package/package.json +1 -1
  39. package/dist/examples/BarChartExample.svelte +0 -78
  40. package/dist/examples/BarChartExample.svelte.d.ts +0 -18
  41. package/dist/lib/brewing/types.d.ts +0 -162
  42. package/dist/lib/brewing/types.js +0 -73
  43. package/dist/lib/xscale.d.ts +0 -11
  44. package/dist/lib/xscale.js +0 -31
package/dist/Plot.svelte CHANGED
@@ -20,6 +20,7 @@
20
20
  import Waterfall from './geoms/Waterfall.svelte'
21
21
  import Hexbin from './geoms/Hexbin.svelte'
22
22
  import Ribbon from './geoms/Ribbon.svelte'
23
+ import Radar from './geoms/Radar.svelte'
23
24
  import Highlight from './geoms/Highlight.svelte'
24
25
  import Trend from './geoms/Trend.svelte'
25
26
 
@@ -112,9 +113,7 @@
112
113
 
113
114
  const gridValue = $derived(spec?.grid ?? grid)
114
115
  const showGrid = $derived(gridValue !== false)
115
- const gridLines = $derived(
116
- gridValue === true || gridValue === false ? 'auto' : gridValue
117
- )
116
+ const gridLines = $derived(gridValue === true || gridValue === false ? 'auto' : gridValue)
118
117
  const showLegend = $derived(spec?.legend ?? legend)
119
118
  const chartTitle = $derived(spec?.title ?? title)
120
119
  const chartSummary = $derived(spec?.summary ?? summary)
@@ -124,9 +123,11 @@
124
123
  const tableColumns = $derived.by(() => {
125
124
  // Dedupe: a channel may repeat (e.g. x === color), and the table's keyed each
126
125
  // requires unique column names.
127
- const cols = [...new Set(
128
- [spec?.x, spec?.y, spec?.color ?? spec?.fill].filter((c): c is string => Boolean(c))
129
- )]
126
+ const cols = [
127
+ ...new Set(
128
+ [spec?.x, spec?.y, spec?.color ?? spec?.fill].filter((c): c is string => Boolean(c))
129
+ )
130
+ ]
130
131
  if (cols.length > 0) return cols
131
132
  const first = tableData[0]
132
133
  return first ? Object.keys(first) : []
@@ -180,7 +181,10 @@
180
181
  candlestick: Candlestick,
181
182
  waterfall: Waterfall,
182
183
  hexbin: Hexbin,
183
- ribbon: Ribbon
184
+ ribbon: Ribbon,
185
+ // Radar reads the generic x/y/color this path passes via its own aliases, and takes
186
+ // its axis order from `options.axes` — see Radar.svelte's Props.
187
+ radar: Radar
184
188
  }
185
189
 
186
190
  function resolveGeomComponent(type: string) {
@@ -235,8 +239,20 @@
235
239
 
236
240
  <!-- Axes -->
237
241
  {#if axes}
238
- <Axis type="x" label={spec?.labels?.[spec?.x ?? ''] ?? ''} format={xFormat} ticks={xTicks} {minorTicks} />
239
- <Axis type="y" label={spec?.labels?.[spec?.y ?? ''] ?? ''} format={yFormat} ticks={yTicks} {minorTicks} />
242
+ <Axis
243
+ type="x"
244
+ label={spec?.labels?.[spec?.x ?? ''] ?? ''}
245
+ format={xFormat}
246
+ ticks={xTicks}
247
+ {minorTicks}
248
+ />
249
+ <Axis
250
+ type="y"
251
+ label={spec?.labels?.[spec?.y ?? ''] ?? ''}
252
+ format={yFormat}
253
+ ticks={yTicks}
254
+ {minorTicks}
255
+ />
240
256
  {/if}
241
257
 
242
258
  {#if trend !== null && trend !== undefined}
@@ -95,6 +95,7 @@ export class PlotState {
95
95
  ribbon: number;
96
96
  waterfall: number;
97
97
  rule: number;
98
+ radar: number;
98
99
  };
99
100
  patterns: string[];
100
101
  symbols: string[];
@@ -482,7 +482,14 @@ export class PlotState {
482
482
  if (!geom) return []
483
483
  const stat = geom.stat ?? 'identity'
484
484
  if (stat === 'identity') return this.#rawData
485
- const mergedChannels = { ...this.#channels, ...geom.channels }
485
+ // Strip explicit `undefined` values before spreading: a geom that omits a channel
486
+ // to inherit it from the container sends `{ x: undefined }`, not `{}`, which would
487
+ // otherwise become an own property that clobbers the container's value below (see
488
+ // SparkState.geomData for the fuller writeup — same bug, same fix, both classes).
489
+ const geomChannels = Object.fromEntries(
490
+ Object.entries(geom.channels ?? {}).filter(([, v]) => v !== undefined)
491
+ )
492
+ const mergedChannels = { ...this.#channels, ...geomChannels }
486
493
  return applyGeomStat(this.#rawData, { stat, channels: mergedChannels }, this.#helpers)
487
494
  }
488
495
 
@@ -0,0 +1,165 @@
1
+ <script lang="ts">
2
+ import { setContext, untrack } from 'svelte'
3
+ import type { Snippet } from 'svelte'
4
+ import { SparkState } from './SparkState.svelte.js'
5
+ import DefinePatterns from './patterns/DefinePatterns.svelte'
6
+
7
+ type Row = Record<string, unknown>
8
+
9
+ type Props = {
10
+ data?: Row[]
11
+ x?: string
12
+ y?: string
13
+ /** Categorical color/series channel — same meaning as a geom's `color`/`fill` prop. */
14
+ color?: string
15
+ width?: number
16
+ height?: number
17
+ min?: number
18
+ max?: number
19
+ /** Anchor value: extends the y-domain to include it AND draws a `[data-plot-baseline]`
20
+ * reference line at its pixel. Owned by the container, not a geom — it already owns
21
+ * the domain extension, and it applies to bar sparks too, not just area. */
22
+ baseline?: number
23
+ /** Literal pattern name (e.g. 'diagonal') to texture the composed geom's fill with.
24
+ * A spark is single-series, so unlike a Plot's pattern CHANNEL (which assigns a
25
+ * distinct pattern per distinct field value), there's no grouping to assign — this
26
+ * names the one pattern to use directly. See `SparkState.patterns` for the mechanism. */
27
+ pattern?: string
28
+ /** Accessible name for the glyph. Omit and one is generated from the series — see
29
+ * `summary` below. Pass a real one whenever the surrounding copy doesn't already
30
+ * say what the sparkline is measuring. */
31
+ label?: string
32
+ /** The geom(s) to compose inside — e.g. `<Line />`, `<Area />`, `<Trend />`. */
33
+ children?: Snippet
34
+ }
35
+
36
+ let {
37
+ data = [],
38
+ x = undefined,
39
+ y = undefined,
40
+ color = undefined,
41
+ width = 80,
42
+ height = 24,
43
+ min = undefined,
44
+ max = undefined,
45
+ baseline = undefined,
46
+ pattern = undefined,
47
+ label = undefined,
48
+ children
49
+ }: Props = $props()
50
+
51
+ // A getter thunk (not a plain object) so every reactive prop it closes over is re-read on
52
+ // each call — used both by the untracked initial construction and the live $effect below.
53
+ const config = () => ({
54
+ data,
55
+ channels: { x, y, color },
56
+ width,
57
+ height,
58
+ min,
59
+ max,
60
+ baseline,
61
+ pattern
62
+ })
63
+
64
+ // untrack: the constructor's initial read of config() must not register as a dependency of
65
+ // whatever scope creates <Spark> — mirrors PlotSurface's
66
+ // `untrack(() => new PlotState(fullConfig()))`. The $effect below is the sole mechanism
67
+ // that keeps the state live.
68
+ const state = untrack(() => new SparkState(config()))
69
+
70
+ // SAME context key PlotState publishes on — this is what lets a geom compose inside a
71
+ // Spark with zero changes: it never knows, or branches on, which container it resolved.
72
+ setContext('plot-state', state)
73
+
74
+ // Keeps props live: SparkState.update() is designed to be re-callable (see its doc
75
+ // comment) and fully re-applies config rather than merging deltas, so every prop change
76
+ // here — including ones read only inside a composed geom's channels — flows through.
77
+ $effect(() => {
78
+ state.update(config())
79
+ })
80
+
81
+ /** Round for speech: drops a pointless `.00` without truncating real precision. */
82
+ function speak(n: number) {
83
+ return String(Math.round(n * 100) / 100)
84
+ }
85
+
86
+ // A sparkline carries meaning purely visually, so without this it is not merely
87
+ // unlabelled — it is unreadable to a screen reader. Cycle 1 shipped every spark as a
88
+ // bare <svg> with no role or name at all.
89
+ //
90
+ // Generated from the series rather than a fixed "sparkline" string: a constant would
91
+ // satisfy an axe rule while still telling the user nothing. Count, direction, endpoints
92
+ // and extent are what someone reads off the shape visually, so they are what the text
93
+ // alternative says.
94
+ const summary = $derived.by(() => {
95
+ const values = y ? data.map((row) => Number(row[y])).filter((v) => Number.isFinite(v)) : []
96
+ if (!values.length) return 'Sparkline with no data'
97
+
98
+ const first = values[0]
99
+ const last = values[values.length - 1]
100
+ // reduce rather than Math.min(...values): spread on a long series risks a stack
101
+ // overflow, and a spark is free to be handed thousands of points.
102
+ const lo = values.reduce((a, b) => (b < a ? b : a), first)
103
+ const hi = values.reduce((a, b) => (b > a ? b : a), first)
104
+ const direction = last > first ? 'rising' : last < first ? 'falling' : 'flat'
105
+
106
+ return (
107
+ `Sparkline: ${values.length} values, ${direction} from ${speak(first)} to ` +
108
+ `${speak(last)}, ranging ${speak(lo)} to ${speak(hi)}.`
109
+ )
110
+ })
111
+
112
+ const accessibleName = $derived(label ?? summary)
113
+
114
+ // The baseline line is the container's concern, not a geom's: it already owns the domain
115
+ // extension (SparkState's y-domain widening), and it applies to bar sparks too, not just
116
+ // area — so a single reference line lives here rather than duplicated inside every geom.
117
+ const baselineY = $derived.by(() => {
118
+ if (baseline === undefined) return null
119
+ const scale = state.yScale
120
+ // buildUnifiedYScale's return type is a union (linear | band) because a PlotState y
121
+ // channel *can* be categorical — a spark's never is (SparkState's #yDomain is always
122
+ // numeric), so this is always the linear branch in practice. `'ticks' in scale` narrows
123
+ // the union for the compiler without a bare property access on a member (ScaleBand)
124
+ // that doesn't have it at all (same union Plot/Grid.svelte narrows for xScale/yScale).
125
+ return 'ticks' in scale ? scale(baseline) : null
126
+ })
127
+ </script>
128
+
129
+ <svg
130
+ {width}
131
+ {height}
132
+ data-spark
133
+ role="img"
134
+ aria-label={accessibleName}
135
+ style="overflow: visible; display: block;"
136
+ >
137
+ <!-- role="img" + aria-label is what actually gets announced; title/desc are the
138
+ SVG-native equivalents, kept for tooling and non-ARIA consumers. `desc` always
139
+ holds the generated data summary even when `label` overrides the name, so the
140
+ numbers stay available rather than being replaced by the caller's wording. -->
141
+ <title>{accessibleName}</title>
142
+ <desc>{summary}</desc>
143
+
144
+ <!-- SVG <pattern> defs for texture fills — the same mechanism PlotSurface wires up, so a
145
+ geom's pattern channel resolves inside a Spark exactly as it does inside a Plot. -->
146
+ <defs>
147
+ <DefinePatterns />
148
+ </defs>
149
+
150
+ {#if baselineY !== null}
151
+ <line x1={0} y1={baselineY} x2={width} y2={baselineY} data-plot-baseline />
152
+ {/if}
153
+
154
+ {@render children?.()}
155
+ </svg>
156
+
157
+ <style>
158
+ [data-plot-baseline] {
159
+ stroke: var(--chart-baseline-color, currentColor);
160
+ stroke-width: var(--chart-baseline-width, 1);
161
+ stroke-dasharray: var(--chart-baseline-dash, 4 4);
162
+ opacity: var(--chart-baseline-opacity, 0.5);
163
+ pointer-events: none;
164
+ }
165
+ </style>
@@ -0,0 +1,31 @@
1
+ import type { Snippet } from 'svelte';
2
+ type Row = Record<string, unknown>;
3
+ type Props = {
4
+ data?: Row[];
5
+ x?: string;
6
+ y?: string;
7
+ /** Categorical color/series channel — same meaning as a geom's `color`/`fill` prop. */
8
+ color?: string;
9
+ width?: number;
10
+ height?: number;
11
+ min?: number;
12
+ max?: number;
13
+ /** Anchor value: extends the y-domain to include it AND draws a `[data-plot-baseline]`
14
+ * reference line at its pixel. Owned by the container, not a geom — it already owns
15
+ * the domain extension, and it applies to bar sparks too, not just area. */
16
+ baseline?: number;
17
+ /** Literal pattern name (e.g. 'diagonal') to texture the composed geom's fill with.
18
+ * A spark is single-series, so unlike a Plot's pattern CHANNEL (which assigns a
19
+ * distinct pattern per distinct field value), there's no grouping to assign — this
20
+ * names the one pattern to use directly. See `SparkState.patterns` for the mechanism. */
21
+ pattern?: string;
22
+ /** Accessible name for the glyph. Omit and one is generated from the series — see
23
+ * `summary` below. Pass a real one whenever the surrounding copy doesn't already
24
+ * say what the sparkline is measuring. */
25
+ label?: string;
26
+ /** The geom(s) to compose inside — e.g. `<Line />`, `<Area />`, `<Trend />`. */
27
+ children?: Snippet;
28
+ };
29
+ declare const Spark: import("svelte").Component<Props, {}, "">;
30
+ type Spark = ReturnType<typeof Spark>;
31
+ export default Spark;
@@ -0,0 +1,138 @@
1
+ /**
2
+ * The full set of members a geom reads off its `'plot-state'` context — derived by
3
+ * scanning every geom for `plotState.*` / `plot.*` / `state.*` reads (`geoms/*.svelte`,
4
+ * `geoms/lib/**`, including the `marks/*.js` adapters). `SparkState` must implement
5
+ * every one of these: a `<Spark>` composes the SAME geom components a `<Plot>` does,
6
+ * and no geom branches on which container it's inside — so a member missing here is a
7
+ * member that throws inside a `<Spark>` at runtime, not at the point that grew it.
8
+ *
9
+ * This is the ONE place to update when that surface legitimately changes. Adding a
10
+ * geom (or a new context read inside an existing one) that needs a `PlotState` member
11
+ * means adding it here too. `spec/spark-contract.spec.js` asserts `SparkState` and
12
+ * `PlotState` both satisfy this exact list and agree on each member's kind (function /
13
+ * Map / array / null / primitive). If that test goes red after you add a member here,
14
+ * that is the signal working as intended — it means `SparkState` is missing the
15
+ * member, not that the test is wrong. Do not "fix" a red conformance test by deleting
16
+ * the entry you just added.
17
+ *
18
+ * Deliberately excluded — read only by `Plot`'s chrome (`Axis`/`Legend`/`Tooltip`/
19
+ * `Grid`) or by `PlotSurface`'s own wiring, never by a geom: `xAxisY`, `yAxisX`,
20
+ * `hovered`, `mode`, `colorScaleType`, `geomTypes`, `colorField`, `patternField`,
21
+ * `symbolField`, `format`, `label`, `tooltip`, `margin`, `bandScale`, `valueScale`,
22
+ * `preset`, `geomComponent`. A `<Spark>` has no axis, legend, tooltip or grid to
23
+ * drive, so none of these ever apply.
24
+ *
25
+ * Also excluded: `selectedRows` / `isSelected` / `setSelected` / `clearSelected` /
26
+ * `applyZoom` / `resetZoom`. `geoms/Highlight.svelte` is the one geom that reads
27
+ * `state?.selectedRows`, but it does so defensively (`?? []`) — multi-select and zoom
28
+ * are explicit non-goals for a spark (see
29
+ * docs/superpowers/specs/2026-08-20-spark-plot-geom-architecture-design.md), so their
30
+ * absence degrades Highlight to "nothing selected", not a runtime error. That's a
31
+ * different failure mode than every member below, where absence throws.
32
+ */
33
+ export const GEOM_CONTRACT: string[];
34
+ /**
35
+ * SparkState — the lean, PlotState-compatible context that lets a sparkline compose
36
+ * real geoms (`<Spark><Line /></Spark>`) instead of Sparkline's own hand-rolled
37
+ * line/area/bar rendering. It publishes on the same 'plot-state' context key as
38
+ * PlotState, so geoms (Line/Area/Bar) read scales, channels and data through the
39
+ * identical interface and never know whether they're inside a spark or a full Plot.
40
+ *
41
+ * An 80×24 glyph has no use for most of what PlotState carries, so this is a thin
42
+ * composition of the same pure scale modules PlotState uses — not a re-implementation
43
+ * of PlotState's scope. It publishes the same registerGeom/updateGeom/unregisterGeom/
44
+ * geomData lifecycle as PlotState (see those methods below), at smaller scope, so
45
+ * GeomState — and therefore any geom component — drives a spark unchanged. Deliberately
46
+ * omitted, permanently (a <Spark> container is a later slice, but none of these ever
47
+ * apply to an 80×24 glyph):
48
+ * - zoom/pan transforms — a spark is a static glyph, never panned or zoomed.
49
+ * - crossfilter/selection — nothing to click or brush at this size.
50
+ * - facets — a spark is always a single small panel, never a grid of panels.
51
+ * - axis/legend chrome and margins — width/height ARE the inner box.
52
+ *
53
+ * This class also carries the rest of the surface a geom reads off `plot-state`:
54
+ * `colors` — a real categorical palette, built from the same brewing helpers PlotState
55
+ * uses, keyed off `channels.color ?? channels.fill` — plus a set of members geoms read
56
+ * but a spark has no use for (symbols/isFlipped/orientation/continuousCategory/
57
+ * continuousColorScale/interactive/place/setHovered/clearHovered/handleSelect). Those are
58
+ * kept present and explicit — empty Maps, false/null/identity, no-op methods — rather
59
+ * than simply absent, so a geom never needs a spark-specific branch and a later
60
+ * conformance test can pin the full contract. `patterns` is the one exception with a real,
61
+ * non-empty case — see its own doc comment below for why a literal name is the only
62
+ * sensible spark case.
63
+ *
64
+ * `data` deliberately holds ONE array (`$state.raw`, not a proxying `$state`), unlike
65
+ * PlotState's `#data`/`#rawData` pair. That split gave PlotState's data two distinct
66
+ * proxy identities for the same source array, breaking identity-based lookups like
67
+ * `plotState.data.indexOf(row)` (see journal 2026-08-13). Geoms will later read rows
68
+ * from this same array, so its identity must be exactly what the caller passed in.
69
+ */
70
+ export class SparkState {
71
+ constructor(config?: {});
72
+ xScale: import("d3-scale").ScaleLinear<number, number, never> | import("d3-scale").ScaleBand<string>;
73
+ yScale: import("d3-scale").ScaleLinear<number, number, never> | import("d3-scale").ScaleBand<string>;
74
+ colors: Map<unknown, {
75
+ fill: string;
76
+ stroke: string;
77
+ }>;
78
+ update(config?: {}): void;
79
+ get data(): any[];
80
+ /** @returns {{ x?: string, y?: string }} */
81
+ get channels(): {
82
+ x?: string;
83
+ y?: string;
84
+ };
85
+ get innerWidth(): number;
86
+ get innerHeight(): number;
87
+ get chartPreset(): {
88
+ colors: string[];
89
+ shades: {
90
+ light: {
91
+ fill: string;
92
+ stroke: string;
93
+ };
94
+ dark: {
95
+ fill: string;
96
+ stroke: string;
97
+ };
98
+ };
99
+ opacity: {
100
+ area: number;
101
+ bar: number;
102
+ line: number;
103
+ point: number;
104
+ arc: number;
105
+ box: number;
106
+ violin: number;
107
+ jitter: number;
108
+ heatmap: number;
109
+ candlestick: number;
110
+ hexbin: number;
111
+ ribbon: number;
112
+ waterfall: number;
113
+ rule: number;
114
+ radar: number;
115
+ };
116
+ patterns: string[];
117
+ symbols: string[];
118
+ };
119
+ get patterns(): Map<any, any>;
120
+ get symbols(): Map<any, any>;
121
+ get isFlipped(): boolean;
122
+ get orientation(): string;
123
+ get continuousCategory(): boolean;
124
+ get continuousColorScale(): null;
125
+ get interactive(): boolean;
126
+ place(u: any, v: any): {
127
+ x: any;
128
+ y: any;
129
+ };
130
+ setHovered(): void;
131
+ clearHovered(): void;
132
+ handleSelect(): void;
133
+ registerGeom(config: any): string;
134
+ updateGeom(id: any, config: any): void;
135
+ unregisterGeom(id: any): void;
136
+ geomData(id: any): any[];
137
+ #private;
138
+ }