@rokkit/chart 1.0.0-next.147 → 1.0.0-next.148
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 +45 -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 +194 -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 +90 -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
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds box geometry for box plot charts.
|
|
3
|
+
* Input data rows must already contain { q1, median, q3, iqr_min, iqr_max } —
|
|
4
|
+
* computed by applyBoxStat before reaching this function.
|
|
5
|
+
*
|
|
6
|
+
* When `fill` differs from `x`, boxes are sub-grouped within each x-band
|
|
7
|
+
* (one narrower box per fill value per x category, like grouped bars).
|
|
8
|
+
* Box body uses the lighter fill shade; whiskers and median use the darker stroke shade.
|
|
9
|
+
*
|
|
10
|
+
* @param {Object[]} data - Pre-aggregated rows with quartile fields
|
|
11
|
+
* @param {{ x: string, fill?: string }} channels
|
|
12
|
+
* `fill` drives the box and whisker color (defaults to x-field).
|
|
13
|
+
* @param {import('d3-scale').ScaleBand} xScale
|
|
14
|
+
* @param {import('d3-scale').ScaleLinear} yScale
|
|
15
|
+
* @param {Map<unknown, {fill:string, stroke:string}>} colors
|
|
16
|
+
* @returns {Array}
|
|
17
|
+
*/
|
|
18
|
+
export function buildBoxes(data, channels, xScale, yScale, colors) {
|
|
19
|
+
const { x: xf, fill: ff } = channels
|
|
20
|
+
const bw = typeof xScale.bandwidth === 'function' ? xScale.bandwidth() : 20
|
|
21
|
+
const grouped = ff && ff !== xf
|
|
22
|
+
|
|
23
|
+
if (grouped) {
|
|
24
|
+
const fillValues = [...new Set(data.map((d) => d[ff]))]
|
|
25
|
+
const n = fillValues.length
|
|
26
|
+
const subBandWidth = bw / n
|
|
27
|
+
const boxWidth = subBandWidth * 0.75
|
|
28
|
+
const whiskerWidth = subBandWidth * 0.4
|
|
29
|
+
|
|
30
|
+
return data.map((d) => {
|
|
31
|
+
const fillVal = d[ff]
|
|
32
|
+
const subIndex = fillValues.indexOf(fillVal)
|
|
33
|
+
const bandStart = xScale(d[xf]) ?? 0
|
|
34
|
+
const cx = bandStart + subIndex * subBandWidth + subBandWidth / 2
|
|
35
|
+
const colorEntry = colors?.get(fillVal) ?? { fill: '#aaa', stroke: '#666' }
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
data: d,
|
|
39
|
+
cx,
|
|
40
|
+
q1: yScale(d.q1),
|
|
41
|
+
median: yScale(d.median),
|
|
42
|
+
q3: yScale(d.q3),
|
|
43
|
+
iqr_min: yScale(d.iqr_min),
|
|
44
|
+
iqr_max: yScale(d.iqr_max),
|
|
45
|
+
width: boxWidth,
|
|
46
|
+
whiskerWidth,
|
|
47
|
+
fill: colorEntry.fill,
|
|
48
|
+
stroke: colorEntry.stroke
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Non-grouped: one box per x category
|
|
54
|
+
const boxWidth = bw * 0.6
|
|
55
|
+
const whiskerWidth = bw * 0.3
|
|
56
|
+
|
|
57
|
+
return data.map((d) => {
|
|
58
|
+
const fillKey = ff ? d[ff] : d[xf]
|
|
59
|
+
const colorEntry = colors?.get(fillKey) ?? { fill: '#aaa', stroke: '#666' }
|
|
60
|
+
const cx = (xScale(d[xf]) ?? 0) + (typeof xScale.bandwidth === 'function' ? bw / 2 : 0)
|
|
61
|
+
return {
|
|
62
|
+
data: d,
|
|
63
|
+
cx,
|
|
64
|
+
q1: yScale(d.q1),
|
|
65
|
+
median: yScale(d.median),
|
|
66
|
+
q3: yScale(d.q3),
|
|
67
|
+
iqr_min: yScale(d.iqr_min),
|
|
68
|
+
iqr_max: yScale(d.iqr_max),
|
|
69
|
+
width: boxWidth,
|
|
70
|
+
whiskerWidth,
|
|
71
|
+
fill: colorEntry.fill,
|
|
72
|
+
stroke: colorEntry.stroke
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { line, curveCatmullRom, curveStep } from 'd3-shape'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {Object[]} data
|
|
5
|
+
* @param {{ x: string, y: string, color?: string }} channels
|
|
6
|
+
* @param {Function} xScale
|
|
7
|
+
* @param {Function} yScale
|
|
8
|
+
* @param {Map} colors
|
|
9
|
+
* @param {'linear'|'smooth'|'step'} [curve]
|
|
10
|
+
* @returns {{ d: string, fill: string, stroke: string, points: {x:number, y:number, data:Object}[], key?: unknown }[]}
|
|
11
|
+
*/
|
|
12
|
+
export function buildLines(data, channels, xScale, yScale, colors, curve) {
|
|
13
|
+
const { x: xf, y: yf, color: cf } = channels
|
|
14
|
+
const xPos = (d) => typeof xScale.bandwidth === 'function'
|
|
15
|
+
? xScale(d[xf]) + xScale.bandwidth() / 2
|
|
16
|
+
: xScale(d[xf])
|
|
17
|
+
const makeGen = () => {
|
|
18
|
+
const gen = line().x(xPos).y((d) => yScale(d[yf]))
|
|
19
|
+
if (curve === 'smooth') gen.curve(curveCatmullRom)
|
|
20
|
+
else if (curve === 'step') gen.curve(curveStep)
|
|
21
|
+
return gen
|
|
22
|
+
}
|
|
23
|
+
const toPoints = (rows) => rows.map((d) => ({ x: xPos(d), y: yScale(d[yf]), data: d }))
|
|
24
|
+
|
|
25
|
+
const sortByX = (rows) => [...rows].sort((a, b) => a[xf] < b[xf] ? -1 : a[xf] > b[xf] ? 1 : 0)
|
|
26
|
+
|
|
27
|
+
if (!cf) {
|
|
28
|
+
const sorted = sortByX(data)
|
|
29
|
+
const stroke = colors?.values().next().value?.stroke ?? '#888'
|
|
30
|
+
return [{ d: makeGen()(sorted), fill: 'none', stroke, points: toPoints(sorted) }]
|
|
31
|
+
}
|
|
32
|
+
const groups = groupBy(data, cf)
|
|
33
|
+
return [...groups.entries()].map(([key, rows]) => {
|
|
34
|
+
const sorted = sortByX(rows)
|
|
35
|
+
const colorEntry = colors?.get(key) ?? { fill: 'none', stroke: '#888' }
|
|
36
|
+
return { d: makeGen()(sorted), fill: 'none', stroke: colorEntry.stroke, points: toPoints(sorted), key }
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function groupBy(arr, field) {
|
|
41
|
+
const map = new Map()
|
|
42
|
+
for (const item of arr) {
|
|
43
|
+
const key = item[field]
|
|
44
|
+
if (!map.has(key)) map.set(key, [])
|
|
45
|
+
map.get(key).push(item)
|
|
46
|
+
}
|
|
47
|
+
return map
|
|
48
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { symbol, symbolCircle, symbolSquare, symbolTriangle, symbolDiamond, symbolCross, symbolStar } from 'd3-shape'
|
|
2
|
+
|
|
3
|
+
const SYMBOL_TYPES = [symbolCircle, symbolSquare, symbolTriangle, symbolDiamond, symbolCross, symbolStar]
|
|
4
|
+
const SYMBOL_NAMES = ['circle', 'square', 'triangle', 'diamond', 'cross', 'star']
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Returns a Map assigning shape names to distinct values, cycling through available shapes.
|
|
8
|
+
* @param {unknown[]} values
|
|
9
|
+
* @returns {Map<unknown, string>}
|
|
10
|
+
*/
|
|
11
|
+
export function assignSymbols(values) {
|
|
12
|
+
return new Map(values.map((v, i) => [v, SYMBOL_NAMES[i % SYMBOL_NAMES.length]]))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Builds an SVG path string for a given shape name and radius.
|
|
17
|
+
* @param {string} shapeName
|
|
18
|
+
* @param {number} r
|
|
19
|
+
* @returns {string}
|
|
20
|
+
*/
|
|
21
|
+
export function buildSymbolPath(shapeName, r) {
|
|
22
|
+
const idx = SYMBOL_NAMES.indexOf(shapeName)
|
|
23
|
+
const type = idx >= 0 ? SYMBOL_TYPES[idx] : symbolCircle
|
|
24
|
+
return symbol().type(type).size(Math.PI * r * r)() ?? ''
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Builds point geometry for scatter/bubble charts.
|
|
29
|
+
* @param {Object[]} data
|
|
30
|
+
* @param {{ x: string, y: string, color?: string, size?: string, symbol?: string }} channels
|
|
31
|
+
* @param {Function} xScale
|
|
32
|
+
* @param {Function} yScale
|
|
33
|
+
* @param {Map} colors
|
|
34
|
+
* @param {Function|null} sizeScale
|
|
35
|
+
* @param {Map<unknown, string>|null} symbolMap — maps symbol field value → shape name
|
|
36
|
+
* @param {number} defaultRadius
|
|
37
|
+
*/
|
|
38
|
+
export function buildPoints(data, channels, xScale, yScale, colors, sizeScale, symbolMap, defaultRadius = 5) {
|
|
39
|
+
const { x: xf, y: yf, color: cf, size: sf, symbol: symf } = channels
|
|
40
|
+
return data.map((d) => {
|
|
41
|
+
const colorKey = cf ? d[cf] : null
|
|
42
|
+
const colorEntry = colors?.get(colorKey) ?? { fill: '#888', stroke: '#444' }
|
|
43
|
+
const r = sf && sizeScale ? sizeScale(d[sf]) : defaultRadius
|
|
44
|
+
const shapeName = symf && symbolMap ? (symbolMap.get(d[symf]) ?? 'circle') : null
|
|
45
|
+
const symbolPath = shapeName ? buildSymbolPath(shapeName, r) : null
|
|
46
|
+
return {
|
|
47
|
+
data: d,
|
|
48
|
+
cx: xScale(d[xf]),
|
|
49
|
+
cy: yScale(d[yf]),
|
|
50
|
+
r,
|
|
51
|
+
fill: colorEntry.fill,
|
|
52
|
+
stroke: colorEntry.stroke,
|
|
53
|
+
symbolPath,
|
|
54
|
+
key: colorKey
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { line, curveCatmullRom } from 'd3-shape'
|
|
2
|
+
|
|
3
|
+
// Relative widths at each stat anchor (fraction of max half-width)
|
|
4
|
+
const DENSITY_AT = { iqr_min: 0.08, q1: 0.55, median: 1.0, q3: 0.55, iqr_max: 0.08 }
|
|
5
|
+
const ANCHOR_ORDER = ['iqr_max', 'q3', 'median', 'q1', 'iqr_min']
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Builds a closed violin shape path for each group.
|
|
9
|
+
* Input rows must have { q1, median, q3, iqr_min, iqr_max } from applyBoxStat.
|
|
10
|
+
*
|
|
11
|
+
* When `fill` differs from `x`, violins are sub-grouped within each x-band
|
|
12
|
+
* (one narrower violin per fill value per x category, like grouped bars).
|
|
13
|
+
* Violin body uses the lighter fill shade; outline uses the darker stroke shade.
|
|
14
|
+
*
|
|
15
|
+
* @param {Object[]} data
|
|
16
|
+
* @param {{ x: string, fill?: string }} channels
|
|
17
|
+
* `fill` drives violin interior and outline (defaults to x-field).
|
|
18
|
+
* @param {import('d3-scale').ScaleBand} xScale
|
|
19
|
+
* @param {import('d3-scale').ScaleLinear} yScale
|
|
20
|
+
* @param {Map} colors
|
|
21
|
+
* @returns {Array}
|
|
22
|
+
*/
|
|
23
|
+
export function buildViolins(data, channels, xScale, yScale, colors) {
|
|
24
|
+
const { x: xf, fill: ff } = channels
|
|
25
|
+
const bw = typeof xScale.bandwidth === 'function' ? xScale.bandwidth() : 40
|
|
26
|
+
const grouped = ff && ff !== xf
|
|
27
|
+
|
|
28
|
+
const pathGen = line()
|
|
29
|
+
.x((pt) => pt.x)
|
|
30
|
+
.y((pt) => pt.y)
|
|
31
|
+
.curve(curveCatmullRom.alpha(0.5))
|
|
32
|
+
|
|
33
|
+
if (grouped) {
|
|
34
|
+
const fillValues = [...new Set(data.map((d) => d[ff]))]
|
|
35
|
+
const n = fillValues.length
|
|
36
|
+
const subBandWidth = bw / n
|
|
37
|
+
const halfMax = subBandWidth * 0.45
|
|
38
|
+
|
|
39
|
+
return data.map((d) => {
|
|
40
|
+
const fillVal = d[ff]
|
|
41
|
+
const subIndex = fillValues.indexOf(fillVal)
|
|
42
|
+
const bandStart = xScale(d[xf]) ?? 0
|
|
43
|
+
const cx = bandStart + subIndex * subBandWidth + subBandWidth / 2
|
|
44
|
+
const colorEntry = colors?.get(fillVal) ?? { fill: '#aaa', stroke: '#666' }
|
|
45
|
+
|
|
46
|
+
const rightPts = ANCHOR_ORDER.map((key) => ({
|
|
47
|
+
x: cx + halfMax * DENSITY_AT[key],
|
|
48
|
+
y: yScale(d[key])
|
|
49
|
+
}))
|
|
50
|
+
const leftPts = [...ANCHOR_ORDER].reverse().map((key) => ({
|
|
51
|
+
x: cx - halfMax * DENSITY_AT[key],
|
|
52
|
+
y: yScale(d[key])
|
|
53
|
+
}))
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
data: d,
|
|
57
|
+
cx,
|
|
58
|
+
d: pathGen([...rightPts, ...leftPts, rightPts[0]]),
|
|
59
|
+
fill: colorEntry.fill,
|
|
60
|
+
stroke: colorEntry.stroke
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Non-grouped: one violin per x category
|
|
66
|
+
const halfMax = bw * 0.45
|
|
67
|
+
|
|
68
|
+
return data.map((d) => {
|
|
69
|
+
const fillKey = ff ? d[ff] : d[xf]
|
|
70
|
+
const colorEntry = colors?.get(fillKey) ?? { fill: '#aaa', stroke: '#666' }
|
|
71
|
+
const cx = (xScale(d[xf]) ?? 0) + (typeof xScale.bandwidth === 'function' ? bw / 2 : 0)
|
|
72
|
+
|
|
73
|
+
const rightPts = ANCHOR_ORDER.map((key) => ({
|
|
74
|
+
x: cx + halfMax * DENSITY_AT[key],
|
|
75
|
+
y: yScale(d[key])
|
|
76
|
+
}))
|
|
77
|
+
const leftPts = [...ANCHOR_ORDER].reverse().map((key) => ({
|
|
78
|
+
x: cx - halfMax * DENSITY_AT[key],
|
|
79
|
+
y: yScale(d[key])
|
|
80
|
+
}))
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
data: d,
|
|
84
|
+
cx,
|
|
85
|
+
d: pathGen([...rightPts, ...leftPts, rightPts[0]]),
|
|
86
|
+
fill: colorEntry.fill,
|
|
87
|
+
stroke: colorEntry.stroke
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a data key to a safe SVG element ID for pattern references.
|
|
3
|
+
* Spaces and non-word characters are replaced to avoid broken url(#...) refs.
|
|
4
|
+
* @param {unknown} key
|
|
5
|
+
* @returns {string}
|
|
6
|
+
*/
|
|
7
|
+
export function toPatternId(key) {
|
|
8
|
+
return `chart-pat-${String(key).replace(/\s+/g, '-').replace(/[^\w-]/g, '_')}`
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Keys must match the keys in packages/chart/src/patterns/patterns.js
|
|
12
|
+
export const PATTERN_ORDER = [
|
|
13
|
+
'diagonal',
|
|
14
|
+
'dots',
|
|
15
|
+
'triangles',
|
|
16
|
+
'hatch',
|
|
17
|
+
'lattice',
|
|
18
|
+
'swell',
|
|
19
|
+
'checkerboard',
|
|
20
|
+
'waves',
|
|
21
|
+
'petals'
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Assigns patterns from PATTERN_ORDER to an array of distinct values.
|
|
26
|
+
* @param {unknown[]} values
|
|
27
|
+
* @returns {Map<unknown, string>}
|
|
28
|
+
*/
|
|
29
|
+
export function assignPatterns(values) {
|
|
30
|
+
return new Map(values.map((v, i) => [v, PATTERN_ORDER[i % PATTERN_ORDER.length]]))
|
|
31
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { scaleBand, scaleLinear, scaleSqrt } from 'd3-scale'
|
|
2
|
+
import { max, extent } from 'd3-array'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Builds an x scale (band for categorical, linear for numeric).
|
|
6
|
+
* @param {Object[]} data
|
|
7
|
+
* @param {string} field
|
|
8
|
+
* @param {number} width - inner width (pixels)
|
|
9
|
+
* @param {{ padding?: number }} opts
|
|
10
|
+
*/
|
|
11
|
+
export function buildXScale(data, field, width, opts = {}) {
|
|
12
|
+
const values = [...new Set(data.map((d) => d[field]))]
|
|
13
|
+
const isNumeric = values.every((v) => typeof v === 'number' || (!isNaN(Number(v)) && v !== ''))
|
|
14
|
+
if (isNumeric) {
|
|
15
|
+
const [minVal, maxVal] = extent(data, (d) => Number(d[field]))
|
|
16
|
+
return scaleLinear().domain([minVal, maxVal]).range([0, width]).nice()
|
|
17
|
+
}
|
|
18
|
+
return scaleBand()
|
|
19
|
+
.domain(values)
|
|
20
|
+
.range([0, width])
|
|
21
|
+
.padding(opts.padding ?? 0.2)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Builds a y linear scale from 0 to max, extended by any layer overrides.
|
|
26
|
+
* @param {Object[]} data
|
|
27
|
+
* @param {string} field
|
|
28
|
+
* @param {number} height - inner height (pixels)
|
|
29
|
+
* @param {Array<{data?: Object[], y?: string}>} layers
|
|
30
|
+
*/
|
|
31
|
+
export function buildYScale(data, field, height, layers = []) {
|
|
32
|
+
let maxVal = max(data, (d) => Number(d[field])) ?? 0
|
|
33
|
+
for (const layer of layers) {
|
|
34
|
+
if (layer.data && layer.y) {
|
|
35
|
+
const layerMax = max(layer.data, (d) => Number(d[layer.y])) ?? 0
|
|
36
|
+
if (layerMax > maxVal) maxVal = layerMax
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return scaleLinear().domain([0, maxVal]).range([height, 0]).nice()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Builds a sqrt scale for bubble/point size.
|
|
44
|
+
* @param {Object[]} data
|
|
45
|
+
* @param {string} field
|
|
46
|
+
* @param {number} maxRadius
|
|
47
|
+
*/
|
|
48
|
+
export function buildSizeScale(data, field, maxRadius = 20) {
|
|
49
|
+
const maxVal = max(data, (d) => Number(d[field])) ?? 1
|
|
50
|
+
return scaleSqrt().domain([0, maxVal]).range([0, maxRadius])
|
|
51
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { SvelteSet } from 'svelte/reactivity'
|
|
2
2
|
import { min, max } from 'd3-array'
|
|
3
|
-
import {
|
|
3
|
+
import { scaleLinear, scaleOrdinal } from 'd3-scale'
|
|
4
4
|
import { schemeCategory10 } from 'd3-scale-chromatic'
|
|
5
5
|
import {} from './types.js'
|
|
6
|
+
import { buildXScale } from '../xscale.js'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* @typedef {import('./types').ChartScales} ChartScales
|
|
@@ -10,31 +11,6 @@ import {} from './types.js'
|
|
|
10
11
|
* @typedef {import('./types').ChartDimensions} ChartDimensions
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
|
-
/**
|
|
14
|
-
* @param {Array} xValues
|
|
15
|
-
* @param {Object} dimensions
|
|
16
|
-
* @param {number} padding
|
|
17
|
-
* @returns {import('d3-scale').ScaleContinuousNumeric|import('d3-scale').ScaleBand}
|
|
18
|
-
*/
|
|
19
|
-
function buildXScale(xValues, dimensions, padding) {
|
|
20
|
-
const xIsDate = xValues.some((v) => v instanceof Date)
|
|
21
|
-
const xIsNumeric = !xIsDate && xValues.every((v) => !isNaN(parseFloat(v)))
|
|
22
|
-
|
|
23
|
-
if (xIsDate) {
|
|
24
|
-
return scaleTime()
|
|
25
|
-
.domain([min(xValues), max(xValues)])
|
|
26
|
-
.range([0, dimensions.innerWidth])
|
|
27
|
-
.nice()
|
|
28
|
-
}
|
|
29
|
-
if (xIsNumeric) {
|
|
30
|
-
return scaleLinear()
|
|
31
|
-
.domain([min([0, ...xValues]), max(xValues)])
|
|
32
|
-
.range([0, dimensions.innerWidth])
|
|
33
|
-
.nice()
|
|
34
|
-
}
|
|
35
|
-
return scaleBand().domain(xValues).range([0, dimensions.innerWidth]).padding(padding)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
14
|
/**
|
|
39
15
|
* @param {Array} data
|
|
40
16
|
* @param {string} colorField
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { sum, mean, min, max, quantile, ascending } from 'd3-array'
|
|
2
|
+
import { dataset } from '@rokkit/data'
|
|
3
|
+
|
|
4
|
+
function sortedQuantile(values, p) {
|
|
5
|
+
return quantile([...values].sort(ascending), p)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Built-in reduction functions. Each receives an array of numeric values.
|
|
10
|
+
* @type {Record<string, (values: number[]) => number>}
|
|
11
|
+
*/
|
|
12
|
+
export const STAT_FNS = {
|
|
13
|
+
sum,
|
|
14
|
+
mean,
|
|
15
|
+
min,
|
|
16
|
+
max,
|
|
17
|
+
count: (values) => values.length
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Computes box plot quartile statistics grouped by x (and optionally color).
|
|
22
|
+
* Output rows have { q1, median, q3, iqr_min, iqr_max } replacing the raw y values.
|
|
23
|
+
*
|
|
24
|
+
* @param {Object[]} data
|
|
25
|
+
* @param {{ x?: string, y?: string, color?: string }} channels
|
|
26
|
+
* @returns {Object[]}
|
|
27
|
+
*/
|
|
28
|
+
export function applyBoxStat(data, channels) {
|
|
29
|
+
const { x: xf, y: yf, color: cf } = channels
|
|
30
|
+
if (!xf || !yf) return data
|
|
31
|
+
const by = [xf, cf].filter(Boolean)
|
|
32
|
+
return dataset(data)
|
|
33
|
+
.groupBy(...by)
|
|
34
|
+
.summarize((row) => row[yf], {
|
|
35
|
+
q1: (v) => sortedQuantile(v, 0.25),
|
|
36
|
+
median: (v) => sortedQuantile(v, 0.5),
|
|
37
|
+
q3: (v) => sortedQuantile(v, 0.75),
|
|
38
|
+
iqr_min: (v) => { const q1 = sortedQuantile(v, 0.25); const q3 = sortedQuantile(v, 0.75); return q1 - 1.5 * (q3 - q1) },
|
|
39
|
+
iqr_max: (v) => { const q1 = sortedQuantile(v, 0.25); const q3 = sortedQuantile(v, 0.75); return q3 + 1.5 * (q3 - q1) }
|
|
40
|
+
})
|
|
41
|
+
.rollup()
|
|
42
|
+
.select()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Aggregates data by one or more grouping fields, reducing the value field
|
|
47
|
+
* using the given stat. Accepts a built-in name or a custom function.
|
|
48
|
+
*
|
|
49
|
+
* @param {Object[]} data
|
|
50
|
+
* @param {{ by: string[], value: string, stat: string|Function }} opts
|
|
51
|
+
* @returns {Object[]}
|
|
52
|
+
*/
|
|
53
|
+
export function applyAggregate(data, { by, value, stat }) {
|
|
54
|
+
if (stat === 'identity' || by.length === 0 || value === null || value === undefined) return data
|
|
55
|
+
const fn = typeof stat === 'function' ? stat : STAT_FNS[stat]
|
|
56
|
+
if (fn === null || fn === undefined) return data
|
|
57
|
+
return dataset(data)
|
|
58
|
+
.groupBy(...by)
|
|
59
|
+
.summarize((row) => row[value], { [value]: fn })
|
|
60
|
+
.rollup()
|
|
61
|
+
.select()
|
|
62
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const SYMBOL_ORDER = ['circle', 'square', 'triangle', 'diamond', 'plus', 'cross', 'star']
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Assigns shapes from SYMBOL_ORDER to an array of distinct values.
|
|
5
|
+
* @param {unknown[]} values
|
|
6
|
+
* @returns {Map<unknown, string>}
|
|
7
|
+
*/
|
|
8
|
+
export function assignSymbols(values) {
|
|
9
|
+
return new Map(values.map((v, i) => [v, SYMBOL_ORDER[i % SYMBOL_ORDER.length]]))
|
|
10
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared JSDoc type definitions for chart component props.
|
|
3
|
+
* Import these in chart components with:
|
|
4
|
+
* @type {import('./lib/plot/chartProps.js').ChartProps}
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {Object} ChartProps
|
|
9
|
+
* Common props shared by all high-level chart wrappers.
|
|
10
|
+
*
|
|
11
|
+
* @property {Object[]} [data=[]] - Data array to visualize
|
|
12
|
+
* @property {string} [x] - Field name for the X axis
|
|
13
|
+
* @property {string} [y] - Field name for the Y axis
|
|
14
|
+
* @property {string} [fill] - Field name for fill color grouping (alias for color)
|
|
15
|
+
* @property {string} [color] - Field name for color grouping
|
|
16
|
+
* @property {string} [pattern] - Field name for pattern fill grouping
|
|
17
|
+
* @property {string} [stat='identity']- Aggregation stat: 'identity' | 'sum' | 'mean' | 'count' | 'min' | 'max' | 'median' | 'boxplot'
|
|
18
|
+
* @property {number} [width=600] - SVG width in pixels
|
|
19
|
+
* @property {number} [height=400] - SVG height in pixels
|
|
20
|
+
* @property {string} [mode='light'] - Color mode: 'light' | 'dark'
|
|
21
|
+
* @property {boolean} [grid=true] - Whether to show grid lines
|
|
22
|
+
* @property {boolean} [legend=false] - Whether to show the legend
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {Object} BarChartProps
|
|
27
|
+
* @extends ChartProps
|
|
28
|
+
* @property {boolean} [stack=false] - Stack bars (true) or group them (false)
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {Object} PieChartProps
|
|
33
|
+
* @property {Object[]} [data=[]] - Data array to visualize
|
|
34
|
+
* @property {string} [label] - Field name for slice labels (drives color key)
|
|
35
|
+
* @property {string} [y] - Field name for slice values (theta)
|
|
36
|
+
* @property {string} [fill] - Alternative to label for color grouping
|
|
37
|
+
* @property {number} [innerRadius=0] - Inner radius as fraction of outer (0=pie, 0.5=donut)
|
|
38
|
+
* @property {string} [stat='sum'] - Aggregation stat (default sum for pie charts)
|
|
39
|
+
* @property {number} [width=400] - SVG width in pixels
|
|
40
|
+
* @property {number} [height=400] - SVG height in pixels
|
|
41
|
+
* @property {string} [mode='light'] - Color mode: 'light' | 'dark'
|
|
42
|
+
* @property {boolean} [legend=false] - Whether to show the legend
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @typedef {Object} BoxViolinChartProps
|
|
47
|
+
* @property {Object[]} [data=[]] - Data array to visualize
|
|
48
|
+
* @property {string} [x] - Field name for the category axis (groups)
|
|
49
|
+
* @property {string} [y] - Field name for the value axis (raw observations)
|
|
50
|
+
* @property {string} [fill] - Field name for fill color; when different from x, sub-groups within each x-band (like grouped bars); lighter shade used for body, darker shade for whiskers/outline
|
|
51
|
+
* @property {number} [width=600] - SVG width in pixels
|
|
52
|
+
* @property {number} [height=400] - SVG height in pixels
|
|
53
|
+
* @property {string} [mode='light'] - Color mode: 'light' | 'dark'
|
|
54
|
+
* @property {boolean} [grid=true] - Whether to show grid lines
|
|
55
|
+
* @property {boolean} [legend=false] - Whether to show the legend
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @typedef {Object} ScatterBubbleChartProps
|
|
60
|
+
* @property {Object[]} [data=[]] - Data array to visualize
|
|
61
|
+
* @property {string} [x] - Field name for X position
|
|
62
|
+
* @property {string} [y] - Field name for Y position
|
|
63
|
+
* @property {string} [color] - Field name for color grouping
|
|
64
|
+
* @property {string} [size] - Field name for point radius (BubbleChart: required)
|
|
65
|
+
* @property {number} [width=600] - SVG width in pixels
|
|
66
|
+
* @property {number} [height=400] - SVG height in pixels
|
|
67
|
+
* @property {string} [mode='light'] - Color mode: 'light' | 'dark'
|
|
68
|
+
* @property {boolean} [grid=true] - Whether to show grid lines
|
|
69
|
+
* @property {boolean} [legend=false] - Whether to show the legend
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @typedef {Object} LineAreaChartProps
|
|
74
|
+
* @extends ChartProps
|
|
75
|
+
* @property {string} [curve] - Line interpolation: 'linear' | 'smooth' | 'step'
|
|
76
|
+
*/
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Applies dimming state to data rows using crossfilter instance.
|
|
3
|
+
* Maps each row to { data: row, dimmed: boolean }.
|
|
4
|
+
*
|
|
5
|
+
* @param {Object[]} data - raw data array
|
|
6
|
+
* @param {Object} cf - crossfilter instance (from createCrossFilter)
|
|
7
|
+
* @param {Object} channels - { x, y, color, ... } field name mapping
|
|
8
|
+
* @returns {{ data: Object, dimmed: boolean }[]}
|
|
9
|
+
*/
|
|
10
|
+
export function applyDimming(data, cf, channels) {
|
|
11
|
+
const fields = Object.values(channels).filter(Boolean)
|
|
12
|
+
return data.map((row) => {
|
|
13
|
+
const dimmed = fields.some((field) => cf.isDimmed(field, row[field]))
|
|
14
|
+
return { data: row, dimmed }
|
|
15
|
+
})
|
|
16
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { extent } from 'd3-array'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Splits data into a Map of panels keyed by facet field value.
|
|
5
|
+
* Preserves insertion order of first occurrence.
|
|
6
|
+
*
|
|
7
|
+
* @param {Object[]} data
|
|
8
|
+
* @param {string} field
|
|
9
|
+
* @returns {Map<unknown, Object[]>}
|
|
10
|
+
*/
|
|
11
|
+
export function splitByField(data, field) {
|
|
12
|
+
const map = new Map()
|
|
13
|
+
for (const row of data) {
|
|
14
|
+
const key = row[field]
|
|
15
|
+
if (!map.has(key)) map.set(key, [])
|
|
16
|
+
map.get(key).push(row)
|
|
17
|
+
}
|
|
18
|
+
return map
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Computes x/y domains for each panel.
|
|
23
|
+
*
|
|
24
|
+
* @param {Map<unknown, Object[]>} panels
|
|
25
|
+
* @param {{ x: string, y: string }} channels
|
|
26
|
+
* @param {'fixed'|'free'|'free_x'|'free_y'} scalesMode
|
|
27
|
+
* @returns {Map<unknown, { xDomain: unknown[], yDomain: [number, number] }>}
|
|
28
|
+
*/
|
|
29
|
+
export function getFacetDomains(panels, channels, scalesMode = 'fixed') {
|
|
30
|
+
const { x: xf, y: yf } = channels
|
|
31
|
+
const allData = [...panels.values()].flat()
|
|
32
|
+
|
|
33
|
+
// Determine if x is categorical (string) or numeric
|
|
34
|
+
const sampleXVal = allData.find(d => d[xf] !== null && d[xf] !== undefined)?.[xf]
|
|
35
|
+
const xIsCategorical = typeof sampleXVal === 'string'
|
|
36
|
+
|
|
37
|
+
// Global domains (for fixed mode)
|
|
38
|
+
const globalXDomain = xIsCategorical
|
|
39
|
+
? [...new Set(allData.map((d) => d[xf]))]
|
|
40
|
+
: extent(allData, (d) => Number(d[xf]))
|
|
41
|
+
const globalYDomain = extent(allData, (d) => Number(d[yf]))
|
|
42
|
+
|
|
43
|
+
const result = new Map()
|
|
44
|
+
for (const [key, rows] of panels.entries()) {
|
|
45
|
+
const freeX = scalesMode === 'free' || scalesMode === 'free_x'
|
|
46
|
+
const freeY = scalesMode === 'free' || scalesMode === 'free_y'
|
|
47
|
+
|
|
48
|
+
const xDomain = freeX
|
|
49
|
+
? (xIsCategorical ? [...new Set(rows.map((d) => d[xf]))] : extent(rows, (d) => Number(d[xf])))
|
|
50
|
+
: globalXDomain
|
|
51
|
+
const yDomain = freeY
|
|
52
|
+
? extent(rows, (d) => Number(d[yf]))
|
|
53
|
+
: globalYDomain
|
|
54
|
+
|
|
55
|
+
result.set(key, { xDomain, yDomain })
|
|
56
|
+
}
|
|
57
|
+
return result
|
|
58
|
+
}
|