@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.
Files changed (90) hide show
  1. package/dist/PlotState.svelte.d.ts +31 -3
  2. package/dist/crossfilter/createCrossFilter.svelte.d.ts +13 -15
  3. package/dist/index.d.ts +6 -1
  4. package/dist/lib/brewing/BoxBrewer.svelte.d.ts +3 -5
  5. package/dist/lib/brewing/QuartileBrewer.svelte.d.ts +9 -0
  6. package/dist/lib/brewing/ViolinBrewer.svelte.d.ts +3 -4
  7. package/dist/lib/brewing/brewer.svelte.d.ts +5 -36
  8. package/dist/lib/brewing/colors.d.ts +10 -1
  9. package/dist/lib/brewing/marks/points.d.ts +17 -2
  10. package/dist/lib/brewing/stats.d.ts +5 -13
  11. package/dist/lib/chart.d.ts +5 -7
  12. package/dist/lib/keyboard-nav.d.ts +15 -0
  13. package/dist/lib/plot/preset.d.ts +1 -1
  14. package/dist/lib/preset.d.ts +30 -0
  15. package/package.json +2 -1
  16. package/src/AnimatedPlot.svelte +375 -206
  17. package/src/Chart.svelte +81 -87
  18. package/src/ChartProvider.svelte +10 -0
  19. package/src/FacetPlot/Panel.svelte +30 -16
  20. package/src/FacetPlot.svelte +100 -76
  21. package/src/Plot/Area.svelte +26 -19
  22. package/src/Plot/Axis.svelte +81 -59
  23. package/src/Plot/Bar.svelte +47 -89
  24. package/src/Plot/Grid.svelte +23 -19
  25. package/src/Plot/Legend.svelte +213 -147
  26. package/src/Plot/Line.svelte +31 -21
  27. package/src/Plot/Point.svelte +35 -22
  28. package/src/Plot/Root.svelte +46 -91
  29. package/src/Plot/Timeline.svelte +82 -82
  30. package/src/Plot/Tooltip.svelte +68 -62
  31. package/src/Plot.svelte +290 -182
  32. package/src/PlotState.svelte.js +339 -267
  33. package/src/Sparkline.svelte +95 -56
  34. package/src/charts/AreaChart.svelte +22 -20
  35. package/src/charts/BarChart.svelte +23 -21
  36. package/src/charts/BoxPlot.svelte +15 -15
  37. package/src/charts/BubbleChart.svelte +17 -17
  38. package/src/charts/LineChart.svelte +20 -20
  39. package/src/charts/PieChart.svelte +30 -20
  40. package/src/charts/ScatterPlot.svelte +20 -19
  41. package/src/charts/ViolinPlot.svelte +15 -15
  42. package/src/crossfilter/CrossFilter.svelte +33 -29
  43. package/src/crossfilter/FilterBar.svelte +17 -25
  44. package/src/crossfilter/FilterHistogram.svelte +290 -0
  45. package/src/crossfilter/FilterSlider.svelte +69 -65
  46. package/src/crossfilter/createCrossFilter.svelte.js +100 -89
  47. package/src/geoms/Arc.svelte +114 -69
  48. package/src/geoms/Area.svelte +67 -39
  49. package/src/geoms/Bar.svelte +184 -126
  50. package/src/geoms/Box.svelte +102 -90
  51. package/src/geoms/LabelPill.svelte +11 -11
  52. package/src/geoms/Line.svelte +110 -87
  53. package/src/geoms/Point.svelte +132 -87
  54. package/src/geoms/Violin.svelte +45 -33
  55. package/src/geoms/lib/areas.js +122 -99
  56. package/src/geoms/lib/bars.js +195 -144
  57. package/src/index.js +21 -14
  58. package/src/lib/brewing/BoxBrewer.svelte.js +8 -50
  59. package/src/lib/brewing/CartesianBrewer.svelte.js +12 -7
  60. package/src/lib/brewing/PieBrewer.svelte.js +5 -5
  61. package/src/lib/brewing/QuartileBrewer.svelte.js +51 -0
  62. package/src/lib/brewing/ViolinBrewer.svelte.js +8 -49
  63. package/src/lib/brewing/brewer.svelte.js +249 -201
  64. package/src/lib/brewing/colors.js +34 -5
  65. package/src/lib/brewing/marks/arcs.js +28 -28
  66. package/src/lib/brewing/marks/areas.js +54 -41
  67. package/src/lib/brewing/marks/bars.js +34 -34
  68. package/src/lib/brewing/marks/boxes.js +51 -51
  69. package/src/lib/brewing/marks/lines.js +37 -30
  70. package/src/lib/brewing/marks/points.js +74 -26
  71. package/src/lib/brewing/marks/violins.js +57 -57
  72. package/src/lib/brewing/patterns.js +25 -11
  73. package/src/lib/brewing/scales.js +20 -20
  74. package/src/lib/brewing/stats.js +40 -28
  75. package/src/lib/brewing/symbols.js +1 -1
  76. package/src/lib/chart.js +12 -4
  77. package/src/lib/keyboard-nav.js +37 -0
  78. package/src/lib/plot/crossfilter.js +5 -5
  79. package/src/lib/plot/facet.js +30 -30
  80. package/src/lib/plot/frames.js +30 -29
  81. package/src/lib/plot/helpers.js +4 -4
  82. package/src/lib/plot/preset.js +48 -34
  83. package/src/lib/plot/scales.js +64 -39
  84. package/src/lib/plot/stat.js +47 -47
  85. package/src/lib/preset.js +41 -0
  86. package/src/patterns/DefinePatterns.svelte +24 -24
  87. package/src/patterns/PatternDef.svelte +1 -1
  88. package/src/patterns/patterns.js +328 -176
  89. package/src/patterns/scale.js +61 -32
  90. package/src/spec/chart-spec.js +64 -21
@@ -1,3 +1,4 @@
1
+ import { SvelteMap, SvelteSet } from 'svelte/reactivity'
1
2
  import { distinct, assignColors } from './colors.js'
2
3
  import { assignPatterns, toPatternId, PATTERN_ORDER } from './patterns.js'
3
4
  import { assignSymbols } from './symbols.js'
@@ -21,209 +22,256 @@ const DEFAULT_MARGIN = { top: 20, right: 20, bottom: 40, left: 50 }
21
22
  * @param {Map<unknown, string>} symbolMap
22
23
  * @returns {{ field: string, items: { label: string, fill: string|null, stroke: string|null, patternId: string|null, shape: string|null }[] }[]}
23
24
  */
25
+ function addAesthetic(byField, field, aesthetic, keys) {
26
+ if (byField.has(field)) {
27
+ byField.get(field).aesthetics.push(aesthetic)
28
+ } else {
29
+ byField.set(field, { aesthetics: [aesthetic], keys })
30
+ }
31
+ }
32
+
33
+ function buildLegendItem(key, aesthetics, colorMap, patternMap, symbolMap) {
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
+ }
43
+
24
44
  export function buildLegendGroups(channels, colorMap, patternMap, symbolMap) {
25
- const cf = channels.fill ?? channels.color
26
- const { pattern: pf, symbol: sf } = channels
27
- const byField = new Map()
28
-
29
- if (cf) {
30
- byField.set(cf, { aesthetics: ['color'], keys: [...colorMap.keys()] })
31
- }
32
- if (pf) {
33
- if (byField.has(pf)) {
34
- byField.get(pf).aesthetics.push('pattern')
35
- } else {
36
- byField.set(pf, { aesthetics: ['pattern'], keys: [...patternMap.keys()] })
37
- }
38
- }
39
- if (sf) {
40
- if (byField.has(sf)) {
41
- byField.get(sf).aesthetics.push('symbol')
42
- } else {
43
- byField.set(sf, { aesthetics: ['symbol'], keys: [...symbolMap.keys()] })
44
- }
45
- }
46
-
47
- return [...byField.entries()].map(([field, { aesthetics, keys }]) => ({
48
- field,
49
- items: keys.filter((k) => k !== null && k !== undefined).map((key) => ({
50
- label: String(key),
51
- fill: aesthetics.includes('color') ? (colorMap.get(key)?.fill ?? null) : null,
52
- stroke: aesthetics.includes('color') ? (colorMap.get(key)?.stroke ?? null) : null,
53
- patternId:
54
- aesthetics.includes('pattern') && patternMap.has(key) ? toPatternId(key) : null,
55
- shape: aesthetics.includes('symbol') ? (symbolMap.get(key) ?? 'circle') : null
56
- }))
57
- })).filter((group) => group.items.length > 0)
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)
58
61
  }
59
62
 
60
63
  export class ChartBrewer {
61
- #rawData = $state([])
62
- #channels = $state({})
63
- #width = $state(600)
64
- #height = $state(400)
65
- #mode = $state('light')
66
- #margin = $state(DEFAULT_MARGIN)
67
- #layers = $state([])
68
- #curve = $state(/** @type {'linear'|'smooth'|'step'|undefined} */(undefined))
69
- #stat = $state('identity')
70
-
71
- /**
72
- * Override in subclasses to apply stat aggregation.
73
- * @param {Object[]} data
74
- * @param {Object} channels
75
- * @param {string|Function} stat
76
- * @returns {Object[]}
77
- */
78
- transform(data, _channels, _stat) {
79
- return data
80
- }
81
-
82
- /** Aggregated data — all derived marks read this, not #rawData */
83
- processedData = $derived(this.transform(this.#rawData, this.#channels, this.#stat))
84
-
85
- /** Exposes channels to subclasses for use in their own $derived properties */
86
- get channels() { return this.#channels }
87
-
88
- // Maps are built from rawData so the legend always reflects the full set of
89
- // original values — independent of whichever stat aggregation is applied.
90
- // e.g. pattern=quarter with stat=sum still shows all 8 quarters in the legend.
91
-
92
- /** @type {Map<unknown, {fill:string,stroke:string}>} */
93
- colorMap = $derived(
94
- (this.#channels.fill ?? this.#channels.color)
95
- ? assignColors(distinct(this.#rawData, this.#channels.fill ?? this.#channels.color), this.#mode)
96
- : new Map()
97
- )
98
-
99
- /** @type {Map<unknown, string>} */
100
- patternMap = $derived(
101
- this.#channels.pattern
102
- ? assignPatterns(distinct(this.#rawData, this.#channels.pattern))
103
- : new Map()
104
- )
105
-
106
- /**
107
- * Unified pattern defs for ChartPatternDefs.
108
- * When fill and pattern map the same field, pattern key = color key (simple case).
109
- * When they differ, each unique (fillKey, patternKey) pair gets its own pattern def
110
- * so bars/areas can have distinct colors per region AND distinct textures per category.
111
- * @type {Array<{ id: string, name: string, fill: string, stroke: string }>}
112
- */
113
- patternDefs = $derived((() => {
114
- const pf = this.#channels.pattern
115
- const ff = this.#channels.fill ?? this.#channels.color
116
- if (!pf || this.patternMap.size === 0) return []
117
- if (!ff || pf === ff) {
118
- // Same field: pattern key = fill key — simple 1:1 lookup
119
- return Array.from(this.patternMap.entries()).map(([key, name]) => {
120
- const color = this.colorMap.get(key) ?? { fill: '#ddd', stroke: '#666' }
121
- return { id: toPatternId(key), name, fill: color.fill, stroke: color.stroke }
122
- })
123
- }
124
- // Different fields: need two sets of defs in the SVG:
125
- // 1. Simple defs (neutral background) referenced by legend swatches via toPatternId(patternKey)
126
- // 2. Composite defs (fill-colored background) referenced by bars via toPatternId(fillKey::patternKey)
127
- const defs = []
128
- for (const [pk, name] of this.patternMap.entries()) {
129
- defs.push({ id: toPatternId(pk), name, fill: '#ddd', stroke: '#666' })
130
- }
131
- const seenComposite = new Set()
132
- for (const d of this.processedData) {
133
- const fk = d[ff]
134
- const pk = d[pf]
135
- if (pk === null || pk === undefined) continue
136
- const compositeKey = `${fk}::${pk}`
137
- if (seenComposite.has(compositeKey)) continue
138
- seenComposite.add(compositeKey)
139
- const name = this.patternMap.get(pk) ?? PATTERN_ORDER[0]
140
- const color = this.colorMap.get(fk) ?? { fill: '#ddd', stroke: '#666' }
141
- defs.push({ id: toPatternId(compositeKey), name, fill: color.fill, stroke: color.stroke })
142
- }
143
- return defs
144
- })())
145
-
146
- /** @type {Map<unknown, string>} */
147
- symbolMap = $derived(
148
- this.#channels.symbol
149
- ? assignSymbols(distinct(this.#rawData, this.#channels.symbol))
150
- : new Map()
151
- )
152
-
153
- get innerWidth() { return this.#width - this.#margin.left - this.#margin.right }
154
- get innerHeight() { return this.#height - this.#margin.top - this.#margin.bottom }
155
-
156
- xScale = $derived(
157
- this.#channels.x
158
- ? buildXScale(this.processedData, this.#channels.x, this.innerWidth)
159
- : null
160
- )
161
-
162
- yScale = $derived(
163
- this.#channels.y
164
- ? buildYScale(this.processedData, this.#channels.y, this.innerHeight, this.#layers)
165
- : null
166
- )
167
-
168
- sizeScale = $derived(
169
- this.#channels.size
170
- ? buildSizeScale(this.processedData, this.#channels.size)
171
- : null
172
- )
173
-
174
- bars = $derived(
175
- this.xScale && this.yScale
176
- ? buildBars(this.processedData, this.#channels, this.xScale, this.yScale, this.colorMap, this.patternMap)
177
- : []
178
- )
179
-
180
- lines = $derived(
181
- this.xScale && this.yScale
182
- ? buildLines(this.processedData, this.#channels, this.xScale, this.yScale, this.colorMap, this.#curve)
183
- : []
184
- )
185
-
186
- areas = $derived(
187
- this.xScale && this.yScale
188
- ? buildAreas(this.processedData, this.#channels, this.xScale, this.yScale, this.colorMap, this.#curve, this.patternMap)
189
- : []
190
- )
191
-
192
- arcs = $derived(
193
- this.#channels.y
194
- ? buildArcs(this.processedData, this.#channels, this.colorMap, this.#width, this.#height)
195
- : []
196
- )
197
-
198
- points = $derived(
199
- this.xScale && this.yScale
200
- ? buildPoints(this.processedData, this.#channels, this.xScale, this.yScale, this.colorMap, this.sizeScale, this.symbolMap)
201
- : []
202
- )
203
-
204
- legendGroups = $derived(
205
- buildLegendGroups(this.#channels, this.colorMap, this.patternMap, this.symbolMap)
206
- )
207
-
208
- get margin() { return this.#margin }
209
- get width() { return this.#width }
210
- get height() { return this.#height }
211
- get mode() { return this.#mode }
212
-
213
- /**
214
- * @param {{ data?: Object[], channels?: Object, width?: number, height?: number, mode?: string, margin?: Object, layers?: Object[], curve?: string, stat?: string|Function }} opts
215
- * Supported channel keys: `x`, `y`, `fill`, `color`, `pattern`, `symbol`, `size`, `label`.
216
- * `frame` is reserved for future animation use (no-op).
217
- */
218
- update(opts = {}) {
219
- if (opts.data !== undefined) this.#rawData = opts.data
220
- if (opts.channels !== undefined) this.#channels = opts.channels
221
- if (opts.width !== undefined) this.#width = opts.width
222
- if (opts.height !== undefined) this.#height = opts.height
223
- if (opts.mode !== undefined) this.#mode = opts.mode
224
- if (opts.margin !== undefined) this.#margin = { ...DEFAULT_MARGIN, ...opts.margin }
225
- if (opts.layers !== undefined) this.#layers = opts.layers
226
- if (opts.curve !== undefined) this.#curve = opts.curve
227
- if (opts.stat !== undefined) this.#stat = opts.stat
228
- }
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
+ }
229
277
  }
@@ -1,4 +1,18 @@
1
- import palette from './palette.json'
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
- if (!field) return []
11
- return [...new Set(data.map((d) => d[field]))].filter((v) => v !== null && v !== undefined)
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
- return new Map(values.map((v, i) => [v, palette[i % palette.length].shades[mode]]))
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
- 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 = key !== null && key !== undefined && patterns?.has(key)
29
- ? 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
- })
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
  }