@rokkit/chart 1.0.0-next.151 → 1.0.0-next.155

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 (86) hide show
  1. package/dist/PlotState.svelte.d.ts +26 -0
  2. package/dist/index.d.ts +6 -1
  3. package/dist/lib/brewing/BoxBrewer.svelte.d.ts +3 -5
  4. package/dist/lib/brewing/QuartileBrewer.svelte.d.ts +9 -0
  5. package/dist/lib/brewing/ViolinBrewer.svelte.d.ts +3 -4
  6. package/dist/lib/brewing/colors.d.ts +10 -1
  7. package/dist/lib/brewing/marks/points.d.ts +17 -2
  8. package/dist/lib/keyboard-nav.d.ts +15 -0
  9. package/dist/lib/plot/preset.d.ts +1 -1
  10. package/dist/lib/preset.d.ts +30 -0
  11. package/package.json +2 -1
  12. package/src/AnimatedPlot.svelte +375 -207
  13. package/src/Chart.svelte +81 -84
  14. package/src/ChartProvider.svelte +10 -0
  15. package/src/FacetPlot/Panel.svelte +30 -16
  16. package/src/FacetPlot.svelte +100 -76
  17. package/src/Plot/Area.svelte +26 -19
  18. package/src/Plot/Axis.svelte +81 -59
  19. package/src/Plot/Bar.svelte +47 -89
  20. package/src/Plot/Grid.svelte +23 -19
  21. package/src/Plot/Legend.svelte +213 -147
  22. package/src/Plot/Line.svelte +31 -21
  23. package/src/Plot/Point.svelte +35 -22
  24. package/src/Plot/Root.svelte +46 -91
  25. package/src/Plot/Timeline.svelte +82 -82
  26. package/src/Plot/Tooltip.svelte +68 -62
  27. package/src/Plot.svelte +290 -174
  28. package/src/PlotState.svelte.js +338 -265
  29. package/src/Sparkline.svelte +95 -56
  30. package/src/charts/AreaChart.svelte +22 -20
  31. package/src/charts/BarChart.svelte +23 -21
  32. package/src/charts/BoxPlot.svelte +15 -15
  33. package/src/charts/BubbleChart.svelte +17 -17
  34. package/src/charts/LineChart.svelte +20 -20
  35. package/src/charts/PieChart.svelte +30 -20
  36. package/src/charts/ScatterPlot.svelte +20 -19
  37. package/src/charts/ViolinPlot.svelte +15 -15
  38. package/src/crossfilter/CrossFilter.svelte +33 -29
  39. package/src/crossfilter/FilterBar.svelte +17 -25
  40. package/src/crossfilter/FilterHistogram.svelte +290 -0
  41. package/src/crossfilter/FilterSlider.svelte +69 -65
  42. package/src/crossfilter/createCrossFilter.svelte.js +94 -90
  43. package/src/geoms/Arc.svelte +114 -69
  44. package/src/geoms/Area.svelte +67 -39
  45. package/src/geoms/Bar.svelte +184 -126
  46. package/src/geoms/Box.svelte +101 -91
  47. package/src/geoms/LabelPill.svelte +11 -11
  48. package/src/geoms/Line.svelte +110 -86
  49. package/src/geoms/Point.svelte +130 -90
  50. package/src/geoms/Violin.svelte +51 -41
  51. package/src/geoms/lib/areas.js +122 -99
  52. package/src/geoms/lib/bars.js +195 -144
  53. package/src/index.js +21 -14
  54. package/src/lib/brewing/BoxBrewer.svelte.js +8 -50
  55. package/src/lib/brewing/CartesianBrewer.svelte.js +11 -7
  56. package/src/lib/brewing/PieBrewer.svelte.js +5 -5
  57. package/src/lib/brewing/QuartileBrewer.svelte.js +51 -0
  58. package/src/lib/brewing/ViolinBrewer.svelte.js +8 -49
  59. package/src/lib/brewing/brewer.svelte.js +242 -195
  60. package/src/lib/brewing/colors.js +34 -5
  61. package/src/lib/brewing/marks/arcs.js +28 -28
  62. package/src/lib/brewing/marks/areas.js +54 -41
  63. package/src/lib/brewing/marks/bars.js +34 -34
  64. package/src/lib/brewing/marks/boxes.js +51 -51
  65. package/src/lib/brewing/marks/lines.js +37 -30
  66. package/src/lib/brewing/marks/points.js +74 -26
  67. package/src/lib/brewing/marks/violins.js +57 -57
  68. package/src/lib/brewing/patterns.js +25 -11
  69. package/src/lib/brewing/scales.js +17 -17
  70. package/src/lib/brewing/stats.js +37 -29
  71. package/src/lib/brewing/symbols.js +1 -1
  72. package/src/lib/chart.js +2 -1
  73. package/src/lib/keyboard-nav.js +37 -0
  74. package/src/lib/plot/crossfilter.js +5 -5
  75. package/src/lib/plot/facet.js +30 -30
  76. package/src/lib/plot/frames.js +30 -29
  77. package/src/lib/plot/helpers.js +4 -4
  78. package/src/lib/plot/preset.js +48 -34
  79. package/src/lib/plot/scales.js +64 -39
  80. package/src/lib/plot/stat.js +47 -47
  81. package/src/lib/preset.js +41 -0
  82. package/src/patterns/DefinePatterns.svelte +24 -24
  83. package/src/patterns/README.md +3 -0
  84. package/src/patterns/patterns.js +328 -176
  85. package/src/patterns/scale.js +61 -32
  86. package/src/spec/chart-spec.js +64 -21
@@ -1,6 +1,7 @@
1
1
  import { scaleBand } from 'd3-scale'
2
2
  import { stack } from 'd3-shape'
3
3
  import { toPatternId } from '../../lib/brewing/patterns.js'
4
+ import { isLiteralColor } from '../../lib/brewing/colors.js'
4
5
 
5
6
  /**
6
7
  * Returns a band scale suitable for bar x-positioning.
@@ -9,10 +10,10 @@ import { toPatternId } from '../../lib/brewing/patterns.js'
9
10
  * derives a band scale from the distinct values in the data.
10
11
  */
11
12
  function ensureBandX(xScale, data, xField) {
12
- if (typeof xScale?.bandwidth === 'function') return xScale
13
- const [r0, r1] = xScale.range()
14
- const domain = [...new Set(data.map((d) => d[xField]))]
15
- return scaleBand().domain(domain).range([r0, r1]).padding(0.2)
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)
16
17
  }
17
18
 
18
19
  /**
@@ -20,153 +21,203 @@ function ensureBandX(xScale, data, xField) {
20
21
  * These are the fields that cause multiple bars within a single x-band.
21
22
  */
22
23
  function subBandFields(channels) {
23
- const { x: xf, color: cf, pattern: pf } = channels
24
- const seen = new Set()
25
- const out = []
26
- for (const f of [cf, pf]) {
27
- if (f && f !== xf && !seen.has(f)) { seen.add(f); out.push(f) }
28
- }
29
- return out
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
30
34
  }
31
35
 
32
36
  export function buildGroupedBars(data, channels, xScale, yScale, colors, innerHeight, patterns) {
33
- const { x: xf, y: yf, color: cf, pattern: pf } = channels
34
-
35
- const bandScale = ensureBandX(xScale, data, xf)
36
-
37
- // Sub-banding: only fields that differ from x drive grouping within a band
38
- const subFields = subBandFields(channels)
39
- const getSubKey = (d) => subFields.map((f) => String(d[f])).join('::')
40
- const subDomain = subFields.length > 0 ? [...new Set(data.map(getSubKey))] : []
41
- const subScale = subDomain.length > 1
42
- ? scaleBand().domain(subDomain).range([0, bandScale.bandwidth()]).padding(0.05)
43
- : null
44
-
45
- return data.map((d, i) => {
46
- const xVal = d[xf]
47
- const colorKey = cf ? d[cf] : null
48
- const patternKey = pf ? d[pf] : null
49
- const subKey = getSubKey(d)
50
-
51
- const colorEntry = colors?.get(colorKey) ?? colors?.values().next().value ?? { fill: '#888', stroke: '#888' }
52
- const patternId = patternKey !== null && patternKey !== undefined && patterns?.has(patternKey)
53
- ? toPatternId(String(patternKey))
54
- : null
55
-
56
- const bandX = bandScale(xVal) ?? 0
57
- const subX = subScale && subKey ? (subScale(subKey) ?? 0) : 0
58
- const barX = bandX + subX
59
- const barWidth = subScale ? subScale.bandwidth() : bandScale.bandwidth()
60
- const barY = yScale(d[yf])
61
- const barHeight = innerHeight - barY
62
-
63
- return {
64
- data: d,
65
- key: `${String(xVal)}::${subKey}::${i}`,
66
- x: barX,
67
- y: barY,
68
- width: barWidth,
69
- height: barHeight,
70
- fill: colorEntry.fill,
71
- stroke: colorEntry.stroke,
72
- patternId
73
- }
74
- })
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
+ })
75
89
  }
76
90
 
77
91
  export function buildStackedBars(data, channels, xScale, yScale, colors, innerHeight, patterns) {
78
- const { x: xf, y: yf, color: cf, pattern: pf } = channels
79
-
80
- const bandScale = ensureBandX(xScale, data, xf)
81
-
82
- // Stack dimension: first non-x grouping field (prefer pattern, then color)
83
- const subFields = subBandFields(channels)
84
- if (subFields.length === 0) {
85
- return buildGroupedBars(data, channels, xScale, yScale, colors, innerHeight, patterns)
86
- }
87
- const stackField = subFields[0]
88
-
89
- const xCategories = [...new Set(data.map((d) => d[xf]))]
90
- const stackCategories = [...new Set(data.map((d) => d[stackField]))]
91
-
92
- const lookup = new Map()
93
- for (const d of data) {
94
- if (!lookup.has(d[xf])) lookup.set(d[xf], {})
95
- lookup.get(d[xf])[d[stackField]] = Number(d[yf])
96
- }
97
-
98
- const wide = xCategories.map((xVal) => {
99
- const row = { [xf]: xVal }
100
- for (const sk of stackCategories) row[sk] = lookup.get(xVal)?.[sk] ?? 0
101
- return row
102
- })
103
-
104
- const stackGen = stack().keys(stackCategories)
105
- const layers = stackGen(wide)
106
-
107
- const bars = []
108
- for (const layer of layers) {
109
- const stackKey = layer.key
110
-
111
- for (const point of layer) {
112
- const [y0, y1] = point
113
- const xVal = point.data[xf]
114
-
115
- // Color lookup: cf may equal xf (= xVal) or stackField (= stackKey)
116
- const colorKey = cf
117
- ? (cf === xf ? xVal : cf === stackField ? stackKey : null)
118
- : null
119
- const colorEntry = colors?.get(colorKey) ?? { fill: '#888', stroke: '#888' }
120
-
121
- // Pattern lookup: pf may equal xf (= xVal) or stackField (= stackKey)
122
- const patternKey = pf
123
- ? (pf === xf ? xVal : pf === stackField ? stackKey : null)
124
- : null
125
- const patternId = patternKey !== null && patternKey !== undefined && patterns?.has(patternKey)
126
- ? toPatternId(String(patternKey))
127
- : null
128
-
129
- bars.push({
130
- data: point.data,
131
- key: `${String(xVal)}::${String(stackKey)}`,
132
- x: bandScale(xVal) ?? 0,
133
- y: yScale(y1),
134
- width: bandScale.bandwidth(),
135
- height: yScale(y0) - yScale(y1),
136
- fill: colorEntry.fill,
137
- stroke: colorEntry.stroke,
138
- patternId
139
- })
140
- }
141
- }
142
- return bars
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
143
154
  }
144
155
 
145
156
  export function buildHorizontalBars(data, channels, xScale, yScale, colors, _innerHeight) {
146
- const { x: xf, y: yf, color: cf } = channels
147
- const colorKeys = cf ? [...new Set(data.map((d) => d[cf]))] : []
148
- const subScale = colorKeys.length > 1
149
- ? scaleBand().domain(colorKeys).range([0, yScale.bandwidth()]).padding(0.05)
150
- : null
151
-
152
- return data.map((d) => {
153
- const yVal = d[yf]
154
- const colorKey = cf ? d[cf] : null
155
- const colorEntry = colors?.get(colorKey) ?? colors?.values().next().value ?? { fill: '#888', stroke: '#888' }
156
-
157
- const bandY = yScale(yVal) ?? 0
158
- const subY = subScale ? (subScale(colorKey) ?? 0) : 0
159
-
160
- return {
161
- data: d,
162
- key: `${String(yVal)}::${String(colorKey ?? '')}`,
163
- x: 0,
164
- y: bandY + subY,
165
- width: xScale(d[xf]),
166
- height: subScale ? subScale.bandwidth() : yScale.bandwidth(),
167
- fill: colorEntry.fill,
168
- stroke: colorEntry.stroke,
169
- patternId: null
170
- }
171
- })
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
+ })
172
223
  }
package/src/index.js CHANGED
@@ -8,8 +8,8 @@ import Area from './Plot/Area.svelte'
8
8
  import Point from './Plot/Point.svelte'
9
9
  import Arc from './Plot/Arc.svelte'
10
10
 
11
- // Export components (old Plot namespacerenamed to avoid collision with new PlotChart export)
12
- export const PlotLayers = {
11
+ // Composable Plot primitivesuse as <Plot.Root>, <Plot.Axis>, <Plot.Bar>, etc.
12
+ export const Plot = {
13
13
  Root,
14
14
  Axis,
15
15
  Bar,
@@ -21,20 +21,25 @@ export const PlotLayers = {
21
21
  Arc
22
22
  }
23
23
 
24
+ /** @deprecated Use Plot instead */
25
+ export const PlotLayers = Plot
26
+
24
27
  // New Plot system
25
- export { default as PlotChart } from './Plot.svelte'
28
+ export { default as PlotChart } from './Plot.svelte'
29
+ export { default as ChartProvider } from './ChartProvider.svelte'
30
+ export { createChartPreset, defaultPreset } from './lib/preset.js'
26
31
 
27
32
  // Facets and Animation
28
- export { default as FacetPlot } from './FacetPlot.svelte'
33
+ export { default as FacetPlot } from './FacetPlot.svelte'
29
34
  export { default as AnimatedPlot } from './AnimatedPlot.svelte'
30
35
 
31
36
  // Geom components (for declarative use inside PlotChart)
32
- export { default as GeomBar } from './geoms/Bar.svelte'
33
- export { default as GeomLine } from './geoms/Line.svelte'
34
- export { default as GeomArea } from './geoms/Area.svelte'
35
- export { default as GeomPoint } from './geoms/Point.svelte'
36
- export { default as GeomArc } from './geoms/Arc.svelte'
37
- export { default as GeomBox } from './geoms/Box.svelte'
37
+ export { default as GeomBar } from './geoms/Bar.svelte'
38
+ export { default as GeomLine } from './geoms/Line.svelte'
39
+ export { default as GeomArea } from './geoms/Area.svelte'
40
+ export { default as GeomPoint } from './geoms/Point.svelte'
41
+ export { default as GeomArc } from './geoms/Arc.svelte'
42
+ export { default as GeomBox } from './geoms/Box.svelte'
38
43
  export { default as GeomViolin } from './geoms/Violin.svelte'
39
44
 
40
45
  // Export standalone components
@@ -57,11 +62,13 @@ export { ChartBrewer } from './lib/brewing/index.svelte.js'
57
62
  export * from './lib/brewing/index.svelte.js'
58
63
  export { CartesianBrewer } from './lib/brewing/CartesianBrewer.svelte.js'
59
64
  export { PieBrewer } from './lib/brewing/PieBrewer.svelte.js'
65
+ export { QuartileBrewer } from './lib/brewing/QuartileBrewer.svelte.js'
60
66
  export { BoxBrewer } from './lib/brewing/BoxBrewer.svelte.js'
61
67
  export { ViolinBrewer } from './lib/brewing/ViolinBrewer.svelte.js'
62
68
 
63
69
  // CrossFilter system
64
- export { createCrossFilter } from './crossfilter/createCrossFilter.svelte.js'
65
- export { default as CrossFilter } from './crossfilter/CrossFilter.svelte'
66
- export { default as FilterBar } from './crossfilter/FilterBar.svelte'
67
- export { default as FilterSlider } from './crossfilter/FilterSlider.svelte'
70
+ export { createCrossFilter } from './crossfilter/createCrossFilter.svelte.js'
71
+ export { default as CrossFilter } from './crossfilter/CrossFilter.svelte'
72
+ export { default as FilterBar } from './crossfilter/FilterBar.svelte'
73
+ export { default as FilterSlider } from './crossfilter/FilterSlider.svelte'
74
+ export { default as FilterHistogram } from './crossfilter/FilterHistogram.svelte'
@@ -1,56 +1,14 @@
1
- import { quantile, ascending } from 'd3-array'
2
- import { dataset } from '@rokkit/data'
3
- import { ChartBrewer } from './brewer.svelte.js'
1
+ import { QuartileBrewer } from './QuartileBrewer.svelte.js'
4
2
  import { buildBoxes } from './marks/boxes.js'
5
- import { buildXScale, buildYScale } from './scales.js'
6
-
7
- function sortedQuantile(values, p) {
8
- return quantile([...values].sort(ascending), p)
9
- }
10
3
 
11
4
  /**
12
- * Brewer for box plots. Always computes quartile statistics regardless of the stat prop.
13
- * Groups by x + fill (primary) or x + color (fallback).
5
+ * Brewer for box plots.
14
6
  * fill = box interior color, color = whisker/outline stroke.
15
7
  */
16
- export class BoxBrewer extends ChartBrewer {
17
- transform(data, channels) {
18
- if (!channels.x || !channels.y) return data
19
- const by = [channels.x, channels.fill ?? channels.color].filter(Boolean)
20
- return dataset(data)
21
- .groupBy(...by)
22
- .summarize((row) => row[channels.y], {
23
- q1: (v) => sortedQuantile(v, 0.25),
24
- median: (v) => sortedQuantile(v, 0.5),
25
- q3: (v) => sortedQuantile(v, 0.75),
26
- iqr_min: (v) => { const q1 = sortedQuantile(v, 0.25); const q3 = sortedQuantile(v, 0.75); return q1 - 1.5 * (q3 - q1) },
27
- iqr_max: (v) => { const q1 = sortedQuantile(v, 0.25); const q3 = sortedQuantile(v, 0.75); return q3 + 1.5 * (q3 - q1) }
28
- })
29
- .rollup()
30
- .select()
31
- }
32
-
33
- /**
34
- * Override xScale to use processedData (post-transform rows have the x field).
35
- */
36
- xScale = $derived(
37
- this.channels.x && this.processedData.length > 0
38
- ? buildXScale(this.processedData, this.channels.x, this.innerWidth)
39
- : null
40
- )
41
-
42
- /**
43
- * Override yScale to use iqr_max as the upper bound of the quartile data.
44
- */
45
- yScale = $derived(
46
- this.channels.y && this.processedData.length > 0
47
- ? buildYScale(this.processedData, 'iqr_max', this.innerHeight)
48
- : null
49
- )
50
-
51
- boxes = $derived(
52
- this.xScale && this.yScale
53
- ? buildBoxes(this.processedData, this.channels, this.xScale, this.yScale, this.colorMap)
54
- : []
55
- )
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
+ )
56
14
  }
@@ -7,11 +7,15 @@ import { applyAggregate } from './stats.js'
7
7
  * Groups by x (and fill/color if set) and applies the given stat.
8
8
  */
9
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 = [...new SvelteSet([channels.x, channels.fill, channels.color, channels.pattern].filter(Boolean))]
15
- return applyAggregate(data, { by, value: channels.y, stat })
16
- }
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
+ }
17
21
  }
@@ -6,9 +6,9 @@ import { applyAggregate } from './stats.js'
6
6
  * 'identity' is not meaningful for pie charts — falls back to 'sum'.
7
7
  */
8
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
- }
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
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
+ }
@@ -1,55 +1,14 @@
1
- import { quantile, ascending } from 'd3-array'
2
- import { dataset } from '@rokkit/data'
3
- import { ChartBrewer } from './brewer.svelte.js'
1
+ import { QuartileBrewer } from './QuartileBrewer.svelte.js'
4
2
  import { buildViolins } from './marks/violins.js'
5
- import { buildXScale, buildYScale } from './scales.js'
6
-
7
- function sortedQuantile(values, p) {
8
- return quantile([...values].sort(ascending), p)
9
- }
10
3
 
11
4
  /**
12
- * Brewer for violin plots. Computes the same quartile statistics as BoxBrewer.
5
+ * Brewer for violin plots.
13
6
  * fill = violin body color, color = outline stroke.
14
7
  */
15
- export class ViolinBrewer 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) => { const q1 = sortedQuantile(v, 0.25); const q3 = sortedQuantile(v, 0.75); return q1 - 1.5 * (q3 - q1) },
26
- iqr_max: (v) => { const q1 = sortedQuantile(v, 0.25); const q3 = sortedQuantile(v, 0.75); return q3 + 1.5 * (q3 - q1) }
27
- })
28
- .rollup()
29
- .select()
30
- }
31
-
32
- /**
33
- * Override xScale to use processedData (post-transform rows have the x field).
34
- */
35
- xScale = $derived(
36
- this.channels.x && this.processedData.length > 0
37
- ? buildXScale(this.processedData, this.channels.x, this.innerWidth)
38
- : null
39
- )
40
-
41
- /**
42
- * Override yScale to use iqr_max as the upper bound of the quartile data.
43
- */
44
- yScale = $derived(
45
- this.channels.y && this.processedData.length > 0
46
- ? buildYScale(this.processedData, 'iqr_max', this.innerHeight)
47
- : null
48
- )
49
-
50
- violins = $derived(
51
- this.xScale && this.yScale
52
- ? buildViolins(this.processedData, this.channels, this.xScale, this.yScale, this.colorMap)
53
- : []
54
- )
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
+ )
55
14
  }