@rokkit/chart 1.3.7 → 1.3.9

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 (162) hide show
  1. package/dist/AnimatedPlot.svelte +26 -8
  2. package/dist/AnimatedPlot.svelte.d.ts +5 -0
  3. package/dist/Chart.svelte +51 -65
  4. package/dist/FacetPlot.svelte +1 -1
  5. package/dist/Plot/Axis.svelte +19 -10
  6. package/dist/Plot/Root.svelte +67 -46
  7. package/dist/Plot/Root.svelte.d.ts +29 -2
  8. package/dist/Plot/Timeline.svelte +39 -13
  9. package/dist/Plot/index.d.ts +5 -5
  10. package/dist/Plot/index.js +5 -5
  11. package/dist/Plot.svelte +95 -178
  12. package/dist/Plot.svelte.d.ts +4 -0
  13. package/dist/PlotState.svelte.d.ts +29 -1
  14. package/dist/PlotState.svelte.js +163 -19
  15. package/dist/PlotSurface.svelte +162 -0
  16. package/dist/PlotSurface.svelte.d.ts +25 -0
  17. package/dist/charts/AreaChart.svelte +12 -2
  18. package/dist/charts/AreaChart.svelte.d.ts +6 -0
  19. package/dist/charts/BarChart.svelte +40 -4
  20. package/dist/charts/BarChart.svelte.d.ts +15 -1
  21. package/dist/charts/BoxPlot.svelte +7 -3
  22. package/dist/charts/BoxPlot.svelte.d.ts +2 -0
  23. package/dist/charts/BubbleChart.svelte +37 -4
  24. package/dist/charts/BubbleChart.svelte.d.ts +12 -1
  25. package/dist/charts/LineChart.svelte +4 -2
  26. package/dist/charts/LineChart.svelte.d.ts +1 -0
  27. package/dist/charts/PieChart.svelte +5 -3
  28. package/dist/charts/PieChart.svelte.d.ts +1 -0
  29. package/dist/charts/ScatterPlot.svelte +35 -4
  30. package/dist/charts/ScatterPlot.svelte.d.ts +11 -1
  31. package/dist/charts/ViolinPlot.svelte +7 -3
  32. package/dist/charts/ViolinPlot.svelte.d.ts +2 -0
  33. package/dist/examples/BarChartExample.svelte +1 -1
  34. package/dist/geoms/Arc.svelte +21 -54
  35. package/dist/geoms/Arc.svelte.d.ts +4 -4
  36. package/dist/geoms/Area.svelte +45 -44
  37. package/dist/geoms/Area.svelte.d.ts +8 -1
  38. package/dist/geoms/Bar.svelte +33 -45
  39. package/dist/geoms/Bar.svelte.d.ts +9 -0
  40. package/dist/geoms/Box.svelte +103 -61
  41. package/dist/geoms/Box.svelte.d.ts +12 -3
  42. package/dist/geoms/Candlestick.svelte +26 -75
  43. package/dist/geoms/Candlestick.svelte.d.ts +2 -0
  44. package/dist/geoms/Heatmap.svelte +21 -66
  45. package/dist/geoms/Heatmap.svelte.d.ts +2 -0
  46. package/dist/geoms/Hexbin.svelte +20 -100
  47. package/dist/geoms/Hexbin.svelte.d.ts +2 -0
  48. package/dist/geoms/Jitter.svelte +67 -0
  49. package/dist/geoms/Jitter.svelte.d.ts +17 -0
  50. package/dist/geoms/Line.svelte +27 -31
  51. package/dist/geoms/Line.svelte.d.ts +5 -0
  52. package/dist/geoms/Point.svelte +31 -60
  53. package/dist/geoms/Point.svelte.d.ts +5 -1
  54. package/dist/geoms/Ribbon.svelte +30 -124
  55. package/dist/geoms/Ribbon.svelte.d.ts +2 -0
  56. package/dist/geoms/Rule.svelte +117 -0
  57. package/dist/geoms/Rule.svelte.d.ts +19 -0
  58. package/dist/geoms/Violin.svelte +36 -31
  59. package/dist/geoms/Violin.svelte.d.ts +10 -3
  60. package/dist/geoms/Waterfall.svelte +25 -79
  61. package/dist/geoms/Waterfall.svelte.d.ts +2 -0
  62. package/dist/geoms/lib/GeomState.svelte.d.ts +51 -0
  63. package/dist/geoms/lib/GeomState.svelte.js +79 -0
  64. package/dist/geoms/lib/aesthetics.d.ts +36 -0
  65. package/dist/geoms/lib/aesthetics.js +45 -0
  66. package/dist/geoms/lib/areas.d.ts +6 -2
  67. package/dist/geoms/lib/areas.js +20 -12
  68. package/dist/geoms/lib/bars.d.ts +18 -3
  69. package/dist/geoms/lib/bars.js +80 -101
  70. package/dist/geoms/lib/marks/arc.d.ts +24 -0
  71. package/dist/geoms/lib/marks/arc.js +28 -0
  72. package/dist/geoms/lib/marks/area.d.ts +23 -0
  73. package/dist/geoms/lib/marks/area.js +38 -0
  74. package/dist/geoms/lib/marks/bar.d.ts +16 -0
  75. package/dist/geoms/lib/marks/bar.js +43 -0
  76. package/dist/geoms/lib/marks/box.d.ts +14 -0
  77. package/dist/geoms/lib/marks/box.js +30 -0
  78. package/dist/geoms/lib/marks/candlestick.d.ts +27 -0
  79. package/dist/geoms/lib/marks/candlestick.js +52 -0
  80. package/dist/geoms/lib/marks/heatmap.d.ts +24 -0
  81. package/dist/geoms/lib/marks/heatmap.js +41 -0
  82. package/dist/geoms/lib/marks/hexbin.d.ts +29 -0
  83. package/dist/geoms/lib/marks/hexbin.js +62 -0
  84. package/dist/geoms/lib/marks/jitter.d.ts +21 -0
  85. package/dist/geoms/lib/marks/jitter.js +36 -0
  86. package/dist/geoms/lib/marks/line.d.ts +26 -0
  87. package/dist/geoms/lib/marks/line.js +25 -0
  88. package/dist/geoms/lib/marks/point.d.ts +25 -0
  89. package/dist/geoms/lib/marks/point.js +58 -0
  90. package/dist/geoms/lib/marks/ribbon.d.ts +31 -0
  91. package/dist/geoms/lib/marks/ribbon.js +77 -0
  92. package/dist/geoms/lib/marks/violin.d.ts +13 -0
  93. package/dist/geoms/lib/marks/violin.js +31 -0
  94. package/dist/geoms/lib/marks/waterfall.d.ts +25 -0
  95. package/dist/geoms/lib/marks/waterfall.js +58 -0
  96. package/dist/index.d.ts +25 -12
  97. package/dist/index.js +26 -13
  98. package/dist/lib/brewing/marks/arcs.js +7 -1
  99. package/dist/lib/brewing/marks/boxes.d.ts +11 -2
  100. package/dist/lib/brewing/marks/boxes.js +32 -11
  101. package/dist/lib/brewing/marks/lines.d.ts +4 -1
  102. package/dist/lib/brewing/marks/lines.js +18 -15
  103. package/dist/lib/brewing/marks/swarm.d.ts +32 -0
  104. package/dist/lib/brewing/marks/swarm.js +125 -0
  105. package/dist/lib/brewing/marks/violins.d.ts +4 -20
  106. package/dist/lib/brewing/marks/violins.js +41 -25
  107. package/dist/lib/brewing/stats.d.ts +3 -1
  108. package/dist/lib/brewing/stats.js +28 -11
  109. package/dist/lib/plot/scales.js +12 -7
  110. package/dist/lib/plot/types.d.ts +9 -1
  111. package/dist/lib/plot/types.js +3 -1
  112. package/dist/lib/preset.d.ts +11 -1
  113. package/dist/lib/preset.js +16 -4
  114. package/package.json +1 -1
  115. package/dist/Plot/Arc.svelte +0 -30
  116. package/dist/Plot/Arc.svelte.d.ts +0 -18
  117. package/dist/Plot/Area.svelte +0 -54
  118. package/dist/Plot/Area.svelte.d.ts +0 -14
  119. package/dist/Plot/Bar.svelte +0 -69
  120. package/dist/Plot/Bar.svelte.d.ts +0 -11
  121. package/dist/Plot/Line.svelte +0 -54
  122. package/dist/Plot/Line.svelte.d.ts +0 -13
  123. package/dist/Plot/Point.svelte +0 -58
  124. package/dist/Plot/Point.svelte.d.ts +0 -13
  125. package/dist/lib/brewer.d.ts +0 -9
  126. package/dist/lib/brewer.js +0 -25
  127. package/dist/lib/brewing/BoxBrewer.svelte.d.ts +0 -8
  128. package/dist/lib/brewing/BoxBrewer.svelte.js +0 -14
  129. package/dist/lib/brewing/CartesianBrewer.svelte.d.ts +0 -8
  130. package/dist/lib/brewing/CartesianBrewer.svelte.js +0 -21
  131. package/dist/lib/brewing/PieBrewer.svelte.d.ts +0 -8
  132. package/dist/lib/brewing/PieBrewer.svelte.js +0 -14
  133. package/dist/lib/brewing/QuartileBrewer.svelte.d.ts +0 -9
  134. package/dist/lib/brewing/QuartileBrewer.svelte.js +0 -51
  135. package/dist/lib/brewing/ViolinBrewer.svelte.d.ts +0 -8
  136. package/dist/lib/brewing/ViolinBrewer.svelte.js +0 -14
  137. package/dist/lib/brewing/axes.svelte.d.ts +0 -66
  138. package/dist/lib/brewing/axes.svelte.js +0 -270
  139. package/dist/lib/brewing/bars.svelte.d.ts +0 -56
  140. package/dist/lib/brewing/bars.svelte.js +0 -201
  141. package/dist/lib/brewing/brewer.svelte.d.ts +0 -124
  142. package/dist/lib/brewing/brewer.svelte.js +0 -277
  143. package/dist/lib/brewing/dimensions.svelte.d.ts +0 -35
  144. package/dist/lib/brewing/dimensions.svelte.js +0 -56
  145. package/dist/lib/brewing/index.svelte.d.ts +0 -118
  146. package/dist/lib/brewing/index.svelte.js +0 -205
  147. package/dist/lib/brewing/legends.svelte.d.ts +0 -48
  148. package/dist/lib/brewing/legends.svelte.js +0 -137
  149. package/dist/lib/brewing/marks/areas.d.ts +0 -31
  150. package/dist/lib/brewing/marks/areas.js +0 -72
  151. package/dist/lib/brewing/marks/bars.d.ts +0 -1
  152. package/dist/lib/brewing/marks/bars.js +0 -49
  153. package/dist/lib/brewing/scales.svelte.d.ts +0 -24
  154. package/dist/lib/brewing/scales.svelte.js +0 -82
  155. package/dist/lib/context.d.ts +0 -13
  156. package/dist/lib/context.js +0 -131
  157. package/dist/lib/plot/chartProps.d.ts +0 -177
  158. package/dist/lib/plot/chartProps.js +0 -76
  159. package/dist/lib/scales.svelte.d.ts +0 -35
  160. package/dist/lib/scales.svelte.js +0 -151
  161. package/dist/lib/ticks.d.ts +0 -36
  162. package/dist/lib/ticks.js +0 -46
@@ -34,6 +34,11 @@
34
34
  tween?: boolean
35
35
  sorted?: boolean
36
36
  dynamicDomain?: boolean
37
+ /** Chart direction for standard (non-race) animations — flips via the same geom
38
+ * place() mechanism as static charts. `flip` is sugar for 'horizontal'. Ignored for
39
+ * the bar-chart race, which is inherently horizontal. */
40
+ orientation?: 'vertical' | 'horizontal'
41
+ flip?: boolean
37
42
  label?: boolean | string | ((data: Row) => string)
38
43
  children?: Snippet
39
44
  }
@@ -53,16 +58,22 @@
53
58
  helpers = {},
54
59
  width = 600,
55
60
  height = 400,
56
- mode = 'light',
61
+ mode = undefined,
57
62
  grid = true,
58
63
  legend = false,
59
64
  tween = true,
60
65
  sorted = false,
61
66
  dynamicDomain = false,
67
+ orientation = undefined,
68
+ flip = false,
62
69
  label = false,
63
70
  children
64
71
  }: Props = $props()
65
72
 
73
+ // Standard-animation direction (non-race). Flows to the frameSpec so the geoms flip via the
74
+ // same place() path as static charts; the frame tween interpolates values orientation-agnostically.
75
+ const resolvedOrientation = $derived(flip ? 'horizontal' : orientation)
76
+
66
77
  // Effective geom list: explicit array takes precedence; otherwise build from shorthand props
67
78
  const effectiveGeoms = $derived(
68
79
  geoms.length > 0
@@ -348,23 +359,30 @@
348
359
  }))
349
360
  )
350
361
 
351
- const xDomainForFrame = $derived(
362
+ // The race's value-axis domain (the caller's x = the measured value), dynamic or static.
363
+ const valueDomainForFrame = $derived(
352
364
  isHorizontalRace && dynamicDomain ? $xDomainTween : staticDomains.xDomain
353
365
  )
354
366
  const frameSpec = $derived<PlotSpec>({
355
367
  data: $displayTween,
356
- x,
357
- y: isHorizontalRace ? '_rank' : y,
368
+ // Race uses the STANDARD convention so it flips through the same place() path as every
369
+ // chart: x = the continuous `_rank` (category/position axis), y = the caller's value.
370
+ // `continuousCategory` keeps the rank scale linear so fractional ranks tween smoothly.
371
+ x: isHorizontalRace ? '_rank' : x,
372
+ y: isHorizontalRace ? x : y,
358
373
  color,
359
374
  geoms: isHorizontalRace ? raceGeoms : prepared.geoms,
360
- xDomain: xDomainForFrame,
361
- yDomain: isHorizontalRace ? [0, entityCount - 1] : staticDomains.yDomain,
362
- orientation: isHorizontalRace ? 'horizontal' : undefined
375
+ // Pad the rank axis half a slot each end so the first/last bar's thickness doesn't spill
376
+ // past the axis (a continuous-category bar centres on the rank with a step-derived height).
377
+ xDomain: isHorizontalRace ? [-0.5, entityCount - 0.5] : staticDomains.xDomain,
378
+ yDomain: isHorizontalRace ? valueDomainForFrame : staticDomains.yDomain,
379
+ orientation: isHorizontalRace ? 'horizontal' : resolvedOrientation,
380
+ continuousCategory: isHorizontalRace
363
381
  })
364
382
  </script>
365
383
 
366
384
  <div data-plot-animated>
367
- <PlotChart spec={frameSpec} {helpers} {width} {height} {mode} {grid} {legend}>
385
+ <PlotChart spec={frameSpec} {helpers} {width} {height} {mode} {grid} {legend} animate={false}>
368
386
  {@render children?.()}
369
387
  </PlotChart>
370
388
 
@@ -27,6 +27,11 @@ type Props = {
27
27
  tween?: boolean;
28
28
  sorted?: boolean;
29
29
  dynamicDomain?: boolean;
30
+ /** Chart direction for standard (non-race) animations — flips via the same geom
31
+ * place() mechanism as static charts. `flip` is sugar for 'horizontal'. Ignored for
32
+ * the bar-chart race, which is inherently horizontal. */
33
+ orientation?: 'vertical' | 'horizontal';
34
+ flip?: boolean;
30
35
  label?: boolean | string | ((data: Row) => string);
31
36
  children?: Snippet;
32
37
  };
package/dist/Chart.svelte CHANGED
@@ -1,19 +1,8 @@
1
1
  <script lang="ts">
2
- import { setContext } from 'svelte'
3
2
  import type { Snippet } from 'svelte'
4
- import { ChartBrewer } from './lib/brewing/brewer.svelte.js'
3
+ import Plot from './Plot.svelte'
5
4
  import type { ChartSpec } from './spec/chart-spec.js'
6
-
7
- type Channels = {
8
- x?: string
9
- y?: string
10
- color?: string
11
- pattern?: string
12
- fill?: string
13
- size?: string
14
- label?: string
15
- symbol?: string
16
- }
5
+ import type { PlotSpec, GeomSpec } from './lib/plot/types.js'
17
6
 
18
7
  type Props = {
19
8
  spec?: ChartSpec
@@ -45,63 +34,60 @@
45
34
  symbol = undefined,
46
35
  width = 600,
47
36
  height = 400,
48
- mode = 'light',
37
+ mode = undefined,
49
38
  children
50
39
  }: Props = $props()
51
40
 
52
- const brewer = new ChartBrewer()
53
- setContext('chart-brewer', brewer)
54
-
55
- function buildChannels(): Channels {
56
- const channels: Channels = {}
57
- if (x) channels.x = x
58
- if (y) channels.y = y
59
- if (color) channels.color = color
60
- if (pattern) channels.pattern = pattern
61
- if (fill) channels.fill = fill
62
- if (size) channels.size = size
63
- if (label) channels.label = label
64
- if (symbol) channels.symbol = symbol
65
- return channels
66
- }
67
-
68
- $effect(() => {
41
+ // `Chart` is a thin wrapper over the geom-path host (`Plot`). It maps the fluent
42
+ // `ChartSpec` builder (data / channels / layers / options) onto a `PlotSpec` and renders
43
+ // the layers as geoms. Axes/grid stay off by default — `Chart` historically rendered just
44
+ // the marks (plus any declarative geom children). See the brewer-consolidation spec
45
+ // (docs/backlog/2026-08-17-chart-aesthetics-unification.md §9).
46
+ const plotSpec = $derived.by((): PlotSpec => {
69
47
  if (spec) {
70
- brewer.update({
71
- data: spec.data,
72
- channels: spec.channels,
73
- width,
74
- height,
75
- mode,
76
- layers: spec.layers
77
- })
78
- } else {
79
- brewer.update({ data, channels: buildChannels(), width, height, mode })
48
+ const ch = (spec.channels ?? {}) as {
49
+ x?: string
50
+ y?: string
51
+ color?: string
52
+ pattern?: string
53
+ }
54
+ const opts = (spec.options ?? {}) as {
55
+ width?: number
56
+ height?: number
57
+ grid?: unknown
58
+ legend?: unknown
59
+ }
60
+ const layers = (spec.layers ?? []) as GeomSpec[]
61
+ return {
62
+ data: spec.data as Record<string, unknown>[],
63
+ x: ch.x,
64
+ y: ch.y,
65
+ color: ch.color,
66
+ pattern: ch.pattern,
67
+ width: opts.width ?? width,
68
+ height: opts.height ?? height,
69
+ grid: 'grid' in opts,
70
+ legend: 'legend' in opts,
71
+ geoms: layers.map((l) => ({ pattern: ch.pattern, ...l }))
72
+ }
73
+ }
74
+ return {
75
+ data,
76
+ x,
77
+ y,
78
+ color: color ?? fill,
79
+ pattern,
80
+ size,
81
+ symbol,
82
+ width,
83
+ height,
84
+ grid: false,
85
+ legend: false,
86
+ geoms: []
80
87
  }
81
88
  })
82
89
  </script>
83
90
 
84
- <div class="chart-container" data-chart-root>
85
- <svg {width} {height} viewBox="0 0 {width} {height}" role="img" aria-label="Chart visualization">
86
- <g class="chart-area" data-chart-canvas>
87
- {@render children?.()}
88
- </g>
89
- </svg>
90
- </div>
91
-
92
- <style>
93
- .chart-container {
94
- position: relative;
95
- width: 100%;
96
- height: auto;
97
- }
98
-
99
- svg {
100
- display: block;
101
- overflow: visible;
102
- }
103
-
104
- .chart-area {
105
- pointer-events: all;
106
- }
107
- </style>
91
+ <Plot spec={plotSpec} axes={false} label={label ?? false} {mode}>
92
+ {@render children?.()}
93
+ </Plot>
@@ -47,7 +47,7 @@
47
47
  panelHeight,
48
48
  width = 900,
49
49
  height = 300,
50
- mode = 'light',
50
+ mode = undefined,
51
51
  grid = true,
52
52
  legend = false,
53
53
  children
@@ -59,16 +59,21 @@
59
59
  const xTicks = $derived(buildTicks(state.xScale))
60
60
  const yTicks = $derived(buildTicks(state.yScale))
61
61
 
62
- const xTransform = $derived(`translate(0, ${state.xAxisY ?? state.innerHeight})`)
63
- const yTransform = $derived(`translate(${state.yAxisX ?? 0}, 0)`)
62
+ // When flipped, the x-channel (category) axis stands up on the left and the value (y)
63
+ // axis lies along the bottom — each axis type renders on the opposite screen orientation.
64
+ const flipped = $derived(state.isFlipped)
65
+ const renderAsX = $derived((type === 'x') !== flipped)
66
+ const ticks = $derived(type === 'x' ? xTicks : yTicks)
67
+ const hAxisY = $derived(flipped ? state.innerHeight : (state.xAxisY ?? state.innerHeight))
68
+ const vAxisX = $derived(flipped ? 0 : (state.yAxisX ?? 0))
64
69
  </script>
65
70
 
66
- {#if type === 'x'}
67
- <g class="axis x-axis" transform={xTransform} data-plot-axis="x">
71
+ {#if renderAsX}
72
+ <g class="axis x-axis" transform={`translate(0, ${hAxisY})`} data-plot-axis="x">
68
73
  {#if showLine}
69
74
  <line x1="0" y1="0" x2={state.innerWidth} y2="0" data-plot-axis-line />
70
75
  {/if}
71
- {#each xTicks as tick (tick.minor ? `minor-${tick.pos}` : tick.value)}
76
+ {#each ticks as tick (tick.minor ? `minor-${tick.pos}` : tick.value)}
72
77
  <g transform="translate({tick.pos}, 0)" data-plot-tick>
73
78
  {#if showTicks}
74
79
  <line
@@ -96,11 +101,11 @@
96
101
  {/if}
97
102
  </g>
98
103
  {:else}
99
- <g class="axis y-axis" transform={yTransform} data-plot-axis="y">
104
+ <g class="axis y-axis" transform={`translate(${vAxisX}, 0)`} data-plot-axis="y">
100
105
  {#if showLine}
101
106
  <line x1="0" y1="0" x2="0" y2={state.innerHeight} data-plot-axis-line />
102
107
  {/if}
103
- {#each yTicks as tick (tick.minor ? `minor-${tick.pos}` : tick.value)}
108
+ {#each ticks as tick (tick.minor ? `minor-${tick.pos}` : tick.value)}
104
109
  <g transform="translate(0, {tick.pos})" data-plot-tick>
105
110
  {#if showTicks}
106
111
  <line
@@ -130,17 +135,21 @@
130
135
  {/if}
131
136
 
132
137
  <style>
138
+ /* Readable, theme-adaptive axis colours (fall back to currentColor for a base-only
139
+ consumer that doesn't provide the --ink token vocabulary). currentColor alone was a
140
+ mode-hardcoded surface tone that went faint on a dark theme paper. */
133
141
  .axis {
134
142
  font-size: 11px;
135
- fill: currentColor;
136
- stroke: currentColor;
143
+ fill: var(--ink-mute, currentColor);
144
+ stroke: var(--ink-mute, currentColor);
137
145
  }
138
146
  .axis-label {
139
147
  font-size: 13px;
140
148
  font-weight: 500;
149
+ fill: var(--ink, currentColor);
141
150
  }
142
151
  [data-plot-axis-line] {
143
- stroke: currentColor;
152
+ stroke: var(--paper-edge, currentColor);
144
153
  /* Decoration only — never intercept clicks meant for data marks beneath it. */
145
154
  pointer-events: none;
146
155
  }
@@ -1,20 +1,46 @@
1
1
  <script lang="ts">
2
- import { setContext, untrack } from 'svelte'
3
2
  import type { Snippet } from 'svelte'
4
- import { PlotState } from '../PlotState.svelte.js'
5
3
  import { defaultPreset } from '../lib/preset.js'
4
+ import PlotSurface from '../PlotSurface.svelte'
6
5
 
7
6
  type Margin = { top?: number; right?: number; bottom?: number; left?: number }
7
+ type Row = Record<string, unknown>
8
8
 
9
9
  type Props = {
10
- data?: Record<string, unknown>[]
10
+ data?: Row[]
11
11
  x?: string
12
12
  y?: string
13
13
  color?: string
14
+ /** Interior aesthetic default (field); geoms inherit unless they set their own. */
15
+ fill?: string
14
16
  width?: number
15
17
  height?: number
16
18
  margin?: Margin
17
19
  mode?: 'light' | 'dark'
20
+ /** Chart direction. 'horizontal' stands the category axis up and runs the value
21
+ * axis sideways — x/y channels are unchanged. `flip` is sugar for 'horizontal'. */
22
+ orientation?: 'vertical' | 'horizontal'
23
+ flip?: boolean
24
+ /** Order the category (band) axis by aggregated value instead of by label — bars sorted
25
+ * by size (histogram-style). 'desc' (largest first) | 'asc'. */
26
+ sort?: 'asc' | 'desc'
27
+ /** Explicit scale domain overrides. */
28
+ xDomain?: unknown[]
29
+ yDomain?: unknown[]
30
+ /** Continuous colour scale controls (sequential/diverging). */
31
+ colorScale?: unknown
32
+ colorScheme?: string
33
+ colorDomain?: unknown[]
34
+ colorMidpoint?: number
35
+ /** Enable d3-zoom pan/zoom. */
36
+ zoom?: boolean
37
+ /** Opt-in click-to-select; fires onselect(detail) and syncs bind:selected (row refs). */
38
+ selectable?: boolean
39
+ onselect?: (detail: unknown) => void
40
+ selected?: Row[]
41
+ /** Animate marks on data/flip changes. Enabled one frame after mount so the
42
+ * initial layout paints un-animated. Default `true`. */
43
+ animate?: boolean
18
44
  children?: Snippet
19
45
  }
20
46
 
@@ -23,55 +49,50 @@
23
49
  x = undefined,
24
50
  y = undefined,
25
51
  color = undefined,
52
+ fill = undefined,
26
53
  width = 600,
27
54
  height = 400,
28
55
  margin = undefined,
29
- mode = 'light',
56
+ mode = undefined,
57
+ orientation = undefined,
58
+ flip = false,
59
+ sort = undefined,
60
+ xDomain = undefined,
61
+ yDomain = undefined,
62
+ colorScale = undefined,
63
+ colorScheme = undefined,
64
+ colorDomain = undefined,
65
+ colorMidpoint = undefined,
66
+ zoom = false,
67
+ selectable = false,
68
+ onselect = undefined,
69
+ selected = $bindable([]),
70
+ animate = true,
30
71
  children
31
72
  }: Props = $props()
32
73
 
33
- const plotState = untrack(
34
- () =>
35
- new PlotState({
36
- data,
37
- channels: { x, y, color },
38
- width,
39
- height,
40
- margin,
41
- mode,
42
- chartPreset: defaultPreset
43
- })
44
- )
45
-
46
- $effect(() => {
47
- plotState.update({
48
- data,
49
- channels: { x, y, color },
50
- width,
51
- height,
52
- margin,
53
- mode,
54
- chartPreset: defaultPreset
55
- })
74
+ // The composable root is just PlotSurface (the shared shell) fed a config from these props.
75
+ const config = $derived({
76
+ data,
77
+ channels: { x, y, color, fill },
78
+ width,
79
+ height,
80
+ margin,
81
+ mode,
82
+ orientation: flip ? 'horizontal' : orientation,
83
+ sort,
84
+ xDomain,
85
+ yDomain,
86
+ colorScale,
87
+ colorScheme,
88
+ colorDomain,
89
+ colorMidpoint,
90
+ onselect,
91
+ selectable,
92
+ chartPreset: defaultPreset
56
93
  })
57
-
58
- setContext('plot-state', plotState)
59
-
60
- const svgWidth = $derived(plotState.innerWidth + (margin?.left ?? 50) + (margin?.right ?? 30))
61
- const svgHeight = $derived(plotState.innerHeight + (margin?.top ?? 20) + (margin?.bottom ?? 40))
62
- const marginLeft = $derived(margin?.left ?? 50)
63
- const marginTop = $derived(margin?.top ?? 20)
64
94
  </script>
65
95
 
66
- <svg
67
- {width}
68
- {height}
69
- viewBox="0 0 {svgWidth} {svgHeight}"
70
- role="img"
71
- aria-label="Chart visualization"
72
- data-plot-root
73
- >
74
- <g transform="translate({marginLeft}, {marginTop})" data-plot-canvas>
75
- {@render children?.()}
76
- </g>
77
- </svg>
96
+ <PlotSurface {config} {animate} {zoom} bind:selected>
97
+ {@render children?.()}
98
+ </PlotSurface>
@@ -5,17 +5,44 @@ type Margin = {
5
5
  bottom?: number;
6
6
  left?: number;
7
7
  };
8
+ type Row = Record<string, unknown>;
8
9
  type Props = {
9
- data?: Record<string, unknown>[];
10
+ data?: Row[];
10
11
  x?: string;
11
12
  y?: string;
12
13
  color?: string;
14
+ /** Interior aesthetic default (field); geoms inherit unless they set their own. */
15
+ fill?: string;
13
16
  width?: number;
14
17
  height?: number;
15
18
  margin?: Margin;
16
19
  mode?: 'light' | 'dark';
20
+ /** Chart direction. 'horizontal' stands the category axis up and runs the value
21
+ * axis sideways — x/y channels are unchanged. `flip` is sugar for 'horizontal'. */
22
+ orientation?: 'vertical' | 'horizontal';
23
+ flip?: boolean;
24
+ /** Order the category (band) axis by aggregated value instead of by label — bars sorted
25
+ * by size (histogram-style). 'desc' (largest first) | 'asc'. */
26
+ sort?: 'asc' | 'desc';
27
+ /** Explicit scale domain overrides. */
28
+ xDomain?: unknown[];
29
+ yDomain?: unknown[];
30
+ /** Continuous colour scale controls (sequential/diverging). */
31
+ colorScale?: unknown;
32
+ colorScheme?: string;
33
+ colorDomain?: unknown[];
34
+ colorMidpoint?: number;
35
+ /** Enable d3-zoom pan/zoom. */
36
+ zoom?: boolean;
37
+ /** Opt-in click-to-select; fires onselect(detail) and syncs bind:selected (row refs). */
38
+ selectable?: boolean;
39
+ onselect?: (detail: unknown) => void;
40
+ selected?: Row[];
41
+ /** Animate marks on data/flip changes. Enabled one frame after mount so the
42
+ * initial layout paints un-animated. Default `true`. */
43
+ animate?: boolean;
17
44
  children?: Snippet;
18
45
  };
19
- declare const Root: import("svelte").Component<Props, {}, "">;
46
+ declare const Root: import("svelte").Component<Props, {}, "selected">;
20
47
  type Root = ReturnType<typeof Root>;
21
48
  export default Root;
@@ -24,8 +24,6 @@
24
24
  const safeIndex = $derived(
25
25
  frameKeys.length === 0 ? 0 : Math.min(currentIndex, frameKeys.length - 1)
26
26
  )
27
-
28
- const SPEEDS = [0.5, 1, 1.5, 2, 4]
29
27
  </script>
30
28
 
31
29
  <div class="timeline" data-plot-timeline>
@@ -56,17 +54,20 @@
56
54
  data-plot-timeline-scrub
57
55
  />
58
56
 
59
- <!-- Speed selector -->
60
- <select
61
- aria-label="Playback speed"
62
- value={speed}
63
- onchange={(e) => onspeed?.(Number(e.currentTarget.value))}
64
- data-plot-timeline-speed
65
- >
66
- {#each SPEEDS as s (s)}
67
- <option value={s}>{s}×</option>
68
- {/each}
69
- </select>
57
+ <!-- Speed (× multiplier) — themed numeric input -->
58
+ <div class="speed">
59
+ <input
60
+ type="number"
61
+ min="0.25"
62
+ max="8"
63
+ step="0.25"
64
+ value={speed}
65
+ aria-label="Playback speed"
66
+ onchange={(e) => onspeed?.(Math.max(0.25, Number(e.currentTarget.value) || 1))}
67
+ data-plot-timeline-speed
68
+ />
69
+ <span class="speed-unit" aria-hidden="true">×</span>
70
+ </div>
70
71
  </div>
71
72
 
72
73
  <style>
@@ -76,6 +77,7 @@
76
77
  gap: 8px;
77
78
  padding: 8px 0;
78
79
  font-size: 12px;
80
+ color: var(--ink-mute, currentColor);
79
81
  }
80
82
  .play-pause {
81
83
  font-size: 16px;
@@ -83,12 +85,36 @@
83
85
  background: none;
84
86
  border: none;
85
87
  padding: 0;
88
+ color: var(--ink, currentColor);
86
89
  }
87
90
  .scrub {
88
91
  flex: 1;
92
+ accent-color: var(--accent, currentColor);
89
93
  }
90
94
  .frame-label {
91
95
  min-width: 4ch;
92
96
  text-align: right;
97
+ color: var(--ink, currentColor);
98
+ }
99
+ .speed {
100
+ display: inline-flex;
101
+ align-items: center;
102
+ gap: 2px;
103
+ }
104
+ .speed input {
105
+ width: 3.5em;
106
+ font: inherit;
107
+ color: var(--ink, currentColor);
108
+ background: var(--paper, transparent);
109
+ border: 1px solid var(--paper-edge, currentColor);
110
+ border-radius: 4px;
111
+ padding: 2px 4px;
112
+ }
113
+ .speed input:focus-visible {
114
+ outline: 2px solid var(--accent, currentColor);
115
+ outline-offset: 1px;
116
+ }
117
+ .speed-unit {
118
+ color: var(--ink-mute, currentColor);
93
119
  }
94
120
  </style>
@@ -1,9 +1,9 @@
1
1
  export { default as Root } from "./Root.svelte";
2
2
  export { default as Axis } from "./Axis.svelte";
3
- export { default as Bar } from "./Bar.svelte";
3
+ export { default as Bar } from "../geoms/Bar.svelte";
4
4
  export { default as Legend } from "./Legend.svelte";
5
5
  export { default as Grid } from "./Grid.svelte";
6
- export { default as Line } from "./Line.svelte";
7
- export { default as Area } from "./Area.svelte";
8
- export { default as Point } from "./Point.svelte";
9
- export { default as Arc } from "./Arc.svelte";
6
+ export { default as Line } from "../geoms/Line.svelte";
7
+ export { default as Area } from "../geoms/Area.svelte";
8
+ export { default as Point } from "../geoms/Point.svelte";
9
+ export { default as Arc } from "../geoms/Arc.svelte";
@@ -1,9 +1,9 @@
1
1
  export { default as Root } from './Root.svelte'
2
2
  export { default as Axis } from './Axis.svelte'
3
- export { default as Bar } from './Bar.svelte'
3
+ export { default as Bar } from '../geoms/Bar.svelte'
4
4
  export { default as Legend } from './Legend.svelte'
5
5
  export { default as Grid } from './Grid.svelte'
6
- export { default as Line } from './Line.svelte'
7
- export { default as Area } from './Area.svelte'
8
- export { default as Point } from './Point.svelte'
9
- export { default as Arc } from './Arc.svelte'
6
+ export { default as Line } from '../geoms/Line.svelte'
7
+ export { default as Area } from '../geoms/Area.svelte'
8
+ export { default as Point } from '../geoms/Point.svelte'
9
+ export { default as Arc } from '../geoms/Arc.svelte'