diff --git a/src/marks/box.js b/src/marks/box.js
index 7d74db6972..a0b35b320c 100644
--- a/src/marks/box.js
+++ b/src/marks/box.js
@@ -1,4 +1,4 @@
-import {max, min, quantile} from "d3";
+import {max, min, quantile, quantileSorted} from "d3";
import {marks} from "../mark.js";
import {identity} from "../options.js";
import {groupX, groupY, groupZ} from "../transforms/group.js";
@@ -7,6 +7,8 @@ import {barX, barY} from "./bar.js";
import {dot} from "./dot.js";
import {ruleX, ruleY} from "./rule.js";
import {tickX, tickY} from "./tick.js";
+import {pointerX, pointerY} from "../interactions/pointer.js";
+import {tip as tipmark} from "./tip.js";
// Returns a composite mark for producing a horizontal box plot, applying the
// necessary statistical transforms. The boxes are grouped by y, if present.
@@ -21,6 +23,7 @@ export function boxX(
strokeOpacity,
strokeWidth = 2,
sort,
+ tip,
...options
} = {}
) {
@@ -29,7 +32,8 @@ export function boxX(
ruleY(data, group({x1: loqr1, x2: hiqr2}, {x, y, stroke, strokeOpacity, ...options})),
barX(data, group({x1: "p25", x2: "p75"}, {x, y, fill, fillOpacity, ...options})),
tickX(data, group({x: "p50"}, {x, y, stroke, strokeOpacity, strokeWidth, sort, ...options})),
- dot(data, map({x: oqr}, {x, y, z: y, stroke, strokeOpacity, ...options}))
+ dot(data, map({x: oqr}, {x, y, z: y, stroke, strokeOpacity, ...options})),
+ tip && tipmark(data, pointerY(map({x: boxStats}, {x, y, z: y, ...options})))
);
}
@@ -46,6 +50,7 @@ export function boxY(
strokeOpacity,
strokeWidth = 2,
sort,
+ tip,
...options
} = {}
) {
@@ -54,7 +59,8 @@ export function boxY(
ruleX(data, group({y1: loqr1, y2: hiqr2}, {x, y, stroke, strokeOpacity, ...options})),
barY(data, group({y1: "p25", y2: "p75"}, {x, y, fill, fillOpacity, ...options})),
tickY(data, group({y: "p50"}, {x, y, stroke, strokeOpacity, strokeWidth, sort, ...options})),
- dot(data, map({y: oqr}, {x, y, z: x, stroke, strokeOpacity, ...options}))
+ dot(data, map({y: oqr}, {x, y, z: x, stroke, strokeOpacity, ...options})),
+ tip && tipmark(data, pointerX(map({y: boxStats}, {x, y, z: x, ...options})))
);
}
@@ -82,3 +88,26 @@ function quartile1(values) {
function quartile3(values) {
return quantile(values, 0.75);
}
+
+function boxStats(values) {
+ const V = Float64Array.from(
+ (function* (V) {
+ for (let v of V) if (v !== null && !isNaN((v = +v))) yield v;
+ })(values)
+ ).sort();
+ const q1 = quantileSorted(V, 0.25);
+ const mi = quantileSorted(V, 0.5);
+ const q3 = quantileSorted(V, 0.75);
+ const lo = q1 * 2.5 - q3 * 1.5;
+ const loqr1 = V.find((d) => d >= lo);
+ const hi = q3 * 2.5 - q1 * 1.5;
+ let hiqr2;
+ for (let i = V.length - 1; i >= 0; --i) {
+ if (V[i] <= hi) {
+ hiqr2 = V[i];
+ break;
+ }
+ }
+ const report = [q3, q1, mi, hiqr2, loqr1];
+ return values.map((d) => (d !== null && (d < loqr1 || d > hiqr2) ? d : report.pop() ?? NaN));
+}
diff --git a/test/output/boxplotFacetInterval.svg b/test/output/boxplotFacetInterval.svg
index 473440c09a..018369f33e 100644
--- a/test/output/boxplotFacetInterval.svg
+++ b/test/output/boxplotFacetInterval.svg
@@ -596,4 +596,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/output/boxplotY.svg b/test/output/boxplotY.svg
new file mode 100644
index 0000000000..eae07311eb
--- /dev/null
+++ b/test/output/boxplotY.svg
@@ -0,0 +1,664 @@
+
\ No newline at end of file
diff --git a/test/plots/boxplot.ts b/test/plots/boxplot.ts
index 014db9c0ea..3010213d9e 100644
--- a/test/plots/boxplot.ts
+++ b/test/plots/boxplot.ts
@@ -17,7 +17,7 @@ export async function boxplotFacetInterval() {
marks: [
Plot.boxX(
olympians.filter((d) => d.height),
- {x: "weight", fy: "height"}
+ {x: "weight", fy: "height", tip: true}
)
]
});
@@ -40,3 +40,21 @@ export async function boxplotFacetNegativeInterval() {
]
});
}
+
+export async function boxplotY() {
+ const olympians = await d3.csv("data/athletes.csv", d3.autoType);
+ return Plot.plot({
+ fx: {
+ grid: true,
+ tickFormat: String, // for debugging
+ interval: 5,
+ reverse: true
+ },
+ marks: [
+ Plot.boxY(
+ olympians.filter((d) => d.height),
+ {fx: "weight", y: "height", tip: true}
+ )
+ ]
+ });
+}