Published
Edited
May 23, 2021
Fork of Interaction
Insert cell
Insert cell
{
const svg = d3.create('svg')
.attr('width', 100)
.attr('height', 100);
svg.append('circle')
.attr('cx', 50)
.attr('cy', 50)
.attr('r', 25)
.attr('fill', 'red')
// we can pass a function directly
.on('mouseenter', function() {
// this refers to the circle
// select it and change its attributes
d3.select(this)
.attr('r', 50)
.attr('fill', 'blue');
})
// or separate the function out
.on('mouseleave', mouseLeave);
function mouseLeave() {
d3.select(this)
.attr('r', 25)
.attr('fill', 'red');
}
return svg.node();
}
Insert cell
Insert cell
Insert cell
{
// margin convention
const margin = {top: 10, right: 100, bottom: 50, left: 100};
const visWidth = 600 - margin.left - margin.right;
const visHeight = 460 - margin.top - margin.bottom;

const svg = d3.create('svg')
.attr('width', visWidth + margin.left + margin.right)
.attr('height', visHeight + margin.top + margin.bottom);

const g = svg.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
// create scales
const x = d3.scaleLinear()
.domain(d3.extent(carData, d => d.horsepower)).nice()
.range([0, visWidth]);
const y = d3.scaleLinear()
.domain(d3.extent(carData, d => d.weight)).nice()
.range([visHeight, 0]);
// create and add axes
const xAxis = d3.axisBottom(x);
g.append("g")
.attr('transform', `translate(0, ${visHeight})`)
.call(xAxis)
.call(g => g.selectAll('.domain').remove())
.append('text')
.attr('x', visWidth / 2)
.attr('y', 40)
.attr('fill', 'black')
.attr('text-anchor', 'middle')
.text('horsepower');
const yAxis = d3.axisLeft(y);
g.append('g')
.call(yAxis)
.call(g => g.selectAll('.domain').remove())
.append('text')
.attr('x', -40)
.attr('y', visHeight / 2)
.attr('fill', 'black')
.attr('dominant-baseline', 'middle')
.text('weight (lbs)');
// draw grid, based on https://observablehq.com/@d3/scatterplot
const grid = g.append('g')
.attr('class', 'grid');
grid.append('g')
.selectAll('line')
.data(y.ticks())
.join('line')
.attr('stroke', '#d3d3d3')
.attr('x1', 0)
.attr('x2', visWidth)
.attr('y1', d => 0.5 + y(d))
.attr('y2', d => 0.5 + y(d));
grid.append('g')
.selectAll('line')
.data(x.ticks())
.join('line')
.attr('stroke', '#d3d3d3')
.attr('x1', d => 0.5 + x(d))
.attr('x2', d => 0.5 + x(d))
.attr('y1', d => 0)
.attr('y2', d => visHeight);
// draw points
const radius = 3;
g.selectAll('circle')
.data(carData)
.join('circle')
.attr('cx', d => x(d.horsepower))
.attr('cy', d => y(d.weight))
.attr('fill', d => carColor(d.origin))
.attr('opacity', 1)
.attr('r', radius)
// ********** new stuff starts here **********
.on('mouseenter', mouseEnter)
.on('mouseleave', mouseLeave);
// create tooltip
const tooltip = g.append('g')
.attr('visibility', 'hidden');
const tooltipHeight = 15;
const tooltipRect = tooltip.append('rect')
.attr('fill', 'black')
.attr('rx', 5)
.attr('height', tooltipHeight);
const tooltipText = tooltip.append('text')
.attr('fill', 'white')
.attr('font-family', 'sans-serif')
.attr('font-size', 10)
.attr('dy', 2)
.attr('dx', 2)
.attr('dominant-baseline', 'hanging')
// handle hovering over a circle
function mouseEnter(event, d) {
// make the circle larger
d3.select(this)
.attr('r', radius * 2);
// update the text and get its width
tooltipText.text(d.name);
const labelWidth = tooltipText.node().getComputedTextLength();
// set the width of the tooltip's background rectangle
// to match the width of the label
tooltipRect.attr('width', labelWidth + 4);
// move the tooltip and make it visible
const xPos = x(d.horsepower) + radius * 3;
const yPos = y(d.weight) - tooltipHeight / 2;

tooltip.attr('transform', `translate(${xPos},${yPos})`)
.attr('visibility', 'visible');
}
// handle leaving a circle
function mouseLeave(event, d) {
// reset its size
d3.select(this)
.attr('r', radius)
// make the tooltip invisible
tooltip
.attr('visibility', 'hidden');
}
return svg.node();
}
Insert cell
Insert cell
viewof clickCount = {
const svg = d3.create('svg')
.attr('width', 100)
.attr('height', 100);

let count = 0;

svg.append('circle')
.attr('cx', 50)
.attr('cy', 50)
.attr('r', 25)
.attr('fill', 'green')
.on('click', function() {
count += 1;
// update the value
svg.property('value', count);
// emit an input event to tell Observable that
// there's a new value
svg.dispatch('input');
});

// set the value to zero to start
svg.property('value', 0).dispatch('input');

return svg.node();
}
Insert cell
clickCount
Insert cell
Insert cell
Insert cell
Insert cell
{
// margin convention
const margin = {top: 10, right: 100, bottom: 50, left: 100};
const visWidth = 600 - margin.left - margin.right;
const visHeight = 460 - margin.top - margin.bottom;

const svg = d3.create('svg')
.attr('width', visWidth + margin.left + margin.right)
.attr('height', visHeight + margin.top + margin.bottom);

const g = svg.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
// create scales
const x = d3.scaleLinear()
.domain(d3.extent(carData, d => d.horsepower)).nice()
.range([0, visWidth]);
const y = d3.scaleLinear()
.domain(d3.extent(carData, d => d.weight)).nice()
.range([visHeight, 0]);
// create and add axes
const xAxis = d3.axisBottom(x);
g.append("g")
.attr('transform', `translate(0, ${visHeight})`)
.call(xAxis)
.call(g => g.selectAll('.domain').remove())
.append('text')
.attr('x', visWidth / 2)
.attr('y', 40)
.attr('fill', 'black')
.attr('text-anchor', 'middle')
.text('horsepower');
const yAxis = d3.axisLeft(y);
g.append('g')
.call(yAxis)
.call(g => g.selectAll('.domain').remove())
.append('text')
.attr('x', -40)
.attr('y', visHeight / 2)
.attr('fill', 'black')
.attr('dominant-baseline', 'middle')
.text('weight (lbs)');
// draw grid, based on https://observablehq.com/@d3/scatterplot
const grid = g.append('g')
.attr('class', 'grid');
grid.append('g')
.selectAll('line')
.data(y.ticks())
.join('line')
.attr('stroke', '#d3d3d3')
.attr('x1', 0)
.attr('x2', visWidth)
.attr('y1', d => 0.5 + y(d))
.attr('y2', d => 0.5 + y(d));
grid.append('g')
.selectAll('line')
.data(x.ticks())
.join('line')
.attr('stroke', '#d3d3d3')
.attr('x1', d => 0.5 + x(d))
.attr('x2', d => 0.5 + x(d))
.attr('y1', d => 0)
.attr('y2', d => visHeight);
// draw points
g.selectAll('circle')
.data(carData.filter(d => selections.get(d.origin)))
.join('circle')
.attr('cx', d => x(d.horsepower))
.attr('cy', d => y(d.weight))
.attr('fill', d => carColor(d.origin))
.attr('opacity', 1)
.attr('r', 3);
return svg.node();
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
viewof cars = {
// margin convention
const margin = {top: 10, right: 10, bottom: 50, left: 100};
const visWidth = 510 - margin.left - margin.right;
const visHeight = 460 - margin.top - margin.bottom;

const svg = d3.create('svg')
.attr('width', visWidth + margin.left + margin.right)
.attr('height', visHeight + margin.top + margin.bottom);

const g = svg.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
// create scales
const x = d3.scaleLinear()
.domain(d3.extent(carData, d => d.horsepower)).nice()
.range([0, visWidth]);
const y = d3.scaleLinear()
.domain(d3.extent(carData, d => d.weight)).nice()
.range([visHeight, 0]);
// create and add axes
const xAxis = d3.axisBottom(x);
g.append("g")
.attr('transform', `translate(0, ${visHeight})`)
.call(xAxis)
.call(g => g.selectAll('.domain').remove())
.append('text')
.attr('x', visWidth / 2)
.attr('y', 40)
.attr('fill', 'black')
.attr('text-anchor', 'middle')
.text('horsepower');
const yAxis = d3.axisLeft(y);
g.append('g')
.call(yAxis)
.call(g => g.selectAll('.domain').remove())
.append('text')
.attr('x', -40)
.attr('y', visHeight / 2)
.attr('fill', 'black')
.attr('dominant-baseline', 'middle')
.text('weight (lbs)');
// draw grid, based on https://observablehq.com/@d3/scatterplot
const grid = g.append('g');
grid.append('g')
.selectAll('line')
.data(y.ticks())
.join('line')
.attr('stroke', '#d3d3d3')
.attr('x1', 0)
.attr('x2', visWidth)
.attr('y1', d => 0.5 + y(d))
.attr('y2', d => 0.5 + y(d));
grid.append('g')
.selectAll('line')
.data(x.ticks())
.join('line')
.attr('stroke', '#d3d3d3')
.attr('x1', d => 0.5 + x(d))
.attr('x2', d => 0.5 + x(d))
.attr('y1', d => 0)
.attr('y2', d => visHeight);
// draw points
const radius = 3;
const dots = g.selectAll('circle')
.data(carData)
.join('circle')
.attr('cx', d => x(d.horsepower))
.attr('cy', d => y(d.weight))
.attr('fill', d => carColor(d.origin))
.attr('opacity', 1)
.attr('r', radius);
// ********** new stuff starts here **********
const brush = d3.brush()
// set the space that the brush can take up
.extent([[0, 0], [visWidth, visHeight]])
// handle events
.on('brush', onBrush)
.on('end', onEnd);
g.append('g')
.call(brush);
function onBrush(event) {
// event.selection gives us the coordinates of the
// top left and bottom right of the brush box
const [[x1, y1], [x2, y2]] = event.selection;
// return true if the dot is in the brush box, false otherwise
function isBrushed(d) {
const cx = x(d.horsepower);
const cy = y(d.weight)
return cx >= x1 && cx <= x2 && cy >= y1 && cy <= y2;
}
// style the dots that aren't selected
dots.classed('grayed', d => !isBrushed(d));
// Observable specific:
// update the data that appears in the cars variable
// this works the same as doing
// svg.node().value = carData.filter(isBrushed);
// svg.node().dispatchEvent(new CustomEvent('input'));
svg.property('value', carData.filter(isBrushed)).dispatch('input');
}
function onEnd(event) {
// if the brush is cleared
if (event.selection === null) {
dots.classed('grayed', false);
svg.property('value', []).dispatch('input');
}
}
svg.property('value', []).dispatch('input');

return svg.node();
}
Insert cell
Insert cell
html`<style>
.grayed {
opacity: 0.25;
}
</style>`
Insert cell
Insert cell
Insert cell
{
// margin convention
const margin = {top: 10, right: 10, bottom: 50, left: 100};
const visWidth = 510 - margin.left - margin.right;
const visHeight = 460 - margin.top - margin.bottom;

const svg = d3.create('svg')
.attr('width', visWidth + margin.left + margin.right)
.attr('height', visHeight + margin.top + margin.bottom);

const g = svg.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
// create scales
const x = d3.scaleLinear()
.domain(d3.extent(carData, d => d.horsepower)).nice()
.range([0, visWidth]);
const y = d3.scaleLinear()
.domain(d3.extent(carData, d => d.weight)).nice()
.range([visHeight, 0]);
// create and add axes
const xAxis = d3.axisBottom(x);
const xAxisGroup = g.append("g")
.attr('transform', `translate(0, ${visHeight})`)
.call(xAxis)
.call(g => g.selectAll('.domain').remove());
xAxisGroup.append('text')
.attr('x', visWidth / 2)
.attr('y', 40)
.attr('fill', 'black')
.attr('text-anchor', 'middle')
.text('horsepower');
const yAxis = d3.axisLeft(y);
const yAxisGroup = g.append('g')
.call(yAxis)
.call(g => g.selectAll('.domain').remove());
yAxisGroup.append('text')
.attr('x', -40)
.attr('y', visHeight / 2)
.attr('fill', 'black')
.attr('dominant-baseline', 'middle')
.text('weight (lbs)');
// draw grid
const grid = g.append('g');
grid.append('rect')
.attr('width', visWidth)
.attr('height', visHeight)
.attr('fill', 'white');
let yLines = grid.append('g')
.selectAll('line');
let xLines = grid.append('g')
.selectAll('line');
function drawGridLines(x, y) {
yLines = yLines.data(y.ticks())
.join('line')
.attr('stroke', '#d3d3d3')
.attr('x1', 0)
.attr('x2', visWidth)
.attr('y1', d => 0.5 + y(d))
.attr('y2', d => 0.5 + y(d));
xLines = xLines.data(x.ticks())
.join('line')
.attr('stroke', '#d3d3d3')
.attr('x1', d => 0.5 + x(d))
.attr('x2', d => 0.5 + x(d))
.attr('y1', d => 0)
.attr('y2', d => visHeight);
}
drawGridLines(x, y);
// draw points
// ********** new stuff starts here **********
// clip path hides dots that go outside of the vis when zooming
svg.append('clipPath')
.attr('id', 'border')
.append('rect')
.attr('width', visWidth)
.attr('height', visHeight)
.attr('fill', 'white');
const dotsGroup = g.append('g')
.attr('clip-path', 'url(#border)');
const dots = dotsGroup.selectAll('circle')
.data(carData)
.join('circle')
.attr('cx', d => x(d.horsepower))
.attr('cy', d => y(d.weight))
.attr('fill', d => carColor(d.origin))
.attr('opacity', 1)
.attr('r', 3);
const zoom = d3.zoom()
.extent([[0, 0], [visWidth, visHeight]])
// determine how much you can zoom out and in
.scaleExtent([1, Infinity])
.on('zoom', onZoom);
g.call(zoom);
function onZoom(event) {
// get updated scales
const xNew = event.transform.rescaleX(x);
const yNew = event.transform.rescaleY(y);
// update the position of the dots
dots.attr('cx', d => xNew(d.horsepower))
.attr('cy', d => yNew(d.weight));
// update the axes
xAxisGroup.call(xAxis.scale(xNew))
.call(g => g.selectAll('.domain').remove());

yAxisGroup.call(yAxis.scale(yNew))
.call(g => g.selectAll('.domain').remove());
// update the grid
drawGridLines(xNew, yNew);
}
return svg.node();
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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