@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
@@ -4,6 +4,7 @@
4
4
  import type { PlotState } from '../PlotState.svelte.js'
5
5
  import { buildPoints } from '../lib/brewing/marks/points.js'
6
6
  import { keyboardNav } from '../lib/keyboard-nav.js'
7
+ import { buildSelectDetail } from '../lib/select.js'
7
8
  import LabelPill from './LabelPill.svelte'
8
9
 
9
10
  type Row = Record<string, unknown>
@@ -52,6 +53,21 @@
52
53
  const plotState = getContext<PlotState>('plot-state')
53
54
  let id = $state<string | null>(null)
54
55
 
56
+ function selectPoint(pt: { data: Row }, event: MouseEvent | KeyboardEvent) {
57
+ onselect?.(pt.data)
58
+ if (plotState.interactive)
59
+ plotState.handleSelect(
60
+ buildSelectDetail(
61
+ pt.data,
62
+ plotState.data.indexOf(pt.data),
63
+ { x, y },
64
+ 'point',
65
+ color ? pt.data[color] : undefined,
66
+ event
67
+ )
68
+ )
69
+ }
70
+
55
71
  onMount(() => {
56
72
  id = plotState.registerGeom({
57
73
  type: 'point',
@@ -91,7 +107,7 @@
91
107
 
92
108
  const points = $derived.by(() => {
93
109
  if (!data?.length || !xScale || !yScale) return []
94
- return buildPoints(
110
+ const raw = buildPoints(
95
111
  data,
96
112
  { x, y, color, size, symbol: symbolField },
97
113
  xScale,
@@ -102,6 +118,12 @@
102
118
  defaultRadius,
103
119
  options?.jitter ?? null
104
120
  )
121
+ if (!plotState.isFlipped) return raw
122
+ // Horizontal: swap each point's screen coords (value → screen-X, category → screen-Y).
123
+ return raw.map((p) => {
124
+ const s = plotState.place(p.cx, p.cy)
125
+ return { ...p, cx: s.x, cy: s.y }
126
+ })
105
127
  })
106
128
  </script>
107
129
 
@@ -118,14 +140,16 @@
118
140
  stroke-width="1"
119
141
  fill-opacity={options.opacity ?? plotState.chartPreset.opacity.point}
120
142
  data-plot-element="point"
121
- role={onselect || keyboard ? 'button' : 'graphics-symbol'}
122
- tabindex={onselect || keyboard ? 0 : undefined}
123
- style:cursor={onselect ? 'pointer' : undefined}
143
+ role={onselect || keyboard || plotState.interactive ? 'button' : 'graphics-symbol'}
144
+ tabindex={onselect || keyboard || plotState.interactive ? 0 : undefined}
145
+ style:cursor={onselect || keyboard || plotState.interactive ? 'pointer' : undefined}
124
146
  aria-label="{pt.data[x ?? '']}, {pt.data[y ?? '']}"
125
147
  onmouseenter={() => plotState.setHovered(pt.data)}
126
148
  onmouseleave={() => plotState.clearHovered()}
127
- onclick={onselect ? () => onselect(pt.data) : undefined}
128
- onkeydown={onselect ? (e) => (e.key === 'Enter' || e.key === ' ') && onselect(pt.data) : undefined}
149
+ onclick={onselect || keyboard || plotState.interactive ? (e) => selectPoint(pt, e) : undefined}
150
+ onkeydown={onselect || keyboard || plotState.interactive
151
+ ? (e) => (e.key === 'Enter' || e.key === ' ') && selectPoint(pt, e)
152
+ : undefined}
129
153
  use:keyboardNav={keyboard}
130
154
  />
131
155
  {:else}
@@ -138,14 +162,16 @@
138
162
  stroke-width="1"
139
163
  fill-opacity={options.opacity ?? plotState.chartPreset.opacity.point}
140
164
  data-plot-element="point"
141
- role={onselect || keyboard ? 'button' : 'graphics-symbol'}
142
- tabindex={onselect || keyboard ? 0 : undefined}
143
- style:cursor={onselect ? 'pointer' : undefined}
165
+ role={onselect || keyboard || plotState.interactive ? 'button' : 'graphics-symbol'}
166
+ tabindex={onselect || keyboard || plotState.interactive ? 0 : undefined}
167
+ style:cursor={onselect || keyboard || plotState.interactive ? 'pointer' : undefined}
144
168
  aria-label="{pt.data[x ?? '']}, {pt.data[y ?? '']}"
145
169
  onmouseenter={() => plotState.setHovered(pt.data)}
146
170
  onmouseleave={() => plotState.clearHovered()}
147
- onclick={onselect ? () => onselect(pt.data) : undefined}
148
- onkeydown={onselect ? (e) => (e.key === 'Enter' || e.key === ' ') && onselect(pt.data) : undefined}
171
+ onclick={onselect || keyboard || plotState.interactive ? (e) => selectPoint(pt, e) : undefined}
172
+ onkeydown={onselect || keyboard || plotState.interactive
173
+ ? (e) => (e.key === 'Enter' || e.key === ' ') && selectPoint(pt, e)
174
+ : undefined}
149
175
  use:keyboardNav={keyboard}
150
176
  />
151
177
  {/if}
@@ -0,0 +1,113 @@
1
+ <script lang="ts">
2
+ import { getContext } from 'svelte'
3
+ import type { PlotState } from '../PlotState.svelte.js'
4
+
5
+ type RuleValue = number | string
6
+ type Props = {
7
+ /** Vertical reference line(s) at these x-axis value(s) (a value, or a band category). */
8
+ x?: RuleValue | RuleValue[]
9
+ /** Horizontal reference line(s) at these y-axis value(s). */
10
+ y?: RuleValue | RuleValue[]
11
+ stroke?: string
12
+ /** SVG stroke-dasharray (default dashed). Pass '' for a solid line. */
13
+ dash?: string
14
+ strokeWidth?: number
15
+ /** Optional label drawn at the line's end. String, or a fn of the value. */
16
+ label?: string | ((value: RuleValue) => string)
17
+ }
18
+
19
+ let {
20
+ x,
21
+ y,
22
+ stroke = undefined,
23
+ dash = '4 4',
24
+ strokeWidth = 1,
25
+ label = undefined
26
+ }: Props = $props()
27
+
28
+ const plotState = getContext<PlotState>('plot-state')
29
+ const xScale = $derived(plotState.xScale)
30
+ const yScale = $derived(plotState.yScale)
31
+
32
+ const toArray = (v: RuleValue | RuleValue[] | undefined): RuleValue[] =>
33
+ v === undefined ? [] : Array.isArray(v) ? v : [v]
34
+
35
+ // Position along an axis: band scales resolve to the band centre, linear scales map directly.
36
+ function axisPos(scale: { (v: unknown): number | undefined; bandwidth?: () => number }, v: unknown) {
37
+ const base = scale(v) ?? 0
38
+ return typeof scale.bandwidth === 'function' ? base + scale.bandwidth() / 2 : base
39
+ }
40
+ // The full [min, max] screen extent of a scale's range (spans the perpendicular axis).
41
+ function axisExtent(scale: { range: () => number[] }) {
42
+ const r = scale.range()
43
+ return [Math.min(...r), Math.max(...r)]
44
+ }
45
+
46
+ const labelFor = (v: RuleValue) =>
47
+ typeof label === 'function' ? label(v) : typeof label === 'string' ? label : null
48
+
49
+ // Each rule is expressed in (x-channel, y-channel) space and placed to screen, so it
50
+ // transposes under orientation flip: a y-value reference is horizontal when vertical and
51
+ // vertical when flipped (it stays perpendicular to the value axis).
52
+ const rules = $derived.by(() => {
53
+ if (!xScale || !yScale) return []
54
+ const place = plotState.place.bind(plotState)
55
+ const out: Array<{
56
+ key: string
57
+ x1: number
58
+ y1: number
59
+ x2: number
60
+ y2: number
61
+ value: RuleValue
62
+ text: string | null
63
+ }> = []
64
+ const [xLo, xHi] = axisExtent(xScale)
65
+ const [yLo, yHi] = axisExtent(yScale)
66
+
67
+ for (const yv of toArray(y)) {
68
+ const yc = axisPos(yScale, yv)
69
+ const a = place(xLo, yc)
70
+ const b = place(xHi, yc)
71
+ out.push({ key: `y-${yv}`, x1: a.x, y1: a.y, x2: b.x, y2: b.y, value: yv, text: labelFor(yv) })
72
+ }
73
+ for (const xv of toArray(x)) {
74
+ const xc = axisPos(xScale, xv)
75
+ const a = place(xc, yLo)
76
+ const b = place(xc, yHi)
77
+ out.push({ key: `x-${xv}`, x1: a.x, y1: a.y, x2: b.x, y2: b.y, value: xv, text: labelFor(xv) })
78
+ }
79
+ return out
80
+ })
81
+
82
+ const strokeColor = $derived(stroke ?? 'currentColor')
83
+ </script>
84
+
85
+ {#if rules.length > 0}
86
+ <g data-plot-geom="rule">
87
+ {#each rules as rule (rule.key)}
88
+ <line
89
+ x1={rule.x1}
90
+ y1={rule.y1}
91
+ x2={rule.x2}
92
+ y2={rule.y2}
93
+ stroke={strokeColor}
94
+ stroke-width={strokeWidth}
95
+ stroke-dasharray={dash || undefined}
96
+ data-plot-element="rule"
97
+ data-plot-value={rule.value}
98
+ />
99
+ {#if rule.text}
100
+ <text
101
+ x={rule.x2}
102
+ y={rule.y2}
103
+ dx="-4"
104
+ dy="-4"
105
+ text-anchor="end"
106
+ font-size="10"
107
+ fill={strokeColor}
108
+ data-plot-element="rule-label"
109
+ >{rule.text}</text>
110
+ {/if}
111
+ {/each}
112
+ </g>
113
+ {/if}
@@ -0,0 +1,16 @@
1
+ type RuleValue = number | string;
2
+ type Props = {
3
+ /** Vertical reference line(s) at these x-axis value(s) (a value, or a band category). */
4
+ x?: RuleValue | RuleValue[];
5
+ /** Horizontal reference line(s) at these y-axis value(s). */
6
+ y?: RuleValue | RuleValue[];
7
+ stroke?: string;
8
+ /** SVG stroke-dasharray (default dashed). Pass '' for a solid line. */
9
+ dash?: string;
10
+ strokeWidth?: number;
11
+ /** Optional label drawn at the line's end. String, or a fn of the value. */
12
+ label?: string | ((value: RuleValue) => string);
13
+ };
14
+ declare const Rule: import("svelte").Component<Props, {}, "">;
15
+ type Rule = ReturnType<typeof Rule>;
16
+ export default Rule;
@@ -0,0 +1,66 @@
1
+ <script lang="ts">
2
+ import { getContext } from 'svelte'
3
+ import { line as d3line } from 'd3-shape'
4
+ import type { PlotState } from '../PlotState.svelte.js'
5
+ import { computeTrend } from '../lib/trend.js'
6
+ import { scalePos } from '../lib/scale.js'
7
+
8
+ type Method = string | number | { type: string; [k: string]: unknown }
9
+ type Props = { x?: string; y?: string; trend?: Method | Method[] }
10
+ let { x, y, trend = undefined }: Props = $props()
11
+
12
+ const state = getContext<PlotState>('plot-state')
13
+
14
+ const methods = $derived(
15
+ Array.isArray(trend) ? trend : trend === null || trend === undefined ? [] : [trend]
16
+ )
17
+
18
+ const typeOf = (m: Method) => {
19
+ const t = typeof m === 'string' ? m : typeof m === 'number' ? 'value' : (m?.type ?? '')
20
+ return t === 'mean' ? 'avg' : t
21
+ }
22
+
23
+ const paths = $derived.by(() => {
24
+ const rows = state?.data ?? []
25
+ const xs = state?.xScale
26
+ const ys = state?.yScale
27
+ if (!rows.length || !xs || !ys || !x || !y || !methods.length) return []
28
+ const out: { d: string; type: string; i: number }[] = []
29
+ methods.forEach((m, idx) => {
30
+ const res = computeTrend(rows, { x, y }, m)
31
+ if (!res) return
32
+ if (res.kind === 'constant') {
33
+ const yy = ys(res.value)
34
+ out.push({ d: `M0,${yy} L${state.innerWidth},${yy}`, type: typeOf(m), i: idx })
35
+ } else {
36
+ const pts = res.values
37
+ .map((v, i) => ({ vx: scalePos(xs, rows[i][x]), vy: ys(v) }))
38
+ .filter((p) => Number.isFinite(p.vx) && Number.isFinite(p.vy))
39
+ const gen = d3line<{ vx: number; vy: number }>()
40
+ .x((p) => p.vx)
41
+ .y((p) => p.vy)
42
+ const d = gen(pts)
43
+ if (d) out.push({ d, type: typeOf(m), i: idx })
44
+ }
45
+ })
46
+ return out
47
+ })
48
+ </script>
49
+
50
+ {#if paths.length}
51
+ <g data-plot-geom="trend">
52
+ {#each paths as p (p.i)}
53
+ <path d={p.d} data-plot-trend={p.type} />
54
+ {/each}
55
+ </g>
56
+ {/if}
57
+
58
+ <style>
59
+ [data-plot-trend] {
60
+ fill: none;
61
+ stroke: var(--chart-trend-color, currentColor);
62
+ stroke-width: var(--chart-trend-width, 1);
63
+ stroke-dasharray: var(--chart-trend-dash, 4 4);
64
+ opacity: var(--chart-trend-opacity, 0.7);
65
+ }
66
+ </style>
@@ -0,0 +1,12 @@
1
+ type Method = string | number | {
2
+ type: string;
3
+ [k: string]: unknown;
4
+ };
5
+ type Props = {
6
+ x?: string;
7
+ y?: string;
8
+ trend?: Method | Method[];
9
+ };
10
+ declare const Trend: import("svelte").Component<Props, {}, "">;
11
+ type Trend = ReturnType<typeof Trend>;
12
+ export default Trend;
@@ -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;