@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
|
@@ -23,208 +23,255 @@ const DEFAULT_MARGIN = { top: 20, right: 20, bottom: 40, left: 50 }
|
|
|
23
23
|
* @returns {{ field: string, items: { label: string, fill: string|null, stroke: string|null, patternId: string|null, shape: string|null }[] }[]}
|
|
24
24
|
*/
|
|
25
25
|
function addAesthetic(byField, field, aesthetic, keys) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
if (byField.has(field)) {
|
|
27
|
+
byField.get(field).aesthetics.push(aesthetic)
|
|
28
|
+
} else {
|
|
29
|
+
byField.set(field, { aesthetics: [aesthetic], keys })
|
|
30
|
+
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
function buildLegendItem(key, aesthetics, colorMap, patternMap, symbolMap) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
const hasColor = aesthetics.includes('color')
|
|
35
|
+
return {
|
|
36
|
+
label: String(key),
|
|
37
|
+
fill: hasColor ? (colorMap.get(key)?.fill ?? null) : null,
|
|
38
|
+
stroke: hasColor ? (colorMap.get(key)?.stroke ?? null) : null,
|
|
39
|
+
patternId: aesthetics.includes('pattern') && patternMap.has(key) ? toPatternId(key) : null,
|
|
40
|
+
shape: aesthetics.includes('symbol') ? (symbolMap.get(key) ?? 'circle') : null
|
|
41
|
+
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
export function buildLegendGroups(channels, colorMap, patternMap, symbolMap) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
45
|
+
const cf = channels.fill ?? channels.color
|
|
46
|
+
const { pattern: pf, symbol: sf } = channels
|
|
47
|
+
const byField = new SvelteMap()
|
|
48
|
+
|
|
49
|
+
if (cf) byField.set(cf, { aesthetics: ['color'], keys: [...colorMap.keys()] })
|
|
50
|
+
if (pf) addAesthetic(byField, pf, 'pattern', [...patternMap.keys()])
|
|
51
|
+
if (sf) addAesthetic(byField, sf, 'symbol', [...symbolMap.keys()])
|
|
52
|
+
|
|
53
|
+
return [...byField.entries()]
|
|
54
|
+
.map(([field, { aesthetics, keys }]) => ({
|
|
55
|
+
field,
|
|
56
|
+
items: keys
|
|
57
|
+
.filter((k) => k !== null && k !== undefined)
|
|
58
|
+
.map((key) => buildLegendItem(key, aesthetics, colorMap, patternMap, symbolMap))
|
|
59
|
+
}))
|
|
60
|
+
.filter((group) => group.items.length > 0)
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
export class ChartBrewer {
|
|
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
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
64
|
+
#rawData = $state([])
|
|
65
|
+
#channels = $state({})
|
|
66
|
+
#width = $state(600)
|
|
67
|
+
#height = $state(400)
|
|
68
|
+
#mode = $state('light')
|
|
69
|
+
#margin = $state(DEFAULT_MARGIN)
|
|
70
|
+
#layers = $state([])
|
|
71
|
+
#curve = $state(/** @type {'linear'|'smooth'|'step'|undefined} */ (undefined))
|
|
72
|
+
#stat = $state('identity')
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Override in subclasses to apply stat aggregation.
|
|
76
|
+
* @param {Object[]} data
|
|
77
|
+
* @param {Object} channels
|
|
78
|
+
* @param {string|Function} stat
|
|
79
|
+
* @returns {Object[]}
|
|
80
|
+
*/
|
|
81
|
+
transform(data, _channels, _stat) {
|
|
82
|
+
return data
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Aggregated data — all derived marks read this, not #rawData */
|
|
86
|
+
processedData = $derived(this.transform(this.#rawData, this.#channels, this.#stat))
|
|
87
|
+
|
|
88
|
+
/** Exposes channels to subclasses for use in their own $derived properties */
|
|
89
|
+
get channels() {
|
|
90
|
+
return this.#channels
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Maps are built from rawData so the legend always reflects the full set of
|
|
94
|
+
// original values — independent of whichever stat aggregation is applied.
|
|
95
|
+
// e.g. pattern=quarter with stat=sum still shows all 8 quarters in the legend.
|
|
96
|
+
|
|
97
|
+
/** @type {Map<unknown, {fill:string,stroke:string}>} */
|
|
98
|
+
colorMap = $derived(
|
|
99
|
+
(this.#channels.fill ?? this.#channels.color)
|
|
100
|
+
? assignColors(
|
|
101
|
+
distinct(this.#rawData, this.#channels.fill ?? this.#channels.color),
|
|
102
|
+
this.#mode
|
|
103
|
+
)
|
|
104
|
+
: new SvelteMap()
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
/** @type {Map<unknown, string>} */
|
|
108
|
+
patternMap = $derived(
|
|
109
|
+
this.#channels.pattern
|
|
110
|
+
? assignPatterns(distinct(this.#rawData, this.#channels.pattern))
|
|
111
|
+
: new SvelteMap()
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Unified pattern defs for ChartPatternDefs.
|
|
116
|
+
* When fill and pattern map the same field, pattern key = color key (simple case).
|
|
117
|
+
* When they differ, each unique (fillKey, patternKey) pair gets its own pattern def
|
|
118
|
+
* so bars/areas can have distinct colors per region AND distinct textures per category.
|
|
119
|
+
* @type {Array<{ id: string, name: string, fill: string, stroke: string }>}
|
|
120
|
+
*/
|
|
121
|
+
patternDefs = $derived(
|
|
122
|
+
(() => {
|
|
123
|
+
const pf = this.#channels.pattern
|
|
124
|
+
const ff = this.#channels.fill ?? this.#channels.color
|
|
125
|
+
if (!pf || this.patternMap.size === 0) return []
|
|
126
|
+
if (!ff || pf === ff) {
|
|
127
|
+
// Same field: pattern key = fill key — simple 1:1 lookup
|
|
128
|
+
return Array.from(this.patternMap.entries()).map(([key, name]) => {
|
|
129
|
+
const color = this.colorMap.get(key) ?? { fill: '#ddd', stroke: '#666' }
|
|
130
|
+
return { id: toPatternId(key), name, fill: color.fill, stroke: color.stroke }
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
// Different fields: need two sets of defs in the SVG:
|
|
134
|
+
// 1. Simple defs (neutral background) — referenced by legend swatches via toPatternId(patternKey)
|
|
135
|
+
// 2. Composite defs (fill-colored background) — referenced by bars via toPatternId(fillKey::patternKey)
|
|
136
|
+
const defs = []
|
|
137
|
+
for (const [pk, name] of this.patternMap.entries()) {
|
|
138
|
+
defs.push({ id: toPatternId(pk), name, fill: '#ddd', stroke: '#666' })
|
|
139
|
+
}
|
|
140
|
+
const seenComposite = new SvelteSet()
|
|
141
|
+
for (const d of this.processedData) {
|
|
142
|
+
const fk = d[ff]
|
|
143
|
+
const pk = d[pf]
|
|
144
|
+
if (pk === null || pk === undefined) continue
|
|
145
|
+
const compositeKey = `${fk}::${pk}`
|
|
146
|
+
if (seenComposite.has(compositeKey)) continue
|
|
147
|
+
seenComposite.add(compositeKey)
|
|
148
|
+
const name = this.patternMap.get(pk) ?? PATTERN_ORDER[0]
|
|
149
|
+
const color = this.colorMap.get(fk) ?? { fill: '#ddd', stroke: '#666' }
|
|
150
|
+
defs.push({ id: toPatternId(compositeKey), name, fill: color.fill, stroke: color.stroke })
|
|
151
|
+
}
|
|
152
|
+
return defs
|
|
153
|
+
})()
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
/** @type {Map<unknown, string>} */
|
|
157
|
+
symbolMap = $derived(
|
|
158
|
+
this.#channels.symbol
|
|
159
|
+
? assignSymbols(distinct(this.#rawData, this.#channels.symbol))
|
|
160
|
+
: new SvelteMap()
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
get innerWidth() {
|
|
164
|
+
return this.#width - this.#margin.left - this.#margin.right
|
|
165
|
+
}
|
|
166
|
+
get innerHeight() {
|
|
167
|
+
return this.#height - this.#margin.top - this.#margin.bottom
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
xScale = $derived(
|
|
171
|
+
this.#channels.x ? buildXScale(this.processedData, this.#channels.x, this.innerWidth) : null
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
yScale = $derived(
|
|
175
|
+
this.#channels.y
|
|
176
|
+
? buildYScale(this.processedData, this.#channels.y, this.innerHeight, this.#layers)
|
|
177
|
+
: null
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
sizeScale = $derived(
|
|
181
|
+
this.#channels.size ? buildSizeScale(this.processedData, this.#channels.size) : null
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
bars = $derived(
|
|
185
|
+
this.xScale && this.yScale
|
|
186
|
+
? buildBars(
|
|
187
|
+
this.processedData,
|
|
188
|
+
this.#channels,
|
|
189
|
+
this.xScale,
|
|
190
|
+
this.yScale,
|
|
191
|
+
this.colorMap,
|
|
192
|
+
this.patternMap
|
|
193
|
+
)
|
|
194
|
+
: []
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
lines = $derived(
|
|
198
|
+
this.xScale && this.yScale
|
|
199
|
+
? buildLines(
|
|
200
|
+
this.processedData,
|
|
201
|
+
this.#channels,
|
|
202
|
+
this.xScale,
|
|
203
|
+
this.yScale,
|
|
204
|
+
this.colorMap,
|
|
205
|
+
this.#curve
|
|
206
|
+
)
|
|
207
|
+
: []
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
areas = $derived(
|
|
211
|
+
this.xScale && this.yScale
|
|
212
|
+
? buildAreas(
|
|
213
|
+
this.processedData,
|
|
214
|
+
this.#channels,
|
|
215
|
+
this.xScale,
|
|
216
|
+
this.yScale,
|
|
217
|
+
this.colorMap,
|
|
218
|
+
this.#curve,
|
|
219
|
+
this.patternMap
|
|
220
|
+
)
|
|
221
|
+
: []
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
arcs = $derived(
|
|
225
|
+
this.#channels.y
|
|
226
|
+
? buildArcs(this.processedData, this.#channels, this.colorMap, this.#width, this.#height)
|
|
227
|
+
: []
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
points = $derived(
|
|
231
|
+
this.xScale && this.yScale
|
|
232
|
+
? buildPoints(
|
|
233
|
+
this.processedData,
|
|
234
|
+
this.#channels,
|
|
235
|
+
this.xScale,
|
|
236
|
+
this.yScale,
|
|
237
|
+
this.colorMap,
|
|
238
|
+
this.sizeScale,
|
|
239
|
+
this.symbolMap
|
|
240
|
+
)
|
|
241
|
+
: []
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
legendGroups = $derived(
|
|
245
|
+
buildLegendGroups(this.#channels, this.colorMap, this.patternMap, this.symbolMap)
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
get margin() {
|
|
249
|
+
return this.#margin
|
|
250
|
+
}
|
|
251
|
+
get width() {
|
|
252
|
+
return this.#width
|
|
253
|
+
}
|
|
254
|
+
get height() {
|
|
255
|
+
return this.#height
|
|
256
|
+
}
|
|
257
|
+
get mode() {
|
|
258
|
+
return this.#mode
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* @param {{ data?: Object[], channels?: Object, width?: number, height?: number, mode?: string, margin?: Object, layers?: Object[], curve?: string, stat?: string|Function }} opts
|
|
263
|
+
* Supported channel keys: `x`, `y`, `fill`, `color`, `pattern`, `symbol`, `size`, `label`.
|
|
264
|
+
* `frame` is reserved for future animation use (no-op).
|
|
265
|
+
*/
|
|
266
|
+
update(opts = {}) {
|
|
267
|
+
if (opts.data !== undefined) this.#rawData = opts.data
|
|
268
|
+
if (opts.channels !== undefined) this.#channels = opts.channels
|
|
269
|
+
if (opts.width !== undefined) this.#width = opts.width
|
|
270
|
+
if (opts.height !== undefined) this.#height = opts.height
|
|
271
|
+
if (opts.mode !== undefined) this.#mode = opts.mode
|
|
272
|
+
if (opts.margin !== undefined) this.#margin = { ...DEFAULT_MARGIN, ...opts.margin }
|
|
273
|
+
if (opts.layers !== undefined) this.#layers = opts.layers
|
|
274
|
+
if (opts.curve !== undefined) this.#curve = opts.curve
|
|
275
|
+
if (opts.stat !== undefined) this.#stat = opts.stat
|
|
276
|
+
}
|
|
230
277
|
}
|
|
@@ -1,4 +1,18 @@
|
|
|
1
|
-
import
|
|
1
|
+
import masterPalette from '../palette.json'
|
|
2
|
+
import { defaultPreset } from '../preset.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Returns true if the value looks like a CSS color literal (not a field name).
|
|
6
|
+
* Supports hex (#rgb, #rrggbb, #rrggbbaa), and functional notations (rgb, hsl, oklch, etc.).
|
|
7
|
+
* @param {unknown} value
|
|
8
|
+
* @returns {boolean}
|
|
9
|
+
*/
|
|
10
|
+
export function isLiteralColor(value) {
|
|
11
|
+
if (!value || typeof value !== 'string') return false
|
|
12
|
+
if (/^#([0-9a-fA-F]{3,8})$/.test(value)) return true
|
|
13
|
+
if (/^(rgb|rgba|hsl|hsla|oklch|oklab|hwb|lab|lch|color)\s*\(/i.test(value)) return true
|
|
14
|
+
return false
|
|
15
|
+
}
|
|
2
16
|
|
|
3
17
|
/**
|
|
4
18
|
* Extracts distinct values for a given field from the data array.
|
|
@@ -7,16 +21,31 @@ import palette from './palette.json'
|
|
|
7
21
|
* @returns {unknown[]}
|
|
8
22
|
*/
|
|
9
23
|
export function distinct(data, field) {
|
|
10
|
-
|
|
11
|
-
|
|
24
|
+
if (!field) return []
|
|
25
|
+
return [...new Set(data.map((d) => d[field]))].filter((v) => v !== null && v !== undefined)
|
|
12
26
|
}
|
|
13
27
|
|
|
14
28
|
/**
|
|
15
29
|
* Assigns palette colors to an array of distinct values.
|
|
16
30
|
* @param {unknown[]} values
|
|
17
31
|
* @param {'light'|'dark'} mode
|
|
32
|
+
* @param {typeof defaultPreset} preset
|
|
18
33
|
* @returns {Map<unknown, {fill: string, stroke: string}>}
|
|
19
34
|
*/
|
|
20
|
-
export function assignColors(values, mode = 'light') {
|
|
21
|
-
|
|
35
|
+
export function assignColors(values, mode = 'light', preset = defaultPreset) {
|
|
36
|
+
const { colors, shades } = preset
|
|
37
|
+
const { fill, stroke } = shades[mode]
|
|
38
|
+
return new Map(
|
|
39
|
+
values.map((v, i) => {
|
|
40
|
+
const colorName = colors[i % colors.length]
|
|
41
|
+
const group = masterPalette[colorName]
|
|
42
|
+
return [
|
|
43
|
+
v,
|
|
44
|
+
{
|
|
45
|
+
fill: group?.[fill] ?? '#888',
|
|
46
|
+
stroke: group?.[stroke] ?? '#444'
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
})
|
|
50
|
+
)
|
|
22
51
|
}
|
|
@@ -12,32 +12,32 @@ import { toPatternId } from '../../brewing/patterns.js'
|
|
|
12
12
|
* @param {Map<unknown, string>} [patterns]
|
|
13
13
|
*/
|
|
14
14
|
export function buildArcs(data, channels, colors, width, height, opts = {}, patterns) {
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
15
|
+
const { color: lf, y: yf } = channels
|
|
16
|
+
const radius = Math.min(width, height) / 2
|
|
17
|
+
const innerRadius = opts.innerRadius ?? 0
|
|
18
|
+
const pieGen = pie().value((d) => Number(d[yf]))
|
|
19
|
+
const arcGen = arc().innerRadius(innerRadius).outerRadius(radius)
|
|
20
|
+
const slices = pieGen(data)
|
|
21
|
+
const total = slices.reduce((s, sl) => s + (sl.endAngle - sl.startAngle), 0)
|
|
22
|
+
// Label radius: midpoint between inner and outer (or 70% out for solid pie)
|
|
23
|
+
const labelRadius = innerRadius > 0 ? (innerRadius + radius) / 2 : radius * 0.65
|
|
24
|
+
const labelArc = arc().innerRadius(labelRadius).outerRadius(labelRadius)
|
|
25
|
+
return slices.map((slice) => {
|
|
26
|
+
const key = slice.data[lf]
|
|
27
|
+
const colorEntry = colors?.get(key) ?? { fill: '#888', stroke: '#fff' }
|
|
28
|
+
const patternId =
|
|
29
|
+
key !== null && key !== undefined && patterns?.has(key) ? toPatternId(String(key)) : null
|
|
30
|
+
const pct = Math.round(((slice.endAngle - slice.startAngle) / total) * 100)
|
|
31
|
+
const [cx, cy] = labelArc.centroid(slice)
|
|
32
|
+
return {
|
|
33
|
+
d: arcGen(slice),
|
|
34
|
+
fill: colorEntry.fill,
|
|
35
|
+
stroke: colorEntry.stroke,
|
|
36
|
+
key,
|
|
37
|
+
patternId,
|
|
38
|
+
pct,
|
|
39
|
+
centroid: [cx, cy],
|
|
40
|
+
data: slice.data
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
43
|
}
|