@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/Area.svelte
CHANGED
|
@@ -1,73 +1,74 @@
|
|
|
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 { buildAreaMarks } from './lib/marks/area.js'
|
|
5
6
|
import { buildSelectDetail } from '../lib/select.js'
|
|
6
7
|
|
|
7
8
|
type Row = Record<string, unknown>
|
|
8
|
-
|
|
9
9
|
type Options = {
|
|
10
|
+
/** @deprecated use `position="stack"` — kept as an alias. */
|
|
10
11
|
stack?: boolean
|
|
11
12
|
curve?: 'linear' | 'smooth' | 'step'
|
|
12
|
-
opacity?: number
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
type Props = {
|
|
16
16
|
x?: string
|
|
17
17
|
y?: string
|
|
18
|
+
/** Border aesthetic (field); a border is drawn only when both fill and color are set. */
|
|
18
19
|
color?: string
|
|
20
|
+
/** Interior aesthetic (field); falls back to `color`. */
|
|
21
|
+
fill?: string
|
|
19
22
|
pattern?: string
|
|
23
|
+
/** Multi-series arrangement: 'stack' | 'fill' (100%) | 'identity' (overlap, default). */
|
|
24
|
+
position?: 'stack' | 'fill' | 'identity'
|
|
25
|
+
/** Fixed area opacity 0–1; defaults to the per-geom preset (area = 0.6). */
|
|
26
|
+
alpha?: number
|
|
20
27
|
stat?: string
|
|
21
28
|
options?: Options
|
|
22
29
|
}
|
|
23
30
|
|
|
24
|
-
let { x, y, color, pattern, stat = 'identity', options = {} }: Props =
|
|
31
|
+
let { x, y, color, fill, pattern, position, alpha, stat = 'identity', options = {} }: Props =
|
|
32
|
+
$props()
|
|
33
|
+
|
|
34
|
+
// `options.stack: true` is the back-compat alias for position='stack'.
|
|
35
|
+
const resolvedPosition = $derived(position ?? (options?.stack ? 'stack' : 'identity'))
|
|
25
36
|
|
|
26
37
|
const plotState = getContext<PlotState>('plot-state')
|
|
27
|
-
|
|
38
|
+
|
|
39
|
+
const geom = new GeomState(plotState, () => ({
|
|
40
|
+
type: 'area',
|
|
41
|
+
channels: { x, y, color, fill, pattern },
|
|
42
|
+
stat,
|
|
43
|
+
options: { stack: options?.stack ?? false, position: resolvedPosition, curve: options?.curve },
|
|
44
|
+
alpha,
|
|
45
|
+
build: buildAreaMarks
|
|
46
|
+
}))
|
|
47
|
+
onMount(geom.register)
|
|
48
|
+
onDestroy(geom.destroy)
|
|
49
|
+
$effect(geom.sync)
|
|
50
|
+
|
|
51
|
+
const areas = $derived(geom.marks)
|
|
52
|
+
// Per-row tooltip hit-circles are rendered from the geom's rows + shared scales.
|
|
53
|
+
const rows = $derived(geom.data)
|
|
54
|
+
const xScale = $derived(plotState.xScale)
|
|
55
|
+
const yScale = $derived(plotState.yScale)
|
|
56
|
+
|
|
57
|
+
const seriesField = $derived(fill ?? color)
|
|
28
58
|
|
|
29
59
|
function selectPoint(d: Row, event: MouseEvent | KeyboardEvent) {
|
|
30
60
|
if (plotState.interactive)
|
|
31
61
|
plotState.handleSelect(
|
|
32
|
-
buildSelectDetail(
|
|
62
|
+
buildSelectDetail(
|
|
63
|
+
d,
|
|
64
|
+
plotState.data.indexOf(d),
|
|
65
|
+
{ x, y },
|
|
66
|
+
'area',
|
|
67
|
+
seriesField ? d[seriesField] : undefined,
|
|
68
|
+
event
|
|
69
|
+
)
|
|
33
70
|
)
|
|
34
71
|
}
|
|
35
|
-
|
|
36
|
-
onMount(() => {
|
|
37
|
-
id = plotState.registerGeom({
|
|
38
|
-
type: 'area',
|
|
39
|
-
channels: { x, y, color, pattern },
|
|
40
|
-
stat,
|
|
41
|
-
options: { stack: options?.stack ?? false }
|
|
42
|
-
})
|
|
43
|
-
})
|
|
44
|
-
onDestroy(() => {
|
|
45
|
-
if (id) plotState.unregisterGeom(id)
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
$effect(() => {
|
|
49
|
-
if (id)
|
|
50
|
-
plotState.updateGeom(id, {
|
|
51
|
-
channels: { x, y, color, pattern },
|
|
52
|
-
stat,
|
|
53
|
-
options: { stack: options?.stack ?? false }
|
|
54
|
-
})
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
const data = $derived(id ? plotState.geomData(id) : [])
|
|
58
|
-
const xScale = $derived(plotState.xScale)
|
|
59
|
-
const yScale = $derived(plotState.yScale)
|
|
60
|
-
const colors = $derived(plotState.colors)
|
|
61
|
-
const patterns = $derived(plotState.patterns)
|
|
62
|
-
|
|
63
|
-
const areas = $derived.by(() => {
|
|
64
|
-
if (!data?.length || !xScale || !yScale || !x || !y) return []
|
|
65
|
-
const channels = { x, y, color, pattern }
|
|
66
|
-
if (options.stack) {
|
|
67
|
-
return buildStackedAreas(data, channels, xScale, yScale, colors, options.curve, patterns)
|
|
68
|
-
}
|
|
69
|
-
return buildAreas(data, channels, xScale, yScale, colors, options.curve, patterns)
|
|
70
|
-
})
|
|
71
72
|
</script>
|
|
72
73
|
|
|
73
74
|
{#if areas.length > 0}
|
|
@@ -76,7 +77,7 @@
|
|
|
76
77
|
<path
|
|
77
78
|
d={seg.d}
|
|
78
79
|
fill={seg.fill}
|
|
79
|
-
fill-opacity={seg.patternId ? 1 :
|
|
80
|
+
fill-opacity={seg.patternId ? 1 : seg.alpha}
|
|
80
81
|
stroke={seg.stroke ?? 'none'}
|
|
81
82
|
data-plot-element="area"
|
|
82
83
|
/>
|
|
@@ -86,7 +87,7 @@
|
|
|
86
87
|
{/each}
|
|
87
88
|
<!-- Invisible hit circles for tooltip: one per data point -->
|
|
88
89
|
{#if x && y}
|
|
89
|
-
{#each
|
|
90
|
+
{#each rows as d, i (`hover::${i}`)}
|
|
90
91
|
{@const px = typeof xScale?.bandwidth === 'function' ? (xScale(d[x]) ?? 0) + xScale.bandwidth() / 2 : (xScale?.(d[x]) ?? 0)}
|
|
91
92
|
{@const py = yScale?.(d[y]) ?? 0}
|
|
92
93
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
type Options = {
|
|
2
|
+
/** @deprecated use `position="stack"` — kept as an alias. */
|
|
2
3
|
stack?: boolean;
|
|
3
4
|
curve?: 'linear' | 'smooth' | 'step';
|
|
4
|
-
opacity?: number;
|
|
5
5
|
};
|
|
6
6
|
type Props = {
|
|
7
7
|
x?: string;
|
|
8
8
|
y?: string;
|
|
9
|
+
/** Border aesthetic (field); a border is drawn only when both fill and color are set. */
|
|
9
10
|
color?: string;
|
|
11
|
+
/** Interior aesthetic (field); falls back to `color`. */
|
|
12
|
+
fill?: string;
|
|
10
13
|
pattern?: string;
|
|
14
|
+
/** Multi-series arrangement: 'stack' | 'fill' (100%) | 'identity' (overlap, default). */
|
|
15
|
+
position?: 'stack' | 'fill' | 'identity';
|
|
16
|
+
/** Fixed area opacity 0–1; defaults to the per-geom preset (area = 0.6). */
|
|
17
|
+
alpha?: number;
|
|
11
18
|
stat?: string;
|
|
12
19
|
options?: Options;
|
|
13
20
|
};
|
package/dist/geoms/Bar.svelte
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
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 { GeomState } from './lib/GeomState.svelte.js'
|
|
6
|
+
import { buildBarMarks } from './lib/marks/bar.js'
|
|
6
7
|
import { keyboardNav } from '../lib/keyboard-nav.js'
|
|
7
8
|
import { buildSelectDetail } from '../lib/select.js'
|
|
8
9
|
import LabelPill from './LabelPill.svelte'
|
|
@@ -10,6 +11,7 @@
|
|
|
10
11
|
type Row = Record<string, unknown>
|
|
11
12
|
type CrossFilter = ReturnType<typeof createCrossFilter>
|
|
12
13
|
type Options = {
|
|
14
|
+
/** @deprecated use `position="stack"` — kept as an alias. */
|
|
13
15
|
stack?: boolean
|
|
14
16
|
orientation?: 'horizontal' | 'vertical'
|
|
15
17
|
labelInside?: boolean
|
|
@@ -19,9 +21,17 @@
|
|
|
19
21
|
type Props = {
|
|
20
22
|
x?: string
|
|
21
23
|
y?: string
|
|
24
|
+
/** Border/outline aesthetic (field). */
|
|
22
25
|
color?: string
|
|
26
|
+
/** Interior aesthetic (field); falls back to `color`. */
|
|
23
27
|
fill?: string
|
|
24
28
|
pattern?: string
|
|
29
|
+
/** Multi-series arrangement: 'stack' | 'dodge' (default) | 'fill' (100%) | 'identity' (overlap). */
|
|
30
|
+
position?: 'stack' | 'dodge' | 'fill' | 'identity'
|
|
31
|
+
/** Sub-series field for stack/dodge; defaults to `fill ?? color`. */
|
|
32
|
+
group?: string
|
|
33
|
+
/** Fixed bar opacity 0–1; defaults to the per-geom preset (bar = 1). */
|
|
34
|
+
alpha?: number
|
|
25
35
|
label?: boolean | string | ((data: Row) => unknown)
|
|
26
36
|
stat?: string
|
|
27
37
|
options?: Options
|
|
@@ -34,8 +44,11 @@
|
|
|
34
44
|
x,
|
|
35
45
|
y,
|
|
36
46
|
color,
|
|
37
|
-
fill
|
|
47
|
+
fill,
|
|
38
48
|
pattern,
|
|
49
|
+
position,
|
|
50
|
+
group,
|
|
51
|
+
alpha,
|
|
39
52
|
label = false,
|
|
40
53
|
stat = 'identity',
|
|
41
54
|
options = {},
|
|
@@ -44,8 +57,8 @@
|
|
|
44
57
|
keyboard = false
|
|
45
58
|
}: Props = $props()
|
|
46
59
|
|
|
47
|
-
// `
|
|
48
|
-
const
|
|
60
|
+
// `options.stack: true` is the back-compat alias for position='stack'.
|
|
61
|
+
const resolvedPosition = $derived(position ?? (options?.stack ? 'stack' : 'dodge'))
|
|
49
62
|
|
|
50
63
|
function resolveLabel(data: Row, defaultField: string | undefined): string | null {
|
|
51
64
|
if (!label) return null
|
|
@@ -68,48 +81,22 @@
|
|
|
68
81
|
|
|
69
82
|
const plotState = getContext<PlotState>('plot-state')
|
|
70
83
|
const cf = getContext<CrossFilter | undefined>('crossfilter')
|
|
71
|
-
let id = $state<string | null>(null)
|
|
72
|
-
|
|
73
|
-
onMount(() => {
|
|
74
|
-
id = plotState.registerGeom({
|
|
75
|
-
type: 'bar',
|
|
76
|
-
channels: { x, y, color: colorChannel, pattern },
|
|
77
|
-
stat,
|
|
78
|
-
options: { stack: options?.stack ?? false }
|
|
79
|
-
})
|
|
80
|
-
})
|
|
81
|
-
onDestroy(() => {
|
|
82
|
-
if (id) plotState.unregisterGeom(id)
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
$effect(() => {
|
|
86
|
-
if (id)
|
|
87
|
-
plotState.updateGeom(id, {
|
|
88
|
-
channels: { x, y, color: colorChannel, pattern },
|
|
89
|
-
stat,
|
|
90
|
-
options: { stack: options?.stack ?? false }
|
|
91
|
-
})
|
|
92
|
-
})
|
|
93
84
|
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
85
|
+
const geom = new GeomState(plotState, () => ({
|
|
86
|
+
type: 'bar',
|
|
87
|
+
channels: { x, y, color, fill, pattern, group },
|
|
88
|
+
stat,
|
|
89
|
+
options: { stack: options?.stack ?? false, position: resolvedPosition },
|
|
90
|
+
alpha,
|
|
91
|
+
build: buildBarMarks
|
|
92
|
+
}))
|
|
93
|
+
onMount(geom.register)
|
|
94
|
+
onDestroy(geom.destroy)
|
|
95
|
+
$effect(geom.sync)
|
|
96
|
+
|
|
97
|
+
const bars = $derived(geom.marks)
|
|
99
98
|
const effectiveOrientation = $derived(options.orientation ?? plotState.orientation)
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
const bars = $derived.by(() => {
|
|
103
|
-
if (!data?.length || !xScale || !yScale) return []
|
|
104
|
-
const channels = { x, y, color: colorChannel, pattern }
|
|
105
|
-
if (effectiveOrientation === 'horizontal') {
|
|
106
|
-
return buildHorizontalBars(data, channels, xScale, yScale, colors, innerHeight)
|
|
107
|
-
}
|
|
108
|
-
if (options.stack) {
|
|
109
|
-
return buildStackedBars(data, channels, xScale, yScale, colors, innerHeight, patterns)
|
|
110
|
-
}
|
|
111
|
-
return buildGroupedBars(data, channels, xScale, yScale, colors, innerHeight, patterns)
|
|
112
|
-
})
|
|
99
|
+
const seriesField = $derived(fill ?? color)
|
|
113
100
|
|
|
114
101
|
let dimmedByKey = $state<Record<string, boolean>>({})
|
|
115
102
|
|
|
@@ -144,7 +131,7 @@
|
|
|
144
131
|
plotState.data.indexOf(bar.data),
|
|
145
132
|
{ x, y },
|
|
146
133
|
'bar',
|
|
147
|
-
|
|
134
|
+
seriesField ? bar.data[seriesField] : undefined,
|
|
148
135
|
event
|
|
149
136
|
)
|
|
150
137
|
)
|
|
@@ -177,6 +164,7 @@
|
|
|
177
164
|
width={Math.max(0, bar.width)}
|
|
178
165
|
height={Math.max(0, bar.height)}
|
|
179
166
|
fill={bar.fill}
|
|
167
|
+
fill-opacity={bar.alpha}
|
|
180
168
|
stroke={bar.stroke ?? 'none'}
|
|
181
169
|
stroke-width={bar.stroke ? 0.5 : 0}
|
|
182
170
|
data-plot-element="bar"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
type Row = Record<string, unknown>;
|
|
2
2
|
type Options = {
|
|
3
|
+
/** @deprecated use `position="stack"` — kept as an alias. */
|
|
3
4
|
stack?: boolean;
|
|
4
5
|
orientation?: 'horizontal' | 'vertical';
|
|
5
6
|
labelInside?: boolean;
|
|
@@ -8,9 +9,17 @@ type Options = {
|
|
|
8
9
|
type Props = {
|
|
9
10
|
x?: string;
|
|
10
11
|
y?: string;
|
|
12
|
+
/** Border/outline aesthetic (field). */
|
|
11
13
|
color?: string;
|
|
14
|
+
/** Interior aesthetic (field); falls back to `color`. */
|
|
12
15
|
fill?: string;
|
|
13
16
|
pattern?: string;
|
|
17
|
+
/** Multi-series arrangement: 'stack' | 'dodge' (default) | 'fill' (100%) | 'identity' (overlap). */
|
|
18
|
+
position?: 'stack' | 'dodge' | 'fill' | 'identity';
|
|
19
|
+
/** Sub-series field for stack/dodge; defaults to `fill ?? color`. */
|
|
20
|
+
group?: string;
|
|
21
|
+
/** Fixed bar opacity 0–1; defaults to the per-geom preset (bar = 1). */
|
|
22
|
+
alpha?: number;
|
|
14
23
|
label?: boolean | string | ((data: Row) => unknown);
|
|
15
24
|
stat?: string;
|
|
16
25
|
options?: Options;
|
package/dist/geoms/Box.svelte
CHANGED
|
@@ -1,122 +1,164 @@
|
|
|
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 { buildBoxMarks } from './lib/marks/box.js'
|
|
5
6
|
|
|
6
7
|
type Props = {
|
|
7
8
|
x?: string
|
|
8
9
|
y?: string
|
|
10
|
+
/** Box interior aesthetic (field); defaults to `x`. */
|
|
9
11
|
fill?: string
|
|
12
|
+
/** Outline aesthetic (field); overrides whisker/median/border stroke when set. */
|
|
13
|
+
color?: string
|
|
10
14
|
stat?: string
|
|
11
|
-
|
|
15
|
+
/** Offset the box to one half of the band (raincloud/split plots). Default 'center'. */
|
|
16
|
+
side?: 'left' | 'right' | 'center'
|
|
17
|
+
/** Box thickness as a fraction of the (sub-)band — use a small value (e.g. 0.16) for a
|
|
18
|
+
* thin raincloud box. Defaults to the full box width. */
|
|
19
|
+
width?: number
|
|
20
|
+
/** Data field driving a texture fill (from the shared pattern set) instead of a solid fill. */
|
|
21
|
+
pattern?: string
|
|
22
|
+
/** Fixed box opacity 0–1; defaults to the per-geom preset (box = 0.5). */
|
|
23
|
+
alpha?: number
|
|
12
24
|
}
|
|
13
25
|
|
|
14
|
-
let { x, y, fill, stat = 'boxplot',
|
|
26
|
+
let { x, y, fill, color, stat = 'boxplot', side = 'center', width, pattern, alpha }: Props =
|
|
27
|
+
$props()
|
|
15
28
|
|
|
16
29
|
const plotState = getContext<PlotState>('plot-state')
|
|
17
|
-
let id = $state<string | null>(null)
|
|
18
30
|
|
|
19
|
-
//
|
|
20
|
-
|
|
31
|
+
// Fall back to the root Plot channels when the geom's own x/y are omitted, so
|
|
32
|
+
// <Plot.Box> composes under <Plot.Root x y> without silently NaN-ing.
|
|
33
|
+
const xf = $derived(x ?? plotState.channels?.x)
|
|
34
|
+
const yf = $derived(y ?? plotState.channels?.y)
|
|
35
|
+
// fill ?? effective-x drives the colors map for both box interior and whisker strokes
|
|
36
|
+
const fillChannel = $derived(fill ?? xf)
|
|
21
37
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
const geom = new GeomState(plotState, () => ({
|
|
39
|
+
type: 'box',
|
|
40
|
+
channels: { x: xf, y: yf, fill: fillChannel, color, pattern },
|
|
41
|
+
stat,
|
|
42
|
+
options: { side, width },
|
|
43
|
+
alpha,
|
|
44
|
+
build: buildBoxMarks
|
|
45
|
+
}))
|
|
46
|
+
onMount(geom.register)
|
|
47
|
+
onDestroy(geom.destroy)
|
|
48
|
+
$effect(geom.sync)
|
|
33
49
|
|
|
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 boxes = $derived.by(() => {
|
|
44
|
-
if (!data?.length || !xScale || !yScale) return []
|
|
45
|
-
return buildBoxes(data, { x, fill: fillChannel }, xScale, yScale, colors)
|
|
46
|
-
})
|
|
50
|
+
const boxes = $derived(geom.marks)
|
|
47
51
|
</script>
|
|
48
52
|
|
|
49
53
|
{#if boxes.length > 0}
|
|
50
54
|
<g data-plot-geom="box">
|
|
51
55
|
{#each boxes as box, i (`${String(box.cx)}::${i}`)}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
{@const
|
|
55
|
-
{@const
|
|
56
|
+
<!-- Each primitive is placed through plotState.place(band, value) so the whole
|
|
57
|
+
box transposes correctly under orientation='horizontal' (flip). -->
|
|
58
|
+
{@const c1 = plotState.place(box.cx - box.width / 2, box.q3)}
|
|
59
|
+
{@const c2 = plotState.place(box.cx + box.width / 2, box.q1)}
|
|
60
|
+
{@const mA = plotState.place(box.cx - box.width / 2, box.median)}
|
|
61
|
+
{@const mB = plotState.place(box.cx + box.width / 2, box.median)}
|
|
62
|
+
{@const wLoA = plotState.place(box.cx, box.q1)}
|
|
63
|
+
{@const wLoB = plotState.place(box.cx, box.iqr_min)}
|
|
64
|
+
{@const wHiA = plotState.place(box.cx, box.q3)}
|
|
65
|
+
{@const wHiB = plotState.place(box.cx, box.iqr_max)}
|
|
66
|
+
{@const capLoA = plotState.place(box.cx - box.whiskerWidth / 2, box.iqr_min)}
|
|
67
|
+
{@const capLoB = plotState.place(box.cx + box.whiskerWidth / 2, box.iqr_min)}
|
|
68
|
+
{@const capHiA = plotState.place(box.cx - box.whiskerWidth / 2, box.iqr_max)}
|
|
69
|
+
{@const capHiB = plotState.place(box.cx + box.whiskerWidth / 2, box.iqr_max)}
|
|
56
70
|
<!-- Box body (IQR): lighter fill shade -->
|
|
57
71
|
<rect
|
|
58
|
-
x={
|
|
59
|
-
y={
|
|
60
|
-
width={
|
|
61
|
-
height={Math.
|
|
72
|
+
x={Math.min(c1.x, c2.x)}
|
|
73
|
+
y={Math.min(c1.y, c2.y)}
|
|
74
|
+
width={Math.abs(c2.x - c1.x)}
|
|
75
|
+
height={Math.abs(c2.y - c1.y)}
|
|
62
76
|
fill={box.fill}
|
|
63
|
-
fill-opacity={
|
|
77
|
+
fill-opacity={box.alpha}
|
|
64
78
|
stroke={box.stroke}
|
|
65
79
|
stroke-width="1"
|
|
66
80
|
data-plot-element="box-body"
|
|
67
|
-
role="
|
|
81
|
+
role="graphics-symbol"
|
|
82
|
+
aria-label={`${String(box.data[xf ?? ''] ?? 'box')}: median ${box.data.median}, IQR ${box.data.q1}–${box.data.q3}`}
|
|
68
83
|
onmouseenter={() => plotState.setHovered(box.data)}
|
|
69
84
|
onmouseleave={() => plotState.clearHovered()}
|
|
70
85
|
/>
|
|
86
|
+
{#if box.patternId}
|
|
87
|
+
<!-- Texture fill overlay (matches the box body) -->
|
|
88
|
+
<rect
|
|
89
|
+
x={Math.min(c1.x, c2.x)}
|
|
90
|
+
y={Math.min(c1.y, c2.y)}
|
|
91
|
+
width={Math.abs(c2.x - c1.x)}
|
|
92
|
+
height={Math.abs(c2.y - c1.y)}
|
|
93
|
+
fill="url(#{box.patternId})"
|
|
94
|
+
pointer-events="none"
|
|
95
|
+
/>
|
|
96
|
+
{/if}
|
|
71
97
|
<!-- Median line: darker stroke shade -->
|
|
72
98
|
<line
|
|
73
|
-
x1={
|
|
74
|
-
y1={
|
|
75
|
-
x2={
|
|
76
|
-
y2={
|
|
99
|
+
x1={mA.x}
|
|
100
|
+
y1={mA.y}
|
|
101
|
+
x2={mB.x}
|
|
102
|
+
y2={mB.y}
|
|
77
103
|
stroke={box.stroke}
|
|
78
104
|
stroke-width="2"
|
|
79
105
|
data-plot-element="box-median"
|
|
80
106
|
/>
|
|
81
107
|
<!-- Lower whisker (q1 to iqr_min) -->
|
|
82
108
|
<line
|
|
83
|
-
x1={
|
|
84
|
-
y1={
|
|
85
|
-
x2={
|
|
86
|
-
y2={
|
|
109
|
+
x1={wLoA.x}
|
|
110
|
+
y1={wLoA.y}
|
|
111
|
+
x2={wLoB.x}
|
|
112
|
+
y2={wLoB.y}
|
|
87
113
|
stroke={box.stroke}
|
|
88
114
|
stroke-width="1"
|
|
89
115
|
data-plot-element="box-whisker"
|
|
90
116
|
/>
|
|
91
117
|
<!-- Upper whisker (q3 to iqr_max) -->
|
|
92
118
|
<line
|
|
93
|
-
x1={
|
|
94
|
-
y1={
|
|
95
|
-
x2={
|
|
96
|
-
y2={
|
|
119
|
+
x1={wHiA.x}
|
|
120
|
+
y1={wHiA.y}
|
|
121
|
+
x2={wHiB.x}
|
|
122
|
+
y2={wHiB.y}
|
|
97
123
|
stroke={box.stroke}
|
|
98
124
|
stroke-width="1"
|
|
99
125
|
data-plot-element="box-whisker"
|
|
100
126
|
/>
|
|
101
127
|
<!-- Lower whisker cap -->
|
|
102
128
|
<line
|
|
103
|
-
x1={
|
|
104
|
-
y1={
|
|
105
|
-
x2={
|
|
106
|
-
y2={
|
|
129
|
+
x1={capLoA.x}
|
|
130
|
+
y1={capLoA.y}
|
|
131
|
+
x2={capLoB.x}
|
|
132
|
+
y2={capLoB.y}
|
|
107
133
|
stroke={box.stroke}
|
|
108
134
|
stroke-width="1"
|
|
109
135
|
/>
|
|
110
136
|
<!-- Upper whisker cap -->
|
|
111
137
|
<line
|
|
112
|
-
x1={
|
|
113
|
-
y1={
|
|
114
|
-
x2={
|
|
115
|
-
y2={
|
|
138
|
+
x1={capHiA.x}
|
|
139
|
+
y1={capHiA.y}
|
|
140
|
+
x2={capHiB.x}
|
|
141
|
+
y2={capHiB.y}
|
|
116
142
|
stroke={box.stroke}
|
|
117
143
|
stroke-width="1"
|
|
118
144
|
/>
|
|
119
|
-
<!--
|
|
145
|
+
<!-- Outliers: individual points beyond the 1.5·IQR fence -->
|
|
146
|
+
{#each box.outliers as o, oi (`${i}-outlier-${oi}`)}
|
|
147
|
+
{@const op = plotState.place(box.cx, o.cy)}
|
|
148
|
+
<circle
|
|
149
|
+
cx={op.x}
|
|
150
|
+
cy={op.y}
|
|
151
|
+
r="2"
|
|
152
|
+
fill="none"
|
|
153
|
+
stroke={box.stroke}
|
|
154
|
+
stroke-width="1"
|
|
155
|
+
data-plot-element="box-outlier"
|
|
156
|
+
role="graphics-symbol"
|
|
157
|
+
aria-label={`outlier: ${o.value}`}
|
|
158
|
+
onmouseenter={() => plotState.setHovered({ ...box.data, value: o.value })}
|
|
159
|
+
onmouseleave={() => plotState.clearHovered()}
|
|
160
|
+
/>
|
|
161
|
+
{/each}
|
|
120
162
|
{/each}
|
|
121
163
|
</g>
|
|
122
164
|
{/if}
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
type Props = {
|
|
2
2
|
x?: string;
|
|
3
3
|
y?: string;
|
|
4
|
+
/** Box interior aesthetic (field); defaults to `x`. */
|
|
4
5
|
fill?: string;
|
|
6
|
+
/** Outline aesthetic (field); overrides whisker/median/border stroke when set. */
|
|
7
|
+
color?: string;
|
|
5
8
|
stat?: string;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
/** Offset the box to one half of the band (raincloud/split plots). Default 'center'. */
|
|
10
|
+
side?: 'left' | 'right' | 'center';
|
|
11
|
+
/** Box thickness as a fraction of the (sub-)band — use a small value (e.g. 0.16) for a
|
|
12
|
+
* thin raincloud box. Defaults to the full box width. */
|
|
13
|
+
width?: number;
|
|
14
|
+
/** Data field driving a texture fill (from the shared pattern set) instead of a solid fill. */
|
|
15
|
+
pattern?: string;
|
|
16
|
+
/** Fixed box opacity 0–1; defaults to the per-geom preset (box = 0.5). */
|
|
17
|
+
alpha?: number;
|
|
9
18
|
};
|
|
10
19
|
declare const Box: import("svelte").Component<Props, {}, "">;
|
|
11
20
|
type Box = ReturnType<typeof Box>;
|