@rokkit/chart 1.3.7 → 1.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AnimatedPlot.svelte +26 -8
- package/dist/AnimatedPlot.svelte.d.ts +5 -0
- package/dist/Chart.svelte +1 -1
- package/dist/FacetPlot.svelte +1 -1
- package/dist/Plot/Axis.svelte +19 -10
- package/dist/Plot/Root.svelte +64 -46
- package/dist/Plot/Root.svelte.d.ts +27 -2
- package/dist/Plot/Timeline.svelte +39 -13
- package/dist/Plot/index.d.ts +4 -4
- package/dist/Plot/index.js +4 -4
- package/dist/Plot.svelte +95 -178
- package/dist/Plot.svelte.d.ts +4 -0
- package/dist/PlotState.svelte.d.ts +16 -0
- package/dist/PlotState.svelte.js +118 -16
- package/dist/PlotSurface.svelte +162 -0
- package/dist/PlotSurface.svelte.d.ts +25 -0
- package/dist/charts/AreaChart.svelte +4 -1
- package/dist/charts/AreaChart.svelte.d.ts +1 -0
- package/dist/charts/BarChart.svelte +32 -3
- package/dist/charts/BarChart.svelte.d.ts +10 -1
- package/dist/charts/BoxPlot.svelte +4 -2
- package/dist/charts/BoxPlot.svelte.d.ts +1 -0
- package/dist/charts/BubbleChart.svelte +34 -3
- package/dist/charts/BubbleChart.svelte.d.ts +11 -1
- package/dist/charts/LineChart.svelte +1 -1
- package/dist/charts/PieChart.svelte +1 -1
- package/dist/charts/ScatterPlot.svelte +32 -3
- package/dist/charts/ScatterPlot.svelte.d.ts +10 -1
- package/dist/charts/ViolinPlot.svelte +4 -2
- package/dist/charts/ViolinPlot.svelte.d.ts +1 -0
- package/dist/examples/BarChartExample.svelte +1 -1
- package/dist/geoms/Arc.svelte +3 -2
- package/dist/geoms/Area.svelte +2 -2
- package/dist/geoms/Bar.svelte +17 -6
- package/dist/geoms/Box.svelte +131 -76
- package/dist/geoms/Box.svelte.d.ts +7 -0
- package/dist/geoms/Candlestick.svelte +17 -11
- package/dist/geoms/Jitter.svelte +82 -0
- package/dist/geoms/Jitter.svelte.d.ts +15 -0
- package/dist/geoms/Line.svelte +1 -1
- package/dist/geoms/Point.svelte +7 -1
- package/dist/geoms/Rule.svelte +113 -0
- package/dist/geoms/Rule.svelte.d.ts +16 -0
- package/dist/geoms/Violin.svelte +30 -6
- package/dist/geoms/Violin.svelte.d.ts +5 -0
- package/dist/geoms/Waterfall.svelte +16 -9
- package/dist/geoms/lib/areas.d.ts +6 -2
- package/dist/geoms/lib/areas.js +17 -11
- package/dist/geoms/lib/bars.d.ts +18 -3
- package/dist/geoms/lib/bars.js +70 -96
- package/dist/index.d.ts +24 -4
- package/dist/index.js +25 -5
- package/dist/lib/brewing/ViolinBrewer.svelte.d.ts +1 -1
- 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/package.json +1 -1
- 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/geoms/Bar.svelte
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { getContext, onMount, onDestroy } from 'svelte'
|
|
3
3
|
import type { PlotState } from '../PlotState.svelte.js'
|
|
4
4
|
import type { createCrossFilter } from '../crossfilter/createCrossFilter.svelte.js'
|
|
5
|
-
import {
|
|
5
|
+
import { buildBars, buildStackedBars } from './lib/bars.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'
|
|
@@ -102,13 +102,24 @@
|
|
|
102
102
|
const bars = $derived.by(() => {
|
|
103
103
|
if (!data?.length || !xScale || !yScale) return []
|
|
104
104
|
const channels = { x, y, color: colorChannel, pattern }
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
// One builder, one path: express bars in (category, value) space and map to screen via
|
|
106
|
+
// place() (identity vertical, swap when flipped). `continuousCategory` keeps a linear
|
|
107
|
+
// category axis (the bar-chart race's tweened rank) so it positions/tweens smoothly.
|
|
108
|
+
const place = plotState.place.bind(plotState)
|
|
108
109
|
if (options.stack) {
|
|
109
|
-
return buildStackedBars(data, channels, xScale, yScale, colors, innerHeight, patterns)
|
|
110
|
+
return buildStackedBars(data, channels, xScale, yScale, colors, innerHeight, patterns, place)
|
|
110
111
|
}
|
|
111
|
-
return
|
|
112
|
+
return buildBars(
|
|
113
|
+
data,
|
|
114
|
+
channels,
|
|
115
|
+
xScale,
|
|
116
|
+
yScale,
|
|
117
|
+
colors,
|
|
118
|
+
innerHeight,
|
|
119
|
+
patterns,
|
|
120
|
+
place,
|
|
121
|
+
plotState.continuousCategory
|
|
122
|
+
)
|
|
112
123
|
})
|
|
113
124
|
|
|
114
125
|
let dimmedByKey = $state<Record<string, boolean>>({})
|
package/dist/geoms/Box.svelte
CHANGED
|
@@ -8,21 +8,32 @@
|
|
|
8
8
|
y?: string
|
|
9
9
|
fill?: string
|
|
10
10
|
stat?: string
|
|
11
|
+
/** Offset the box to one half of the band (raincloud/split plots). Default 'center'. */
|
|
12
|
+
side?: 'left' | 'right' | 'center'
|
|
13
|
+
/** Box thickness as a fraction of the (sub-)band — use a small value (e.g. 0.16) for a
|
|
14
|
+
* thin raincloud box. Defaults to the full box width. */
|
|
15
|
+
width?: number
|
|
16
|
+
/** Data field driving a texture fill (from the shared pattern set) instead of a solid fill. */
|
|
17
|
+
pattern?: string
|
|
11
18
|
options?: { opacity?: number }
|
|
12
19
|
}
|
|
13
20
|
|
|
14
|
-
let { x, y, fill, stat = 'boxplot', options = {} }: Props = $props()
|
|
21
|
+
let { x, y, fill, stat = 'boxplot', side = 'center', width, pattern, options = {} }: Props = $props()
|
|
15
22
|
|
|
16
23
|
const plotState = getContext<PlotState>('plot-state')
|
|
17
24
|
let id = $state<string | null>(null)
|
|
18
25
|
|
|
19
|
-
//
|
|
20
|
-
|
|
26
|
+
// Fall back to the root Plot channels when the geom's own x/y are omitted, so
|
|
27
|
+
// <Plot.Box> composes under <Plot.Root x y> without silently NaN-ing.
|
|
28
|
+
const xf = $derived(x ?? plotState.channels?.x)
|
|
29
|
+
const yf = $derived(y ?? plotState.channels?.y)
|
|
30
|
+
// fill ?? effective-x drives the colors map for both box interior and whisker strokes
|
|
31
|
+
const fillChannel = $derived(fill ?? xf)
|
|
21
32
|
|
|
22
33
|
onMount(() => {
|
|
23
34
|
id = plotState.registerGeom({
|
|
24
35
|
type: 'box',
|
|
25
|
-
channels: { x, y, color: fillChannel },
|
|
36
|
+
channels: { x: xf, y: yf, color: fillChannel, pattern },
|
|
26
37
|
stat,
|
|
27
38
|
options
|
|
28
39
|
})
|
|
@@ -32,91 +43,135 @@
|
|
|
32
43
|
})
|
|
33
44
|
|
|
34
45
|
$effect(() => {
|
|
35
|
-
if (id)
|
|
46
|
+
if (id)
|
|
47
|
+
plotState.updateGeom(id, { channels: { x: xf, y: yf, color: fillChannel, pattern }, stat })
|
|
36
48
|
})
|
|
37
49
|
|
|
38
50
|
const data = $derived(id ? plotState.geomData(id) : [])
|
|
39
51
|
const xScale = $derived(plotState.xScale)
|
|
40
52
|
const yScale = $derived(plotState.yScale)
|
|
41
53
|
const colors = $derived(plotState.colors)
|
|
54
|
+
const patterns = $derived(plotState.patterns)
|
|
42
55
|
|
|
43
56
|
const boxes = $derived.by(() => {
|
|
44
57
|
if (!data?.length || !xScale || !yScale) return []
|
|
45
|
-
return buildBoxes(data, { x, fill: fillChannel }, xScale, yScale, colors
|
|
58
|
+
return buildBoxes(data, { x: xf, fill: fillChannel, pattern }, xScale, yScale, colors, {
|
|
59
|
+
side,
|
|
60
|
+
width,
|
|
61
|
+
patterns
|
|
62
|
+
})
|
|
46
63
|
})
|
|
47
64
|
</script>
|
|
48
65
|
|
|
49
66
|
{#if boxes.length > 0}
|
|
50
67
|
<g data-plot-geom="box">
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
68
|
+
{#each boxes as box, i (`${String(box.cx)}::${i}`)}
|
|
69
|
+
<!-- Each primitive is placed through plotState.place(band, value) so the whole
|
|
70
|
+
box transposes correctly under orientation='horizontal' (flip). -->
|
|
71
|
+
{@const c1 = plotState.place(box.cx - box.width / 2, box.q3)}
|
|
72
|
+
{@const c2 = plotState.place(box.cx + box.width / 2, box.q1)}
|
|
73
|
+
{@const mA = plotState.place(box.cx - box.width / 2, box.median)}
|
|
74
|
+
{@const mB = plotState.place(box.cx + box.width / 2, box.median)}
|
|
75
|
+
{@const wLoA = plotState.place(box.cx, box.q1)}
|
|
76
|
+
{@const wLoB = plotState.place(box.cx, box.iqr_min)}
|
|
77
|
+
{@const wHiA = plotState.place(box.cx, box.q3)}
|
|
78
|
+
{@const wHiB = plotState.place(box.cx, box.iqr_max)}
|
|
79
|
+
{@const capLoA = plotState.place(box.cx - box.whiskerWidth / 2, box.iqr_min)}
|
|
80
|
+
{@const capLoB = plotState.place(box.cx + box.whiskerWidth / 2, box.iqr_min)}
|
|
81
|
+
{@const capHiA = plotState.place(box.cx - box.whiskerWidth / 2, box.iqr_max)}
|
|
82
|
+
{@const capHiB = plotState.place(box.cx + box.whiskerWidth / 2, box.iqr_max)}
|
|
83
|
+
<!-- Box body (IQR): lighter fill shade -->
|
|
84
|
+
<rect
|
|
85
|
+
x={Math.min(c1.x, c2.x)}
|
|
86
|
+
y={Math.min(c1.y, c2.y)}
|
|
87
|
+
width={Math.abs(c2.x - c1.x)}
|
|
88
|
+
height={Math.abs(c2.y - c1.y)}
|
|
89
|
+
fill={box.fill}
|
|
90
|
+
fill-opacity={options?.opacity ?? plotState.chartPreset.opacity.box}
|
|
91
|
+
stroke={box.stroke}
|
|
92
|
+
stroke-width="1"
|
|
93
|
+
data-plot-element="box-body"
|
|
94
|
+
role="graphics-symbol"
|
|
95
|
+
aria-label={`${String(box.data[xf ?? ''] ?? 'box')}: median ${box.data.median}, IQR ${box.data.q1}–${box.data.q3}`}
|
|
96
|
+
onmouseenter={() => plotState.setHovered(box.data)}
|
|
97
|
+
onmouseleave={() => plotState.clearHovered()}
|
|
98
|
+
/>
|
|
99
|
+
{#if box.patternId}
|
|
100
|
+
<!-- Texture fill overlay (matches the box body) -->
|
|
101
|
+
<rect
|
|
102
|
+
x={Math.min(c1.x, c2.x)}
|
|
103
|
+
y={Math.min(c1.y, c2.y)}
|
|
104
|
+
width={Math.abs(c2.x - c1.x)}
|
|
105
|
+
height={Math.abs(c2.y - c1.y)}
|
|
106
|
+
fill="url(#{box.patternId})"
|
|
107
|
+
pointer-events="none"
|
|
108
|
+
/>
|
|
109
|
+
{/if}
|
|
110
|
+
<!-- Median line: darker stroke shade -->
|
|
111
|
+
<line
|
|
112
|
+
x1={mA.x}
|
|
113
|
+
y1={mA.y}
|
|
114
|
+
x2={mB.x}
|
|
115
|
+
y2={mB.y}
|
|
116
|
+
stroke={box.stroke}
|
|
117
|
+
stroke-width="2"
|
|
118
|
+
data-plot-element="box-median"
|
|
119
|
+
/>
|
|
120
|
+
<!-- Lower whisker (q1 to iqr_min) -->
|
|
121
|
+
<line
|
|
122
|
+
x1={wLoA.x}
|
|
123
|
+
y1={wLoA.y}
|
|
124
|
+
x2={wLoB.x}
|
|
125
|
+
y2={wLoB.y}
|
|
126
|
+
stroke={box.stroke}
|
|
127
|
+
stroke-width="1"
|
|
128
|
+
data-plot-element="box-whisker"
|
|
129
|
+
/>
|
|
130
|
+
<!-- Upper whisker (q3 to iqr_max) -->
|
|
131
|
+
<line
|
|
132
|
+
x1={wHiA.x}
|
|
133
|
+
y1={wHiA.y}
|
|
134
|
+
x2={wHiB.x}
|
|
135
|
+
y2={wHiB.y}
|
|
136
|
+
stroke={box.stroke}
|
|
137
|
+
stroke-width="1"
|
|
138
|
+
data-plot-element="box-whisker"
|
|
139
|
+
/>
|
|
140
|
+
<!-- Lower whisker cap -->
|
|
141
|
+
<line
|
|
142
|
+
x1={capLoA.x}
|
|
143
|
+
y1={capLoA.y}
|
|
144
|
+
x2={capLoB.x}
|
|
145
|
+
y2={capLoB.y}
|
|
146
|
+
stroke={box.stroke}
|
|
147
|
+
stroke-width="1"
|
|
148
|
+
/>
|
|
149
|
+
<!-- Upper whisker cap -->
|
|
150
|
+
<line
|
|
151
|
+
x1={capHiA.x}
|
|
152
|
+
y1={capHiA.y}
|
|
153
|
+
x2={capHiB.x}
|
|
154
|
+
y2={capHiB.y}
|
|
155
|
+
stroke={box.stroke}
|
|
156
|
+
stroke-width="1"
|
|
157
|
+
/>
|
|
158
|
+
<!-- Outliers: individual points beyond the 1.5·IQR fence -->
|
|
159
|
+
{#each box.outliers as o, oi (`${i}-outlier-${oi}`)}
|
|
160
|
+
{@const op = plotState.place(box.cx, o.cy)}
|
|
161
|
+
<circle
|
|
162
|
+
cx={op.x}
|
|
163
|
+
cy={op.y}
|
|
164
|
+
r="2"
|
|
165
|
+
fill="none"
|
|
166
|
+
stroke={box.stroke}
|
|
167
|
+
stroke-width="1"
|
|
168
|
+
data-plot-element="box-outlier"
|
|
169
|
+
role="graphics-symbol"
|
|
170
|
+
aria-label={`outlier: ${o.value}`}
|
|
171
|
+
onmouseenter={() => plotState.setHovered({ ...box.data, value: o.value })}
|
|
172
|
+
onmouseleave={() => plotState.clearHovered()}
|
|
173
|
+
/>
|
|
174
|
+
{/each}
|
|
175
|
+
{/each}
|
|
121
176
|
</g>
|
|
122
177
|
{/if}
|
|
@@ -3,6 +3,13 @@ type Props = {
|
|
|
3
3
|
y?: string;
|
|
4
4
|
fill?: string;
|
|
5
5
|
stat?: string;
|
|
6
|
+
/** Offset the box to one half of the band (raincloud/split plots). Default 'center'. */
|
|
7
|
+
side?: 'left' | 'right' | 'center';
|
|
8
|
+
/** Box thickness as a fraction of the (sub-)band — use a small value (e.g. 0.16) for a
|
|
9
|
+
* thin raincloud box. Defaults to the full box width. */
|
|
10
|
+
width?: number;
|
|
11
|
+
/** Data field driving a texture fill (from the shared pattern set) instead of a solid fill. */
|
|
12
|
+
pattern?: string;
|
|
6
13
|
options?: {
|
|
7
14
|
opacity?: number;
|
|
8
15
|
};
|
|
@@ -81,18 +81,24 @@
|
|
|
81
81
|
const bodyTop = yScale(isUp ? closeVal : openVal)
|
|
82
82
|
const bodyBottom = yScale(isUp ? openVal : closeVal)
|
|
83
83
|
|
|
84
|
+
// Place (band, value) points so the candle transposes under orientation flip.
|
|
85
|
+
const b1 = plotState.place(xPos, bodyTop)
|
|
86
|
+
const b2 = plotState.place(xPos + bodyWidth, bodyBottom)
|
|
87
|
+
const wa = plotState.place(xPos + bodyWidth / 2, yScale(highVal))
|
|
88
|
+
const wb = plotState.place(xPos + bodyWidth / 2, yScale(lowVal))
|
|
84
89
|
return {
|
|
85
90
|
key: `${d[x ?? '']}-${i}`,
|
|
86
91
|
// Body
|
|
87
|
-
bodyX:
|
|
88
|
-
bodyY:
|
|
89
|
-
bodyWidth,
|
|
90
|
-
bodyHeight: Math.max(1,
|
|
92
|
+
bodyX: Math.min(b1.x, b2.x),
|
|
93
|
+
bodyY: Math.min(b1.y, b2.y),
|
|
94
|
+
bodyWidth: Math.abs(b2.x - b1.x),
|
|
95
|
+
bodyHeight: Math.max(1, Math.abs(b2.y - b1.y)),
|
|
91
96
|
fill: isUp ? upColor : downColor,
|
|
92
97
|
// Wick
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
98
|
+
wickX1: wa.x,
|
|
99
|
+
wickY1: wa.y,
|
|
100
|
+
wickX2: wb.x,
|
|
101
|
+
wickY2: wb.y,
|
|
96
102
|
data: d
|
|
97
103
|
}
|
|
98
104
|
})
|
|
@@ -104,10 +110,10 @@
|
|
|
104
110
|
{#each candles as c (c.key)}
|
|
105
111
|
<!-- Wick (high to low) -->
|
|
106
112
|
<line
|
|
107
|
-
x1={c.
|
|
108
|
-
y1={c.
|
|
109
|
-
x2={c.
|
|
110
|
-
y2={c.
|
|
113
|
+
x1={c.wickX1}
|
|
114
|
+
y1={c.wickY1}
|
|
115
|
+
x2={c.wickX2}
|
|
116
|
+
y2={c.wickY2}
|
|
111
117
|
stroke={c.fill}
|
|
112
118
|
stroke-width={wickWidth}
|
|
113
119
|
data-plot-element="wick"
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getContext, onMount, onDestroy } from 'svelte'
|
|
3
|
+
import type { PlotState } from '../PlotState.svelte.js'
|
|
4
|
+
import { buildSwarm } from '../lib/brewing/marks/swarm.js'
|
|
5
|
+
|
|
6
|
+
type Props = {
|
|
7
|
+
x?: string
|
|
8
|
+
y?: string
|
|
9
|
+
fill?: string
|
|
10
|
+
r?: number
|
|
11
|
+
method?: 'jitter' | 'swarm'
|
|
12
|
+
/** Confine points to one half of the band (raincloud/split plots). Default 'center'. */
|
|
13
|
+
side?: 'left' | 'right' | 'center'
|
|
14
|
+
options?: { opacity?: number }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let { x, y, fill, r = 2, method = 'jitter', side = 'center', options = {} }: Props = $props()
|
|
18
|
+
|
|
19
|
+
const plotState = getContext<PlotState>('plot-state')
|
|
20
|
+
let id = $state<string | null>(null)
|
|
21
|
+
|
|
22
|
+
// Fall back to the root Plot channels when the geom omits x/y (composes under
|
|
23
|
+
// <Plot.Root x y> without silently NaN-ing).
|
|
24
|
+
const xf = $derived(x ?? plotState.channels?.x)
|
|
25
|
+
const yf = $derived(y ?? plotState.channels?.y)
|
|
26
|
+
// fill ?? effective-x drives the color lookup
|
|
27
|
+
const fillChannel = $derived(fill ?? xf)
|
|
28
|
+
|
|
29
|
+
onMount(() => {
|
|
30
|
+
id = plotState.registerGeom({
|
|
31
|
+
type: 'jitter',
|
|
32
|
+
channels: { x: xf, y: yf, color: fillChannel },
|
|
33
|
+
stat: 'identity',
|
|
34
|
+
options
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
onDestroy(() => {
|
|
38
|
+
if (id) plotState.unregisterGeom(id)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
$effect(() => {
|
|
42
|
+
if (id) plotState.updateGeom(id, { channels: { x: xf, y: yf, color: fillChannel }, stat: 'identity' })
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const data = $derived(id ? plotState.geomData(id) : [])
|
|
46
|
+
const xScale = $derived(plotState.xScale)
|
|
47
|
+
const yScale = $derived(plotState.yScale)
|
|
48
|
+
const colors = $derived(plotState.colors)
|
|
49
|
+
|
|
50
|
+
const points = $derived.by(() => {
|
|
51
|
+
if (!data?.length || !xScale || !yScale) return []
|
|
52
|
+
const raw = buildSwarm(data, { x: xf, y: yf, fill: fillChannel }, xScale, yScale, colors, { method, r, side })
|
|
53
|
+
if (!plotState.isFlipped) return raw
|
|
54
|
+
// Horizontal: the band axis (swarm spread) stands up on screen-Y and the value
|
|
55
|
+
// axis runs along screen-X — swap the computed coords through place().
|
|
56
|
+
return raw.map((p) => {
|
|
57
|
+
const s = plotState.place(p.cx, p.cy)
|
|
58
|
+
return { ...p, cx: s.x, cy: s.y }
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
{#if points.length > 0}
|
|
64
|
+
<g data-plot-geom="jitter">
|
|
65
|
+
{#each points as pt, i (`${String(pt.cx)}::${i}`)}
|
|
66
|
+
<circle
|
|
67
|
+
cx={pt.cx}
|
|
68
|
+
cy={pt.cy}
|
|
69
|
+
{r}
|
|
70
|
+
fill={pt.fill}
|
|
71
|
+
fill-opacity={options?.opacity ?? plotState.chartPreset.opacity.point}
|
|
72
|
+
stroke={pt.stroke}
|
|
73
|
+
stroke-width="0.5"
|
|
74
|
+
data-plot-element="jitter-point"
|
|
75
|
+
role="graphics-symbol"
|
|
76
|
+
aria-label={`(${String(pt.data[xf ?? ''])}, ${String(pt.data[yf ?? ''])})`}
|
|
77
|
+
onmouseenter={() => plotState.setHovered(pt.data)}
|
|
78
|
+
onmouseleave={() => plotState.clearHovered()}
|
|
79
|
+
/>
|
|
80
|
+
{/each}
|
|
81
|
+
</g>
|
|
82
|
+
{/if}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
type Props = {
|
|
2
|
+
x?: string;
|
|
3
|
+
y?: string;
|
|
4
|
+
fill?: string;
|
|
5
|
+
r?: number;
|
|
6
|
+
method?: 'jitter' | 'swarm';
|
|
7
|
+
/** Confine points to one half of the band (raincloud/split plots). Default 'center'. */
|
|
8
|
+
side?: 'left' | 'right' | 'center';
|
|
9
|
+
options?: {
|
|
10
|
+
opacity?: number;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
declare const Jitter: import("svelte").Component<Props, {}, "">;
|
|
14
|
+
type Jitter = ReturnType<typeof Jitter>;
|
|
15
|
+
export default Jitter;
|
package/dist/geoms/Line.svelte
CHANGED
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
|
|
86
86
|
const lines = $derived.by(() => {
|
|
87
87
|
if (!data?.length || !xScale || !yScale) return []
|
|
88
|
-
return buildLines(data, { x, y, color }, xScale, yScale, colors, options.curve)
|
|
88
|
+
return buildLines(data, { x, y, color }, xScale, yScale, colors, options.curve, plotState.place.bind(plotState))
|
|
89
89
|
})
|
|
90
90
|
|
|
91
91
|
const markerRadius = $derived(options.markerRadius ?? 4)
|
package/dist/geoms/Point.svelte
CHANGED
|
@@ -107,7 +107,7 @@
|
|
|
107
107
|
|
|
108
108
|
const points = $derived.by(() => {
|
|
109
109
|
if (!data?.length || !xScale || !yScale) return []
|
|
110
|
-
|
|
110
|
+
const raw = buildPoints(
|
|
111
111
|
data,
|
|
112
112
|
{ x, y, color, size, symbol: symbolField },
|
|
113
113
|
xScale,
|
|
@@ -118,6 +118,12 @@
|
|
|
118
118
|
defaultRadius,
|
|
119
119
|
options?.jitter ?? null
|
|
120
120
|
)
|
|
121
|
+
if (!plotState.isFlipped) return raw
|
|
122
|
+
// Horizontal: swap each point's screen coords (value → screen-X, category → screen-Y).
|
|
123
|
+
return raw.map((p) => {
|
|
124
|
+
const s = plotState.place(p.cx, p.cy)
|
|
125
|
+
return { ...p, cx: s.x, cy: s.y }
|
|
126
|
+
})
|
|
121
127
|
})
|
|
122
128
|
</script>
|
|
123
129
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getContext } from 'svelte'
|
|
3
|
+
import type { PlotState } from '../PlotState.svelte.js'
|
|
4
|
+
|
|
5
|
+
type RuleValue = number | string
|
|
6
|
+
type Props = {
|
|
7
|
+
/** Vertical reference line(s) at these x-axis value(s) (a value, or a band category). */
|
|
8
|
+
x?: RuleValue | RuleValue[]
|
|
9
|
+
/** Horizontal reference line(s) at these y-axis value(s). */
|
|
10
|
+
y?: RuleValue | RuleValue[]
|
|
11
|
+
stroke?: string
|
|
12
|
+
/** SVG stroke-dasharray (default dashed). Pass '' for a solid line. */
|
|
13
|
+
dash?: string
|
|
14
|
+
strokeWidth?: number
|
|
15
|
+
/** Optional label drawn at the line's end. String, or a fn of the value. */
|
|
16
|
+
label?: string | ((value: RuleValue) => string)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
x,
|
|
21
|
+
y,
|
|
22
|
+
stroke = undefined,
|
|
23
|
+
dash = '4 4',
|
|
24
|
+
strokeWidth = 1,
|
|
25
|
+
label = undefined
|
|
26
|
+
}: Props = $props()
|
|
27
|
+
|
|
28
|
+
const plotState = getContext<PlotState>('plot-state')
|
|
29
|
+
const xScale = $derived(plotState.xScale)
|
|
30
|
+
const yScale = $derived(plotState.yScale)
|
|
31
|
+
|
|
32
|
+
const toArray = (v: RuleValue | RuleValue[] | undefined): RuleValue[] =>
|
|
33
|
+
v === undefined ? [] : Array.isArray(v) ? v : [v]
|
|
34
|
+
|
|
35
|
+
// Position along an axis: band scales resolve to the band centre, linear scales map directly.
|
|
36
|
+
function axisPos(scale: { (v: unknown): number | undefined; bandwidth?: () => number }, v: unknown) {
|
|
37
|
+
const base = scale(v) ?? 0
|
|
38
|
+
return typeof scale.bandwidth === 'function' ? base + scale.bandwidth() / 2 : base
|
|
39
|
+
}
|
|
40
|
+
// The full [min, max] screen extent of a scale's range (spans the perpendicular axis).
|
|
41
|
+
function axisExtent(scale: { range: () => number[] }) {
|
|
42
|
+
const r = scale.range()
|
|
43
|
+
return [Math.min(...r), Math.max(...r)]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const labelFor = (v: RuleValue) =>
|
|
47
|
+
typeof label === 'function' ? label(v) : typeof label === 'string' ? label : null
|
|
48
|
+
|
|
49
|
+
// Each rule is expressed in (x-channel, y-channel) space and placed to screen, so it
|
|
50
|
+
// transposes under orientation flip: a y-value reference is horizontal when vertical and
|
|
51
|
+
// vertical when flipped (it stays perpendicular to the value axis).
|
|
52
|
+
const rules = $derived.by(() => {
|
|
53
|
+
if (!xScale || !yScale) return []
|
|
54
|
+
const place = plotState.place.bind(plotState)
|
|
55
|
+
const out: Array<{
|
|
56
|
+
key: string
|
|
57
|
+
x1: number
|
|
58
|
+
y1: number
|
|
59
|
+
x2: number
|
|
60
|
+
y2: number
|
|
61
|
+
value: RuleValue
|
|
62
|
+
text: string | null
|
|
63
|
+
}> = []
|
|
64
|
+
const [xLo, xHi] = axisExtent(xScale)
|
|
65
|
+
const [yLo, yHi] = axisExtent(yScale)
|
|
66
|
+
|
|
67
|
+
for (const yv of toArray(y)) {
|
|
68
|
+
const yc = axisPos(yScale, yv)
|
|
69
|
+
const a = place(xLo, yc)
|
|
70
|
+
const b = place(xHi, yc)
|
|
71
|
+
out.push({ key: `y-${yv}`, x1: a.x, y1: a.y, x2: b.x, y2: b.y, value: yv, text: labelFor(yv) })
|
|
72
|
+
}
|
|
73
|
+
for (const xv of toArray(x)) {
|
|
74
|
+
const xc = axisPos(xScale, xv)
|
|
75
|
+
const a = place(xc, yLo)
|
|
76
|
+
const b = place(xc, yHi)
|
|
77
|
+
out.push({ key: `x-${xv}`, x1: a.x, y1: a.y, x2: b.x, y2: b.y, value: xv, text: labelFor(xv) })
|
|
78
|
+
}
|
|
79
|
+
return out
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const strokeColor = $derived(stroke ?? 'currentColor')
|
|
83
|
+
</script>
|
|
84
|
+
|
|
85
|
+
{#if rules.length > 0}
|
|
86
|
+
<g data-plot-geom="rule">
|
|
87
|
+
{#each rules as rule (rule.key)}
|
|
88
|
+
<line
|
|
89
|
+
x1={rule.x1}
|
|
90
|
+
y1={rule.y1}
|
|
91
|
+
x2={rule.x2}
|
|
92
|
+
y2={rule.y2}
|
|
93
|
+
stroke={strokeColor}
|
|
94
|
+
stroke-width={strokeWidth}
|
|
95
|
+
stroke-dasharray={dash || undefined}
|
|
96
|
+
data-plot-element="rule"
|
|
97
|
+
data-plot-value={rule.value}
|
|
98
|
+
/>
|
|
99
|
+
{#if rule.text}
|
|
100
|
+
<text
|
|
101
|
+
x={rule.x2}
|
|
102
|
+
y={rule.y2}
|
|
103
|
+
dx="-4"
|
|
104
|
+
dy="-4"
|
|
105
|
+
text-anchor="end"
|
|
106
|
+
font-size="10"
|
|
107
|
+
fill={strokeColor}
|
|
108
|
+
data-plot-element="rule-label"
|
|
109
|
+
>{rule.text}</text>
|
|
110
|
+
{/if}
|
|
111
|
+
{/each}
|
|
112
|
+
</g>
|
|
113
|
+
{/if}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
type RuleValue = number | string;
|
|
2
|
+
type Props = {
|
|
3
|
+
/** Vertical reference line(s) at these x-axis value(s) (a value, or a band category). */
|
|
4
|
+
x?: RuleValue | RuleValue[];
|
|
5
|
+
/** Horizontal reference line(s) at these y-axis value(s). */
|
|
6
|
+
y?: RuleValue | RuleValue[];
|
|
7
|
+
stroke?: string;
|
|
8
|
+
/** SVG stroke-dasharray (default dashed). Pass '' for a solid line. */
|
|
9
|
+
dash?: string;
|
|
10
|
+
strokeWidth?: number;
|
|
11
|
+
/** Optional label drawn at the line's end. String, or a fn of the value. */
|
|
12
|
+
label?: string | ((value: RuleValue) => string);
|
|
13
|
+
};
|
|
14
|
+
declare const Rule: import("svelte").Component<Props, {}, "">;
|
|
15
|
+
type Rule = ReturnType<typeof Rule>;
|
|
16
|
+
export default Rule;
|