@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.
Files changed (137) hide show
  1. package/README.md +150 -46
  2. package/package.json +40 -51
  3. package/src/AnimatedPlot.svelte +383 -0
  4. package/src/Chart.svelte +87 -59
  5. package/src/ChartExporter.svelte +125 -0
  6. package/src/ChartLegend.svelte +148 -0
  7. package/src/ChartProvider.svelte +10 -0
  8. package/src/FacetPlot/Panel.svelte +37 -0
  9. package/src/FacetPlot.svelte +114 -0
  10. package/src/Plot/Arc.svelte +29 -0
  11. package/src/Plot/Area.svelte +32 -0
  12. package/src/Plot/Axis.svelte +164 -0
  13. package/src/Plot/Bar.svelte +54 -0
  14. package/src/Plot/Grid.svelte +34 -0
  15. package/src/Plot/Legend.svelte +268 -0
  16. package/src/Plot/Line.svelte +37 -0
  17. package/src/Plot/Point.svelte +40 -0
  18. package/src/Plot/Root.svelte +62 -0
  19. package/src/Plot/Timeline.svelte +95 -0
  20. package/src/Plot/Tooltip.svelte +87 -0
  21. package/src/Plot/index.js +9 -0
  22. package/src/Plot.svelte +313 -0
  23. package/src/PlotState.svelte.js +375 -0
  24. package/src/Sparkline.svelte +108 -0
  25. package/src/Symbol.svelte +15 -11
  26. package/src/Texture.svelte +9 -7
  27. package/src/charts/AreaChart.svelte +30 -0
  28. package/src/charts/BarChart.svelte +31 -0
  29. package/src/charts/BoxPlot.svelte +24 -0
  30. package/src/charts/BubbleChart.svelte +28 -0
  31. package/src/charts/LineChart.svelte +31 -0
  32. package/src/charts/PieChart.svelte +35 -0
  33. package/src/charts/ScatterPlot.svelte +31 -0
  34. package/src/charts/ViolinPlot.svelte +24 -0
  35. package/src/crossfilter/CrossFilter.svelte +42 -0
  36. package/src/crossfilter/FilterBar.svelte +24 -0
  37. package/src/crossfilter/FilterHistogram.svelte +290 -0
  38. package/src/crossfilter/FilterSlider.svelte +83 -0
  39. package/src/crossfilter/createCrossFilter.svelte.js +124 -0
  40. package/src/elements/Bar.svelte +20 -16
  41. package/src/elements/ColorRamp.svelte +19 -19
  42. package/src/elements/ContinuousLegend.svelte +41 -19
  43. package/src/elements/DefinePatterns.svelte +9 -7
  44. package/src/elements/DiscreteLegend.svelte +13 -11
  45. package/src/elements/Label.svelte +4 -9
  46. package/src/elements/SymbolGrid.svelte +8 -9
  47. package/src/examples/BarChartExample.svelte +81 -0
  48. package/src/geoms/Arc.svelte +126 -0
  49. package/src/geoms/Area.svelte +78 -0
  50. package/src/geoms/Bar.svelte +200 -0
  51. package/src/geoms/Box.svelte +113 -0
  52. package/src/geoms/Candlestick.svelte +110 -0
  53. package/src/geoms/Heatmap.svelte +92 -0
  54. package/src/geoms/Hexbin.svelte +114 -0
  55. package/src/geoms/LabelPill.svelte +17 -0
  56. package/src/geoms/Line.svelte +123 -0
  57. package/src/geoms/Point.svelte +145 -0
  58. package/src/geoms/Ribbon.svelte +161 -0
  59. package/src/geoms/Violin.svelte +56 -0
  60. package/src/geoms/Waterfall.svelte +114 -0
  61. package/src/geoms/lib/areas.js +154 -0
  62. package/src/geoms/lib/bars.js +223 -0
  63. package/src/index.js +81 -2
  64. package/src/lib/brewing/BoxBrewer.svelte.js +14 -0
  65. package/src/lib/brewing/CartesianBrewer.svelte.js +21 -0
  66. package/src/lib/brewing/PieBrewer.svelte.js +14 -0
  67. package/src/lib/brewing/QuartileBrewer.svelte.js +51 -0
  68. package/src/lib/brewing/ViolinBrewer.svelte.js +14 -0
  69. package/src/lib/brewing/axes.svelte.js +270 -0
  70. package/src/lib/brewing/bars.svelte.js +201 -0
  71. package/src/lib/brewing/brewer.svelte.js +277 -0
  72. package/src/lib/brewing/colors.js +157 -0
  73. package/src/lib/brewing/dimensions.svelte.js +56 -0
  74. package/src/lib/brewing/index.svelte.js +205 -0
  75. package/src/lib/brewing/legends.svelte.js +137 -0
  76. package/src/lib/brewing/marks/arcs.js +43 -0
  77. package/src/lib/brewing/marks/areas.js +72 -0
  78. package/src/lib/brewing/marks/bars.js +49 -0
  79. package/src/lib/brewing/marks/boxes.js +75 -0
  80. package/src/lib/brewing/marks/lines.js +55 -0
  81. package/src/lib/brewing/marks/points.js +105 -0
  82. package/src/lib/brewing/marks/violins.js +90 -0
  83. package/src/lib/brewing/patterns.js +45 -0
  84. package/src/lib/brewing/scales.js +51 -0
  85. package/src/lib/brewing/scales.svelte.js +82 -0
  86. package/src/lib/brewing/stats.js +74 -0
  87. package/src/lib/brewing/symbols.js +10 -0
  88. package/src/lib/brewing/types.js +73 -0
  89. package/src/lib/chart.js +16 -8
  90. package/src/lib/context.js +131 -0
  91. package/src/lib/keyboard-nav.js +37 -0
  92. package/src/lib/plot/chartProps.js +76 -0
  93. package/src/lib/plot/crossfilter.js +16 -0
  94. package/src/lib/plot/facet.js +58 -0
  95. package/src/lib/plot/frames.js +81 -0
  96. package/src/lib/plot/helpers.js +14 -0
  97. package/src/lib/plot/preset.js +67 -0
  98. package/src/lib/plot/scales.js +81 -0
  99. package/src/lib/plot/stat.js +92 -0
  100. package/src/lib/plot/types.js +65 -0
  101. package/src/lib/preset.js +41 -0
  102. package/src/lib/scales.svelte.js +151 -0
  103. package/src/lib/swatch.js +4 -7
  104. package/src/lib/utils.js +142 -0
  105. package/src/lib/xscale.js +31 -0
  106. package/src/patterns/DefinePatterns.svelte +32 -0
  107. package/src/patterns/PatternDef.svelte +27 -0
  108. package/src/patterns/index.js +4 -14
  109. package/src/patterns/patterns.js +360 -0
  110. package/src/patterns/scale.js +116 -0
  111. package/src/spec/chart-spec.js +72 -0
  112. package/src/symbols/RoundedSquare.svelte +18 -12
  113. package/src/symbols/Shape.svelte +22 -16
  114. package/src/symbols/constants/index.js +1 -1
  115. package/LICENSE +0 -21
  116. package/src/lib/index.js +0 -4
  117. package/src/lib/plots.js +0 -27
  118. package/src/patterns/Brick.svelte +0 -17
  119. package/src/patterns/Circles.svelte +0 -18
  120. package/src/patterns/CrossHatch.svelte +0 -14
  121. package/src/patterns/CurvedWave.svelte +0 -9
  122. package/src/patterns/Dots.svelte +0 -19
  123. package/src/patterns/OutlineCircles.svelte +0 -15
  124. package/src/patterns/Tile.svelte +0 -17
  125. package/src/patterns/Triangles.svelte +0 -15
  126. package/src/patterns/Waves.svelte +0 -13
  127. package/src/patterns/paths/NamedPattern.svelte +0 -12
  128. package/src/patterns/paths/constants.js +0 -4
  129. package/src/plots/Plot.svelte +0 -40
  130. package/src/plots/index.js +0 -1
  131. package/src/template/Texture.svelte +0 -16
  132. package/src/template/constants.js +0 -43
  133. package/src/template/shapes/Circles.svelte +0 -16
  134. package/src/template/shapes/Lines.svelte +0 -17
  135. package/src/template/shapes/Path.svelte +0 -12
  136. package/src/template/shapes/Polygons.svelte +0 -18
  137. 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 Symbol } from './Symbol.svelte'
3
- export * from './plots'
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
+ }