@rokkit/chart 1.3.6 → 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 +21 -10
- package/dist/Plot/Grid.svelte +32 -13
- package/dist/Plot/Grid.svelte.d.ts +6 -1
- 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 +128 -164
- package/dist/Plot.svelte.d.ts +18 -2
- package/dist/PlotState.svelte.d.ts +23 -0
- package/dist/PlotState.svelte.js +155 -16
- package/dist/PlotSurface.svelte +162 -0
- package/dist/PlotSurface.svelte.d.ts +25 -0
- package/dist/charts/AreaChart.svelte +37 -4
- package/dist/charts/AreaChart.svelte.d.ts +12 -2
- 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 +35 -4
- package/dist/charts/LineChart.svelte.d.ts +11 -2
- 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 +20 -3
- package/dist/geoms/Bar.svelte +57 -13
- 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/Highlight.svelte +105 -0
- package/dist/geoms/Highlight.svelte.d.ts +10 -0
- package/dist/geoms/Jitter.svelte +82 -0
- package/dist/geoms/Jitter.svelte.d.ts +15 -0
- package/dist/geoms/Line.svelte +26 -9
- package/dist/geoms/Point.svelte +37 -11
- package/dist/geoms/Rule.svelte +113 -0
- package/dist/geoms/Rule.svelte.d.ts +16 -0
- package/dist/geoms/Trend.svelte +66 -0
- package/dist/geoms/Trend.svelte.d.ts +12 -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 +30 -4
- package/dist/index.js +31 -5
- package/dist/lib/brewing/ViolinBrewer.svelte.d.ts +1 -1
- package/dist/lib/brewing/brewer.svelte.d.ts +1 -0
- 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 +6 -2
- package/dist/lib/brewing/marks/lines.js +19 -12
- 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/highlight.d.ts +10 -0
- package/dist/lib/highlight.js +43 -0
- 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/scale.d.ts +10 -0
- package/dist/lib/scale.js +14 -0
- package/dist/lib/select.d.ts +22 -0
- package/dist/lib/select.js +22 -0
- package/dist/lib/trend.d.ts +27 -0
- package/dist/lib/trend.js +113 -0
- package/dist/patterns/README.md +1 -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/Point.svelte
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import type { PlotState } from '../PlotState.svelte.js'
|
|
5
5
|
import { buildPoints } from '../lib/brewing/marks/points.js'
|
|
6
6
|
import { keyboardNav } from '../lib/keyboard-nav.js'
|
|
7
|
+
import { buildSelectDetail } from '../lib/select.js'
|
|
7
8
|
import LabelPill from './LabelPill.svelte'
|
|
8
9
|
|
|
9
10
|
type Row = Record<string, unknown>
|
|
@@ -52,6 +53,21 @@
|
|
|
52
53
|
const plotState = getContext<PlotState>('plot-state')
|
|
53
54
|
let id = $state<string | null>(null)
|
|
54
55
|
|
|
56
|
+
function selectPoint(pt: { data: Row }, event: MouseEvent | KeyboardEvent) {
|
|
57
|
+
onselect?.(pt.data)
|
|
58
|
+
if (plotState.interactive)
|
|
59
|
+
plotState.handleSelect(
|
|
60
|
+
buildSelectDetail(
|
|
61
|
+
pt.data,
|
|
62
|
+
plotState.data.indexOf(pt.data),
|
|
63
|
+
{ x, y },
|
|
64
|
+
'point',
|
|
65
|
+
color ? pt.data[color] : undefined,
|
|
66
|
+
event
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
55
71
|
onMount(() => {
|
|
56
72
|
id = plotState.registerGeom({
|
|
57
73
|
type: 'point',
|
|
@@ -91,7 +107,7 @@
|
|
|
91
107
|
|
|
92
108
|
const points = $derived.by(() => {
|
|
93
109
|
if (!data?.length || !xScale || !yScale) return []
|
|
94
|
-
|
|
110
|
+
const raw = buildPoints(
|
|
95
111
|
data,
|
|
96
112
|
{ x, y, color, size, symbol: symbolField },
|
|
97
113
|
xScale,
|
|
@@ -102,6 +118,12 @@
|
|
|
102
118
|
defaultRadius,
|
|
103
119
|
options?.jitter ?? null
|
|
104
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
|
+
})
|
|
105
127
|
})
|
|
106
128
|
</script>
|
|
107
129
|
|
|
@@ -118,14 +140,16 @@
|
|
|
118
140
|
stroke-width="1"
|
|
119
141
|
fill-opacity={options.opacity ?? plotState.chartPreset.opacity.point}
|
|
120
142
|
data-plot-element="point"
|
|
121
|
-
role={onselect || keyboard ? 'button' : 'graphics-symbol'}
|
|
122
|
-
tabindex={onselect || keyboard ? 0 : undefined}
|
|
123
|
-
style:cursor={onselect ? 'pointer' : undefined}
|
|
143
|
+
role={onselect || keyboard || plotState.interactive ? 'button' : 'graphics-symbol'}
|
|
144
|
+
tabindex={onselect || keyboard || plotState.interactive ? 0 : undefined}
|
|
145
|
+
style:cursor={onselect || keyboard || plotState.interactive ? 'pointer' : undefined}
|
|
124
146
|
aria-label="{pt.data[x ?? '']}, {pt.data[y ?? '']}"
|
|
125
147
|
onmouseenter={() => plotState.setHovered(pt.data)}
|
|
126
148
|
onmouseleave={() => plotState.clearHovered()}
|
|
127
|
-
onclick={onselect ? () =>
|
|
128
|
-
onkeydown={onselect
|
|
149
|
+
onclick={onselect || keyboard || plotState.interactive ? (e) => selectPoint(pt, e) : undefined}
|
|
150
|
+
onkeydown={onselect || keyboard || plotState.interactive
|
|
151
|
+
? (e) => (e.key === 'Enter' || e.key === ' ') && selectPoint(pt, e)
|
|
152
|
+
: undefined}
|
|
129
153
|
use:keyboardNav={keyboard}
|
|
130
154
|
/>
|
|
131
155
|
{:else}
|
|
@@ -138,14 +162,16 @@
|
|
|
138
162
|
stroke-width="1"
|
|
139
163
|
fill-opacity={options.opacity ?? plotState.chartPreset.opacity.point}
|
|
140
164
|
data-plot-element="point"
|
|
141
|
-
role={onselect || keyboard ? 'button' : 'graphics-symbol'}
|
|
142
|
-
tabindex={onselect || keyboard ? 0 : undefined}
|
|
143
|
-
style:cursor={onselect ? 'pointer' : undefined}
|
|
165
|
+
role={onselect || keyboard || plotState.interactive ? 'button' : 'graphics-symbol'}
|
|
166
|
+
tabindex={onselect || keyboard || plotState.interactive ? 0 : undefined}
|
|
167
|
+
style:cursor={onselect || keyboard || plotState.interactive ? 'pointer' : undefined}
|
|
144
168
|
aria-label="{pt.data[x ?? '']}, {pt.data[y ?? '']}"
|
|
145
169
|
onmouseenter={() => plotState.setHovered(pt.data)}
|
|
146
170
|
onmouseleave={() => plotState.clearHovered()}
|
|
147
|
-
onclick={onselect ? () =>
|
|
148
|
-
onkeydown={onselect
|
|
171
|
+
onclick={onselect || keyboard || plotState.interactive ? (e) => selectPoint(pt, e) : undefined}
|
|
172
|
+
onkeydown={onselect || keyboard || plotState.interactive
|
|
173
|
+
? (e) => (e.key === 'Enter' || e.key === ' ') && selectPoint(pt, e)
|
|
174
|
+
: undefined}
|
|
149
175
|
use:keyboardNav={keyboard}
|
|
150
176
|
/>
|
|
151
177
|
{/if}
|
|
@@ -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;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getContext } from 'svelte'
|
|
3
|
+
import { line as d3line } from 'd3-shape'
|
|
4
|
+
import type { PlotState } from '../PlotState.svelte.js'
|
|
5
|
+
import { computeTrend } from '../lib/trend.js'
|
|
6
|
+
import { scalePos } from '../lib/scale.js'
|
|
7
|
+
|
|
8
|
+
type Method = string | number | { type: string; [k: string]: unknown }
|
|
9
|
+
type Props = { x?: string; y?: string; trend?: Method | Method[] }
|
|
10
|
+
let { x, y, trend = undefined }: Props = $props()
|
|
11
|
+
|
|
12
|
+
const state = getContext<PlotState>('plot-state')
|
|
13
|
+
|
|
14
|
+
const methods = $derived(
|
|
15
|
+
Array.isArray(trend) ? trend : trend === null || trend === undefined ? [] : [trend]
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
const typeOf = (m: Method) => {
|
|
19
|
+
const t = typeof m === 'string' ? m : typeof m === 'number' ? 'value' : (m?.type ?? '')
|
|
20
|
+
return t === 'mean' ? 'avg' : t
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const paths = $derived.by(() => {
|
|
24
|
+
const rows = state?.data ?? []
|
|
25
|
+
const xs = state?.xScale
|
|
26
|
+
const ys = state?.yScale
|
|
27
|
+
if (!rows.length || !xs || !ys || !x || !y || !methods.length) return []
|
|
28
|
+
const out: { d: string; type: string; i: number }[] = []
|
|
29
|
+
methods.forEach((m, idx) => {
|
|
30
|
+
const res = computeTrend(rows, { x, y }, m)
|
|
31
|
+
if (!res) return
|
|
32
|
+
if (res.kind === 'constant') {
|
|
33
|
+
const yy = ys(res.value)
|
|
34
|
+
out.push({ d: `M0,${yy} L${state.innerWidth},${yy}`, type: typeOf(m), i: idx })
|
|
35
|
+
} else {
|
|
36
|
+
const pts = res.values
|
|
37
|
+
.map((v, i) => ({ vx: scalePos(xs, rows[i][x]), vy: ys(v) }))
|
|
38
|
+
.filter((p) => Number.isFinite(p.vx) && Number.isFinite(p.vy))
|
|
39
|
+
const gen = d3line<{ vx: number; vy: number }>()
|
|
40
|
+
.x((p) => p.vx)
|
|
41
|
+
.y((p) => p.vy)
|
|
42
|
+
const d = gen(pts)
|
|
43
|
+
if (d) out.push({ d, type: typeOf(m), i: idx })
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
return out
|
|
47
|
+
})
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
{#if paths.length}
|
|
51
|
+
<g data-plot-geom="trend">
|
|
52
|
+
{#each paths as p (p.i)}
|
|
53
|
+
<path d={p.d} data-plot-trend={p.type} />
|
|
54
|
+
{/each}
|
|
55
|
+
</g>
|
|
56
|
+
{/if}
|
|
57
|
+
|
|
58
|
+
<style>
|
|
59
|
+
[data-plot-trend] {
|
|
60
|
+
fill: none;
|
|
61
|
+
stroke: var(--chart-trend-color, currentColor);
|
|
62
|
+
stroke-width: var(--chart-trend-width, 1);
|
|
63
|
+
stroke-dasharray: var(--chart-trend-dash, 4 4);
|
|
64
|
+
opacity: var(--chart-trend-opacity, 0.7);
|
|
65
|
+
}
|
|
66
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type Method = string | number | {
|
|
2
|
+
type: string;
|
|
3
|
+
[k: string]: unknown;
|
|
4
|
+
};
|
|
5
|
+
type Props = {
|
|
6
|
+
x?: string;
|
|
7
|
+
y?: string;
|
|
8
|
+
trend?: Method | Method[];
|
|
9
|
+
};
|
|
10
|
+
declare const Trend: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type Trend = ReturnType<typeof Trend>;
|
|
12
|
+
export default Trend;
|
package/dist/geoms/Violin.svelte
CHANGED
|
@@ -8,21 +8,30 @@
|
|
|
8
8
|
y?: string
|
|
9
9
|
fill?: string
|
|
10
10
|
stat?: string
|
|
11
|
+
/** Half-violin: 'left'/'right' draw one side (flat edge at the band centre) for
|
|
12
|
+
* raincloud/split plots; 'center' (default) is the full symmetric silhouette. */
|
|
13
|
+
side?: 'left' | 'right' | 'center'
|
|
14
|
+
/** Data field driving a texture fill (from the shared pattern set) instead of a solid fill. */
|
|
15
|
+
pattern?: string
|
|
11
16
|
options?: { opacity?: number }
|
|
12
17
|
}
|
|
13
18
|
|
|
14
|
-
let { x, y, fill, stat = 'boxplot', options = {} }: Props = $props()
|
|
19
|
+
let { x, y, fill, stat = 'boxplot', side = 'center', pattern, options = {} }: Props = $props()
|
|
15
20
|
|
|
16
21
|
const plotState = getContext<PlotState>('plot-state')
|
|
17
22
|
let id = $state<string | null>(null)
|
|
18
23
|
|
|
19
|
-
//
|
|
20
|
-
|
|
24
|
+
// Fall back to the root Plot channels when the geom omits x/y (composes under
|
|
25
|
+
// <Plot.Root x y> without silently NaN-ing).
|
|
26
|
+
const xf = $derived(x ?? plotState.channels?.x)
|
|
27
|
+
const yf = $derived(y ?? plotState.channels?.y)
|
|
28
|
+
// fill ?? effective-x drives the colors map for both violin interior and outline
|
|
29
|
+
const fillChannel = $derived(fill ?? xf)
|
|
21
30
|
|
|
22
31
|
onMount(() => {
|
|
23
32
|
id = plotState.registerGeom({
|
|
24
33
|
type: 'violin',
|
|
25
|
-
channels: { x, y, color: fillChannel },
|
|
34
|
+
channels: { x: xf, y: yf, color: fillChannel, pattern },
|
|
26
35
|
stat,
|
|
27
36
|
options
|
|
28
37
|
})
|
|
@@ -32,17 +41,28 @@
|
|
|
32
41
|
})
|
|
33
42
|
|
|
34
43
|
$effect(() => {
|
|
35
|
-
if (id)
|
|
44
|
+
if (id)
|
|
45
|
+
plotState.updateGeom(id, { channels: { x: xf, y: yf, color: fillChannel, pattern }, stat })
|
|
36
46
|
})
|
|
37
47
|
|
|
38
48
|
const data = $derived(id ? plotState.geomData(id) : [])
|
|
39
49
|
const xScale = $derived(plotState.xScale)
|
|
40
50
|
const yScale = $derived(plotState.yScale)
|
|
41
51
|
const colors = $derived(plotState.colors)
|
|
52
|
+
const patterns = $derived(plotState.patterns)
|
|
42
53
|
|
|
43
54
|
const violins = $derived.by(() => {
|
|
44
55
|
if (!data?.length || !xScale || !yScale) return []
|
|
45
|
-
return buildViolins(
|
|
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
|
+
)
|
|
46
66
|
})
|
|
47
67
|
</script>
|
|
48
68
|
|
|
@@ -60,6 +80,10 @@
|
|
|
60
80
|
onmouseenter={() => plotState.setHovered(v.data)}
|
|
61
81
|
onmouseleave={() => plotState.clearHovered()}
|
|
62
82
|
/>
|
|
83
|
+
{#if v.patternId}
|
|
84
|
+
<!-- Texture fill overlay (matches the violin silhouette) -->
|
|
85
|
+
<path d={v.d} fill="url(#{v.patternId})" pointer-events="none" />
|
|
86
|
+
{/if}
|
|
63
87
|
{/each}
|
|
64
88
|
</g>
|
|
65
89
|
{/if}
|
|
@@ -3,6 +3,11 @@ type Props = {
|
|
|
3
3
|
y?: string;
|
|
4
4
|
fill?: string;
|
|
5
5
|
stat?: string;
|
|
6
|
+
/** Half-violin: 'left'/'right' draw one side (flat edge at the band centre) for
|
|
7
|
+
* raincloud/split plots; 'center' (default) is the full symmetric silhouette. */
|
|
8
|
+
side?: 'left' | 'right' | 'center';
|
|
9
|
+
/** Data field driving a texture fill (from the shared pattern set) instead of a solid fill. */
|
|
10
|
+
pattern?: string;
|
|
6
11
|
options?: {
|
|
7
12
|
opacity?: number;
|
|
8
13
|
};
|
|
@@ -87,14 +87,19 @@
|
|
|
87
87
|
fill = yVal >= 0 ? positiveColor : negativeColor
|
|
88
88
|
}
|
|
89
89
|
|
|
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)
|
|
90
93
|
return {
|
|
91
94
|
key: `${xVal}-${i}`,
|
|
92
|
-
x:
|
|
93
|
-
y:
|
|
94
|
-
width:
|
|
95
|
-
height: Math.max(1,
|
|
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)),
|
|
96
99
|
fill,
|
|
97
|
-
|
|
100
|
+
bandStart: xPos,
|
|
101
|
+
bandEnd: xPos + bw,
|
|
102
|
+
cumY: yScale(cumulative) ?? 0,
|
|
98
103
|
data: d
|
|
99
104
|
}
|
|
100
105
|
})
|
|
@@ -119,11 +124,13 @@
|
|
|
119
124
|
</rect>
|
|
120
125
|
<!-- Connector line to next bar -->
|
|
121
126
|
{#if i < bars.length - 1}
|
|
127
|
+
{@const cs = plotState.place(bar.bandEnd, bar.cumY)}
|
|
128
|
+
{@const ce = plotState.place(bars[i + 1].bandStart, bar.cumY)}
|
|
122
129
|
<line
|
|
123
|
-
x1={
|
|
124
|
-
y1={
|
|
125
|
-
x2={
|
|
126
|
-
y2={
|
|
130
|
+
x1={cs.x}
|
|
131
|
+
y1={cs.y}
|
|
132
|
+
x2={ce.x}
|
|
133
|
+
y2={ce.y}
|
|
127
134
|
stroke="currentColor"
|
|
128
135
|
stroke-width={connectorWidth}
|
|
129
136
|
stroke-dasharray="3 2"
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* @param {Map<unknown, {fill: string, stroke: string}>} colors
|
|
9
9
|
* @param {'linear'|'smooth'|'step'} [curve]
|
|
10
10
|
* @param {Map<unknown, string>} [patterns]
|
|
11
|
+
* @param {Function} [place] - orientation mapper (x, y) => {x, y}
|
|
11
12
|
* @returns {{ d: string, fill: string, stroke: string, key: unknown, patternId: string|null }[]}
|
|
12
13
|
*/
|
|
13
14
|
export function buildAreas(data: Object[], channels: {
|
|
@@ -18,7 +19,7 @@ export function buildAreas(data: Object[], channels: {
|
|
|
18
19
|
}, xScale: Function, yScale: Function, colors: Map<unknown, {
|
|
19
20
|
fill: string;
|
|
20
21
|
stroke: string;
|
|
21
|
-
}>, curve?: "linear" | "smooth" | "step", patterns?: Map<unknown, string
|
|
22
|
+
}>, curve?: "linear" | "smooth" | "step", patterns?: Map<unknown, string>, place?: Function): {
|
|
22
23
|
d: string;
|
|
23
24
|
fill: string;
|
|
24
25
|
stroke: string;
|
|
@@ -45,7 +46,10 @@ export function buildStackedAreas(data: Object[], channels: {
|
|
|
45
46
|
}, xScale: Function, yScale: Function, colors: Map<unknown, {
|
|
46
47
|
fill: string;
|
|
47
48
|
stroke: string;
|
|
48
|
-
}>, curve?: "linear" | "smooth" | "step", patterns?: Map<unknown, string
|
|
49
|
+
}>, curve?: "linear" | "smooth" | "step", patterns?: Map<unknown, string>, place?: (x: any, y: any) => {
|
|
50
|
+
x: any;
|
|
51
|
+
y: any;
|
|
52
|
+
}): {
|
|
49
53
|
d: string;
|
|
50
54
|
fill: string;
|
|
51
55
|
stroke: string;
|
package/dist/geoms/lib/areas.js
CHANGED
|
@@ -11,20 +11,24 @@ import { toPatternId } from '../../lib/brewing/patterns.js'
|
|
|
11
11
|
* @param {Map<unknown, {fill: string, stroke: string}>} colors
|
|
12
12
|
* @param {'linear'|'smooth'|'step'} [curve]
|
|
13
13
|
* @param {Map<unknown, string>} [patterns]
|
|
14
|
+
* @param {Function} [place] - orientation mapper (x, y) => {x, y}
|
|
14
15
|
* @returns {{ d: string, fill: string, stroke: string, key: unknown, patternId: string|null }[]}
|
|
15
16
|
*/
|
|
16
|
-
export function buildAreas(data, channels, xScale, yScale, colors, curve, patterns) {
|
|
17
|
+
export function buildAreas(data, channels, xScale, yScale, colors, curve, patterns, place = (x, y) => ({ x, y })) {
|
|
17
18
|
const { x: xf, y: yf, color: cf, pattern: pf } = channels
|
|
18
19
|
const baseline = yScale.range()[0] // bottom of the chart (y pixel max)
|
|
19
20
|
|
|
20
21
|
const xPos = (d) =>
|
|
21
22
|
typeof xScale.bandwidth === 'function' ? xScale(d[xf]) + xScale.bandwidth() / 2 : xScale(d[xf])
|
|
22
23
|
|
|
24
|
+
// Each area edge (baseline + top) is placed so the area transposes under orientation.
|
|
25
|
+
const toEdge = (d) => ({ base: place(xPos(d), baseline), top: place(xPos(d), yScale(d[yf])) })
|
|
23
26
|
const makeGen = () => {
|
|
24
27
|
const gen = area()
|
|
25
|
-
.
|
|
26
|
-
.y0(
|
|
27
|
-
.
|
|
28
|
+
.x0((p) => p.base.x)
|
|
29
|
+
.y0((p) => p.base.y)
|
|
30
|
+
.x1((p) => p.top.x)
|
|
31
|
+
.y1((p) => p.top.y)
|
|
28
32
|
if (curve === 'smooth') gen.curve(curveCatmullRom)
|
|
29
33
|
else if (curve === 'step') gen.curve(curveStep)
|
|
30
34
|
return gen
|
|
@@ -47,7 +51,7 @@ export function buildAreas(data, channels, xScale, yScale, colors, curve, patter
|
|
|
47
51
|
patternKey !== null && patternKey !== undefined && patterns?.has(patternKey)
|
|
48
52
|
? toPatternId(String(patternKey))
|
|
49
53
|
: null
|
|
50
|
-
return [{ d: makeGen()(sortByX(data)), fill: entry.fill, stroke: 'none', key: null, patternId }]
|
|
54
|
+
return [{ d: makeGen()(sortByX(data).map(toEdge)), fill: entry.fill, stroke: 'none', key: null, patternId }]
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
// Group by color field
|
|
@@ -72,7 +76,7 @@ export function buildAreas(data, channels, xScale, yScale, colors, curve, patter
|
|
|
72
76
|
patternKey !== null && patternKey !== undefined && patterns?.has(patternKey)
|
|
73
77
|
? toPatternId(String(patternKey))
|
|
74
78
|
: null
|
|
75
|
-
return { d: makeGen()(sortByX(rows)), fill: entry.fill, stroke: 'none', key, patternId }
|
|
79
|
+
return { d: makeGen()(sortByX(rows).map(toEdge)), fill: entry.fill, stroke: 'none', key, patternId }
|
|
76
80
|
})
|
|
77
81
|
}
|
|
78
82
|
|
|
@@ -88,7 +92,7 @@ export function buildAreas(data, channels, xScale, yScale, colors, curve, patter
|
|
|
88
92
|
* @param {Map<unknown, string>} [patterns]
|
|
89
93
|
* @returns {{ d: string, fill: string, stroke: string, key: unknown, patternId: string|null }[]}
|
|
90
94
|
*/
|
|
91
|
-
export function buildStackedAreas(data, channels, xScale, yScale, colors, curve, patterns) {
|
|
95
|
+
export function buildStackedAreas(data, channels, xScale, yScale, colors, curve, patterns, place = (x, y) => ({ x, y })) {
|
|
92
96
|
const { x: xf, y: yf, color: cf, pattern: pf } = channels
|
|
93
97
|
if (!cf) return buildAreas(data, channels, xScale, yScale, colors, curve, patterns)
|
|
94
98
|
|
|
@@ -115,11 +119,13 @@ export function buildStackedAreas(data, channels, xScale, yScale, colors, curve,
|
|
|
115
119
|
? xScale(d.data[xf]) + xScale.bandwidth() / 2
|
|
116
120
|
: xScale(d.data[xf])
|
|
117
121
|
|
|
122
|
+
const toEdge = (d) => ({ base: place(xPos(d), yScale(d[0])), top: place(xPos(d), yScale(d[1])) })
|
|
118
123
|
const makeGen = () => {
|
|
119
124
|
const gen = area()
|
|
120
|
-
.
|
|
121
|
-
.y0((
|
|
122
|
-
.
|
|
125
|
+
.x0((p) => p.base.x)
|
|
126
|
+
.y0((p) => p.base.y)
|
|
127
|
+
.x1((p) => p.top.x)
|
|
128
|
+
.y1((p) => p.top.y)
|
|
123
129
|
if (curve === 'smooth') gen.curve(curveCatmullRom)
|
|
124
130
|
else if (curve === 'step') gen.curve(curveStep)
|
|
125
131
|
return gen
|
|
@@ -144,7 +150,7 @@ export function buildStackedAreas(data, channels, xScale, yScale, colors, curve,
|
|
|
144
150
|
? toPatternId(String(patternKey))
|
|
145
151
|
: null
|
|
146
152
|
return {
|
|
147
|
-
d: makeGen()(layer) ?? '',
|
|
153
|
+
d: makeGen()(layer.map(toEdge)) ?? '',
|
|
148
154
|
fill: entry.fill,
|
|
149
155
|
stroke: 'none',
|
|
150
156
|
key: colorKey,
|
package/dist/geoms/lib/bars.d.ts
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The one bar builder. Expresses each bar in abstract (category, value) space and maps it to
|
|
3
|
+
* screen via `place()`, so it renders vertical or horizontal (flip) with the same code.
|
|
4
|
+
*
|
|
5
|
+
* Category (x) positioning has two modes:
|
|
6
|
+
* - band (default): a scaleBand slots each category; sub-bands split grouped series.
|
|
7
|
+
* - continuous (`continuousCategory`): a LINEAR position scale (e.g. a bar-chart race's tweened
|
|
8
|
+
* `_rank`). Each bar centres on `xScale(pos)` with a fixed thickness from the scale step, so a
|
|
9
|
+
* fractional position tweens smoothly (a band scale can only jump between slots). No sub-banding.
|
|
10
|
+
*/
|
|
11
|
+
export function buildBars(data: any, channels: any, xScale: any, yScale: any, colors: any, innerHeight: any, patterns: any, place?: (x: any, y: any) => {
|
|
12
|
+
x: any;
|
|
13
|
+
y: any;
|
|
14
|
+
}, continuousCategory?: boolean): any;
|
|
15
|
+
export function buildStackedBars(data: any, channels: any, xScale: any, yScale: any, colors: any, innerHeight: any, patterns: any, place?: (x: any, y: any) => {
|
|
16
|
+
x: any;
|
|
17
|
+
y: any;
|
|
18
|
+
}): any;
|