@rokkit/chart 1.3.7 → 1.3.9
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 +51 -65
- package/dist/FacetPlot.svelte +1 -1
- package/dist/Plot/Axis.svelte +19 -10
- package/dist/Plot/Root.svelte +67 -46
- package/dist/Plot/Root.svelte.d.ts +29 -2
- package/dist/Plot/Timeline.svelte +39 -13
- package/dist/Plot/index.d.ts +5 -5
- package/dist/Plot/index.js +5 -5
- package/dist/Plot.svelte +95 -178
- package/dist/Plot.svelte.d.ts +4 -0
- package/dist/PlotState.svelte.d.ts +29 -1
- package/dist/PlotState.svelte.js +163 -19
- package/dist/PlotSurface.svelte +162 -0
- package/dist/PlotSurface.svelte.d.ts +25 -0
- package/dist/charts/AreaChart.svelte +12 -2
- package/dist/charts/AreaChart.svelte.d.ts +6 -0
- package/dist/charts/BarChart.svelte +40 -4
- package/dist/charts/BarChart.svelte.d.ts +15 -1
- package/dist/charts/BoxPlot.svelte +7 -3
- package/dist/charts/BoxPlot.svelte.d.ts +2 -0
- package/dist/charts/BubbleChart.svelte +37 -4
- package/dist/charts/BubbleChart.svelte.d.ts +12 -1
- package/dist/charts/LineChart.svelte +4 -2
- package/dist/charts/LineChart.svelte.d.ts +1 -0
- package/dist/charts/PieChart.svelte +5 -3
- package/dist/charts/PieChart.svelte.d.ts +1 -0
- package/dist/charts/ScatterPlot.svelte +35 -4
- package/dist/charts/ScatterPlot.svelte.d.ts +11 -1
- package/dist/charts/ViolinPlot.svelte +7 -3
- package/dist/charts/ViolinPlot.svelte.d.ts +2 -0
- package/dist/examples/BarChartExample.svelte +1 -1
- package/dist/geoms/Arc.svelte +21 -54
- package/dist/geoms/Arc.svelte.d.ts +4 -4
- package/dist/geoms/Area.svelte +45 -44
- package/dist/geoms/Area.svelte.d.ts +8 -1
- package/dist/geoms/Bar.svelte +33 -45
- package/dist/geoms/Bar.svelte.d.ts +9 -0
- package/dist/geoms/Box.svelte +103 -61
- package/dist/geoms/Box.svelte.d.ts +12 -3
- package/dist/geoms/Candlestick.svelte +26 -75
- package/dist/geoms/Candlestick.svelte.d.ts +2 -0
- package/dist/geoms/Heatmap.svelte +21 -66
- package/dist/geoms/Heatmap.svelte.d.ts +2 -0
- package/dist/geoms/Hexbin.svelte +20 -100
- package/dist/geoms/Hexbin.svelte.d.ts +2 -0
- package/dist/geoms/Jitter.svelte +67 -0
- package/dist/geoms/Jitter.svelte.d.ts +17 -0
- package/dist/geoms/Line.svelte +27 -31
- package/dist/geoms/Line.svelte.d.ts +5 -0
- package/dist/geoms/Point.svelte +31 -60
- package/dist/geoms/Point.svelte.d.ts +5 -1
- package/dist/geoms/Ribbon.svelte +30 -124
- package/dist/geoms/Ribbon.svelte.d.ts +2 -0
- package/dist/geoms/Rule.svelte +117 -0
- package/dist/geoms/Rule.svelte.d.ts +19 -0
- package/dist/geoms/Violin.svelte +36 -31
- package/dist/geoms/Violin.svelte.d.ts +10 -3
- package/dist/geoms/Waterfall.svelte +25 -79
- package/dist/geoms/Waterfall.svelte.d.ts +2 -0
- package/dist/geoms/lib/GeomState.svelte.d.ts +51 -0
- package/dist/geoms/lib/GeomState.svelte.js +79 -0
- package/dist/geoms/lib/aesthetics.d.ts +36 -0
- package/dist/geoms/lib/aesthetics.js +45 -0
- package/dist/geoms/lib/areas.d.ts +6 -2
- package/dist/geoms/lib/areas.js +20 -12
- package/dist/geoms/lib/bars.d.ts +18 -3
- package/dist/geoms/lib/bars.js +80 -101
- package/dist/geoms/lib/marks/arc.d.ts +24 -0
- package/dist/geoms/lib/marks/arc.js +28 -0
- package/dist/geoms/lib/marks/area.d.ts +23 -0
- package/dist/geoms/lib/marks/area.js +38 -0
- package/dist/geoms/lib/marks/bar.d.ts +16 -0
- package/dist/geoms/lib/marks/bar.js +43 -0
- package/dist/geoms/lib/marks/box.d.ts +14 -0
- package/dist/geoms/lib/marks/box.js +30 -0
- package/dist/geoms/lib/marks/candlestick.d.ts +27 -0
- package/dist/geoms/lib/marks/candlestick.js +52 -0
- package/dist/geoms/lib/marks/heatmap.d.ts +24 -0
- package/dist/geoms/lib/marks/heatmap.js +41 -0
- package/dist/geoms/lib/marks/hexbin.d.ts +29 -0
- package/dist/geoms/lib/marks/hexbin.js +62 -0
- package/dist/geoms/lib/marks/jitter.d.ts +21 -0
- package/dist/geoms/lib/marks/jitter.js +36 -0
- package/dist/geoms/lib/marks/line.d.ts +26 -0
- package/dist/geoms/lib/marks/line.js +25 -0
- package/dist/geoms/lib/marks/point.d.ts +25 -0
- package/dist/geoms/lib/marks/point.js +58 -0
- package/dist/geoms/lib/marks/ribbon.d.ts +31 -0
- package/dist/geoms/lib/marks/ribbon.js +77 -0
- package/dist/geoms/lib/marks/violin.d.ts +13 -0
- package/dist/geoms/lib/marks/violin.js +31 -0
- package/dist/geoms/lib/marks/waterfall.d.ts +25 -0
- package/dist/geoms/lib/marks/waterfall.js +58 -0
- package/dist/index.d.ts +25 -12
- package/dist/index.js +26 -13
- 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 +4 -1
- package/dist/lib/brewing/marks/lines.js +18 -15
- 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/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/preset.d.ts +11 -1
- package/dist/lib/preset.js +16 -4
- package/package.json +1 -1
- package/dist/Plot/Arc.svelte +0 -30
- package/dist/Plot/Arc.svelte.d.ts +0 -18
- 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
- package/dist/lib/brewer.d.ts +0 -9
- package/dist/lib/brewer.js +0 -25
- package/dist/lib/brewing/BoxBrewer.svelte.d.ts +0 -8
- package/dist/lib/brewing/BoxBrewer.svelte.js +0 -14
- package/dist/lib/brewing/CartesianBrewer.svelte.d.ts +0 -8
- package/dist/lib/brewing/CartesianBrewer.svelte.js +0 -21
- package/dist/lib/brewing/PieBrewer.svelte.d.ts +0 -8
- package/dist/lib/brewing/PieBrewer.svelte.js +0 -14
- package/dist/lib/brewing/QuartileBrewer.svelte.d.ts +0 -9
- package/dist/lib/brewing/QuartileBrewer.svelte.js +0 -51
- package/dist/lib/brewing/ViolinBrewer.svelte.d.ts +0 -8
- package/dist/lib/brewing/ViolinBrewer.svelte.js +0 -14
- package/dist/lib/brewing/axes.svelte.d.ts +0 -66
- package/dist/lib/brewing/axes.svelte.js +0 -270
- package/dist/lib/brewing/bars.svelte.d.ts +0 -56
- package/dist/lib/brewing/bars.svelte.js +0 -201
- package/dist/lib/brewing/brewer.svelte.d.ts +0 -124
- package/dist/lib/brewing/brewer.svelte.js +0 -277
- package/dist/lib/brewing/dimensions.svelte.d.ts +0 -35
- package/dist/lib/brewing/dimensions.svelte.js +0 -56
- package/dist/lib/brewing/index.svelte.d.ts +0 -118
- package/dist/lib/brewing/index.svelte.js +0 -205
- package/dist/lib/brewing/legends.svelte.d.ts +0 -48
- package/dist/lib/brewing/legends.svelte.js +0 -137
- package/dist/lib/brewing/marks/areas.d.ts +0 -31
- package/dist/lib/brewing/marks/areas.js +0 -72
- package/dist/lib/brewing/marks/bars.d.ts +0 -1
- package/dist/lib/brewing/marks/bars.js +0 -49
- package/dist/lib/brewing/scales.svelte.d.ts +0 -24
- package/dist/lib/brewing/scales.svelte.js +0 -82
- package/dist/lib/context.d.ts +0 -13
- package/dist/lib/context.js +0 -131
- package/dist/lib/plot/chartProps.d.ts +0 -177
- package/dist/lib/plot/chartProps.js +0 -76
- package/dist/lib/scales.svelte.d.ts +0 -35
- package/dist/lib/scales.svelte.js +0 -151
- package/dist/lib/ticks.d.ts +0 -36
- package/dist/lib/ticks.js +0 -46
|
@@ -8,17 +8,26 @@
|
|
|
8
8
|
* Box body uses the lighter fill shade; whiskers and median use the darker stroke shade.
|
|
9
9
|
*
|
|
10
10
|
* @param {Object[]} data - Pre-aggregated rows with quartile fields
|
|
11
|
-
* @param {{ x?: string, fill?: string }} channels
|
|
11
|
+
* @param {{ x?: string, fill?: string, pattern?: string }} channels
|
|
12
12
|
* `fill` drives the box and whisker color (defaults to x-field).
|
|
13
13
|
* @param {import('d3-scale').ScaleBand} xScale
|
|
14
14
|
* @param {import('d3-scale').ScaleLinear} yScale
|
|
15
15
|
* @param {Map<unknown, {fill:string, stroke:string}>} colors
|
|
16
|
+
* @param {{ side?: 'left'|'right'|'center', width?: number, patterns?: Map<unknown, string> }} [opts]
|
|
17
|
+
* `side` offsets the box to one half of the band; `width` is the box thickness as a
|
|
18
|
+
* fraction of the (sub-)band (for a thin raincloud box); `patterns` maps a pattern-channel
|
|
19
|
+
* value to a pattern name for texture fills. Defaults preserve the full box.
|
|
16
20
|
* @returns {Array}
|
|
17
21
|
*/
|
|
18
22
|
export function buildBoxes(data: Object[], channels: {
|
|
19
23
|
x?: string;
|
|
20
24
|
fill?: string;
|
|
25
|
+
pattern?: string;
|
|
21
26
|
}, xScale: import("d3-scale").ScaleBand<any>, yScale: import("d3-scale").ScaleLinear<any, any, never>, colors: Map<unknown, {
|
|
22
27
|
fill: string;
|
|
23
28
|
stroke: string;
|
|
24
|
-
}
|
|
29
|
+
}>, opts?: {
|
|
30
|
+
side?: "left" | "right" | "center";
|
|
31
|
+
width?: number;
|
|
32
|
+
patterns?: Map<unknown, string>;
|
|
33
|
+
}): any[];
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { toPatternId } from '../patterns.js'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Builds box geometry for box plot charts.
|
|
3
5
|
* Input data rows must already contain { q1, median, q3, iqr_min, iqr_max } —
|
|
@@ -8,30 +10,42 @@
|
|
|
8
10
|
* Box body uses the lighter fill shade; whiskers and median use the darker stroke shade.
|
|
9
11
|
*
|
|
10
12
|
* @param {Object[]} data - Pre-aggregated rows with quartile fields
|
|
11
|
-
* @param {{ x?: string, fill?: string }} channels
|
|
13
|
+
* @param {{ x?: string, fill?: string, pattern?: string }} channels
|
|
12
14
|
* `fill` drives the box and whisker color (defaults to x-field).
|
|
13
15
|
* @param {import('d3-scale').ScaleBand} xScale
|
|
14
16
|
* @param {import('d3-scale').ScaleLinear} yScale
|
|
15
17
|
* @param {Map<unknown, {fill:string, stroke:string}>} colors
|
|
18
|
+
* @param {{ side?: 'left'|'right'|'center', width?: number, patterns?: Map<unknown, string> }} [opts]
|
|
19
|
+
* `side` offsets the box to one half of the band; `width` is the box thickness as a
|
|
20
|
+
* fraction of the (sub-)band (for a thin raincloud box); `patterns` maps a pattern-channel
|
|
21
|
+
* value to a pattern name for texture fills. Defaults preserve the full box.
|
|
16
22
|
* @returns {Array}
|
|
17
23
|
*/
|
|
18
|
-
export function buildBoxes(data, channels, xScale, yScale, colors) {
|
|
19
|
-
const { x: xf, fill: ff } = channels
|
|
24
|
+
export function buildBoxes(data, channels, xScale, yScale, colors, opts = {}) {
|
|
25
|
+
const { x: xf, fill: ff, pattern: pf } = channels
|
|
26
|
+
const { side = 'center', width, patterns } = opts
|
|
20
27
|
const bw = typeof xScale.bandwidth === 'function' ? xScale.bandwidth() : 20
|
|
21
28
|
const grouped = ff && ff !== xf
|
|
29
|
+
// Offset the box centre to the middle of the chosen half-lane.
|
|
30
|
+
const laneOffset = (unit) => (side === 'left' ? -unit / 4 : side === 'right' ? unit / 4 : 0)
|
|
31
|
+
// Texture fill: patternId per box from the pattern channel value (null = solid fill).
|
|
32
|
+
const patternIdFor = (d) => {
|
|
33
|
+
const key = pf ? d[pf] : null
|
|
34
|
+
return key !== null && key !== undefined && patterns?.has(key) ? toPatternId(String(key)) : null
|
|
35
|
+
}
|
|
22
36
|
|
|
23
37
|
if (grouped) {
|
|
24
38
|
const fillValues = [...new Set(data.map((d) => d[ff]))]
|
|
25
39
|
const n = fillValues.length
|
|
26
40
|
const subBandWidth = bw / n
|
|
27
|
-
const boxWidth = subBandWidth * 0.75
|
|
28
|
-
const whiskerWidth = subBandWidth * 0.4
|
|
41
|
+
const boxWidth = width !== undefined ? subBandWidth * width : subBandWidth * 0.75
|
|
42
|
+
const whiskerWidth = width !== undefined ? boxWidth * 0.53 : subBandWidth * 0.4
|
|
29
43
|
|
|
30
44
|
return data.map((d) => {
|
|
31
45
|
const fillVal = d[ff]
|
|
32
46
|
const subIndex = fillValues.indexOf(fillVal)
|
|
33
47
|
const bandStart = xScale(d[xf]) ?? 0
|
|
34
|
-
const cx = bandStart + subIndex * subBandWidth + subBandWidth / 2
|
|
48
|
+
const cx = bandStart + subIndex * subBandWidth + subBandWidth / 2 + laneOffset(subBandWidth)
|
|
35
49
|
const colorEntry = colors?.get(fillVal) ?? { fill: '#aaa', stroke: '#666' }
|
|
36
50
|
|
|
37
51
|
return {
|
|
@@ -42,22 +56,27 @@ export function buildBoxes(data, channels, xScale, yScale, colors) {
|
|
|
42
56
|
q3: yScale(d.q3),
|
|
43
57
|
iqr_min: yScale(d.iqr_min),
|
|
44
58
|
iqr_max: yScale(d.iqr_max),
|
|
59
|
+
outliers: (d.outliers ?? []).map((v) => ({ cy: yScale(v), value: v })),
|
|
45
60
|
width: boxWidth,
|
|
46
61
|
whiskerWidth,
|
|
47
62
|
fill: colorEntry.fill,
|
|
48
|
-
stroke: colorEntry.stroke
|
|
63
|
+
stroke: colorEntry.stroke,
|
|
64
|
+
patternId: patternIdFor(d)
|
|
49
65
|
}
|
|
50
66
|
})
|
|
51
67
|
}
|
|
52
68
|
|
|
53
69
|
// Non-grouped: one box per x category
|
|
54
|
-
const boxWidth = bw * 0.6
|
|
55
|
-
const whiskerWidth = bw * 0.3
|
|
70
|
+
const boxWidth = width !== undefined ? bw * width : bw * 0.6
|
|
71
|
+
const whiskerWidth = width !== undefined ? boxWidth * 0.5 : bw * 0.3
|
|
56
72
|
|
|
57
73
|
return data.map((d) => {
|
|
58
74
|
const fillKey = ff ? d[ff] : d[xf]
|
|
59
75
|
const colorEntry = colors?.get(fillKey) ?? { fill: '#aaa', stroke: '#666' }
|
|
60
|
-
const cx =
|
|
76
|
+
const cx =
|
|
77
|
+
(xScale(d[xf]) ?? 0) +
|
|
78
|
+
(typeof xScale.bandwidth === 'function' ? bw / 2 : 0) +
|
|
79
|
+
laneOffset(bw)
|
|
61
80
|
return {
|
|
62
81
|
data: d,
|
|
63
82
|
cx,
|
|
@@ -66,10 +85,12 @@ export function buildBoxes(data, channels, xScale, yScale, colors) {
|
|
|
66
85
|
q3: yScale(d.q3),
|
|
67
86
|
iqr_min: yScale(d.iqr_min),
|
|
68
87
|
iqr_max: yScale(d.iqr_max),
|
|
88
|
+
outliers: (d.outliers ?? []).map((v) => ({ cy: yScale(v), value: v })),
|
|
69
89
|
width: boxWidth,
|
|
70
90
|
whiskerWidth,
|
|
71
91
|
fill: colorEntry.fill,
|
|
72
|
-
stroke: colorEntry.stroke
|
|
92
|
+
stroke: colorEntry.stroke,
|
|
93
|
+
patternId: patternIdFor(d)
|
|
73
94
|
}
|
|
74
95
|
})
|
|
75
96
|
}
|
|
@@ -11,7 +11,10 @@ export function buildLines(data: Object[], channels: {
|
|
|
11
11
|
x?: string;
|
|
12
12
|
y?: string;
|
|
13
13
|
color?: string;
|
|
14
|
-
}, xScale: Function, yScale: Function, colors: Map<any, any>, curve?: "linear" | "smooth" | "step"
|
|
14
|
+
}, xScale: Function, yScale: Function, colors: Map<any, any>, curve?: "linear" | "smooth" | "step", place?: (x: any, y: any) => {
|
|
15
|
+
x: any;
|
|
16
|
+
y: any;
|
|
17
|
+
}): {
|
|
15
18
|
d: string | null;
|
|
16
19
|
fill: string;
|
|
17
20
|
stroke: string;
|
|
@@ -9,40 +9,43 @@ import { line, curveCatmullRom, curveStep } from 'd3-shape'
|
|
|
9
9
|
* @param {'linear'|'smooth'|'step'} [curve]
|
|
10
10
|
* @returns {{ d: string | null, fill: string, stroke: string, points: {x:number, y:number, data: Record<string, unknown>, i: number}[], key?: unknown }[]}
|
|
11
11
|
*/
|
|
12
|
-
export function buildLines(data, channels, xScale, yScale, colors, curve) {
|
|
12
|
+
export function buildLines(data, channels, xScale, yScale, colors, curve, place = (x, y) => ({ x, y })) {
|
|
13
13
|
const { x: xf, y: yf, color: cf } = channels
|
|
14
14
|
const xPos = (d) =>
|
|
15
15
|
typeof xScale.bandwidth === 'function' ? xScale(d[xf]) + xScale.bandwidth() / 2 : xScale(d[xf])
|
|
16
|
+
// Screen points, sorted by the x-channel position, each mapped through place() so the
|
|
17
|
+
// whole line transposes under orientation='horizontal' (identity default → unchanged).
|
|
18
|
+
const screenPoints = (rows) => {
|
|
19
|
+
const withIndices = rows.map((d, i) => ({ data: d, i }))
|
|
20
|
+
withIndices.sort((a, b) => xPos(a.data) - xPos(b.data))
|
|
21
|
+
return withIndices.map(({ data: d, i }) => {
|
|
22
|
+
const p = place(xPos(d), yScale(d[yf]))
|
|
23
|
+
return { x: p.x, y: p.y, data: d, i }
|
|
24
|
+
})
|
|
25
|
+
}
|
|
16
26
|
const makeGen = () => {
|
|
17
27
|
const gen = line()
|
|
18
|
-
.x((
|
|
19
|
-
.y((
|
|
28
|
+
.x((p) => p.x)
|
|
29
|
+
.y((p) => p.y)
|
|
20
30
|
if (curve === 'smooth') gen.curve(curveCatmullRom)
|
|
21
31
|
else if (curve === 'step') gen.curve(curveStep)
|
|
22
32
|
return gen
|
|
23
33
|
}
|
|
24
|
-
const toPoints = (rowsWithIndices) => rowsWithIndices.map(({ data: d, i }) => ({ x: xPos(d), y: yScale(d[yf]), data: d, i }))
|
|
25
|
-
|
|
26
|
-
const sortByX = (rows) => {
|
|
27
|
-
const withIndices = rows.map((d, i) => ({ data: d, i }))
|
|
28
|
-
withIndices.sort((a, b) => xPos(a.data) - xPos(b.data))
|
|
29
|
-
return withIndices
|
|
30
|
-
}
|
|
31
34
|
|
|
32
35
|
if (!cf) {
|
|
33
|
-
const
|
|
36
|
+
const pts = screenPoints(data)
|
|
34
37
|
const stroke = colors?.values().next().value?.stroke ?? '#888'
|
|
35
|
-
return [{ d: makeGen()(
|
|
38
|
+
return [{ d: makeGen()(pts), fill: 'none', stroke, points: pts }]
|
|
36
39
|
}
|
|
37
40
|
const groups = groupBy(data, cf)
|
|
38
41
|
return [...groups.entries()].map(([key, rows]) => {
|
|
39
|
-
const
|
|
42
|
+
const pts = screenPoints(rows)
|
|
40
43
|
const colorEntry = colors?.get(key) ?? { fill: 'none', stroke: '#888' }
|
|
41
44
|
return {
|
|
42
|
-
d: makeGen()(
|
|
45
|
+
d: makeGen()(pts),
|
|
43
46
|
fill: 'none',
|
|
44
47
|
stroke: colorEntry.stroke,
|
|
45
|
-
points:
|
|
48
|
+
points: pts,
|
|
46
49
|
key
|
|
47
50
|
}
|
|
48
51
|
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Places one point per datum within each x-band. Two layouts:
|
|
3
|
+
* - 'jitter': deterministic horizontal offset seeded by index-in-group.
|
|
4
|
+
* - 'swarm': 1-D beeswarm — sort by y, dodge horizontally to avoid overlap.
|
|
5
|
+
*
|
|
6
|
+
* @param {Object[]} data - raw rows (stat=identity)
|
|
7
|
+
* @param {{ x?: string, y?: string, fill?: string }} channels
|
|
8
|
+
* @param {import('d3-scale').ScaleBand} xScale
|
|
9
|
+
* @param {import('d3-scale').ScaleLinear} yScale
|
|
10
|
+
* @param {Map<unknown, {fill:string, stroke:string}>} colors
|
|
11
|
+
* @param {{ method?: 'jitter'|'swarm', r?: number, side?: 'left'|'right'|'center' }} opts
|
|
12
|
+
* `side` confines points to one half of the band (for raincloud/half-plots).
|
|
13
|
+
* @returns {Array<{ cx:number, cy:number, fill:string, stroke:string, data:Record<string, unknown> }>}
|
|
14
|
+
*/
|
|
15
|
+
export function buildSwarm(data: Object[], channels: {
|
|
16
|
+
x?: string;
|
|
17
|
+
y?: string;
|
|
18
|
+
fill?: string;
|
|
19
|
+
}, xScale: import("d3-scale").ScaleBand<any>, yScale: import("d3-scale").ScaleLinear<any, any, never>, colors: Map<unknown, {
|
|
20
|
+
fill: string;
|
|
21
|
+
stroke: string;
|
|
22
|
+
}>, opts?: {
|
|
23
|
+
method?: "jitter" | "swarm";
|
|
24
|
+
r?: number;
|
|
25
|
+
side?: "left" | "right" | "center";
|
|
26
|
+
}): Array<{
|
|
27
|
+
cx: number;
|
|
28
|
+
cy: number;
|
|
29
|
+
fill: string;
|
|
30
|
+
stroke: string;
|
|
31
|
+
data: Record<string, unknown>;
|
|
32
|
+
}>;
|
|
@@ -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()
|
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]
|