Skip to content

Commit c9062a6

Browse files
mbostockFil
andauthored
boxX, boxY (#777)
* boxX, boxY * Fil/box (#778) * default options: Plot.boxX(values) * Use the definition where the whiskers connect existing data points Co-authored-by: Philippe Rivière <[email protected]>
1 parent 80a7b01 commit c9062a6

File tree

7 files changed

+140
-44
lines changed

7 files changed

+140
-44
lines changed

src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export {plot, Mark, marks} from "./plot.js";
22
export {Area, area, areaX, areaY} from "./marks/area.js";
33
export {Arrow, arrow} from "./marks/arrow.js";
44
export {BarX, BarY, barX, barY} from "./marks/bar.js";
5+
export {boxX, boxY} from "./marks/box.js";
56
export {Cell, cell, cellX, cellY} from "./marks/cell.js";
67
export {Dot, dot, dotX, dotY} from "./marks/dot.js";
78
export {Frame, frame} from "./marks/frame.js";

src/marks/box.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import {min, max, quantile} from "d3";
2+
import {marks} from "../plot.js";
3+
import {groupX, groupY, groupZ} from "../transforms/group.js";
4+
import {map} from "../transforms/map.js";
5+
import {barX, barY} from "./bar.js";
6+
import {dot} from "./dot.js";
7+
import {ruleX, ruleY} from "./rule.js";
8+
import {tickX, tickY} from "./tick.js";
9+
10+
// Returns a composite mark for producing a horizontal box plot, applying the
11+
// necessary statistical transforms. The boxes are grouped by y, if present.
12+
export function boxX(data, {
13+
x = {transform: x => x},
14+
y = null,
15+
fill = "#ccc",
16+
fillOpacity,
17+
stroke = "currentColor",
18+
strokeOpacity,
19+
strokeWidth = 2,
20+
...options
21+
} = {}) {
22+
const group = y != null ? groupY : groupZ;
23+
return marks(
24+
ruleY(data, group({x1: loqr1, x2: hiqr2}, {x, y, stroke, strokeOpacity, ...options})),
25+
barX(data, group({x1: "p25", x2: "p75"}, {x, y, fill, fillOpacity, ...options})),
26+
tickX(data, group({x: "p50"}, {x, y, stroke, strokeOpacity, strokeWidth, ...options})),
27+
dot(data, map({x: oqr}, {x, y, z: y, stroke, strokeOpacity, ...options}))
28+
);
29+
}
30+
31+
// Returns a composite mark for producing a vertical box plot, applying the
32+
// necessary statistical transforms. The boxes are grouped by x, if present.
33+
export function boxY(data, {
34+
y = {transform: y => y},
35+
x = null,
36+
fill = "#ccc",
37+
fillOpacity,
38+
stroke = "currentColor",
39+
strokeOpacity,
40+
strokeWidth = 2,
41+
...options
42+
} = {}) {
43+
const group = x != null ? groupX : groupZ;
44+
return marks(
45+
ruleX(data, group({y1: loqr1, y2: hiqr2}, {x, y, stroke, strokeOpacity, ...options})),
46+
barY(data, group({y1: "p25", y2: "p75"}, {x, y, fill, fillOpacity, ...options})),
47+
tickY(data, group({y: "p50"}, {x, y, stroke, strokeOpacity, strokeWidth, ...options})),
48+
dot(data, map({y: oqr}, {x, y, z: x, stroke, strokeOpacity, ...options}))
49+
);
50+
}
51+
52+
// A map function that returns only outliers, returning NaN for non-outliers
53+
function oqr(values) {
54+
const r1 = loqr1(values);
55+
const r2 = hiqr2(values);
56+
return values.map(v => v < r1 || v > r2 ? v : NaN);
57+
}
58+
59+
function loqr1(values, value) {
60+
const lo = quartile1(values, value) * 2.5 - quartile3(values, value) * 1.5;
61+
return min(values, d => d >= lo ? d : NaN);
62+
}
63+
64+
function hiqr2(values, value) {
65+
const hi = quartile3(values, value) * 2.5 - quartile1(values, value) * 1.5;
66+
return max(values, d => d <= hi ? d : NaN);
67+
}
68+
69+
function quartile1(values, value) {
70+
return quantile(values, 0.25, value);
71+
}
72+
73+
function quartile3(values, value) {
74+
return quantile(values, 0.75, value);
75+
}

test/output/boxplot.svg

Lines changed: 55 additions & 0 deletions
Loading

test/output/morleyBoxplot.svg

Lines changed: 2 additions & 2 deletions
Loading

test/plots/boxplot.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import * as Plot from "@observablehq/plot";
2+
3+
export default async function() {
4+
return Plot.boxX([0, 3, 4.4, 4.5, 4.6, 5, 7]).plot();
5+
}

test/plots/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export {default as athletesWeightCumulative} from "./athletes-weight-cumulative.
2323
export {default as availability} from "./availability.js";
2424
export {default as ballotStatusRace} from "./ballot-status-race.js";
2525
export {default as beckerBarley} from "./becker-barley.js";
26+
export {default as boxplot} from "./boxplot.js";
2627
export {default as caltrain} from "./caltrain.js";
2728
export {default as carsMpg} from "./cars-mpg.js";
2829
export {default as carsParcoords} from "./cars-parcoords.js";

test/plots/morley-boxplot.js

Lines changed: 1 addition & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,46 +3,5 @@ import * as d3 from "d3";
33

44
export default async function() {
55
const morley = await d3.csv("data/morley.csv", d3.autoType);
6-
return boxX(morley, {x: "Speed", y: "Expt"}).plot({x: {grid: true, inset: 6}});
7-
}
8-
9-
// Returns a composite mark for producing a box plot, applying the necessary
10-
// statistical transforms. The boxes are grouped by y, if present.
11-
function boxX(data, {
12-
x = x => x,
13-
y = null,
14-
fill = "#ccc",
15-
stroke = "currentColor",
16-
...options
17-
} = {}) {
18-
return Plot.marks(
19-
Plot.ruleY(data, Plot.groupY({x1: iqr1, x2: iqr2}, {x, y, stroke, ...options})),
20-
Plot.barX(data, Plot.groupY({x1: "p25", x2: "p75"}, {x, y, fill, ...options})),
21-
Plot.tickX(data, Plot.groupY({x: "p50"}, {x, y, stroke, strokeWidth: 2, ...options})),
22-
Plot.dot(data, Plot.map({x: outliers}, {x, y, z: y, stroke, ...options}))
23-
);
24-
}
25-
26-
// A map function that returns only outliers, returning NaN for non-outliers
27-
// (values within 1.5× of the interquartile range).
28-
function outliers(values) {
29-
const r1 = iqr1(values);
30-
const r2 = iqr2(values);
31-
return values.map(v => v < r1 || v > r2 ? v : NaN);
32-
}
33-
34-
function iqr1(values, value) {
35-
return Math.max(d3.min(values, value), quartile1(values, value) * 2.5 - quartile3(values, value) * 1.5);
36-
}
37-
38-
function iqr2(values, value) {
39-
return Math.min(d3.max(values, value), quartile3(values, value) * 2.5 - quartile1(values, value) * 1.5);
40-
}
41-
42-
function quartile1(values, value) {
43-
return d3.quantile(values, 0.25, value);
44-
}
45-
46-
function quartile3(values, value) {
47-
return d3.quantile(values, 0.75, value);
6+
return Plot.boxX(morley, {x: "Speed", y: "Expt"}).plot({x: {grid: true, inset: 6}});
487
}

0 commit comments

Comments
 (0)