@rokkit/chart 1.0.0-next.16 → 1.0.0-next.161
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/README.md +150 -46
- package/package.json +42 -45
- package/src/AnimatedPlot.svelte +383 -0
- package/src/Chart.svelte +95 -0
- package/src/ChartProvider.svelte +10 -0
- package/src/FacetPlot/Panel.svelte +37 -0
- package/src/FacetPlot.svelte +114 -0
- package/src/Plot/Arc.svelte +29 -0
- package/src/Plot/Area.svelte +32 -0
- package/src/Plot/Axis.svelte +95 -0
- package/src/Plot/Bar.svelte +54 -0
- package/src/Plot/Grid.svelte +34 -0
- package/src/Plot/Legend.svelte +233 -0
- package/src/Plot/Line.svelte +37 -0
- package/src/Plot/Point.svelte +40 -0
- package/src/Plot/Root.svelte +62 -0
- package/src/Plot/Timeline.svelte +95 -0
- package/src/Plot/Tooltip.svelte +87 -0
- package/src/Plot/index.js +9 -0
- package/src/Plot.svelte +297 -0
- package/src/PlotState.svelte.js +350 -0
- package/src/Sparkline.svelte +108 -0
- package/src/Symbol.svelte +21 -0
- package/src/Texture.svelte +18 -0
- package/src/charts/AreaChart.svelte +27 -0
- package/src/charts/BarChart.svelte +28 -0
- package/src/charts/BoxPlot.svelte +21 -0
- package/src/charts/BubbleChart.svelte +23 -0
- package/src/charts/LineChart.svelte +26 -0
- package/src/charts/PieChart.svelte +35 -0
- package/src/charts/ScatterPlot.svelte +26 -0
- package/src/charts/ViolinPlot.svelte +21 -0
- package/src/crossfilter/CrossFilter.svelte +42 -0
- package/src/crossfilter/FilterBar.svelte +24 -0
- package/src/crossfilter/FilterHistogram.svelte +290 -0
- package/src/crossfilter/FilterSlider.svelte +83 -0
- package/src/crossfilter/createCrossFilter.svelte.js +124 -0
- package/src/elements/Bar.svelte +22 -24
- package/src/elements/ColorRamp.svelte +20 -22
- package/src/elements/ContinuousLegend.svelte +20 -17
- package/src/elements/DefinePatterns.svelte +24 -0
- package/src/elements/DiscreteLegend.svelte +15 -15
- package/src/elements/Label.svelte +4 -8
- package/src/elements/SymbolGrid.svelte +22 -0
- package/src/elements/index.js +6 -0
- package/src/examples/BarChartExample.svelte +81 -0
- package/src/geoms/Arc.svelte +126 -0
- package/src/geoms/Area.svelte +78 -0
- package/src/geoms/Bar.svelte +200 -0
- package/src/geoms/Box.svelte +113 -0
- package/src/geoms/LabelPill.svelte +17 -0
- package/src/geoms/Line.svelte +123 -0
- package/src/geoms/Point.svelte +145 -0
- package/src/geoms/Violin.svelte +56 -0
- package/src/geoms/lib/areas.js +154 -0
- package/src/geoms/lib/bars.js +223 -0
- package/src/index.js +74 -16
- package/src/lib/brewer.js +25 -0
- package/src/lib/brewing/BoxBrewer.svelte.js +14 -0
- package/src/lib/brewing/CartesianBrewer.svelte.js +21 -0
- package/src/lib/brewing/PieBrewer.svelte.js +14 -0
- package/src/lib/brewing/QuartileBrewer.svelte.js +51 -0
- package/src/lib/brewing/ViolinBrewer.svelte.js +14 -0
- package/src/lib/brewing/axes.svelte.js +270 -0
- package/src/lib/brewing/bars.svelte.js +201 -0
- package/src/lib/brewing/brewer.svelte.js +277 -0
- package/src/lib/brewing/colors.js +51 -0
- package/src/lib/brewing/dimensions.svelte.js +56 -0
- package/src/lib/brewing/index.svelte.js +205 -0
- package/src/lib/brewing/legends.svelte.js +137 -0
- package/src/lib/brewing/marks/arcs.js +43 -0
- package/src/lib/brewing/marks/areas.js +72 -0
- package/src/lib/brewing/marks/bars.js +49 -0
- package/src/lib/brewing/marks/boxes.js +75 -0
- package/src/lib/brewing/marks/lines.js +55 -0
- package/src/lib/brewing/marks/points.js +105 -0
- package/src/lib/brewing/marks/violins.js +90 -0
- package/src/lib/brewing/patterns.js +45 -0
- package/src/lib/brewing/scales.js +51 -0
- package/src/lib/brewing/scales.svelte.js +82 -0
- package/src/lib/brewing/stats.js +74 -0
- package/src/lib/brewing/symbols.js +10 -0
- package/src/lib/brewing/types.js +73 -0
- package/src/lib/chart.js +221 -0
- package/src/lib/context.js +131 -0
- package/src/lib/grid.js +85 -0
- package/src/lib/keyboard-nav.js +37 -0
- package/src/lib/plot/chartProps.js +76 -0
- package/src/lib/plot/crossfilter.js +16 -0
- package/src/lib/plot/facet.js +58 -0
- package/src/lib/plot/frames.js +81 -0
- package/src/lib/plot/helpers.js +14 -0
- package/src/lib/plot/preset.js +67 -0
- package/src/lib/plot/scales.js +81 -0
- package/src/lib/plot/stat.js +92 -0
- package/src/lib/plot/types.js +65 -0
- package/src/lib/preset.js +41 -0
- package/src/lib/scales.svelte.js +151 -0
- package/src/lib/swatch.js +13 -0
- package/src/lib/ticks.js +46 -0
- package/src/lib/utils.js +111 -118
- package/src/lib/xscale.js +31 -0
- package/src/patterns/DefinePatterns.svelte +32 -0
- package/src/patterns/PatternDef.svelte +27 -0
- package/src/patterns/index.js +4 -0
- package/src/patterns/patterns.js +360 -0
- package/src/patterns/scale.js +116 -0
- package/src/spec/chart-spec.js +72 -0
- package/src/symbols/RoundedSquare.svelte +33 -0
- package/src/symbols/Shape.svelte +37 -0
- package/src/symbols/constants/index.js +4 -0
- package/src/symbols/index.js +9 -0
- package/src/symbols/outline.svelte +60 -0
- package/src/symbols/solid.svelte +60 -0
- package/LICENSE +0 -21
- package/src/chart/FacetGrid.svelte +0 -51
- package/src/chart/Grid.svelte +0 -34
- package/src/chart/Legend.svelte +0 -16
- package/src/chart/PatternDefs.svelte +0 -13
- package/src/chart/Swatch.svelte +0 -93
- package/src/chart/SwatchButton.svelte +0 -29
- package/src/chart/SwatchGrid.svelte +0 -55
- package/src/chart/Symbol.svelte +0 -37
- package/src/chart/Texture.svelte +0 -16
- package/src/chart/TexturedShape.svelte +0 -27
- package/src/chart/TimelapseChart.svelte +0 -97
- package/src/chart/Timer.svelte +0 -27
- package/src/chart.js +0 -9
- package/src/components/charts/Axis.svelte +0 -66
- package/src/components/charts/Chart.svelte +0 -35
- package/src/components/index.js +0 -23
- package/src/components/lib/axis.js +0 -0
- package/src/components/lib/chart.js +0 -187
- package/src/components/lib/color.js +0 -327
- package/src/components/lib/funnel.js +0 -204
- package/src/components/lib/index.js +0 -19
- package/src/components/lib/pattern.js +0 -190
- package/src/components/lib/rollup.js +0 -55
- package/src/components/lib/shape.js +0 -199
- package/src/components/lib/summary.js +0 -145
- package/src/components/lib/theme.js +0 -23
- package/src/components/lib/timer.js +0 -41
- package/src/components/lib/utils.js +0 -165
- package/src/components/plots/BarPlot.svelte +0 -36
- package/src/components/plots/BoxPlot.svelte +0 -54
- package/src/components/plots/ScatterPlot.svelte +0 -30
- package/src/components/store.js +0 -70
- package/src/constants.js +0 -66
- package/src/elements/PatternDefs.svelte +0 -13
- package/src/elements/PatternMask.svelte +0 -20
- package/src/elements/Symbol.svelte +0 -38
- package/src/elements/Tooltip.svelte +0 -23
- package/src/funnel.svelte +0 -35
- package/src/geom.js +0 -105
- package/src/lib/axis.js +0 -75
- package/src/lib/colors.js +0 -32
- package/src/lib/geom.js +0 -4
- package/src/lib/shapes.js +0 -144
- package/src/lib/timer.js +0 -44
- package/src/lookup.js +0 -29
- package/src/plots/BarPlot.svelte +0 -55
- package/src/plots/BoxPlot.svelte +0 -0
- package/src/plots/FunnelPlot.svelte +0 -33
- package/src/plots/HeatMap.svelte +0 -5
- package/src/plots/HeatMapCalendar.svelte +0 -129
- package/src/plots/LinePlot.svelte +0 -55
- package/src/plots/Plot.svelte +0 -25
- package/src/plots/RankBarPlot.svelte +0 -38
- package/src/plots/ScatterPlot.svelte +0 -20
- package/src/plots/ViolinPlot.svelte +0 -11
- package/src/plots/heatmap.js +0 -70
- package/src/plots/index.js +0 -10
- package/src/swatch.js +0 -11
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { SvelteMap, SvelteSet } from 'svelte/reactivity'
|
|
2
|
+
import { distinct, assignColors } from './colors.js'
|
|
3
|
+
import { assignPatterns, toPatternId, PATTERN_ORDER } from './patterns.js'
|
|
4
|
+
import { assignSymbols } from './symbols.js'
|
|
5
|
+
import { buildXScale, buildYScale, buildSizeScale } from './scales.js'
|
|
6
|
+
import { buildBars } from './marks/bars.js'
|
|
7
|
+
import { buildLines } from './marks/lines.js'
|
|
8
|
+
import { buildAreas } from './marks/areas.js'
|
|
9
|
+
import { buildArcs } from './marks/arcs.js'
|
|
10
|
+
import { buildPoints } from './marks/points.js'
|
|
11
|
+
|
|
12
|
+
const DEFAULT_MARGIN = { top: 20, right: 20, bottom: 40, left: 50 }
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Groups aesthetic channel mappings by field name, merging aesthetics that
|
|
16
|
+
* share the same field into one legend section.
|
|
17
|
+
*
|
|
18
|
+
* @param {{ fill?: string, color?: string, pattern?: string, symbol?: string }} channels
|
|
19
|
+
* `fill` takes precedence over `color` for polygon charts (bars, areas, pie slices).
|
|
20
|
+
* @param {Map<unknown, {fill:string, stroke:string}>} colorMap
|
|
21
|
+
* @param {Map<unknown, string>} patternMap
|
|
22
|
+
* @param {Map<unknown, string>} symbolMap
|
|
23
|
+
* @returns {{ field: string, items: { label: string, fill: string|null, stroke: string|null, patternId: string|null, shape: string|null }[] }[]}
|
|
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
|
+
|
|
44
|
+
export function buildLegendGroups(channels, colorMap, patternMap, symbolMap) {
|
|
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)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class ChartBrewer {
|
|
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
|
+
}
|
|
277
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Extracts distinct values for a given field from the data array.
|
|
19
|
+
* @param {Object[]} data
|
|
20
|
+
* @param {string|null} field
|
|
21
|
+
* @returns {unknown[]}
|
|
22
|
+
*/
|
|
23
|
+
export function distinct(data, field) {
|
|
24
|
+
if (!field) return []
|
|
25
|
+
return [...new Set(data.map((d) => d[field]))].filter((v) => v !== null && v !== undefined)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Assigns palette colors to an array of distinct values.
|
|
30
|
+
* @param {unknown[]} values
|
|
31
|
+
* @param {'light'|'dark'} mode
|
|
32
|
+
* @param {typeof defaultPreset} preset
|
|
33
|
+
* @returns {Map<unknown, {fill: string, stroke: string}>}
|
|
34
|
+
*/
|
|
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
|
+
)
|
|
51
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {} from './types.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {import('./types').ChartMargin} ChartMargin
|
|
5
|
+
* @typedef {import('./types').ChartDimensions} ChartDimensions
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Default chart margin
|
|
10
|
+
* @type {ChartMargin}
|
|
11
|
+
*/
|
|
12
|
+
export const DEFAULT_MARGIN = { top: 20, right: 30, bottom: 40, left: 50 }
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Creates chart dimensions based on width, height and margins
|
|
16
|
+
*
|
|
17
|
+
* @param {number} width - Total chart width
|
|
18
|
+
* @param {number} height - Total chart height
|
|
19
|
+
* @param {ChartMargin} margin - Chart margins
|
|
20
|
+
* @returns {ChartDimensions} Chart dimensions
|
|
21
|
+
*/
|
|
22
|
+
export function createDimensions(width = 600, height = 400, margin = DEFAULT_MARGIN) {
|
|
23
|
+
return {
|
|
24
|
+
width,
|
|
25
|
+
height,
|
|
26
|
+
margin: { ...margin },
|
|
27
|
+
innerWidth: width - margin.left - margin.right,
|
|
28
|
+
innerHeight: height - margin.top - margin.bottom
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Updates existing dimensions with new values
|
|
34
|
+
*
|
|
35
|
+
* @param {ChartDimensions} dimensions - Current dimensions
|
|
36
|
+
* @param {Object} updates - Values to update
|
|
37
|
+
* @param {number} [updates.width] - New width
|
|
38
|
+
* @param {number} [updates.height] - New height
|
|
39
|
+
* @param {ChartMargin} [updates.margin] - New margin
|
|
40
|
+
* @returns {ChartDimensions} Updated dimensions
|
|
41
|
+
*/
|
|
42
|
+
export function updateDimensions(dimensions, updates = {}) {
|
|
43
|
+
const newDimensions = { ...dimensions }
|
|
44
|
+
|
|
45
|
+
if (updates.width !== undefined) newDimensions.width = updates.width
|
|
46
|
+
if (updates.height !== undefined) newDimensions.height = updates.height
|
|
47
|
+
if (updates.margin !== undefined) newDimensions.margin = { ...updates.margin }
|
|
48
|
+
|
|
49
|
+
// Recalculate inner dimensions
|
|
50
|
+
newDimensions.innerWidth =
|
|
51
|
+
newDimensions.width - newDimensions.margin.left - newDimensions.margin.right
|
|
52
|
+
newDimensions.innerHeight =
|
|
53
|
+
newDimensions.height - newDimensions.margin.top - newDimensions.margin.bottom
|
|
54
|
+
|
|
55
|
+
return newDimensions
|
|
56
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { createDimensions, updateDimensions } from './dimensions.svelte.js'
|
|
2
|
+
import { createScales, getOrigin } from './scales.svelte.js'
|
|
3
|
+
import { createBars, filterBars, createGroupedBars } from './bars.svelte.js'
|
|
4
|
+
import { createXAxis, createYAxis, createGrid, createTickAttributes } from './axes.svelte.js'
|
|
5
|
+
import { createLegend, filterByLegend, createLegendItemAttributes } from './legends.svelte.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Chart Brewing - A collection of pure functions for chart data preparation
|
|
9
|
+
*/
|
|
10
|
+
export {
|
|
11
|
+
// Dimensions
|
|
12
|
+
createDimensions,
|
|
13
|
+
updateDimensions,
|
|
14
|
+
|
|
15
|
+
// Scales
|
|
16
|
+
createScales,
|
|
17
|
+
getOrigin,
|
|
18
|
+
|
|
19
|
+
// Bars
|
|
20
|
+
createBars,
|
|
21
|
+
filterBars,
|
|
22
|
+
createGroupedBars,
|
|
23
|
+
|
|
24
|
+
// Axes
|
|
25
|
+
createXAxis,
|
|
26
|
+
createYAxis,
|
|
27
|
+
createGrid,
|
|
28
|
+
createTickAttributes,
|
|
29
|
+
|
|
30
|
+
// Legends
|
|
31
|
+
createLegend,
|
|
32
|
+
filterByLegend,
|
|
33
|
+
createLegendItemAttributes
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Main class that manages chart state and provides access to all brewing functions
|
|
38
|
+
*/
|
|
39
|
+
export class ChartBrewer {
|
|
40
|
+
#data = []
|
|
41
|
+
#dimensions = createDimensions()
|
|
42
|
+
#scales = { x: null, y: null, color: null }
|
|
43
|
+
#fields = { x: null, y: null, color: null }
|
|
44
|
+
#options = {
|
|
45
|
+
padding: 0.2,
|
|
46
|
+
animationDuration: 300
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Creates a new ChartBrewer instance
|
|
51
|
+
*
|
|
52
|
+
* @param {Object} options Configuration options
|
|
53
|
+
*/
|
|
54
|
+
constructor(options = {}) {
|
|
55
|
+
this.#dimensions = createDimensions(options.width, options.height, options.margin)
|
|
56
|
+
|
|
57
|
+
if (options.padding !== undefined) this.#options.padding = options.padding
|
|
58
|
+
if (options.animationDuration !== undefined)
|
|
59
|
+
this.#options.animationDuration = options.animationDuration
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Sets the data for the chart
|
|
64
|
+
*
|
|
65
|
+
* @param {Array} data Data array
|
|
66
|
+
* @returns {ChartBrewer} this for method chaining
|
|
67
|
+
*/
|
|
68
|
+
setData(data) {
|
|
69
|
+
this.#data = Array.isArray(data) ? data : []
|
|
70
|
+
return this
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Sets the field mappings for axes and color
|
|
75
|
+
*
|
|
76
|
+
* @param {Object} fields Field mappings
|
|
77
|
+
* @returns {ChartBrewer} this for method chaining
|
|
78
|
+
*/
|
|
79
|
+
setFields({ x, y, color }) {
|
|
80
|
+
if (x !== undefined) this.#fields.x = x
|
|
81
|
+
if (y !== undefined) this.#fields.y = y
|
|
82
|
+
if (color !== undefined) this.#fields.color = color
|
|
83
|
+
return this
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Sets the dimensions of the chart
|
|
88
|
+
*
|
|
89
|
+
* @param {Object} dimensions Chart dimensions
|
|
90
|
+
* @returns {ChartBrewer} this for method chaining
|
|
91
|
+
*/
|
|
92
|
+
setDimensions({ width, height, margin }) {
|
|
93
|
+
this.#dimensions = updateDimensions(this.#dimensions, { width, height, margin })
|
|
94
|
+
return this
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Creates scales based on data and dimensions
|
|
99
|
+
*
|
|
100
|
+
* @returns {ChartBrewer} this for method chaining
|
|
101
|
+
*/
|
|
102
|
+
createScales() {
|
|
103
|
+
this.#scales = createScales(this.#data, this.#fields, this.#dimensions, {
|
|
104
|
+
padding: this.#options.padding
|
|
105
|
+
})
|
|
106
|
+
return this
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Creates bar data for rendering
|
|
111
|
+
*
|
|
112
|
+
* @returns {Array} Data for rendering bars
|
|
113
|
+
*/
|
|
114
|
+
createBars() {
|
|
115
|
+
return createBars(this.#data, this.#fields, this.#scales, { dimensions: this.#dimensions })
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Creates x-axis tick data for rendering
|
|
120
|
+
*
|
|
121
|
+
* @param {Object} options Axis options
|
|
122
|
+
* @returns {Object} Axis rendering data
|
|
123
|
+
*/
|
|
124
|
+
createXAxis(options = {}) {
|
|
125
|
+
return createXAxis(this.#scales, this.#dimensions, options)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Creates y-axis tick data for rendering
|
|
130
|
+
*
|
|
131
|
+
* @param {Object} options Axis options
|
|
132
|
+
* @returns {Object} Axis rendering data
|
|
133
|
+
*/
|
|
134
|
+
createYAxis(options = {}) {
|
|
135
|
+
return createYAxis(this.#scales, this.#dimensions, options)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Creates grid line data for rendering
|
|
140
|
+
*
|
|
141
|
+
* @param {Object} options Grid options
|
|
142
|
+
* @returns {Object} Grid rendering data
|
|
143
|
+
*/
|
|
144
|
+
createGrid(options = {}) {
|
|
145
|
+
return createGrid(this.#scales, this.#dimensions, options)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Creates legend data for rendering
|
|
150
|
+
*
|
|
151
|
+
* @param {Object} options Legend options
|
|
152
|
+
* @returns {Object} Legend rendering data
|
|
153
|
+
*/
|
|
154
|
+
createLegend(options = {}) {
|
|
155
|
+
return createLegend(this.#data, this.#fields, this.#scales, {
|
|
156
|
+
...options,
|
|
157
|
+
dimensions: this.#dimensions
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Gets all chart dimensions
|
|
163
|
+
*
|
|
164
|
+
* @returns {Object} Chart dimensions
|
|
165
|
+
*/
|
|
166
|
+
getDimensions() {
|
|
167
|
+
return { ...this.#dimensions }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Gets all chart scales
|
|
172
|
+
*
|
|
173
|
+
* @returns {Object} Chart scales
|
|
174
|
+
*/
|
|
175
|
+
getScales() {
|
|
176
|
+
return { ...this.#scales }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Gets the animation duration
|
|
181
|
+
*
|
|
182
|
+
* @returns {number} Animation duration in ms
|
|
183
|
+
*/
|
|
184
|
+
getAnimationDuration() {
|
|
185
|
+
return this.#options.animationDuration
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Gets the data being used
|
|
190
|
+
*
|
|
191
|
+
* @returns {Array} Chart data
|
|
192
|
+
*/
|
|
193
|
+
getData() {
|
|
194
|
+
return [...this.#data]
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Gets the fields configuration
|
|
199
|
+
*
|
|
200
|
+
* @returns {Object} Fields configuration
|
|
201
|
+
*/
|
|
202
|
+
getFields() {
|
|
203
|
+
return { ...this.#fields }
|
|
204
|
+
}
|
|
205
|
+
}
|