@rokkit/chart 1.3.6 → 1.3.8
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/AnimatedPlot.svelte +26 -8
- package/dist/AnimatedPlot.svelte.d.ts +5 -0
- package/dist/Chart.svelte +1 -1
- package/dist/FacetPlot.svelte +1 -1
- package/dist/Plot/Axis.svelte +21 -10
- package/dist/Plot/Grid.svelte +32 -13
- package/dist/Plot/Grid.svelte.d.ts +6 -1
- package/dist/Plot/Root.svelte +64 -46
- package/dist/Plot/Root.svelte.d.ts +27 -2
- package/dist/Plot/Timeline.svelte +39 -13
- package/dist/Plot/index.d.ts +4 -4
- package/dist/Plot/index.js +4 -4
- package/dist/Plot.svelte +128 -164
- package/dist/Plot.svelte.d.ts +18 -2
- package/dist/PlotState.svelte.d.ts +23 -0
- package/dist/PlotState.svelte.js +155 -16
- package/dist/PlotSurface.svelte +162 -0
- package/dist/PlotSurface.svelte.d.ts +25 -0
- package/dist/charts/AreaChart.svelte +37 -4
- package/dist/charts/AreaChart.svelte.d.ts +12 -2
- package/dist/charts/BarChart.svelte +32 -3
- package/dist/charts/BarChart.svelte.d.ts +10 -1
- package/dist/charts/BoxPlot.svelte +4 -2
- package/dist/charts/BoxPlot.svelte.d.ts +1 -0
- package/dist/charts/BubbleChart.svelte +34 -3
- package/dist/charts/BubbleChart.svelte.d.ts +11 -1
- package/dist/charts/LineChart.svelte +35 -4
- package/dist/charts/LineChart.svelte.d.ts +11 -2
- package/dist/charts/PieChart.svelte +1 -1
- package/dist/charts/ScatterPlot.svelte +32 -3
- package/dist/charts/ScatterPlot.svelte.d.ts +10 -1
- package/dist/charts/ViolinPlot.svelte +4 -2
- package/dist/charts/ViolinPlot.svelte.d.ts +1 -0
- package/dist/examples/BarChartExample.svelte +1 -1
- package/dist/geoms/Arc.svelte +3 -2
- package/dist/geoms/Area.svelte +20 -3
- package/dist/geoms/Bar.svelte +57 -13
- package/dist/geoms/Box.svelte +131 -76
- package/dist/geoms/Box.svelte.d.ts +7 -0
- package/dist/geoms/Candlestick.svelte +17 -11
- package/dist/geoms/Highlight.svelte +105 -0
- package/dist/geoms/Highlight.svelte.d.ts +10 -0
- package/dist/geoms/Jitter.svelte +82 -0
- package/dist/geoms/Jitter.svelte.d.ts +15 -0
- package/dist/geoms/Line.svelte +26 -9
- package/dist/geoms/Point.svelte +37 -11
- package/dist/geoms/Rule.svelte +113 -0
- package/dist/geoms/Rule.svelte.d.ts +16 -0
- package/dist/geoms/Trend.svelte +66 -0
- package/dist/geoms/Trend.svelte.d.ts +12 -0
- package/dist/geoms/Violin.svelte +30 -6
- package/dist/geoms/Violin.svelte.d.ts +5 -0
- package/dist/geoms/Waterfall.svelte +16 -9
- package/dist/geoms/lib/areas.d.ts +6 -2
- package/dist/geoms/lib/areas.js +17 -11
- package/dist/geoms/lib/bars.d.ts +18 -3
- package/dist/geoms/lib/bars.js +70 -96
- package/dist/index.d.ts +30 -4
- package/dist/index.js +31 -5
- package/dist/lib/brewing/ViolinBrewer.svelte.d.ts +1 -1
- package/dist/lib/brewing/brewer.svelte.d.ts +1 -0
- package/dist/lib/brewing/marks/arcs.js +7 -1
- package/dist/lib/brewing/marks/boxes.d.ts +11 -2
- package/dist/lib/brewing/marks/boxes.js +32 -11
- package/dist/lib/brewing/marks/lines.d.ts +6 -2
- package/dist/lib/brewing/marks/lines.js +19 -12
- package/dist/lib/brewing/marks/swarm.d.ts +32 -0
- package/dist/lib/brewing/marks/swarm.js +125 -0
- package/dist/lib/brewing/marks/violins.d.ts +4 -20
- package/dist/lib/brewing/marks/violins.js +41 -25
- package/dist/lib/brewing/stats.d.ts +3 -1
- package/dist/lib/brewing/stats.js +28 -11
- package/dist/lib/highlight.d.ts +10 -0
- package/dist/lib/highlight.js +43 -0
- package/dist/lib/plot/scales.js +12 -7
- package/dist/lib/plot/types.d.ts +9 -1
- package/dist/lib/plot/types.js +3 -1
- package/dist/lib/scale.d.ts +10 -0
- package/dist/lib/scale.js +14 -0
- package/dist/lib/select.d.ts +22 -0
- package/dist/lib/select.js +22 -0
- package/dist/lib/trend.d.ts +27 -0
- package/dist/lib/trend.js +113 -0
- package/dist/patterns/README.md +1 -1
- package/package.json +1 -1
- package/dist/Plot/Area.svelte +0 -54
- package/dist/Plot/Area.svelte.d.ts +0 -14
- package/dist/Plot/Bar.svelte +0 -69
- package/dist/Plot/Bar.svelte.d.ts +0 -11
- package/dist/Plot/Line.svelte +0 -54
- package/dist/Plot/Line.svelte.d.ts +0 -13
- package/dist/Plot/Point.svelte +0 -58
- package/dist/Plot/Point.svelte.d.ts +0 -13
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { ascending } from 'd3-array'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Deterministic pseudo-random in [0, 1) from a non-negative integer seed.
|
|
5
|
+
* Avoids Math.random so layouts are stable across renders (repo rule).
|
|
6
|
+
*
|
|
7
|
+
* @param {number} n
|
|
8
|
+
* @returns {number}
|
|
9
|
+
*/
|
|
10
|
+
function hashUnit(n) {
|
|
11
|
+
const x = Math.sin(n + 1) * 43758.5453
|
|
12
|
+
return x - Math.floor(x)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Places one point per datum within each x-band. Two layouts:
|
|
17
|
+
* - 'jitter': deterministic horizontal offset seeded by index-in-group.
|
|
18
|
+
* - 'swarm': 1-D beeswarm — sort by y, dodge horizontally to avoid overlap.
|
|
19
|
+
*
|
|
20
|
+
* @param {Object[]} data - raw rows (stat=identity)
|
|
21
|
+
* @param {{ x?: string, y?: string, fill?: string }} channels
|
|
22
|
+
* @param {import('d3-scale').ScaleBand} xScale
|
|
23
|
+
* @param {import('d3-scale').ScaleLinear} yScale
|
|
24
|
+
* @param {Map<unknown, {fill:string, stroke:string}>} colors
|
|
25
|
+
* @param {{ method?: 'jitter'|'swarm', r?: number, side?: 'left'|'right'|'center' }} opts
|
|
26
|
+
* `side` confines points to one half of the band (for raincloud/half-plots).
|
|
27
|
+
* @returns {Array<{ cx:number, cy:number, fill:string, stroke:string, data:Record<string, unknown> }>}
|
|
28
|
+
*/
|
|
29
|
+
export function buildSwarm(data, channels, xScale, yScale, colors, opts = {}) {
|
|
30
|
+
const { x: xf, y: yf, fill: ff } = channels
|
|
31
|
+
const { method = 'jitter', r = 2, side = 'center' } = opts
|
|
32
|
+
const bw = typeof xScale.bandwidth === 'function' ? xScale.bandwidth() : 40
|
|
33
|
+
// One-sided layouts get the full half-band on their side; centred straddles ±halfBand.
|
|
34
|
+
const halfBand = (bw / 2) * 0.8 // leave a small gutter
|
|
35
|
+
|
|
36
|
+
// Group rows by x value, preserving first-seen order.
|
|
37
|
+
const groups = new Map()
|
|
38
|
+
for (const d of data) {
|
|
39
|
+
const key = d[xf]
|
|
40
|
+
if (!groups.has(key)) groups.set(key, [])
|
|
41
|
+
groups.get(key).push(d)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const result = []
|
|
45
|
+
for (const [xVal, rows] of groups) {
|
|
46
|
+
const bandStart = typeof xScale.bandwidth === 'function' ? (xScale(xVal) ?? 0) : (xScale(xVal) ?? 0) - bw / 2
|
|
47
|
+
const center = bandStart + bw / 2
|
|
48
|
+
const offsets =
|
|
49
|
+
method === 'swarm'
|
|
50
|
+
? swarmOffsets(rows, yf, yScale, r, halfBand, side)
|
|
51
|
+
: rows.map((_, i) => jitterOffset(hashUnit(i), halfBand, side))
|
|
52
|
+
|
|
53
|
+
rows.forEach((d, i) => {
|
|
54
|
+
const fillKey = ff ? d[ff] : xVal
|
|
55
|
+
const colorEntry = colors?.get(fillKey) ?? { fill: '#aaa', stroke: '#666' }
|
|
56
|
+
result.push({
|
|
57
|
+
data: d,
|
|
58
|
+
cx: center + offsets[i],
|
|
59
|
+
cy: yScale(d[yf]),
|
|
60
|
+
fill: colorEntry.fill,
|
|
61
|
+
stroke: colorEntry.stroke
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
return result
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Map a [0,1) hash to a horizontal offset, confined to the requested side.
|
|
69
|
+
// center → [-halfBand, halfBand]; right → [0, halfBand]; left → [-halfBand, 0]
|
|
70
|
+
function jitterOffset(u, halfBand, side) {
|
|
71
|
+
if (side === 'right') return u * halfBand
|
|
72
|
+
if (side === 'left') return -u * halfBand
|
|
73
|
+
return (u * 2 - 1) * halfBand
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Computes horizontal offsets for a beeswarm: sort by y, then for each point
|
|
78
|
+
* pick the smallest-magnitude offset that doesn't collide (distance >= 2r) with
|
|
79
|
+
* any already-placed point in the group. When the band is over capacity (more
|
|
80
|
+
* points than fit at 2r spacing), fall back to the least-crowded slot (max
|
|
81
|
+
* separation) so overflow points spread across the band instead of stacking at
|
|
82
|
+
* the center — a best-effort layout, not a hard non-overlap guarantee.
|
|
83
|
+
* `side` confines candidates to one half of the band. Returns offsets in INPUT order.
|
|
84
|
+
*/
|
|
85
|
+
function swarmOffsets(rows, yf, yScale, r, halfBand, side = 'center') {
|
|
86
|
+
const order = rows
|
|
87
|
+
.map((d, i) => ({ i, cy: yScale(d[yf]) }))
|
|
88
|
+
.sort((a, b) => ascending(a.cy, b.cy))
|
|
89
|
+
const placed = []
|
|
90
|
+
const offsetByIndex = new Array(rows.length).fill(0)
|
|
91
|
+
const step = r / 2
|
|
92
|
+
const kMax = Math.ceil((2 * halfBand) / step)
|
|
93
|
+
|
|
94
|
+
for (const { i, cy } of order) {
|
|
95
|
+
let chosen = 0
|
|
96
|
+
let placedOne = false
|
|
97
|
+
let best = { offset: 0, sep: -Infinity }
|
|
98
|
+
for (let k = 0; k <= kMax; k++) {
|
|
99
|
+
const candidate =
|
|
100
|
+
side === 'right'
|
|
101
|
+
? k * step
|
|
102
|
+
: side === 'left'
|
|
103
|
+
? -k * step
|
|
104
|
+
: k === 0
|
|
105
|
+
? 0
|
|
106
|
+
: (k % 2 === 1 ? 1 : -1) * Math.ceil(k / 2) * step
|
|
107
|
+
if (Math.abs(candidate) > halfBand) continue
|
|
108
|
+
const sep = placed.length
|
|
109
|
+
? Math.min(...placed.map((p) => Math.hypot(candidate - p.offset, cy - p.cy)))
|
|
110
|
+
: Infinity
|
|
111
|
+
if (sep >= 2 * r) {
|
|
112
|
+
chosen = candidate
|
|
113
|
+
placedOne = true
|
|
114
|
+
break
|
|
115
|
+
}
|
|
116
|
+
if (sep > best.sep) best = { offset: candidate, sep }
|
|
117
|
+
}
|
|
118
|
+
// Band over capacity: fall back to the least-crowded slot (max separation)
|
|
119
|
+
// rather than stacking every overflow point at the center.
|
|
120
|
+
if (!placedOne) chosen = best.offset
|
|
121
|
+
placed.push({ offset: chosen, cy })
|
|
122
|
+
offsetByIndex[i] = chosen
|
|
123
|
+
}
|
|
124
|
+
return offsetByIndex
|
|
125
|
+
}
|
|
@@ -1,20 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* When `fill` differs from `x`, violins are sub-grouped within each x-band
|
|
6
|
-
* (one narrower violin per fill value per x category, like grouped bars).
|
|
7
|
-
* Violin body uses the lighter fill shade; outline uses the darker stroke shade.
|
|
8
|
-
*
|
|
9
|
-
* @param {Object[]} data
|
|
10
|
-
* @param {{ x?: string, fill?: string }} channels
|
|
11
|
-
* `fill` drives violin interior and outline (defaults to x-field).
|
|
12
|
-
* @param {import('d3-scale').ScaleBand} xScale
|
|
13
|
-
* @param {import('d3-scale').ScaleLinear} yScale
|
|
14
|
-
* @param {Map} colors
|
|
15
|
-
* @returns {Array}
|
|
16
|
-
*/
|
|
17
|
-
export function buildViolins(data: Object[], channels: {
|
|
18
|
-
x?: string;
|
|
19
|
-
fill?: string;
|
|
20
|
-
}, xScale: import("d3-scale").ScaleBand<any>, yScale: import("d3-scale").ScaleLinear<any, any, never>, colors: Map<any, any>): any[];
|
|
1
|
+
export function buildViolins(data: any, channels: any, xScale: any, yScale: any, colors: any, place: (x: any, y: any) => {
|
|
2
|
+
x: any;
|
|
3
|
+
y: any;
|
|
4
|
+
}, side: string | undefined, patterns: any): any;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { line, curveCatmullRom } from 'd3-shape'
|
|
2
|
+
import { toPatternId } from '../patterns.js'
|
|
2
3
|
|
|
3
4
|
// Relative widths at each stat anchor (fraction of max half-width)
|
|
4
5
|
const DENSITY_AT = { iqr_min: 0.08, q1: 0.55, median: 1.0, q3: 0.55, iqr_max: 0.08 }
|
|
@@ -13,17 +14,48 @@ const ANCHOR_ORDER = ['iqr_max', 'q3', 'median', 'q1', 'iqr_min']
|
|
|
13
14
|
* Violin body uses the lighter fill shade; outline uses the darker stroke shade.
|
|
14
15
|
*
|
|
15
16
|
* @param {Object[]} data
|
|
16
|
-
* @param {{ x?: string, fill?: string }} channels
|
|
17
|
+
* @param {{ x?: string, fill?: string, pattern?: string }} channels
|
|
17
18
|
* `fill` drives violin interior and outline (defaults to x-field).
|
|
18
19
|
* @param {import('d3-scale').ScaleBand} xScale
|
|
19
20
|
* @param {import('d3-scale').ScaleLinear} yScale
|
|
20
21
|
* @param {Map} colors
|
|
21
22
|
* @returns {Array}
|
|
22
23
|
*/
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
// Build the silhouette points for one violin. `side` selects half vs full:
|
|
25
|
+
// 'right' → density bulges right of the centre line, flat edge at cx
|
|
26
|
+
// 'left' → density bulges left, flat edge at cx
|
|
27
|
+
// 'center' → symmetric (both sides) — the default
|
|
28
|
+
function violinPoints(cx, halfMax, d, yScale, side) {
|
|
29
|
+
const right = ANCHOR_ORDER.map((key) => ({ x: cx + halfMax * DENSITY_AT[key], y: yScale(d[key]) }))
|
|
30
|
+
const left = [...ANCHOR_ORDER].reverse().map((key) => ({
|
|
31
|
+
x: cx - halfMax * DENSITY_AT[key],
|
|
32
|
+
y: yScale(d[key])
|
|
33
|
+
}))
|
|
34
|
+
const topY = yScale(d.iqr_max)
|
|
35
|
+
const botY = yScale(d.iqr_min)
|
|
36
|
+
if (side === 'right') return [...right, { x: cx, y: botY }, { x: cx, y: topY }]
|
|
37
|
+
if (side === 'left') return [...left, { x: cx, y: topY }, { x: cx, y: botY }]
|
|
38
|
+
return [...right, ...left, right[0]]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function buildViolins(
|
|
42
|
+
data,
|
|
43
|
+
channels,
|
|
44
|
+
xScale,
|
|
45
|
+
yScale,
|
|
46
|
+
colors,
|
|
47
|
+
place = (x, y) => ({ x, y }),
|
|
48
|
+
side = 'center',
|
|
49
|
+
patterns
|
|
50
|
+
) {
|
|
51
|
+
const { x: xf, fill: ff, pattern: pf } = channels
|
|
25
52
|
const bw = typeof xScale.bandwidth === 'function' ? xScale.bandwidth() : 40
|
|
26
53
|
const grouped = ff && ff !== xf
|
|
54
|
+
// Texture fill: patternId per violin from the pattern channel value (null = solid fill).
|
|
55
|
+
const patternIdFor = (d) => {
|
|
56
|
+
const key = pf ? d[pf] : null
|
|
57
|
+
return key !== null && key !== undefined && patterns?.has(key) ? toPatternId(String(key)) : null
|
|
58
|
+
}
|
|
27
59
|
|
|
28
60
|
const pathGen = line()
|
|
29
61
|
.x((pt) => pt.x)
|
|
@@ -43,21 +75,13 @@ export function buildViolins(data, channels, xScale, yScale, colors) {
|
|
|
43
75
|
const cx = bandStart + subIndex * subBandWidth + subBandWidth / 2
|
|
44
76
|
const colorEntry = colors?.get(fillVal) ?? { fill: '#aaa', stroke: '#666' }
|
|
45
77
|
|
|
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
78
|
return {
|
|
56
79
|
data: d,
|
|
57
80
|
cx,
|
|
58
|
-
d: pathGen(
|
|
81
|
+
d: pathGen(violinPoints(cx, halfMax, d, yScale, side).map((pt) => place(pt.x, pt.y))),
|
|
59
82
|
fill: colorEntry.fill,
|
|
60
|
-
stroke: colorEntry.stroke
|
|
83
|
+
stroke: colorEntry.stroke,
|
|
84
|
+
patternId: patternIdFor(d)
|
|
61
85
|
}
|
|
62
86
|
})
|
|
63
87
|
}
|
|
@@ -70,21 +94,13 @@ export function buildViolins(data, channels, xScale, yScale, colors) {
|
|
|
70
94
|
const colorEntry = colors?.get(fillKey) ?? { fill: '#aaa', stroke: '#666' }
|
|
71
95
|
const cx = (xScale(d[xf]) ?? 0) + (typeof xScale.bandwidth === 'function' ? bw / 2 : 0)
|
|
72
96
|
|
|
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
97
|
return {
|
|
83
98
|
data: d,
|
|
84
99
|
cx,
|
|
85
|
-
d: pathGen(
|
|
100
|
+
d: pathGen(violinPoints(cx, halfMax, d, yScale, side).map((pt) => place(pt.x, pt.y))),
|
|
86
101
|
fill: colorEntry.fill,
|
|
87
|
-
stroke: colorEntry.stroke
|
|
102
|
+
stroke: colorEntry.stroke,
|
|
103
|
+
patternId: patternIdFor(d)
|
|
88
104
|
}
|
|
89
105
|
})
|
|
90
106
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Computes box plot quartile statistics grouped by x (and optionally color).
|
|
3
|
-
* Output rows have { q1, median, q3, iqr_min, iqr_max } replacing the raw y values.
|
|
3
|
+
* Output rows have { q1, median, q3, iqr_min, iqr_max, outliers } replacing the raw y values.
|
|
4
|
+
* iqr_min/iqr_max are Tukey-clamped whisker endpoints (most extreme datum within
|
|
5
|
+
* the 1.5·IQR fence); outliers holds values outside the fence.
|
|
4
6
|
*
|
|
5
7
|
* @param {Object[]} data
|
|
6
8
|
* @param {{ x?: string, y?: string, color?: string }} channels
|
|
@@ -5,6 +5,28 @@ function sortedQuantile(values, p) {
|
|
|
5
5
|
return quantile([...values].sort(ascending), p)
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Tukey whisker bounds for a group of numeric values.
|
|
10
|
+
* Whiskers clamp to the most extreme datum within the 1.5·IQR fence;
|
|
11
|
+
* values outside the fence are outliers.
|
|
12
|
+
*
|
|
13
|
+
* @param {number[]} values
|
|
14
|
+
* @returns {{ min: number, max: number, outliers: number[] }}
|
|
15
|
+
*/
|
|
16
|
+
function whiskerBounds(values) {
|
|
17
|
+
const q1 = sortedQuantile(values, 0.25)
|
|
18
|
+
const q3 = sortedQuantile(values, 0.75)
|
|
19
|
+
const iqr = q3 - q1
|
|
20
|
+
const lo = q1 - 1.5 * iqr
|
|
21
|
+
const hi = q3 + 1.5 * iqr
|
|
22
|
+
const within = values.filter((v) => v >= lo && v <= hi)
|
|
23
|
+
return {
|
|
24
|
+
min: within.length ? min(within) : q1,
|
|
25
|
+
max: within.length ? max(within) : q3,
|
|
26
|
+
outliers: values.filter((v) => v < lo || v > hi)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
8
30
|
/**
|
|
9
31
|
* Built-in reduction functions. Each receives an array of numeric values.
|
|
10
32
|
* @type {Record<string, (values: number[]) => number>}
|
|
@@ -19,7 +41,9 @@ export const STAT_FNS = {
|
|
|
19
41
|
|
|
20
42
|
/**
|
|
21
43
|
* 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.
|
|
44
|
+
* Output rows have { q1, median, q3, iqr_min, iqr_max, outliers } replacing the raw y values.
|
|
45
|
+
* iqr_min/iqr_max are Tukey-clamped whisker endpoints (most extreme datum within
|
|
46
|
+
* the 1.5·IQR fence); outliers holds values outside the fence.
|
|
23
47
|
*
|
|
24
48
|
* @param {Object[]} data
|
|
25
49
|
* @param {{ x?: string, y?: string, color?: string }} channels
|
|
@@ -35,16 +59,9 @@ export function applyBoxStat(data, channels) {
|
|
|
35
59
|
q1: (v) => sortedQuantile(v, 0.25),
|
|
36
60
|
median: (v) => sortedQuantile(v, 0.5),
|
|
37
61
|
q3: (v) => sortedQuantile(v, 0.75),
|
|
38
|
-
iqr_min: (v) =>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return q1 - 1.5 * (q3 - q1)
|
|
42
|
-
},
|
|
43
|
-
iqr_max: (v) => {
|
|
44
|
-
const q1 = sortedQuantile(v, 0.25)
|
|
45
|
-
const q3 = sortedQuantile(v, 0.75)
|
|
46
|
-
return q3 + 1.5 * (q3 - q1)
|
|
47
|
-
}
|
|
62
|
+
iqr_min: (v) => whiskerBounds(v).min,
|
|
63
|
+
iqr_max: (v) => whiskerBounds(v).max,
|
|
64
|
+
outliers: (v) => whiskerBounds(v).outliers
|
|
48
65
|
})
|
|
49
66
|
.rollup()
|
|
50
67
|
.select()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a highlight selector to zero or more row indices.
|
|
3
|
+
* @param {Array<Record<string, unknown>>} rows
|
|
4
|
+
* @param {'first'|'last'|'min'|'max'|number|((row:any,i:number)=>boolean)} selector
|
|
5
|
+
* @param {{ y?: string }} [opts]
|
|
6
|
+
* @returns {number[]}
|
|
7
|
+
*/
|
|
8
|
+
export function resolveHighlight(rows: Array<Record<string, unknown>>, selector: "first" | "last" | "min" | "max" | number | ((row: any, i: number) => boolean), opts?: {
|
|
9
|
+
y?: string;
|
|
10
|
+
}): number[];
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a highlight selector to zero or more row indices.
|
|
3
|
+
* @param {Array<Record<string, unknown>>} rows
|
|
4
|
+
* @param {'first'|'last'|'min'|'max'|number|((row:any,i:number)=>boolean)} selector
|
|
5
|
+
* @param {{ y?: string }} [opts]
|
|
6
|
+
* @returns {number[]}
|
|
7
|
+
*/
|
|
8
|
+
export function resolveHighlight(rows, selector, opts = {}) {
|
|
9
|
+
if (!Array.isArray(rows) || rows.length === 0 || selector === null || selector === undefined)
|
|
10
|
+
return []
|
|
11
|
+
const { y } = opts
|
|
12
|
+
|
|
13
|
+
if (typeof selector === 'function') {
|
|
14
|
+
const out = []
|
|
15
|
+
for (let i = 0; i < rows.length; i++) if (selector(rows[i], i)) out.push(i)
|
|
16
|
+
return out
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (typeof selector === 'number') {
|
|
20
|
+
const i = selector < 0 ? rows.length + selector : selector
|
|
21
|
+
return i >= 0 && i < rows.length ? [i] : []
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (selector === 'first') return [0]
|
|
25
|
+
if (selector === 'last') return [rows.length - 1]
|
|
26
|
+
|
|
27
|
+
if (selector === 'min' || selector === 'max') {
|
|
28
|
+
if (!y) return []
|
|
29
|
+
let best = -1
|
|
30
|
+
let bestVal = null
|
|
31
|
+
for (let i = 0; i < rows.length; i++) {
|
|
32
|
+
const v = Number(rows[i][y])
|
|
33
|
+
if (Number.isNaN(v)) continue
|
|
34
|
+
if (bestVal === null || (selector === 'min' ? v < bestVal : v > bestVal)) {
|
|
35
|
+
bestVal = v
|
|
36
|
+
best = i
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return best === -1 ? [] : [best]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return []
|
|
43
|
+
}
|
package/dist/lib/plot/scales.js
CHANGED
|
@@ -17,6 +17,7 @@ export function inferOrientation(xType, yType) {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export function buildUnifiedXScale(datasets, field, width, opts = {}) {
|
|
20
|
+
const range = opts.range ?? [0, width]
|
|
20
21
|
const allValues = datasets.flatMap((d) => d.map((r) => r[field]))
|
|
21
22
|
const isNumeric = allValues.every(
|
|
22
23
|
(v) => typeof v === 'number' || (!isNaN(Number(v)) && String(v).trim() !== '')
|
|
@@ -26,11 +27,14 @@ export function buildUnifiedXScale(datasets, field, width, opts = {}) {
|
|
|
26
27
|
if (opts.domain) {
|
|
27
28
|
const domainIsNumeric = opts.domain.every((v) => typeof v === 'number')
|
|
28
29
|
if (!opts.band && (domainIsNumeric || isNumeric)) {
|
|
29
|
-
|
|
30
|
+
const s = scaleLinear().domain(opts.domain).range(range)
|
|
31
|
+
// nice() rounds the domain to pretty numbers — wrong for a synthetic position axis
|
|
32
|
+
// (a bar-race rank) whose domain is deliberately padded (e.g. [-0.5, N-0.5]).
|
|
33
|
+
return opts.nice === false ? s : s.nice()
|
|
30
34
|
}
|
|
31
35
|
return scaleBand()
|
|
32
36
|
.domain(opts.domain)
|
|
33
|
-
.range(
|
|
37
|
+
.range(range)
|
|
34
38
|
.padding(opts.padding ?? 0.2)
|
|
35
39
|
}
|
|
36
40
|
|
|
@@ -40,20 +44,21 @@ export function buildUnifiedXScale(datasets, field, width, opts = {}) {
|
|
|
40
44
|
const domainMin = (opts.includeZero ?? false) ? 0 : (minVal ?? 0)
|
|
41
45
|
return scaleLinear()
|
|
42
46
|
.domain([domainMin, maxVal ?? 0])
|
|
43
|
-
.range(
|
|
47
|
+
.range(range)
|
|
44
48
|
.nice()
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
const domain = [...new Set(allValues)].filter((v) => v !== null && v !== undefined)
|
|
48
52
|
return scaleBand()
|
|
49
53
|
.domain(domain)
|
|
50
|
-
.range(
|
|
54
|
+
.range(range)
|
|
51
55
|
.padding(opts.padding ?? 0.2)
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
export function buildUnifiedYScale(datasets, field, height, opts = {}) {
|
|
59
|
+
const yrange = opts.range ?? [height, 0]
|
|
55
60
|
if (opts.domain) {
|
|
56
|
-
return scaleLinear().domain(opts.domain).range(
|
|
61
|
+
return scaleLinear().domain(opts.domain).range(yrange).nice()
|
|
57
62
|
}
|
|
58
63
|
const rawValues = datasets.flatMap((d) => d.map((r) => r[field])).filter((v) => v !== null && v !== undefined)
|
|
59
64
|
const isNumeric = rawValues.length > 0 && rawValues.every(
|
|
@@ -62,14 +67,14 @@ export function buildUnifiedYScale(datasets, field, height, opts = {}) {
|
|
|
62
67
|
if (!isNumeric) {
|
|
63
68
|
// Categorical y-axis (e.g. horizontal bar chart) — use scaleBand
|
|
64
69
|
const domain = [...new Set(rawValues.map(String))]
|
|
65
|
-
return scaleBand().domain(domain).range(
|
|
70
|
+
return scaleBand().domain(domain).range(yrange).padding(0.2)
|
|
66
71
|
}
|
|
67
72
|
const numericValues = rawValues.map(Number)
|
|
68
73
|
const [minVal, maxVal] = extent(numericValues)
|
|
69
74
|
const domainMin = (opts.includeZero ?? true) ? 0 : (minVal ?? 0)
|
|
70
75
|
return scaleLinear()
|
|
71
76
|
.domain([domainMin, maxVal ?? 0])
|
|
72
|
-
.range(
|
|
77
|
+
.range(yrange)
|
|
73
78
|
.nice()
|
|
74
79
|
}
|
|
75
80
|
|
package/dist/lib/plot/types.d.ts
CHANGED
|
@@ -29,7 +29,7 @@ export type PlotSpec = {
|
|
|
29
29
|
theta?: string | undefined;
|
|
30
30
|
labels?: Record<string, string> | undefined;
|
|
31
31
|
xDomain?: unknown[] | undefined;
|
|
32
|
-
yDomain?:
|
|
32
|
+
yDomain?: unknown[] | undefined;
|
|
33
33
|
xLabel?: string | undefined;
|
|
34
34
|
yLabel?: string | undefined;
|
|
35
35
|
axisOrigin?: [number, number] | undefined;
|
|
@@ -54,6 +54,14 @@ export type PlotSpec = {
|
|
|
54
54
|
title?: string | undefined;
|
|
55
55
|
summary?: string | undefined;
|
|
56
56
|
orientation?: "horizontal" | "vertical" | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* Order the category (band) axis by aggregated value — bars sorted by size (histogram-style).
|
|
59
|
+
*/
|
|
60
|
+
sort?: "asc" | "desc" | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* Keep the category (x) axis linear (a continuous position scale) instead of band-forcing it — a bar-chart race's tweened rank. Set only by AnimatedPlot; not a public authoring flag.
|
|
63
|
+
*/
|
|
64
|
+
continuousCategory?: boolean | undefined;
|
|
57
65
|
/**
|
|
58
66
|
* Stack (true) or group (false) multi-series bars. Forwarded to spec-driven bar geoms via `options.stack`. Default: false.
|
|
59
67
|
*/
|
package/dist/lib/plot/types.js
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* @property {string} [theta]
|
|
30
30
|
* @property {Record<string, string>} [labels]
|
|
31
31
|
* @property {unknown[]} [xDomain]
|
|
32
|
-
* @property {
|
|
32
|
+
* @property {unknown[]} [yDomain]
|
|
33
33
|
* @property {string} [xLabel]
|
|
34
34
|
* @property {string} [yLabel]
|
|
35
35
|
* @property {[number, number]} [axisOrigin]
|
|
@@ -46,6 +46,8 @@
|
|
|
46
46
|
* @property {string} [title]
|
|
47
47
|
* @property {string} [summary]
|
|
48
48
|
* @property {'horizontal'|'vertical'} [orientation]
|
|
49
|
+
* @property {'asc'|'desc'} [sort] Order the category (band) axis by aggregated value — bars sorted by size (histogram-style).
|
|
50
|
+
* @property {boolean} [continuousCategory] Keep the category (x) axis linear (a continuous position scale) instead of band-forcing it — a bar-chart race's tweened rank. Set only by AnimatedPlot; not a public authoring flag.
|
|
49
51
|
* @property {boolean} [stack] Stack (true) or group (false) multi-series bars. Forwarded to spec-driven bar geoms via `options.stack`. Default: false.
|
|
50
52
|
* @property {string} [preset]
|
|
51
53
|
* @property {number} [width]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map a value through a d3 scale to its plotted position, centering on band
|
|
3
|
+
* scales (adds `bandwidth/2`). Returns `NaN` when the scale yields `undefined`
|
|
4
|
+
* (value outside the domain) so overlay callers can drop the point via
|
|
5
|
+
* `Number.isFinite`.
|
|
6
|
+
* @param {(v: unknown) => number | undefined} scale
|
|
7
|
+
* @param {unknown} v
|
|
8
|
+
* @returns {number}
|
|
9
|
+
*/
|
|
10
|
+
export function scalePos(scale: (v: unknown) => number | undefined, v: unknown): number;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map a value through a d3 scale to its plotted position, centering on band
|
|
3
|
+
* scales (adds `bandwidth/2`). Returns `NaN` when the scale yields `undefined`
|
|
4
|
+
* (value outside the domain) so overlay callers can drop the point via
|
|
5
|
+
* `Number.isFinite`.
|
|
6
|
+
* @param {(v: unknown) => number | undefined} scale
|
|
7
|
+
* @param {unknown} v
|
|
8
|
+
* @returns {number}
|
|
9
|
+
*/
|
|
10
|
+
export function scalePos(scale, v) {
|
|
11
|
+
const p = scale(v)
|
|
12
|
+
if (p === undefined) return NaN
|
|
13
|
+
return typeof scale.bandwidth === 'function' ? p + scale.bandwidth() / 2 : p
|
|
14
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assemble the SelectDetail handed to a chart's onselect callback.
|
|
3
|
+
* @param {Record<string, unknown>} datum
|
|
4
|
+
* @param {number} index index within the geom's rendered data
|
|
5
|
+
* @param {{ x?: string, y?: string }} channels
|
|
6
|
+
* @param {'line'|'point'|'area'|'bar'} geom
|
|
7
|
+
* @param {unknown} series color/group value, or undefined
|
|
8
|
+
* @param {MouseEvent|KeyboardEvent} event
|
|
9
|
+
*/
|
|
10
|
+
export function buildSelectDetail(datum: Record<string, unknown>, index: number, channels: {
|
|
11
|
+
x?: string;
|
|
12
|
+
y?: string;
|
|
13
|
+
}, geom: "line" | "point" | "area" | "bar", series: unknown, event: MouseEvent | KeyboardEvent): {
|
|
14
|
+
datum: Record<string, unknown>;
|
|
15
|
+
index: number;
|
|
16
|
+
series: {} | undefined;
|
|
17
|
+
value: unknown;
|
|
18
|
+
x: unknown;
|
|
19
|
+
y: unknown;
|
|
20
|
+
geom: "bar" | "line" | "area" | "point";
|
|
21
|
+
event: MouseEvent | KeyboardEvent;
|
|
22
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assemble the SelectDetail handed to a chart's onselect callback.
|
|
3
|
+
* @param {Record<string, unknown>} datum
|
|
4
|
+
* @param {number} index index within the geom's rendered data
|
|
5
|
+
* @param {{ x?: string, y?: string }} channels
|
|
6
|
+
* @param {'line'|'point'|'area'|'bar'} geom
|
|
7
|
+
* @param {unknown} series color/group value, or undefined
|
|
8
|
+
* @param {MouseEvent|KeyboardEvent} event
|
|
9
|
+
*/
|
|
10
|
+
export function buildSelectDetail(datum, index, channels, geom, series, event) {
|
|
11
|
+
const { x, y } = channels
|
|
12
|
+
return {
|
|
13
|
+
datum,
|
|
14
|
+
index,
|
|
15
|
+
series: series ?? undefined,
|
|
16
|
+
value: y ? datum[y] : undefined,
|
|
17
|
+
x: x ? datum[x] : undefined,
|
|
18
|
+
y: y ? datum[y] : undefined,
|
|
19
|
+
geom,
|
|
20
|
+
event
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function mean(ys: any): number | null;
|
|
2
|
+
export function median(ys: any): any;
|
|
3
|
+
/** Least-squares line. Returns { m, b } or null (< 2 points or zero x-variance). */
|
|
4
|
+
export function linearRegression(xs: any, ys: any): {
|
|
5
|
+
m: number;
|
|
6
|
+
b: number;
|
|
7
|
+
} | null;
|
|
8
|
+
/** Trailing simple moving average; length preserved (partial window at the head). */
|
|
9
|
+
export function movingAverage(ys: any, window: any): number[];
|
|
10
|
+
/** Exponential moving average; alpha = alpha ?? 2/(span+1). Length preserved. */
|
|
11
|
+
export function ema(ys: any, { span, alpha }?: {}): any[];
|
|
12
|
+
/** Fit y = a·e^(bx) via log-linear least squares. Null if any y ≤ 0 or < 2 points. */
|
|
13
|
+
export function expRegression(xs: any, ys: any): {
|
|
14
|
+
a: number;
|
|
15
|
+
b: number;
|
|
16
|
+
} | null;
|
|
17
|
+
/**
|
|
18
|
+
* Compute a trend for `rows` on channels {x, y}.
|
|
19
|
+
* @returns {{kind:'constant', value:number} | {kind:'series', values:number[]} | null}
|
|
20
|
+
*/
|
|
21
|
+
export function computeTrend(rows: any, channels: any, method: any): {
|
|
22
|
+
kind: "constant";
|
|
23
|
+
value: number;
|
|
24
|
+
} | {
|
|
25
|
+
kind: "series";
|
|
26
|
+
values: number[];
|
|
27
|
+
} | null;
|