@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.
- package/dist/AnimatedPlot.svelte +26 -8
- package/dist/AnimatedPlot.svelte.d.ts +5 -0
- package/dist/Chart.svelte +51 -65
- package/dist/FacetPlot.svelte +1 -1
- package/dist/Plot/Axis.svelte +19 -10
- package/dist/Plot/Root.svelte +67 -46
- package/dist/Plot/Root.svelte.d.ts +29 -2
- package/dist/Plot/Timeline.svelte +39 -13
- package/dist/Plot/index.d.ts +5 -5
- package/dist/Plot/index.js +5 -5
- package/dist/Plot.svelte +95 -178
- package/dist/Plot.svelte.d.ts +4 -0
- package/dist/PlotState.svelte.d.ts +29 -1
- package/dist/PlotState.svelte.js +163 -19
- package/dist/PlotSurface.svelte +162 -0
- package/dist/PlotSurface.svelte.d.ts +25 -0
- package/dist/charts/AreaChart.svelte +12 -2
- package/dist/charts/AreaChart.svelte.d.ts +6 -0
- package/dist/charts/BarChart.svelte +40 -4
- package/dist/charts/BarChart.svelte.d.ts +15 -1
- package/dist/charts/BoxPlot.svelte +7 -3
- package/dist/charts/BoxPlot.svelte.d.ts +2 -0
- package/dist/charts/BubbleChart.svelte +37 -4
- package/dist/charts/BubbleChart.svelte.d.ts +12 -1
- package/dist/charts/LineChart.svelte +4 -2
- package/dist/charts/LineChart.svelte.d.ts +1 -0
- package/dist/charts/PieChart.svelte +5 -3
- package/dist/charts/PieChart.svelte.d.ts +1 -0
- package/dist/charts/ScatterPlot.svelte +35 -4
- package/dist/charts/ScatterPlot.svelte.d.ts +11 -1
- package/dist/charts/ViolinPlot.svelte +7 -3
- package/dist/charts/ViolinPlot.svelte.d.ts +2 -0
- package/dist/examples/BarChartExample.svelte +1 -1
- package/dist/geoms/Arc.svelte +21 -54
- 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 -45
- package/dist/geoms/Bar.svelte.d.ts +9 -0
- package/dist/geoms/Box.svelte +103 -61
- package/dist/geoms/Box.svelte.d.ts +12 -3
- package/dist/geoms/Candlestick.svelte +26 -75
- 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 +67 -0
- package/dist/geoms/Jitter.svelte.d.ts +17 -0
- package/dist/geoms/Line.svelte +27 -31
- package/dist/geoms/Line.svelte.d.ts +5 -0
- package/dist/geoms/Point.svelte +31 -60
- 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 +117 -0
- package/dist/geoms/Rule.svelte.d.ts +19 -0
- package/dist/geoms/Violin.svelte +36 -31
- package/dist/geoms/Violin.svelte.d.ts +10 -3
- package/dist/geoms/Waterfall.svelte +25 -79
- 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 +6 -2
- package/dist/geoms/lib/areas.js +20 -12
- package/dist/geoms/lib/bars.d.ts +18 -3
- package/dist/geoms/lib/bars.js +80 -101
- 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 +25 -12
- package/dist/index.js +26 -13
- package/dist/lib/brewing/marks/arcs.js +7 -1
- package/dist/lib/brewing/marks/boxes.d.ts +11 -2
- package/dist/lib/brewing/marks/boxes.js +32 -11
- package/dist/lib/brewing/marks/lines.d.ts +4 -1
- package/dist/lib/brewing/marks/lines.js +18 -15
- package/dist/lib/brewing/marks/swarm.d.ts +32 -0
- package/dist/lib/brewing/marks/swarm.js +125 -0
- package/dist/lib/brewing/marks/violins.d.ts +4 -20
- package/dist/lib/brewing/marks/violins.js +41 -25
- package/dist/lib/brewing/stats.d.ts +3 -1
- package/dist/lib/brewing/stats.js +28 -11
- package/dist/lib/plot/scales.js +12 -7
- package/dist/lib/plot/types.d.ts +9 -1
- package/dist/lib/plot/types.js +3 -1
- 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/Plot/Area.svelte +0 -54
- package/dist/Plot/Area.svelte.d.ts +0 -14
- package/dist/Plot/Bar.svelte +0 -69
- package/dist/Plot/Bar.svelte.d.ts +0 -11
- package/dist/Plot/Line.svelte +0 -54
- package/dist/Plot/Line.svelte.d.ts +0 -13
- package/dist/Plot/Point.svelte +0 -58
- package/dist/Plot/Point.svelte.d.ts +0 -13
- 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/Point.svelte
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { getContext, onMount, onDestroy } from 'svelte'
|
|
3
|
-
import { scaleSqrt } from 'd3-scale'
|
|
4
3
|
import type { PlotState } from '../PlotState.svelte.js'
|
|
5
|
-
import {
|
|
4
|
+
import { GeomState } from './lib/GeomState.svelte.js'
|
|
5
|
+
import { buildPointMarks } from './lib/marks/point.js'
|
|
6
6
|
import { keyboardNav } from '../lib/keyboard-nav.js'
|
|
7
7
|
import { buildSelectDetail } from '../lib/select.js'
|
|
8
8
|
import LabelPill from './LabelPill.svelte'
|
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
minRadius?: number
|
|
13
13
|
maxRadius?: number
|
|
14
14
|
radius?: number
|
|
15
|
-
opacity?: number
|
|
16
15
|
jitter?: { width?: number; height?: number } | null
|
|
17
16
|
labelOffset?: { x?: number; y?: number }
|
|
18
17
|
}
|
|
@@ -20,9 +19,14 @@
|
|
|
20
19
|
type Props = {
|
|
21
20
|
x?: string
|
|
22
21
|
y?: string
|
|
22
|
+
/** Outline / point-stroke aesthetic (field). */
|
|
23
23
|
color?: string
|
|
24
|
+
/** Interior aesthetic (field); falls back to `color` when omitted. */
|
|
25
|
+
fill?: string
|
|
24
26
|
size?: string
|
|
25
27
|
symbol?: string
|
|
28
|
+
/** Fixed mark opacity 0–1; defaults to the per-geom preset (point = 0.8). */
|
|
29
|
+
alpha?: number
|
|
26
30
|
label?: boolean | string | ((data: Row) => unknown)
|
|
27
31
|
stat?: string
|
|
28
32
|
options?: Options
|
|
@@ -34,8 +38,10 @@
|
|
|
34
38
|
x,
|
|
35
39
|
y,
|
|
36
40
|
color,
|
|
41
|
+
fill,
|
|
37
42
|
size,
|
|
38
43
|
symbol: symbolField,
|
|
44
|
+
alpha,
|
|
39
45
|
label = false,
|
|
40
46
|
stat = 'identity',
|
|
41
47
|
options = {},
|
|
@@ -51,7 +57,24 @@
|
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
const plotState = getContext<PlotState>('plot-state')
|
|
54
|
-
|
|
60
|
+
|
|
61
|
+
// All plumbing (register/data/scales/colors) + mark building (geometry + fill/color/alpha)
|
|
62
|
+
// lives in GeomState + buildPointMarks. This component only renders + handles interactivity.
|
|
63
|
+
const geom = new GeomState(plotState, () => ({
|
|
64
|
+
type: 'point',
|
|
65
|
+
channels: { x, y, color, fill, size, symbol: symbolField },
|
|
66
|
+
stat,
|
|
67
|
+
options,
|
|
68
|
+
alpha,
|
|
69
|
+
build: buildPointMarks
|
|
70
|
+
}))
|
|
71
|
+
onMount(geom.register)
|
|
72
|
+
onDestroy(geom.destroy)
|
|
73
|
+
$effect(geom.sync)
|
|
74
|
+
|
|
75
|
+
const points = $derived(geom.marks)
|
|
76
|
+
|
|
77
|
+
const seriesField = $derived(fill ?? color)
|
|
55
78
|
|
|
56
79
|
function selectPoint(pt: { data: Row }, event: MouseEvent | KeyboardEvent) {
|
|
57
80
|
onselect?.(pt.data)
|
|
@@ -62,69 +85,17 @@
|
|
|
62
85
|
plotState.data.indexOf(pt.data),
|
|
63
86
|
{ x, y },
|
|
64
87
|
'point',
|
|
65
|
-
|
|
88
|
+
seriesField ? pt.data[seriesField] : undefined,
|
|
66
89
|
event
|
|
67
90
|
)
|
|
68
91
|
)
|
|
69
92
|
}
|
|
70
|
-
|
|
71
|
-
onMount(() => {
|
|
72
|
-
id = plotState.registerGeom({
|
|
73
|
-
type: 'point',
|
|
74
|
-
channels: { x, y, color, size, symbol: symbolField },
|
|
75
|
-
stat,
|
|
76
|
-
options
|
|
77
|
-
})
|
|
78
|
-
})
|
|
79
|
-
onDestroy(() => {
|
|
80
|
-
if (id) plotState.unregisterGeom(id)
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
$effect(() => {
|
|
84
|
-
if (id) plotState.updateGeom(id, { channels: { x, y, color, size, symbol: symbolField }, stat })
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
const data = $derived(id ? plotState.geomData(id) : [])
|
|
88
|
-
const xScale = $derived(plotState.xScale)
|
|
89
|
-
const yScale = $derived(plotState.yScale)
|
|
90
|
-
const colors = $derived(plotState.colors)
|
|
91
|
-
const symbolMap = $derived(plotState.symbols)
|
|
92
|
-
|
|
93
|
-
function buildSizeScale() {
|
|
94
|
-
if (!size || !data?.length) return null
|
|
95
|
-
const vals = data.map((d) => Number(d[size])).filter((v) => !isNaN(v))
|
|
96
|
-
if (!vals.length) return null
|
|
97
|
-
const minVal = Math.min(...vals)
|
|
98
|
-
const maxVal = Math.max(...vals)
|
|
99
|
-
const minRadius = options.minRadius ?? 3
|
|
100
|
-
const maxRadius = options.maxRadius ?? 20
|
|
101
|
-
return scaleSqrt().domain([minVal, maxVal]).range([minRadius, maxRadius])
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const sizeScale = $derived.by(() => buildSizeScale())
|
|
105
|
-
|
|
106
|
-
const defaultRadius = $derived(options.radius ?? 4)
|
|
107
|
-
|
|
108
|
-
const points = $derived.by(() => {
|
|
109
|
-
if (!data?.length || !xScale || !yScale) return []
|
|
110
|
-
return buildPoints(
|
|
111
|
-
data,
|
|
112
|
-
{ x, y, color, size, symbol: symbolField },
|
|
113
|
-
xScale,
|
|
114
|
-
yScale,
|
|
115
|
-
colors,
|
|
116
|
-
sizeScale,
|
|
117
|
-
symbolMap,
|
|
118
|
-
defaultRadius,
|
|
119
|
-
options?.jitter ?? null
|
|
120
|
-
)
|
|
121
|
-
})
|
|
122
93
|
</script>
|
|
123
94
|
|
|
124
95
|
{#if points.length > 0}
|
|
125
96
|
<g data-plot-geom="point">
|
|
126
97
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
127
|
-
{#each points as pt
|
|
98
|
+
{#each points as pt (pt.key)}
|
|
128
99
|
{#if pt.symbolPath}
|
|
129
100
|
<path
|
|
130
101
|
transform="translate({pt.cx},{pt.cy})"
|
|
@@ -132,7 +103,7 @@
|
|
|
132
103
|
fill={pt.fill}
|
|
133
104
|
stroke={pt.stroke}
|
|
134
105
|
stroke-width="1"
|
|
135
|
-
fill-opacity={
|
|
106
|
+
fill-opacity={pt.alpha}
|
|
136
107
|
data-plot-element="point"
|
|
137
108
|
role={onselect || keyboard || plotState.interactive ? 'button' : 'graphics-symbol'}
|
|
138
109
|
tabindex={onselect || keyboard || plotState.interactive ? 0 : undefined}
|
|
@@ -154,7 +125,7 @@
|
|
|
154
125
|
fill={pt.fill}
|
|
155
126
|
stroke={pt.stroke}
|
|
156
127
|
stroke-width="1"
|
|
157
|
-
fill-opacity={
|
|
128
|
+
fill-opacity={pt.alpha}
|
|
158
129
|
data-plot-element="point"
|
|
159
130
|
role={onselect || keyboard || plotState.interactive ? 'button' : 'graphics-symbol'}
|
|
160
131
|
tabindex={onselect || keyboard || plotState.interactive ? 0 : undefined}
|
|
@@ -3,7 +3,6 @@ type Options = {
|
|
|
3
3
|
minRadius?: number;
|
|
4
4
|
maxRadius?: number;
|
|
5
5
|
radius?: number;
|
|
6
|
-
opacity?: number;
|
|
7
6
|
jitter?: {
|
|
8
7
|
width?: number;
|
|
9
8
|
height?: number;
|
|
@@ -16,9 +15,14 @@ type Options = {
|
|
|
16
15
|
type Props = {
|
|
17
16
|
x?: string;
|
|
18
17
|
y?: string;
|
|
18
|
+
/** Outline / point-stroke aesthetic (field). */
|
|
19
19
|
color?: string;
|
|
20
|
+
/** Interior aesthetic (field); falls back to `color` when omitted. */
|
|
21
|
+
fill?: string;
|
|
20
22
|
size?: string;
|
|
21
23
|
symbol?: string;
|
|
24
|
+
/** Fixed mark opacity 0–1; defaults to the per-geom preset (point = 0.8). */
|
|
25
|
+
alpha?: number;
|
|
22
26
|
label?: boolean | string | ((data: Row) => unknown);
|
|
23
27
|
stat?: string;
|
|
24
28
|
options?: Options;
|
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"
|
|
@@ -0,0 +1,117 @@
|
|
|
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
|
+
/** Line color (aligns with the other geoms' `color` aesthetic). */
|
|
12
|
+
color?: string
|
|
13
|
+
/** @deprecated use `color` — kept as an alias. */
|
|
14
|
+
stroke?: string
|
|
15
|
+
/** SVG stroke-dasharray (default dashed). Pass '' for a solid line. */
|
|
16
|
+
dash?: string
|
|
17
|
+
strokeWidth?: number
|
|
18
|
+
/** Optional label drawn at the line's end. String, or a fn of the value. */
|
|
19
|
+
label?: string | ((value: RuleValue) => string)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let {
|
|
23
|
+
x,
|
|
24
|
+
y,
|
|
25
|
+
color = undefined,
|
|
26
|
+
stroke = undefined,
|
|
27
|
+
dash = '4 4',
|
|
28
|
+
strokeWidth = 1,
|
|
29
|
+
label = undefined
|
|
30
|
+
}: Props = $props()
|
|
31
|
+
|
|
32
|
+
const plotState = getContext<PlotState>('plot-state')
|
|
33
|
+
const xScale = $derived(plotState.xScale)
|
|
34
|
+
const yScale = $derived(plotState.yScale)
|
|
35
|
+
|
|
36
|
+
const toArray = (v: RuleValue | RuleValue[] | undefined): RuleValue[] =>
|
|
37
|
+
v === undefined ? [] : Array.isArray(v) ? v : [v]
|
|
38
|
+
|
|
39
|
+
// Position along an axis: band scales resolve to the band centre, linear scales map directly.
|
|
40
|
+
function axisPos(scale: { (v: unknown): number | undefined; bandwidth?: () => number }, v: unknown) {
|
|
41
|
+
const base = scale(v) ?? 0
|
|
42
|
+
return typeof scale.bandwidth === 'function' ? base + scale.bandwidth() / 2 : base
|
|
43
|
+
}
|
|
44
|
+
// The full [min, max] screen extent of a scale's range (spans the perpendicular axis).
|
|
45
|
+
function axisExtent(scale: { range: () => number[] }) {
|
|
46
|
+
const r = scale.range()
|
|
47
|
+
return [Math.min(...r), Math.max(...r)]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const labelFor = (v: RuleValue) =>
|
|
51
|
+
typeof label === 'function' ? label(v) : typeof label === 'string' ? label : null
|
|
52
|
+
|
|
53
|
+
// Each rule is expressed in (x-channel, y-channel) space and placed to screen, so it
|
|
54
|
+
// transposes under orientation flip: a y-value reference is horizontal when vertical and
|
|
55
|
+
// vertical when flipped (it stays perpendicular to the value axis).
|
|
56
|
+
const rules = $derived.by(() => {
|
|
57
|
+
if (!xScale || !yScale) return []
|
|
58
|
+
const place = plotState.place.bind(plotState)
|
|
59
|
+
const out: Array<{
|
|
60
|
+
key: string
|
|
61
|
+
x1: number
|
|
62
|
+
y1: number
|
|
63
|
+
x2: number
|
|
64
|
+
y2: number
|
|
65
|
+
value: RuleValue
|
|
66
|
+
text: string | null
|
|
67
|
+
}> = []
|
|
68
|
+
const [xLo, xHi] = axisExtent(xScale)
|
|
69
|
+
const [yLo, yHi] = axisExtent(yScale)
|
|
70
|
+
|
|
71
|
+
for (const yv of toArray(y)) {
|
|
72
|
+
const yc = axisPos(yScale, yv)
|
|
73
|
+
const a = place(xLo, yc)
|
|
74
|
+
const b = place(xHi, yc)
|
|
75
|
+
out.push({ key: `y-${yv}`, x1: a.x, y1: a.y, x2: b.x, y2: b.y, value: yv, text: labelFor(yv) })
|
|
76
|
+
}
|
|
77
|
+
for (const xv of toArray(x)) {
|
|
78
|
+
const xc = axisPos(xScale, xv)
|
|
79
|
+
const a = place(xc, yLo)
|
|
80
|
+
const b = place(xc, yHi)
|
|
81
|
+
out.push({ key: `x-${xv}`, x1: a.x, y1: a.y, x2: b.x, y2: b.y, value: xv, text: labelFor(xv) })
|
|
82
|
+
}
|
|
83
|
+
return out
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const strokeColor = $derived(color ?? stroke ?? 'currentColor')
|
|
87
|
+
</script>
|
|
88
|
+
|
|
89
|
+
{#if rules.length > 0}
|
|
90
|
+
<g data-plot-geom="rule">
|
|
91
|
+
{#each rules as rule (rule.key)}
|
|
92
|
+
<line
|
|
93
|
+
x1={rule.x1}
|
|
94
|
+
y1={rule.y1}
|
|
95
|
+
x2={rule.x2}
|
|
96
|
+
y2={rule.y2}
|
|
97
|
+
stroke={strokeColor}
|
|
98
|
+
stroke-width={strokeWidth}
|
|
99
|
+
stroke-dasharray={dash || undefined}
|
|
100
|
+
data-plot-element="rule"
|
|
101
|
+
data-plot-value={rule.value}
|
|
102
|
+
/>
|
|
103
|
+
{#if rule.text}
|
|
104
|
+
<text
|
|
105
|
+
x={rule.x2}
|
|
106
|
+
y={rule.y2}
|
|
107
|
+
dx="-4"
|
|
108
|
+
dy="-4"
|
|
109
|
+
text-anchor="end"
|
|
110
|
+
font-size="10"
|
|
111
|
+
fill={strokeColor}
|
|
112
|
+
data-plot-element="rule-label"
|
|
113
|
+
>{rule.text}</text>
|
|
114
|
+
{/if}
|
|
115
|
+
{/each}
|
|
116
|
+
</g>
|
|
117
|
+
{/if}
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
/** Line color (aligns with the other geoms' `color` aesthetic). */
|
|
8
|
+
color?: string;
|
|
9
|
+
/** @deprecated use `color` — kept as an alias. */
|
|
10
|
+
stroke?: string;
|
|
11
|
+
/** SVG stroke-dasharray (default dashed). Pass '' for a solid line. */
|
|
12
|
+
dash?: string;
|
|
13
|
+
strokeWidth?: number;
|
|
14
|
+
/** Optional label drawn at the line's end. String, or a fn of the value. */
|
|
15
|
+
label?: string | ((value: RuleValue) => string);
|
|
16
|
+
};
|
|
17
|
+
declare const Rule: import("svelte").Component<Props, {}, "">;
|
|
18
|
+
type Rule = ReturnType<typeof Rule>;
|
|
19
|
+
export default Rule;
|
package/dist/geoms/Violin.svelte
CHANGED
|
@@ -1,49 +1,50 @@
|
|
|
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
|
|
16
|
+
* raincloud/split plots; 'center' (default) is the full symmetric silhouette. */
|
|
17
|
+
side?: 'left' | 'right' | 'center'
|
|
18
|
+
/** Data field driving a texture fill (from the shared pattern set) instead of a solid fill. */
|
|
19
|
+
pattern?: string
|
|
20
|
+
/** Fixed violin opacity 0–1; defaults to the per-geom preset (violin = 0.5). */
|
|
21
|
+
alpha?: number
|
|
12
22
|
}
|
|
13
23
|
|
|
14
|
-
let { x, y, fill, stat = 'boxplot',
|
|
24
|
+
let { x, y, fill, color, stat = 'boxplot', side = 'center', pattern, alpha }: Props = $props()
|
|
15
25
|
|
|
16
26
|
const plotState = getContext<PlotState>('plot-state')
|
|
17
|
-
let id = $state<string | null>(null)
|
|
18
27
|
|
|
19
|
-
//
|
|
20
|
-
|
|
28
|
+
// Fall back to the root Plot channels when the geom omits x/y (composes under
|
|
29
|
+
// <Plot.Root x y> without silently NaN-ing).
|
|
30
|
+
const xf = $derived(x ?? plotState.channels?.x)
|
|
31
|
+
const yf = $derived(y ?? plotState.channels?.y)
|
|
32
|
+
// fill ?? effective-x drives the colors map for both violin interior and outline
|
|
33
|
+
const fillChannel = $derived(fill ?? xf)
|
|
21
34
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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)
|
|
33
46
|
|
|
34
|
-
$
|
|
35
|
-
if (id) plotState.updateGeom(id, { channels: { x, y, color: fillChannel }, stat })
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
const data = $derived(id ? plotState.geomData(id) : [])
|
|
39
|
-
const xScale = $derived(plotState.xScale)
|
|
40
|
-
const yScale = $derived(plotState.yScale)
|
|
41
|
-
const colors = $derived(plotState.colors)
|
|
42
|
-
|
|
43
|
-
const violins = $derived.by(() => {
|
|
44
|
-
if (!data?.length || !xScale || !yScale) return []
|
|
45
|
-
return buildViolins(data, { x, fill: fillChannel }, xScale, yScale, colors)
|
|
46
|
-
})
|
|
47
|
+
const violins = $derived(geom.marks)
|
|
47
48
|
</script>
|
|
48
49
|
|
|
49
50
|
{#if violins.length > 0}
|
|
@@ -52,7 +53,7 @@
|
|
|
52
53
|
<path
|
|
53
54
|
d={v.d}
|
|
54
55
|
fill={v.fill}
|
|
55
|
-
fill-opacity={
|
|
56
|
+
fill-opacity={v.alpha}
|
|
56
57
|
stroke={v.stroke}
|
|
57
58
|
stroke-width="1.5"
|
|
58
59
|
data-plot-element="violin"
|
|
@@ -60,6 +61,10 @@
|
|
|
60
61
|
onmouseenter={() => plotState.setHovered(v.data)}
|
|
61
62
|
onmouseleave={() => plotState.clearHovered()}
|
|
62
63
|
/>
|
|
64
|
+
{#if v.patternId}
|
|
65
|
+
<!-- Texture fill overlay (matches the violin silhouette) -->
|
|
66
|
+
<path d={v.d} fill="url(#{v.patternId})" pointer-events="none" />
|
|
67
|
+
{/if}
|
|
63
68
|
{/each}
|
|
64
69
|
</g>
|
|
65
70
|
{/if}
|
|
@@ -1,11 +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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
/** Half-violin: 'left'/'right' draw one side (flat edge at the band centre) for
|
|
10
|
+
* raincloud/split plots; 'center' (default) is the full symmetric silhouette. */
|
|
11
|
+
side?: 'left' | 'right' | 'center';
|
|
12
|
+
/** Data field driving a texture fill (from the shared pattern set) instead of a solid fill. */
|
|
13
|
+
pattern?: string;
|
|
14
|
+
/** Fixed violin opacity 0–1; defaults to the per-geom preset (violin = 0.5). */
|
|
15
|
+
alpha?: number;
|
|
9
16
|
};
|
|
10
17
|
declare const Violin: import("svelte").Component<Props, {}, "">;
|
|
11
18
|
type Violin = ReturnType<typeof Violin>;
|