@rokkit/chart 1.3.8 → 1.3.10

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 (140) hide show
  1. package/dist/Chart.svelte +50 -64
  2. package/dist/Plot/Root.svelte +4 -1
  3. package/dist/Plot/Root.svelte.d.ts +2 -0
  4. package/dist/Plot/index.d.ts +1 -1
  5. package/dist/Plot/index.js +1 -1
  6. package/dist/Plot.svelte +4 -1
  7. package/dist/Plot.svelte.d.ts +2 -0
  8. package/dist/PlotState.svelte.d.ts +14 -2
  9. package/dist/PlotState.svelte.js +81 -5
  10. package/dist/charts/AreaChart.svelte +8 -1
  11. package/dist/charts/AreaChart.svelte.d.ts +5 -0
  12. package/dist/charts/BarChart.svelte +8 -1
  13. package/dist/charts/BarChart.svelte.d.ts +5 -0
  14. package/dist/charts/BoxPlot.svelte +3 -1
  15. package/dist/charts/BoxPlot.svelte.d.ts +1 -0
  16. package/dist/charts/BubbleChart.svelte +3 -1
  17. package/dist/charts/BubbleChart.svelte.d.ts +1 -0
  18. package/dist/charts/LineChart.svelte +3 -1
  19. package/dist/charts/LineChart.svelte.d.ts +1 -0
  20. package/dist/charts/PieChart.svelte +4 -2
  21. package/dist/charts/PieChart.svelte.d.ts +1 -0
  22. package/dist/charts/ScatterPlot.svelte +3 -1
  23. package/dist/charts/ScatterPlot.svelte.d.ts +1 -0
  24. package/dist/charts/ViolinPlot.svelte +3 -1
  25. package/dist/charts/ViolinPlot.svelte.d.ts +1 -0
  26. package/dist/geoms/Arc.svelte +21 -55
  27. package/dist/geoms/Arc.svelte.d.ts +4 -4
  28. package/dist/geoms/Area.svelte +45 -44
  29. package/dist/geoms/Area.svelte.d.ts +8 -1
  30. package/dist/geoms/Bar.svelte +33 -56
  31. package/dist/geoms/Bar.svelte.d.ts +9 -0
  32. package/dist/geoms/Box.svelte +118 -131
  33. package/dist/geoms/Box.svelte.d.ts +5 -3
  34. package/dist/geoms/Candlestick.svelte +22 -77
  35. package/dist/geoms/Candlestick.svelte.d.ts +2 -0
  36. package/dist/geoms/Heatmap.svelte +21 -66
  37. package/dist/geoms/Heatmap.svelte.d.ts +2 -0
  38. package/dist/geoms/Hexbin.svelte +20 -100
  39. package/dist/geoms/Hexbin.svelte.d.ts +2 -0
  40. package/dist/geoms/Jitter.svelte +21 -36
  41. package/dist/geoms/Jitter.svelte.d.ts +5 -3
  42. package/dist/geoms/Line.svelte +27 -31
  43. package/dist/geoms/Line.svelte.d.ts +5 -0
  44. package/dist/geoms/Point.svelte +31 -66
  45. package/dist/geoms/Point.svelte.d.ts +5 -1
  46. package/dist/geoms/Ribbon.svelte +30 -124
  47. package/dist/geoms/Ribbon.svelte.d.ts +2 -0
  48. package/dist/geoms/Rule.svelte +5 -1
  49. package/dist/geoms/Rule.svelte.d.ts +3 -0
  50. package/dist/geoms/Violin.svelte +21 -40
  51. package/dist/geoms/Violin.svelte.d.ts +5 -3
  52. package/dist/geoms/Waterfall.svelte +19 -80
  53. package/dist/geoms/Waterfall.svelte.d.ts +2 -0
  54. package/dist/geoms/lib/GeomState.svelte.d.ts +51 -0
  55. package/dist/geoms/lib/GeomState.svelte.js +79 -0
  56. package/dist/geoms/lib/aesthetics.d.ts +36 -0
  57. package/dist/geoms/lib/aesthetics.js +45 -0
  58. package/dist/geoms/lib/areas.d.ts +1 -1
  59. package/dist/geoms/lib/areas.js +18 -5
  60. package/dist/geoms/lib/bars.d.ts +2 -2
  61. package/dist/geoms/lib/bars.js +37 -17
  62. package/dist/geoms/lib/marks/arc.d.ts +24 -0
  63. package/dist/geoms/lib/marks/arc.js +28 -0
  64. package/dist/geoms/lib/marks/area.d.ts +23 -0
  65. package/dist/geoms/lib/marks/area.js +38 -0
  66. package/dist/geoms/lib/marks/bar.d.ts +16 -0
  67. package/dist/geoms/lib/marks/bar.js +43 -0
  68. package/dist/geoms/lib/marks/box.d.ts +14 -0
  69. package/dist/geoms/lib/marks/box.js +30 -0
  70. package/dist/geoms/lib/marks/candlestick.d.ts +27 -0
  71. package/dist/geoms/lib/marks/candlestick.js +52 -0
  72. package/dist/geoms/lib/marks/heatmap.d.ts +24 -0
  73. package/dist/geoms/lib/marks/heatmap.js +47 -0
  74. package/dist/geoms/lib/marks/hexbin.d.ts +29 -0
  75. package/dist/geoms/lib/marks/hexbin.js +62 -0
  76. package/dist/geoms/lib/marks/jitter.d.ts +21 -0
  77. package/dist/geoms/lib/marks/jitter.js +36 -0
  78. package/dist/geoms/lib/marks/line.d.ts +26 -0
  79. package/dist/geoms/lib/marks/line.js +25 -0
  80. package/dist/geoms/lib/marks/point.d.ts +25 -0
  81. package/dist/geoms/lib/marks/point.js +58 -0
  82. package/dist/geoms/lib/marks/ribbon.d.ts +32 -0
  83. package/dist/geoms/lib/marks/ribbon.js +80 -0
  84. package/dist/geoms/lib/marks/violin.d.ts +13 -0
  85. package/dist/geoms/lib/marks/violin.js +31 -0
  86. package/dist/geoms/lib/marks/waterfall.d.ts +25 -0
  87. package/dist/geoms/lib/marks/waterfall.js +58 -0
  88. package/dist/index.d.ts +1 -8
  89. package/dist/index.js +1 -8
  90. package/dist/lib/brewing/colors.d.ts +31 -1
  91. package/dist/lib/brewing/colors.js +34 -1
  92. package/dist/lib/brewing/marks/arcs.js +9 -4
  93. package/dist/lib/brewing/marks/boxes.js +8 -4
  94. package/dist/lib/brewing/marks/lines.js +13 -2
  95. package/dist/lib/brewing/marks/points.js +7 -2
  96. package/dist/lib/brewing/marks/swarm.js +6 -2
  97. package/dist/lib/brewing/marks/violins.js +8 -4
  98. package/dist/lib/preset.d.ts +11 -1
  99. package/dist/lib/preset.js +16 -4
  100. package/package.json +1 -1
  101. package/dist/Plot/Arc.svelte +0 -30
  102. package/dist/Plot/Arc.svelte.d.ts +0 -18
  103. package/dist/lib/brewer.d.ts +0 -9
  104. package/dist/lib/brewer.js +0 -25
  105. package/dist/lib/brewing/BoxBrewer.svelte.d.ts +0 -8
  106. package/dist/lib/brewing/BoxBrewer.svelte.js +0 -14
  107. package/dist/lib/brewing/CartesianBrewer.svelte.d.ts +0 -8
  108. package/dist/lib/brewing/CartesianBrewer.svelte.js +0 -21
  109. package/dist/lib/brewing/PieBrewer.svelte.d.ts +0 -8
  110. package/dist/lib/brewing/PieBrewer.svelte.js +0 -14
  111. package/dist/lib/brewing/QuartileBrewer.svelte.d.ts +0 -9
  112. package/dist/lib/brewing/QuartileBrewer.svelte.js +0 -51
  113. package/dist/lib/brewing/ViolinBrewer.svelte.d.ts +0 -8
  114. package/dist/lib/brewing/ViolinBrewer.svelte.js +0 -14
  115. package/dist/lib/brewing/axes.svelte.d.ts +0 -66
  116. package/dist/lib/brewing/axes.svelte.js +0 -270
  117. package/dist/lib/brewing/bars.svelte.d.ts +0 -56
  118. package/dist/lib/brewing/bars.svelte.js +0 -201
  119. package/dist/lib/brewing/brewer.svelte.d.ts +0 -124
  120. package/dist/lib/brewing/brewer.svelte.js +0 -277
  121. package/dist/lib/brewing/dimensions.svelte.d.ts +0 -35
  122. package/dist/lib/brewing/dimensions.svelte.js +0 -56
  123. package/dist/lib/brewing/index.svelte.d.ts +0 -118
  124. package/dist/lib/brewing/index.svelte.js +0 -205
  125. package/dist/lib/brewing/legends.svelte.d.ts +0 -48
  126. package/dist/lib/brewing/legends.svelte.js +0 -137
  127. package/dist/lib/brewing/marks/areas.d.ts +0 -31
  128. package/dist/lib/brewing/marks/areas.js +0 -72
  129. package/dist/lib/brewing/marks/bars.d.ts +0 -1
  130. package/dist/lib/brewing/marks/bars.js +0 -49
  131. package/dist/lib/brewing/scales.svelte.d.ts +0 -24
  132. package/dist/lib/brewing/scales.svelte.js +0 -82
  133. package/dist/lib/context.d.ts +0 -13
  134. package/dist/lib/context.js +0 -131
  135. package/dist/lib/plot/chartProps.d.ts +0 -177
  136. package/dist/lib/plot/chartProps.js +0 -76
  137. package/dist/lib/scales.svelte.d.ts +0 -35
  138. package/dist/lib/scales.svelte.js +0 -151
  139. package/dist/lib/ticks.d.ts +0 -36
  140. package/dist/lib/ticks.js +0 -46
@@ -1,8 +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
-
5
- type Node = { name: string; y: number; height: number }
4
+ import { GeomState } from './lib/GeomState.svelte.js'
5
+ import { buildRibbonMarks } from './lib/marks/ribbon.js'
6
6
 
7
7
  type Options = {
8
8
  source?: string
@@ -15,141 +15,47 @@
15
15
  y?: string
16
16
  color?: string
17
17
  fill?: string
18
+ /** Fixed ribbon opacity 0–1; defaults to the per-geom preset (ribbon = 0.5). */
19
+ alpha?: number
18
20
  stat?: string
19
21
  options?: Options
20
22
  }
21
23
 
22
- let {
23
- x = undefined,
24
- y = undefined,
25
- color = undefined,
26
- fill: fillProp = undefined,
27
- stat = 'identity',
28
- options = {}
29
- }: Props = $props()
24
+ let { x, y, color, fill, alpha, stat = 'identity', options = {} }: Props = $props()
30
25
 
26
+ const colorField = $derived(fill ?? color)
31
27
  const sourceField = $derived(options.source ?? 'source')
32
28
  const targetField = $derived(options.target ?? 'target')
33
29
  const valueField = $derived(options.value ?? 'value')
34
- const colorChannel = $derived(fillProp ?? color)
35
-
36
30
  const plotState = getContext<PlotState>('plot-state')
37
- let id = $state<string | null>(null)
38
-
39
- onMount(() => {
40
- id = plotState.registerGeom({
41
- type: 'ribbon',
42
- channels: { x, y, color: colorChannel },
43
- stat
44
- })
45
- })
46
- onDestroy(() => {
47
- if (id) plotState.unregisterGeom(id)
48
- })
49
-
50
- $effect(() => {
51
- if (id)
52
- plotState.updateGeom(id, {
53
- channels: { x, y, color: colorChannel },
54
- stat
55
- })
56
- })
57
-
58
- const data = $derived(id ? plotState.geomData(id) : [])
59
- const colors = $derived(plotState.colors)
60
- const innerHeight = $derived(plotState.innerHeight)
61
- const innerWidth = $derived(plotState.innerWidth)
62
-
63
- // Compute Sankey-style layout
64
- // eslint-disable-next-line max-lines-per-function, complexity
65
- const ribbons = $derived.by(() => {
66
- if (!data?.length) return { links: [], sourceNodes: [], targetNodes: [] }
67
-
68
- // Aggregate flows
69
- const flows = data.map((d) => ({
70
- source: String(d[sourceField]),
71
- target: String(d[targetField]),
72
- value: Number(d[valueField]) || 0,
73
- data: d
74
- }))
75
-
76
- // Compute node positions
77
- // eslint-disable-next-line svelte/prefer-svelte-reactivity
78
- const sourceMap = new Map<string, number>()
79
- // eslint-disable-next-line svelte/prefer-svelte-reactivity
80
- const targetMap = new Map<string, number>()
81
- for (const f of flows) {
82
- sourceMap.set(f.source, (sourceMap.get(f.source) ?? 0) + f.value)
83
- targetMap.set(f.target, (targetMap.get(f.target) ?? 0) + f.value)
84
- }
85
-
86
- const totalSource = Math.max(1, [...sourceMap.values()].reduce((s, v) => s + v, 0))
87
- const totalTarget = Math.max(1, [...targetMap.values()].reduce((s, v) => s + v, 0))
88
- const padding = 4
89
- const availHeight = innerHeight - padding * (Math.max(sourceMap.size, targetMap.size) - 1)
90
-
91
- // Position source nodes on left
92
- let sourceY = 0
93
- const sourceNodes: Node[] = []
94
- for (const [name, value] of sourceMap) {
95
- const h = (value / totalSource) * availHeight
96
- sourceNodes.push({ name, y: sourceY, height: h })
97
- sourceY += h + padding
98
- }
99
-
100
- // Position target nodes on right
101
- let targetY = 0
102
- const targetNodes: Node[] = []
103
- for (const [name, value] of targetMap) {
104
- const h = (value / totalTarget) * availHeight
105
- targetNodes.push({ name, y: targetY, height: h })
106
- targetY += h + padding
107
- }
108
-
109
- // Build ribbon paths
110
- // eslint-disable-next-line svelte/prefer-svelte-reactivity
111
- const sourceOffsets = new Map(sourceNodes.map((n) => [n.name, n.y]))
112
- // eslint-disable-next-line svelte/prefer-svelte-reactivity
113
- const targetOffsets = new Map(targetNodes.map((n) => [n.name, n.y]))
114
- const x0 = 0
115
- const x1 = innerWidth
116
-
117
- // eslint-disable-next-line complexity
118
- const links = flows.map((f, i) => {
119
- const sy0 = sourceOffsets.get(f.source) ?? 0
120
- const ty0 = targetOffsets.get(f.target) ?? 0
121
- const sourceTotal = sourceMap.get(f.source) ?? 1
122
- const targetTotal = targetMap.get(f.target) ?? 1
123
- const sh = (f.value / sourceTotal) * (sourceNodes.find((n) => n.name === f.source)?.height ?? 0)
124
- const th = (f.value / targetTotal) * (targetNodes.find((n) => n.name === f.target)?.height ?? 0)
125
-
126
- const path = `M${x0},${sy0} C${innerWidth * 0.4},${sy0} ${innerWidth * 0.6},${ty0} ${x1},${ty0} L${x1},${ty0 + th} C${innerWidth * 0.6},${ty0 + th} ${innerWidth * 0.4},${sy0 + sh} ${x0},${sy0 + sh} Z`
127
-
128
- // Update offsets for stacking
129
- sourceOffsets.set(f.source, sy0 + sh)
130
- targetOffsets.set(f.target, ty0 + th)
131
-
132
- const fillColor = colors?.get(f.source)?.fill ?? colors?.get(f.target)?.fill ?? '#888'
133
-
134
- return {
135
- key: `ribbon-${i}`,
136
- d: path,
137
- fill: fillColor,
138
- data: f.data
139
- }
140
- })
141
31
 
142
- return { links, sourceNodes, targetNodes }
143
- })
32
+ const geom = new GeomState(plotState, () => ({
33
+ type: 'ribbon',
34
+ channels: { x, y, color: colorField },
35
+ stat,
36
+ options,
37
+ alpha,
38
+ build: buildRibbonMarks
39
+ }))
40
+ onMount(geom.register)
41
+ onDestroy(geom.destroy)
42
+ $effect(geom.sync)
43
+
44
+ // GeomState returns [] for empty data; buildRibbonMarks otherwise returns
45
+ // { links, sourceNodes, targetNodes } — normalize each to an array.
46
+ const marks = $derived(geom.marks)
47
+ const links = $derived(marks?.links ?? [])
48
+ const sourceNodes = $derived(marks?.sourceNodes ?? [])
49
+ const targetNodes = $derived(marks?.targetNodes ?? [])
144
50
  </script>
145
51
 
146
- {#if ribbons.links.length > 0}
52
+ {#if links.length > 0}
147
53
  <g data-plot-geom="ribbon">
148
- {#each ribbons.links as link (link.key)}
54
+ {#each links as link (link.key)}
149
55
  <path
150
56
  d={link.d}
151
57
  fill={link.fill}
152
- opacity="0.5"
58
+ opacity={link.alpha}
153
59
  data-plot-element="ribbon"
154
60
  role="img"
155
61
  onmouseenter={() => plotState.setHovered(link.data)}
@@ -159,7 +65,7 @@
159
65
  </path>
160
66
  {/each}
161
67
  <!-- Source node labels -->
162
- {#each ribbons.sourceNodes as node (node.name)}
68
+ {#each sourceNodes as node (node.name)}
163
69
  <text
164
70
  x="-4"
165
71
  y={node.y + node.height / 2}
@@ -171,9 +77,9 @@
171
77
  >{node.name}</text>
172
78
  {/each}
173
79
  <!-- Target node labels -->
174
- {#each ribbons.targetNodes as node (node.name)}
80
+ {#each targetNodes as node (node.name)}
175
81
  <text
176
- x={innerWidth + 4}
82
+ x={plotState.innerWidth + 4}
177
83
  y={node.y + node.height / 2}
178
84
  text-anchor="start"
179
85
  dominant-baseline="middle"
@@ -8,6 +8,8 @@ type Props = {
8
8
  y?: string;
9
9
  color?: string;
10
10
  fill?: string;
11
+ /** Fixed ribbon opacity 0–1; defaults to the per-geom preset (ribbon = 0.5). */
12
+ alpha?: number;
11
13
  stat?: string;
12
14
  options?: Options;
13
15
  };
@@ -8,6 +8,9 @@
8
8
  x?: RuleValue | RuleValue[]
9
9
  /** Horizontal reference line(s) at these y-axis value(s). */
10
10
  y?: RuleValue | RuleValue[]
11
+ /** Line color (aligns with the other geoms' `color` aesthetic). */
12
+ color?: string
13
+ /** @deprecated use `color` — kept as an alias. */
11
14
  stroke?: string
12
15
  /** SVG stroke-dasharray (default dashed). Pass '' for a solid line. */
13
16
  dash?: string
@@ -19,6 +22,7 @@
19
22
  let {
20
23
  x,
21
24
  y,
25
+ color = undefined,
22
26
  stroke = undefined,
23
27
  dash = '4 4',
24
28
  strokeWidth = 1,
@@ -79,7 +83,7 @@
79
83
  return out
80
84
  })
81
85
 
82
- const strokeColor = $derived(stroke ?? 'currentColor')
86
+ const strokeColor = $derived(color ?? stroke ?? 'currentColor')
83
87
  </script>
84
88
 
85
89
  {#if rules.length > 0}
@@ -4,6 +4,9 @@ type Props = {
4
4
  x?: RuleValue | RuleValue[];
5
5
  /** Horizontal reference line(s) at these y-axis value(s). */
6
6
  y?: RuleValue | RuleValue[];
7
+ /** Line color (aligns with the other geoms' `color` aesthetic). */
8
+ color?: string;
9
+ /** @deprecated use `color` — kept as an alias. */
7
10
  stroke?: string;
8
11
  /** SVG stroke-dasharray (default dashed). Pass '' for a solid line. */
9
12
  dash?: string;
@@ -1,25 +1,29 @@
1
1
  <script lang="ts">
2
2
  import { getContext, onMount, onDestroy } from 'svelte'
3
3
  import type { PlotState } from '../PlotState.svelte.js'
4
- import { buildViolins } from '../lib/brewing/marks/violins.js'
4
+ import { GeomState } from './lib/GeomState.svelte.js'
5
+ import { buildViolinMarks } from './lib/marks/violin.js'
5
6
 
6
7
  type Props = {
7
8
  x?: string
8
9
  y?: string
10
+ /** Violin interior aesthetic (field); defaults to `x`. */
9
11
  fill?: string
12
+ /** Outline aesthetic (field); overrides the silhouette stroke when set. */
13
+ color?: string
10
14
  stat?: string
11
15
  /** Half-violin: 'left'/'right' draw one side (flat edge at the band centre) for
12
16
  * raincloud/split plots; 'center' (default) is the full symmetric silhouette. */
13
17
  side?: 'left' | 'right' | 'center'
14
18
  /** Data field driving a texture fill (from the shared pattern set) instead of a solid fill. */
15
19
  pattern?: string
16
- options?: { opacity?: number }
20
+ /** Fixed violin opacity 0–1; defaults to the per-geom preset (violin = 0.5). */
21
+ alpha?: number
17
22
  }
18
23
 
19
- let { x, y, fill, stat = 'boxplot', side = 'center', pattern, options = {} }: Props = $props()
24
+ let { x, y, fill, color, stat = 'boxplot', side = 'center', pattern, alpha }: Props = $props()
20
25
 
21
26
  const plotState = getContext<PlotState>('plot-state')
22
- let id = $state<string | null>(null)
23
27
 
24
28
  // Fall back to the root Plot channels when the geom omits x/y (composes under
25
29
  // <Plot.Root x y> without silently NaN-ing).
@@ -28,42 +32,19 @@
28
32
  // fill ?? effective-x drives the colors map for both violin interior and outline
29
33
  const fillChannel = $derived(fill ?? xf)
30
34
 
31
- onMount(() => {
32
- id = plotState.registerGeom({
33
- type: 'violin',
34
- channels: { x: xf, y: yf, color: fillChannel, pattern },
35
- stat,
36
- options
37
- })
38
- })
39
- onDestroy(() => {
40
- if (id) plotState.unregisterGeom(id)
41
- })
35
+ const geom = new GeomState(plotState, () => ({
36
+ type: 'violin',
37
+ channels: { x: xf, y: yf, fill: fillChannel, color, pattern },
38
+ stat,
39
+ options: { side },
40
+ alpha,
41
+ build: buildViolinMarks
42
+ }))
43
+ onMount(geom.register)
44
+ onDestroy(geom.destroy)
45
+ $effect(geom.sync)
42
46
 
43
- $effect(() => {
44
- if (id)
45
- plotState.updateGeom(id, { channels: { x: xf, y: yf, color: fillChannel, pattern }, stat })
46
- })
47
-
48
- const data = $derived(id ? plotState.geomData(id) : [])
49
- const xScale = $derived(plotState.xScale)
50
- const yScale = $derived(plotState.yScale)
51
- const colors = $derived(plotState.colors)
52
- const patterns = $derived(plotState.patterns)
53
-
54
- const violins = $derived.by(() => {
55
- if (!data?.length || !xScale || !yScale) return []
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
- )
66
- })
47
+ const violins = $derived(geom.marks)
67
48
  </script>
68
49
 
69
50
  {#if violins.length > 0}
@@ -72,7 +53,7 @@
72
53
  <path
73
54
  d={v.d}
74
55
  fill={v.fill}
75
- fill-opacity={options?.opacity ?? plotState.chartPreset.opacity.violin}
56
+ fill-opacity={v.alpha}
76
57
  stroke={v.stroke}
77
58
  stroke-width="1.5"
78
59
  data-plot-element="violin"
@@ -1,16 +1,18 @@
1
1
  type Props = {
2
2
  x?: string;
3
3
  y?: string;
4
+ /** Violin interior aesthetic (field); defaults to `x`. */
4
5
  fill?: string;
6
+ /** Outline aesthetic (field); overrides the silhouette stroke when set. */
7
+ color?: string;
5
8
  stat?: string;
6
9
  /** Half-violin: 'left'/'right' draw one side (flat edge at the band centre) for
7
10
  * raincloud/split plots; 'center' (default) is the full symmetric silhouette. */
8
11
  side?: 'left' | 'right' | 'center';
9
12
  /** Data field driving a texture fill (from the shared pattern set) instead of a solid fill. */
10
13
  pattern?: string;
11
- options?: {
12
- opacity?: number;
13
- };
14
+ /** Fixed violin opacity 0–1; defaults to the per-geom preset (violin = 0.5). */
15
+ alpha?: number;
14
16
  };
15
17
  declare const Violin: import("svelte").Component<Props, {}, "">;
16
18
  type Violin = ReturnType<typeof Violin>;
@@ -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,95 +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
- // 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)
93
- return {
94
- key: `${xVal}-${i}`,
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)),
99
- fill,
100
- bandStart: xPos,
101
- bandEnd: xPos + bw,
102
- cumY: yScale(cumulative) ?? 0,
103
- data: d
104
- }
105
- })
106
- })
44
+ const bars = $derived(geom.marks)
107
45
  </script>
108
46
 
109
47
  {#if bars.length > 0}
@@ -115,6 +53,7 @@
115
53
  width={Math.max(0, bar.width)}
116
54
  height={bar.height}
117
55
  fill={bar.fill}
56
+ fill-opacity={bar.alpha}
118
57
  data-plot-element="waterfall-bar"
119
58
  role="img"
120
59
  onmouseenter={() => plotState.setHovered(bar.data)}
@@ -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;