Public
Edited
Oct 17, 2024
Paused
Importers
5 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
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;
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();
}
});

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

// 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

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more