@rokkit/chart 1.0.0-next.151 → 1.0.0-next.155
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/PlotState.svelte.d.ts +26 -0
- package/dist/index.d.ts +6 -1
- package/dist/lib/brewing/BoxBrewer.svelte.d.ts +3 -5
- package/dist/lib/brewing/QuartileBrewer.svelte.d.ts +9 -0
- package/dist/lib/brewing/ViolinBrewer.svelte.d.ts +3 -4
- package/dist/lib/brewing/colors.d.ts +10 -1
- package/dist/lib/brewing/marks/points.d.ts +17 -2
- package/dist/lib/keyboard-nav.d.ts +15 -0
- package/dist/lib/plot/preset.d.ts +1 -1
- package/dist/lib/preset.d.ts +30 -0
- package/package.json +2 -1
- package/src/AnimatedPlot.svelte +375 -207
- package/src/Chart.svelte +81 -84
- package/src/ChartProvider.svelte +10 -0
- package/src/FacetPlot/Panel.svelte +30 -16
- package/src/FacetPlot.svelte +100 -76
- package/src/Plot/Area.svelte +26 -19
- package/src/Plot/Axis.svelte +81 -59
- package/src/Plot/Bar.svelte +47 -89
- package/src/Plot/Grid.svelte +23 -19
- package/src/Plot/Legend.svelte +213 -147
- package/src/Plot/Line.svelte +31 -21
- package/src/Plot/Point.svelte +35 -22
- package/src/Plot/Root.svelte +46 -91
- package/src/Plot/Timeline.svelte +82 -82
- package/src/Plot/Tooltip.svelte +68 -62
- package/src/Plot.svelte +290 -174
- package/src/PlotState.svelte.js +338 -265
- package/src/Sparkline.svelte +95 -56
- package/src/charts/AreaChart.svelte +22 -20
- package/src/charts/BarChart.svelte +23 -21
- package/src/charts/BoxPlot.svelte +15 -15
- package/src/charts/BubbleChart.svelte +17 -17
- package/src/charts/LineChart.svelte +20 -20
- package/src/charts/PieChart.svelte +30 -20
- package/src/charts/ScatterPlot.svelte +20 -19
- package/src/charts/ViolinPlot.svelte +15 -15
- package/src/crossfilter/CrossFilter.svelte +33 -29
- package/src/crossfilter/FilterBar.svelte +17 -25
- package/src/crossfilter/FilterHistogram.svelte +290 -0
- package/src/crossfilter/FilterSlider.svelte +69 -65
- package/src/crossfilter/createCrossFilter.svelte.js +94 -90
- package/src/geoms/Arc.svelte +114 -69
- package/src/geoms/Area.svelte +67 -39
- package/src/geoms/Bar.svelte +184 -126
- package/src/geoms/Box.svelte +101 -91
- package/src/geoms/LabelPill.svelte +11 -11
- package/src/geoms/Line.svelte +110 -86
- package/src/geoms/Point.svelte +130 -90
- package/src/geoms/Violin.svelte +51 -41
- package/src/geoms/lib/areas.js +122 -99
- package/src/geoms/lib/bars.js +195 -144
- package/src/index.js +21 -14
- package/src/lib/brewing/BoxBrewer.svelte.js +8 -50
- package/src/lib/brewing/CartesianBrewer.svelte.js +11 -7
- package/src/lib/brewing/PieBrewer.svelte.js +5 -5
- package/src/lib/brewing/QuartileBrewer.svelte.js +51 -0
- package/src/lib/brewing/ViolinBrewer.svelte.js +8 -49
- package/src/lib/brewing/brewer.svelte.js +242 -195
- package/src/lib/brewing/colors.js +34 -5
- package/src/lib/brewing/marks/arcs.js +28 -28
- package/src/lib/brewing/marks/areas.js +54 -41
- package/src/lib/brewing/marks/bars.js +34 -34
- package/src/lib/brewing/marks/boxes.js +51 -51
- package/src/lib/brewing/marks/lines.js +37 -30
- package/src/lib/brewing/marks/points.js +74 -26
- package/src/lib/brewing/marks/violins.js +57 -57
- package/src/lib/brewing/patterns.js +25 -11
- package/src/lib/brewing/scales.js +17 -17
- package/src/lib/brewing/stats.js +37 -29
- package/src/lib/brewing/symbols.js +1 -1
- package/src/lib/chart.js +2 -1
- package/src/lib/keyboard-nav.js +37 -0
- package/src/lib/plot/crossfilter.js +5 -5
- package/src/lib/plot/facet.js +30 -30
- package/src/lib/plot/frames.js +30 -29
- package/src/lib/plot/helpers.js +4 -4
- package/src/lib/plot/preset.js +48 -34
- package/src/lib/plot/scales.js +64 -39
- package/src/lib/plot/stat.js +47 -47
- package/src/lib/preset.js +41 -0
- package/src/patterns/DefinePatterns.svelte +24 -24
- package/src/patterns/README.md +3 -0
- package/src/patterns/patterns.js +328 -176
- package/src/patterns/scale.js +61 -32
- package/src/spec/chart-spec.js +64 -21
|
@@ -12,48 +12,61 @@ import { toPatternId } from '../patterns.js'
|
|
|
12
12
|
* @param {Map} [patternMap]
|
|
13
13
|
*/
|
|
14
14
|
export function buildAreas(data, channels, xScale, yScale, colors, curve, patternMap) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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) =>
|
|
19
|
+
typeof xScale.bandwidth === 'function' ? xScale(d[xf]) + xScale.bandwidth() / 2 : xScale(d[xf])
|
|
20
|
+
const makeGen = () => {
|
|
21
|
+
const gen = area()
|
|
22
|
+
.x(xPos)
|
|
23
|
+
.y0(innerHeight)
|
|
24
|
+
.y1((d) => yScale(d[yf]))
|
|
25
|
+
if (curve === 'smooth') gen.curve(curveCatmullRom)
|
|
26
|
+
else if (curve === 'step') gen.curve(curveStep)
|
|
27
|
+
return gen
|
|
28
|
+
}
|
|
29
|
+
if (!cf) {
|
|
30
|
+
const colorEntry = colors?.values().next().value ?? { fill: '#888', stroke: '#444' }
|
|
31
|
+
return [
|
|
32
|
+
{
|
|
33
|
+
d: makeGen()(data),
|
|
34
|
+
fill: colorEntry.fill,
|
|
35
|
+
stroke: 'none',
|
|
36
|
+
colorKey: null,
|
|
37
|
+
patternKey: null,
|
|
38
|
+
patternId: null
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
const groups = groupBy(data, cf)
|
|
43
|
+
return [...groups.entries()].map(([key, rows]) => {
|
|
44
|
+
const colorEntry = colors?.get(key) ?? { fill: '#888', stroke: '#444' }
|
|
45
|
+
const patternKey = pf ? (pf === cf ? key : rows[0]?.[pf]) : null
|
|
46
|
+
const patternName =
|
|
47
|
+
patternKey !== null && patternKey !== undefined ? patternMap?.get(patternKey) : null
|
|
48
|
+
const compositePatternKey =
|
|
49
|
+
cf && pf && cf !== pf && patternKey !== null && patternKey !== undefined
|
|
50
|
+
? `${key}::${patternKey}`
|
|
51
|
+
: patternKey
|
|
52
|
+
return {
|
|
53
|
+
d: makeGen()(rows),
|
|
54
|
+
fill: colorEntry.fill,
|
|
55
|
+
stroke: 'none',
|
|
56
|
+
key,
|
|
57
|
+
colorKey: key,
|
|
58
|
+
patternKey,
|
|
59
|
+
patternId: patternName ? toPatternId(compositePatternKey) : null
|
|
60
|
+
}
|
|
61
|
+
})
|
|
49
62
|
}
|
|
50
63
|
|
|
51
64
|
function groupBy(arr, field) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
65
|
+
const map = new Map()
|
|
66
|
+
for (const item of arr) {
|
|
67
|
+
const key = item[field]
|
|
68
|
+
if (!map.has(key)) map.set(key, [])
|
|
69
|
+
map.get(key).push(item)
|
|
70
|
+
}
|
|
71
|
+
return map
|
|
59
72
|
}
|
|
@@ -11,39 +11,39 @@
|
|
|
11
11
|
import { toPatternId } from '../patterns.js'
|
|
12
12
|
|
|
13
13
|
export function buildBars(data, channels, xScale, yScale, colors, patternMap) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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 =
|
|
26
|
+
patternKey !== null && patternKey !== undefined ? patternMap?.get(patternKey) : null
|
|
27
|
+
// When fill and pattern are different fields, bars need a composite pattern def id
|
|
28
|
+
// so each (region, category) pair gets its uniquely colored+textured pattern.
|
|
29
|
+
const compositePatternKey =
|
|
30
|
+
ff && pf && ff !== pf && patternKey !== null && patternKey !== undefined
|
|
31
|
+
? `${d[ff]}::${patternKey}`
|
|
32
|
+
: patternKey
|
|
33
|
+
const barX = typeof xScale.bandwidth === 'function' ? xScale(xVal) : 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
49
|
}
|
|
@@ -16,60 +16,60 @@
|
|
|
16
16
|
* @returns {Array}
|
|
17
17
|
*/
|
|
18
18
|
export function buildBoxes(data, channels, xScale, yScale, colors) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
const { x: xf, fill: ff } = channels
|
|
20
|
+
const bw = typeof xScale.bandwidth === 'function' ? xScale.bandwidth() : 20
|
|
21
|
+
const grouped = ff && ff !== xf
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
// Non-grouped: one box per x category
|
|
54
|
+
const boxWidth = bw * 0.6
|
|
55
|
+
const whiskerWidth = bw * 0.3
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
75
|
}
|
|
@@ -10,39 +10,46 @@ import { line, curveCatmullRom, curveStep } from 'd3-shape'
|
|
|
10
10
|
* @returns {{ d: string, fill: string, stroke: string, points: {x:number, y:number, data:Object}[], key?: unknown }[]}
|
|
11
11
|
*/
|
|
12
12
|
export function buildLines(data, channels, xScale, yScale, colors, curve) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
13
|
+
const { x: xf, y: yf, color: cf } = channels
|
|
14
|
+
const xPos = (d) =>
|
|
15
|
+
typeof xScale.bandwidth === 'function' ? xScale(d[xf]) + xScale.bandwidth() / 2 : xScale(d[xf])
|
|
16
|
+
const makeGen = () => {
|
|
17
|
+
const gen = line()
|
|
18
|
+
.x(xPos)
|
|
19
|
+
.y((d) => yScale(d[yf]))
|
|
20
|
+
if (curve === 'smooth') gen.curve(curveCatmullRom)
|
|
21
|
+
else if (curve === 'step') gen.curve(curveStep)
|
|
22
|
+
return gen
|
|
23
|
+
}
|
|
24
|
+
const toPoints = (rows) => rows.map((d) => ({ x: xPos(d), y: yScale(d[yf]), data: d }))
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
const sortByX = (rows) => [...rows].sort((a, b) => xPos(a) - xPos(b))
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
28
|
+
if (!cf) {
|
|
29
|
+
const sorted = sortByX(data)
|
|
30
|
+
const stroke = colors?.values().next().value?.stroke ?? '#888'
|
|
31
|
+
return [{ d: makeGen()(sorted), fill: 'none', stroke, points: toPoints(sorted) }]
|
|
32
|
+
}
|
|
33
|
+
const groups = groupBy(data, cf)
|
|
34
|
+
return [...groups.entries()].map(([key, rows]) => {
|
|
35
|
+
const sorted = sortByX(rows)
|
|
36
|
+
const colorEntry = colors?.get(key) ?? { fill: 'none', stroke: '#888' }
|
|
37
|
+
return {
|
|
38
|
+
d: makeGen()(sorted),
|
|
39
|
+
fill: 'none',
|
|
40
|
+
stroke: colorEntry.stroke,
|
|
41
|
+
points: toPoints(sorted),
|
|
42
|
+
key
|
|
43
|
+
}
|
|
44
|
+
})
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
function groupBy(arr, field) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
const map = new Map()
|
|
49
|
+
for (const item of arr) {
|
|
50
|
+
const key = item[field]
|
|
51
|
+
if (!map.has(key)) map.set(key, [])
|
|
52
|
+
map.get(key).push(item)
|
|
53
|
+
}
|
|
54
|
+
return map
|
|
48
55
|
}
|
|
@@ -1,15 +1,33 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
symbol,
|
|
3
|
+
symbolCircle,
|
|
4
|
+
symbolSquare,
|
|
5
|
+
symbolTriangle,
|
|
6
|
+
symbolDiamond,
|
|
7
|
+
symbolCross,
|
|
8
|
+
symbolStar
|
|
9
|
+
} from 'd3-shape'
|
|
10
|
+
import { defaultPreset } from '../../preset.js'
|
|
2
11
|
|
|
3
|
-
const SYMBOL_TYPES = [
|
|
12
|
+
const SYMBOL_TYPES = [
|
|
13
|
+
symbolCircle,
|
|
14
|
+
symbolSquare,
|
|
15
|
+
symbolTriangle,
|
|
16
|
+
symbolDiamond,
|
|
17
|
+
symbolCross,
|
|
18
|
+
symbolStar
|
|
19
|
+
]
|
|
4
20
|
const SYMBOL_NAMES = ['circle', 'square', 'triangle', 'diamond', 'cross', 'star']
|
|
5
21
|
|
|
6
22
|
/**
|
|
7
23
|
* Returns a Map assigning shape names to distinct values, cycling through available shapes.
|
|
8
24
|
* @param {unknown[]} values
|
|
25
|
+
* @param {typeof defaultPreset} preset
|
|
9
26
|
* @returns {Map<unknown, string>}
|
|
10
27
|
*/
|
|
11
|
-
export function assignSymbols(values) {
|
|
12
|
-
|
|
28
|
+
export function assignSymbols(values, preset = defaultPreset) {
|
|
29
|
+
const names = preset.symbols
|
|
30
|
+
return new Map(values.map((v, i) => [v, names[i % names.length]]))
|
|
13
31
|
}
|
|
14
32
|
|
|
15
33
|
/**
|
|
@@ -19,9 +37,26 @@ export function assignSymbols(values) {
|
|
|
19
37
|
* @returns {string}
|
|
20
38
|
*/
|
|
21
39
|
export function buildSymbolPath(shapeName, r) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
40
|
+
const idx = SYMBOL_NAMES.indexOf(shapeName)
|
|
41
|
+
const type = idx >= 0 ? SYMBOL_TYPES[idx] : symbolCircle
|
|
42
|
+
return (
|
|
43
|
+
symbol()
|
|
44
|
+
.type(type)
|
|
45
|
+
.size(Math.PI * r * r)() ?? ''
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Returns a stable pseudo-random offset for a given index.
|
|
51
|
+
* Uses a linear congruential generator seeded by index — no external dependency,
|
|
52
|
+
* stable across re-renders.
|
|
53
|
+
* @param {number} i - row index (seed)
|
|
54
|
+
* @param {number} range - total spread (jitter is ±range/2)
|
|
55
|
+
* @returns {number}
|
|
56
|
+
*/
|
|
57
|
+
export function jitterOffset(i, range) {
|
|
58
|
+
const r = ((i * 1664525 + 1013904223) >>> 0) / 0xffffffff
|
|
59
|
+
return (r - 0.5) * range
|
|
25
60
|
}
|
|
26
61
|
|
|
27
62
|
/**
|
|
@@ -34,24 +69,37 @@ export function buildSymbolPath(shapeName, r) {
|
|
|
34
69
|
* @param {Function|null} sizeScale
|
|
35
70
|
* @param {Map<unknown, string>|null} symbolMap — maps symbol field value → shape name
|
|
36
71
|
* @param {number} defaultRadius
|
|
72
|
+
* @param {{ width?: number, height?: number }|null} jitter
|
|
37
73
|
*/
|
|
38
|
-
export function buildPoints(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
74
|
+
export function buildPoints(
|
|
75
|
+
data,
|
|
76
|
+
channels,
|
|
77
|
+
xScale,
|
|
78
|
+
yScale,
|
|
79
|
+
colors,
|
|
80
|
+
sizeScale,
|
|
81
|
+
symbolMap,
|
|
82
|
+
defaultRadius = 5,
|
|
83
|
+
jitter = null
|
|
84
|
+
) {
|
|
85
|
+
const { x: xf, y: yf, color: cf, size: sf, symbol: symf } = channels
|
|
86
|
+
return data.map((d, i) => {
|
|
87
|
+
const colorKey = cf ? d[cf] : null
|
|
88
|
+
const colorEntry = colors?.get(colorKey) ?? { fill: '#888', stroke: '#444' }
|
|
89
|
+
const r = sf && sizeScale ? sizeScale(d[sf]) : defaultRadius
|
|
90
|
+
const shapeName = symf && symbolMap ? (symbolMap.get(d[symf]) ?? 'circle') : null
|
|
91
|
+
const symbolPath = shapeName ? buildSymbolPath(shapeName, r) : null
|
|
92
|
+
const jx = jitter?.width ? jitterOffset(i, jitter.width) : 0
|
|
93
|
+
const jy = jitter?.height ? jitterOffset(i + 100000, jitter.height) : 0
|
|
94
|
+
return {
|
|
95
|
+
data: d,
|
|
96
|
+
cx: xScale(d[xf]) + jx,
|
|
97
|
+
cy: yScale(d[yf]) + jy,
|
|
98
|
+
r,
|
|
99
|
+
fill: colorEntry.fill,
|
|
100
|
+
stroke: colorEntry.stroke,
|
|
101
|
+
symbolPath,
|
|
102
|
+
key: colorKey
|
|
103
|
+
}
|
|
104
|
+
})
|
|
57
105
|
}
|
|
@@ -21,70 +21,70 @@ const ANCHOR_ORDER = ['iqr_max', 'q3', 'median', 'q1', 'iqr_min']
|
|
|
21
21
|
* @returns {Array}
|
|
22
22
|
*/
|
|
23
23
|
export function buildViolins(data, channels, xScale, yScale, colors) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
const { x: xf, fill: ff } = channels
|
|
25
|
+
const bw = typeof xScale.bandwidth === 'function' ? xScale.bandwidth() : 40
|
|
26
|
+
const grouped = ff && ff !== xf
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
const pathGen = line()
|
|
29
|
+
.x((pt) => pt.x)
|
|
30
|
+
.y((pt) => pt.y)
|
|
31
|
+
.curve(curveCatmullRom.alpha(0.5))
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
64
|
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
// Non-grouped: one violin per x category
|
|
66
|
+
const halfMax = bw * 0.45
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
72
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
81
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
90
|
}
|