@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.
Files changed (86) hide show
  1. package/dist/PlotState.svelte.d.ts +26 -0
  2. package/dist/index.d.ts +6 -1
  3. package/dist/lib/brewing/BoxBrewer.svelte.d.ts +3 -5
  4. package/dist/lib/brewing/QuartileBrewer.svelte.d.ts +9 -0
  5. package/dist/lib/brewing/ViolinBrewer.svelte.d.ts +3 -4
  6. package/dist/lib/brewing/colors.d.ts +10 -1
  7. package/dist/lib/brewing/marks/points.d.ts +17 -2
  8. package/dist/lib/keyboard-nav.d.ts +15 -0
  9. package/dist/lib/plot/preset.d.ts +1 -1
  10. package/dist/lib/preset.d.ts +30 -0
  11. package/package.json +2 -1
  12. package/src/AnimatedPlot.svelte +375 -207
  13. package/src/Chart.svelte +81 -84
  14. package/src/ChartProvider.svelte +10 -0
  15. package/src/FacetPlot/Panel.svelte +30 -16
  16. package/src/FacetPlot.svelte +100 -76
  17. package/src/Plot/Area.svelte +26 -19
  18. package/src/Plot/Axis.svelte +81 -59
  19. package/src/Plot/Bar.svelte +47 -89
  20. package/src/Plot/Grid.svelte +23 -19
  21. package/src/Plot/Legend.svelte +213 -147
  22. package/src/Plot/Line.svelte +31 -21
  23. package/src/Plot/Point.svelte +35 -22
  24. package/src/Plot/Root.svelte +46 -91
  25. package/src/Plot/Timeline.svelte +82 -82
  26. package/src/Plot/Tooltip.svelte +68 -62
  27. package/src/Plot.svelte +290 -174
  28. package/src/PlotState.svelte.js +338 -265
  29. package/src/Sparkline.svelte +95 -56
  30. package/src/charts/AreaChart.svelte +22 -20
  31. package/src/charts/BarChart.svelte +23 -21
  32. package/src/charts/BoxPlot.svelte +15 -15
  33. package/src/charts/BubbleChart.svelte +17 -17
  34. package/src/charts/LineChart.svelte +20 -20
  35. package/src/charts/PieChart.svelte +30 -20
  36. package/src/charts/ScatterPlot.svelte +20 -19
  37. package/src/charts/ViolinPlot.svelte +15 -15
  38. package/src/crossfilter/CrossFilter.svelte +33 -29
  39. package/src/crossfilter/FilterBar.svelte +17 -25
  40. package/src/crossfilter/FilterHistogram.svelte +290 -0
  41. package/src/crossfilter/FilterSlider.svelte +69 -65
  42. package/src/crossfilter/createCrossFilter.svelte.js +94 -90
  43. package/src/geoms/Arc.svelte +114 -69
  44. package/src/geoms/Area.svelte +67 -39
  45. package/src/geoms/Bar.svelte +184 -126
  46. package/src/geoms/Box.svelte +101 -91
  47. package/src/geoms/LabelPill.svelte +11 -11
  48. package/src/geoms/Line.svelte +110 -86
  49. package/src/geoms/Point.svelte +130 -90
  50. package/src/geoms/Violin.svelte +51 -41
  51. package/src/geoms/lib/areas.js +122 -99
  52. package/src/geoms/lib/bars.js +195 -144
  53. package/src/index.js +21 -14
  54. package/src/lib/brewing/BoxBrewer.svelte.js +8 -50
  55. package/src/lib/brewing/CartesianBrewer.svelte.js +11 -7
  56. package/src/lib/brewing/PieBrewer.svelte.js +5 -5
  57. package/src/lib/brewing/QuartileBrewer.svelte.js +51 -0
  58. package/src/lib/brewing/ViolinBrewer.svelte.js +8 -49
  59. package/src/lib/brewing/brewer.svelte.js +242 -195
  60. package/src/lib/brewing/colors.js +34 -5
  61. package/src/lib/brewing/marks/arcs.js +28 -28
  62. package/src/lib/brewing/marks/areas.js +54 -41
  63. package/src/lib/brewing/marks/bars.js +34 -34
  64. package/src/lib/brewing/marks/boxes.js +51 -51
  65. package/src/lib/brewing/marks/lines.js +37 -30
  66. package/src/lib/brewing/marks/points.js +74 -26
  67. package/src/lib/brewing/marks/violins.js +57 -57
  68. package/src/lib/brewing/patterns.js +25 -11
  69. package/src/lib/brewing/scales.js +17 -17
  70. package/src/lib/brewing/stats.js +37 -29
  71. package/src/lib/brewing/symbols.js +1 -1
  72. package/src/lib/chart.js +2 -1
  73. package/src/lib/keyboard-nav.js +37 -0
  74. package/src/lib/plot/crossfilter.js +5 -5
  75. package/src/lib/plot/facet.js +30 -30
  76. package/src/lib/plot/frames.js +30 -29
  77. package/src/lib/plot/helpers.js +4 -4
  78. package/src/lib/plot/preset.js +48 -34
  79. package/src/lib/plot/scales.js +64 -39
  80. package/src/lib/plot/stat.js +47 -47
  81. package/src/lib/preset.js +41 -0
  82. package/src/patterns/DefinePatterns.svelte +24 -24
  83. package/src/patterns/README.md +3 -0
  84. package/src/patterns/patterns.js +328 -176
  85. package/src/patterns/scale.js +61 -32
  86. package/src/spec/chart-spec.js +64 -21
@@ -1,105 +1,145 @@
1
1
  <script>
2
- import { getContext, onMount, onDestroy } from 'svelte'
3
- import { scaleSqrt } from 'd3-scale'
4
- import { buildPoints } from '../lib/brewing/marks/points.js'
5
- import LabelPill from './LabelPill.svelte'
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
- let { x, y, color, size, symbol: symbolField, label = false, stat = 'identity', options = {} } = $props()
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
- * @param {Record<string, unknown>} data
11
- * @returns {string | null}
12
- */
13
- function resolveLabel(data) {
14
- if (!label) return null
15
- if (label === true) return String(data[y] ?? '')
16
- if (typeof label === 'function') return String(label(data) ?? '')
17
- return typeof label === 'string' ? String(data[label] ?? '') : null
18
- }
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
+ }
19
31
 
20
- const plotState = getContext('plot-state')
21
- let id = $state(null)
32
+ const plotState = getContext('plot-state')
33
+ let id = $state(null)
22
34
 
23
- onMount(() => {
24
- id = plotState.registerGeom({ type: 'point', channels: { x, y, color, size, symbol: symbolField }, stat, options })
25
- })
26
- onDestroy(() => { if (id) plotState.unregisterGeom(id) })
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
+ })
27
46
 
28
- $effect(() => {
29
- if (id) plotState.updateGeom(id, { channels: { x, y, color, size, symbol: symbolField }, stat })
30
- })
47
+ $effect(() => {
48
+ if (id) plotState.updateGeom(id, { channels: { x, y, color, size, symbol: symbolField }, stat })
49
+ })
31
50
 
32
- const data = $derived(id ? plotState.geomData(id) : [])
33
- const xScale = $derived(plotState.xScale)
34
- const yScale = $derived(plotState.yScale)
35
- const colors = $derived(plotState.colors)
36
- const symbolMap = $derived(plotState.symbols)
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)
37
56
 
38
- function buildSizeScale() {
39
- if (!size || !data?.length) return null
40
- const vals = data.map((d) => Number(d[size])).filter((v) => !isNaN(v))
41
- if (!vals.length) return null
42
- const minVal = Math.min(...vals)
43
- const maxVal = Math.max(...vals)
44
- const minRadius = options.minRadius ?? 3
45
- const maxRadius = options.maxRadius ?? 20
46
- return scaleSqrt().domain([minVal, maxVal]).range([minRadius, maxRadius])
47
- }
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
+ }
48
67
 
49
- const sizeScale = $derived.by(() => buildSizeScale())
68
+ const sizeScale = $derived.by(() => buildSizeScale())
50
69
 
51
- const defaultRadius = $derived(options.radius ?? 4)
70
+ const defaultRadius = $derived(options.radius ?? 4)
52
71
 
53
- const points = $derived.by(() => {
54
- if (!data?.length || !xScale || !yScale) return []
55
- return buildPoints(data, { x, y, color, size, symbol: symbolField }, xScale, yScale, colors, sizeScale, symbolMap, defaultRadius)
56
- })
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
+ })
57
86
  </script>
58
87
 
59
88
  {#if points.length > 0}
60
- <g data-plot-geom="point">
61
- {#each points as pt, i (`${i}::${pt.data[x]}::${pt.data[y]}`)}
62
- {#if pt.symbolPath}
63
- <path
64
- transform="translate({pt.cx},{pt.cy})"
65
- d={pt.symbolPath}
66
- fill={pt.fill}
67
- stroke={pt.stroke}
68
- stroke-width="1"
69
- fill-opacity={options.opacity ?? 0.8}
70
- data-plot-element="point"
71
- role="graphics-symbol"
72
- aria-label="{pt.data[x]}, {pt.data[y]}"
73
- onmouseenter={() => plotState.setHovered(pt.data)}
74
- onmouseleave={() => plotState.clearHovered()}
75
- />
76
- {:else}
77
- <circle
78
- cx={pt.cx}
79
- cy={pt.cy}
80
- r={pt.r}
81
- fill={pt.fill}
82
- stroke={pt.stroke}
83
- stroke-width="1"
84
- fill-opacity={options.opacity ?? 0.8}
85
- data-plot-element="point"
86
- role="graphics-symbol"
87
- aria-label="{pt.data[x]}, {pt.data[y]}"
88
- onmouseenter={() => plotState.setHovered(pt.data)}
89
- onmouseleave={() => plotState.clearHovered()}
90
- />
91
- {/if}
92
- {#if label}
93
- {@const text = resolveLabel(pt.data)}
94
- {#if text}
95
- <LabelPill
96
- x={pt.cx + (options.labelOffset?.x ?? 0)}
97
- y={pt.cy - pt.r + (options.labelOffset?.y ?? -12)}
98
- {text}
99
- color={pt.stroke ?? '#333'}
100
- />
101
- {/if}
102
- {/if}
103
- {/each}
104
- </g>
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>
105
145
  {/if}
@@ -1,46 +1,56 @@
1
1
  <script>
2
- import { getContext, onMount, onDestroy } from 'svelte'
3
- import { buildViolins } from '../lib/brewing/marks/violins.js'
4
-
5
- let { x, y, fill, stat = 'boxplot', options = {} } = $props()
6
-
7
- const plotState = getContext('plot-state')
8
- let id = $state(null)
9
-
10
- // fill ?? x drives the colors map for both violin interior and outline
11
- const fillChannel = $derived(fill ?? x)
12
-
13
- onMount(() => {
14
- id = plotState.registerGeom({ type: 'violin', channels: { x, y, color: fillChannel }, stat, options })
15
- })
16
- onDestroy(() => { if (id) plotState.unregisterGeom(id) })
17
-
18
- $effect(() => {
19
- if (id) plotState.updateGeom(id, { channels: { x, y, color: fillChannel }, stat })
20
- })
21
-
22
- const data = $derived(id ? plotState.geomData(id) : [])
23
- const xScale = $derived(plotState.xScale)
24
- const yScale = $derived(plotState.yScale)
25
- const colors = $derived(plotState.colors)
26
-
27
- const violins = $derived.by(() => {
28
- if (!data?.length || !xScale || !yScale) return []
29
- return buildViolins(data, { x, fill: fillChannel }, xScale, yScale, colors)
30
- })
2
+ import { getContext, onMount, onDestroy } from 'svelte'
3
+ import { buildViolins } from '../lib/brewing/marks/violins.js'
4
+
5
+ let { x, y, fill, stat = 'boxplot', options = {} } = $props()
6
+
7
+ const plotState = getContext('plot-state')
8
+ let id = $state(null)
9
+
10
+ // fill ?? x drives the colors map for both violin interior and outline
11
+ const fillChannel = $derived(fill ?? x)
12
+
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
+ })
24
+
25
+ $effect(() => {
26
+ if (id) plotState.updateGeom(id, { channels: { x, y, color: fillChannel }, stat })
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
+ })
31
38
  </script>
32
39
 
33
40
  {#if violins.length > 0}
34
- <g data-plot-geom="violin">
35
- {#each violins as v, i (`${String(v.cx) }::${ i}`)}
36
- <path
37
- d={v.d}
38
- fill={v.fill}
39
- fill-opacity="0.5"
40
- stroke={v.stroke}
41
- stroke-width="1.5"
42
- data-plot-element="violin"
43
- />
44
- {/each}
45
- </g>
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>
46
56
  {/if}
@@ -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
- 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) => typeof xScale.bandwidth === 'function'
21
- ? xScale(d[xf]) + xScale.bandwidth() / 2
22
- : xScale(d[xf])
23
-
24
- const makeGen = () => {
25
- const gen = area()
26
- .x(xPos)
27
- .y0(baseline)
28
- .y1((d) => yScale(d[yf]))
29
- if (curve === 'smooth') gen.curve(curveCatmullRom)
30
- else if (curve === 'step') gen.curve(curveStep)
31
- return gen
32
- }
33
-
34
- const sortByX = (rows) => [...rows].sort((a, b) => a[xf] < b[xf] ? -1 : a[xf] > b[xf] ? 1 : 0)
35
-
36
- if (!cf) {
37
- const entry = colors?.values().next().value ?? { fill: '#888', stroke: '#888' }
38
- const patternKey = pf ? data[0]?.[pf] : null
39
- const patternId = patternKey !== null && patternKey !== undefined && patterns?.has(patternKey)
40
- ? toPatternId(String(patternKey)) : null
41
- return [{ d: makeGen()(sortByX(data)), fill: entry.fill, stroke: 'none', key: null, patternId }]
42
- }
43
-
44
- // Group by color field
45
- const groups = new Map()
46
- for (const d of data) {
47
- const key = d[cf]
48
- if (!groups.has(key)) groups.set(key, [])
49
- groups.get(key).push(d)
50
- }
51
- // For different-field patterns, assign positionally so each area gets a distinct pattern
52
- const orderedPatternKeys = pf && pf !== cf ? [...(patterns?.keys() ?? [])] : null
53
-
54
- return [...groups.entries()].map(([key, rows], i) => {
55
- const entry = colors?.get(key) ?? { fill: '#888', stroke: '#888' }
56
- // Same field or no pf: look up by colorKey. Different field: assign positionally.
57
- const patternKey = !pf ? key : pf === cf ? key : (orderedPatternKeys?.[i % orderedPatternKeys.length] ?? null)
58
- const patternId = patternKey !== null && patternKey !== undefined && patterns?.has(patternKey)
59
- ? toPatternId(String(patternKey)) : null
60
- return { d: makeGen()(sortByX(rows)), fill: entry.fill, stroke: 'none', key, patternId }
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
- const { x: xf, y: yf, color: cf, pattern: pf } = channels
78
- if (!cf) return buildAreas(data, channels, xScale, yScale, colors, curve, patterns)
79
-
80
- const xCategories = [...new Set(data.map((d) => d[xf]))]
81
- .sort((a, b) => a < b ? -1 : a > b ? 1 : 0)
82
- const colorCategories = [...new Set(data.map((d) => d[cf]))]
83
-
84
- // Build wide-form lookup: xVal → { colorKey: yVal }
85
- const lookup = new Map()
86
- for (const d of data) {
87
- if (!lookup.has(d[xf])) lookup.set(d[xf], {})
88
- lookup.get(d[xf])[d[cf]] = Number(d[yf])
89
- }
90
-
91
- const wide = xCategories.map((xVal) => {
92
- const row = { [xf]: xVal }
93
- for (const c of colorCategories) row[c] = lookup.get(xVal)?.[c] ?? 0
94
- return row
95
- })
96
-
97
- const xPos = (d) => typeof xScale.bandwidth === 'function'
98
- ? xScale(d.data[xf]) + xScale.bandwidth() / 2
99
- : xScale(d.data[xf])
100
-
101
- const makeGen = () => {
102
- const gen = area()
103
- .x(xPos)
104
- .y0((d) => yScale(d[0]))
105
- .y1((d) => yScale(d[1]))
106
- if (curve === 'smooth') gen.curve(curveCatmullRom)
107
- else if (curve === 'step') gen.curve(curveStep)
108
- return gen
109
- }
110
-
111
- const stackGen = stack().keys(colorCategories)
112
- const layers = stackGen(wide)
113
-
114
- const orderedPatternKeys = pf && pf !== cf ? [...(patterns?.keys() ?? [])] : null
115
-
116
- return layers.map((layer, i) => {
117
- const colorKey = layer.key
118
- const entry = colors?.get(colorKey) ?? { fill: '#888', stroke: '#888' }
119
- // Same field (or no pf): look up by colorKey. Different field: assign positionally.
120
- const patternKey = !pf ? colorKey : pf === cf ? colorKey : (orderedPatternKeys?.[i % orderedPatternKeys.length] ?? null)
121
- const patternId = patternKey !== null && patternKey !== undefined && patterns?.has(patternKey)
122
- ? toPatternId(String(patternKey)) : null
123
- return {
124
- d: makeGen()(layer) ?? '',
125
- fill: entry.fill,
126
- stroke: 'none',
127
- key: colorKey,
128
- patternId
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
  }