Public
Edited
Apr 22
Importers
Insert cell
Insert cell
viewof selection = BrushableHistogram(data, {
x: (d) => d.culmen_length_mm,
value: [35, 40],
xLabel: xAttr
})
Insert cell
viewof xAttr = Inputs.select(attrs, {label: "Binning Attribute"})
Insert cell
viewof data = dataInput({value: penguins})
Insert cell
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
} = {}
) {
// ✅ Add here the code that creates your widget
let element = Histogram(data, {
x,
xLabel,
width,
height,
marginTop,
marginBottom,
vertical,
fill
});

// 🧰 Enhance your html element with reactive value and event handling
let widget = ReactiveWidget(element, { value, showValue });

// *** Handles ***
// 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;
}
Insert cell
function Histogram(
data,
{
x = (d) => d[0],
xLabel = "",
yLabel = "Frequency",
width = 600,
height = 300,
marginTop = 20,
marginRight = 20,
marginBottom = 20,
marginLeft = 40,
vertical = false,
fill = "steelblue"
} = {}
) {
// Code from https://observablehq.com/@d3/histogram/

// Bin the data.
const bins = d3.bin().thresholds(40).value(x)(data);

const xRange = [marginLeft, width - marginRight];
const yRange = [marginTop, height - marginBottom];

// Declare the x (horizontal position) scale.
const xS = d3
.scaleLinear()
.domain([bins[0].x0, bins[bins.length - 1].x1])
.range(vertical ? yRange.reverse() : xRange);

// Declare the y (vertical position) scale.
const yS = d3
.scaleLinear()
.domain([0, d3.max(bins, (d) => d.length)])
.range(vertical ? xRange : yRange.reverse());

// Create the SVG container.
const svg = d3
.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto;");

// Add a rect for each bin.
const rects = svg
.append("g")
.attr("fill", fill)
.selectAll()
.data(bins)
.join("rect")
.attr(vertical ? "y" : "x", (d) => vertical ? xS(d.x1) : xS(d.x0) + 1)
.attr(
vertical ? "height" : "width",
(d) => vertical ? (xS(d.x0) - xS(d.x1) - 1) : (xS(d.x1) - xS(d.x0) - 1)
)
.attr(vertical ? "x" : "y", (d) => (vertical ? yS(0) : yS(d.length)))
.attr(vertical ? "width" : "height", (d) =>
vertical ? yS(d.length) : yS(0) - yS(d.length)
);

// Add the x-axis and label.
svg
.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(
d3
.axisBottom(vertical ? yS : xS)
.ticks(width / 80)
.tickSizeOuter(0)
)
.call((g) =>
g
.append("text")
.attr("x", width)
.attr("y", marginBottom - 4)
.attr("fill", "currentColor")
.attr("text-anchor", "end")
.text(`${vertical ? yLabel : xLabel}→`)
);

// Add the y-axis and label, and remove the domain line.
svg
.append("g")
.attr("transform", `translate(${marginLeft},0)`)
.call(d3.axisLeft(vertical ? xS : yS).ticks(height / 40))
.call((g) => g.select(".domain").remove())
.call((g) =>
g
.append("text")
.attr("x", -marginLeft)
.attr("y", 10)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text(`↑ ${vertical ? xLabel : yLabel}`)
);

// Exposing intervals
svg.node()._margin = {
left: marginLeft,
right: marginRight,
bottom: marginBottom,
top: marginTop
};
svg.node()._height = height;
svg.node()._width = width;
svg.node()._xS = xS;
svg.node()._bins = bins;
svg.node()._rects = rects;

return svg.node();
}
Insert cell
attrs = Object.keys(data[0] ).filter(a => !isNaN(data[0][a]))
Insert cell
import {dataInput} from "@john-guerra/data-input"
Insert cell
ReactiveWidget = require("reactive-widget-helper")
Insert cell
Interval(penguins, { x: d => d[xAttr]})
Insert cell
function Interval(data, { numbersPattern = "[0-9]*.?[0-9]{0,2}" } = {}) {
const options = arguments[1];

const histogram = BrushableHistogram(data, options);

const [minX, maxX] = histogram._xS.domain();
const min = InputNumber([minX, maxX], { label: "Min: ", value: minX, step: 0.01 });
const max = InputNumber([minX, maxX], { label: "Max: ", value: maxX, step: 0.01 });

const target = htl.html`<div>${histogram}</div><div>${min}${max}</div>`;
const widget = ReactiveWidget(target, { value: histogram.value, showValue });

function showValue() {
if (widget.value) {
histogram.value = widget.value;
min.value = widget.value[0];
max.value = widget.value[1];
}
}

histogram.addEventListener("input", (evt) => {
evt.preventDefault();
widget.setValue(histogram.value);
});
min.addEventListener("input", (evt) => {
evt.preventDefault();
widget.setValue([+min.value, +histogram.value[1]]);
});
max.addEventListener("input", (evt) => {
evt.preventDefault();
widget.setValue([+histogram.value[0], +max.value]);
});
showValue();
return widget;
}
Insert cell
import {InputNumber} from "@john-guerra/input-number"
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more