Public
Edited
Apr 10
Paused
Importers
7 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function ReactiveWidgetTemplate(
data,
{
value = 0 // following observable inputs, the value option contains the intial value
} = {}
) {
// 🧰 The interval value selected by the user interaction
let intervalValue = value;

// 🧰 The HTML element that we will return
// ✅ Add here the visual representation of your widget
let target = htl.html`<output>${intervalValue}</output>
<button onClick=${() => setValue((intervalValue += 1))}>+</button>`;

// 🧰 Usually you have a function to reflect in the UI the current value
function showValue() {
// ✅ Add here the code that updates the current interaction
target.querySelector("output").value = intervalValue;
}

// 🧰 And a function to update the current internal value. This one triggers the input event
function setValue(newValue) {
intervalValue = newValue;
showValue();
target.dispatchEvent(new CustomEvent("input", { bubbles: true }));
}

// 🧰 The value attribute represents the current interaction
Object.defineProperty(target, "value", {
get() {
return intervalValue;
},
set(newValue) {
intervalValue = newValue;
showValue();
}
});

// 🧰 Listen to the input event to show the current interaction
target.addEventListener("input", showValue);

// 🧰 Finally return the html element
return target;
}
Insert cell
viewof count = ReactiveWidgetTemplate()
Insert cell
viewof count2 = Inputs.bind(ReactiveWidgetTemplate(), viewof count)
Insert cell
Insert cell
Inputs.bind(Inputs.range([0, 100], {step: 1}), viewof count)
Insert cell
Inputs.bind(html`<input type="range">`, viewof count)
Insert cell
Insert cell
viewof helperCount = CounterWidget()
Insert cell
Inputs.bind(Inputs.range([0, 100], {step: 1}), viewof helperCount)
Insert cell
function CounterWidget(
data,
{
value = 0 // following observable inputs, the value option contains the intial value
} = {}
) {
let widget;
// ✅ Add here the code that creates your widget as an HTML element
const element = d3.create("button").on("click", () => {
widget.setValue(widget.value + 1);
});

function showValue() {
console.log("Counter Widget showValue", data, widget.value);
// ✅ Add here the code that updates the current selection
element.text(`click ${widget.value}`);
}

// 🧰 Then wrap your visualization (html element) with the reactive value
// and input events
widget = ReactiveWidget(element.node(), { value, showValue });

// 🧰 Show the initial value
showValue();

// 🧰 Finally return the html element
return widget;
}
Insert cell
Insert cell
function ReactiveWidget(target, { showValue = () => {}, value } = {}) {
// 🧰 The interval value selected by the user interaction
let intervalValue = value;

// 🧰 And a function to update the current internal value. This one triggers the input event
function setValue(newValue) {
intervalValue = newValue;
target.dispatchEvent(new CustomEvent("input", { bubbles: true }));
}

// 🧰 The value attribute setter and getter
Object.defineProperty(target, "value", {
get() {
return intervalValue;
},
set(newValue) {
intervalValue = newValue;
showValue();
}
});

// 🧰 Listen to the input event, and reflec the current value
target.addEventListener("input", (evt) => {
evt.stopPropagation();
evt.preventDefault();
showValue(evt);
});

// Expose the setValue
target.setValue = setValue;

// 🧰 Finally return the html element
return target;
}
Insert cell
Insert cell
viewof selection = BrushableHistogram(cars, {
x: (d) => d[xAttr],
xLabel: xAttr, height: 200,
value: [12, 32] // initial position
})
Insert cell
Insert cell
Histogram(
cars.filter(
(d) =>
!selection.length ||
(d[xAttr] >= selection[0] && d[xAttr] <= selection[1])
),
{ x: (d) => d[xAttr], xLabel: xAttr, height: 150 }
)
Insert cell
// For sending a external event to the widget, change it's value, and trigger an input event
function resetBrush() {
viewof selection.value = [];
viewof selection.dispatchEvent(new Event("input", {bubbles: true}));
}
Insert cell
Insert cell
BrushableHistogram = function (
data,
{
value = [],
x = (d) => d[0],
xLabel = "",
width = 600,
height = 300,
marginBottom = 30,
vertical = false
} = {}
) {
// ✅ Add here the code that creates your widget
let element = Histogram(data, {
x,
xLabel,
width,
height,
marginBottom,
vertical
});

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

// 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));
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("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
gBrush.call(brush.move, x1 > x0 ? (vertical ? [x1,x0] :[x0, x1]).map(element._xS) : null);
}

showValue();

// 🧰 Finally return the html element
return widget;
}
Insert cell
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
} = {}
) {
// 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.
svg
.append("g")
.attr("fill", "steelblue")
.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;

return svg.node();
}
Insert cell
Insert cell
async function BrushableScatterplot(
data,
{
value = [], // initial selection
color = "color",
x = "x",
y = "y",
size = "size",
width = 400,
height = 400
} = {}
) {
const spec = getScatterplot(data, { data, color, x, y, size, width, height, value });
// 🧰 The HTML element that we will return
let widget = await vegaSelected(
spec,
{ signal: "brush" } // listen to the brush signal
);

// 🧰 Finally return the html element
return widget;
}
Insert cell
Insert cell
(viewof histY)._height
Insert cell
scatterBrush[xAttr]
Insert cell
Insert cell
viewof scatterBrush = BrushableScatterplot(cars, {
x: xAttr,
y: yAttr,
color: "cylinders",
size: "year",
width: 600,
value: { [xAttr]: histX, [yAttr]: histY }
})
Insert cell
viewof histX = BrushableHistogram(cars, {
x: (d) => d[xAttr],
xLabel: xAttr,
width: 450,
height: 200,
value: [15, 20]
})
Insert cell
viewof histY = BrushableHistogram(cars, {
x: (d) => d[yAttr],
xLabel: yAttr,
width: 200,
height: 450,
value: [120, 150],
vertical: true
})
Insert cell
Insert cell
{
const setHistograms = () => {
// Notice that we don't trigger the input event of the scatterplot to avoid an infinite loop
// The scatterplot is the primary widget, so it its the one that notifies to the histograms
// and not backwards
if (scatterBrush[xAttr]) viewof histX.value = scatterBrush[xAttr];
if (scatterBrush[yAttr]) viewof histY.value = scatterBrush[yAttr];
};

viewof scatterBrush.addEventListener("input", setHistograms);

invalidation.then(() =>
viewof scatterBrush.removeEventListener("input", setHistograms)
);
}
Insert cell
xAttr = "economy (mpg)"
Insert cell
yAttr = "power (hp)"
Insert cell
// Returns a vega-lite spec for a scatterplot
function getScatterplot(
data,
{
color = "color",
x = "x",
y = "y",
size = "size",
width = 500,
height = 500,
value = {}
} = {}
) {
const brush = vl.selectInterval("brush").encodings(["x", "y"]).value(value);

const colorIsNumeric = !isNaN(data[0][color]);
const colorField = colorIsNumeric
? vl.color().fieldN(color)
: vl.color().fieldQ(color);

const chart = vl
.markCircle({ tooltip: { data: true } })
.params(brush)
.data(data)
.encode(
vl.x().fieldQ(x).scale({zero: false}),
vl.y().fieldQ(y).scale({zero: false}),
vl.color().if(brush, colorField).value("#777"),
vl
.size()
.field(size)
.scale({ range: [10, 300] })
)
.height((width * 2) / 3)
.width((width * 2) / 3)
.config({
scale: { grid: null }
});

return chart.toObject();
}
Insert cell
Insert cell
Insert cell
Insert cell
function BrushableHistogramFull(
data,
{
value = [],
x = (d) => d[0],
xLabel = "",
width = 600,
height = 300,
marginBottom = 30,
} = {}
) {
// 🧰 The interval value, in this case the brush selection
let intervalValue = value;

// 🧰 For the HTML element we will create a basic Histogram and add a brush to it
let target = Histogram(data, { x, xLabel, width, height, marginBottom });

// Add the brush code
function brushended(event) {
const selection = event.selection;

if (!event.sourceEvent || !selection) return;
const [x0, x1] = selection.map((d) => target._xS.invert(d));

setValue([x0, x1]);
}
const brush = d3
.brushX()
.extent([
[target._margin.left, target._margin.top],
[
target._width - target._margin.right,
target._height - target._margin.bottom
]
])
.on("brush end", brushended);
const gBrush = d3.select(target).append("g").call(brush);

// 🧰 ShowValue will display the current internalValue brush position
function showValue() {
const [x0, x1] = intervalValue;
// Update the brush position
gBrush.call(brush.move, x1 > x0 ? [x0, x1].map(target._xS) : null);
}

// 🧰 And a function to update the current internal value. This one triggers the input event
function setValue(newValue) {
intervalValue = newValue;
showValue();
target.dispatchEvent(new CustomEvent("input", { bubbles: true }));
}

// 🧰 The value attribute setter and getter
Object.defineProperty(target, "value", {
get() {
return intervalValue;
},
set(newValue) {
intervalValue = newValue;
showValue();
}
});

showValue();

// 🧰 Listen to the input event, and reflec the current value
target.addEventListener("input", showValue);

// 🧰 Finally return the html element
return target;
}
Insert cell
import {vl} from "@vega/vega-lite-api-v5"
Insert cell
import {vegaSelected} from "@john-guerra/vega-selected"
Insert cell
cars
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