function BrushableHistogram(
data,
{
value = [],
x = (d) => d[0],
xLabel = "",
width = 600,
height = 200,
marginTop = 20,
marginBottom = 30,
vertical = false,
fill = "steelblue",
nonSelectedFill = "#eee",
handleFill = "#ddd",
handleStroke = "#aaa",
handleSize = 15,
handleInnerRadius = 5,
handleCornerRadius = 2
} = {}
) {
let element = Histogram(data, {
x,
xLabel,
width,
height,
marginTop,
marginBottom,
vertical,
fill
});
let widget = ReactiveWidget(element, { value, showValue });
// https://observablehq.com/@d3/brush-handles
const arc = d3
.arc()
.innerRadius(handleInnerRadius)
.outerRadius(handleSize)
.cornerRadius(handleCornerRadius)
.startAngle(0)
.endAngle((d, i) => (i ? Math.PI : -Math.PI));
const brushHandle = (g, selection) =>
g
.selectAll(".handle--custom")
.data([{ type: "w" }, { type: "e" }])
.join((enter) =>
enter
.append("path")
.attr("class", "handle--custom")
.attr("fill", handleFill)
.attr("fill-opacity", 0.8)
.attr("stroke", handleStroke)
.attr("stroke-width", 1.5)
.attr("cursor", "ew-resize")
.attr("d", arc)
)
.attr("display", selection === null ? "none" : null)
.attr(
"transform",
selection === null
? null
: (d, i) =>
`translate(${selection[i]},${
(height - marginTop + marginBottom) / 2
})`
);
// Enhance the Histogram with a brush
function brushended(event) {
let selection = event.selection;
if (!event.sourceEvent || !selection) return;
const [x0, x1] = selection.map((d) => element._xS.invert(d));
// Move Handles
d3.select(this).call(brushHandle, selection);
widget.setValue(vertical ? [x1, x0] : [x0, x1]);
}
const brush = (vertical ? d3.brushY() : d3.brushX())
.extent([
[element._margin.left, element._margin.top],
[
element._width - element._margin.right,
element._height - element._margin.bottom
]
])
.on("start brush end", brushended);
const gBrush = d3.select(element).append("g").call(brush);
// 🧰 ShowValue will display the current internalValue brush position
function showValue() {
// ✅ Add here the code that updates the current interaction
const [x0, x1] = widget.value;
// Update the brush position
const selection =
x1 > x0 ? (vertical ? [x1, x0] : [x0, x1]).map(element._xS) : null;
// Highlight
if (selection === null) {
widget._rects.attr("fill", nonSelectedFill);
} else {
widget._rects.attr("fill", (d) =>
x0 <= d.x1 && d.x0 <= x1 ? fill : nonSelectedFill
);
}
gBrush.call(brush.move, selection);
gBrush.call(brushHandle, selection); // update the handle as well
}
// If no value, select the whole domain
if (!value?.length) widget.value = widget._xS.domain();
showValue();
// 🧰 Finally return the html element
return widget;
}