@rokkit/chart 1.3.7 → 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 (76) 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 +19 -10
  6. package/dist/Plot/Root.svelte +64 -46
  7. package/dist/Plot/Root.svelte.d.ts +27 -2
  8. package/dist/Plot/Timeline.svelte +39 -13
  9. package/dist/Plot/index.d.ts +4 -4
  10. package/dist/Plot/index.js +4 -4
  11. package/dist/Plot.svelte +95 -178
  12. package/dist/Plot.svelte.d.ts +4 -0
  13. package/dist/PlotState.svelte.d.ts +16 -0
  14. package/dist/PlotState.svelte.js +118 -16
  15. package/dist/PlotSurface.svelte +162 -0
  16. package/dist/PlotSurface.svelte.d.ts +25 -0
  17. package/dist/charts/AreaChart.svelte +4 -1
  18. package/dist/charts/AreaChart.svelte.d.ts +1 -0
  19. package/dist/charts/BarChart.svelte +32 -3
  20. package/dist/charts/BarChart.svelte.d.ts +10 -1
  21. package/dist/charts/BoxPlot.svelte +4 -2
  22. package/dist/charts/BoxPlot.svelte.d.ts +1 -0
  23. package/dist/charts/BubbleChart.svelte +34 -3
  24. package/dist/charts/BubbleChart.svelte.d.ts +11 -1
  25. package/dist/charts/LineChart.svelte +1 -1
  26. package/dist/charts/PieChart.svelte +1 -1
  27. package/dist/charts/ScatterPlot.svelte +32 -3
  28. package/dist/charts/ScatterPlot.svelte.d.ts +10 -1
  29. package/dist/charts/ViolinPlot.svelte +4 -2
  30. package/dist/charts/ViolinPlot.svelte.d.ts +1 -0
  31. package/dist/examples/BarChartExample.svelte +1 -1
  32. package/dist/geoms/Arc.svelte +3 -2
  33. package/dist/geoms/Area.svelte +2 -2
  34. package/dist/geoms/Bar.svelte +17 -6
  35. package/dist/geoms/Box.svelte +131 -76
  36. package/dist/geoms/Box.svelte.d.ts +7 -0
  37. package/dist/geoms/Candlestick.svelte +17 -11
  38. package/dist/geoms/Jitter.svelte +82 -0
  39. package/dist/geoms/Jitter.svelte.d.ts +15 -0
  40. package/dist/geoms/Line.svelte +1 -1
  41. package/dist/geoms/Point.svelte +7 -1
  42. package/dist/geoms/Rule.svelte +113 -0
  43. package/dist/geoms/Rule.svelte.d.ts +16 -0
  44. package/dist/geoms/Violin.svelte +30 -6
  45. package/dist/geoms/Violin.svelte.d.ts +5 -0
  46. package/dist/geoms/Waterfall.svelte +16 -9
  47. package/dist/geoms/lib/areas.d.ts +6 -2
  48. package/dist/geoms/lib/areas.js +17 -11
  49. package/dist/geoms/lib/bars.d.ts +18 -3
  50. package/dist/geoms/lib/bars.js +70 -96
  51. package/dist/index.d.ts +24 -4
  52. package/dist/index.js +25 -5
  53. package/dist/lib/brewing/ViolinBrewer.svelte.d.ts +1 -1
  54. package/dist/lib/brewing/marks/arcs.js +7 -1
  55. package/dist/lib/brewing/marks/boxes.d.ts +11 -2
  56. package/dist/lib/brewing/marks/boxes.js +32 -11
  57. package/dist/lib/brewing/marks/lines.d.ts +4 -1
  58. package/dist/lib/brewing/marks/lines.js +18 -15
  59. package/dist/lib/brewing/marks/swarm.d.ts +32 -0
  60. package/dist/lib/brewing/marks/swarm.js +125 -0
  61. package/dist/lib/brewing/marks/violins.d.ts +4 -20
  62. package/dist/lib/brewing/marks/violins.js +41 -25
  63. package/dist/lib/brewing/stats.d.ts +3 -1
  64. package/dist/lib/brewing/stats.js +28 -11
  65. package/dist/lib/plot/scales.js +12 -7
  66. package/dist/lib/plot/types.d.ts +9 -1
  67. package/dist/lib/plot/types.js +3 -1
  68. package/package.json +1 -1
  69. package/dist/Plot/Area.svelte +0 -54
  70. package/dist/Plot/Area.svelte.d.ts +0 -14
  71. package/dist/Plot/Bar.svelte +0 -69
  72. package/dist/Plot/Bar.svelte.d.ts +0 -11
  73. package/dist/Plot/Line.svelte +0 -54
  74. package/dist/Plot/Line.svelte.d.ts +0 -13
  75. package/dist/Plot/Point.svelte +0 -58
  76. package/dist/Plot/Point.svelte.d.ts +0 -13
@@ -8,21 +8,30 @@
8
8
  y?: string
9
9
  fill?: string
10
10
  stat?: string
11
+ /** Half-violin: 'left'/'right' draw one side (flat edge at the band centre) for
12
+ * raincloud/split plots; 'center' (default) is the full symmetric silhouette. */
13
+ side?: 'left' | 'right' | 'center'
14
+ /** Data field driving a texture fill (from the shared pattern set) instead of a solid fill. */
15
+ pattern?: string
11
16
  options?: { opacity?: number }
12
17
  }
13
18
 
14
- let { x, y, fill, stat = 'boxplot', options = {} }: Props = $props()
19
+ let { x, y, fill, stat = 'boxplot', side = 'center', pattern, options = {} }: Props = $props()
15
20
 
16
21
  const plotState = getContext<PlotState>('plot-state')
17
22
  let id = $state<string | null>(null)
18
23
 
19
- // fill ?? x drives the colors map for both violin interior and outline
20
- const fillChannel = $derived(fill ?? x)
24
+ // Fall back to the root Plot channels when the geom omits x/y (composes under
25
+ // <Plot.Root x y> without silently NaN-ing).
26
+ const xf = $derived(x ?? plotState.channels?.x)
27
+ const yf = $derived(y ?? plotState.channels?.y)
28
+ // fill ?? effective-x drives the colors map for both violin interior and outline
29
+ const fillChannel = $derived(fill ?? xf)
21
30
 
22
31
  onMount(() => {
23
32
  id = plotState.registerGeom({
24
33
  type: 'violin',
25
- channels: { x, y, color: fillChannel },
34
+ channels: { x: xf, y: yf, color: fillChannel, pattern },
26
35
  stat,
27
36
  options
28
37
  })
@@ -32,17 +41,28 @@
32
41
  })
33
42
 
34
43
  $effect(() => {
35
- if (id) plotState.updateGeom(id, { channels: { x, y, color: fillChannel }, stat })
44
+ if (id)
45
+ plotState.updateGeom(id, { channels: { x: xf, y: yf, color: fillChannel, pattern }, stat })
36
46
  })
37
47
 
38
48
  const data = $derived(id ? plotState.geomData(id) : [])
39
49
  const xScale = $derived(plotState.xScale)
40
50
  const yScale = $derived(plotState.yScale)
41
51
  const colors = $derived(plotState.colors)
52
+ const patterns = $derived(plotState.patterns)
42
53
 
43
54
  const violins = $derived.by(() => {
44
55
  if (!data?.length || !xScale || !yScale) return []
45
- return buildViolins(data, { x, fill: fillChannel }, xScale, yScale, colors)
56
+ return buildViolins(
57
+ data,
58
+ { x: xf, fill: fillChannel, pattern },
59
+ xScale,
60
+ yScale,
61
+ colors,
62
+ plotState.place.bind(plotState),
63
+ side,
64
+ patterns
65
+ )
46
66
  })
47
67
  </script>
48
68
 
@@ -60,6 +80,10 @@
60
80
  onmouseenter={() => plotState.setHovered(v.data)}
61
81
  onmouseleave={() => plotState.clearHovered()}
62
82
  />
83
+ {#if v.patternId}
84
+ <!-- Texture fill overlay (matches the violin silhouette) -->
85
+ <path d={v.d} fill="url(#{v.patternId})" pointer-events="none" />
86
+ {/if}
63
87
  {/each}
64
88
  </g>
65
89
  {/if}
@@ -3,6 +3,11 @@ type Props = {
3
3
  y?: string;
4
4
  fill?: string;
5
5
  stat?: string;
6
+ /** Half-violin: 'left'/'right' draw one side (flat edge at the band centre) for
7
+ * raincloud/split plots; 'center' (default) is the full symmetric silhouette. */
8
+ side?: 'left' | 'right' | 'center';
9
+ /** Data field driving a texture fill (from the shared pattern set) instead of a solid fill. */
10
+ pattern?: string;
6
11
  options?: {
7
12
  opacity?: number;
8
13
  };
@@ -87,14 +87,19 @@
87
87
  fill = yVal >= 0 ? positiveColor : negativeColor
88
88
  }
89
89
 
90
+ // Place two (band, value) corners so the bar transposes under orientation flip.
91
+ const c1 = plotState.place(xPos, barTop)
92
+ const c2 = plotState.place(xPos + bw, barBottom)
90
93
  return {
91
94
  key: `${xVal}-${i}`,
92
- x: xPos,
93
- y: barTop,
94
- width: bw,
95
- height: Math.max(1, barBottom - barTop),
95
+ x: Math.min(c1.x, c2.x),
96
+ y: Math.min(c1.y, c2.y),
97
+ width: Math.abs(c2.x - c1.x),
98
+ height: Math.max(1, Math.abs(c2.y - c1.y)),
96
99
  fill,
97
- cumulative,
100
+ bandStart: xPos,
101
+ bandEnd: xPos + bw,
102
+ cumY: yScale(cumulative) ?? 0,
98
103
  data: d
99
104
  }
100
105
  })
@@ -119,11 +124,13 @@
119
124
  </rect>
120
125
  <!-- Connector line to next bar -->
121
126
  {#if i < bars.length - 1}
127
+ {@const cs = plotState.place(bar.bandEnd, bar.cumY)}
128
+ {@const ce = plotState.place(bars[i + 1].bandStart, bar.cumY)}
122
129
  <line
123
- x1={bar.x + bar.width}
124
- y1={yScale(bar.cumulative)}
125
- x2={bars[i + 1].x}
126
- y2={yScale(bar.cumulative)}
130
+ x1={cs.x}
131
+ y1={cs.y}
132
+ x2={ce.x}
133
+ y2={ce.y}
127
134
  stroke="currentColor"
128
135
  stroke-width={connectorWidth}
129
136
  stroke-dasharray="3 2"
@@ -8,6 +8,7 @@
8
8
  * @param {Map<unknown, {fill: string, stroke: string}>} colors
9
9
  * @param {'linear'|'smooth'|'step'} [curve]
10
10
  * @param {Map<unknown, string>} [patterns]
11
+ * @param {Function} [place] - orientation mapper (x, y) => {x, y}
11
12
  * @returns {{ d: string, fill: string, stroke: string, key: unknown, patternId: string|null }[]}
12
13
  */
13
14
  export function buildAreas(data: Object[], channels: {
@@ -18,7 +19,7 @@ export function buildAreas(data: Object[], channels: {
18
19
  }, xScale: Function, yScale: Function, colors: Map<unknown, {
19
20
  fill: string;
20
21
  stroke: string;
21
- }>, curve?: "linear" | "smooth" | "step", patterns?: Map<unknown, string>): {
22
+ }>, curve?: "linear" | "smooth" | "step", patterns?: Map<unknown, string>, place?: Function): {
22
23
  d: string;
23
24
  fill: string;
24
25
  stroke: string;
@@ -45,7 +46,10 @@ export function buildStackedAreas(data: Object[], channels: {
45
46
  }, xScale: Function, yScale: Function, colors: Map<unknown, {
46
47
  fill: string;
47
48
  stroke: string;
48
- }>, curve?: "linear" | "smooth" | "step", patterns?: Map<unknown, string>): {
49
+ }>, curve?: "linear" | "smooth" | "step", patterns?: Map<unknown, string>, place?: (x: any, y: any) => {
50
+ x: any;
51
+ y: any;
52
+ }): {
49
53
  d: string;
50
54
  fill: string;
51
55
  stroke: string;
@@ -11,20 +11,24 @@ import { toPatternId } from '../../lib/brewing/patterns.js'
11
11
  * @param {Map<unknown, {fill: string, stroke: string}>} colors
12
12
  * @param {'linear'|'smooth'|'step'} [curve]
13
13
  * @param {Map<unknown, string>} [patterns]
14
+ * @param {Function} [place] - orientation mapper (x, y) => {x, y}
14
15
  * @returns {{ d: string, fill: string, stroke: string, key: unknown, patternId: string|null }[]}
15
16
  */
16
- export function buildAreas(data, channels, xScale, yScale, colors, curve, patterns) {
17
+ export function buildAreas(data, channels, xScale, yScale, colors, curve, patterns, place = (x, y) => ({ x, y })) {
17
18
  const { x: xf, y: yf, color: cf, pattern: pf } = channels
18
19
  const baseline = yScale.range()[0] // bottom of the chart (y pixel max)
19
20
 
20
21
  const xPos = (d) =>
21
22
  typeof xScale.bandwidth === 'function' ? xScale(d[xf]) + xScale.bandwidth() / 2 : xScale(d[xf])
22
23
 
24
+ // Each area edge (baseline + top) is placed so the area transposes under orientation.
25
+ const toEdge = (d) => ({ base: place(xPos(d), baseline), top: place(xPos(d), yScale(d[yf])) })
23
26
  const makeGen = () => {
24
27
  const gen = area()
25
- .x(xPos)
26
- .y0(baseline)
27
- .y1((d) => yScale(d[yf]))
28
+ .x0((p) => p.base.x)
29
+ .y0((p) => p.base.y)
30
+ .x1((p) => p.top.x)
31
+ .y1((p) => p.top.y)
28
32
  if (curve === 'smooth') gen.curve(curveCatmullRom)
29
33
  else if (curve === 'step') gen.curve(curveStep)
30
34
  return gen
@@ -47,7 +51,7 @@ export function buildAreas(data, channels, xScale, yScale, colors, curve, patter
47
51
  patternKey !== null && patternKey !== undefined && patterns?.has(patternKey)
48
52
  ? toPatternId(String(patternKey))
49
53
  : null
50
- return [{ d: makeGen()(sortByX(data)), fill: entry.fill, stroke: 'none', key: null, patternId }]
54
+ return [{ d: makeGen()(sortByX(data).map(toEdge)), fill: entry.fill, stroke: 'none', key: null, patternId }]
51
55
  }
52
56
 
53
57
  // Group by color field
@@ -72,7 +76,7 @@ export function buildAreas(data, channels, xScale, yScale, colors, curve, patter
72
76
  patternKey !== null && patternKey !== undefined && patterns?.has(patternKey)
73
77
  ? toPatternId(String(patternKey))
74
78
  : null
75
- return { d: makeGen()(sortByX(rows)), fill: entry.fill, stroke: 'none', key, patternId }
79
+ return { d: makeGen()(sortByX(rows).map(toEdge)), fill: entry.fill, stroke: 'none', key, patternId }
76
80
  })
77
81
  }
78
82
 
@@ -88,7 +92,7 @@ export function buildAreas(data, channels, xScale, yScale, colors, curve, patter
88
92
  * @param {Map<unknown, string>} [patterns]
89
93
  * @returns {{ d: string, fill: string, stroke: string, key: unknown, patternId: string|null }[]}
90
94
  */
91
- export function buildStackedAreas(data, channels, xScale, yScale, colors, curve, patterns) {
95
+ export function buildStackedAreas(data, channels, xScale, yScale, colors, curve, patterns, place = (x, y) => ({ x, y })) {
92
96
  const { x: xf, y: yf, color: cf, pattern: pf } = channels
93
97
  if (!cf) return buildAreas(data, channels, xScale, yScale, colors, curve, patterns)
94
98
 
@@ -115,11 +119,13 @@ export function buildStackedAreas(data, channels, xScale, yScale, colors, curve,
115
119
  ? xScale(d.data[xf]) + xScale.bandwidth() / 2
116
120
  : xScale(d.data[xf])
117
121
 
122
+ const toEdge = (d) => ({ base: place(xPos(d), yScale(d[0])), top: place(xPos(d), yScale(d[1])) })
118
123
  const makeGen = () => {
119
124
  const gen = area()
120
- .x(xPos)
121
- .y0((d) => yScale(d[0]))
122
- .y1((d) => yScale(d[1]))
125
+ .x0((p) => p.base.x)
126
+ .y0((p) => p.base.y)
127
+ .x1((p) => p.top.x)
128
+ .y1((p) => p.top.y)
123
129
  if (curve === 'smooth') gen.curve(curveCatmullRom)
124
130
  else if (curve === 'step') gen.curve(curveStep)
125
131
  return gen
@@ -144,7 +150,7 @@ export function buildStackedAreas(data, channels, xScale, yScale, colors, curve,
144
150
  ? toPatternId(String(patternKey))
145
151
  : null
146
152
  return {
147
- d: makeGen()(layer) ?? '',
153
+ d: makeGen()(layer.map(toEdge)) ?? '',
148
154
  fill: entry.fill,
149
155
  stroke: 'none',
150
156
  key: colorKey,
@@ -1,3 +1,18 @@
1
- export function buildGroupedBars(data: any, channels: any, xScale: any, yScale: any, colors: any, innerHeight: any, patterns: any): any;
2
- export function buildStackedBars(data: any, channels: any, xScale: any, yScale: any, colors: any, innerHeight: any, patterns: any): any;
3
- export function buildHorizontalBars(data: any, channels: any, xScale: any, yScale: any, colors: any, _innerHeight: any): any;
1
+ /**
2
+ * The one bar builder. Expresses each bar in abstract (category, value) space and maps it to
3
+ * screen via `place()`, so it renders vertical or horizontal (flip) with the same code.
4
+ *
5
+ * Category (x) positioning has two modes:
6
+ * - band (default): a scaleBand slots each category; sub-bands split grouped series.
7
+ * - continuous (`continuousCategory`): a LINEAR position scale (e.g. a bar-chart race's tweened
8
+ * `_rank`). Each bar centres on `xScale(pos)` with a fixed thickness from the scale step, so a
9
+ * fractional position tweens smoothly (a band scale can only jump between slots). No sub-banding.
10
+ */
11
+ export function buildBars(data: any, channels: any, xScale: any, yScale: any, colors: any, innerHeight: any, patterns: any, place?: (x: any, y: any) => {
12
+ x: any;
13
+ y: any;
14
+ }, continuousCategory?: boolean): any;
15
+ export function buildStackedBars(data: any, channels: any, xScale: any, yScale: any, colors: any, innerHeight: any, patterns: any, place?: (x: any, y: any) => {
16
+ x: any;
17
+ y: any;
18
+ }): any;
@@ -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
@@ -10,6 +10,15 @@ export namespace Plot {
10
10
  export { Arc };
11
11
  export { Highlight };
12
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 };
13
22
  }
14
23
  export namespace PlotLayers { }
15
24
  export { default as PlotChart } from "./Plot.svelte";
@@ -32,6 +41,8 @@ export { default as GeomHexbin } from "./geoms/Hexbin.svelte";
32
41
  export { default as GeomRibbon } from "./geoms/Ribbon.svelte";
33
42
  export { default as GeomHighlight } from "./geoms/Highlight.svelte";
34
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";
35
46
  export { default as Chart } from "./Chart.svelte";
36
47
  export { default as Sparkline } from "./Sparkline.svelte";
37
48
  export { default as BarChart } from "./charts/BarChart.svelte";
@@ -57,14 +68,23 @@ export { default as FilterSlider } from "./crossfilter/FilterSlider.svelte";
57
68
  export { default as FilterHistogram } from "./crossfilter/FilterHistogram.svelte";
58
69
  import Root from './Plot/Root.svelte';
59
70
  import Axis from './Plot/Axis.svelte';
60
- import Bar from './Plot/Bar.svelte';
71
+ import Bar from './geoms/Bar.svelte';
61
72
  import Grid from './Plot/Grid.svelte';
62
73
  import Legend from './Plot/Legend.svelte';
63
- import Line from './Plot/Line.svelte';
64
- import Area from './Plot/Area.svelte';
65
- 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';
66
77
  import Arc from './Plot/Arc.svelte';
67
78
  import Highlight from './geoms/Highlight.svelte';
68
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';
69
89
  export { createChartPreset, defaultPreset } from "./lib/preset.js";
70
90
  export { buildSequentialScale, buildDivergingScale } from "./lib/brewing/colors.js";
package/dist/index.js CHANGED
@@ -1,14 +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
10
  import Highlight from './geoms/Highlight.svelte'
11
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'
12
21
 
13
22
  // Composable Plot primitives — use as <Plot.Root>, <Plot.Axis>, <Plot.Bar>, etc.
14
23
  export const Plot = {
@@ -22,7 +31,16 @@ export const Plot = {
22
31
  Point,
23
32
  Arc,
24
33
  Highlight,
25
- Trend
34
+ Trend,
35
+ Box,
36
+ Violin,
37
+ Jitter,
38
+ Candlestick,
39
+ Heatmap,
40
+ Hexbin,
41
+ Ribbon,
42
+ Waterfall,
43
+ Rule
26
44
  }
27
45
 
28
46
  /** @deprecated Use Plot instead */
@@ -54,6 +72,8 @@ export { default as GeomHexbin } from './geoms/Hexbin.svelte'
54
72
  export { default as GeomRibbon } from './geoms/Ribbon.svelte'
55
73
  export { default as GeomHighlight } from './geoms/Highlight.svelte'
56
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'
57
77
 
58
78
  // Export standalone components
59
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';
@@ -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[];