@rokkit/chart 1.0.0-next.99 → 1.0.2
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.
- package/README.md +150 -46
- package/package.json +40 -51
- package/src/AnimatedPlot.svelte +383 -0
- package/src/Chart.svelte +87 -59
- package/src/ChartExporter.svelte +125 -0
- package/src/ChartLegend.svelte +148 -0
- package/src/ChartProvider.svelte +10 -0
- package/src/FacetPlot/Panel.svelte +37 -0
- package/src/FacetPlot.svelte +114 -0
- package/src/Plot/Arc.svelte +29 -0
- package/src/Plot/Area.svelte +32 -0
- package/src/Plot/Axis.svelte +164 -0
- package/src/Plot/Bar.svelte +54 -0
- package/src/Plot/Grid.svelte +34 -0
- package/src/Plot/Legend.svelte +268 -0
- package/src/Plot/Line.svelte +37 -0
- package/src/Plot/Point.svelte +40 -0
- package/src/Plot/Root.svelte +62 -0
- package/src/Plot/Timeline.svelte +95 -0
- package/src/Plot/Tooltip.svelte +87 -0
- package/src/Plot/index.js +9 -0
- package/src/Plot.svelte +313 -0
- package/src/PlotState.svelte.js +375 -0
- package/src/Sparkline.svelte +108 -0
- package/src/Symbol.svelte +15 -11
- package/src/Texture.svelte +9 -7
- package/src/charts/AreaChart.svelte +30 -0
- package/src/charts/BarChart.svelte +31 -0
- package/src/charts/BoxPlot.svelte +24 -0
- package/src/charts/BubbleChart.svelte +28 -0
- package/src/charts/LineChart.svelte +31 -0
- package/src/charts/PieChart.svelte +35 -0
- package/src/charts/ScatterPlot.svelte +31 -0
- package/src/charts/ViolinPlot.svelte +24 -0
- package/src/crossfilter/CrossFilter.svelte +42 -0
- package/src/crossfilter/FilterBar.svelte +24 -0
- package/src/crossfilter/FilterHistogram.svelte +290 -0
- package/src/crossfilter/FilterSlider.svelte +83 -0
- package/src/crossfilter/createCrossFilter.svelte.js +124 -0
- package/src/elements/Bar.svelte +20 -16
- package/src/elements/ColorRamp.svelte +19 -19
- package/src/elements/ContinuousLegend.svelte +41 -19
- package/src/elements/DefinePatterns.svelte +9 -7
- package/src/elements/DiscreteLegend.svelte +13 -11
- package/src/elements/Label.svelte +4 -9
- package/src/elements/SymbolGrid.svelte +8 -9
- package/src/examples/BarChartExample.svelte +81 -0
- package/src/geoms/Arc.svelte +126 -0
- package/src/geoms/Area.svelte +78 -0
- package/src/geoms/Bar.svelte +200 -0
- package/src/geoms/Box.svelte +113 -0
- package/src/geoms/Candlestick.svelte +110 -0
- package/src/geoms/Heatmap.svelte +92 -0
- package/src/geoms/Hexbin.svelte +114 -0
- package/src/geoms/LabelPill.svelte +17 -0
- package/src/geoms/Line.svelte +123 -0
- package/src/geoms/Point.svelte +145 -0
- package/src/geoms/Ribbon.svelte +161 -0
- package/src/geoms/Violin.svelte +56 -0
- package/src/geoms/Waterfall.svelte +114 -0
- package/src/geoms/lib/areas.js +154 -0
- package/src/geoms/lib/bars.js +223 -0
- package/src/index.js +81 -2
- package/src/lib/brewing/BoxBrewer.svelte.js +14 -0
- package/src/lib/brewing/CartesianBrewer.svelte.js +21 -0
- package/src/lib/brewing/PieBrewer.svelte.js +14 -0
- package/src/lib/brewing/QuartileBrewer.svelte.js +51 -0
- package/src/lib/brewing/ViolinBrewer.svelte.js +14 -0
- package/src/lib/brewing/axes.svelte.js +270 -0
- package/src/lib/brewing/bars.svelte.js +201 -0
- package/src/lib/brewing/brewer.svelte.js +277 -0
- package/src/lib/brewing/colors.js +157 -0
- package/src/lib/brewing/dimensions.svelte.js +56 -0
- package/src/lib/brewing/index.svelte.js +205 -0
- package/src/lib/brewing/legends.svelte.js +137 -0
- package/src/lib/brewing/marks/arcs.js +43 -0
- package/src/lib/brewing/marks/areas.js +72 -0
- package/src/lib/brewing/marks/bars.js +49 -0
- package/src/lib/brewing/marks/boxes.js +75 -0
- package/src/lib/brewing/marks/lines.js +55 -0
- package/src/lib/brewing/marks/points.js +105 -0
- package/src/lib/brewing/marks/violins.js +90 -0
- package/src/lib/brewing/patterns.js +45 -0
- package/src/lib/brewing/scales.js +51 -0
- package/src/lib/brewing/scales.svelte.js +82 -0
- package/src/lib/brewing/stats.js +74 -0
- package/src/lib/brewing/symbols.js +10 -0
- package/src/lib/brewing/types.js +73 -0
- package/src/lib/chart.js +16 -8
- package/src/lib/context.js +131 -0
- package/src/lib/keyboard-nav.js +37 -0
- package/src/lib/plot/chartProps.js +76 -0
- package/src/lib/plot/crossfilter.js +16 -0
- package/src/lib/plot/facet.js +58 -0
- package/src/lib/plot/frames.js +81 -0
- package/src/lib/plot/helpers.js +14 -0
- package/src/lib/plot/preset.js +67 -0
- package/src/lib/plot/scales.js +81 -0
- package/src/lib/plot/stat.js +92 -0
- package/src/lib/plot/types.js +65 -0
- package/src/lib/preset.js +41 -0
- package/src/lib/scales.svelte.js +151 -0
- package/src/lib/swatch.js +4 -7
- package/src/lib/utils.js +142 -0
- package/src/lib/xscale.js +31 -0
- package/src/patterns/DefinePatterns.svelte +32 -0
- package/src/patterns/PatternDef.svelte +27 -0
- package/src/patterns/index.js +4 -14
- package/src/patterns/patterns.js +360 -0
- package/src/patterns/scale.js +116 -0
- package/src/spec/chart-spec.js +72 -0
- package/src/symbols/RoundedSquare.svelte +18 -12
- package/src/symbols/Shape.svelte +22 -16
- package/src/symbols/constants/index.js +1 -1
- package/LICENSE +0 -21
- package/src/lib/index.js +0 -4
- package/src/lib/plots.js +0 -27
- package/src/patterns/Brick.svelte +0 -17
- package/src/patterns/Circles.svelte +0 -18
- package/src/patterns/CrossHatch.svelte +0 -14
- package/src/patterns/CurvedWave.svelte +0 -9
- package/src/patterns/Dots.svelte +0 -19
- package/src/patterns/OutlineCircles.svelte +0 -15
- package/src/patterns/Tile.svelte +0 -17
- package/src/patterns/Triangles.svelte +0 -15
- package/src/patterns/Waves.svelte +0 -13
- package/src/patterns/paths/NamedPattern.svelte +0 -12
- package/src/patterns/paths/constants.js +0 -4
- package/src/plots/Plot.svelte +0 -40
- package/src/plots/index.js +0 -1
- package/src/template/Texture.svelte +0 -16
- package/src/template/constants.js +0 -43
- package/src/template/shapes/Circles.svelte +0 -16
- package/src/template/shapes/Lines.svelte +0 -17
- package/src/template/shapes/Path.svelte +0 -12
- package/src/template/shapes/Polygons.svelte +0 -18
- 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'
|