@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.
- 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,223 @@
|
|
|
1
|
+
import { scaleBand } from 'd3-scale'
|
|
2
|
+
import { stack } from 'd3-shape'
|
|
3
|
+
import { toPatternId } from '../../lib/brewing/patterns.js'
|
|
4
|
+
import { isLiteralColor } from '../../lib/brewing/colors.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Returns a band scale suitable for bar x-positioning.
|
|
8
|
+
* When xScale is already a band scale, returns it unchanged.
|
|
9
|
+
* When xScale is a linear scale (numeric x field like year/month),
|
|
10
|
+
* derives a band scale from the distinct values in the data.
|
|
11
|
+
*/
|
|
12
|
+
function ensureBandX(xScale, data, xField) {
|
|
13
|
+
if (typeof xScale?.bandwidth === 'function') return xScale
|
|
14
|
+
const [r0, r1] = xScale.range()
|
|
15
|
+
const domain = [...new Set(data.map((d) => d[xField]))]
|
|
16
|
+
return scaleBand().domain(domain).range([r0, r1]).padding(0.2)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Returns the sub-band fields: distinct non-x fields among [color, pattern].
|
|
21
|
+
* These are the fields that cause multiple bars within a single x-band.
|
|
22
|
+
*/
|
|
23
|
+
function subBandFields(channels) {
|
|
24
|
+
const { x: xf, color: cf, pattern: pf } = channels
|
|
25
|
+
const seen = new Set()
|
|
26
|
+
const out = []
|
|
27
|
+
for (const f of [cf, pf]) {
|
|
28
|
+
if (f && f !== xf && !seen.has(f) && !isLiteralColor(f)) {
|
|
29
|
+
seen.add(f)
|
|
30
|
+
out.push(f)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return out
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function buildGroupedBars(data, channels, xScale, yScale, colors, innerHeight, patterns) {
|
|
37
|
+
const { x: xf, y: yf, color: cf, pattern: pf } = channels
|
|
38
|
+
|
|
39
|
+
const bandScale = ensureBandX(xScale, data, xf)
|
|
40
|
+
|
|
41
|
+
// Sub-banding: only fields that differ from x drive grouping within a band
|
|
42
|
+
const subFields = subBandFields(channels)
|
|
43
|
+
const getSubKey = (d) => subFields.map((f) => String(d[f])).join('::')
|
|
44
|
+
const subDomain = subFields.length > 0 ? [...new Set(data.map(getSubKey))] : []
|
|
45
|
+
const subScale =
|
|
46
|
+
subDomain.length > 1
|
|
47
|
+
? scaleBand().domain(subDomain).range([0, bandScale.bandwidth()]).padding(0.05)
|
|
48
|
+
: null
|
|
49
|
+
|
|
50
|
+
// Baseline for bars: at y=0 when domain spans zero (supports negative bars),
|
|
51
|
+
// otherwise at the bottom of the chart area.
|
|
52
|
+
const yDomain = typeof yScale.bandwidth !== 'function' ? yScale.domain?.() : null
|
|
53
|
+
const baseline =
|
|
54
|
+
yDomain && yDomain[0] <= 0 && yDomain[yDomain.length - 1] >= 0
|
|
55
|
+
? (yScale(0) ?? innerHeight)
|
|
56
|
+
: innerHeight
|
|
57
|
+
|
|
58
|
+
return data.map((d, i) => {
|
|
59
|
+
const xVal = d[xf]
|
|
60
|
+
const colorKey = cf ? d[cf] : null
|
|
61
|
+
const patternKey = pf ? d[pf] : null
|
|
62
|
+
const subKey = getSubKey(d)
|
|
63
|
+
|
|
64
|
+
const colorEntry = colors?.get(colorKey) ??
|
|
65
|
+
colors?.values().next().value ?? { fill: '#888', stroke: '#888' }
|
|
66
|
+
const patternId =
|
|
67
|
+
patternKey !== null && patternKey !== undefined && patterns?.has(patternKey)
|
|
68
|
+
? toPatternId(String(patternKey))
|
|
69
|
+
: null
|
|
70
|
+
|
|
71
|
+
const bandX = bandScale(xVal) ?? 0
|
|
72
|
+
const subX = subScale && subKey ? (subScale(subKey) ?? 0) : 0
|
|
73
|
+
const barX = bandX + subX
|
|
74
|
+
const barWidth = subScale ? subScale.bandwidth() : bandScale.bandwidth()
|
|
75
|
+
const barY = yScale(d[yf]) ?? baseline
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
data: d,
|
|
79
|
+
key: `${String(xVal)}::${subKey}::${i}`,
|
|
80
|
+
x: barX,
|
|
81
|
+
y: Math.min(barY, baseline),
|
|
82
|
+
width: barWidth,
|
|
83
|
+
height: Math.abs(baseline - barY),
|
|
84
|
+
fill: colorEntry.fill,
|
|
85
|
+
stroke: colorEntry.stroke,
|
|
86
|
+
patternId
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function buildStackedBars(data, channels, xScale, yScale, colors, innerHeight, patterns) {
|
|
92
|
+
const { x: xf, y: yf, color: cf, pattern: pf } = channels
|
|
93
|
+
|
|
94
|
+
const bandScale = ensureBandX(xScale, data, xf)
|
|
95
|
+
|
|
96
|
+
// Stack dimension: first non-x grouping field (prefer pattern, then color)
|
|
97
|
+
const subFields = subBandFields(channels)
|
|
98
|
+
if (subFields.length === 0) {
|
|
99
|
+
return buildGroupedBars(data, channels, xScale, yScale, colors, innerHeight, patterns)
|
|
100
|
+
}
|
|
101
|
+
const stackField = subFields[0]
|
|
102
|
+
|
|
103
|
+
const xCategories = [...new Set(data.map((d) => d[xf]))]
|
|
104
|
+
const stackCategories = [...new Set(data.map((d) => d[stackField]))]
|
|
105
|
+
|
|
106
|
+
const lookup = new Map()
|
|
107
|
+
for (const d of data) {
|
|
108
|
+
if (!lookup.has(d[xf])) lookup.set(d[xf], {})
|
|
109
|
+
lookup.get(d[xf])[d[stackField]] = Number(d[yf])
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const wide = xCategories.map((xVal) => {
|
|
113
|
+
const row = { [xf]: xVal }
|
|
114
|
+
for (const sk of stackCategories) row[sk] = lookup.get(xVal)?.[sk] ?? 0
|
|
115
|
+
return row
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
const stackGen = stack().keys(stackCategories)
|
|
119
|
+
const layers = stackGen(wide)
|
|
120
|
+
|
|
121
|
+
const bars = []
|
|
122
|
+
for (const layer of layers) {
|
|
123
|
+
const stackKey = layer.key
|
|
124
|
+
|
|
125
|
+
for (const point of layer) {
|
|
126
|
+
const [y0, y1] = point
|
|
127
|
+
const xVal = point.data[xf]
|
|
128
|
+
|
|
129
|
+
// Color lookup: cf may equal xf (= xVal) or stackField (= stackKey)
|
|
130
|
+
const colorKey = cf ? (cf === xf ? xVal : cf === stackField ? stackKey : null) : null
|
|
131
|
+
const colorEntry = colors?.get(colorKey) ?? { fill: '#888', stroke: '#888' }
|
|
132
|
+
|
|
133
|
+
// Pattern lookup: pf may equal xf (= xVal) or stackField (= stackKey)
|
|
134
|
+
const patternKey = pf ? (pf === xf ? xVal : pf === stackField ? stackKey : null) : null
|
|
135
|
+
const patternId =
|
|
136
|
+
patternKey !== null && patternKey !== undefined && patterns?.has(patternKey)
|
|
137
|
+
? toPatternId(String(patternKey))
|
|
138
|
+
: null
|
|
139
|
+
|
|
140
|
+
bars.push({
|
|
141
|
+
data: point.data,
|
|
142
|
+
key: `${String(xVal)}::${String(stackKey)}`,
|
|
143
|
+
x: bandScale(xVal) ?? 0,
|
|
144
|
+
y: yScale(y1),
|
|
145
|
+
width: bandScale.bandwidth(),
|
|
146
|
+
height: yScale(y0) - yScale(y1),
|
|
147
|
+
fill: colorEntry.fill,
|
|
148
|
+
stroke: colorEntry.stroke,
|
|
149
|
+
patternId
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return bars
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function buildHorizontalBars(data, channels, xScale, yScale, colors, _innerHeight) {
|
|
157
|
+
const { x: xf, y: yf, color: cf } = channels
|
|
158
|
+
const isBand = typeof yScale?.bandwidth === 'function'
|
|
159
|
+
|
|
160
|
+
if (!isBand) {
|
|
161
|
+
// Linear y-scale: rank-based bar chart race.
|
|
162
|
+
// Each row has a numeric _rank that tweens smoothly between frames.
|
|
163
|
+
// No sub-grouping — each rank position holds exactly one entity.
|
|
164
|
+
const [r0, r1] = yScale.range() // [innerHeight, 0]
|
|
165
|
+
const [d0, d1] = yScale.domain() // [0, N-1]
|
|
166
|
+
const step = Math.max(Math.abs(r0 - r1) / Math.max(d1 - d0, 1), 1)
|
|
167
|
+
const barH = step * 0.9
|
|
168
|
+
|
|
169
|
+
return data.map((d) => {
|
|
170
|
+
const rankVal = Number(d[yf])
|
|
171
|
+
const colorKey = cf ? d[cf] : null
|
|
172
|
+
const colorEntry =
|
|
173
|
+
colors?.get(colorKey) ??
|
|
174
|
+
colors?.values().next().value ?? { fill: '#888', stroke: '#888' }
|
|
175
|
+
|
|
176
|
+
const centerY = yScale(rankVal) ?? 0
|
|
177
|
+
return {
|
|
178
|
+
data: d,
|
|
179
|
+
// Key by entity name (_entity) so Svelte reuses elements as rank tweens
|
|
180
|
+
key: `${String(d._entity ?? d[yf])}::${String(colorKey ?? '')}`,
|
|
181
|
+
x: 0,
|
|
182
|
+
y: centerY - barH / 2,
|
|
183
|
+
width: xScale(d[xf]) ?? 0,
|
|
184
|
+
height: barH,
|
|
185
|
+
fill: colorEntry.fill,
|
|
186
|
+
stroke: colorEntry.stroke,
|
|
187
|
+
patternId: null
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Band scale: standard grouped horizontal bars.
|
|
193
|
+
// Only create sub-bands when multiple entities share the same y-band (true grouping).
|
|
194
|
+
const yVals = new Set(data.map((d) => d[yf]))
|
|
195
|
+
const colorKeys = cf && !isLiteralColor(cf) ? [...new Set(data.map((d) => d[cf]))] : []
|
|
196
|
+
const hasSubBands = colorKeys.length > 1 && data.length > yVals.size
|
|
197
|
+
const subScale = hasSubBands
|
|
198
|
+
? scaleBand().domain(colorKeys).range([0, yScale.bandwidth()]).padding(0.05)
|
|
199
|
+
: null
|
|
200
|
+
|
|
201
|
+
return data.map((d) => {
|
|
202
|
+
const yVal = d[yf]
|
|
203
|
+
const colorKey = cf ? d[cf] : null
|
|
204
|
+
const colorEntry =
|
|
205
|
+
colors?.get(colorKey) ??
|
|
206
|
+
colors?.values().next().value ?? { fill: '#888', stroke: '#888' }
|
|
207
|
+
|
|
208
|
+
const bandY = yScale(yVal) ?? 0
|
|
209
|
+
const subY = subScale ? (subScale(colorKey) ?? 0) : 0
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
data: d,
|
|
213
|
+
key: `${String(yVal)}::${String(colorKey ?? '')}`,
|
|
214
|
+
x: 0,
|
|
215
|
+
y: bandY + subY,
|
|
216
|
+
width: xScale(d[xf]) ?? 0,
|
|
217
|
+
height: subScale ? subScale.bandwidth() : yScale.bandwidth(),
|
|
218
|
+
fill: colorEntry.fill,
|
|
219
|
+
stroke: colorEntry.stroke,
|
|
220
|
+
patternId: null
|
|
221
|
+
}
|
|
222
|
+
})
|
|
223
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,3 +1,82 @@
|
|
|
1
|
+
import Root from './Plot/Root.svelte'
|
|
2
|
+
import Axis from './Plot/Axis.svelte'
|
|
3
|
+
import Bar from './Plot/Bar.svelte'
|
|
4
|
+
import Grid from './Plot/Grid.svelte'
|
|
5
|
+
import Legend from './Plot/Legend.svelte'
|
|
6
|
+
import Line from './Plot/Line.svelte'
|
|
7
|
+
import Area from './Plot/Area.svelte'
|
|
8
|
+
import Point from './Plot/Point.svelte'
|
|
9
|
+
import Arc from './Plot/Arc.svelte'
|
|
10
|
+
|
|
11
|
+
// Composable Plot primitives — use as <Plot.Root>, <Plot.Axis>, <Plot.Bar>, etc.
|
|
12
|
+
export const Plot = {
|
|
13
|
+
Root,
|
|
14
|
+
Axis,
|
|
15
|
+
Bar,
|
|
16
|
+
Grid,
|
|
17
|
+
Legend,
|
|
18
|
+
Line,
|
|
19
|
+
Area,
|
|
20
|
+
Point,
|
|
21
|
+
Arc
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** @deprecated Use Plot instead */
|
|
25
|
+
export const PlotLayers = Plot
|
|
26
|
+
|
|
27
|
+
// New Plot system
|
|
28
|
+
export { default as PlotChart } from './Plot.svelte'
|
|
29
|
+
export { default as ChartProvider } from './ChartProvider.svelte'
|
|
30
|
+
export { default as ChartLegend } from './ChartLegend.svelte'
|
|
31
|
+
export { default as ChartExporter } from './ChartExporter.svelte'
|
|
32
|
+
export { createChartPreset, defaultPreset } from './lib/preset.js'
|
|
33
|
+
|
|
34
|
+
// Facets and Animation
|
|
35
|
+
export { default as FacetPlot } from './FacetPlot.svelte'
|
|
36
|
+
export { default as AnimatedPlot } from './AnimatedPlot.svelte'
|
|
37
|
+
|
|
38
|
+
// Geom components (for declarative use inside PlotChart)
|
|
39
|
+
export { default as GeomBar } from './geoms/Bar.svelte'
|
|
40
|
+
export { default as GeomLine } from './geoms/Line.svelte'
|
|
41
|
+
export { default as GeomArea } from './geoms/Area.svelte'
|
|
42
|
+
export { default as GeomPoint } from './geoms/Point.svelte'
|
|
43
|
+
export { default as GeomArc } from './geoms/Arc.svelte'
|
|
44
|
+
export { default as GeomBox } from './geoms/Box.svelte'
|
|
45
|
+
export { default as GeomViolin } from './geoms/Violin.svelte'
|
|
46
|
+
export { default as GeomHeatmap } from './geoms/Heatmap.svelte'
|
|
47
|
+
export { default as GeomCandlestick } from './geoms/Candlestick.svelte'
|
|
48
|
+
export { default as GeomWaterfall } from './geoms/Waterfall.svelte'
|
|
49
|
+
export { default as GeomHexbin } from './geoms/Hexbin.svelte'
|
|
50
|
+
export { default as GeomRibbon } from './geoms/Ribbon.svelte'
|
|
51
|
+
|
|
52
|
+
// Export standalone components
|
|
1
53
|
export { default as Chart } from './Chart.svelte'
|
|
2
|
-
export { default as
|
|
3
|
-
export
|
|
54
|
+
export { default as Sparkline } from './Sparkline.svelte'
|
|
55
|
+
export { default as BarChart } from './charts/BarChart.svelte'
|
|
56
|
+
export { default as LineChart } from './charts/LineChart.svelte'
|
|
57
|
+
export { default as AreaChart } from './charts/AreaChart.svelte'
|
|
58
|
+
export { default as PieChart } from './charts/PieChart.svelte'
|
|
59
|
+
export { default as ScatterPlot } from './charts/ScatterPlot.svelte'
|
|
60
|
+
export { default as BoxPlot } from './charts/BoxPlot.svelte'
|
|
61
|
+
export { default as ViolinPlot } from './charts/ViolinPlot.svelte'
|
|
62
|
+
export { default as BubbleChart } from './charts/BubbleChart.svelte'
|
|
63
|
+
|
|
64
|
+
// Export state and types
|
|
65
|
+
export { PlotState } from './PlotState.svelte.js'
|
|
66
|
+
|
|
67
|
+
// Export utilities
|
|
68
|
+
export { ChartBrewer } from './lib/brewing/index.svelte.js'
|
|
69
|
+
export * from './lib/brewing/index.svelte.js'
|
|
70
|
+
export { buildSequentialScale, buildDivergingScale } from './lib/brewing/colors.js'
|
|
71
|
+
export { CartesianBrewer } from './lib/brewing/CartesianBrewer.svelte.js'
|
|
72
|
+
export { PieBrewer } from './lib/brewing/PieBrewer.svelte.js'
|
|
73
|
+
export { QuartileBrewer } from './lib/brewing/QuartileBrewer.svelte.js'
|
|
74
|
+
export { BoxBrewer } from './lib/brewing/BoxBrewer.svelte.js'
|
|
75
|
+
export { ViolinBrewer } from './lib/brewing/ViolinBrewer.svelte.js'
|
|
76
|
+
|
|
77
|
+
// CrossFilter system
|
|
78
|
+
export { createCrossFilter } from './crossfilter/createCrossFilter.svelte.js'
|
|
79
|
+
export { default as CrossFilter } from './crossfilter/CrossFilter.svelte'
|
|
80
|
+
export { default as FilterBar } from './crossfilter/FilterBar.svelte'
|
|
81
|
+
export { default as FilterSlider } from './crossfilter/FilterSlider.svelte'
|
|
82
|
+
export { default as FilterHistogram } from './crossfilter/FilterHistogram.svelte'
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { QuartileBrewer } from './QuartileBrewer.svelte.js'
|
|
2
|
+
import { buildBoxes } from './marks/boxes.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Brewer for box plots.
|
|
6
|
+
* fill = box interior color, color = whisker/outline stroke.
|
|
7
|
+
*/
|
|
8
|
+
export class BoxBrewer extends QuartileBrewer {
|
|
9
|
+
boxes = $derived(
|
|
10
|
+
this.xScale && this.yScale
|
|
11
|
+
? buildBoxes(this.processedData, this.channels, this.xScale, this.yScale, this.colorMap)
|
|
12
|
+
: []
|
|
13
|
+
)
|
|
14
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { SvelteSet } from 'svelte/reactivity'
|
|
2
|
+
import { ChartBrewer } from './brewer.svelte.js'
|
|
3
|
+
import { applyAggregate } from './stats.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Brewer for cartesian charts (Bar, Line, Area).
|
|
7
|
+
* Groups by x (and fill/color if set) and applies the given stat.
|
|
8
|
+
*/
|
|
9
|
+
export class CartesianBrewer extends ChartBrewer {
|
|
10
|
+
transform(data, channels, stat) {
|
|
11
|
+
if (stat === 'identity' || !channels.x || !channels.y) return data
|
|
12
|
+
// Group by all mapped aesthetic dimensions so they survive aggregation.
|
|
13
|
+
// e.g. x=region, fill=region, pattern=quarter → by=['region','quarter']
|
|
14
|
+
const by = [
|
|
15
|
+
...new SvelteSet(
|
|
16
|
+
[channels.x, channels.fill, channels.color, channels.pattern].filter(Boolean)
|
|
17
|
+
)
|
|
18
|
+
]
|
|
19
|
+
return applyAggregate(data, { by, value: channels.y, stat })
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ChartBrewer } from './brewer.svelte.js'
|
|
2
|
+
import { applyAggregate } from './stats.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Brewer for pie charts. Always aggregates by the label field.
|
|
6
|
+
* 'identity' is not meaningful for pie charts — falls back to 'sum'.
|
|
7
|
+
*/
|
|
8
|
+
export class PieBrewer extends ChartBrewer {
|
|
9
|
+
transform(data, channels, stat) {
|
|
10
|
+
if (!channels.label || !channels.y) return data
|
|
11
|
+
const effectiveStat = stat === 'identity' ? 'sum' : (stat ?? 'sum')
|
|
12
|
+
return applyAggregate(data, { by: [channels.label], value: channels.y, stat: effectiveStat })
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { quantile, ascending } from 'd3-array'
|
|
2
|
+
import { dataset } from '@rokkit/data'
|
|
3
|
+
import { ChartBrewer } from './brewer.svelte.js'
|
|
4
|
+
import { buildXScale, buildYScale } from './scales.js'
|
|
5
|
+
|
|
6
|
+
function sortedQuantile(values, p) {
|
|
7
|
+
return quantile([...values].sort(ascending), p)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Shared base for box and violin plots.
|
|
12
|
+
* Groups by x + fill (primary) or x + color (fallback) and computes quartile statistics.
|
|
13
|
+
* Subclasses add their mark-specific $derived (boxes, violins, etc.).
|
|
14
|
+
*/
|
|
15
|
+
export class QuartileBrewer extends ChartBrewer {
|
|
16
|
+
transform(data, channels) {
|
|
17
|
+
if (!channels.x || !channels.y) return data
|
|
18
|
+
const by = [channels.x, channels.fill ?? channels.color].filter(Boolean)
|
|
19
|
+
return dataset(data)
|
|
20
|
+
.groupBy(...by)
|
|
21
|
+
.summarize((row) => row[channels.y], {
|
|
22
|
+
q1: (v) => sortedQuantile(v, 0.25),
|
|
23
|
+
median: (v) => sortedQuantile(v, 0.5),
|
|
24
|
+
q3: (v) => sortedQuantile(v, 0.75),
|
|
25
|
+
iqr_min: (v) => {
|
|
26
|
+
const q1 = sortedQuantile(v, 0.25)
|
|
27
|
+
const q3 = sortedQuantile(v, 0.75)
|
|
28
|
+
return q1 - 1.5 * (q3 - q1)
|
|
29
|
+
},
|
|
30
|
+
iqr_max: (v) => {
|
|
31
|
+
const q1 = sortedQuantile(v, 0.25)
|
|
32
|
+
const q3 = sortedQuantile(v, 0.75)
|
|
33
|
+
return q3 + 1.5 * (q3 - q1)
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
.rollup()
|
|
37
|
+
.select()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
xScale = $derived(
|
|
41
|
+
this.channels.x && this.processedData.length > 0
|
|
42
|
+
? buildXScale(this.processedData, this.channels.x, this.innerWidth)
|
|
43
|
+
: null
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
yScale = $derived(
|
|
47
|
+
this.channels.y && this.processedData.length > 0
|
|
48
|
+
? buildYScale(this.processedData, 'iqr_max', this.innerHeight)
|
|
49
|
+
: null
|
|
50
|
+
)
|
|
51
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { QuartileBrewer } from './QuartileBrewer.svelte.js'
|
|
2
|
+
import { buildViolins } from './marks/violins.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Brewer for violin plots.
|
|
6
|
+
* fill = violin body color, color = outline stroke.
|
|
7
|
+
*/
|
|
8
|
+
export class ViolinBrewer extends QuartileBrewer {
|
|
9
|
+
violins = $derived(
|
|
10
|
+
this.xScale && this.yScale
|
|
11
|
+
? buildViolins(this.processedData, this.channels, this.xScale, this.yScale, this.colorMap)
|
|
12
|
+
: []
|
|
13
|
+
)
|
|
14
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
// import { scaleBand } from 'd3-scale'
|
|
2
|
+
|
|
3
|
+
import {} from './types.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {import('./types').TickData} TickData
|
|
7
|
+
* @typedef {import('./types').AxisData} AxisData
|
|
8
|
+
* @typedef {import('./types').ChartScales} ChartScales
|
|
9
|
+
* @typedef {import('./types').ChartDimensions} ChartDimensions
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const IDENTITY = (v) => v
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {Object} xScale
|
|
16
|
+
* @param {number|null} tickCount
|
|
17
|
+
* @returns {Array}
|
|
18
|
+
*/
|
|
19
|
+
function xTicksFromContinuous(xScale, tickCount) {
|
|
20
|
+
return tickCount !== null ? xScale.ticks(tickCount) : xScale.ticks()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {Array} domain
|
|
25
|
+
* @param {number} tickCount
|
|
26
|
+
* @returns {Array}
|
|
27
|
+
*/
|
|
28
|
+
function downsampleDomain(domain, tickCount) {
|
|
29
|
+
const step = Math.max(1, Math.floor(domain.length / tickCount))
|
|
30
|
+
return domain.filter((_, i) => i % step === 0)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {Object} xScale
|
|
35
|
+
* @param {number|null} tickCount
|
|
36
|
+
* @returns {Array}
|
|
37
|
+
*/
|
|
38
|
+
function xTicksFromBand(xScale, tickCount) {
|
|
39
|
+
const domain = xScale.domain()
|
|
40
|
+
if (tickCount === null || tickCount >= domain.length) return domain
|
|
41
|
+
return downsampleDomain(domain, tickCount)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {Object} xScale
|
|
46
|
+
* @param {number|null} tickCount
|
|
47
|
+
* @returns {Array}
|
|
48
|
+
*/
|
|
49
|
+
function resolveXTicks(xScale, tickCount) {
|
|
50
|
+
return xScale.ticks ? xTicksFromContinuous(xScale, tickCount) : xTicksFromBand(xScale, tickCount)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {Object} xScale
|
|
55
|
+
* @param {unknown} value
|
|
56
|
+
* @returns {number}
|
|
57
|
+
*/
|
|
58
|
+
function xPosition(xScale, value) {
|
|
59
|
+
return xScale.bandwidth ? xScale(value) + xScale.bandwidth() / 2 : xScale(value)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {Object} scale
|
|
64
|
+
* @param {Array} values
|
|
65
|
+
* @param {Function} formatter
|
|
66
|
+
* @param {(scale: Object, v: unknown) => number} getPosition
|
|
67
|
+
* @returns {Array}
|
|
68
|
+
*/
|
|
69
|
+
function mapTicks(scale, values, formatter, getPosition) {
|
|
70
|
+
return values.map((value) => ({
|
|
71
|
+
value,
|
|
72
|
+
position: getPosition(scale, value),
|
|
73
|
+
formattedValue: formatter(value)
|
|
74
|
+
}))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {Object} options
|
|
79
|
+
* @returns {{ tickCount: number|null, formatter: Function, label: string }}
|
|
80
|
+
*/
|
|
81
|
+
function parseAxisOptions(options) {
|
|
82
|
+
const opts = options || {}
|
|
83
|
+
return {
|
|
84
|
+
tickCount: opts.tickCount !== undefined ? opts.tickCount : null,
|
|
85
|
+
formatter: opts.tickFormat ? opts.tickFormat : IDENTITY,
|
|
86
|
+
label: opts.label ? opts.label : ''
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Creates x-axis tick data for rendering
|
|
92
|
+
*
|
|
93
|
+
* @param {Object} scales - Chart scales
|
|
94
|
+
* @param {Function} scales.x - X-axis scale
|
|
95
|
+
* @param {Object} dimensions - Chart dimensions
|
|
96
|
+
* @param {Object} options - Axis options
|
|
97
|
+
* @param {number} [options.tickCount] - Number of ticks to show
|
|
98
|
+
* @param {Function} [options.tickFormat] - Tick formatting function
|
|
99
|
+
* @param {string} [options.label] - Axis label
|
|
100
|
+
* @returns {AxisData} Axis rendering data
|
|
101
|
+
*/
|
|
102
|
+
export function createXAxis(scales, dimensions, options) {
|
|
103
|
+
if (!scales.x) return { ticks: [] }
|
|
104
|
+
const { tickCount, formatter, label } = parseAxisOptions(options)
|
|
105
|
+
const ticks = mapTicks(scales.x, resolveXTicks(scales.x, tickCount), formatter, xPosition)
|
|
106
|
+
return {
|
|
107
|
+
ticks,
|
|
108
|
+
label,
|
|
109
|
+
transform: `translate(0, ${dimensions.innerHeight})`,
|
|
110
|
+
labelTransform: `translate(${dimensions.innerWidth / 2}, 35)`
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @param {Object} yScale
|
|
116
|
+
* @param {number|null} tickCount
|
|
117
|
+
* @returns {Array}
|
|
118
|
+
*/
|
|
119
|
+
function yTicksFromDomain(yScale, tickCount) {
|
|
120
|
+
const domain = yScale.domain()
|
|
121
|
+
if (tickCount === null || domain.length <= tickCount) return domain
|
|
122
|
+
return downsampleDomain(domain, tickCount)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @param {Object} yScale
|
|
127
|
+
* @param {number|null} tickCount
|
|
128
|
+
* @returns {Array}
|
|
129
|
+
*/
|
|
130
|
+
function resolveYTicks(yScale, tickCount) {
|
|
131
|
+
if (yScale.ticks) return tickCount !== null ? yScale.ticks(tickCount) : yScale.ticks()
|
|
132
|
+
if (yScale.domain) return yTicksFromDomain(yScale, tickCount)
|
|
133
|
+
return []
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @param {Object} scale
|
|
138
|
+
* @param {unknown} value
|
|
139
|
+
* @returns {unknown}
|
|
140
|
+
*/
|
|
141
|
+
function yPositionPassthrough(scale, value) {
|
|
142
|
+
return scale(value)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Creates y-axis tick data for rendering
|
|
147
|
+
*
|
|
148
|
+
* @param {Object} scales - Chart scales
|
|
149
|
+
* @param {Function} scales.y - Y-axis scale
|
|
150
|
+
* @param {Object} dimensions - Chart dimensions
|
|
151
|
+
* @param {Object} options - Axis options
|
|
152
|
+
* @param {number} [options.tickCount] - Number of ticks to show
|
|
153
|
+
* @param {Function} [options.tickFormat] - Tick formatting function
|
|
154
|
+
* @param {string} [options.label] - Axis label
|
|
155
|
+
* @returns {AxisData} Axis rendering data
|
|
156
|
+
*/
|
|
157
|
+
export function createYAxis(scales, dimensions, options) {
|
|
158
|
+
if (!scales.y) return { ticks: [] }
|
|
159
|
+
const { tickCount, formatter, label } = parseAxisOptions(options)
|
|
160
|
+
const ticks = mapTicks(
|
|
161
|
+
scales.y,
|
|
162
|
+
resolveYTicks(scales.y, tickCount),
|
|
163
|
+
formatter,
|
|
164
|
+
yPositionPassthrough
|
|
165
|
+
)
|
|
166
|
+
return {
|
|
167
|
+
ticks,
|
|
168
|
+
label,
|
|
169
|
+
transform: 'translate(0, 0)',
|
|
170
|
+
labelTransform: `translate(-40, ${dimensions.innerHeight / 2}) rotate(-90)`
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* @param {Object} scales
|
|
176
|
+
* @param {Object} dimensions
|
|
177
|
+
* @param {number|null} xTickCount
|
|
178
|
+
* @returns {Array}
|
|
179
|
+
*/
|
|
180
|
+
function buildXLines(scales, dimensions, xTickCount) {
|
|
181
|
+
return createXAxis(scales, dimensions, { tickCount: xTickCount }).ticks.map((tick) => ({
|
|
182
|
+
x1: tick.position,
|
|
183
|
+
y1: 0,
|
|
184
|
+
x2: tick.position,
|
|
185
|
+
y2: dimensions.innerHeight
|
|
186
|
+
}))
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @param {Object} scales
|
|
191
|
+
* @param {Object} dimensions
|
|
192
|
+
* @param {number|null} yTickCount
|
|
193
|
+
* @returns {Array}
|
|
194
|
+
*/
|
|
195
|
+
function buildYLines(scales, dimensions, yTickCount) {
|
|
196
|
+
return createYAxis(scales, dimensions, { tickCount: yTickCount }).ticks.map((tick) => ({
|
|
197
|
+
x1: 0,
|
|
198
|
+
y1: tick.position,
|
|
199
|
+
x2: dimensions.innerWidth,
|
|
200
|
+
y2: tick.position
|
|
201
|
+
}))
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* @param {Object} opts
|
|
206
|
+
* @returns {{ direction: string, xTickCount: number|null, yTickCount: number|null }}
|
|
207
|
+
*/
|
|
208
|
+
function parseGridOptions(opts) {
|
|
209
|
+
const o = opts || {}
|
|
210
|
+
return {
|
|
211
|
+
direction: o.direction !== undefined ? o.direction : 'both',
|
|
212
|
+
xTickCount: o.xTickCount !== undefined ? o.xTickCount : null,
|
|
213
|
+
yTickCount: o.yTickCount !== undefined ? o.yTickCount : null
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* @param {string} direction
|
|
219
|
+
* @returns {{ showX: boolean, showY: boolean }}
|
|
220
|
+
*/
|
|
221
|
+
function gridDirections(direction) {
|
|
222
|
+
return {
|
|
223
|
+
showX: direction === 'x' || direction === 'both',
|
|
224
|
+
showY: direction === 'y' || direction === 'both'
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Creates grid line data for rendering
|
|
230
|
+
*
|
|
231
|
+
* @param {Object} scales - Chart scales
|
|
232
|
+
* @param {Object} dimensions - Chart dimensions
|
|
233
|
+
* @param {Object} options - Grid options
|
|
234
|
+
* @param {string} [options.direction='both'] - Grid direction ('x', 'y', or 'both')
|
|
235
|
+
* @param {number} [options.xTickCount] - Number of x-axis ticks
|
|
236
|
+
* @param {number} [options.yTickCount] - Number of y-axis ticks
|
|
237
|
+
* @returns {Object} Grid rendering data
|
|
238
|
+
*/
|
|
239
|
+
export function createGrid(scales, dimensions, options) {
|
|
240
|
+
const { direction, xTickCount, yTickCount } = parseGridOptions(options)
|
|
241
|
+
const { showX, showY } = gridDirections(direction)
|
|
242
|
+
return {
|
|
243
|
+
xLines: showX && scales.x ? buildXLines(scales, dimensions, xTickCount) : [],
|
|
244
|
+
yLines: showY && scales.y ? buildYLines(scales, dimensions, yTickCount) : []
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Creates DOM attributes for a tick element
|
|
250
|
+
*
|
|
251
|
+
* @param {TickData} tick - Tick data
|
|
252
|
+
* @param {string} axis - Axis type ('x' or 'y')
|
|
253
|
+
* @returns {Object} Attributes for the tick
|
|
254
|
+
*/
|
|
255
|
+
export function createTickAttributes(tick, axis) {
|
|
256
|
+
if (axis === 'x') {
|
|
257
|
+
return {
|
|
258
|
+
'data-plot-tick': 'major',
|
|
259
|
+
transform: `translate(${tick.position}, 0)`,
|
|
260
|
+
'text-anchor': 'middle'
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
return {
|
|
264
|
+
'data-plot-tick': 'major',
|
|
265
|
+
transform: `translate(0, ${tick.position})`,
|
|
266
|
+
'text-anchor': 'end',
|
|
267
|
+
'dominant-baseline': 'middle'
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|