@rokkit/chart 1.0.0-next.151 → 1.0.0-next.155
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/PlotState.svelte.d.ts +26 -0
- package/dist/index.d.ts +6 -1
- package/dist/lib/brewing/BoxBrewer.svelte.d.ts +3 -5
- package/dist/lib/brewing/QuartileBrewer.svelte.d.ts +9 -0
- package/dist/lib/brewing/ViolinBrewer.svelte.d.ts +3 -4
- package/dist/lib/brewing/colors.d.ts +10 -1
- package/dist/lib/brewing/marks/points.d.ts +17 -2
- package/dist/lib/keyboard-nav.d.ts +15 -0
- package/dist/lib/plot/preset.d.ts +1 -1
- package/dist/lib/preset.d.ts +30 -0
- package/package.json +2 -1
- package/src/AnimatedPlot.svelte +375 -207
- package/src/Chart.svelte +81 -84
- package/src/ChartProvider.svelte +10 -0
- package/src/FacetPlot/Panel.svelte +30 -16
- package/src/FacetPlot.svelte +100 -76
- package/src/Plot/Area.svelte +26 -19
- package/src/Plot/Axis.svelte +81 -59
- package/src/Plot/Bar.svelte +47 -89
- package/src/Plot/Grid.svelte +23 -19
- package/src/Plot/Legend.svelte +213 -147
- package/src/Plot/Line.svelte +31 -21
- package/src/Plot/Point.svelte +35 -22
- package/src/Plot/Root.svelte +46 -91
- package/src/Plot/Timeline.svelte +82 -82
- package/src/Plot/Tooltip.svelte +68 -62
- package/src/Plot.svelte +290 -174
- package/src/PlotState.svelte.js +338 -265
- package/src/Sparkline.svelte +95 -56
- package/src/charts/AreaChart.svelte +22 -20
- package/src/charts/BarChart.svelte +23 -21
- package/src/charts/BoxPlot.svelte +15 -15
- package/src/charts/BubbleChart.svelte +17 -17
- package/src/charts/LineChart.svelte +20 -20
- package/src/charts/PieChart.svelte +30 -20
- package/src/charts/ScatterPlot.svelte +20 -19
- package/src/charts/ViolinPlot.svelte +15 -15
- package/src/crossfilter/CrossFilter.svelte +33 -29
- package/src/crossfilter/FilterBar.svelte +17 -25
- package/src/crossfilter/FilterHistogram.svelte +290 -0
- package/src/crossfilter/FilterSlider.svelte +69 -65
- package/src/crossfilter/createCrossFilter.svelte.js +94 -90
- package/src/geoms/Arc.svelte +114 -69
- package/src/geoms/Area.svelte +67 -39
- package/src/geoms/Bar.svelte +184 -126
- package/src/geoms/Box.svelte +101 -91
- package/src/geoms/LabelPill.svelte +11 -11
- package/src/geoms/Line.svelte +110 -86
- package/src/geoms/Point.svelte +130 -90
- package/src/geoms/Violin.svelte +51 -41
- package/src/geoms/lib/areas.js +122 -99
- package/src/geoms/lib/bars.js +195 -144
- package/src/index.js +21 -14
- package/src/lib/brewing/BoxBrewer.svelte.js +8 -50
- package/src/lib/brewing/CartesianBrewer.svelte.js +11 -7
- package/src/lib/brewing/PieBrewer.svelte.js +5 -5
- package/src/lib/brewing/QuartileBrewer.svelte.js +51 -0
- package/src/lib/brewing/ViolinBrewer.svelte.js +8 -49
- package/src/lib/brewing/brewer.svelte.js +242 -195
- package/src/lib/brewing/colors.js +34 -5
- package/src/lib/brewing/marks/arcs.js +28 -28
- package/src/lib/brewing/marks/areas.js +54 -41
- package/src/lib/brewing/marks/bars.js +34 -34
- package/src/lib/brewing/marks/boxes.js +51 -51
- package/src/lib/brewing/marks/lines.js +37 -30
- package/src/lib/brewing/marks/points.js +74 -26
- package/src/lib/brewing/marks/violins.js +57 -57
- package/src/lib/brewing/patterns.js +25 -11
- package/src/lib/brewing/scales.js +17 -17
- package/src/lib/brewing/stats.js +37 -29
- package/src/lib/brewing/symbols.js +1 -1
- package/src/lib/chart.js +2 -1
- package/src/lib/keyboard-nav.js +37 -0
- package/src/lib/plot/crossfilter.js +5 -5
- package/src/lib/plot/facet.js +30 -30
- package/src/lib/plot/frames.js +30 -29
- package/src/lib/plot/helpers.js +4 -4
- package/src/lib/plot/preset.js +48 -34
- package/src/lib/plot/scales.js +64 -39
- package/src/lib/plot/stat.js +47 -47
- package/src/lib/preset.js +41 -0
- package/src/patterns/DefinePatterns.svelte +24 -24
- package/src/patterns/README.md +3 -0
- package/src/patterns/patterns.js +328 -176
- package/src/patterns/scale.js +61 -32
- package/src/spec/chart-spec.js +64 -21
package/src/geoms/Arc.svelte
CHANGED
|
@@ -1,81 +1,126 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import { getContext, onMount, onDestroy } from 'svelte'
|
|
3
|
+
import { buildArcs } from '../lib/brewing/marks/arcs.js'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
/**
|
|
6
|
+
* `fill` is the primary prop name; `color` is accepted as an alias for
|
|
7
|
+
* spec-driven usage (Plot.svelte passes `color` to all geoms generically).
|
|
8
|
+
* @type {{ theta?: string, fill?: string, color?: string, pattern?: string, stat?: string, labelFn?: (data: Record<string, unknown>) => string, options?: { innerRadius?: number } }}
|
|
9
|
+
*/
|
|
10
|
+
let {
|
|
11
|
+
theta,
|
|
12
|
+
fill,
|
|
13
|
+
color,
|
|
14
|
+
pattern,
|
|
15
|
+
labelFn = undefined,
|
|
16
|
+
stat = 'identity',
|
|
17
|
+
options = {},
|
|
18
|
+
onselect = undefined
|
|
19
|
+
} = $props()
|
|
11
20
|
|
|
12
|
-
|
|
21
|
+
const fillField = $derived(fill ?? color)
|
|
13
22
|
|
|
14
|
-
|
|
15
|
-
|
|
23
|
+
const plotState = getContext('plot-state')
|
|
24
|
+
let id = $state(null)
|
|
16
25
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
onMount(() => {
|
|
27
|
+
id = plotState.registerGeom({
|
|
28
|
+
type: 'arc',
|
|
29
|
+
channels: { color: fillField, y: theta, pattern },
|
|
30
|
+
stat,
|
|
31
|
+
options
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
onDestroy(() => {
|
|
35
|
+
if (id) plotState.unregisterGeom(id)
|
|
36
|
+
})
|
|
21
37
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
38
|
+
$effect(() => {
|
|
39
|
+
if (id) plotState.updateGeom(id, { channels: { color: fillField, y: theta, pattern }, stat })
|
|
40
|
+
})
|
|
25
41
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
42
|
+
const data = $derived(id ? plotState.geomData(id) : [])
|
|
43
|
+
const colors = $derived(plotState.colors)
|
|
44
|
+
const patterns = $derived(plotState.patterns)
|
|
45
|
+
const w = $derived(plotState.innerWidth)
|
|
46
|
+
const h = $derived(plotState.innerHeight)
|
|
31
47
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
48
|
+
const arcs = $derived.by(() => {
|
|
49
|
+
if (!data?.length) return []
|
|
50
|
+
// Guard: skip until data catches up after a fill-field change.
|
|
51
|
+
// When fillField changes, the $effect updates the geom asynchronously, but
|
|
52
|
+
// this derived runs first with stale data whose rows don't have the new
|
|
53
|
+
// field — causing all keys to be undefined (duplicate key error).
|
|
54
|
+
if (fillField && !(fillField in data[0])) return []
|
|
55
|
+
const innerRadius = ((options.innerRadius ?? 0) * Math.min(w, h)) / 2
|
|
56
|
+
return buildArcs(
|
|
57
|
+
data,
|
|
58
|
+
{ color: fillField, y: theta, pattern },
|
|
59
|
+
colors,
|
|
60
|
+
w,
|
|
61
|
+
h,
|
|
62
|
+
{ innerRadius },
|
|
63
|
+
patterns
|
|
64
|
+
)
|
|
65
|
+
})
|
|
42
66
|
</script>
|
|
43
67
|
|
|
44
68
|
{#if arcs.length > 0}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
69
|
+
<g data-plot-geom="arc" transform="translate({w / 2}, {h / 2})">
|
|
70
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
71
|
+
{#each arcs as arc (arc.key)}
|
|
72
|
+
<path
|
|
73
|
+
d={arc.d}
|
|
74
|
+
fill={arc.fill}
|
|
75
|
+
stroke={arc.stroke}
|
|
76
|
+
stroke-width="1"
|
|
77
|
+
role={onselect ? 'button' : 'presentation'}
|
|
78
|
+
tabindex={onselect ? 0 : undefined}
|
|
79
|
+
style:cursor={onselect ? 'pointer' : undefined}
|
|
80
|
+
data-plot-element="arc"
|
|
81
|
+
onmouseenter={() => plotState.setHovered({ ...arc.data, '%': `${arc.pct}%` })}
|
|
82
|
+
onmouseleave={() => plotState.clearHovered()}
|
|
83
|
+
onclick={onselect ? () => onselect({ ...arc.data, '%': `${arc.pct}%` }) : undefined}
|
|
84
|
+
onkeydown={onselect ? (e) => (e.key === 'Enter' || e.key === ' ') && onselect({ ...arc.data, '%': `${arc.pct}%` }) : undefined}
|
|
85
|
+
/>
|
|
86
|
+
{#if arc.patternId}
|
|
87
|
+
<path
|
|
88
|
+
d={arc.d}
|
|
89
|
+
fill="url(#{arc.patternId})"
|
|
90
|
+
stroke={arc.stroke}
|
|
91
|
+
stroke-width="1"
|
|
92
|
+
pointer-events="none"
|
|
93
|
+
data-plot-element="arc"
|
|
94
|
+
/>
|
|
95
|
+
{/if}
|
|
96
|
+
{#if arc.pct >= 5}
|
|
97
|
+
{@const labelText = labelFn ? String(labelFn(arc.data) ?? '') : `${arc.pct}%`}
|
|
98
|
+
{#if labelText}
|
|
99
|
+
{@const lw = Math.max(36, labelText.length * 7 + 12)}
|
|
100
|
+
<g
|
|
101
|
+
transform="translate({arc.centroid[0]},{arc.centroid[1]})"
|
|
102
|
+
pointer-events="none"
|
|
103
|
+
data-plot-element="arc-label"
|
|
104
|
+
>
|
|
105
|
+
<rect
|
|
106
|
+
x={-lw / 2}
|
|
107
|
+
y="-9"
|
|
108
|
+
width={lw}
|
|
109
|
+
height="18"
|
|
110
|
+
rx="4"
|
|
111
|
+
fill="white"
|
|
112
|
+
fill-opacity="0.82"
|
|
113
|
+
/>
|
|
114
|
+
<text
|
|
115
|
+
text-anchor="middle"
|
|
116
|
+
dominant-baseline="central"
|
|
117
|
+
font-size="11"
|
|
118
|
+
font-weight="600"
|
|
119
|
+
fill={arc.stroke}>{labelText}</text
|
|
120
|
+
>
|
|
121
|
+
</g>
|
|
122
|
+
{/if}
|
|
123
|
+
{/if}
|
|
124
|
+
{/each}
|
|
125
|
+
</g>
|
|
81
126
|
{/if}
|
package/src/geoms/Area.svelte
CHANGED
|
@@ -1,50 +1,78 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import { getContext, onMount, onDestroy } from 'svelte'
|
|
3
|
+
import { buildAreas, buildStackedAreas } from './lib/areas.js'
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
let { x, y, color, pattern, stat = 'identity', options = {} } = $props()
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
const plotState = getContext('plot-state')
|
|
8
|
+
let id = $state(null)
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
onMount(() => {
|
|
11
|
+
id = plotState.registerGeom({
|
|
12
|
+
type: 'area',
|
|
13
|
+
channels: { x, y, color, pattern },
|
|
14
|
+
stat,
|
|
15
|
+
options: { stack: options?.stack ?? false }
|
|
16
|
+
})
|
|
17
|
+
})
|
|
18
|
+
onDestroy(() => {
|
|
19
|
+
if (id) plotState.unregisterGeom(id)
|
|
20
|
+
})
|
|
14
21
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
22
|
+
$effect(() => {
|
|
23
|
+
if (id)
|
|
24
|
+
plotState.updateGeom(id, {
|
|
25
|
+
channels: { x, y, color, pattern },
|
|
26
|
+
stat,
|
|
27
|
+
options: { stack: options?.stack ?? false }
|
|
28
|
+
})
|
|
29
|
+
})
|
|
18
30
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
const data = $derived(id ? plotState.geomData(id) : [])
|
|
32
|
+
const xScale = $derived(plotState.xScale)
|
|
33
|
+
const yScale = $derived(plotState.yScale)
|
|
34
|
+
const colors = $derived(plotState.colors)
|
|
35
|
+
const patterns = $derived(plotState.patterns)
|
|
24
36
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
const areas = $derived.by(() => {
|
|
38
|
+
if (!data?.length || !xScale || !yScale) return []
|
|
39
|
+
const channels = { x, y, color, pattern }
|
|
40
|
+
if (options.stack) {
|
|
41
|
+
return buildStackedAreas(data, channels, xScale, yScale, colors, options.curve, patterns)
|
|
42
|
+
}
|
|
43
|
+
return buildAreas(data, channels, xScale, yScale, colors, options.curve, patterns)
|
|
44
|
+
})
|
|
33
45
|
</script>
|
|
34
46
|
|
|
35
47
|
{#if areas.length > 0}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
<g data-plot-geom="area">
|
|
49
|
+
{#each areas as seg (seg.key ?? seg.d)}
|
|
50
|
+
<path
|
|
51
|
+
d={seg.d}
|
|
52
|
+
fill={seg.fill}
|
|
53
|
+
fill-opacity={seg.patternId ? 1 : (options.opacity ?? plotState.chartPreset.opacity.area)}
|
|
54
|
+
stroke={seg.stroke ?? 'none'}
|
|
55
|
+
data-plot-element="area"
|
|
56
|
+
/>
|
|
57
|
+
{#if seg.patternId}
|
|
58
|
+
<path d={seg.d} fill="url(#{seg.patternId})" data-plot-element="area" />
|
|
59
|
+
{/if}
|
|
60
|
+
{/each}
|
|
61
|
+
<!-- Invisible hit circles for tooltip: one per data point -->
|
|
62
|
+
{#each data as d, i (`hover::${i}`)}
|
|
63
|
+
{@const px = typeof xScale?.bandwidth === 'function' ? (xScale(d[x]) ?? 0) + xScale.bandwidth() / 2 : (xScale?.(d[x]) ?? 0)}
|
|
64
|
+
{@const py = yScale?.(d[y]) ?? 0}
|
|
65
|
+
<circle
|
|
66
|
+
cx={px}
|
|
67
|
+
cy={py}
|
|
68
|
+
r="8"
|
|
69
|
+
fill="transparent"
|
|
70
|
+
stroke="none"
|
|
71
|
+
role="presentation"
|
|
72
|
+
data-plot-element="area-hover"
|
|
73
|
+
onmouseenter={() => plotState.setHovered(d)}
|
|
74
|
+
onmouseleave={() => plotState.clearHovered()}
|
|
75
|
+
/>
|
|
76
|
+
{/each}
|
|
77
|
+
</g>
|
|
50
78
|
{/if}
|
package/src/geoms/Bar.svelte
CHANGED
|
@@ -1,142 +1,200 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import { getContext, onMount, onDestroy } from 'svelte'
|
|
3
|
+
import { buildGroupedBars, buildStackedBars, buildHorizontalBars } from './lib/bars.js'
|
|
4
|
+
import { keyboardNav } from '../lib/keyboard-nav.js'
|
|
5
|
+
import LabelPill from './LabelPill.svelte'
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
let {
|
|
8
|
+
x,
|
|
9
|
+
y,
|
|
10
|
+
color,
|
|
11
|
+
fill: fillProp,
|
|
12
|
+
pattern,
|
|
13
|
+
label = false,
|
|
14
|
+
stat = 'identity',
|
|
15
|
+
options = {},
|
|
16
|
+
filterable = false,
|
|
17
|
+
onselect = undefined,
|
|
18
|
+
keyboard = false
|
|
19
|
+
} = $props()
|
|
7
20
|
|
|
8
|
-
|
|
9
|
-
|
|
21
|
+
// `fill` is accepted as an alias for `color` (consistent with Arc.svelte)
|
|
22
|
+
const colorChannel = $derived(fillProp ?? color)
|
|
10
23
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
/**
|
|
25
|
+
* @param {Record<string, unknown>} data
|
|
26
|
+
* @param {string} defaultField
|
|
27
|
+
* @returns {string | null}
|
|
28
|
+
*/
|
|
29
|
+
function resolveLabel(data, defaultField) {
|
|
30
|
+
if (!label) return null
|
|
31
|
+
if (label === true) return String(data[defaultField] ?? '')
|
|
32
|
+
if (typeof label === 'function') return String(label(data) ?? '')
|
|
33
|
+
if (typeof label === 'string') return String(data[label] ?? '')
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
23
36
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Pick white or dark text based on perceived luminance of a hex fill color.
|
|
39
|
+
* @param {string | undefined} hex
|
|
40
|
+
* @returns {string}
|
|
41
|
+
*/
|
|
42
|
+
function contrastColor(hex) {
|
|
43
|
+
if (!hex || !hex.startsWith('#') || hex.length < 7) return 'white'
|
|
44
|
+
const r = parseInt(hex.slice(1, 3), 16) / 255
|
|
45
|
+
const g = parseInt(hex.slice(3, 5), 16) / 255
|
|
46
|
+
const b = parseInt(hex.slice(5, 7), 16) / 255
|
|
47
|
+
return 0.299 * r + 0.587 * g + 0.114 * b > 0.55 ? '#333' : 'white'
|
|
48
|
+
}
|
|
27
49
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
onDestroy(() => {
|
|
32
|
-
if (id) plotState.unregisterGeom(id)
|
|
33
|
-
})
|
|
50
|
+
const plotState = getContext('plot-state')
|
|
51
|
+
const cf = getContext('crossfilter')
|
|
52
|
+
let id = $state(null)
|
|
34
53
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
54
|
+
onMount(() => {
|
|
55
|
+
id = plotState.registerGeom({
|
|
56
|
+
type: 'bar',
|
|
57
|
+
channels: { x, y, color: colorChannel, pattern },
|
|
58
|
+
stat,
|
|
59
|
+
options: { stack: options?.stack ?? false }
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
onDestroy(() => {
|
|
63
|
+
if (id) plotState.unregisterGeom(id)
|
|
64
|
+
})
|
|
38
65
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
66
|
+
$effect(() => {
|
|
67
|
+
if (id)
|
|
68
|
+
plotState.updateGeom(id, {
|
|
69
|
+
channels: { x, y, color: colorChannel, pattern },
|
|
70
|
+
stat,
|
|
71
|
+
options: { stack: options?.stack ?? false }
|
|
72
|
+
})
|
|
73
|
+
})
|
|
46
74
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return buildStackedBars(data, channels, xScale, yScale, colors, innerHeight, patterns)
|
|
55
|
-
}
|
|
56
|
-
return buildGroupedBars(data, channels, xScale, yScale, colors, innerHeight, patterns)
|
|
57
|
-
})
|
|
75
|
+
const data = $derived(id ? plotState.geomData(id) : [])
|
|
76
|
+
const xScale = $derived(plotState.xScale)
|
|
77
|
+
const yScale = $derived(plotState.yScale)
|
|
78
|
+
const colors = $derived(plotState.colors)
|
|
79
|
+
const patterns = $derived(plotState.patterns)
|
|
80
|
+
const effectiveOrientation = $derived(options.orientation ?? plotState.orientation)
|
|
81
|
+
const innerHeight = $derived(plotState.innerHeight)
|
|
58
82
|
|
|
59
|
-
|
|
60
|
-
|
|
83
|
+
const bars = $derived.by(() => {
|
|
84
|
+
if (!data?.length || !xScale || !yScale) return []
|
|
85
|
+
const channels = { x, y, color: colorChannel, pattern }
|
|
86
|
+
if (effectiveOrientation === 'horizontal') {
|
|
87
|
+
return buildHorizontalBars(data, channels, xScale, yScale, colors, innerHeight)
|
|
88
|
+
}
|
|
89
|
+
if (options.stack) {
|
|
90
|
+
return buildStackedBars(data, channels, xScale, yScale, colors, innerHeight, patterns)
|
|
91
|
+
}
|
|
92
|
+
return buildGroupedBars(data, channels, xScale, yScale, colors, innerHeight, patterns)
|
|
93
|
+
})
|
|
61
94
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
// cf.version is a $state counter that increments on every filter mutation.
|
|
65
|
-
// Reading it here establishes a reactive dependency so the effect re-runs
|
|
66
|
-
// whenever any filter changes — including changes from sibling FilterBars.
|
|
67
|
-
void cf.version
|
|
68
|
-
const next = /** @type {Record<string, boolean>} */ ({})
|
|
69
|
-
for (const bar of bars) {
|
|
70
|
-
const dimmedByX = x ? cf.isDimmed(x, bar.data[x]) : false
|
|
71
|
-
const dimmedByY = y ? cf.isDimmed(y, bar.data[y]) : false
|
|
72
|
-
next[bar.key] = dimmedByX || dimmedByY
|
|
73
|
-
}
|
|
74
|
-
dimmedByKey = next
|
|
75
|
-
})
|
|
95
|
+
/** @type {Record<string, boolean>} */
|
|
96
|
+
let dimmedByKey = $state({})
|
|
76
97
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
98
|
+
$effect(() => {
|
|
99
|
+
if (!cf) {
|
|
100
|
+
dimmedByKey = {}
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
// cf.version is a $state counter that increments on every filter mutation.
|
|
104
|
+
// Reading it here establishes a reactive dependency so the effect re-runs
|
|
105
|
+
// whenever any filter changes — including changes from sibling FilterBars.
|
|
106
|
+
void cf.version
|
|
107
|
+
const next = /** @type {Record<string, boolean>} */ ({})
|
|
108
|
+
for (const bar of bars) {
|
|
109
|
+
const dimmedByX = x ? cf.isDimmed(x, bar.data[x]) : false
|
|
110
|
+
const dimmedByY = y ? cf.isDimmed(y, bar.data[y]) : false
|
|
111
|
+
next[bar.key] = dimmedByX || dimmedByY
|
|
112
|
+
}
|
|
113
|
+
dimmedByKey = next
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
function handleBarClick(barX) {
|
|
117
|
+
if (!filterable || !x || !cf) return
|
|
118
|
+
cf.toggleCategorical(x, barX)
|
|
119
|
+
}
|
|
81
120
|
</script>
|
|
82
121
|
|
|
83
122
|
{#if bars.length > 0}
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
123
|
+
<g data-plot-geom="bar">
|
|
124
|
+
{#each bars as bar (bar.key)}
|
|
125
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
126
|
+
<rect
|
|
127
|
+
x={bar.x}
|
|
128
|
+
y={bar.y}
|
|
129
|
+
width={Math.max(0, bar.width)}
|
|
130
|
+
height={Math.max(0, bar.height)}
|
|
131
|
+
fill={bar.fill}
|
|
132
|
+
stroke={bar.stroke ?? 'none'}
|
|
133
|
+
stroke-width={bar.stroke ? 0.5 : 0}
|
|
134
|
+
data-plot-element="bar"
|
|
135
|
+
data-plot-value={bar.data[y]}
|
|
136
|
+
data-plot-category={bar.data[x]}
|
|
137
|
+
data-dimmed={dimmedByKey[bar.key] ? true : undefined}
|
|
138
|
+
style:cursor={filterable || onselect ? 'pointer' : undefined}
|
|
139
|
+
onclick={filterable && x ? () => { handleBarClick(bar.data[x]); onselect?.(bar.data) } : onselect ? () => onselect(bar.data) : undefined}
|
|
140
|
+
onkeydown={filterable && x
|
|
141
|
+
? (e) => (e.key === 'Enter' || e.key === ' ') && handleBarClick(bar.data[x])
|
|
142
|
+
: onselect ? (e) => (e.key === 'Enter' || e.key === ' ') && onselect(bar.data) : undefined}
|
|
143
|
+
role={filterable || onselect || keyboard ? 'button' : 'graphics-symbol'}
|
|
144
|
+
tabindex={filterable || onselect || keyboard ? 0 : undefined}
|
|
145
|
+
use:keyboardNav={keyboard}
|
|
146
|
+
aria-label="{bar.data[x]}: {bar.data[y]}"
|
|
147
|
+
onmouseenter={() => plotState.setHovered(bar.data)}
|
|
148
|
+
onmouseleave={() => plotState.clearHovered()}
|
|
149
|
+
>
|
|
150
|
+
<title>{bar.data[x]}: {bar.data[y]}</title>
|
|
151
|
+
</rect>
|
|
152
|
+
{#if bar.patternId}
|
|
153
|
+
<rect
|
|
154
|
+
x={bar.x}
|
|
155
|
+
y={bar.y}
|
|
156
|
+
width={Math.max(0, bar.width)}
|
|
157
|
+
height={Math.max(0, bar.height)}
|
|
158
|
+
fill="url(#{bar.patternId})"
|
|
159
|
+
pointer-events="none"
|
|
160
|
+
/>
|
|
161
|
+
{/if}
|
|
162
|
+
{#if label}
|
|
163
|
+
{@const text = resolveLabel(bar.data, effectiveOrientation === 'horizontal' ? x : y)}
|
|
164
|
+
{#if text}
|
|
165
|
+
{#if effectiveOrientation === 'horizontal'}
|
|
166
|
+
{#if options.labelInside}
|
|
167
|
+
{@const estimatedWidth = text.length * 7 + 16}
|
|
168
|
+
{@const fitsInside = bar.width >= estimatedWidth}
|
|
169
|
+
<text
|
|
170
|
+
x={fitsInside ? bar.x + bar.width - 8 : bar.x + bar.width + 6}
|
|
171
|
+
y={bar.y + bar.height / 2}
|
|
172
|
+
dominant-baseline="central"
|
|
173
|
+
text-anchor={fitsInside ? 'end' : 'start'}
|
|
174
|
+
font-size="11"
|
|
175
|
+
font-weight="600"
|
|
176
|
+
fill={fitsInside ? contrastColor(bar.fill) : (bar.stroke ?? '#555')}
|
|
177
|
+
pointer-events="none"
|
|
178
|
+
data-plot-element="label"
|
|
179
|
+
>{text}</text>
|
|
180
|
+
{:else}
|
|
181
|
+
<LabelPill
|
|
182
|
+
x={bar.x + bar.width + (options.labelOffset ?? 8)}
|
|
183
|
+
y={bar.y + bar.height / 2}
|
|
184
|
+
{text}
|
|
185
|
+
color={bar.stroke ?? '#333'}
|
|
186
|
+
/>
|
|
187
|
+
{/if}
|
|
188
|
+
{:else}
|
|
189
|
+
<LabelPill
|
|
190
|
+
x={bar.x + bar.width / 2}
|
|
191
|
+
y={bar.y + (options.labelOffset ?? -8)}
|
|
192
|
+
{text}
|
|
193
|
+
color={bar.stroke ?? '#333'}
|
|
194
|
+
/>
|
|
195
|
+
{/if}
|
|
196
|
+
{/if}
|
|
197
|
+
{/if}
|
|
198
|
+
{/each}
|
|
199
|
+
</g>
|
|
142
200
|
{/if}
|