@rokkit/chart 1.0.0-next.150 → 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 +31 -3
- package/dist/crossfilter/createCrossFilter.svelte.d.ts +13 -15
- 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/brewer.svelte.d.ts +5 -36
- package/dist/lib/brewing/colors.d.ts +10 -1
- package/dist/lib/brewing/marks/points.d.ts +17 -2
- package/dist/lib/brewing/stats.d.ts +5 -13
- package/dist/lib/chart.d.ts +5 -7
- 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 -206
- package/src/Chart.svelte +81 -87
- 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 -182
- package/src/PlotState.svelte.js +339 -267
- 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 +100 -89
- 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 +102 -90
- package/src/geoms/LabelPill.svelte +11 -11
- package/src/geoms/Line.svelte +110 -87
- package/src/geoms/Point.svelte +132 -87
- package/src/geoms/Violin.svelte +45 -33
- 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 +12 -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 +249 -201
- 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 +20 -20
- package/src/lib/brewing/stats.js +40 -28
- package/src/lib/brewing/symbols.js +1 -1
- package/src/lib/chart.js +12 -4
- 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/PatternDef.svelte +1 -1
- 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/Point.svelte
CHANGED
|
@@ -1,100 +1,145 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import { getContext, onMount, onDestroy } from 'svelte'
|
|
3
|
+
import { scaleSqrt } from 'd3-scale'
|
|
4
|
+
import { buildPoints } from '../lib/brewing/marks/points.js'
|
|
5
|
+
import { keyboardNav } from '../lib/keyboard-nav.js'
|
|
6
|
+
import LabelPill from './LabelPill.svelte'
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
let {
|
|
9
|
+
x,
|
|
10
|
+
y,
|
|
11
|
+
color,
|
|
12
|
+
size,
|
|
13
|
+
symbol: symbolField,
|
|
14
|
+
label = false,
|
|
15
|
+
stat = 'identity',
|
|
16
|
+
options = {},
|
|
17
|
+
onselect = undefined,
|
|
18
|
+
keyboard = false
|
|
19
|
+
} = $props()
|
|
8
20
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
21
|
+
/**
|
|
22
|
+
* @param {Record<string, unknown>} data
|
|
23
|
+
* @returns {string | null}
|
|
24
|
+
*/
|
|
25
|
+
function resolveLabel(data) {
|
|
26
|
+
if (!label) return null
|
|
27
|
+
if (label === true) return String(data[y] ?? '')
|
|
28
|
+
if (typeof label === 'function') return String(label(data) ?? '')
|
|
29
|
+
return typeof label === 'string' ? String(data[label] ?? '') : null
|
|
30
|
+
}
|
|
20
31
|
|
|
21
|
-
|
|
22
|
-
|
|
32
|
+
const plotState = getContext('plot-state')
|
|
33
|
+
let id = $state(null)
|
|
23
34
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
35
|
+
onMount(() => {
|
|
36
|
+
id = plotState.registerGeom({
|
|
37
|
+
type: 'point',
|
|
38
|
+
channels: { x, y, color, size, symbol: symbolField },
|
|
39
|
+
stat,
|
|
40
|
+
options
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
onDestroy(() => {
|
|
44
|
+
if (id) plotState.unregisterGeom(id)
|
|
45
|
+
})
|
|
28
46
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
47
|
+
$effect(() => {
|
|
48
|
+
if (id) plotState.updateGeom(id, { channels: { x, y, color, size, symbol: symbolField }, stat })
|
|
49
|
+
})
|
|
32
50
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
51
|
+
const data = $derived(id ? plotState.geomData(id) : [])
|
|
52
|
+
const xScale = $derived(plotState.xScale)
|
|
53
|
+
const yScale = $derived(plotState.yScale)
|
|
54
|
+
const colors = $derived(plotState.colors)
|
|
55
|
+
const symbolMap = $derived(plotState.symbols)
|
|
38
56
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
57
|
+
function buildSizeScale() {
|
|
58
|
+
if (!size || !data?.length) return null
|
|
59
|
+
const vals = data.map((d) => Number(d[size])).filter((v) => !isNaN(v))
|
|
60
|
+
if (!vals.length) return null
|
|
61
|
+
const minVal = Math.min(...vals)
|
|
62
|
+
const maxVal = Math.max(...vals)
|
|
63
|
+
const minRadius = options.minRadius ?? 3
|
|
64
|
+
const maxRadius = options.maxRadius ?? 20
|
|
65
|
+
return scaleSqrt().domain([minVal, maxVal]).range([minRadius, maxRadius])
|
|
66
|
+
}
|
|
47
67
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
68
|
+
const sizeScale = $derived.by(() => buildSizeScale())
|
|
69
|
+
|
|
70
|
+
const defaultRadius = $derived(options.radius ?? 4)
|
|
71
|
+
|
|
72
|
+
const points = $derived.by(() => {
|
|
73
|
+
if (!data?.length || !xScale || !yScale) return []
|
|
74
|
+
return buildPoints(
|
|
75
|
+
data,
|
|
76
|
+
{ x, y, color, size, symbol: symbolField },
|
|
77
|
+
xScale,
|
|
78
|
+
yScale,
|
|
79
|
+
colors,
|
|
80
|
+
sizeScale,
|
|
81
|
+
symbolMap,
|
|
82
|
+
defaultRadius,
|
|
83
|
+
options?.jitter ?? null
|
|
84
|
+
)
|
|
85
|
+
})
|
|
52
86
|
</script>
|
|
53
87
|
|
|
54
88
|
{#if points.length > 0}
|
|
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
|
-
|
|
89
|
+
<g data-plot-geom="point">
|
|
90
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
91
|
+
{#each points as pt, i (`${i}::${pt.data[x]}::${pt.data[y]}`)}
|
|
92
|
+
{#if pt.symbolPath}
|
|
93
|
+
<path
|
|
94
|
+
transform="translate({pt.cx},{pt.cy})"
|
|
95
|
+
d={pt.symbolPath}
|
|
96
|
+
fill={pt.fill}
|
|
97
|
+
stroke={pt.stroke}
|
|
98
|
+
stroke-width="1"
|
|
99
|
+
fill-opacity={options.opacity ?? plotState.chartPreset.opacity.point}
|
|
100
|
+
data-plot-element="point"
|
|
101
|
+
role={onselect || keyboard ? 'button' : 'graphics-symbol'}
|
|
102
|
+
tabindex={onselect || keyboard ? 0 : undefined}
|
|
103
|
+
style:cursor={onselect ? 'pointer' : undefined}
|
|
104
|
+
aria-label="{pt.data[x]}, {pt.data[y]}"
|
|
105
|
+
onmouseenter={() => plotState.setHovered(pt.data)}
|
|
106
|
+
onmouseleave={() => plotState.clearHovered()}
|
|
107
|
+
onclick={onselect ? () => onselect(pt.data) : undefined}
|
|
108
|
+
onkeydown={onselect ? (e) => (e.key === 'Enter' || e.key === ' ') && onselect(pt.data) : undefined}
|
|
109
|
+
use:keyboardNav={keyboard}
|
|
110
|
+
/>
|
|
111
|
+
{:else}
|
|
112
|
+
<circle
|
|
113
|
+
cx={pt.cx}
|
|
114
|
+
cy={pt.cy}
|
|
115
|
+
r={pt.r}
|
|
116
|
+
fill={pt.fill}
|
|
117
|
+
stroke={pt.stroke}
|
|
118
|
+
stroke-width="1"
|
|
119
|
+
fill-opacity={options.opacity ?? plotState.chartPreset.opacity.point}
|
|
120
|
+
data-plot-element="point"
|
|
121
|
+
role={onselect || keyboard ? 'button' : 'graphics-symbol'}
|
|
122
|
+
tabindex={onselect || keyboard ? 0 : undefined}
|
|
123
|
+
style:cursor={onselect ? 'pointer' : undefined}
|
|
124
|
+
aria-label="{pt.data[x]}, {pt.data[y]}"
|
|
125
|
+
onmouseenter={() => plotState.setHovered(pt.data)}
|
|
126
|
+
onmouseleave={() => plotState.clearHovered()}
|
|
127
|
+
onclick={onselect ? () => onselect(pt.data) : undefined}
|
|
128
|
+
onkeydown={onselect ? (e) => (e.key === 'Enter' || e.key === ' ') && onselect(pt.data) : undefined}
|
|
129
|
+
use:keyboardNav={keyboard}
|
|
130
|
+
/>
|
|
131
|
+
{/if}
|
|
132
|
+
{#if label}
|
|
133
|
+
{@const text = resolveLabel(pt.data)}
|
|
134
|
+
{#if text}
|
|
135
|
+
<LabelPill
|
|
136
|
+
x={pt.cx + (options.labelOffset?.x ?? 0)}
|
|
137
|
+
y={pt.cy - pt.r + (options.labelOffset?.y ?? -12)}
|
|
138
|
+
{text}
|
|
139
|
+
color={pt.stroke ?? '#333'}
|
|
140
|
+
/>
|
|
141
|
+
{/if}
|
|
142
|
+
{/if}
|
|
143
|
+
{/each}
|
|
144
|
+
</g>
|
|
100
145
|
{/if}
|
package/src/geoms/Violin.svelte
CHANGED
|
@@ -1,44 +1,56 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import { getContext, onMount, onDestroy } from 'svelte'
|
|
3
|
+
import { buildViolins } from '../lib/brewing/marks/violins.js'
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
let { x, y, fill, stat = 'boxplot', options = {} } = $props()
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
const plotState = getContext('plot-state')
|
|
8
|
+
let id = $state(null)
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
id = plotState.registerGeom({ type: 'violin', channels: { x, y, color: fill ?? x }, stat, options })
|
|
13
|
-
})
|
|
14
|
-
onDestroy(() => { if (id) plotState.unregisterGeom(id) })
|
|
10
|
+
// fill ?? x drives the colors map for both violin interior and outline
|
|
11
|
+
const fillChannel = $derived(fill ?? x)
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
onMount(() => {
|
|
14
|
+
id = plotState.registerGeom({
|
|
15
|
+
type: 'violin',
|
|
16
|
+
channels: { x, y, color: fillChannel },
|
|
17
|
+
stat,
|
|
18
|
+
options
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
onDestroy(() => {
|
|
22
|
+
if (id) plotState.unregisterGeom(id)
|
|
23
|
+
})
|
|
19
24
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const colors = $derived(plotState.colors)
|
|
25
|
+
$effect(() => {
|
|
26
|
+
if (id) plotState.updateGeom(id, { channels: { x, y, color: fillChannel }, stat })
|
|
27
|
+
})
|
|
24
28
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
const data = $derived(id ? plotState.geomData(id) : [])
|
|
30
|
+
const xScale = $derived(plotState.xScale)
|
|
31
|
+
const yScale = $derived(plotState.yScale)
|
|
32
|
+
const colors = $derived(plotState.colors)
|
|
33
|
+
|
|
34
|
+
const violins = $derived.by(() => {
|
|
35
|
+
if (!data?.length || !xScale || !yScale) return []
|
|
36
|
+
return buildViolins(data, { x, fill: fillChannel }, xScale, yScale, colors)
|
|
37
|
+
})
|
|
29
38
|
</script>
|
|
30
39
|
|
|
31
40
|
{#if violins.length > 0}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
<g data-plot-geom="violin">
|
|
42
|
+
{#each violins as v, i (`${String(v.cx)}::${i}`)}
|
|
43
|
+
<path
|
|
44
|
+
d={v.d}
|
|
45
|
+
fill={v.fill}
|
|
46
|
+
fill-opacity={options?.opacity ?? plotState.chartPreset.opacity.violin}
|
|
47
|
+
stroke={v.stroke}
|
|
48
|
+
stroke-width="1.5"
|
|
49
|
+
data-plot-element="violin"
|
|
50
|
+
role="presentation"
|
|
51
|
+
onmouseenter={() => plotState.setHovered(v.data)}
|
|
52
|
+
onmouseleave={() => plotState.clearHovered()}
|
|
53
|
+
/>
|
|
54
|
+
{/each}
|
|
55
|
+
</g>
|
|
44
56
|
{/if}
|
package/src/geoms/lib/areas.js
CHANGED
|
@@ -14,51 +14,66 @@ import { toPatternId } from '../../lib/brewing/patterns.js'
|
|
|
14
14
|
* @returns {{ d: string, fill: string, stroke: string, key: unknown, patternId: string|null }[]}
|
|
15
15
|
*/
|
|
16
16
|
export function buildAreas(data, channels, xScale, yScale, colors, curve, patterns) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
17
|
+
const { x: xf, y: yf, color: cf, pattern: pf } = channels
|
|
18
|
+
const baseline = yScale.range()[0] // bottom of the chart (y pixel max)
|
|
19
|
+
|
|
20
|
+
const xPos = (d) =>
|
|
21
|
+
typeof xScale.bandwidth === 'function' ? xScale(d[xf]) + xScale.bandwidth() / 2 : xScale(d[xf])
|
|
22
|
+
|
|
23
|
+
const makeGen = () => {
|
|
24
|
+
const gen = area()
|
|
25
|
+
.x(xPos)
|
|
26
|
+
.y0(baseline)
|
|
27
|
+
.y1((d) => yScale(d[yf]))
|
|
28
|
+
if (curve === 'smooth') gen.curve(curveCatmullRom)
|
|
29
|
+
else if (curve === 'step') gen.curve(curveStep)
|
|
30
|
+
return gen
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// For band (categorical) x scales, sort by domain index to preserve intended ordering.
|
|
34
|
+
// For continuous scales, sort numerically so the path draws left-to-right.
|
|
35
|
+
const sortByX = (rows) => {
|
|
36
|
+
if (typeof xScale.bandwidth === 'function') {
|
|
37
|
+
const domain = xScale.domain()
|
|
38
|
+
return [...rows].sort((a, b) => domain.indexOf(a[xf]) - domain.indexOf(b[xf]))
|
|
39
|
+
}
|
|
40
|
+
return [...rows].sort((a, b) => (a[xf] < b[xf] ? -1 : a[xf] > b[xf] ? 1 : 0))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!cf) {
|
|
44
|
+
const entry = colors?.values().next().value ?? { fill: '#888', stroke: '#888' }
|
|
45
|
+
const patternKey = pf ? data[0]?.[pf] : null
|
|
46
|
+
const patternId =
|
|
47
|
+
patternKey !== null && patternKey !== undefined && patterns?.has(patternKey)
|
|
48
|
+
? toPatternId(String(patternKey))
|
|
49
|
+
: null
|
|
50
|
+
return [{ d: makeGen()(sortByX(data)), fill: entry.fill, stroke: 'none', key: null, patternId }]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Group by color field
|
|
54
|
+
const groups = new Map()
|
|
55
|
+
for (const d of data) {
|
|
56
|
+
const key = d[cf]
|
|
57
|
+
if (!groups.has(key)) groups.set(key, [])
|
|
58
|
+
groups.get(key).push(d)
|
|
59
|
+
}
|
|
60
|
+
// For different-field patterns, assign positionally so each area gets a distinct pattern
|
|
61
|
+
const orderedPatternKeys = pf && pf !== cf ? [...(patterns?.keys() ?? [])] : null
|
|
62
|
+
|
|
63
|
+
return [...groups.entries()].map(([key, rows], i) => {
|
|
64
|
+
const entry = colors?.get(key) ?? { fill: '#888', stroke: '#888' }
|
|
65
|
+
// Same field or no pf: look up by colorKey. Different field: assign positionally.
|
|
66
|
+
const patternKey = !pf
|
|
67
|
+
? key
|
|
68
|
+
: pf === cf
|
|
69
|
+
? key
|
|
70
|
+
: (orderedPatternKeys?.[i % orderedPatternKeys.length] ?? null)
|
|
71
|
+
const patternId =
|
|
72
|
+
patternKey !== null && patternKey !== undefined && patterns?.has(patternKey)
|
|
73
|
+
? toPatternId(String(patternKey))
|
|
74
|
+
: null
|
|
75
|
+
return { d: makeGen()(sortByX(rows)), fill: entry.fill, stroke: 'none', key, patternId }
|
|
76
|
+
})
|
|
62
77
|
}
|
|
63
78
|
|
|
64
79
|
/**
|
|
@@ -74,58 +89,66 @@ export function buildAreas(data, channels, xScale, yScale, colors, curve, patter
|
|
|
74
89
|
* @returns {{ d: string, fill: string, stroke: string, key: unknown, patternId: string|null }[]}
|
|
75
90
|
*/
|
|
76
91
|
export function buildStackedAreas(data, channels, xScale, yScale, colors, curve, patterns) {
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
92
|
+
const { x: xf, y: yf, color: cf, pattern: pf } = channels
|
|
93
|
+
if (!cf) return buildAreas(data, channels, xScale, yScale, colors, curve, patterns)
|
|
94
|
+
|
|
95
|
+
const xCategories = [...new Set(data.map((d) => d[xf]))].sort((a, b) =>
|
|
96
|
+
a < b ? -1 : a > b ? 1 : 0
|
|
97
|
+
)
|
|
98
|
+
const colorCategories = [...new Set(data.map((d) => d[cf]))]
|
|
99
|
+
|
|
100
|
+
// Build wide-form lookup: xVal → { colorKey: yVal }
|
|
101
|
+
const lookup = new Map()
|
|
102
|
+
for (const d of data) {
|
|
103
|
+
if (!lookup.has(d[xf])) lookup.set(d[xf], {})
|
|
104
|
+
lookup.get(d[xf])[d[cf]] = Number(d[yf])
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const wide = xCategories.map((xVal) => {
|
|
108
|
+
const row = { [xf]: xVal }
|
|
109
|
+
for (const c of colorCategories) row[c] = lookup.get(xVal)?.[c] ?? 0
|
|
110
|
+
return row
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
const xPos = (d) =>
|
|
114
|
+
typeof xScale.bandwidth === 'function'
|
|
115
|
+
? xScale(d.data[xf]) + xScale.bandwidth() / 2
|
|
116
|
+
: xScale(d.data[xf])
|
|
117
|
+
|
|
118
|
+
const makeGen = () => {
|
|
119
|
+
const gen = area()
|
|
120
|
+
.x(xPos)
|
|
121
|
+
.y0((d) => yScale(d[0]))
|
|
122
|
+
.y1((d) => yScale(d[1]))
|
|
123
|
+
if (curve === 'smooth') gen.curve(curveCatmullRom)
|
|
124
|
+
else if (curve === 'step') gen.curve(curveStep)
|
|
125
|
+
return gen
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const stackGen = stack().keys(colorCategories)
|
|
129
|
+
const layers = stackGen(wide)
|
|
130
|
+
|
|
131
|
+
const orderedPatternKeys = pf && pf !== cf ? [...(patterns?.keys() ?? [])] : null
|
|
132
|
+
|
|
133
|
+
return layers.map((layer, i) => {
|
|
134
|
+
const colorKey = layer.key
|
|
135
|
+
const entry = colors?.get(colorKey) ?? { fill: '#888', stroke: '#888' }
|
|
136
|
+
// Same field (or no pf): look up by colorKey. Different field: assign positionally.
|
|
137
|
+
const patternKey = !pf
|
|
138
|
+
? colorKey
|
|
139
|
+
: pf === cf
|
|
140
|
+
? colorKey
|
|
141
|
+
: (orderedPatternKeys?.[i % orderedPatternKeys.length] ?? null)
|
|
142
|
+
const patternId =
|
|
143
|
+
patternKey !== null && patternKey !== undefined && patterns?.has(patternKey)
|
|
144
|
+
? toPatternId(String(patternKey))
|
|
145
|
+
: null
|
|
146
|
+
return {
|
|
147
|
+
d: makeGen()(layer) ?? '',
|
|
148
|
+
fill: entry.fill,
|
|
149
|
+
stroke: 'none',
|
|
150
|
+
key: colorKey,
|
|
151
|
+
patternId
|
|
152
|
+
}
|
|
153
|
+
})
|
|
131
154
|
}
|