@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,95 @@
1
+ <script>
2
+ import { getContext } from 'svelte'
3
+
4
+ /** @type {'x' | 'y'} */
5
+ let { type = 'x', label = '', format = undefined } = $props()
6
+
7
+ const state = getContext('plot-state')
8
+
9
+ const xTicks = $derived.by(() => {
10
+ const s = state.xScale
11
+ if (!s) return []
12
+ if (typeof s.bandwidth === 'function') {
13
+ return s.domain().map((val) => ({
14
+ value: val,
15
+ pos: (s(val) ?? 0) + s.bandwidth() / 2
16
+ }))
17
+ }
18
+ return s.ticks(6).map((val) => ({ value: val, pos: s(val) }))
19
+ })
20
+
21
+ const yTicks = $derived.by(() => {
22
+ const s = state.yScale
23
+ if (!s) return []
24
+ if (typeof s.bandwidth === 'function') {
25
+ return s.domain().map((val) => ({
26
+ value: val,
27
+ pos: (s(val) ?? 0) + s.bandwidth() / 2
28
+ }))
29
+ }
30
+ return s.ticks(6).map((val) => ({ value: val, pos: s(val) }))
31
+ })
32
+
33
+ const xTransform = $derived(`translate(0, ${state.xAxisY ?? state.innerHeight})`)
34
+ const yTransform = $derived(`translate(${state.yAxisX ?? 0}, 0)`)
35
+ </script>
36
+
37
+ {#if type === 'x'}
38
+ <g class="axis x-axis" transform={xTransform} data-plot-axis="x">
39
+ <line x1="0" y1="0" x2={state.innerWidth} y2="0" data-plot-axis-line />
40
+ {#each xTicks as tick (tick.value)}
41
+ <g transform="translate({tick.pos}, 0)" data-plot-tick>
42
+ <line x1="0" y1="0" x2="0" y2="6" stroke="currentColor" />
43
+ <text x="0" y="9" text-anchor="middle" dominant-baseline="hanging" data-plot-tick-label>
44
+ {format ? format(tick.value) : tick.value}
45
+ </text>
46
+ </g>
47
+ {/each}
48
+ {#if label}
49
+ <text
50
+ x={state.innerWidth / 2}
51
+ y="36"
52
+ text-anchor="middle"
53
+ class="axis-label"
54
+ data-plot-axis-label>{label}</text
55
+ >
56
+ {/if}
57
+ </g>
58
+ {:else}
59
+ <g class="axis y-axis" transform={yTransform} data-plot-axis="y">
60
+ <line x1="0" y1="0" x2="0" y2={state.innerHeight} data-plot-axis-line />
61
+ {#each yTicks as tick (tick.value)}
62
+ <g transform="translate(0, {tick.pos})" data-plot-tick>
63
+ <line x1="-6" y1="0" x2="0" y2="0" stroke="currentColor" />
64
+ <text x="-9" y="0" text-anchor="end" dominant-baseline="middle" data-plot-tick-label>
65
+ {format ? format(tick.value) : tick.value}
66
+ </text>
67
+ </g>
68
+ {/each}
69
+ {#if label}
70
+ <text
71
+ transform="rotate(-90)"
72
+ x={-(state.innerHeight / 2)}
73
+ y="-40"
74
+ text-anchor="middle"
75
+ class="axis-label"
76
+ data-plot-axis-label>{label}</text
77
+ >
78
+ {/if}
79
+ </g>
80
+ {/if}
81
+
82
+ <style>
83
+ .axis {
84
+ font-size: 11px;
85
+ fill: currentColor;
86
+ stroke: currentColor;
87
+ }
88
+ .axis-label {
89
+ font-size: 13px;
90
+ font-weight: 500;
91
+ }
92
+ [data-plot-axis-line] {
93
+ stroke: currentColor;
94
+ }
95
+ </style>
@@ -0,0 +1,54 @@
1
+ <script>
2
+ import { getContext } from 'svelte'
3
+
4
+ let { data = undefined, x = undefined, y = undefined, fill = 'steelblue', opacity = 1 } = $props()
5
+
6
+ const state = getContext('plot-state')
7
+
8
+ const bars = $derived.by(() => {
9
+ if (!state?.xScale || !state?.yScale) return []
10
+ const xScale = state.xScale
11
+ const yScale = state.yScale
12
+ const innerHeight = state.innerHeight
13
+ const src = data ?? []
14
+ if (!src.length) return []
15
+
16
+ const bw = typeof xScale.bandwidth === 'function' ? xScale.bandwidth() : 20
17
+ const padding = typeof xScale.bandwidth === 'function' ? bw * 0.05 : 0
18
+
19
+ // Baseline at y=0 when domain spans zero (supports negative bars)
20
+ const yDomain = typeof yScale.bandwidth !== 'function' ? yScale.domain?.() : null
21
+ const baseline =
22
+ yDomain && yDomain[0] <= 0 && yDomain[yDomain.length - 1] >= 0
23
+ ? (yScale(0) ?? innerHeight)
24
+ : innerHeight
25
+
26
+ return src.map((d) => {
27
+ const xVal = x ? d[x] : d
28
+ const yVal = y ? d[y] : d
29
+ const xPos = (xScale(xVal) ?? 0) + padding
30
+ const yPos = yScale(yVal) ?? baseline
31
+ return {
32
+ x: xPos,
33
+ y: Math.min(yPos, baseline),
34
+ width: bw * 0.9,
35
+ height: Math.abs(baseline - yPos),
36
+ label: `${xVal}: ${yVal}`
37
+ }
38
+ })
39
+ })
40
+ </script>
41
+
42
+ {#each bars as bar, i (i)}
43
+ <rect
44
+ x={bar.x}
45
+ y={bar.y}
46
+ width={bar.width}
47
+ height={bar.height}
48
+ {fill}
49
+ {opacity}
50
+ data-plot-element="bar"
51
+ role="graphics-symbol"
52
+ aria-label={bar.label}
53
+ />
54
+ {/each}
@@ -0,0 +1,34 @@
1
+ <script>
2
+ import { getContext } from 'svelte'
3
+
4
+ const state = getContext('plot-state')
5
+
6
+ const xGridLines = $derived.by(() => {
7
+ const s = state.xScale
8
+ if (!s || typeof s.bandwidth !== 'function') return []
9
+ return s.domain().map((val) => ({ pos: (s(val) ?? 0) + s.bandwidth() / 2 }))
10
+ })
11
+
12
+ const yGridLines = $derived.by(() => {
13
+ const s = state.yScale
14
+ if (!s || typeof s.ticks !== 'function') return []
15
+ return s.ticks(6).map((val) => ({ pos: s(val) }))
16
+ })
17
+ </script>
18
+
19
+ <g class="grid" data-plot-grid>
20
+ {#each yGridLines as line (line.pos)}
21
+ <line x1="0" y1={line.pos} x2={state.innerWidth} y2={line.pos} data-plot-grid-line />
22
+ {/each}
23
+ {#each xGridLines as line (line.pos)}
24
+ <line x1={line.pos} y1="0" x2={line.pos} y2={state.innerHeight} data-plot-grid-line="x" />
25
+ {/each}
26
+ </g>
27
+
28
+ <style>
29
+ [data-plot-grid-line] {
30
+ stroke: var(--chart-grid-color, currentColor);
31
+ opacity: 0.15;
32
+ stroke-dasharray: 2 4;
33
+ }
34
+ </style>
@@ -0,0 +1,233 @@
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
+ const gradientStyle = $derived.by(() => {
65
+ if (isCategorical) return ''
66
+ return `background: linear-gradient(to right, #cfe2f3, #084594)`
67
+ })
68
+ </script>
69
+
70
+ {#if isCategorical}
71
+ <div class="legend-root" data-plot-legend>
72
+ <!-- Symbol-only: no color field, just symbol shapes -->
73
+ {#if symbolOnly}
74
+ <div class="legend categorical">
75
+ {#each symbolItems as item (item.key)}
76
+ <div class="legend-item" data-plot-legend-item>
77
+ <svg width="14" height="14" data-plot-legend-swatch>
78
+ <path
79
+ transform="translate(7,7)"
80
+ d={buildSymbolPath(item.shape, 5)}
81
+ fill={item.fill}
82
+ />
83
+ </svg>
84
+ <span class="label" data-plot-legend-label>{item.label}</span>
85
+ </div>
86
+ {/each}
87
+ </div>
88
+
89
+ <!-- Color section (line, point, or fill swatches) -->
90
+ {:else if colorItems.length > 0}
91
+ <div class="legend categorical">
92
+ {#each colorItems as item (item.key)}
93
+ <div class="legend-item" data-plot-legend-item>
94
+ {#if isLineGeom}
95
+ <!-- Line swatch with optional combined symbol -->
96
+ <svg width="24" height="14" data-plot-legend-swatch>
97
+ <line
98
+ x1="2"
99
+ y1="7"
100
+ x2="22"
101
+ y2="7"
102
+ stroke={item.stroke}
103
+ stroke-width="2"
104
+ stroke-linecap="round"
105
+ />
106
+ {#if item.symbolShape}
107
+ <path
108
+ transform="translate(12,7)"
109
+ d={buildSymbolPath(item.symbolShape, 4)}
110
+ fill={item.stroke}
111
+ />
112
+ {/if}
113
+ </svg>
114
+ {:else if isPointGeom && item.symbolShape}
115
+ <!-- Symbol shape swatch for scatter -->
116
+ <svg width="14" height="14" data-plot-legend-swatch>
117
+ <path
118
+ transform="translate(7,7)"
119
+ d={buildSymbolPath(item.symbolShape, 5)}
120
+ fill={item.fill}
121
+ />
122
+ </svg>
123
+ {:else if item.patternId}
124
+ <!-- Fill + pattern overlay -->
125
+ <svg width="14" height="14" data-plot-legend-swatch>
126
+ <rect width="14" height="14" fill={item.fill} />
127
+ <rect width="14" height="14" fill="url(#{item.patternId})" />
128
+ </svg>
129
+ {:else}
130
+ <!-- Plain fill swatch -->
131
+ <span class="swatch" style:background-color={item.fill} data-plot-legend-swatch
132
+ ></span>
133
+ {/if}
134
+ <span class="label" data-plot-legend-label>{item.label}</span>
135
+ </div>
136
+ {/each}
137
+ </div>
138
+ {/if}
139
+
140
+ <!-- Pattern section (different field from color) -->
141
+ {#if patternItems.length > 0}
142
+ <div class="legend categorical legend-section">
143
+ {#each patternItems as item (item.key)}
144
+ <div class="legend-item" data-plot-legend-item>
145
+ <svg width="14" height="14" data-plot-legend-swatch>
146
+ <rect width="14" height="14" fill="var(--color-surface-z2, #ccc)" />
147
+ <rect width="14" height="14" fill="url(#{item.patternId})" />
148
+ </svg>
149
+ <span class="label" data-plot-legend-label>{item.label}</span>
150
+ </div>
151
+ {/each}
152
+ </div>
153
+ {/if}
154
+
155
+ <!-- Symbol section (different field from color) -->
156
+ {#if symbolItems.length > 0 && !symbolOnly}
157
+ <div class="legend categorical legend-section">
158
+ {#each symbolItems as item (item.key)}
159
+ <div class="legend-item" data-plot-legend-item>
160
+ {#if isLineGeom}
161
+ <svg width="24" height="14" data-plot-legend-swatch>
162
+ <line
163
+ x1="2"
164
+ y1="7"
165
+ x2="22"
166
+ y2="7"
167
+ stroke={item.stroke ?? item.fill}
168
+ stroke-width="2"
169
+ stroke-linecap="round"
170
+ stroke-dasharray="4 2"
171
+ />
172
+ <path
173
+ transform="translate(12,7)"
174
+ d={buildSymbolPath(item.shape, 4)}
175
+ fill={item.stroke ?? item.fill}
176
+ />
177
+ </svg>
178
+ {:else}
179
+ <svg width="14" height="14" data-plot-legend-swatch>
180
+ <path
181
+ transform="translate(7,7)"
182
+ d={buildSymbolPath(item.shape, 5)}
183
+ fill={item.fill}
184
+ />
185
+ </svg>
186
+ {/if}
187
+ <span class="label" data-plot-legend-label>{item.label}</span>
188
+ </div>
189
+ {/each}
190
+ </div>
191
+ {/if}
192
+ </div>
193
+ {:else}
194
+ <div class="legend gradient" data-plot-legend>
195
+ <div class="gradient-bar" style={gradientStyle} data-plot-legend-gradient></div>
196
+ </div>
197
+ {/if}
198
+
199
+ <style>
200
+ .legend-root {
201
+ display: flex;
202
+ flex-direction: column;
203
+ gap: 6px;
204
+ margin-top: 8px;
205
+ }
206
+ .legend {
207
+ display: flex;
208
+ flex-wrap: wrap;
209
+ gap: 8px;
210
+ font-size: 12px;
211
+ }
212
+ .legend-section {
213
+ border-top: 1px solid var(--color-surface-z3, #e0e0e0);
214
+ padding-top: 6px;
215
+ }
216
+ .legend-item {
217
+ display: flex;
218
+ align-items: center;
219
+ gap: 4px;
220
+ }
221
+ .swatch {
222
+ display: inline-block;
223
+ width: 14px;
224
+ height: 14px;
225
+ border-radius: 2px;
226
+ flex-shrink: 0;
227
+ }
228
+ .gradient-bar {
229
+ width: 180px;
230
+ height: 14px;
231
+ border-radius: 2px;
232
+ }
233
+ </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>