@rokkit/chart 1.3.8 → 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.
- package/dist/Chart.svelte +50 -64
- package/dist/Plot/Root.svelte +4 -1
- package/dist/Plot/Root.svelte.d.ts +2 -0
- package/dist/Plot/index.d.ts +1 -1
- package/dist/Plot/index.js +1 -1
- package/dist/PlotState.svelte.d.ts +14 -2
- package/dist/PlotState.svelte.js +46 -4
- package/dist/charts/AreaChart.svelte +8 -1
- package/dist/charts/AreaChart.svelte.d.ts +5 -0
- package/dist/charts/BarChart.svelte +8 -1
- package/dist/charts/BarChart.svelte.d.ts +5 -0
- package/dist/charts/BoxPlot.svelte +3 -1
- package/dist/charts/BoxPlot.svelte.d.ts +1 -0
- package/dist/charts/BubbleChart.svelte +3 -1
- package/dist/charts/BubbleChart.svelte.d.ts +1 -0
- package/dist/charts/LineChart.svelte +3 -1
- package/dist/charts/LineChart.svelte.d.ts +1 -0
- package/dist/charts/PieChart.svelte +4 -2
- package/dist/charts/PieChart.svelte.d.ts +1 -0
- package/dist/charts/ScatterPlot.svelte +3 -1
- package/dist/charts/ScatterPlot.svelte.d.ts +1 -0
- package/dist/charts/ViolinPlot.svelte +3 -1
- package/dist/charts/ViolinPlot.svelte.d.ts +1 -0
- package/dist/geoms/Arc.svelte +21 -55
- package/dist/geoms/Arc.svelte.d.ts +4 -4
- package/dist/geoms/Area.svelte +45 -44
- package/dist/geoms/Area.svelte.d.ts +8 -1
- package/dist/geoms/Bar.svelte +33 -56
- package/dist/geoms/Bar.svelte.d.ts +9 -0
- package/dist/geoms/Box.svelte +118 -131
- package/dist/geoms/Box.svelte.d.ts +5 -3
- package/dist/geoms/Candlestick.svelte +22 -77
- package/dist/geoms/Candlestick.svelte.d.ts +2 -0
- package/dist/geoms/Heatmap.svelte +21 -66
- package/dist/geoms/Heatmap.svelte.d.ts +2 -0
- package/dist/geoms/Hexbin.svelte +20 -100
- package/dist/geoms/Hexbin.svelte.d.ts +2 -0
- package/dist/geoms/Jitter.svelte +21 -36
- package/dist/geoms/Jitter.svelte.d.ts +5 -3
- package/dist/geoms/Line.svelte +27 -31
- package/dist/geoms/Line.svelte.d.ts +5 -0
- package/dist/geoms/Point.svelte +31 -66
- package/dist/geoms/Point.svelte.d.ts +5 -1
- package/dist/geoms/Ribbon.svelte +30 -124
- package/dist/geoms/Ribbon.svelte.d.ts +2 -0
- package/dist/geoms/Rule.svelte +5 -1
- package/dist/geoms/Rule.svelte.d.ts +3 -0
- package/dist/geoms/Violin.svelte +21 -40
- package/dist/geoms/Violin.svelte.d.ts +5 -3
- package/dist/geoms/Waterfall.svelte +19 -80
- package/dist/geoms/Waterfall.svelte.d.ts +2 -0
- package/dist/geoms/lib/GeomState.svelte.d.ts +51 -0
- package/dist/geoms/lib/GeomState.svelte.js +79 -0
- package/dist/geoms/lib/aesthetics.d.ts +36 -0
- package/dist/geoms/lib/aesthetics.js +45 -0
- package/dist/geoms/lib/areas.d.ts +1 -1
- package/dist/geoms/lib/areas.js +4 -2
- package/dist/geoms/lib/bars.d.ts +2 -2
- package/dist/geoms/lib/bars.js +13 -8
- package/dist/geoms/lib/marks/arc.d.ts +24 -0
- package/dist/geoms/lib/marks/arc.js +28 -0
- package/dist/geoms/lib/marks/area.d.ts +23 -0
- package/dist/geoms/lib/marks/area.js +38 -0
- package/dist/geoms/lib/marks/bar.d.ts +16 -0
- package/dist/geoms/lib/marks/bar.js +43 -0
- package/dist/geoms/lib/marks/box.d.ts +14 -0
- package/dist/geoms/lib/marks/box.js +30 -0
- package/dist/geoms/lib/marks/candlestick.d.ts +27 -0
- package/dist/geoms/lib/marks/candlestick.js +52 -0
- package/dist/geoms/lib/marks/heatmap.d.ts +24 -0
- package/dist/geoms/lib/marks/heatmap.js +41 -0
- package/dist/geoms/lib/marks/hexbin.d.ts +29 -0
- package/dist/geoms/lib/marks/hexbin.js +62 -0
- package/dist/geoms/lib/marks/jitter.d.ts +21 -0
- package/dist/geoms/lib/marks/jitter.js +36 -0
- package/dist/geoms/lib/marks/line.d.ts +26 -0
- package/dist/geoms/lib/marks/line.js +25 -0
- package/dist/geoms/lib/marks/point.d.ts +25 -0
- package/dist/geoms/lib/marks/point.js +58 -0
- package/dist/geoms/lib/marks/ribbon.d.ts +31 -0
- package/dist/geoms/lib/marks/ribbon.js +77 -0
- package/dist/geoms/lib/marks/violin.d.ts +13 -0
- package/dist/geoms/lib/marks/violin.js +31 -0
- package/dist/geoms/lib/marks/waterfall.d.ts +25 -0
- package/dist/geoms/lib/marks/waterfall.js +58 -0
- package/dist/index.d.ts +1 -8
- package/dist/index.js +1 -8
- package/dist/lib/preset.d.ts +11 -1
- package/dist/lib/preset.js +16 -4
- package/package.json +1 -1
- package/dist/Plot/Arc.svelte +0 -30
- package/dist/Plot/Arc.svelte.d.ts +0 -18
- package/dist/lib/brewer.d.ts +0 -9
- package/dist/lib/brewer.js +0 -25
- package/dist/lib/brewing/BoxBrewer.svelte.d.ts +0 -8
- package/dist/lib/brewing/BoxBrewer.svelte.js +0 -14
- package/dist/lib/brewing/CartesianBrewer.svelte.d.ts +0 -8
- package/dist/lib/brewing/CartesianBrewer.svelte.js +0 -21
- package/dist/lib/brewing/PieBrewer.svelte.d.ts +0 -8
- package/dist/lib/brewing/PieBrewer.svelte.js +0 -14
- package/dist/lib/brewing/QuartileBrewer.svelte.d.ts +0 -9
- package/dist/lib/brewing/QuartileBrewer.svelte.js +0 -51
- package/dist/lib/brewing/ViolinBrewer.svelte.d.ts +0 -8
- package/dist/lib/brewing/ViolinBrewer.svelte.js +0 -14
- package/dist/lib/brewing/axes.svelte.d.ts +0 -66
- package/dist/lib/brewing/axes.svelte.js +0 -270
- package/dist/lib/brewing/bars.svelte.d.ts +0 -56
- package/dist/lib/brewing/bars.svelte.js +0 -201
- package/dist/lib/brewing/brewer.svelte.d.ts +0 -124
- package/dist/lib/brewing/brewer.svelte.js +0 -277
- package/dist/lib/brewing/dimensions.svelte.d.ts +0 -35
- package/dist/lib/brewing/dimensions.svelte.js +0 -56
- package/dist/lib/brewing/index.svelte.d.ts +0 -118
- package/dist/lib/brewing/index.svelte.js +0 -205
- package/dist/lib/brewing/legends.svelte.d.ts +0 -48
- package/dist/lib/brewing/legends.svelte.js +0 -137
- package/dist/lib/brewing/marks/areas.d.ts +0 -31
- package/dist/lib/brewing/marks/areas.js +0 -72
- package/dist/lib/brewing/marks/bars.d.ts +0 -1
- package/dist/lib/brewing/marks/bars.js +0 -49
- package/dist/lib/brewing/scales.svelte.d.ts +0 -24
- package/dist/lib/brewing/scales.svelte.js +0 -82
- package/dist/lib/context.d.ts +0 -13
- package/dist/lib/context.js +0 -131
- package/dist/lib/plot/chartProps.d.ts +0 -177
- package/dist/lib/plot/chartProps.js +0 -76
- package/dist/lib/scales.svelte.d.ts +0 -35
- package/dist/lib/scales.svelte.js +0 -151
- package/dist/lib/ticks.d.ts +0 -36
- package/dist/lib/ticks.js +0 -46
package/dist/geoms/Ribbon.svelte
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
52
|
+
{#if links.length > 0}
|
|
147
53
|
<g data-plot-geom="ribbon">
|
|
148
|
-
{#each
|
|
54
|
+
{#each links as link (link.key)}
|
|
149
55
|
<path
|
|
150
56
|
d={link.d}
|
|
151
57
|
fill={link.fill}
|
|
152
|
-
opacity=
|
|
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
|
|
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
|
|
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"
|
package/dist/geoms/Rule.svelte
CHANGED
|
@@ -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;
|
package/dist/geoms/Violin.svelte
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
$
|
|
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={
|
|
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
|
-
|
|
12
|
-
|
|
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
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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)}
|
|
@@ -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;
|