@rokkit/chart 1.3.6 → 1.3.8

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 (93) hide show
  1. package/dist/AnimatedPlot.svelte +26 -8
  2. package/dist/AnimatedPlot.svelte.d.ts +5 -0
  3. package/dist/Chart.svelte +1 -1
  4. package/dist/FacetPlot.svelte +1 -1
  5. package/dist/Plot/Axis.svelte +21 -10
  6. package/dist/Plot/Grid.svelte +32 -13
  7. package/dist/Plot/Grid.svelte.d.ts +6 -1
  8. package/dist/Plot/Root.svelte +64 -46
  9. package/dist/Plot/Root.svelte.d.ts +27 -2
  10. package/dist/Plot/Timeline.svelte +39 -13
  11. package/dist/Plot/index.d.ts +4 -4
  12. package/dist/Plot/index.js +4 -4
  13. package/dist/Plot.svelte +128 -164
  14. package/dist/Plot.svelte.d.ts +18 -2
  15. package/dist/PlotState.svelte.d.ts +23 -0
  16. package/dist/PlotState.svelte.js +155 -16
  17. package/dist/PlotSurface.svelte +162 -0
  18. package/dist/PlotSurface.svelte.d.ts +25 -0
  19. package/dist/charts/AreaChart.svelte +37 -4
  20. package/dist/charts/AreaChart.svelte.d.ts +12 -2
  21. package/dist/charts/BarChart.svelte +32 -3
  22. package/dist/charts/BarChart.svelte.d.ts +10 -1
  23. package/dist/charts/BoxPlot.svelte +4 -2
  24. package/dist/charts/BoxPlot.svelte.d.ts +1 -0
  25. package/dist/charts/BubbleChart.svelte +34 -3
  26. package/dist/charts/BubbleChart.svelte.d.ts +11 -1
  27. package/dist/charts/LineChart.svelte +35 -4
  28. package/dist/charts/LineChart.svelte.d.ts +11 -2
  29. package/dist/charts/PieChart.svelte +1 -1
  30. package/dist/charts/ScatterPlot.svelte +32 -3
  31. package/dist/charts/ScatterPlot.svelte.d.ts +10 -1
  32. package/dist/charts/ViolinPlot.svelte +4 -2
  33. package/dist/charts/ViolinPlot.svelte.d.ts +1 -0
  34. package/dist/examples/BarChartExample.svelte +1 -1
  35. package/dist/geoms/Arc.svelte +3 -2
  36. package/dist/geoms/Area.svelte +20 -3
  37. package/dist/geoms/Bar.svelte +57 -13
  38. package/dist/geoms/Box.svelte +131 -76
  39. package/dist/geoms/Box.svelte.d.ts +7 -0
  40. package/dist/geoms/Candlestick.svelte +17 -11
  41. package/dist/geoms/Highlight.svelte +105 -0
  42. package/dist/geoms/Highlight.svelte.d.ts +10 -0
  43. package/dist/geoms/Jitter.svelte +82 -0
  44. package/dist/geoms/Jitter.svelte.d.ts +15 -0
  45. package/dist/geoms/Line.svelte +26 -9
  46. package/dist/geoms/Point.svelte +37 -11
  47. package/dist/geoms/Rule.svelte +113 -0
  48. package/dist/geoms/Rule.svelte.d.ts +16 -0
  49. package/dist/geoms/Trend.svelte +66 -0
  50. package/dist/geoms/Trend.svelte.d.ts +12 -0
  51. package/dist/geoms/Violin.svelte +30 -6
  52. package/dist/geoms/Violin.svelte.d.ts +5 -0
  53. package/dist/geoms/Waterfall.svelte +16 -9
  54. package/dist/geoms/lib/areas.d.ts +6 -2
  55. package/dist/geoms/lib/areas.js +17 -11
  56. package/dist/geoms/lib/bars.d.ts +18 -3
  57. package/dist/geoms/lib/bars.js +70 -96
  58. package/dist/index.d.ts +30 -4
  59. package/dist/index.js +31 -5
  60. package/dist/lib/brewing/ViolinBrewer.svelte.d.ts +1 -1
  61. package/dist/lib/brewing/brewer.svelte.d.ts +1 -0
  62. package/dist/lib/brewing/marks/arcs.js +7 -1
  63. package/dist/lib/brewing/marks/boxes.d.ts +11 -2
  64. package/dist/lib/brewing/marks/boxes.js +32 -11
  65. package/dist/lib/brewing/marks/lines.d.ts +6 -2
  66. package/dist/lib/brewing/marks/lines.js +19 -12
  67. package/dist/lib/brewing/marks/swarm.d.ts +32 -0
  68. package/dist/lib/brewing/marks/swarm.js +125 -0
  69. package/dist/lib/brewing/marks/violins.d.ts +4 -20
  70. package/dist/lib/brewing/marks/violins.js +41 -25
  71. package/dist/lib/brewing/stats.d.ts +3 -1
  72. package/dist/lib/brewing/stats.js +28 -11
  73. package/dist/lib/highlight.d.ts +10 -0
  74. package/dist/lib/highlight.js +43 -0
  75. package/dist/lib/plot/scales.js +12 -7
  76. package/dist/lib/plot/types.d.ts +9 -1
  77. package/dist/lib/plot/types.js +3 -1
  78. package/dist/lib/scale.d.ts +10 -0
  79. package/dist/lib/scale.js +14 -0
  80. package/dist/lib/select.d.ts +22 -0
  81. package/dist/lib/select.js +22 -0
  82. package/dist/lib/trend.d.ts +27 -0
  83. package/dist/lib/trend.js +113 -0
  84. package/dist/patterns/README.md +1 -1
  85. package/package.json +1 -1
  86. package/dist/Plot/Area.svelte +0 -54
  87. package/dist/Plot/Area.svelte.d.ts +0 -14
  88. package/dist/Plot/Bar.svelte +0 -69
  89. package/dist/Plot/Bar.svelte.d.ts +0 -11
  90. package/dist/Plot/Line.svelte +0 -54
  91. package/dist/Plot/Line.svelte.d.ts +0 -13
  92. package/dist/Plot/Point.svelte +0 -58
  93. package/dist/Plot/Point.svelte.d.ts +0 -13
@@ -33,27 +33,63 @@ function subBandFields(channels) {
33
33
  return out
34
34
  }
35
35
 
36
- export function buildGroupedBars(data, channels, xScale, yScale, colors, innerHeight, patterns) {
36
+ /**
37
+ * The one bar builder. Expresses each bar in abstract (category, value) space and maps it to
38
+ * screen via `place()`, so it renders vertical or horizontal (flip) with the same code.
39
+ *
40
+ * Category (x) positioning has two modes:
41
+ * - band (default): a scaleBand slots each category; sub-bands split grouped series.
42
+ * - continuous (`continuousCategory`): a LINEAR position scale (e.g. a bar-chart race's tweened
43
+ * `_rank`). Each bar centres on `xScale(pos)` with a fixed thickness from the scale step, so a
44
+ * fractional position tweens smoothly (a band scale can only jump between slots). No sub-banding.
45
+ */
46
+ export function buildBars(
47
+ data,
48
+ channels,
49
+ xScale,
50
+ yScale,
51
+ colors,
52
+ innerHeight,
53
+ patterns,
54
+ place = (x, y) => ({ x, y }),
55
+ continuousCategory = false
56
+ ) {
37
57
  const { x: xf, y: yf, color: cf, pattern: pf } = channels
38
58
 
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)
59
+ const subFields = continuousCategory ? [] : subBandFields(channels)
43
60
  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.
61
+
62
+ let positionOf
63
+ let thicknessOf
64
+ if (continuousCategory) {
65
+ const [r0, r1] = xScale.range()
66
+ const [d0, d1] = xScale.domain()
67
+ const step = Math.max(Math.abs(r0 - r1) / Math.max(d1 - d0, 1), 1)
68
+ const thickness = step * 0.9
69
+ thicknessOf = () => thickness
70
+ positionOf = (d) => (xScale(Number(d[xf])) ?? 0) - thickness / 2
71
+ } else {
72
+ const bandScale = ensureBandX(xScale, data, xf)
73
+ const subDomain = subFields.length > 0 ? [...new Set(data.map(getSubKey))] : []
74
+ const subScale =
75
+ subDomain.length > 1
76
+ ? scaleBand().domain(subDomain).range([0, bandScale.bandwidth()]).padding(0.05)
77
+ : null
78
+ thicknessOf = () => (subScale ? subScale.bandwidth() : bandScale.bandwidth())
79
+ positionOf = (d) => {
80
+ const bandX = bandScale(d[xf]) ?? 0
81
+ const subX = subScale ? (subScale(getSubKey(d)) ?? 0) : 0
82
+ return bandX + subX
83
+ }
84
+ }
85
+
86
+ // Value baseline: at value=0 when the domain spans zero (supports negative bars), else the
87
+ // value-axis origin (yScale.range()[0] — innerHeight when vertical, 0 when flipped).
52
88
  const yDomain = typeof yScale.bandwidth !== 'function' ? yScale.domain?.() : null
53
89
  const baseline =
54
90
  yDomain && yDomain[0] <= 0 && yDomain[yDomain.length - 1] >= 0
55
- ? (yScale(0) ?? innerHeight)
56
- : innerHeight
91
+ ? (yScale(0) ?? yScale.range()[0])
92
+ : yScale.range()[0]
57
93
 
58
94
  return data.map((d, i) => {
59
95
  const xVal = d[xf]
@@ -68,19 +104,26 @@ export function buildGroupedBars(data, channels, xScale, yScale, colors, innerHe
68
104
  ? toPatternId(String(patternKey))
69
105
  : null
70
106
 
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
107
+ const barBand = positionOf(d)
108
+ const barThickness = thicknessOf(d)
109
+ const barValue = yScale(d[yf]) ?? baseline
110
+ // Two opposite corners in (band, value) space, placed to screen — transposes under flip.
111
+ const c1 = place(barBand, baseline)
112
+ const c2 = place(barBand + barThickness, barValue)
113
+
114
+ // Continuous mode keys by _entity so Svelte reuses each bar as its position tweens (a
115
+ // race's rows reorder every frame); band mode keys by category + sub-band + index.
116
+ const key = continuousCategory
117
+ ? `${String(d._entity ?? xVal)}::${String(colorKey ?? '')}`
118
+ : `${String(xVal)}::${subKey}::${i}`
76
119
 
77
120
  return {
78
121
  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),
122
+ key,
123
+ x: Math.min(c1.x, c2.x),
124
+ y: Math.min(c1.y, c2.y),
125
+ width: Math.abs(c2.x - c1.x),
126
+ height: Math.abs(c2.y - c1.y),
84
127
  fill: colorEntry.fill,
85
128
  stroke: colorEntry.stroke,
86
129
  patternId
@@ -88,7 +131,7 @@ export function buildGroupedBars(data, channels, xScale, yScale, colors, innerHe
88
131
  })
89
132
  }
90
133
 
91
- export function buildStackedBars(data, channels, xScale, yScale, colors, innerHeight, patterns) {
134
+ export function buildStackedBars(data, channels, xScale, yScale, colors, innerHeight, patterns, place = (x, y) => ({ x, y })) {
92
135
  const { x: xf, y: yf, color: cf, pattern: pf } = channels
93
136
 
94
137
  const bandScale = ensureBandX(xScale, data, xf)
@@ -96,7 +139,7 @@ export function buildStackedBars(data, channels, xScale, yScale, colors, innerHe
96
139
  // Stack dimension: first non-x grouping field (prefer pattern, then color)
97
140
  const subFields = subBandFields(channels)
98
141
  if (subFields.length === 0) {
99
- return buildGroupedBars(data, channels, xScale, yScale, colors, innerHeight, patterns)
142
+ return buildBars(data, channels, xScale, yScale, colors, innerHeight, patterns, place)
100
143
  }
101
144
  const stackField = subFields[0]
102
145
 
@@ -152,72 +195,3 @@ export function buildStackedBars(data, channels, xScale, yScale, colors, innerHe
152
195
  }
153
196
  return bars
154
197
  }
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/dist/index.d.ts CHANGED
@@ -8,6 +8,17 @@ export namespace Plot {
8
8
  export { Area };
9
9
  export { Point };
10
10
  export { Arc };
11
+ export { Highlight };
12
+ export { Trend };
13
+ export { Box };
14
+ export { Violin };
15
+ export { Jitter };
16
+ export { Candlestick };
17
+ export { Heatmap };
18
+ export { Hexbin };
19
+ export { Ribbon };
20
+ export { Waterfall };
21
+ export { Rule };
11
22
  }
12
23
  export namespace PlotLayers { }
13
24
  export { default as PlotChart } from "./Plot.svelte";
@@ -28,6 +39,10 @@ export { default as GeomCandlestick } from "./geoms/Candlestick.svelte";
28
39
  export { default as GeomWaterfall } from "./geoms/Waterfall.svelte";
29
40
  export { default as GeomHexbin } from "./geoms/Hexbin.svelte";
30
41
  export { default as GeomRibbon } from "./geoms/Ribbon.svelte";
42
+ export { default as GeomHighlight } from "./geoms/Highlight.svelte";
43
+ export { default as GeomTrend } from "./geoms/Trend.svelte";
44
+ export { default as GeomJitter } from "./geoms/Jitter.svelte";
45
+ export { default as GeomRule } from "./geoms/Rule.svelte";
31
46
  export { default as Chart } from "./Chart.svelte";
32
47
  export { default as Sparkline } from "./Sparkline.svelte";
33
48
  export { default as BarChart } from "./charts/BarChart.svelte";
@@ -53,12 +68,23 @@ export { default as FilterSlider } from "./crossfilter/FilterSlider.svelte";
53
68
  export { default as FilterHistogram } from "./crossfilter/FilterHistogram.svelte";
54
69
  import Root from './Plot/Root.svelte';
55
70
  import Axis from './Plot/Axis.svelte';
56
- import Bar from './Plot/Bar.svelte';
71
+ import Bar from './geoms/Bar.svelte';
57
72
  import Grid from './Plot/Grid.svelte';
58
73
  import Legend from './Plot/Legend.svelte';
59
- import Line from './Plot/Line.svelte';
60
- import Area from './Plot/Area.svelte';
61
- import Point from './Plot/Point.svelte';
74
+ import Line from './geoms/Line.svelte';
75
+ import Area from './geoms/Area.svelte';
76
+ import Point from './geoms/Point.svelte';
62
77
  import Arc from './Plot/Arc.svelte';
78
+ import Highlight from './geoms/Highlight.svelte';
79
+ import Trend from './geoms/Trend.svelte';
80
+ import Box from './geoms/Box.svelte';
81
+ import Violin from './geoms/Violin.svelte';
82
+ import Jitter from './geoms/Jitter.svelte';
83
+ import Candlestick from './geoms/Candlestick.svelte';
84
+ import Heatmap from './geoms/Heatmap.svelte';
85
+ import Hexbin from './geoms/Hexbin.svelte';
86
+ import Ribbon from './geoms/Ribbon.svelte';
87
+ import Waterfall from './geoms/Waterfall.svelte';
88
+ import Rule from './geoms/Rule.svelte';
63
89
  export { createChartPreset, defaultPreset } from "./lib/preset.js";
64
90
  export { buildSequentialScale, buildDivergingScale } from "./lib/brewing/colors.js";
package/dist/index.js CHANGED
@@ -1,12 +1,23 @@
1
1
  import Root from './Plot/Root.svelte'
2
2
  import Axis from './Plot/Axis.svelte'
3
- import Bar from './Plot/Bar.svelte'
3
+ import Bar from './geoms/Bar.svelte'
4
4
  import Grid from './Plot/Grid.svelte'
5
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'
6
+ import Line from './geoms/Line.svelte'
7
+ import Area from './geoms/Area.svelte'
8
+ import Point from './geoms/Point.svelte'
9
9
  import Arc from './Plot/Arc.svelte'
10
+ import Highlight from './geoms/Highlight.svelte'
11
+ import Trend from './geoms/Trend.svelte'
12
+ import Box from './geoms/Box.svelte'
13
+ import Violin from './geoms/Violin.svelte'
14
+ import Jitter from './geoms/Jitter.svelte'
15
+ import Candlestick from './geoms/Candlestick.svelte'
16
+ import Heatmap from './geoms/Heatmap.svelte'
17
+ import Hexbin from './geoms/Hexbin.svelte'
18
+ import Ribbon from './geoms/Ribbon.svelte'
19
+ import Waterfall from './geoms/Waterfall.svelte'
20
+ import Rule from './geoms/Rule.svelte'
10
21
 
11
22
  // Composable Plot primitives — use as <Plot.Root>, <Plot.Axis>, <Plot.Bar>, etc.
12
23
  export const Plot = {
@@ -18,7 +29,18 @@ export const Plot = {
18
29
  Line,
19
30
  Area,
20
31
  Point,
21
- Arc
32
+ Arc,
33
+ Highlight,
34
+ Trend,
35
+ Box,
36
+ Violin,
37
+ Jitter,
38
+ Candlestick,
39
+ Heatmap,
40
+ Hexbin,
41
+ Ribbon,
42
+ Waterfall,
43
+ Rule
22
44
  }
23
45
 
24
46
  /** @deprecated Use Plot instead */
@@ -48,6 +70,10 @@ export { default as GeomCandlestick } from './geoms/Candlestick.svelte'
48
70
  export { default as GeomWaterfall } from './geoms/Waterfall.svelte'
49
71
  export { default as GeomHexbin } from './geoms/Hexbin.svelte'
50
72
  export { default as GeomRibbon } from './geoms/Ribbon.svelte'
73
+ export { default as GeomHighlight } from './geoms/Highlight.svelte'
74
+ export { default as GeomTrend } from './geoms/Trend.svelte'
75
+ export { default as GeomJitter } from './geoms/Jitter.svelte'
76
+ export { default as GeomRule } from './geoms/Rule.svelte'
51
77
 
52
78
  // Export standalone components
53
79
  export { default as Chart } from './Chart.svelte'
@@ -3,6 +3,6 @@
3
3
  * fill = violin body color, color = outline stroke.
4
4
  */
5
5
  export class ViolinBrewer extends QuartileBrewer {
6
- violins: any[];
6
+ violins: any;
7
7
  }
8
8
  import { QuartileBrewer } from './QuartileBrewer.svelte.js';
@@ -51,6 +51,7 @@ export class ChartBrewer {
51
51
  x: number;
52
52
  y: number;
53
53
  data: Record<string, unknown>;
54
+ i: number;
54
55
  }[];
55
56
  key?: unknown;
56
57
  }[];
@@ -15,7 +15,13 @@ import { toPatternId } from '../../brewing/patterns.js'
15
15
  export function buildArcs(data, channels, colors, width, height, opts = {}, patterns) {
16
16
  const { color: lf, y: yf } = channels
17
17
  const radius = Math.min(width, height) / 2
18
- const innerRadius = opts.innerRadius ?? 0
18
+ // innerRadius: a value <= 1 is a fraction of the radius (responsive donut);
19
+ // a value > 1 is absolute pixels. Clamp below the outer radius so a bad value
20
+ // (e.g. a fraction/pixel mixup) can never invert the arc into a giant ring
21
+ // rendered outside the circle.
22
+ const rawInner = opts.innerRadius ?? 0
23
+ const innerPx = rawInner <= 1 ? rawInner * radius : rawInner
24
+ const innerRadius = Math.max(0, Math.min(innerPx, radius * 0.98))
19
25
  const pieGen = pie().value((d) => Number(d[yf]))
20
26
  const arcGen = arc().innerRadius(innerRadius).outerRadius(radius)
21
27
  const slices = pieGen(data)
@@ -8,17 +8,26 @@
8
8
  * Box body uses the lighter fill shade; whiskers and median use the darker stroke shade.
9
9
  *
10
10
  * @param {Object[]} data - Pre-aggregated rows with quartile fields
11
- * @param {{ x?: string, fill?: string }} channels
11
+ * @param {{ x?: string, fill?: string, pattern?: string }} channels
12
12
  * `fill` drives the box and whisker color (defaults to x-field).
13
13
  * @param {import('d3-scale').ScaleBand} xScale
14
14
  * @param {import('d3-scale').ScaleLinear} yScale
15
15
  * @param {Map<unknown, {fill:string, stroke:string}>} colors
16
+ * @param {{ side?: 'left'|'right'|'center', width?: number, patterns?: Map<unknown, string> }} [opts]
17
+ * `side` offsets the box to one half of the band; `width` is the box thickness as a
18
+ * fraction of the (sub-)band (for a thin raincloud box); `patterns` maps a pattern-channel
19
+ * value to a pattern name for texture fills. Defaults preserve the full box.
16
20
  * @returns {Array}
17
21
  */
18
22
  export function buildBoxes(data: Object[], channels: {
19
23
  x?: string;
20
24
  fill?: string;
25
+ pattern?: string;
21
26
  }, xScale: import("d3-scale").ScaleBand<any>, yScale: import("d3-scale").ScaleLinear<any, any, never>, colors: Map<unknown, {
22
27
  fill: string;
23
28
  stroke: string;
24
- }>): any[];
29
+ }>, opts?: {
30
+ side?: "left" | "right" | "center";
31
+ width?: number;
32
+ patterns?: Map<unknown, string>;
33
+ }): any[];
@@ -1,3 +1,5 @@
1
+ import { toPatternId } from '../patterns.js'
2
+
1
3
  /**
2
4
  * Builds box geometry for box plot charts.
3
5
  * Input data rows must already contain { q1, median, q3, iqr_min, iqr_max } —
@@ -8,30 +10,42 @@
8
10
  * Box body uses the lighter fill shade; whiskers and median use the darker stroke shade.
9
11
  *
10
12
  * @param {Object[]} data - Pre-aggregated rows with quartile fields
11
- * @param {{ x?: string, fill?: string }} channels
13
+ * @param {{ x?: string, fill?: string, pattern?: string }} channels
12
14
  * `fill` drives the box and whisker color (defaults to x-field).
13
15
  * @param {import('d3-scale').ScaleBand} xScale
14
16
  * @param {import('d3-scale').ScaleLinear} yScale
15
17
  * @param {Map<unknown, {fill:string, stroke:string}>} colors
18
+ * @param {{ side?: 'left'|'right'|'center', width?: number, patterns?: Map<unknown, string> }} [opts]
19
+ * `side` offsets the box to one half of the band; `width` is the box thickness as a
20
+ * fraction of the (sub-)band (for a thin raincloud box); `patterns` maps a pattern-channel
21
+ * value to a pattern name for texture fills. Defaults preserve the full box.
16
22
  * @returns {Array}
17
23
  */
18
- export function buildBoxes(data, channels, xScale, yScale, colors) {
19
- const { x: xf, fill: ff } = channels
24
+ export function buildBoxes(data, channels, xScale, yScale, colors, opts = {}) {
25
+ const { x: xf, fill: ff, pattern: pf } = channels
26
+ const { side = 'center', width, patterns } = opts
20
27
  const bw = typeof xScale.bandwidth === 'function' ? xScale.bandwidth() : 20
21
28
  const grouped = ff && ff !== xf
29
+ // Offset the box centre to the middle of the chosen half-lane.
30
+ const laneOffset = (unit) => (side === 'left' ? -unit / 4 : side === 'right' ? unit / 4 : 0)
31
+ // Texture fill: patternId per box from the pattern channel value (null = solid fill).
32
+ const patternIdFor = (d) => {
33
+ const key = pf ? d[pf] : null
34
+ return key !== null && key !== undefined && patterns?.has(key) ? toPatternId(String(key)) : null
35
+ }
22
36
 
23
37
  if (grouped) {
24
38
  const fillValues = [...new Set(data.map((d) => d[ff]))]
25
39
  const n = fillValues.length
26
40
  const subBandWidth = bw / n
27
- const boxWidth = subBandWidth * 0.75
28
- const whiskerWidth = subBandWidth * 0.4
41
+ const boxWidth = width !== undefined ? subBandWidth * width : subBandWidth * 0.75
42
+ const whiskerWidth = width !== undefined ? boxWidth * 0.53 : subBandWidth * 0.4
29
43
 
30
44
  return data.map((d) => {
31
45
  const fillVal = d[ff]
32
46
  const subIndex = fillValues.indexOf(fillVal)
33
47
  const bandStart = xScale(d[xf]) ?? 0
34
- const cx = bandStart + subIndex * subBandWidth + subBandWidth / 2
48
+ const cx = bandStart + subIndex * subBandWidth + subBandWidth / 2 + laneOffset(subBandWidth)
35
49
  const colorEntry = colors?.get(fillVal) ?? { fill: '#aaa', stroke: '#666' }
36
50
 
37
51
  return {
@@ -42,22 +56,27 @@ export function buildBoxes(data, channels, xScale, yScale, colors) {
42
56
  q3: yScale(d.q3),
43
57
  iqr_min: yScale(d.iqr_min),
44
58
  iqr_max: yScale(d.iqr_max),
59
+ outliers: (d.outliers ?? []).map((v) => ({ cy: yScale(v), value: v })),
45
60
  width: boxWidth,
46
61
  whiskerWidth,
47
62
  fill: colorEntry.fill,
48
- stroke: colorEntry.stroke
63
+ stroke: colorEntry.stroke,
64
+ patternId: patternIdFor(d)
49
65
  }
50
66
  })
51
67
  }
52
68
 
53
69
  // Non-grouped: one box per x category
54
- const boxWidth = bw * 0.6
55
- const whiskerWidth = bw * 0.3
70
+ const boxWidth = width !== undefined ? bw * width : bw * 0.6
71
+ const whiskerWidth = width !== undefined ? boxWidth * 0.5 : bw * 0.3
56
72
 
57
73
  return data.map((d) => {
58
74
  const fillKey = ff ? d[ff] : d[xf]
59
75
  const colorEntry = colors?.get(fillKey) ?? { fill: '#aaa', stroke: '#666' }
60
- const cx = (xScale(d[xf]) ?? 0) + (typeof xScale.bandwidth === 'function' ? bw / 2 : 0)
76
+ const cx =
77
+ (xScale(d[xf]) ?? 0) +
78
+ (typeof xScale.bandwidth === 'function' ? bw / 2 : 0) +
79
+ laneOffset(bw)
61
80
  return {
62
81
  data: d,
63
82
  cx,
@@ -66,10 +85,12 @@ export function buildBoxes(data, channels, xScale, yScale, colors) {
66
85
  q3: yScale(d.q3),
67
86
  iqr_min: yScale(d.iqr_min),
68
87
  iqr_max: yScale(d.iqr_max),
88
+ outliers: (d.outliers ?? []).map((v) => ({ cy: yScale(v), value: v })),
69
89
  width: boxWidth,
70
90
  whiskerWidth,
71
91
  fill: colorEntry.fill,
72
- stroke: colorEntry.stroke
92
+ stroke: colorEntry.stroke,
93
+ patternId: patternIdFor(d)
73
94
  }
74
95
  })
75
96
  }
@@ -5,13 +5,16 @@
5
5
  * @param {Function} yScale
6
6
  * @param {Map} colors
7
7
  * @param {'linear'|'smooth'|'step'} [curve]
8
- * @returns {{ d: string | null, fill: string, stroke: string, points: {x:number, y:number, data: Record<string, unknown>}[], key?: unknown }[]}
8
+ * @returns {{ d: string | null, fill: string, stroke: string, points: {x:number, y:number, data: Record<string, unknown>, i: number}[], key?: unknown }[]}
9
9
  */
10
10
  export function buildLines(data: Object[], channels: {
11
11
  x?: string;
12
12
  y?: string;
13
13
  color?: string;
14
- }, xScale: Function, yScale: Function, colors: Map<any, any>, curve?: "linear" | "smooth" | "step"): {
14
+ }, xScale: Function, yScale: Function, colors: Map<any, any>, curve?: "linear" | "smooth" | "step", place?: (x: any, y: any) => {
15
+ x: any;
16
+ y: any;
17
+ }): {
15
18
  d: string | null;
16
19
  fill: string;
17
20
  stroke: string;
@@ -19,6 +22,7 @@ export function buildLines(data: Object[], channels: {
19
22
  x: number;
20
23
  y: number;
21
24
  data: Record<string, unknown>;
25
+ i: number;
22
26
  }[];
23
27
  key?: unknown;
24
28
  }[];
@@ -7,38 +7,45 @@ import { line, curveCatmullRom, curveStep } from 'd3-shape'
7
7
  * @param {Function} yScale
8
8
  * @param {Map} colors
9
9
  * @param {'linear'|'smooth'|'step'} [curve]
10
- * @returns {{ d: string | null, fill: string, stroke: string, points: {x:number, y:number, data: Record<string, unknown>}[], key?: unknown }[]}
10
+ * @returns {{ d: string | null, fill: string, stroke: string, points: {x:number, y:number, data: Record<string, unknown>, i: number}[], key?: unknown }[]}
11
11
  */
12
- export function buildLines(data, channels, xScale, yScale, colors, curve) {
12
+ export function buildLines(data, channels, xScale, yScale, colors, curve, place = (x, y) => ({ x, y })) {
13
13
  const { x: xf, y: yf, color: cf } = channels
14
14
  const xPos = (d) =>
15
15
  typeof xScale.bandwidth === 'function' ? xScale(d[xf]) + xScale.bandwidth() / 2 : xScale(d[xf])
16
+ // Screen points, sorted by the x-channel position, each mapped through place() so the
17
+ // whole line transposes under orientation='horizontal' (identity default → unchanged).
18
+ const screenPoints = (rows) => {
19
+ const withIndices = rows.map((d, i) => ({ data: d, i }))
20
+ withIndices.sort((a, b) => xPos(a.data) - xPos(b.data))
21
+ return withIndices.map(({ data: d, i }) => {
22
+ const p = place(xPos(d), yScale(d[yf]))
23
+ return { x: p.x, y: p.y, data: d, i }
24
+ })
25
+ }
16
26
  const makeGen = () => {
17
27
  const gen = line()
18
- .x(xPos)
19
- .y((d) => yScale(d[yf]))
28
+ .x((p) => p.x)
29
+ .y((p) => p.y)
20
30
  if (curve === 'smooth') gen.curve(curveCatmullRom)
21
31
  else if (curve === 'step') gen.curve(curveStep)
22
32
  return gen
23
33
  }
24
- const toPoints = (rows) => rows.map((d) => ({ x: xPos(d), y: yScale(d[yf]), data: d }))
25
-
26
- const sortByX = (rows) => [...rows].sort((a, b) => xPos(a) - xPos(b))
27
34
 
28
35
  if (!cf) {
29
- const sorted = sortByX(data)
36
+ const pts = screenPoints(data)
30
37
  const stroke = colors?.values().next().value?.stroke ?? '#888'
31
- return [{ d: makeGen()(sorted), fill: 'none', stroke, points: toPoints(sorted) }]
38
+ return [{ d: makeGen()(pts), fill: 'none', stroke, points: pts }]
32
39
  }
33
40
  const groups = groupBy(data, cf)
34
41
  return [...groups.entries()].map(([key, rows]) => {
35
- const sorted = sortByX(rows)
42
+ const pts = screenPoints(rows)
36
43
  const colorEntry = colors?.get(key) ?? { fill: 'none', stroke: '#888' }
37
44
  return {
38
- d: makeGen()(sorted),
45
+ d: makeGen()(pts),
39
46
  fill: 'none',
40
47
  stroke: colorEntry.stroke,
41
- points: toPoints(sorted),
48
+ points: pts,
42
49
  key
43
50
  }
44
51
  })
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Places one point per datum within each x-band. Two layouts:
3
+ * - 'jitter': deterministic horizontal offset seeded by index-in-group.
4
+ * - 'swarm': 1-D beeswarm — sort by y, dodge horizontally to avoid overlap.
5
+ *
6
+ * @param {Object[]} data - raw rows (stat=identity)
7
+ * @param {{ x?: string, y?: string, fill?: string }} channels
8
+ * @param {import('d3-scale').ScaleBand} xScale
9
+ * @param {import('d3-scale').ScaleLinear} yScale
10
+ * @param {Map<unknown, {fill:string, stroke:string}>} colors
11
+ * @param {{ method?: 'jitter'|'swarm', r?: number, side?: 'left'|'right'|'center' }} opts
12
+ * `side` confines points to one half of the band (for raincloud/half-plots).
13
+ * @returns {Array<{ cx:number, cy:number, fill:string, stroke:string, data:Record<string, unknown> }>}
14
+ */
15
+ export function buildSwarm(data: Object[], channels: {
16
+ x?: string;
17
+ y?: string;
18
+ fill?: string;
19
+ }, xScale: import("d3-scale").ScaleBand<any>, yScale: import("d3-scale").ScaleLinear<any, any, never>, colors: Map<unknown, {
20
+ fill: string;
21
+ stroke: string;
22
+ }>, opts?: {
23
+ method?: "jitter" | "swarm";
24
+ r?: number;
25
+ side?: "left" | "right" | "center";
26
+ }): Array<{
27
+ cx: number;
28
+ cy: number;
29
+ fill: string;
30
+ stroke: string;
31
+ data: Record<string, unknown>;
32
+ }>;