function BoxPlot(data, {
x = ([x]) => x,
y = ([, y]) => y,
width = 640,
height = 400,
marginTop = 20,
marginRight = 30,
marginBottom = 30,
marginLeft = 40,
inset = 0.5,
insetLeft = inset,
insetRight = inset,
xType = d3.scaleLinear,
xDomain,
xRange = [marginLeft, width - marginRight],
yType = d3.scaleLinear,
yDomain,
yRange = [height - marginBottom, marginTop],
thresholds = width / 40,
stroke = "currentColor",
fill = "#ddd",
jitter = 4,
xFormat,
yFormat,
xLabel,
yLabel
} = {}) {
const X = d3.map(data, x);
const Y = d3.map(data, y);
const I = d3.range(X.length).filter(i => !isNaN(X[i]) && !isNaN(Y[i]));
// Compute the bins.
const B = d3.bin()
.thresholds(thresholds)
.value(i => X[i])
(I)
.map(bin => {
const y = i => Y[i];
const min = d3.min(bin, y);
const max = d3.max(bin, y);
const q1 = d3.quantile(bin, 0.25, y);
const q2 = d3.quantile(bin, 0.50, y);
const q3 = d3.quantile(bin, 0.75, y);
const iqr = q3 - q1; // interquartile range
const r0 = Math.max(min, q1 - iqr * 1.5);
const r1 = Math.min(max, q3 + iqr * 1.5);
bin.quartiles = [q1, q2, q3];
bin.range = [r0, r1];
bin.outliers = bin.filter(i => Y[i] < r0 || Y[i] > r1);
return bin;
});
// Compute default domains.
if (xDomain === undefined) xDomain = [d3.min(B, d => d.x0), d3.max(B, d => d.x1)];
if (yDomain === undefined) yDomain = [d3.min(B, d => d.range[0]), d3.max(B, d => d.range[1])];
// Construct scales and axes.
const xScale = xType(xDomain, xRange).interpolate(d3.interpolateRound);
const yScale = yType(yDomain, yRange);
const xAxis = d3.axisBottom(xScale).ticks(thresholds, xFormat).tickSizeOuter(0);
const yAxis = d3.axisLeft(yScale).ticks(height / 40, yFormat);
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;");
svg.append("g")
.attr("transform", `translate(${marginLeft},0)`)
.call(yAxis)
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".tick line").clone()
.attr("x2", width - marginLeft - marginRight)
.attr("stroke-opacity", 0.1))
.call(g => g.append("text")
.attr("x", -marginLeft)
.attr("y", 10)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text(yLabel));
const g = svg.append("g")
.selectAll("g")
.data(B)
.join("g");
g.append("path")
.attr("stroke", stroke)
.attr("d", d => `
M${xScale((d.x0 + d.x1) / 2)},${yScale(d.range[1])}
V${yScale(d.range[0])}
`);
g.append("path")
.attr("fill", fill)
.attr("d", d => `
M${xScale(d.x0) + insetLeft},${yScale(d.quartiles[2])}
H${xScale(d.x1) - insetRight}
V${yScale(d.quartiles[0])}
H${xScale(d.x0) + insetLeft}
Z
`);
g.append("path")
.attr("stroke", stroke)
.attr("stroke-width", 2)
.attr("d", d => `
M${xScale(d.x0) + insetLeft},${yScale(d.quartiles[1])}
H${xScale(d.x1) - insetRight}
`);
g.append("g")
.attr("fill", stroke)
.attr("fill-opacity", 0.2)
.attr("stroke", "none")
.attr("transform", d => `translate(${xScale((d.x0 + d.x1) / 2)},0)`)
.selectAll("circle")
.data(d => d.outliers)
.join("circle")
.attr("r", 2)
.attr("cx", () => (Math.random() - 0.5) * jitter)
.attr("cy", i => yScale(Y[i]));
svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(xAxis)
.call(g => g.append("text")
.attr("x", width)
.attr("y", marginBottom - 4)
.attr("fill", "currentColor")
.attr("text-anchor", "end")
.text(xLabel));
return svg.node();
}