@rokkit/chart 1.0.0-next.99 → 1.0.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 (137) hide show
  1. package/README.md +150 -46
  2. package/package.json +40 -51
  3. package/src/AnimatedPlot.svelte +383 -0
  4. package/src/Chart.svelte +87 -59
  5. package/src/ChartExporter.svelte +125 -0
  6. package/src/ChartLegend.svelte +148 -0
  7. package/src/ChartProvider.svelte +10 -0
  8. package/src/FacetPlot/Panel.svelte +37 -0
  9. package/src/FacetPlot.svelte +114 -0
  10. package/src/Plot/Arc.svelte +29 -0
  11. package/src/Plot/Area.svelte +32 -0
  12. package/src/Plot/Axis.svelte +164 -0
  13. package/src/Plot/Bar.svelte +54 -0
  14. package/src/Plot/Grid.svelte +34 -0
  15. package/src/Plot/Legend.svelte +268 -0
  16. package/src/Plot/Line.svelte +37 -0
  17. package/src/Plot/Point.svelte +40 -0
  18. package/src/Plot/Root.svelte +62 -0
  19. package/src/Plot/Timeline.svelte +95 -0
  20. package/src/Plot/Tooltip.svelte +87 -0
  21. package/src/Plot/index.js +9 -0
  22. package/src/Plot.svelte +313 -0
  23. package/src/PlotState.svelte.js +375 -0
  24. package/src/Sparkline.svelte +108 -0
  25. package/src/Symbol.svelte +15 -11
  26. package/src/Texture.svelte +9 -7
  27. package/src/charts/AreaChart.svelte +30 -0
  28. package/src/charts/BarChart.svelte +31 -0
  29. package/src/charts/BoxPlot.svelte +24 -0
  30. package/src/charts/BubbleChart.svelte +28 -0
  31. package/src/charts/LineChart.svelte +31 -0
  32. package/src/charts/PieChart.svelte +35 -0
  33. package/src/charts/ScatterPlot.svelte +31 -0
  34. package/src/charts/ViolinPlot.svelte +24 -0
  35. package/src/crossfilter/CrossFilter.svelte +42 -0
  36. package/src/crossfilter/FilterBar.svelte +24 -0
  37. package/src/crossfilter/FilterHistogram.svelte +290 -0
  38. package/src/crossfilter/FilterSlider.svelte +83 -0
  39. package/src/crossfilter/createCrossFilter.svelte.js +124 -0
  40. package/src/elements/Bar.svelte +20 -16
  41. package/src/elements/ColorRamp.svelte +19 -19
  42. package/src/elements/ContinuousLegend.svelte +41 -19
  43. package/src/elements/DefinePatterns.svelte +9 -7
  44. package/src/elements/DiscreteLegend.svelte +13 -11
  45. package/src/elements/Label.svelte +4 -9
  46. package/src/elements/SymbolGrid.svelte +8 -9
  47. package/src/examples/BarChartExample.svelte +81 -0
  48. package/src/geoms/Arc.svelte +126 -0
  49. package/src/geoms/Area.svelte +78 -0
  50. package/src/geoms/Bar.svelte +200 -0
  51. package/src/geoms/Box.svelte +113 -0
  52. package/src/geoms/Candlestick.svelte +110 -0
  53. package/src/geoms/Heatmap.svelte +92 -0
  54. package/src/geoms/Hexbin.svelte +114 -0
  55. package/src/geoms/LabelPill.svelte +17 -0
  56. package/src/geoms/Line.svelte +123 -0
  57. package/src/geoms/Point.svelte +145 -0
  58. package/src/geoms/Ribbon.svelte +161 -0
  59. package/src/geoms/Violin.svelte +56 -0
  60. package/src/geoms/Waterfall.svelte +114 -0
  61. package/src/geoms/lib/areas.js +154 -0
  62. package/src/geoms/lib/bars.js +223 -0
  63. package/src/index.js +81 -2
  64. package/src/lib/brewing/BoxBrewer.svelte.js +14 -0
  65. package/src/lib/brewing/CartesianBrewer.svelte.js +21 -0
  66. package/src/lib/brewing/PieBrewer.svelte.js +14 -0
  67. package/src/lib/brewing/QuartileBrewer.svelte.js +51 -0
  68. package/src/lib/brewing/ViolinBrewer.svelte.js +14 -0
  69. package/src/lib/brewing/axes.svelte.js +270 -0
  70. package/src/lib/brewing/bars.svelte.js +201 -0
  71. package/src/lib/brewing/brewer.svelte.js +277 -0
  72. package/src/lib/brewing/colors.js +157 -0
  73. package/src/lib/brewing/dimensions.svelte.js +56 -0
  74. package/src/lib/brewing/index.svelte.js +205 -0
  75. package/src/lib/brewing/legends.svelte.js +137 -0
  76. package/src/lib/brewing/marks/arcs.js +43 -0
  77. package/src/lib/brewing/marks/areas.js +72 -0
  78. package/src/lib/brewing/marks/bars.js +49 -0
  79. package/src/lib/brewing/marks/boxes.js +75 -0
  80. package/src/lib/brewing/marks/lines.js +55 -0
  81. package/src/lib/brewing/marks/points.js +105 -0
  82. package/src/lib/brewing/marks/violins.js +90 -0
  83. package/src/lib/brewing/patterns.js +45 -0
  84. package/src/lib/brewing/scales.js +51 -0
  85. package/src/lib/brewing/scales.svelte.js +82 -0
  86. package/src/lib/brewing/stats.js +74 -0
  87. package/src/lib/brewing/symbols.js +10 -0
  88. package/src/lib/brewing/types.js +73 -0
  89. package/src/lib/chart.js +16 -8
  90. package/src/lib/context.js +131 -0
  91. package/src/lib/keyboard-nav.js +37 -0
  92. package/src/lib/plot/chartProps.js +76 -0
  93. package/src/lib/plot/crossfilter.js +16 -0
  94. package/src/lib/plot/facet.js +58 -0
  95. package/src/lib/plot/frames.js +81 -0
  96. package/src/lib/plot/helpers.js +14 -0
  97. package/src/lib/plot/preset.js +67 -0
  98. package/src/lib/plot/scales.js +81 -0
  99. package/src/lib/plot/stat.js +92 -0
  100. package/src/lib/plot/types.js +65 -0
  101. package/src/lib/preset.js +41 -0
  102. package/src/lib/scales.svelte.js +151 -0
  103. package/src/lib/swatch.js +4 -7
  104. package/src/lib/utils.js +142 -0
  105. package/src/lib/xscale.js +31 -0
  106. package/src/patterns/DefinePatterns.svelte +32 -0
  107. package/src/patterns/PatternDef.svelte +27 -0
  108. package/src/patterns/index.js +4 -14
  109. package/src/patterns/patterns.js +360 -0
  110. package/src/patterns/scale.js +116 -0
  111. package/src/spec/chart-spec.js +72 -0
  112. package/src/symbols/RoundedSquare.svelte +18 -12
  113. package/src/symbols/Shape.svelte +22 -16
  114. package/src/symbols/constants/index.js +1 -1
  115. package/LICENSE +0 -21
  116. package/src/lib/index.js +0 -4
  117. package/src/lib/plots.js +0 -27
  118. package/src/patterns/Brick.svelte +0 -17
  119. package/src/patterns/Circles.svelte +0 -18
  120. package/src/patterns/CrossHatch.svelte +0 -14
  121. package/src/patterns/CurvedWave.svelte +0 -9
  122. package/src/patterns/Dots.svelte +0 -19
  123. package/src/patterns/OutlineCircles.svelte +0 -15
  124. package/src/patterns/Tile.svelte +0 -17
  125. package/src/patterns/Triangles.svelte +0 -15
  126. package/src/patterns/Waves.svelte +0 -13
  127. package/src/patterns/paths/NamedPattern.svelte +0 -12
  128. package/src/patterns/paths/constants.js +0 -4
  129. package/src/plots/Plot.svelte +0 -40
  130. package/src/plots/index.js +0 -1
  131. package/src/template/Texture.svelte +0 -16
  132. package/src/template/constants.js +0 -43
  133. package/src/template/shapes/Circles.svelte +0 -16
  134. package/src/template/shapes/Lines.svelte +0 -17
  135. package/src/template/shapes/Path.svelte +0 -12
  136. package/src/template/shapes/Polygons.svelte +0 -18
  137. package/src/template/shapes/index.js +0 -4
@@ -0,0 +1,268 @@
1
+ <script>
2
+ import { getContext } from 'svelte'
3
+ import { toPatternId } from '../lib/brewing/patterns.js'
4
+ import { buildSymbolPath } from '../lib/brewing/marks/points.js'
5
+
6
+ /** @type {Record<string, string>} */
7
+ let { labels = {} } = $props()
8
+
9
+ const state = getContext('plot-state')
10
+
11
+ const isCategorical = $derived(state.colorScaleType === 'categorical')
12
+ const isLineGeom = $derived(state.geomTypes?.has('line') ?? false)
13
+ const isPointGeom = $derived(state.geomTypes?.has('point') ?? false)
14
+ const hasSymbols = $derived((state.symbols?.size ?? 0) > 0)
15
+
16
+ // Split conditions
17
+ const splitPattern = $derived(
18
+ !!state.colorField && !!state.patternField && state.colorField !== state.patternField
19
+ )
20
+ const splitSymbol = $derived(
21
+ hasSymbols &&
22
+ !!state.colorField &&
23
+ !!state.symbolField &&
24
+ state.colorField !== state.symbolField
25
+ )
26
+ const symbolOnly = $derived(hasSymbols && !state.colorField)
27
+
28
+ // Color section items — combined with same-field pattern/symbol overlays
29
+ const colorItems = $derived(
30
+ [...(state.colors?.entries() ?? [])].map(([key, entry]) => ({
31
+ key,
32
+ label: labels[String(key)] ?? String(key),
33
+ fill: entry.fill,
34
+ stroke: entry.stroke,
35
+ patternId: !splitPattern && state.patterns?.has(key) ? toPatternId(String(key)) : null,
36
+ symbolShape: !splitSymbol && state.symbols?.get(key) ? state.symbols.get(key) : null
37
+ }))
38
+ )
39
+
40
+ // Pattern section — only when pattern encodes a different field than color
41
+ const patternItems = $derived(
42
+ splitPattern
43
+ ? [...(state.patterns?.entries() ?? [])].map(([key]) => ({
44
+ key,
45
+ label: labels[String(key)] ?? String(key),
46
+ patternId: toPatternId(String(key))
47
+ }))
48
+ : []
49
+ )
50
+
51
+ // Symbol section — only when symbol encodes a different field than color, or symbol-only
52
+ const symbolItems = $derived(
53
+ splitSymbol || symbolOnly
54
+ ? [...(state.symbols?.entries() ?? [])].map(([key, shape]) => ({
55
+ key,
56
+ label: labels[String(key)] ?? String(key),
57
+ shape,
58
+ fill: state.colors?.get(key)?.fill ?? '#888',
59
+ stroke: state.colors?.get(key)?.stroke ?? '#888'
60
+ }))
61
+ : []
62
+ )
63
+
64
+ // For continuous color scales, build a CSS gradient from sampled stops
65
+ const GRADIENT_STOPS = 10
66
+ const gradientStyle = $derived.by(() => {
67
+ const cs = state.continuousColorScale
68
+ if (!cs) return 'background: linear-gradient(to right, #cfe2f3, #084594)'
69
+ const domain = cs.domain
70
+ const min = domain[0]
71
+ const max = domain[domain.length - 1]
72
+ const stops = Array.from({ length: GRADIENT_STOPS + 1 }, (_, i) => {
73
+ const t = i / GRADIENT_STOPS
74
+ const value = min + t * (max - min)
75
+ return `${cs.scale(value)} ${(t * 100).toFixed(0)}%`
76
+ })
77
+ return `background: linear-gradient(to right, ${stops.join(', ')})`
78
+ })
79
+
80
+ // Min/max labels for continuous legend
81
+ const continuousLabels = $derived.by(() => {
82
+ const cs = state.continuousColorScale
83
+ if (!cs) return null
84
+ const domain = cs.domain
85
+ const fmt = state.format(state.colorField)
86
+ const min = fmt ? fmt(domain[0]) : String(domain[0])
87
+ const max = fmt ? fmt(domain[domain.length - 1]) : String(domain[domain.length - 1])
88
+ return { min, max }
89
+ })
90
+ </script>
91
+
92
+ {#if isCategorical}
93
+ <div class="legend-root" data-plot-legend>
94
+ <!-- Symbol-only: no color field, just symbol shapes -->
95
+ {#if symbolOnly}
96
+ <div class="legend categorical">
97
+ {#each symbolItems as item (item.key)}
98
+ <div class="legend-item" data-plot-legend-item>
99
+ <svg width="14" height="14" data-plot-legend-swatch>
100
+ <path
101
+ transform="translate(7,7)"
102
+ d={buildSymbolPath(item.shape, 5)}
103
+ fill={item.fill}
104
+ />
105
+ </svg>
106
+ <span class="label" data-plot-legend-label>{item.label}</span>
107
+ </div>
108
+ {/each}
109
+ </div>
110
+
111
+ <!-- Color section (line, point, or fill swatches) -->
112
+ {:else if colorItems.length > 0}
113
+ <div class="legend categorical">
114
+ {#each colorItems as item (item.key)}
115
+ <div class="legend-item" data-plot-legend-item>
116
+ {#if isLineGeom}
117
+ <!-- Line swatch with optional combined symbol -->
118
+ <svg width="24" height="14" data-plot-legend-swatch>
119
+ <line
120
+ x1="2"
121
+ y1="7"
122
+ x2="22"
123
+ y2="7"
124
+ stroke={item.stroke}
125
+ stroke-width="2"
126
+ stroke-linecap="round"
127
+ />
128
+ {#if item.symbolShape}
129
+ <path
130
+ transform="translate(12,7)"
131
+ d={buildSymbolPath(item.symbolShape, 4)}
132
+ fill={item.stroke}
133
+ />
134
+ {/if}
135
+ </svg>
136
+ {:else if isPointGeom && item.symbolShape}
137
+ <!-- Symbol shape swatch for scatter -->
138
+ <svg width="14" height="14" data-plot-legend-swatch>
139
+ <path
140
+ transform="translate(7,7)"
141
+ d={buildSymbolPath(item.symbolShape, 5)}
142
+ fill={item.fill}
143
+ />
144
+ </svg>
145
+ {:else if item.patternId}
146
+ <!-- Fill + pattern overlay -->
147
+ <svg width="14" height="14" data-plot-legend-swatch>
148
+ <rect width="14" height="14" fill={item.fill} />
149
+ <rect width="14" height="14" fill="url(#{item.patternId})" />
150
+ </svg>
151
+ {:else}
152
+ <!-- Plain fill swatch -->
153
+ <span class="swatch" style:background-color={item.fill} data-plot-legend-swatch
154
+ ></span>
155
+ {/if}
156
+ <span class="label" data-plot-legend-label>{item.label}</span>
157
+ </div>
158
+ {/each}
159
+ </div>
160
+ {/if}
161
+
162
+ <!-- Pattern section (different field from color) -->
163
+ {#if patternItems.length > 0}
164
+ <div class="legend categorical legend-section">
165
+ {#each patternItems as item (item.key)}
166
+ <div class="legend-item" data-plot-legend-item>
167
+ <svg width="14" height="14" data-plot-legend-swatch>
168
+ <rect width="14" height="14" fill="var(--color-surface-z2, #ccc)" />
169
+ <rect width="14" height="14" fill="url(#{item.patternId})" />
170
+ </svg>
171
+ <span class="label" data-plot-legend-label>{item.label}</span>
172
+ </div>
173
+ {/each}
174
+ </div>
175
+ {/if}
176
+
177
+ <!-- Symbol section (different field from color) -->
178
+ {#if symbolItems.length > 0 && !symbolOnly}
179
+ <div class="legend categorical legend-section">
180
+ {#each symbolItems as item (item.key)}
181
+ <div class="legend-item" data-plot-legend-item>
182
+ {#if isLineGeom}
183
+ <svg width="24" height="14" data-plot-legend-swatch>
184
+ <line
185
+ x1="2"
186
+ y1="7"
187
+ x2="22"
188
+ y2="7"
189
+ stroke={item.stroke ?? item.fill}
190
+ stroke-width="2"
191
+ stroke-linecap="round"
192
+ stroke-dasharray="4 2"
193
+ />
194
+ <path
195
+ transform="translate(12,7)"
196
+ d={buildSymbolPath(item.shape, 4)}
197
+ fill={item.stroke ?? item.fill}
198
+ />
199
+ </svg>
200
+ {:else}
201
+ <svg width="14" height="14" data-plot-legend-swatch>
202
+ <path
203
+ transform="translate(7,7)"
204
+ d={buildSymbolPath(item.shape, 5)}
205
+ fill={item.fill}
206
+ />
207
+ </svg>
208
+ {/if}
209
+ <span class="label" data-plot-legend-label>{item.label}</span>
210
+ </div>
211
+ {/each}
212
+ </div>
213
+ {/if}
214
+ </div>
215
+ {:else}
216
+ <div class="legend gradient" data-plot-legend>
217
+ <div class="gradient-bar" style={gradientStyle} data-plot-legend-gradient></div>
218
+ {#if continuousLabels}
219
+ <div class="gradient-labels">
220
+ <span>{continuousLabels.min}</span>
221
+ <span>{continuousLabels.max}</span>
222
+ </div>
223
+ {/if}
224
+ </div>
225
+ {/if}
226
+
227
+ <style>
228
+ .legend-root {
229
+ display: flex;
230
+ flex-direction: column;
231
+ gap: 6px;
232
+ margin-top: 8px;
233
+ }
234
+ .legend {
235
+ display: flex;
236
+ flex-wrap: wrap;
237
+ gap: 8px;
238
+ font-size: 12px;
239
+ }
240
+ .legend-section {
241
+ border-top: 1px solid var(--color-surface-z3, #e0e0e0);
242
+ padding-top: 6px;
243
+ }
244
+ .legend-item {
245
+ display: flex;
246
+ align-items: center;
247
+ gap: 4px;
248
+ }
249
+ .swatch {
250
+ display: inline-block;
251
+ width: 14px;
252
+ height: 14px;
253
+ border-radius: 2px;
254
+ flex-shrink: 0;
255
+ }
256
+ .gradient-bar {
257
+ width: 180px;
258
+ height: 14px;
259
+ border-radius: 2px;
260
+ }
261
+ .gradient-labels {
262
+ display: flex;
263
+ justify-content: space-between;
264
+ width: 180px;
265
+ font-size: 11px;
266
+ opacity: 0.8;
267
+ }
268
+ </style>
@@ -0,0 +1,37 @@
1
+ <script>
2
+ import { getContext } from 'svelte'
3
+ import { line as d3Line } from 'd3-shape'
4
+
5
+ let {
6
+ data = [],
7
+ x = undefined,
8
+ y = undefined,
9
+ stroke = 'steelblue',
10
+ strokeWidth = 2,
11
+ curve = undefined
12
+ } = $props()
13
+
14
+ const state = getContext('plot-state')
15
+
16
+ const path = $derived.by(() => {
17
+ if (!state?.xScale || !state?.yScale || !data?.length) return null
18
+ const lineGen = d3Line()
19
+ .x((d) => state.xScale(x ? d[x] : d) ?? 0)
20
+ .y((d) => state.yScale(y ? d[y] : d) ?? 0)
21
+ .defined((d) => d != null)
22
+ if (curve) lineGen.curve(curve)
23
+ return lineGen(data)
24
+ })
25
+ </script>
26
+
27
+ {#if path}
28
+ <path
29
+ d={path}
30
+ fill="none"
31
+ {stroke}
32
+ stroke-width={strokeWidth}
33
+ stroke-linejoin="round"
34
+ stroke-linecap="round"
35
+ data-plot-element="line"
36
+ />
37
+ {/if}
@@ -0,0 +1,40 @@
1
+ <script>
2
+ import { getContext } from 'svelte'
3
+
4
+ let {
5
+ data = [],
6
+ x = undefined,
7
+ y = undefined,
8
+ r = 4,
9
+ fill = 'steelblue',
10
+ stroke = 'white',
11
+ strokeWidth = 1
12
+ } = $props()
13
+
14
+ const state = getContext('plot-state')
15
+
16
+ const points = $derived.by(() => {
17
+ if (!state?.xScale || !state?.yScale || !data?.length) return []
18
+ return data
19
+ .map((d) => ({
20
+ cx: state.xScale(x ? d[x] : d) ?? null,
21
+ cy: state.yScale(y ? d[y] : d) ?? null,
22
+ label: `(${x ? d[x] : d}, ${y ? d[y] : d})`
23
+ }))
24
+ .filter((p) => p.cx !== null && p.cy !== null)
25
+ })
26
+ </script>
27
+
28
+ {#each points as pt, i (i)}
29
+ <circle
30
+ cx={pt.cx}
31
+ cy={pt.cy}
32
+ {r}
33
+ {fill}
34
+ {stroke}
35
+ stroke-width={strokeWidth}
36
+ data-plot-element="point"
37
+ role="graphics-symbol"
38
+ aria-label={pt.label}
39
+ />
40
+ {/each}
@@ -0,0 +1,62 @@
1
+ <script>
2
+ import { setContext, untrack } from 'svelte'
3
+ import { PlotState } from '../PlotState.svelte.js'
4
+ import { defaultPreset } from '../lib/preset.js'
5
+
6
+ let {
7
+ data = [],
8
+ x = undefined,
9
+ y = undefined,
10
+ color = undefined,
11
+ width = 600,
12
+ height = 400,
13
+ margin = undefined,
14
+ mode = 'light',
15
+ children
16
+ } = $props()
17
+
18
+ const plotState = untrack(
19
+ () =>
20
+ new PlotState({
21
+ data,
22
+ channels: { x, y, color },
23
+ width,
24
+ height,
25
+ margin,
26
+ mode,
27
+ chartPreset: defaultPreset
28
+ })
29
+ )
30
+
31
+ $effect(() => {
32
+ plotState.update({
33
+ data,
34
+ channels: { x, y, color },
35
+ width,
36
+ height,
37
+ margin,
38
+ mode,
39
+ chartPreset: defaultPreset
40
+ })
41
+ })
42
+
43
+ setContext('plot-state', plotState)
44
+
45
+ const svgWidth = $derived(plotState.innerWidth + (margin?.left ?? 50) + (margin?.right ?? 30))
46
+ const svgHeight = $derived(plotState.innerHeight + (margin?.top ?? 20) + (margin?.bottom ?? 40))
47
+ const marginLeft = $derived(margin?.left ?? 50)
48
+ const marginTop = $derived(margin?.top ?? 20)
49
+ </script>
50
+
51
+ <svg
52
+ {width}
53
+ {height}
54
+ viewBox="0 0 {svgWidth} {svgHeight}"
55
+ role="img"
56
+ aria-label="Chart visualization"
57
+ data-plot-root
58
+ >
59
+ <g transform="translate({marginLeft}, {marginTop})" data-plot-canvas>
60
+ {@render children?.()}
61
+ </g>
62
+ </svg>
@@ -0,0 +1,95 @@
1
+ <script>
2
+ /**
3
+ * @type {{
4
+ * frameKeys: unknown[],
5
+ * currentIndex: number,
6
+ * playing: boolean,
7
+ * speed: number,
8
+ * onplay: () => void,
9
+ * onpause: () => void,
10
+ * onscrub: (index: number) => void,
11
+ * onspeed: (speed: number) => void
12
+ * }}
13
+ */
14
+ let {
15
+ frameKeys = [],
16
+ currentIndex = 0,
17
+ playing = false,
18
+ speed = 1,
19
+ onplay,
20
+ onpause,
21
+ onscrub,
22
+ onspeed
23
+ } = $props()
24
+
25
+ const safeIndex = $derived(
26
+ frameKeys.length === 0 ? 0 : Math.min(currentIndex, frameKeys.length - 1)
27
+ )
28
+
29
+ const SPEEDS = [0.5, 1, 1.5, 2, 4]
30
+ </script>
31
+
32
+ <div class="timeline" data-plot-timeline>
33
+ <!-- Play / Pause -->
34
+ <button
35
+ class="play-pause"
36
+ aria-label={playing ? 'Pause' : 'Play'}
37
+ onclick={() => (playing ? onpause?.() : onplay?.())}
38
+ disabled={frameKeys.length === 0}
39
+ data-plot-timeline-playpause
40
+ >
41
+ {playing ? '⏸' : '▶'}
42
+ </button>
43
+
44
+ <!-- Frame label -->
45
+ <span class="frame-label" data-plot-timeline-label>{frameKeys[safeIndex] ?? ''}</span>
46
+
47
+ <!-- Scrub slider -->
48
+ <input
49
+ type="range"
50
+ min="0"
51
+ max={Math.max(0, frameKeys.length - 1)}
52
+ value={safeIndex}
53
+ disabled={frameKeys.length === 0}
54
+ class="scrub"
55
+ aria-label="Animation timeline"
56
+ oninput={(e) => onscrub?.(Number(e.currentTarget.value))}
57
+ data-plot-timeline-scrub
58
+ />
59
+
60
+ <!-- Speed selector -->
61
+ <select
62
+ aria-label="Playback speed"
63
+ value={speed}
64
+ onchange={(e) => onspeed?.(Number(e.currentTarget.value))}
65
+ data-plot-timeline-speed
66
+ >
67
+ {#each SPEEDS as s (s)}
68
+ <option value={s}>{s}×</option>
69
+ {/each}
70
+ </select>
71
+ </div>
72
+
73
+ <style>
74
+ .timeline {
75
+ display: flex;
76
+ align-items: center;
77
+ gap: 8px;
78
+ padding: 8px 0;
79
+ font-size: 12px;
80
+ }
81
+ .play-pause {
82
+ font-size: 16px;
83
+ cursor: pointer;
84
+ background: none;
85
+ border: none;
86
+ padding: 0;
87
+ }
88
+ .scrub {
89
+ flex: 1;
90
+ }
91
+ .frame-label {
92
+ min-width: 4ch;
93
+ text-align: right;
94
+ }
95
+ </style>
@@ -0,0 +1,87 @@
1
+ <script>
2
+ import { getContext } from 'svelte'
3
+
4
+ /** @type {{ tooltip?: boolean | ((data: Record<string, unknown>) => string) }} */
5
+ let { tooltip = true } = $props()
6
+
7
+ const plotState = getContext('plot-state')
8
+
9
+ let mouseX = $state(0)
10
+ let mouseY = $state(0)
11
+
12
+ $effect(() => {
13
+ function onMove(e) {
14
+ mouseX = e.clientX
15
+ mouseY = e.clientY
16
+ }
17
+ window.addEventListener('mousemove', onMove)
18
+ return () => window.removeEventListener('mousemove', onMove)
19
+ })
20
+
21
+ const hovered = $derived(plotState.hovered)
22
+ const mode = $derived(plotState.mode)
23
+
24
+ function formatDefault(data) {
25
+ return Object.entries(data)
26
+ .filter(([, v]) => v !== undefined && v !== null)
27
+ .map(
28
+ ([k, v]) =>
29
+ `<div class="tt-row"><span class="tt-key">${k}</span><span class="tt-val">${v}</span></div>`
30
+ )
31
+ .join('')
32
+ }
33
+
34
+ const content = $derived.by(() => {
35
+ if (!hovered) return ''
36
+ if (typeof tooltip === 'function') return String(tooltip(hovered) ?? '')
37
+ return formatDefault(hovered)
38
+ })
39
+ </script>
40
+
41
+ {#if hovered && content}
42
+ <div
43
+ class="plot-tooltip"
44
+ data-mode={mode}
45
+ style:left="{mouseX + 14}px"
46
+ style:top="{mouseY - 8}px"
47
+ >
48
+ {@html content}
49
+ </div>
50
+ {/if}
51
+
52
+ <style>
53
+ .plot-tooltip {
54
+ position: fixed;
55
+ pointer-events: none;
56
+ z-index: 1000;
57
+ padding: 8px 10px;
58
+ border-radius: 6px;
59
+ font-size: 12px;
60
+ line-height: 1.5;
61
+ white-space: nowrap;
62
+ box-shadow: 0 4px 12px rgb(0 0 0 / 0.12);
63
+ background: #ffffff;
64
+ border: 1px solid #e2e8f0;
65
+ color: #1e293b;
66
+ }
67
+
68
+ .plot-tooltip[data-mode='dark'] {
69
+ background: #1e293b;
70
+ border-color: #334155;
71
+ color: #f1f5f9;
72
+ }
73
+
74
+ :global(.tt-row) {
75
+ display: flex;
76
+ gap: 10px;
77
+ justify-content: space-between;
78
+ }
79
+
80
+ :global(.tt-key) {
81
+ opacity: 0.65;
82
+ }
83
+
84
+ :global(.tt-val) {
85
+ font-weight: 600;
86
+ }
87
+ </style>
@@ -0,0 +1,9 @@
1
+ export { default as Root } from './Root.svelte'
2
+ export { default as Axis } from './Axis.svelte'
3
+ export { default as Bar } from './Bar.svelte'
4
+ export { default as Legend } from './Legend.svelte'
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'