@rokkit/chart 1.0.0-next.16 → 1.0.0-next.160

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 (173) hide show
  1. package/README.md +150 -46
  2. package/package.json +42 -45
  3. package/src/AnimatedPlot.svelte +383 -0
  4. package/src/Chart.svelte +95 -0
  5. package/src/ChartProvider.svelte +10 -0
  6. package/src/FacetPlot/Panel.svelte +37 -0
  7. package/src/FacetPlot.svelte +114 -0
  8. package/src/Plot/Arc.svelte +29 -0
  9. package/src/Plot/Area.svelte +32 -0
  10. package/src/Plot/Axis.svelte +95 -0
  11. package/src/Plot/Bar.svelte +54 -0
  12. package/src/Plot/Grid.svelte +34 -0
  13. package/src/Plot/Legend.svelte +233 -0
  14. package/src/Plot/Line.svelte +37 -0
  15. package/src/Plot/Point.svelte +40 -0
  16. package/src/Plot/Root.svelte +62 -0
  17. package/src/Plot/Timeline.svelte +95 -0
  18. package/src/Plot/Tooltip.svelte +87 -0
  19. package/src/Plot/index.js +9 -0
  20. package/src/Plot.svelte +297 -0
  21. package/src/PlotState.svelte.js +350 -0
  22. package/src/Sparkline.svelte +108 -0
  23. package/src/Symbol.svelte +21 -0
  24. package/src/Texture.svelte +18 -0
  25. package/src/charts/AreaChart.svelte +27 -0
  26. package/src/charts/BarChart.svelte +28 -0
  27. package/src/charts/BoxPlot.svelte +21 -0
  28. package/src/charts/BubbleChart.svelte +23 -0
  29. package/src/charts/LineChart.svelte +26 -0
  30. package/src/charts/PieChart.svelte +35 -0
  31. package/src/charts/ScatterPlot.svelte +26 -0
  32. package/src/charts/ViolinPlot.svelte +21 -0
  33. package/src/crossfilter/CrossFilter.svelte +42 -0
  34. package/src/crossfilter/FilterBar.svelte +24 -0
  35. package/src/crossfilter/FilterHistogram.svelte +290 -0
  36. package/src/crossfilter/FilterSlider.svelte +83 -0
  37. package/src/crossfilter/createCrossFilter.svelte.js +124 -0
  38. package/src/elements/Bar.svelte +22 -24
  39. package/src/elements/ColorRamp.svelte +20 -22
  40. package/src/elements/ContinuousLegend.svelte +20 -17
  41. package/src/elements/DefinePatterns.svelte +24 -0
  42. package/src/elements/DiscreteLegend.svelte +15 -15
  43. package/src/elements/Label.svelte +4 -8
  44. package/src/elements/SymbolGrid.svelte +22 -0
  45. package/src/elements/index.js +6 -0
  46. package/src/examples/BarChartExample.svelte +81 -0
  47. package/src/geoms/Arc.svelte +126 -0
  48. package/src/geoms/Area.svelte +78 -0
  49. package/src/geoms/Bar.svelte +200 -0
  50. package/src/geoms/Box.svelte +113 -0
  51. package/src/geoms/LabelPill.svelte +17 -0
  52. package/src/geoms/Line.svelte +123 -0
  53. package/src/geoms/Point.svelte +145 -0
  54. package/src/geoms/Violin.svelte +56 -0
  55. package/src/geoms/lib/areas.js +154 -0
  56. package/src/geoms/lib/bars.js +223 -0
  57. package/src/index.js +74 -16
  58. package/src/lib/brewer.js +25 -0
  59. package/src/lib/brewing/BoxBrewer.svelte.js +14 -0
  60. package/src/lib/brewing/CartesianBrewer.svelte.js +21 -0
  61. package/src/lib/brewing/PieBrewer.svelte.js +14 -0
  62. package/src/lib/brewing/QuartileBrewer.svelte.js +51 -0
  63. package/src/lib/brewing/ViolinBrewer.svelte.js +14 -0
  64. package/src/lib/brewing/axes.svelte.js +270 -0
  65. package/src/lib/brewing/bars.svelte.js +201 -0
  66. package/src/lib/brewing/brewer.svelte.js +277 -0
  67. package/src/lib/brewing/colors.js +51 -0
  68. package/src/lib/brewing/dimensions.svelte.js +56 -0
  69. package/src/lib/brewing/index.svelte.js +205 -0
  70. package/src/lib/brewing/legends.svelte.js +137 -0
  71. package/src/lib/brewing/marks/arcs.js +43 -0
  72. package/src/lib/brewing/marks/areas.js +72 -0
  73. package/src/lib/brewing/marks/bars.js +49 -0
  74. package/src/lib/brewing/marks/boxes.js +75 -0
  75. package/src/lib/brewing/marks/lines.js +55 -0
  76. package/src/lib/brewing/marks/points.js +105 -0
  77. package/src/lib/brewing/marks/violins.js +90 -0
  78. package/src/lib/brewing/patterns.js +45 -0
  79. package/src/lib/brewing/scales.js +51 -0
  80. package/src/lib/brewing/scales.svelte.js +82 -0
  81. package/src/lib/brewing/stats.js +74 -0
  82. package/src/lib/brewing/symbols.js +10 -0
  83. package/src/lib/brewing/types.js +73 -0
  84. package/src/lib/chart.js +221 -0
  85. package/src/lib/context.js +131 -0
  86. package/src/lib/grid.js +85 -0
  87. package/src/lib/keyboard-nav.js +37 -0
  88. package/src/lib/plot/chartProps.js +76 -0
  89. package/src/lib/plot/crossfilter.js +16 -0
  90. package/src/lib/plot/facet.js +58 -0
  91. package/src/lib/plot/frames.js +81 -0
  92. package/src/lib/plot/helpers.js +14 -0
  93. package/src/lib/plot/preset.js +67 -0
  94. package/src/lib/plot/scales.js +81 -0
  95. package/src/lib/plot/stat.js +92 -0
  96. package/src/lib/plot/types.js +65 -0
  97. package/src/lib/preset.js +41 -0
  98. package/src/lib/scales.svelte.js +151 -0
  99. package/src/lib/swatch.js +13 -0
  100. package/src/lib/ticks.js +46 -0
  101. package/src/lib/utils.js +111 -118
  102. package/src/lib/xscale.js +31 -0
  103. package/src/patterns/DefinePatterns.svelte +32 -0
  104. package/src/patterns/PatternDef.svelte +27 -0
  105. package/src/patterns/index.js +4 -0
  106. package/src/patterns/patterns.js +360 -0
  107. package/src/patterns/scale.js +116 -0
  108. package/src/spec/chart-spec.js +72 -0
  109. package/src/symbols/RoundedSquare.svelte +33 -0
  110. package/src/symbols/Shape.svelte +37 -0
  111. package/src/symbols/constants/index.js +4 -0
  112. package/src/symbols/index.js +9 -0
  113. package/src/symbols/outline.svelte +60 -0
  114. package/src/symbols/solid.svelte +60 -0
  115. package/LICENSE +0 -21
  116. package/src/chart/FacetGrid.svelte +0 -51
  117. package/src/chart/Grid.svelte +0 -34
  118. package/src/chart/Legend.svelte +0 -16
  119. package/src/chart/PatternDefs.svelte +0 -13
  120. package/src/chart/Swatch.svelte +0 -93
  121. package/src/chart/SwatchButton.svelte +0 -29
  122. package/src/chart/SwatchGrid.svelte +0 -55
  123. package/src/chart/Symbol.svelte +0 -37
  124. package/src/chart/Texture.svelte +0 -16
  125. package/src/chart/TexturedShape.svelte +0 -27
  126. package/src/chart/TimelapseChart.svelte +0 -97
  127. package/src/chart/Timer.svelte +0 -27
  128. package/src/chart.js +0 -9
  129. package/src/components/charts/Axis.svelte +0 -66
  130. package/src/components/charts/Chart.svelte +0 -35
  131. package/src/components/index.js +0 -23
  132. package/src/components/lib/axis.js +0 -0
  133. package/src/components/lib/chart.js +0 -187
  134. package/src/components/lib/color.js +0 -327
  135. package/src/components/lib/funnel.js +0 -204
  136. package/src/components/lib/index.js +0 -19
  137. package/src/components/lib/pattern.js +0 -190
  138. package/src/components/lib/rollup.js +0 -55
  139. package/src/components/lib/shape.js +0 -199
  140. package/src/components/lib/summary.js +0 -145
  141. package/src/components/lib/theme.js +0 -23
  142. package/src/components/lib/timer.js +0 -41
  143. package/src/components/lib/utils.js +0 -165
  144. package/src/components/plots/BarPlot.svelte +0 -36
  145. package/src/components/plots/BoxPlot.svelte +0 -54
  146. package/src/components/plots/ScatterPlot.svelte +0 -30
  147. package/src/components/store.js +0 -70
  148. package/src/constants.js +0 -66
  149. package/src/elements/PatternDefs.svelte +0 -13
  150. package/src/elements/PatternMask.svelte +0 -20
  151. package/src/elements/Symbol.svelte +0 -38
  152. package/src/elements/Tooltip.svelte +0 -23
  153. package/src/funnel.svelte +0 -35
  154. package/src/geom.js +0 -105
  155. package/src/lib/axis.js +0 -75
  156. package/src/lib/colors.js +0 -32
  157. package/src/lib/geom.js +0 -4
  158. package/src/lib/shapes.js +0 -144
  159. package/src/lib/timer.js +0 -44
  160. package/src/lookup.js +0 -29
  161. package/src/plots/BarPlot.svelte +0 -55
  162. package/src/plots/BoxPlot.svelte +0 -0
  163. package/src/plots/FunnelPlot.svelte +0 -33
  164. package/src/plots/HeatMap.svelte +0 -5
  165. package/src/plots/HeatMapCalendar.svelte +0 -129
  166. package/src/plots/LinePlot.svelte +0 -55
  167. package/src/plots/Plot.svelte +0 -25
  168. package/src/plots/RankBarPlot.svelte +0 -38
  169. package/src/plots/ScatterPlot.svelte +0 -20
  170. package/src/plots/ViolinPlot.svelte +0 -11
  171. package/src/plots/heatmap.js +0 -70
  172. package/src/plots/index.js +0 -10
  173. package/src/swatch.js +0 -11
@@ -0,0 +1,383 @@
1
+ <script>
2
+ import { onMount, onDestroy } from 'svelte'
3
+ import { tweened } from 'svelte/motion'
4
+ import { sineInOut } from 'svelte/easing'
5
+ import { extractFrames, completeFrames, computeStaticDomains } from './lib/plot/frames.js'
6
+ import { applyGeomStat } from './lib/plot/stat.js'
7
+ import Timeline from './Plot/Timeline.svelte'
8
+ import PlotChart from './Plot.svelte'
9
+
10
+ /**
11
+ * @type {{
12
+ * data: Object[],
13
+ * animate: { by: string, duration?: number, loop?: boolean },
14
+ * x?: string,
15
+ * y?: string,
16
+ * color?: string,
17
+ * fill?: string,
18
+ * pattern?: string,
19
+ * symbol?: string,
20
+ * geom?: string,
21
+ * stat?: string,
22
+ * geoms?: import('./lib/plot/types.js').GeomSpec[],
23
+ * helpers?: import('./lib/plot/types.js').PlotHelpers,
24
+ * width?: number,
25
+ * height?: number,
26
+ * mode?: 'light' | 'dark',
27
+ * grid?: boolean,
28
+ * legend?: boolean,
29
+ * tween?: boolean,
30
+ * sorted?: boolean,
31
+ * dynamicDomain?: boolean,
32
+ * label?: boolean | string | ((data: Record<string, unknown>) => string),
33
+ * children?: import('svelte').Snippet
34
+ * }}
35
+ */
36
+ let {
37
+ data = [],
38
+ animate,
39
+ x,
40
+ y,
41
+ color,
42
+ fill = undefined,
43
+ pattern = undefined,
44
+ symbol = undefined,
45
+ geom = 'bar',
46
+ stat = 'identity',
47
+ geoms = [],
48
+ helpers = {},
49
+ width = 600,
50
+ height = 400,
51
+ mode = 'light',
52
+ grid = true,
53
+ legend = false,
54
+ tween = true,
55
+ sorted = false,
56
+ dynamicDomain = false,
57
+ label = false,
58
+ children
59
+ } = $props()
60
+
61
+ // Effective geom list: explicit array takes precedence; otherwise build from shorthand props
62
+ const effectiveGeoms = $derived(
63
+ geoms.length > 0
64
+ ? geoms
65
+ : [
66
+ {
67
+ type: geom,
68
+ stat,
69
+ ...(fill !== undefined && { fill }),
70
+ ...(pattern !== undefined && { pattern }),
71
+ ...(symbol !== undefined && { symbol })
72
+ }
73
+ ]
74
+ )
75
+
76
+ // Pre-aggregate and complete frames when any geom has a non-identity stat
77
+ const prepared = $derived.by(() => {
78
+ const firstNonIdentity = effectiveGeoms.find((g) => g.stat && g.stat !== 'identity')
79
+ if (!firstNonIdentity) return { data, geoms: effectiveGeoms }
80
+
81
+ const aggChannels = { y }
82
+ if (x) aggChannels.x = x
83
+ if (color) aggChannels.color = color
84
+ aggChannels.frame = animate.by
85
+
86
+ const aggregated = applyGeomStat(
87
+ data,
88
+ { stat: firstNonIdentity.stat, channels: aggChannels },
89
+ helpers
90
+ )
91
+ const completeData = completeFrames(aggregated, { x, y, color }, animate.by)
92
+ return { data: completeData, geoms: effectiveGeoms.map((g) => ({ ...g, stat: 'identity' })) }
93
+ })
94
+
95
+ // Extract frames and compute stable domains from the full prepared dataset
96
+ const rawFrames = $derived(extractFrames(prepared.data, animate.by))
97
+ const frameKeys = $derived([...rawFrames.keys()])
98
+
99
+ const channels = $derived({ x, y, color })
100
+ const staticDomains = $derived(
101
+ x && y
102
+ ? computeStaticDomains(prepared.data, channels)
103
+ : { xDomain: undefined, yDomain: undefined }
104
+ )
105
+
106
+ // Tweened x-domain for dynamic axis animation (opt-in via dynamicDomain prop)
107
+ const xDomainTween = tweened([0, 1], { duration: 0 })
108
+ let xDomainInitialized = false
109
+
110
+ // Playback state
111
+ let currentIndex = $state(0)
112
+ let playing = $state(false)
113
+ let speed = $state(1)
114
+
115
+ // Current frame data — already complete (all x/color combos present)
116
+ const currentFrameData = $derived.by(() => {
117
+ const key = frameKeys[currentIndex]
118
+ return rawFrames.get(key) ?? []
119
+ })
120
+
121
+ // Reduced motion preference
122
+ let prefersReducedMotion = $state(false)
123
+ onMount(() => {
124
+ if (typeof window.matchMedia !== 'function') return
125
+ const mq = window.matchMedia('(prefers-reduced-motion: reduce)')
126
+ prefersReducedMotion = mq.matches
127
+ const handler = (e) => { prefersReducedMotion = e.matches }
128
+ mq.addEventListener('change', handler)
129
+ return () => mq.removeEventListener('change', handler)
130
+ })
131
+
132
+ // Animation timing
133
+ const baseDuration = $derived(animate.duration ?? 800)
134
+ const msPerFrame = $derived(Math.round(baseDuration / speed))
135
+
136
+ // rAF-based frame timer — advances currentIndex every msPerFrame ms.
137
+ // Separate from the tween: the tween handles visual interpolation only.
138
+ let lastTime = 0
139
+ let rafId = 0
140
+
141
+ function advanceFrame() {
142
+ currentIndex = currentIndex + 1
143
+ if (currentIndex >= frameKeys.length) {
144
+ if (animate.loop ?? false) currentIndex = 0
145
+ else playing = false
146
+ }
147
+ }
148
+
149
+ function tick(time) {
150
+ if (!playing) return
151
+ if (time - lastTime >= msPerFrame) {
152
+ lastTime = time
153
+ advanceFrame()
154
+ if (!playing) return
155
+ }
156
+ rafId = requestAnimationFrame(tick)
157
+ }
158
+
159
+ $effect(() => {
160
+ // msPerFrame is a tracked dependency: re-runs on speed change, resetting lastTime=0
161
+ const _ms = msPerFrame
162
+ if (playing && !prefersReducedMotion) {
163
+ lastTime = 0
164
+ rafId = requestAnimationFrame(tick)
165
+ } else {
166
+ cancelAnimationFrame(rafId)
167
+ }
168
+ return () => cancelAnimationFrame(rafId)
169
+ })
170
+
171
+ // Reduced motion: step frames on interval instead of rAF
172
+ let reducedInterval = 0
173
+ $effect(() => {
174
+ if (!playing || !prefersReducedMotion) {
175
+ clearInterval(reducedInterval)
176
+ return
177
+ }
178
+ reducedInterval = setInterval(() => {
179
+ currentIndex = currentIndex + 1
180
+ if (currentIndex >= frameKeys.length) {
181
+ if (animate.loop ?? false) currentIndex = 0
182
+ else { playing = false; clearInterval(reducedInterval) }
183
+ }
184
+ }, msPerFrame)
185
+ return () => clearInterval(reducedInterval)
186
+ })
187
+
188
+ onDestroy(() => {
189
+ cancelAnimationFrame(rafId)
190
+ clearInterval(reducedInterval)
191
+ })
192
+
193
+ $effect(() => {
194
+ const len = frameKeys.length
195
+ if (currentIndex >= len && len > 0) {
196
+ currentIndex = len - 1
197
+ playing = false
198
+ } else if (len === 0) {
199
+ currentIndex = 0
200
+ playing = false
201
+ }
202
+ })
203
+
204
+ function handlePlay() { playing = true }
205
+ function handlePause() { playing = false }
206
+ function handleScrub(index) { playing = false; currentIndex = index }
207
+ function handleSpeed(s) { speed = s }
208
+
209
+ // Detect horizontal bar chart race: sorted=true AND y is a categorical string field
210
+ const isHorizontalRace = $derived.by(() => {
211
+ if (!sorted || !prepared.data.length) return false
212
+ const sample = prepared.data[0]
213
+ return y && typeof sample[y] === 'string'
214
+ })
215
+
216
+ // Number of unique entities (y categories) for horizontal race yDomain
217
+ const entityCount = $derived.by(() => {
218
+ if (!isHorizontalRace) return 0
219
+ return new Set(prepared.data.map((d) => d[y])).size
220
+ })
221
+
222
+ // Tweened display data — smoothly interpolates values between frames.
223
+ // Tween duration is 1.1× the frame interval so tweens always overlap:
224
+ // when the next frame fires, the previous tween is ~91% complete.
225
+ // displayTween.set() starts from the current in-flight value → seamless continuous motion.
226
+ const displayTween = tweened([], { duration: 0 })
227
+
228
+ $effect(() => {
229
+ const raw = currentFrameData
230
+ const xField = x
231
+ const yField = y
232
+
233
+ // Build display target for this frame
234
+ let target
235
+ if (isHorizontalRace) {
236
+ const ranked = raw.slice().sort((a, b) => Number(b[xField]) - Number(a[xField]))
237
+ const n = ranked.length
238
+ target = ranked.map((row, i) => ({
239
+ ...row,
240
+ _entity: row[yField],
241
+ _rank: n - 1 - i
242
+ }))
243
+ } else if (sorted) {
244
+ target = raw.slice().sort((a, b) => Number(b[yField]) - Number(a[yField]))
245
+ } else {
246
+ target = raw
247
+ }
248
+
249
+ // Tween duration slightly longer than frame interval → guaranteed overlap, no pause
250
+ const tweenDuration = Math.round(msPerFrame * 1.1)
251
+
252
+ if (!tween || prefersReducedMotion) {
253
+ displayTween.set(target, { duration: 0 })
254
+ } else if (isHorizontalRace) {
255
+ displayTween.set(target, {
256
+ duration: tweenDuration,
257
+ easing: sineInOut,
258
+ interpolate: (a, b) => {
259
+ const aMap = new Map(a.map((r) => [r._entity, r]))
260
+ return (t) =>
261
+ b.map((r) => {
262
+ const p = aMap.get(r._entity) ?? r
263
+ return {
264
+ ...r,
265
+ [xField]: Number(p[xField] ?? 0) * (1 - t) + Number(r[xField]) * t,
266
+ _rank: Number(p._rank ?? r._rank) * (1 - t) + Number(r._rank) * t
267
+ }
268
+ })
269
+ }
270
+ })
271
+ } else if (sorted) {
272
+ displayTween.set(target, {
273
+ duration: tweenDuration,
274
+ easing: sineInOut,
275
+ interpolate: (a, b) => {
276
+ const aMap = new Map(a.map((r) => [r[xField], r]))
277
+ return (t) =>
278
+ b.map((r) => {
279
+ const p = aMap.get(r[xField]) ?? r
280
+ return { ...r, [yField]: Number(p[yField] ?? 0) * (1 - t) + Number(r[yField]) * t }
281
+ })
282
+ }
283
+ })
284
+ } else {
285
+ // Unsorted: if y is categorical (string), key by y-value and lerp x (bar width).
286
+ // This keeps bars at stable band positions while widths animate smoothly.
287
+ const yCategorical = yField && raw.length > 0 && typeof raw[0][yField] === 'string'
288
+ if (yCategorical) {
289
+ displayTween.set(target, {
290
+ duration: tweenDuration,
291
+ easing: sineInOut,
292
+ interpolate: (a, b) => {
293
+ const aMap = new Map(a.map((r) => [r[yField], r]))
294
+ return (t) =>
295
+ b.map((r) => {
296
+ const p = aMap.get(r[yField]) ?? r
297
+ return { ...r, [xField]: Number(p[xField] ?? 0) * (1 - t) + Number(r[xField]) * t }
298
+ })
299
+ }
300
+ })
301
+ } else {
302
+ displayTween.set(target, {
303
+ duration: tweenDuration,
304
+ easing: sineInOut,
305
+ interpolate: (a, b) => (t) =>
306
+ b.map((r, i) => ({
307
+ ...r,
308
+ [yField]: Number(a[i]?.[yField] ?? 0) * (1 - t) + Number(r[yField]) * t
309
+ }))
310
+ })
311
+ }
312
+ }
313
+ })
314
+
315
+ // Tween the x-domain per frame for dynamic axis animation (opt-in).
316
+ // First update is instant to avoid a jarring jump when toggled.
317
+ $effect(() => {
318
+ if (!dynamicDomain || !x || !currentFrameData.length) {
319
+ if (!dynamicDomain) xDomainInitialized = false
320
+ return
321
+ }
322
+ const vals = currentFrameData.map((d) => Number(d[x])).filter((v) => !isNaN(v))
323
+ if (vals.length === 0) return
324
+ const max = Math.max(...vals)
325
+ xDomainTween.set([0, max], {
326
+ duration: xDomainInitialized ? Math.round(msPerFrame * 1.1) : 0,
327
+ easing: sineInOut
328
+ })
329
+ xDomainInitialized = true
330
+ })
331
+
332
+ // For horizontal race: inject orientation, label, and labelInside into each geom
333
+ const raceGeoms = $derived(
334
+ prepared.geoms.map((g) => ({
335
+ ...g,
336
+ ...(label && { label: '_entity' }),
337
+ options: {
338
+ ...(g.options ?? {}),
339
+ orientation: 'horizontal',
340
+ ...(label && { labelInside: true })
341
+ }
342
+ }))
343
+ )
344
+
345
+ const xDomainForFrame = $derived(
346
+ isHorizontalRace && dynamicDomain ? $xDomainTween : staticDomains.xDomain
347
+ )
348
+ const frameSpec = $derived({
349
+ data: $displayTween,
350
+ x,
351
+ y: isHorizontalRace ? '_rank' : y,
352
+ color,
353
+ geoms: isHorizontalRace ? raceGeoms : prepared.geoms,
354
+ xDomain: xDomainForFrame,
355
+ yDomain: isHorizontalRace ? [0, entityCount - 1] : staticDomains.yDomain,
356
+ orientation: isHorizontalRace ? 'horizontal' : undefined
357
+ })
358
+ </script>
359
+
360
+ <div data-plot-animated>
361
+ <PlotChart spec={frameSpec} {helpers} {width} {height} {mode} {grid} {legend}>
362
+ {@render children?.()}
363
+ </PlotChart>
364
+
365
+ <Timeline
366
+ {frameKeys}
367
+ {currentIndex}
368
+ {playing}
369
+ {speed}
370
+ onplay={handlePlay}
371
+ onpause={handlePause}
372
+ onscrub={handleScrub}
373
+ onspeed={handleSpeed}
374
+ />
375
+ </div>
376
+
377
+ <style>
378
+ [data-plot-animated] {
379
+ display: flex;
380
+ flex-direction: column;
381
+ width: 100%;
382
+ }
383
+ </style>
@@ -0,0 +1,95 @@
1
+ <script>
2
+ import { setContext } from 'svelte'
3
+ import { ChartBrewer } from './lib/brewing/brewer.svelte.js'
4
+
5
+ /**
6
+ * @type {{
7
+ * spec?: import('./spec/chart-spec.js').ChartSpec,
8
+ * data?: Object[],
9
+ * x?: string,
10
+ * y?: string,
11
+ * color?: string,
12
+ * pattern?: string,
13
+ * fill?: string,
14
+ * size?: string,
15
+ * label?: string,
16
+ * symbol?: string,
17
+ * width?: number,
18
+ * height?: number,
19
+ * mode?: 'light' | 'dark',
20
+ * children?: import('svelte').Snippet
21
+ * }}
22
+ */
23
+ let {
24
+ spec = undefined,
25
+ data = [],
26
+ x = undefined,
27
+ y = undefined,
28
+ color = undefined,
29
+ pattern = undefined,
30
+ fill = undefined,
31
+ size = undefined,
32
+ label = undefined,
33
+ symbol = undefined,
34
+ width = 600,
35
+ height = 400,
36
+ mode = 'light',
37
+ children
38
+ } = $props()
39
+
40
+ const brewer = new ChartBrewer()
41
+ setContext('chart-brewer', brewer)
42
+
43
+ function buildChannels() {
44
+ const channels = {}
45
+ if (x) channels.x = x
46
+ if (y) channels.y = y
47
+ if (color) channels.color = color
48
+ if (pattern) channels.pattern = pattern
49
+ if (fill) channels.fill = fill
50
+ if (size) channels.size = size
51
+ if (label) channels.label = label
52
+ if (symbol) channels.symbol = symbol
53
+ return channels
54
+ }
55
+
56
+ $effect(() => {
57
+ if (spec) {
58
+ brewer.update({
59
+ data: spec.data,
60
+ channels: spec.channels,
61
+ width,
62
+ height,
63
+ mode,
64
+ layers: spec.layers
65
+ })
66
+ } else {
67
+ brewer.update({ data, channels: buildChannels(), width, height, mode })
68
+ }
69
+ })
70
+ </script>
71
+
72
+ <div class="chart-container" data-chart-root>
73
+ <svg {width} {height} viewBox="0 0 {width} {height}" role="img" aria-label="Chart visualization">
74
+ <g class="chart-area" data-chart-canvas>
75
+ {@render children?.()}
76
+ </g>
77
+ </svg>
78
+ </div>
79
+
80
+ <style>
81
+ .chart-container {
82
+ position: relative;
83
+ width: 100%;
84
+ height: auto;
85
+ }
86
+
87
+ svg {
88
+ display: block;
89
+ overflow: visible;
90
+ }
91
+
92
+ .chart-area {
93
+ pointer-events: all;
94
+ }
95
+ </style>
@@ -0,0 +1,10 @@
1
+ <script>
2
+ import { setContext } from 'svelte'
3
+ import { createChartPreset } from './lib/preset.js'
4
+
5
+ let { preset = createChartPreset(), children } = $props()
6
+
7
+ setContext('chart-preset', { get current() { return preset } })
8
+ </script>
9
+
10
+ {@render children?.()}
@@ -0,0 +1,37 @@
1
+ <script>
2
+ import PlotChart from '../Plot.svelte'
3
+
4
+ let {
5
+ data,
6
+ x,
7
+ y,
8
+ color,
9
+ geoms = [],
10
+ helpers = {},
11
+ width,
12
+ height,
13
+ mode,
14
+ grid,
15
+ legend,
16
+ xDomain,
17
+ yDomain,
18
+ colorDomain,
19
+ children
20
+ } = $props()
21
+
22
+ // Build spec with domain overrides so PlotState uses them
23
+ const spec = $derived({
24
+ data,
25
+ x,
26
+ y,
27
+ color,
28
+ geoms,
29
+ xDomain,
30
+ yDomain,
31
+ colorDomain
32
+ })
33
+ </script>
34
+
35
+ <PlotChart {spec} {helpers} {width} {height} {mode} {grid} {legend}>
36
+ {@render children?.()}
37
+ </PlotChart>
@@ -0,0 +1,114 @@
1
+ <script>
2
+ import { splitByField, getFacetDomains } from './lib/plot/facet.js'
3
+ import { distinct } from './lib/brewing/colors.js'
4
+ import PlotPanel from './FacetPlot/Panel.svelte'
5
+
6
+ /**
7
+ * @type {{
8
+ * data: Object[],
9
+ * facet: { by: string, cols?: number, scales?: 'fixed'|'free'|'free_x'|'free_y' },
10
+ * x?: string,
11
+ * y?: string,
12
+ * color?: string,
13
+ * fill?: string,
14
+ * pattern?: string,
15
+ * symbol?: string,
16
+ * geom?: string,
17
+ * stat?: string,
18
+ * geoms?: import('./lib/plot/types.js').GeomSpec[],
19
+ * helpers?: import('./lib/plot/types.js').PlotHelpers,
20
+ * panelWidth?: number,
21
+ * panelHeight?: number,
22
+ * width?: number,
23
+ * height?: number,
24
+ * mode?: 'light' | 'dark',
25
+ * grid?: boolean,
26
+ * legend?: boolean,
27
+ * children?: import('svelte').Snippet
28
+ * }}
29
+ */
30
+ let {
31
+ data = [],
32
+ facet,
33
+ x,
34
+ y,
35
+ fill = undefined,
36
+ color = undefined,
37
+ pattern = undefined,
38
+ symbol = undefined,
39
+ geom = 'bar',
40
+ stat = 'identity',
41
+ geoms = [],
42
+ helpers = {},
43
+ panelWidth,
44
+ panelHeight,
45
+ width = 900,
46
+ height = 300,
47
+ mode = 'light',
48
+ grid = true,
49
+ legend = false,
50
+ children
51
+ } = $props()
52
+
53
+ // `fill` is accepted as an alias for `color` (bar/area semantics vs line/point)
54
+ const colorChannel = $derived(fill ?? color)
55
+
56
+ // Effective geom list: explicit array takes precedence; otherwise build from shorthand props
57
+ const effectiveGeoms = $derived(
58
+ geoms.length > 0
59
+ ? geoms
60
+ : [
61
+ {
62
+ type: geom,
63
+ stat,
64
+ ...(pattern !== undefined && { pattern }),
65
+ ...(symbol !== undefined && { symbol })
66
+ }
67
+ ]
68
+ )
69
+
70
+ const panels = $derived(splitByField(data, facet.by))
71
+ const scales = $derived(facet.scales ?? 'fixed')
72
+ const domains = $derived(x && y ? getFacetDomains(panels, { x, y }, scales) : new Map())
73
+
74
+ // Global color domain ensures the same value maps to the same color in every panel.
75
+ const colorDomain = $derived(colorChannel ? distinct(data, colorChannel) : undefined)
76
+
77
+ const cols = $derived(facet.cols ?? Math.min(panels.size, 3))
78
+ const pw = $derived(panelWidth ?? Math.floor(width / cols))
79
+ const ph = $derived(panelHeight ?? height)
80
+ </script>
81
+
82
+ <div data-facet-grid style:--facet-cols={cols}>
83
+ {#each [...panels.entries()] as [facetValue, panelData] (`${facetValue}`)}
84
+ <div data-facet-panel data-facet-value={facetValue}>
85
+ <div data-facet-title>{facetValue}</div>
86
+ <PlotPanel
87
+ data={panelData}
88
+ {x}
89
+ {y}
90
+ color={colorChannel}
91
+ geoms={effectiveGeoms}
92
+ {helpers}
93
+ width={pw}
94
+ height={ph}
95
+ {mode}
96
+ {grid}
97
+ legend={false}
98
+ xDomain={domains.get(facetValue)?.xDomain}
99
+ yDomain={domains.get(facetValue)?.yDomain}
100
+ {colorDomain}
101
+ >
102
+ <!-- Render caller-supplied geoms inside every panel (each gets its own PlotState context) -->
103
+ {@render children?.()}
104
+ </PlotPanel>
105
+ </div>
106
+ {/each}
107
+ </div>
108
+
109
+ <!-- Single shared legend outside the grid -->
110
+ {#if legend}
111
+ <div data-facet-legend>
112
+ <!-- Legend content rendered by first panel; simplified for now -->
113
+ </div>
114
+ {/if}
@@ -0,0 +1,29 @@
1
+ <script>
2
+ import { getContext } from 'svelte'
3
+
4
+ const brewer = getContext('chart-brewer')
5
+ </script>
6
+
7
+ {#if brewer && brewer.arcs && brewer.arcs.length > 0}
8
+ <g
9
+ class="chart-arcs"
10
+ data-plot-type="arc"
11
+ transform="translate({brewer.width / 2}, {brewer.height / 2})"
12
+ >
13
+ {#each brewer.arcs as arc (arc.key)}
14
+ <path
15
+ d={arc.d}
16
+ fill={arc.fill}
17
+ stroke={arc.stroke}
18
+ stroke-width="1"
19
+ data-plot-element="arc"
20
+ />
21
+ {/each}
22
+ </g>
23
+ {/if}
24
+
25
+ <style>
26
+ .chart-arcs {
27
+ pointer-events: none;
28
+ }
29
+ </style>
@@ -0,0 +1,32 @@
1
+ <script>
2
+ import { getContext } from 'svelte'
3
+ import { area as d3Area } from 'd3-shape'
4
+
5
+ let {
6
+ data = [],
7
+ x = undefined,
8
+ y = undefined,
9
+ y0 = undefined,
10
+ fill = 'steelblue',
11
+ opacity = 0.7,
12
+ curve = undefined
13
+ } = $props()
14
+
15
+ const state = getContext('plot-state')
16
+
17
+ const path = $derived.by(() => {
18
+ if (!state?.xScale || !state?.yScale || !data?.length) return null
19
+ const innerHeight = state.innerHeight
20
+ const areaGen = d3Area()
21
+ .x((d) => state.xScale(x ? d[x] : d) ?? 0)
22
+ .y1((d) => state.yScale(y ? d[y] : d) ?? 0)
23
+ .y0((d) => (y0 !== undefined ? state.yScale(d[y0] ?? y0) : innerHeight) ?? innerHeight)
24
+ .defined((d) => d != null)
25
+ if (curve) areaGen.curve(curve)
26
+ return areaGen(data)
27
+ })
28
+ </script>
29
+
30
+ {#if path}
31
+ <path d={path} {fill} {opacity} stroke="none" data-plot-element="area" />
32
+ {/if}