@rokkit/chart 1.3.7 → 1.3.9

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 (162) hide show
  1. package/dist/AnimatedPlot.svelte +26 -8
  2. package/dist/AnimatedPlot.svelte.d.ts +5 -0
  3. package/dist/Chart.svelte +51 -65
  4. package/dist/FacetPlot.svelte +1 -1
  5. package/dist/Plot/Axis.svelte +19 -10
  6. package/dist/Plot/Root.svelte +67 -46
  7. package/dist/Plot/Root.svelte.d.ts +29 -2
  8. package/dist/Plot/Timeline.svelte +39 -13
  9. package/dist/Plot/index.d.ts +5 -5
  10. package/dist/Plot/index.js +5 -5
  11. package/dist/Plot.svelte +95 -178
  12. package/dist/Plot.svelte.d.ts +4 -0
  13. package/dist/PlotState.svelte.d.ts +29 -1
  14. package/dist/PlotState.svelte.js +163 -19
  15. package/dist/PlotSurface.svelte +162 -0
  16. package/dist/PlotSurface.svelte.d.ts +25 -0
  17. package/dist/charts/AreaChart.svelte +12 -2
  18. package/dist/charts/AreaChart.svelte.d.ts +6 -0
  19. package/dist/charts/BarChart.svelte +40 -4
  20. package/dist/charts/BarChart.svelte.d.ts +15 -1
  21. package/dist/charts/BoxPlot.svelte +7 -3
  22. package/dist/charts/BoxPlot.svelte.d.ts +2 -0
  23. package/dist/charts/BubbleChart.svelte +37 -4
  24. package/dist/charts/BubbleChart.svelte.d.ts +12 -1
  25. package/dist/charts/LineChart.svelte +4 -2
  26. package/dist/charts/LineChart.svelte.d.ts +1 -0
  27. package/dist/charts/PieChart.svelte +5 -3
  28. package/dist/charts/PieChart.svelte.d.ts +1 -0
  29. package/dist/charts/ScatterPlot.svelte +35 -4
  30. package/dist/charts/ScatterPlot.svelte.d.ts +11 -1
  31. package/dist/charts/ViolinPlot.svelte +7 -3
  32. package/dist/charts/ViolinPlot.svelte.d.ts +2 -0
  33. package/dist/examples/BarChartExample.svelte +1 -1
  34. package/dist/geoms/Arc.svelte +21 -54
  35. package/dist/geoms/Arc.svelte.d.ts +4 -4
  36. package/dist/geoms/Area.svelte +45 -44
  37. package/dist/geoms/Area.svelte.d.ts +8 -1
  38. package/dist/geoms/Bar.svelte +33 -45
  39. package/dist/geoms/Bar.svelte.d.ts +9 -0
  40. package/dist/geoms/Box.svelte +103 -61
  41. package/dist/geoms/Box.svelte.d.ts +12 -3
  42. package/dist/geoms/Candlestick.svelte +26 -75
  43. package/dist/geoms/Candlestick.svelte.d.ts +2 -0
  44. package/dist/geoms/Heatmap.svelte +21 -66
  45. package/dist/geoms/Heatmap.svelte.d.ts +2 -0
  46. package/dist/geoms/Hexbin.svelte +20 -100
  47. package/dist/geoms/Hexbin.svelte.d.ts +2 -0
  48. package/dist/geoms/Jitter.svelte +67 -0
  49. package/dist/geoms/Jitter.svelte.d.ts +17 -0
  50. package/dist/geoms/Line.svelte +27 -31
  51. package/dist/geoms/Line.svelte.d.ts +5 -0
  52. package/dist/geoms/Point.svelte +31 -60
  53. package/dist/geoms/Point.svelte.d.ts +5 -1
  54. package/dist/geoms/Ribbon.svelte +30 -124
  55. package/dist/geoms/Ribbon.svelte.d.ts +2 -0
  56. package/dist/geoms/Rule.svelte +117 -0
  57. package/dist/geoms/Rule.svelte.d.ts +19 -0
  58. package/dist/geoms/Violin.svelte +36 -31
  59. package/dist/geoms/Violin.svelte.d.ts +10 -3
  60. package/dist/geoms/Waterfall.svelte +25 -79
  61. package/dist/geoms/Waterfall.svelte.d.ts +2 -0
  62. package/dist/geoms/lib/GeomState.svelte.d.ts +51 -0
  63. package/dist/geoms/lib/GeomState.svelte.js +79 -0
  64. package/dist/geoms/lib/aesthetics.d.ts +36 -0
  65. package/dist/geoms/lib/aesthetics.js +45 -0
  66. package/dist/geoms/lib/areas.d.ts +6 -2
  67. package/dist/geoms/lib/areas.js +20 -12
  68. package/dist/geoms/lib/bars.d.ts +18 -3
  69. package/dist/geoms/lib/bars.js +80 -101
  70. package/dist/geoms/lib/marks/arc.d.ts +24 -0
  71. package/dist/geoms/lib/marks/arc.js +28 -0
  72. package/dist/geoms/lib/marks/area.d.ts +23 -0
  73. package/dist/geoms/lib/marks/area.js +38 -0
  74. package/dist/geoms/lib/marks/bar.d.ts +16 -0
  75. package/dist/geoms/lib/marks/bar.js +43 -0
  76. package/dist/geoms/lib/marks/box.d.ts +14 -0
  77. package/dist/geoms/lib/marks/box.js +30 -0
  78. package/dist/geoms/lib/marks/candlestick.d.ts +27 -0
  79. package/dist/geoms/lib/marks/candlestick.js +52 -0
  80. package/dist/geoms/lib/marks/heatmap.d.ts +24 -0
  81. package/dist/geoms/lib/marks/heatmap.js +41 -0
  82. package/dist/geoms/lib/marks/hexbin.d.ts +29 -0
  83. package/dist/geoms/lib/marks/hexbin.js +62 -0
  84. package/dist/geoms/lib/marks/jitter.d.ts +21 -0
  85. package/dist/geoms/lib/marks/jitter.js +36 -0
  86. package/dist/geoms/lib/marks/line.d.ts +26 -0
  87. package/dist/geoms/lib/marks/line.js +25 -0
  88. package/dist/geoms/lib/marks/point.d.ts +25 -0
  89. package/dist/geoms/lib/marks/point.js +58 -0
  90. package/dist/geoms/lib/marks/ribbon.d.ts +31 -0
  91. package/dist/geoms/lib/marks/ribbon.js +77 -0
  92. package/dist/geoms/lib/marks/violin.d.ts +13 -0
  93. package/dist/geoms/lib/marks/violin.js +31 -0
  94. package/dist/geoms/lib/marks/waterfall.d.ts +25 -0
  95. package/dist/geoms/lib/marks/waterfall.js +58 -0
  96. package/dist/index.d.ts +25 -12
  97. package/dist/index.js +26 -13
  98. package/dist/lib/brewing/marks/arcs.js +7 -1
  99. package/dist/lib/brewing/marks/boxes.d.ts +11 -2
  100. package/dist/lib/brewing/marks/boxes.js +32 -11
  101. package/dist/lib/brewing/marks/lines.d.ts +4 -1
  102. package/dist/lib/brewing/marks/lines.js +18 -15
  103. package/dist/lib/brewing/marks/swarm.d.ts +32 -0
  104. package/dist/lib/brewing/marks/swarm.js +125 -0
  105. package/dist/lib/brewing/marks/violins.d.ts +4 -20
  106. package/dist/lib/brewing/marks/violins.js +41 -25
  107. package/dist/lib/brewing/stats.d.ts +3 -1
  108. package/dist/lib/brewing/stats.js +28 -11
  109. package/dist/lib/plot/scales.js +12 -7
  110. package/dist/lib/plot/types.d.ts +9 -1
  111. package/dist/lib/plot/types.js +3 -1
  112. package/dist/lib/preset.d.ts +11 -1
  113. package/dist/lib/preset.js +16 -4
  114. package/package.json +1 -1
  115. package/dist/Plot/Arc.svelte +0 -30
  116. package/dist/Plot/Arc.svelte.d.ts +0 -18
  117. package/dist/Plot/Area.svelte +0 -54
  118. package/dist/Plot/Area.svelte.d.ts +0 -14
  119. package/dist/Plot/Bar.svelte +0 -69
  120. package/dist/Plot/Bar.svelte.d.ts +0 -11
  121. package/dist/Plot/Line.svelte +0 -54
  122. package/dist/Plot/Line.svelte.d.ts +0 -13
  123. package/dist/Plot/Point.svelte +0 -58
  124. package/dist/Plot/Point.svelte.d.ts +0 -13
  125. package/dist/lib/brewer.d.ts +0 -9
  126. package/dist/lib/brewer.js +0 -25
  127. package/dist/lib/brewing/BoxBrewer.svelte.d.ts +0 -8
  128. package/dist/lib/brewing/BoxBrewer.svelte.js +0 -14
  129. package/dist/lib/brewing/CartesianBrewer.svelte.d.ts +0 -8
  130. package/dist/lib/brewing/CartesianBrewer.svelte.js +0 -21
  131. package/dist/lib/brewing/PieBrewer.svelte.d.ts +0 -8
  132. package/dist/lib/brewing/PieBrewer.svelte.js +0 -14
  133. package/dist/lib/brewing/QuartileBrewer.svelte.d.ts +0 -9
  134. package/dist/lib/brewing/QuartileBrewer.svelte.js +0 -51
  135. package/dist/lib/brewing/ViolinBrewer.svelte.d.ts +0 -8
  136. package/dist/lib/brewing/ViolinBrewer.svelte.js +0 -14
  137. package/dist/lib/brewing/axes.svelte.d.ts +0 -66
  138. package/dist/lib/brewing/axes.svelte.js +0 -270
  139. package/dist/lib/brewing/bars.svelte.d.ts +0 -56
  140. package/dist/lib/brewing/bars.svelte.js +0 -201
  141. package/dist/lib/brewing/brewer.svelte.d.ts +0 -124
  142. package/dist/lib/brewing/brewer.svelte.js +0 -277
  143. package/dist/lib/brewing/dimensions.svelte.d.ts +0 -35
  144. package/dist/lib/brewing/dimensions.svelte.js +0 -56
  145. package/dist/lib/brewing/index.svelte.d.ts +0 -118
  146. package/dist/lib/brewing/index.svelte.js +0 -205
  147. package/dist/lib/brewing/legends.svelte.d.ts +0 -48
  148. package/dist/lib/brewing/legends.svelte.js +0 -137
  149. package/dist/lib/brewing/marks/areas.d.ts +0 -31
  150. package/dist/lib/brewing/marks/areas.js +0 -72
  151. package/dist/lib/brewing/marks/bars.d.ts +0 -1
  152. package/dist/lib/brewing/marks/bars.js +0 -49
  153. package/dist/lib/brewing/scales.svelte.d.ts +0 -24
  154. package/dist/lib/brewing/scales.svelte.js +0 -82
  155. package/dist/lib/context.d.ts +0 -13
  156. package/dist/lib/context.js +0 -131
  157. package/dist/lib/plot/chartProps.d.ts +0 -177
  158. package/dist/lib/plot/chartProps.js +0 -76
  159. package/dist/lib/scales.svelte.d.ts +0 -35
  160. package/dist/lib/scales.svelte.js +0 -151
  161. package/dist/lib/ticks.d.ts +0 -36
  162. package/dist/lib/ticks.js +0 -46
@@ -1,6 +1,8 @@
1
1
  <script lang="ts">
2
2
  import { getContext, onMount, onDestroy } from 'svelte'
3
3
  import type { PlotState } from '../PlotState.svelte.js'
4
+ import { GeomState } from './lib/GeomState.svelte.js'
5
+ import { buildWaterfallMarks } from './lib/marks/waterfall.js'
4
6
 
5
7
  type Options = {
6
8
  positiveColor?: string
@@ -15,90 +17,31 @@
15
17
  y?: string
16
18
  color?: string
17
19
  fill?: string
20
+ /** Fixed bar opacity 0–1; defaults to the per-geom preset (waterfall = 1). */
21
+ alpha?: number
18
22
  stat?: string
19
23
  options?: Options
20
24
  }
21
25
 
22
- let {
23
- x,
24
- y,
25
- color = undefined,
26
- fill: fillProp = undefined,
27
- stat = 'identity',
28
- options = {}
29
- }: Props = $props()
26
+ let { x, y, color, fill, alpha, stat = 'identity', options = {} }: Props = $props()
30
27
 
31
- const positiveColor = $derived(options.positiveColor ?? '#22c55e')
32
- const negativeColor = $derived(options.negativeColor ?? '#ef4444')
33
- const totalColor = $derived(options.totalColor ?? '#3b82f6')
34
- const totalField = $derived(options.totalField ?? undefined)
28
+ const colorField = $derived(fill ?? color)
35
29
  const connectorWidth = $derived(options.connectorWidth ?? 0.5)
36
-
37
30
  const plotState = getContext<PlotState>('plot-state')
38
- let id = $state<string | null>(null)
39
-
40
- onMount(() => {
41
- id = plotState.registerGeom({
42
- type: 'waterfall',
43
- channels: { x, y, color: fillProp ?? color },
44
- stat
45
- })
46
- })
47
- onDestroy(() => {
48
- if (id) plotState.unregisterGeom(id)
49
- })
50
-
51
- $effect(() => {
52
- if (id)
53
- plotState.updateGeom(id, {
54
- channels: { x, y, color: fillProp ?? color },
55
- stat
56
- })
57
- })
58
-
59
- const data = $derived(id ? plotState.geomData(id) : [])
60
- const xScale = $derived(plotState.xScale)
61
- const yScale = $derived(plotState.yScale)
62
-
63
- // eslint-disable-next-line max-lines-per-function
64
- const bars = $derived.by(() => {
65
- if (!data?.length || !xScale || !yScale) return []
66
- const bw = typeof xScale.bandwidth === 'function' ? xScale.bandwidth() : 10
67
-
68
- let cumulative = 0
69
- return data.map((d, i) => {
70
- const xVal = d[x ?? '']
71
- const yVal = Number(d[y ?? ''])
72
- const isTotal = totalField ? Boolean(d[totalField]) : false
73
- const xPos = xScale(xVal) ?? 0
74
- let barTop: number
75
- let barBottom: number
76
- let fill: string
77
31
 
78
- if (isTotal) {
79
- barTop = yScale(Math.max(0, cumulative)) ?? 0
80
- barBottom = yScale(0) ?? 0
81
- fill = totalColor
82
- } else {
83
- const start = cumulative
84
- cumulative += yVal
85
- barTop = yScale(Math.max(start, cumulative)) ?? 0
86
- barBottom = yScale(Math.min(start, cumulative)) ?? 0
87
- fill = yVal >= 0 ? positiveColor : negativeColor
88
- }
32
+ const geom = new GeomState(plotState, () => ({
33
+ type: 'waterfall',
34
+ channels: { x, y, color: colorField },
35
+ stat,
36
+ options,
37
+ alpha,
38
+ build: buildWaterfallMarks
39
+ }))
40
+ onMount(geom.register)
41
+ onDestroy(geom.destroy)
42
+ $effect(geom.sync)
89
43
 
90
- return {
91
- key: `${xVal}-${i}`,
92
- x: xPos,
93
- y: barTop,
94
- width: bw,
95
- height: Math.max(1, barBottom - barTop),
96
- fill,
97
- cumulative,
98
- data: d
99
- }
100
- })
101
- })
44
+ const bars = $derived(geom.marks)
102
45
  </script>
103
46
 
104
47
  {#if bars.length > 0}
@@ -110,6 +53,7 @@
110
53
  width={Math.max(0, bar.width)}
111
54
  height={bar.height}
112
55
  fill={bar.fill}
56
+ fill-opacity={bar.alpha}
113
57
  data-plot-element="waterfall-bar"
114
58
  role="img"
115
59
  onmouseenter={() => plotState.setHovered(bar.data)}
@@ -119,11 +63,13 @@
119
63
  </rect>
120
64
  <!-- Connector line to next bar -->
121
65
  {#if i < bars.length - 1}
66
+ {@const cs = plotState.place(bar.bandEnd, bar.cumY)}
67
+ {@const ce = plotState.place(bars[i + 1].bandStart, bar.cumY)}
122
68
  <line
123
- x1={bar.x + bar.width}
124
- y1={yScale(bar.cumulative)}
125
- x2={bars[i + 1].x}
126
- y2={yScale(bar.cumulative)}
69
+ x1={cs.x}
70
+ y1={cs.y}
71
+ x2={ce.x}
72
+ y2={ce.y}
127
73
  stroke="currentColor"
128
74
  stroke-width={connectorWidth}
129
75
  stroke-dasharray="3 2"
@@ -10,6 +10,8 @@ type Props = {
10
10
  y?: string;
11
11
  color?: string;
12
12
  fill?: string;
13
+ /** Fixed bar opacity 0–1; defaults to the per-geom preset (waterfall = 1). */
14
+ alpha?: number;
13
15
  stat?: string;
14
16
  options?: Options;
15
17
  };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Per-geom reactive state, fed by the shared `PlotState` context. Owns the geom's
3
+ * registration lifecycle and derives the renderable `marks` array, so a geom component
4
+ * only has to render marks + wire interactivity. This is the integrated version of the
5
+ * old brewer idea — one shared path, not a parallel one.
6
+ * See docs/backlog/2026-08-17-chart-aesthetics-unification.md §11.
7
+ *
8
+ * @typedef {Object} GeomConfig
9
+ * @property {string} type
10
+ * @property {Record<string, string | undefined>} channels
11
+ * @property {string} [stat]
12
+ * @property {Record<string, unknown>} [options]
13
+ * @property {number} [alpha]
14
+ * @property {(ctx: { data: any[], plot: any, channels: any, options: any, alpha: number|undefined, type: string }) => any} build
15
+ */
16
+ export class GeomState {
17
+ /**
18
+ * @param {any} plot - the PlotState from context
19
+ * @param {() => GeomConfig} config - a getter thunk (Svelte passes reactive values to
20
+ * classes via getters), read reactively on every marks recompute.
21
+ */
22
+ constructor(plot: any, config: () => GeomConfig);
23
+ register: () => void;
24
+ sync: () => void;
25
+ destroy: () => void;
26
+ get data(): any;
27
+ marks: any;
28
+ #private;
29
+ }
30
+ /**
31
+ * Per-geom reactive state, fed by the shared `PlotState` context. Owns the geom's
32
+ * registration lifecycle and derives the renderable `marks` array, so a geom component
33
+ * only has to render marks + wire interactivity. This is the integrated version of the
34
+ * old brewer idea — one shared path, not a parallel one.
35
+ * See docs/backlog/2026-08-17-chart-aesthetics-unification.md §11.
36
+ */
37
+ export type GeomConfig = {
38
+ type: string;
39
+ channels: Record<string, string | undefined>;
40
+ stat?: string | undefined;
41
+ options?: Record<string, unknown> | undefined;
42
+ alpha?: number | undefined;
43
+ build: (ctx: {
44
+ data: any[];
45
+ plot: any;
46
+ channels: any;
47
+ options: any;
48
+ alpha: number | undefined;
49
+ type: string;
50
+ }) => any;
51
+ };
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Per-geom reactive state, fed by the shared `PlotState` context. Owns the geom's
3
+ * registration lifecycle and derives the renderable `marks` array, so a geom component
4
+ * only has to render marks + wire interactivity. This is the integrated version of the
5
+ * old brewer idea — one shared path, not a parallel one.
6
+ * See docs/backlog/2026-08-17-chart-aesthetics-unification.md §11.
7
+ *
8
+ * @typedef {Object} GeomConfig
9
+ * @property {string} type
10
+ * @property {Record<string, string | undefined>} channels
11
+ * @property {string} [stat]
12
+ * @property {Record<string, unknown>} [options]
13
+ * @property {number} [alpha]
14
+ * @property {(ctx: { data: any[], plot: any, channels: any, options: any, alpha: number|undefined, type: string }) => any} build
15
+ */
16
+ export class GeomState {
17
+ #plot
18
+ #config
19
+ #id = $state(null)
20
+
21
+ /**
22
+ * @param {any} plot - the PlotState from context
23
+ * @param {() => GeomConfig} config - a getter thunk (Svelte passes reactive values to
24
+ * classes via getters), read reactively on every marks recompute.
25
+ */
26
+ constructor(plot, config) {
27
+ this.#plot = plot
28
+ this.#config = config
29
+ }
30
+
31
+ // Arrow methods so they can be passed straight to onMount/onDestroy/$effect without
32
+ // losing `this` (see Svelte $state "Classes" docs).
33
+ register = () => {
34
+ const c = this.#config()
35
+ this.#id = this.#plot.registerGeom({
36
+ type: c.type,
37
+ channels: c.channels,
38
+ stat: c.stat ?? 'identity',
39
+ options: c.options ?? {}
40
+ })
41
+ }
42
+
43
+ sync = () => {
44
+ if (this.#id === null) return
45
+ const c = this.#config()
46
+ this.#plot.updateGeom(this.#id, {
47
+ channels: c.channels,
48
+ stat: c.stat ?? 'identity',
49
+ options: c.options ?? {}
50
+ })
51
+ }
52
+
53
+ destroy = () => {
54
+ if (this.#id !== null) this.#plot.unregisterGeom(this.#id)
55
+ }
56
+
57
+ #data = $derived(this.#id !== null ? this.#plot.geomData(this.#id) : [])
58
+
59
+ // The geom's rows (post-stat). Exposed for templates that render per-row overlays
60
+ // (e.g. an area's invisible tooltip hit-circles) alongside the computed `marks`.
61
+ get data() {
62
+ return this.#data
63
+ }
64
+
65
+ // The renderable array. `build` reads scales/colors/preset from `plot` synchronously,
66
+ // so those reads register as dependencies and `marks` recomputes on any change.
67
+ marks = $derived.by(() => {
68
+ const c = this.#config()
69
+ if (!this.#data.length) return []
70
+ return c.build({
71
+ data: this.#data,
72
+ plot: this.#plot,
73
+ channels: c.channels,
74
+ options: c.options ?? {},
75
+ alpha: c.alpha,
76
+ type: c.type
77
+ })
78
+ })
79
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Resolve a mark's interior (fill) and outline (stroke) from the `fill` and `color`
3
+ * channels through the shared categorical `colors` palette (Map<value, {fill, stroke}>).
4
+ *
5
+ * ggplot fallback:
6
+ * - color-only → fill AND stroke from the color value's entry (backward compatible)
7
+ * - fill-only → fill AND stroke from the fill value's entry
8
+ * - both → interior from the fill value, outline from the color value
9
+ * - neither → the first palette entry (single-series)
10
+ *
11
+ * @param {Record<string, unknown>} row
12
+ * @param {{ fill?: string, color?: string }} channels
13
+ * @param {Map<unknown, { fill: string, stroke: string }>} colors
14
+ * @returns {{ fill: string, stroke: string }}
15
+ */
16
+ export function resolveFillStroke(row: Record<string, unknown>, channels: {
17
+ fill?: string;
18
+ color?: string;
19
+ }, colors: Map<unknown, {
20
+ fill: string;
21
+ stroke: string;
22
+ }>): {
23
+ fill: string;
24
+ stroke: string;
25
+ };
26
+ /**
27
+ * Resolve a geom's mark opacity: an explicit fixed `alpha`, else the per-geom preset
28
+ * default, else fully opaque.
29
+ * @param {number|undefined} alpha
30
+ * @param {string} type - geom type key into preset.opacity
31
+ * @param {{ opacity?: Record<string, number> }} [preset]
32
+ * @returns {number}
33
+ */
34
+ export function resolveAlpha(alpha: number | undefined, type: string, preset?: {
35
+ opacity?: Record<string, number>;
36
+ }): number;
@@ -0,0 +1,45 @@
1
+ // Shared aesthetic resolution for all geoms. Done ONCE here so every geom's build
2
+ // function inherits the same ggplot-aligned fill/color and alpha rules.
3
+ // See docs/backlog/2026-08-17-chart-aesthetics-unification.md §2, §3, §11.
4
+
5
+ const GRAY = { fill: '#888', stroke: '#888' }
6
+
7
+ /**
8
+ * Resolve a mark's interior (fill) and outline (stroke) from the `fill` and `color`
9
+ * channels through the shared categorical `colors` palette (Map<value, {fill, stroke}>).
10
+ *
11
+ * ggplot fallback:
12
+ * - color-only → fill AND stroke from the color value's entry (backward compatible)
13
+ * - fill-only → fill AND stroke from the fill value's entry
14
+ * - both → interior from the fill value, outline from the color value
15
+ * - neither → the first palette entry (single-series)
16
+ *
17
+ * @param {Record<string, unknown>} row
18
+ * @param {{ fill?: string, color?: string }} channels
19
+ * @param {Map<unknown, { fill: string, stroke: string }>} colors
20
+ * @returns {{ fill: string, stroke: string }}
21
+ */
22
+ export function resolveFillStroke(row, channels, colors) {
23
+ const { fill, color } = channels ?? {}
24
+ const first = colors?.values?.().next?.().value ?? GRAY
25
+ const fillEntry = fill ? (colors?.get(row?.[fill]) ?? first) : null
26
+ const colorEntry = color ? (colors?.get(row?.[color]) ?? first) : null
27
+ const base = fillEntry ?? colorEntry ?? first
28
+ return {
29
+ fill: (fillEntry ?? base).fill,
30
+ stroke: (colorEntry ?? base).stroke
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Resolve a geom's mark opacity: an explicit fixed `alpha`, else the per-geom preset
36
+ * default, else fully opaque.
37
+ * @param {number|undefined} alpha
38
+ * @param {string} type - geom type key into preset.opacity
39
+ * @param {{ opacity?: Record<string, number> }} [preset]
40
+ * @returns {number}
41
+ */
42
+ export function resolveAlpha(alpha, type, preset) {
43
+ if (typeof alpha === 'number') return alpha
44
+ return preset?.opacity?.[type] ?? 1
45
+ }
@@ -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
+ }, normalize?: boolean): {
49
53
  d: string;
50
54
  fill: string;
51
55
  stroke: string;
@@ -1,4 +1,4 @@
1
- import { area, stack, curveCatmullRom, curveStep } from 'd3-shape'
1
+ import { area, stack, stackOffsetExpand, curveCatmullRom, curveStep } from 'd3-shape'
2
2
  import { toPatternId } from '../../lib/brewing/patterns.js'
3
3
 
4
4
  /**
@@ -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 }), normalize = false) {
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,17 +119,21 @@ 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
126
132
  }
127
133
 
128
134
  const stackGen = stack().keys(colorCategories)
135
+ // position='fill' → normalize each x column to [0,1] (100% stacked area).
136
+ if (normalize) stackGen.offset(stackOffsetExpand)
129
137
  const layers = stackGen(wide)
130
138
 
131
139
  const orderedPatternKeys = pf && pf !== cf ? [...(patterns?.keys() ?? [])] : null
@@ -144,7 +152,7 @@ export function buildStackedAreas(data, channels, xScale, yScale, colors, curve,
144
152
  ? toPatternId(String(patternKey))
145
153
  : null
146
154
  return {
147
- d: makeGen()(layer) ?? '',
155
+ d: makeGen()(layer.map(toEdge)) ?? '',
148
156
  fill: entry.fill,
149
157
  stroke: 'none',
150
158
  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, dodge?: 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
+ }, normalize?: boolean): any;