@rokkit/chart 1.0.0-next.147 → 1.0.0-next.149
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/Plot/index.d.ts +4 -0
- package/dist/PlotState.svelte.d.ts +47 -0
- package/dist/crossfilter/createCrossFilter.svelte.d.ts +15 -0
- package/dist/geoms/lib/areas.d.ts +52 -0
- package/dist/geoms/lib/bars.d.ts +3 -0
- package/dist/index.d.ts +38 -1
- package/dist/lib/brewing/BoxBrewer.svelte.d.ts +10 -0
- package/dist/lib/brewing/CartesianBrewer.svelte.d.ts +8 -0
- package/dist/lib/brewing/PieBrewer.svelte.d.ts +8 -0
- package/dist/lib/brewing/ViolinBrewer.svelte.d.ts +9 -0
- package/dist/lib/brewing/brewer.svelte.d.ts +145 -0
- package/dist/lib/brewing/colors.d.ts +17 -0
- package/dist/lib/brewing/marks/arcs.d.ts +17 -0
- package/dist/lib/brewing/marks/areas.d.ts +31 -0
- package/dist/lib/brewing/marks/bars.d.ts +1 -0
- package/dist/lib/brewing/marks/boxes.d.ts +24 -0
- package/dist/lib/brewing/marks/lines.d.ts +24 -0
- package/dist/lib/brewing/marks/points.d.ts +40 -0
- package/dist/lib/brewing/marks/violins.d.ts +20 -0
- package/dist/lib/brewing/patterns.d.ts +14 -0
- package/dist/lib/brewing/scales.d.ts +28 -0
- package/dist/lib/brewing/stats.d.ts +31 -0
- package/dist/lib/brewing/symbols.d.ts +7 -0
- package/dist/lib/plot/chartProps.d.ts +177 -0
- package/dist/lib/plot/crossfilter.d.ts +13 -0
- package/dist/lib/plot/facet.d.ts +24 -0
- package/dist/lib/plot/frames.d.ts +47 -0
- package/dist/lib/plot/helpers.d.ts +3 -0
- package/dist/lib/plot/preset.d.ts +29 -0
- package/dist/lib/plot/scales.d.ts +5 -0
- package/dist/lib/plot/stat.d.ts +32 -0
- package/dist/lib/plot/types.d.ts +89 -0
- package/dist/lib/scales.svelte.d.ts +1 -1
- package/dist/lib/swatch.d.ts +12 -0
- package/dist/lib/utils.d.ts +1 -0
- package/dist/lib/xscale.d.ts +11 -0
- package/dist/patterns/index.d.ts +4 -9
- package/dist/patterns/patterns.d.ts +72 -0
- package/dist/patterns/scale.d.ts +30 -0
- package/package.json +9 -3
- package/src/AnimatedPlot.svelte +214 -0
- package/src/Chart.svelte +101 -0
- package/src/FacetPlot/Panel.svelte +23 -0
- package/src/FacetPlot.svelte +90 -0
- package/src/Plot/Arc.svelte +29 -0
- package/src/Plot/Area.svelte +25 -0
- package/src/Plot/Axis.svelte +62 -84
- package/src/Plot/Grid.svelte +20 -58
- package/src/Plot/Legend.svelte +160 -120
- package/src/Plot/Line.svelte +27 -0
- package/src/Plot/Point.svelte +27 -0
- package/src/Plot/Timeline.svelte +95 -0
- package/src/Plot/Tooltip.svelte +81 -0
- package/src/Plot/index.js +4 -0
- package/src/Plot.svelte +189 -0
- package/src/PlotState.svelte.js +278 -0
- package/src/Sparkline.svelte +69 -0
- package/src/charts/AreaChart.svelte +25 -0
- package/src/charts/BarChart.svelte +26 -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 +25 -0
- package/src/charts/ScatterPlot.svelte +25 -0
- package/src/charts/ViolinPlot.svelte +21 -0
- package/src/crossfilter/CrossFilter.svelte +38 -0
- package/src/crossfilter/FilterBar.svelte +32 -0
- package/src/crossfilter/FilterSlider.svelte +79 -0
- package/src/crossfilter/createCrossFilter.svelte.js +113 -0
- package/src/elements/SymbolGrid.svelte +6 -7
- package/src/geoms/Arc.svelte +81 -0
- package/src/geoms/Area.svelte +50 -0
- package/src/geoms/Bar.svelte +142 -0
- package/src/geoms/Box.svelte +101 -0
- package/src/geoms/LabelPill.svelte +17 -0
- package/src/geoms/Line.svelte +100 -0
- package/src/geoms/Point.svelte +100 -0
- package/src/geoms/Violin.svelte +44 -0
- package/src/geoms/lib/areas.js +131 -0
- package/src/geoms/lib/bars.js +172 -0
- package/src/index.js +52 -3
- package/src/lib/brewing/BoxBrewer.svelte.js +56 -0
- package/src/lib/brewing/CartesianBrewer.svelte.js +16 -0
- package/src/lib/brewing/PieBrewer.svelte.js +14 -0
- package/src/lib/brewing/ViolinBrewer.svelte.js +55 -0
- package/src/lib/brewing/brewer.svelte.js +229 -0
- package/src/lib/brewing/colors.js +22 -0
- package/src/lib/brewing/marks/arcs.js +43 -0
- package/src/lib/brewing/marks/areas.js +59 -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 +48 -0
- package/src/lib/brewing/marks/points.js +57 -0
- package/src/lib/brewing/marks/violins.js +90 -0
- package/src/lib/brewing/patterns.js +31 -0
- package/src/lib/brewing/scales.js +51 -0
- package/src/lib/brewing/scales.svelte.js +2 -26
- package/src/lib/brewing/stats.js +62 -0
- package/src/lib/brewing/symbols.js +10 -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 +80 -0
- package/src/lib/plot/helpers.js +14 -0
- package/src/lib/plot/preset.js +53 -0
- package/src/lib/plot/scales.js +56 -0
- package/src/lib/plot/stat.js +92 -0
- package/src/lib/plot/types.js +65 -0
- package/src/lib/scales.svelte.js +2 -26
- package/src/lib/swatch.js +13 -0
- package/src/lib/utils.js +9 -0
- 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 -14
- package/src/patterns/patterns.js +208 -0
- package/src/patterns/scale.js +87 -0
- package/src/spec/chart-spec.js +29 -0
- package/src/symbols/Shape.svelte +1 -1
- package/src/symbols/constants/index.js +1 -1
- package/dist/old_lib/index.d.ts +0 -4
- package/dist/old_lib/plots.d.ts +0 -3
- package/dist/old_lib/swatch.d.ts +0 -285
- package/dist/old_lib/utils.d.ts +0 -1
- package/dist/patterns/paths/constants.d.ts +0 -1
- package/dist/template/constants.d.ts +0 -43
- package/dist/template/shapes/index.d.ts +0 -4
- package/src/old_lib/index.js +0 -4
- package/src/old_lib/plots.js +0 -27
- package/src/old_lib/swatch.js +0 -16
- package/src/old_lib/utils.js +0 -8
- package/src/patterns/Brick.svelte +0 -15
- package/src/patterns/Circles.svelte +0 -18
- package/src/patterns/CrossHatch.svelte +0 -12
- package/src/patterns/CurvedWave.svelte +0 -7
- package/src/patterns/Dots.svelte +0 -20
- package/src/patterns/OutlineCircles.svelte +0 -13
- package/src/patterns/Tile.svelte +0 -16
- package/src/patterns/Triangles.svelte +0 -13
- package/src/patterns/Waves.svelte +0 -9
- package/src/patterns/paths/NamedPattern.svelte +0 -9
- package/src/patterns/paths/constants.js +0 -4
- package/src/template/Texture.svelte +0 -13
- package/src/template/constants.js +0 -43
- package/src/template/shapes/Circles.svelte +0 -15
- package/src/template/shapes/Lines.svelte +0 -16
- package/src/template/shapes/Path.svelte +0 -9
- package/src/template/shapes/Polygons.svelte +0 -15
- package/src/template/shapes/index.js +0 -4
- /package/dist/{old_lib → lib}/brewer.d.ts +0 -0
- /package/dist/{old_lib → lib}/chart.d.ts +0 -0
- /package/dist/{old_lib → lib}/grid.d.ts +0 -0
- /package/dist/{old_lib → lib}/ticks.d.ts +0 -0
- /package/src/{old_lib → lib}/brewer.js +0 -0
- /package/src/{old_lib → lib}/chart.js +0 -0
- /package/src/{old_lib → lib}/grid.js +0 -0
- /package/src/{old_lib → lib}/ticks.js +0 -0
package/src/index.js
CHANGED
|
@@ -3,16 +3,65 @@ import Axis from './Plot/Axis.svelte'
|
|
|
3
3
|
import Bar from './Plot/Bar.svelte'
|
|
4
4
|
import Grid from './Plot/Grid.svelte'
|
|
5
5
|
import Legend from './Plot/Legend.svelte'
|
|
6
|
+
import Line from './Plot/Line.svelte'
|
|
7
|
+
import Area from './Plot/Area.svelte'
|
|
8
|
+
import Point from './Plot/Point.svelte'
|
|
9
|
+
import Arc from './Plot/Arc.svelte'
|
|
6
10
|
|
|
7
|
-
// Export components
|
|
8
|
-
export const
|
|
11
|
+
// Export components (old Plot namespace — renamed to avoid collision with new PlotChart export)
|
|
12
|
+
export const PlotLayers = {
|
|
9
13
|
Root,
|
|
10
14
|
Axis,
|
|
11
15
|
Bar,
|
|
12
16
|
Grid,
|
|
13
|
-
Legend
|
|
17
|
+
Legend,
|
|
18
|
+
Line,
|
|
19
|
+
Area,
|
|
20
|
+
Point,
|
|
21
|
+
Arc
|
|
14
22
|
}
|
|
15
23
|
|
|
24
|
+
// New Plot system
|
|
25
|
+
export { default as PlotChart } from './Plot.svelte'
|
|
26
|
+
|
|
27
|
+
// Facets and Animation
|
|
28
|
+
export { default as FacetPlot } from './FacetPlot.svelte'
|
|
29
|
+
export { default as AnimatedPlot } from './AnimatedPlot.svelte'
|
|
30
|
+
|
|
31
|
+
// Geom components (for declarative use inside PlotChart)
|
|
32
|
+
export { default as GeomBar } from './geoms/Bar.svelte'
|
|
33
|
+
export { default as GeomLine } from './geoms/Line.svelte'
|
|
34
|
+
export { default as GeomArea } from './geoms/Area.svelte'
|
|
35
|
+
export { default as GeomPoint } from './geoms/Point.svelte'
|
|
36
|
+
export { default as GeomArc } from './geoms/Arc.svelte'
|
|
37
|
+
export { default as GeomBox } from './geoms/Box.svelte'
|
|
38
|
+
export { default as GeomViolin } from './geoms/Violin.svelte'
|
|
39
|
+
|
|
40
|
+
// Export standalone components
|
|
41
|
+
export { default as Chart } from './Chart.svelte'
|
|
42
|
+
export { default as Sparkline } from './Sparkline.svelte'
|
|
43
|
+
export { default as BarChart } from './charts/BarChart.svelte'
|
|
44
|
+
export { default as LineChart } from './charts/LineChart.svelte'
|
|
45
|
+
export { default as AreaChart } from './charts/AreaChart.svelte'
|
|
46
|
+
export { default as PieChart } from './charts/PieChart.svelte'
|
|
47
|
+
export { default as ScatterPlot } from './charts/ScatterPlot.svelte'
|
|
48
|
+
export { default as BoxPlot } from './charts/BoxPlot.svelte'
|
|
49
|
+
export { default as ViolinPlot } from './charts/ViolinPlot.svelte'
|
|
50
|
+
export { default as BubbleChart } from './charts/BubbleChart.svelte'
|
|
51
|
+
|
|
52
|
+
// Export state and types
|
|
53
|
+
export { PlotState } from './PlotState.svelte.js'
|
|
54
|
+
|
|
16
55
|
// Export utilities
|
|
17
56
|
export { ChartBrewer } from './lib/brewing/index.svelte.js'
|
|
18
57
|
export * from './lib/brewing/index.svelte.js'
|
|
58
|
+
export { CartesianBrewer } from './lib/brewing/CartesianBrewer.svelte.js'
|
|
59
|
+
export { PieBrewer } from './lib/brewing/PieBrewer.svelte.js'
|
|
60
|
+
export { BoxBrewer } from './lib/brewing/BoxBrewer.svelte.js'
|
|
61
|
+
export { ViolinBrewer } from './lib/brewing/ViolinBrewer.svelte.js'
|
|
62
|
+
|
|
63
|
+
// CrossFilter system
|
|
64
|
+
export { createCrossFilter } from './crossfilter/createCrossFilter.svelte.js'
|
|
65
|
+
export { default as CrossFilter } from './crossfilter/CrossFilter.svelte'
|
|
66
|
+
export { default as FilterBar } from './crossfilter/FilterBar.svelte'
|
|
67
|
+
export { default as FilterSlider } from './crossfilter/FilterSlider.svelte'
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { quantile, ascending } from 'd3-array'
|
|
2
|
+
import { dataset } from '@rokkit/data'
|
|
3
|
+
import { ChartBrewer } from './brewer.svelte.js'
|
|
4
|
+
import { buildBoxes } from './marks/boxes.js'
|
|
5
|
+
import { buildXScale, buildYScale } from './scales.js'
|
|
6
|
+
|
|
7
|
+
function sortedQuantile(values, p) {
|
|
8
|
+
return quantile([...values].sort(ascending), p)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Brewer for box plots. Always computes quartile statistics regardless of the stat prop.
|
|
13
|
+
* Groups by x + fill (primary) or x + color (fallback).
|
|
14
|
+
* fill = box interior color, color = whisker/outline stroke.
|
|
15
|
+
*/
|
|
16
|
+
export class BoxBrewer extends ChartBrewer {
|
|
17
|
+
transform(data, channels) {
|
|
18
|
+
if (!channels.x || !channels.y) return data
|
|
19
|
+
const by = [channels.x, channels.fill ?? channels.color].filter(Boolean)
|
|
20
|
+
return dataset(data)
|
|
21
|
+
.groupBy(...by)
|
|
22
|
+
.summarize((row) => row[channels.y], {
|
|
23
|
+
q1: (v) => sortedQuantile(v, 0.25),
|
|
24
|
+
median: (v) => sortedQuantile(v, 0.5),
|
|
25
|
+
q3: (v) => sortedQuantile(v, 0.75),
|
|
26
|
+
iqr_min: (v) => { const q1 = sortedQuantile(v, 0.25); const q3 = sortedQuantile(v, 0.75); return q1 - 1.5 * (q3 - q1) },
|
|
27
|
+
iqr_max: (v) => { const q1 = sortedQuantile(v, 0.25); const q3 = sortedQuantile(v, 0.75); return q3 + 1.5 * (q3 - q1) }
|
|
28
|
+
})
|
|
29
|
+
.rollup()
|
|
30
|
+
.select()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Override xScale to use processedData (post-transform rows have the x field).
|
|
35
|
+
*/
|
|
36
|
+
xScale = $derived(
|
|
37
|
+
this.channels.x && this.processedData.length > 0
|
|
38
|
+
? buildXScale(this.processedData, this.channels.x, this.innerWidth)
|
|
39
|
+
: null
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Override yScale to use iqr_max as the upper bound of the quartile data.
|
|
44
|
+
*/
|
|
45
|
+
yScale = $derived(
|
|
46
|
+
this.channels.y && this.processedData.length > 0
|
|
47
|
+
? buildYScale(this.processedData, 'iqr_max', this.innerHeight)
|
|
48
|
+
: null
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
boxes = $derived(
|
|
52
|
+
this.xScale && this.yScale
|
|
53
|
+
? buildBoxes(this.processedData, this.channels, this.xScale, this.yScale, this.colorMap)
|
|
54
|
+
: []
|
|
55
|
+
)
|
|
56
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ChartBrewer } from './brewer.svelte.js'
|
|
2
|
+
import { applyAggregate } from './stats.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Brewer for cartesian charts (Bar, Line, Area).
|
|
6
|
+
* Groups by x (and fill/color if set) and applies the given stat.
|
|
7
|
+
*/
|
|
8
|
+
export class CartesianBrewer extends ChartBrewer {
|
|
9
|
+
transform(data, channels, stat) {
|
|
10
|
+
if (stat === 'identity' || !channels.x || !channels.y) return data
|
|
11
|
+
// Group by all mapped aesthetic dimensions so they survive aggregation.
|
|
12
|
+
// e.g. x=region, fill=region, pattern=quarter → by=['region','quarter']
|
|
13
|
+
const by = [...new Set([channels.x, channels.fill, channels.color, channels.pattern].filter(Boolean))]
|
|
14
|
+
return applyAggregate(data, { by, value: channels.y, stat })
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ChartBrewer } from './brewer.svelte.js'
|
|
2
|
+
import { applyAggregate } from './stats.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Brewer for pie charts. Always aggregates by the label field.
|
|
6
|
+
* 'identity' is not meaningful for pie charts — falls back to 'sum'.
|
|
7
|
+
*/
|
|
8
|
+
export class PieBrewer extends ChartBrewer {
|
|
9
|
+
transform(data, channels, stat) {
|
|
10
|
+
if (!channels.label || !channels.y) return data
|
|
11
|
+
const effectiveStat = stat === 'identity' ? 'sum' : (stat ?? 'sum')
|
|
12
|
+
return applyAggregate(data, { by: [channels.label], value: channels.y, stat: effectiveStat })
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { quantile, ascending } from 'd3-array'
|
|
2
|
+
import { dataset } from '@rokkit/data'
|
|
3
|
+
import { ChartBrewer } from './brewer.svelte.js'
|
|
4
|
+
import { buildViolins } from './marks/violins.js'
|
|
5
|
+
import { buildXScale, buildYScale } from './scales.js'
|
|
6
|
+
|
|
7
|
+
function sortedQuantile(values, p) {
|
|
8
|
+
return quantile([...values].sort(ascending), p)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Brewer for violin plots. Computes the same quartile statistics as BoxBrewer.
|
|
13
|
+
* fill = violin body color, color = outline stroke.
|
|
14
|
+
*/
|
|
15
|
+
export class ViolinBrewer extends ChartBrewer {
|
|
16
|
+
transform(data, channels) {
|
|
17
|
+
if (!channels.x || !channels.y) return data
|
|
18
|
+
const by = [channels.x, channels.fill ?? channels.color].filter(Boolean)
|
|
19
|
+
return dataset(data)
|
|
20
|
+
.groupBy(...by)
|
|
21
|
+
.summarize((row) => row[channels.y], {
|
|
22
|
+
q1: (v) => sortedQuantile(v, 0.25),
|
|
23
|
+
median: (v) => sortedQuantile(v, 0.5),
|
|
24
|
+
q3: (v) => sortedQuantile(v, 0.75),
|
|
25
|
+
iqr_min: (v) => { const q1 = sortedQuantile(v, 0.25); const q3 = sortedQuantile(v, 0.75); return q1 - 1.5 * (q3 - q1) },
|
|
26
|
+
iqr_max: (v) => { const q1 = sortedQuantile(v, 0.25); const q3 = sortedQuantile(v, 0.75); return q3 + 1.5 * (q3 - q1) }
|
|
27
|
+
})
|
|
28
|
+
.rollup()
|
|
29
|
+
.select()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Override xScale to use processedData (post-transform rows have the x field).
|
|
34
|
+
*/
|
|
35
|
+
xScale = $derived(
|
|
36
|
+
this.channels.x && this.processedData.length > 0
|
|
37
|
+
? buildXScale(this.processedData, this.channels.x, this.innerWidth)
|
|
38
|
+
: null
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Override yScale to use iqr_max as the upper bound of the quartile data.
|
|
43
|
+
*/
|
|
44
|
+
yScale = $derived(
|
|
45
|
+
this.channels.y && this.processedData.length > 0
|
|
46
|
+
? buildYScale(this.processedData, 'iqr_max', this.innerHeight)
|
|
47
|
+
: null
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
violins = $derived(
|
|
51
|
+
this.xScale && this.yScale
|
|
52
|
+
? buildViolins(this.processedData, this.channels, this.xScale, this.yScale, this.colorMap)
|
|
53
|
+
: []
|
|
54
|
+
)
|
|
55
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { distinct, assignColors } from './colors.js'
|
|
2
|
+
import { assignPatterns, toPatternId, PATTERN_ORDER } from './patterns.js'
|
|
3
|
+
import { assignSymbols } from './symbols.js'
|
|
4
|
+
import { buildXScale, buildYScale, buildSizeScale } from './scales.js'
|
|
5
|
+
import { buildBars } from './marks/bars.js'
|
|
6
|
+
import { buildLines } from './marks/lines.js'
|
|
7
|
+
import { buildAreas } from './marks/areas.js'
|
|
8
|
+
import { buildArcs } from './marks/arcs.js'
|
|
9
|
+
import { buildPoints } from './marks/points.js'
|
|
10
|
+
|
|
11
|
+
const DEFAULT_MARGIN = { top: 20, right: 20, bottom: 40, left: 50 }
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Groups aesthetic channel mappings by field name, merging aesthetics that
|
|
15
|
+
* share the same field into one legend section.
|
|
16
|
+
*
|
|
17
|
+
* @param {{ fill?: string, color?: string, pattern?: string, symbol?: string }} channels
|
|
18
|
+
* `fill` takes precedence over `color` for polygon charts (bars, areas, pie slices).
|
|
19
|
+
* @param {Map<unknown, {fill:string, stroke:string}>} colorMap
|
|
20
|
+
* @param {Map<unknown, string>} patternMap
|
|
21
|
+
* @param {Map<unknown, string>} symbolMap
|
|
22
|
+
* @returns {{ field: string, items: { label: string, fill: string|null, stroke: string|null, patternId: string|null, shape: string|null }[] }[]}
|
|
23
|
+
*/
|
|
24
|
+
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)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
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
|
+
}
|
|
229
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import palette from './palette.json'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extracts distinct values for a given field from the data array.
|
|
5
|
+
* @param {Object[]} data
|
|
6
|
+
* @param {string|null} field
|
|
7
|
+
* @returns {unknown[]}
|
|
8
|
+
*/
|
|
9
|
+
export function distinct(data, field) {
|
|
10
|
+
if (!field) return []
|
|
11
|
+
return [...new Set(data.map((d) => d[field]))].filter((v) => v !== null && v !== undefined)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Assigns palette colors to an array of distinct values.
|
|
16
|
+
* @param {unknown[]} values
|
|
17
|
+
* @param {'light'|'dark'} mode
|
|
18
|
+
* @returns {Map<unknown, {fill: string, stroke: string}>}
|
|
19
|
+
*/
|
|
20
|
+
export function assignColors(values, mode = 'light') {
|
|
21
|
+
return new Map(values.map((v, i) => [v, palette[i % palette.length].shades[mode]]))
|
|
22
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { pie, arc } from 'd3-shape'
|
|
2
|
+
import { toPatternId } from '../../brewing/patterns.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Builds arc geometry for pie/donut charts.
|
|
6
|
+
* @param {Object[]} data
|
|
7
|
+
* @param {{ color: string, y: string, pattern?: string }} channels
|
|
8
|
+
* @param {Map} colors
|
|
9
|
+
* @param {number} width
|
|
10
|
+
* @param {number} height
|
|
11
|
+
* @param {{ innerRadius?: number }} opts
|
|
12
|
+
* @param {Map<unknown, string>} [patterns]
|
|
13
|
+
*/
|
|
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
|
+
})
|
|
43
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { area, curveCatmullRom, curveStep } from 'd3-shape'
|
|
2
|
+
import { toPatternId } from '../patterns.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {Object[]} data
|
|
6
|
+
* @param {{ x: string, y: string, fill?: string, pattern?: string }} channels
|
|
7
|
+
* `fill` is the primary aesthetic — drives grouping and interior color.
|
|
8
|
+
* @param {Function} xScale
|
|
9
|
+
* @param {Function} yScale
|
|
10
|
+
* @param {Map} colors
|
|
11
|
+
* @param {'linear'|'smooth'|'step'} [curve]
|
|
12
|
+
* @param {Map} [patternMap]
|
|
13
|
+
*/
|
|
14
|
+
export function buildAreas(data, channels, xScale, yScale, colors, curve, patternMap) {
|
|
15
|
+
const { x: xf, y: yf, pattern: pf } = channels
|
|
16
|
+
const cf = channels.fill // fill is the primary aesthetic for area charts
|
|
17
|
+
const innerHeight = yScale.range()[0]
|
|
18
|
+
const xPos = (d) => typeof xScale.bandwidth === 'function'
|
|
19
|
+
? xScale(d[xf]) + xScale.bandwidth() / 2
|
|
20
|
+
: xScale(d[xf])
|
|
21
|
+
const makeGen = () => {
|
|
22
|
+
const gen = area().x(xPos).y0(innerHeight).y1((d) => yScale(d[yf]))
|
|
23
|
+
if (curve === 'smooth') gen.curve(curveCatmullRom)
|
|
24
|
+
else if (curve === 'step') gen.curve(curveStep)
|
|
25
|
+
return gen
|
|
26
|
+
}
|
|
27
|
+
if (!cf) {
|
|
28
|
+
const colorEntry = colors?.values().next().value ?? { fill: '#888', stroke: '#444' }
|
|
29
|
+
return [{ d: makeGen()(data), fill: colorEntry.fill, stroke: 'none', colorKey: null, patternKey: null, patternId: null }]
|
|
30
|
+
}
|
|
31
|
+
const groups = groupBy(data, cf)
|
|
32
|
+
return [...groups.entries()].map(([key, rows]) => {
|
|
33
|
+
const colorEntry = colors?.get(key) ?? { fill: '#888', stroke: '#444' }
|
|
34
|
+
const patternKey = pf ? (pf === cf ? key : rows[0]?.[pf]) : null
|
|
35
|
+
const patternName = patternKey !== null && patternKey !== undefined ? patternMap?.get(patternKey) : null
|
|
36
|
+
const compositePatternKey = (cf && pf && cf !== pf && patternKey !== null && patternKey !== undefined)
|
|
37
|
+
? `${key}::${patternKey}`
|
|
38
|
+
: patternKey
|
|
39
|
+
return {
|
|
40
|
+
d: makeGen()(rows),
|
|
41
|
+
fill: colorEntry.fill,
|
|
42
|
+
stroke: 'none',
|
|
43
|
+
key,
|
|
44
|
+
colorKey: key,
|
|
45
|
+
patternKey,
|
|
46
|
+
patternId: patternName ? toPatternId(compositePatternKey) : null
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function groupBy(arr, field) {
|
|
52
|
+
const map = new Map()
|
|
53
|
+
for (const item of arr) {
|
|
54
|
+
const key = item[field]
|
|
55
|
+
if (!map.has(key)) map.set(key, [])
|
|
56
|
+
map.get(key).push(item)
|
|
57
|
+
}
|
|
58
|
+
return map
|
|
59
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {Object[]} data
|
|
3
|
+
* @param {{ x: string, y: string, fill?: string, color?: string, pattern?: string }} channels
|
|
4
|
+
* `fill` drives the bar interior color. `color` drives the border stroke; falls back to fill.
|
|
5
|
+
* @param {import('d3-scale').ScaleBand|import('d3-scale').ScaleLinear} xScale
|
|
6
|
+
* @param {import('d3-scale').ScaleLinear} yScale
|
|
7
|
+
* @param {Map} colors - value→{fill,stroke}
|
|
8
|
+
* @param {Map} [patternMap] - value→patternName
|
|
9
|
+
* @returns {Array}
|
|
10
|
+
*/
|
|
11
|
+
import { toPatternId } from '../patterns.js'
|
|
12
|
+
|
|
13
|
+
export function buildBars(data, channels, xScale, yScale, colors, patternMap) {
|
|
14
|
+
const { x: xf, y: yf, fill: ff, color: cf, pattern: pf } = channels
|
|
15
|
+
const barWidth = typeof xScale.bandwidth === 'function' ? xScale.bandwidth() : 10
|
|
16
|
+
const innerHeight = yScale.range()[0]
|
|
17
|
+
|
|
18
|
+
return data.map((d) => {
|
|
19
|
+
const xVal = d[xf]
|
|
20
|
+
const fillKey = ff ? d[ff] : xVal // fill channel drives interior color
|
|
21
|
+
const strokeKey = cf ? d[cf] : null // color channel drives border; null = no border
|
|
22
|
+
const colorEntry = colors?.get(fillKey) ?? { fill: '#888', stroke: '#444' }
|
|
23
|
+
const strokeEntry = colors?.get(strokeKey) ?? colorEntry
|
|
24
|
+
const patternKey = pf ? d[pf] : null
|
|
25
|
+
const patternName = patternKey !== null && patternKey !== undefined ? patternMap?.get(patternKey) : null
|
|
26
|
+
// When fill and pattern are different fields, bars need a composite pattern def id
|
|
27
|
+
// so each (region, category) pair gets its uniquely colored+textured pattern.
|
|
28
|
+
const compositePatternKey = (ff && pf && ff !== pf && patternKey !== null && patternKey !== undefined)
|
|
29
|
+
? `${d[ff]}::${patternKey}`
|
|
30
|
+
: patternKey
|
|
31
|
+
const barX = typeof xScale.bandwidth === 'function'
|
|
32
|
+
? xScale(xVal)
|
|
33
|
+
: xScale(xVal) - barWidth / 2
|
|
34
|
+
const barY = yScale(d[yf])
|
|
35
|
+
return {
|
|
36
|
+
data: d,
|
|
37
|
+
key: `${xVal}::${fillKey ?? ''}::${patternKey ?? ''}`,
|
|
38
|
+
x: barX,
|
|
39
|
+
y: barY,
|
|
40
|
+
width: barWidth,
|
|
41
|
+
height: innerHeight - barY,
|
|
42
|
+
fill: colorEntry.fill,
|
|
43
|
+
stroke: strokeKey !== null ? strokeEntry.stroke : null,
|
|
44
|
+
colorKey: fillKey,
|
|
45
|
+
patternKey,
|
|
46
|
+
patternId: patternName ? toPatternId(compositePatternKey) : null
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
}
|