Public
Edited
Jan 5
6 forks
16 stars
Insert cell
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 and pass its name
.on('mouseleave', mouseLeave);
function mouseLeave() {
d3.select(this)
.attr('r', 25)
.attr('fill', 'red');
}
return svg.node();
}
Insert cell
Insert cell
Insert cell
{
// set up

const svg = d3.create('svg')
// add more margin to the right to account for the tooltips
.attr('width', width + 100)
.attr('height', height);

// axes
svg.append("g").call(xAxis, x, 'horsehpower');
svg.append("g").call(yAxis, y, 'weight (lbs)');
// draw points
const radius = 3;
svg.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('r', radius)
// ********** new stuff starts here **********
.on('mouseenter', mouseEnter)
.on('mouseleave', mouseLeave);
// create tooltip
const tooltip = svg.append('g')
.attr('visibility', 'hidden');
const tooltipHeight = 16;
// add a rectangle to the tooltip to serve as a background
const tooltipRect = tooltip.append('rect')
.attr('fill', 'black')
.attr('rx', 5)
.attr('height', tooltipHeight);
// add a text element to the tooltip to contain the label
const tooltipText = tooltip.append('text')
.attr('fill', 'white')
.attr('font-family', 'sans-serif')
.attr('font-size', 12)
.attr('y', 2) // offset it from the edge of the rectangle
.attr('x', 3) // offset it from the edge of the rectangle
.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 label's 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, plus some extra space
tooltipRect.attr('width', labelWidth + 6);
// move the tooltip to the position of the circle (offset by a bit)
// and make the tooltip 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 the size of the circle
d3.select(this)
.attr('r', radius)
// make the tooltip invisible
tooltip
.attr('visibility', 'hidden');
}
return svg.node();
}
Insert cell
Insert cell
clickCount
Insert cell
// putting viewof before the name of the 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 property
svg.property('value', count);
// emit an input event to tell Observable that
// there's a new value for clickCount
svg.dispatch('input');
});

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

return svg.node();
}
Insert cell
Insert cell
selections
Insert cell
Insert cell
{
// set up
const svg = d3.create('svg')
.attr('width', width)
.attr('height', height);
// axes
svg.append("g").call(xAxis, x, 'horsehpower');
svg.append("g").call(yAxis, y, 'weight (lbs)');
// draw points
svg.selectAll('circle')
// filter data to only contain selected car origins
.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
{
// set up

// const margin = {top: 10, right: 100, bottom: 50, left: 105};
const svg = d3.create('svg')
// add some extra margin for the legend
.attr('width', width + 80)
.attr('height', height);
// axes
svg.append("g").call(xAxis, x, 'horsehpower');
svg.append("g").call(yAxis, y, 'weight (lbs)');
// draw points
// make it a function so that we can call it when filtering too
function drawPoints(data) {
svg.selectAll('circle')
.data(data)
.join('circle')
.attr('cx', d => x(d.horsepower))
.attr('cy', d => y(d.weight))
.attr('fill', d => carColor(d.origin))
.attr('r', 3);
}
drawPoints(carData);
// draw legend
const legend = svg.append('g')
.attr('transform', `translate(${width - margin.right},${margin.top})`);
const rows = legend.selectAll('g')
.data(origins)
.join('g')
.attr('transform', (d, i) => `translate(20, ${i * 20})`);
rows.append('rect')
.attr('width', 15)
.attr('height', 15)
.attr('stroke-width', 2)
.attr('stroke', d => carColor(d))
.attr('fill', d => carColor(d))
.on('click', onclick);
rows.append('text')
.attr('font-size', 15)
.attr('x', 20)
.attr('y', 7.5)
.attr('font-family', 'sans-serif')
.attr('dominant-baseline', 'middle')
.text(d => d);
// track which origins are selected
const selected = new Map(origins.map(d => [d, true]));
function onclick(event, d) {
const isSelected = selected.get(d);
// select the square and toggle it
const square = d3.select(this);
square.attr('fill', d => isSelected ? 'white' : carColor(d));
selected.set(d, !isSelected);
// redraw the points
drawPoints(carData.filter(d => selected.get(d.origin)));
}
return svg.node();
}
Insert cell
Insert cell
cars
Insert cell
Insert cell
viewof cars = brushableScatterplot()
Insert cell
function brushableScatterplot() {
// set up

// the value for when there is no brush
const initialValue = carData;

const svg = d3.create('svg')
.attr('width', width)
.attr('height', height)
.property('value', initialValue);

// axes
svg.append("g").call(xAxis, x, 'horsehpower');
svg.append("g").call(yAxis, y, 'weight (lbs)');
// draw points
const radius = 3;
const dots = svg.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([[margin.left, margin.top], [width - margin.right, height - margin.bottom]])
// handle events
.on('brush', onBrush)
.on('end', onEnd);
svg.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
dots.attr('fill', d => isBrushed(d) ? carColor(d.origin) : 'gray');
// update the data that appears in the cars variable
svg.property('value', carData.filter(isBrushed)).dispatch('input');
}
function onEnd(event) {
// if the brush is cleared
if (event.selection === null) {
// reset the color of all of the dots
dots.attr('fill', d => carColor(d.origin));
svg.property('value', initialValue).dispatch('input');
}
}

return svg.node();
}
Insert cell
Insert cell
Insert cell
{
// set up

const svg = d3.create('svg')
.attr('width', width)
.attr('height', height);

// axes

const xAxisGroup = svg.append('g')
.attr('transform', `translate(0, ${height - margin.bottom})`)
// add axis
.call(d3.axisBottom(x))
// remove baseline
.call(g => g.select('.domain').remove())
// add label
.call(g => g.append('text')
.attr('x', margin.left + (width - margin.bottom - margin.top) / 2)
.attr('y', 40)
.attr('fill', 'black')
.attr('text-anchor', 'middle')
.text('horsepower'));

const yAxisGroup = svg.append('g')
.attr('transform', `translate(${margin.left})`)
.call(d3.axisLeft(y))
// remove baseline
.call(g => g.select('.domain').remove())
// add label
.call(g => g.append('text')
.attr('x', -40)
.attr('y', margin.top + (height - margin.top - margin.bottom) / 2)
.attr('fill', 'black')
.attr('dominant-baseline', 'middle')
.text('weight (lbs)'));

// grid based on https://observablehq.com/@d3/scatterplot

const yGrid = svg.append('g');

yGrid.selectAll('line')
.data(y.ticks())
.join('line')
.attr('stroke', '#d3d3d3')
.attr('x1', margin.left)
.attr('x2', width - margin.right)
.attr('y1', d => y(d))
.attr('y2', d => y(d));
const xGrid = svg.append('g');

xGrid.selectAll('line')
.data(x.ticks())
.join('line')
.attr('stroke', '#d3d3d3')
.attr('x1', d => x(d))
.attr('x2', d => x(d))
.attr('y1', margin.top)
.attr('y2', height - margin.bottom);
// clip path hides dots that go outside of the vis when zooming/panning

svg.append('clipPath')
.attr('id', 'border')
.append('rect')
.attr('x', margin.left)
.attr('y', margin.top)
.attr('width', width - margin.left - margin.right)
.attr('height', height - margin.top - margin.bottom)
.attr('fill', 'white');

const dotsGroup = svg.append('g')
.attr('clip-path', 'url(#border)');

// draw circles
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([
[margin.left, margin.top],
[width - margin.right, height - margin.bottom]
])
// determine how much you can zoom out and in
.scaleExtent([1, Infinity])
.on('zoom', onZoom);
svg.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(d3.axisBottom(xNew))
.call(g => g.selectAll('.domain').remove());

yAxisGroup.call(d3.axisLeft(yNew))
.call(g => g.selectAll('.domain').remove());
// update the grid
yGrid.selectAll('line')
.data(yNew.ticks())
.join('line')
.attr('stroke', '#d3d3d3')
.attr('x1', margin.left)
.attr('x2', width - margin.right)
.attr('y1', d => 0.5 + yNew(d))
.attr('y2', d => 0.5 + yNew(d));
xGrid.selectAll('line')
.data(xNew.ticks())
.join('line')
.attr('stroke', '#d3d3d3')
.attr('y1', margin.top)
.attr('y2', height - margin.bottom)
.attr('x1', d => 0.5 + xNew(d))
.attr('x2', d => 0.5 + xNew(d));
}

return svg.node();
}
Insert cell
Insert cell
columns = ['horsepower', 'weight', 'displacement', 'acceleration', 'mpg']
Insert cell
viewof xFeature = Inputs.radio(columns, {value: 'horsepower', label: 'x axis'})
Insert cell
viewof yFeature = Inputs.radio(columns, {value: 'weight', label: 'y axis'})
Insert cell
Insert cell
{
// set up

const svg = d3.create('svg')
.attr('width', width)
.attr('height', height);

// scales

const xScale = d3.scaleLinear()
.domain(d3.extent(carData, d => d[xFeature])).nice()
.range([margin.left, width - margin.right]);

const yScale = d3.scaleLinear()
.domain(d3.extent(carData, d => d[yFeature])).nice()
.range([height - margin.bottom, margin.top])
// axes
svg.append("g").call(xAxis, xScale, xFeature);
svg.append("g").call(yAxis, yScale, yFeature);

// circles
const dots = svg.selectAll('circle')
.data(carData)
.join('circle')
.attr('cx', d => xScale(d[xFeature]))
.attr('cy', d => yScale(d[yFeature]))
.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
carsScatterplot = {
// set up

const svg = d3.create('svg')
.attr('width', width)
.attr('height', height);

// scales

const xScale = d3.scaleLinear()
.range([margin.left, width - margin.right]);

const yScale = d3.scaleLinear()
.range([height - margin.bottom, margin.top]);

// axes

const axesGroup = svg.append('g');
let xAxisGroup = axesGroup.append('g');
let yAxisGroup = axesGroup.append('g');

// dots

const dots = svg.selectAll('circle')
.data(carData)
.join('circle')
.attr('fill', d => carColor(d.origin))
.attr('r', 3);

// update the chart for the given features
function update(xFeat, yFeat) {
// transition

const t = svg.transition()
.duration(500);

// update the scales

xScale.domain(d3.extent(carData, d => d[xFeat])).nice();
yScale.domain(d3.extent(carData, d => d[yFeat])).nice();

// update the axes

// x axis

// fade-out the old axis
xAxisGroup.transition(t)
.attr('opacity', 0)
.remove();

// add the new axis
xAxisGroup = axesGroup.append('g')
// make it invisible
.attr('opacity', 0)
.call(xAxis, xScale, xFeat);

// fade-in the new axis
xAxisGroup.transition(t)
.attr('opacity', 1);

// y axis

// fade-out the old axis
yAxisGroup.transition(t)
.attr('opacity', 0)
.remove();

// add the new axis
yAxisGroup = axesGroup.append('g')
// make it invisible
.attr('opacity', 0)
.call(yAxis, yScale, yFeat);

// fade-in the new axis
yAxisGroup.transition(t)
.attr('opacity', 1);

// update the circles

dots.transition(t)
.attr('cx', d => xScale(d[xFeat]))
.attr('cy', d => yScale(d[yFeat]));
}

// add the update function to the svg.node() object
// so that we can call it outside of this cell
return Object.assign(svg.node(), { update });
}
Insert cell
carsScatterplot.update(xFeat, yFeat)
Insert cell
Insert cell
Insert cell
{
const scatter = brushableScatterplot();
const bar = barChart();

// update the bar chart when the scatterplot
// selection changes
d3.select(scatter).on('input', () => {
bar.update(scatter.value);
});

// intial state of bar chart
bar.update(scatter.value);

// use HTML to place the two charts next to each other
return html`<div style="display: flex">${scatter}${bar}</div>`;
}
Insert cell
function barChart() {
// set up
const margin = {top: 10, right: 20, bottom: 50, left: 50};
const width = 300 + margin.left + margin.right;
const height = 200 + margin.top + margin.bottom;

const svg = d3.create('svg')
.attr('width', width)
.attr('height', height + margin.top + margin.bottom);
// create scales
const x = d3.scaleLinear()
.range([margin.left, width - margin.right]);
const y = d3.scaleBand()
.domain(carColor.domain())
.range([margin.top, height - margin.bottom])
.padding(0.2);
// create and add axes
const xAxis = d3.axisBottom(x).tickSizeOuter(0);
const xAxisGroup = svg.append("g")
.attr("transform", `translate(0, ${height - margin.bottom})`);
xAxisGroup.append("text")
.attr('x', margin.left + (width - margin.left - margin.right) / 2)
.attr("y", 40)
.attr("fill", "black")
.attr("text-anchor", "middle")
.text("Count");
const yAxis = d3.axisLeft(y);
const yAxisGroup = svg.append("g")
.attr("transform", `translate(${margin.left})`)
.call(yAxis)
// remove baseline from the axis
.call(g => g.select(".domain").remove());
let barsGroup = svg.append("g");

function update(data) {
// get the number of cars for each origin
const originCounts = d3.rollup(
data,
group => group.length,
d => d.origin
);

// update x scale
x.domain([0, d3.max(originCounts.values())]).nice()

// update x axis

const t = svg.transition()
.ease(d3.easeLinear)
.duration(200);

xAxisGroup
.transition(t)
.call(xAxis);
// draw bars
barsGroup.selectAll("rect")
.data(originCounts, ([origin, count]) => origin)
.join("rect")
.attr("fill", ([origin, count]) => carColor(origin))
.attr("height", y.bandwidth())
.attr("x", x(0))
.attr("y", ([origin, count]) => y(origin))
.transition(t)
.attr("width", ([origin, count]) => x(count) - x(0))
}
return Object.assign(svg.node(), { update });;
}
Insert cell
Insert cell
Insert cell
margin = ({top: 10, right: 20, bottom: 50, left: 105})
Insert cell
width = 400 + margin.left + margin.right
Insert cell
height = 400 + margin.top + margin.bottom
Insert cell
Insert cell
carColor = d3.scaleOrdinal()
.domain(origins)
.range(d3.schemeCategory10);
Insert cell
x = d3.scaleLinear()
.domain(d3.extent(carData, d => d.horsepower)).nice()
.range([margin.left, width - margin.right])
Insert cell
y = d3.scaleLinear()
.domain(d3.extent(carData, d => d.weight)).nice()
.range([height - margin.bottom, margin.top])
Insert cell
Insert cell
xAxis = (g, scale, label) =>
g.attr('transform', `translate(0, ${height - margin.bottom})`)
// add axis
.call(d3.axisBottom(scale))
// remove baseline
.call(g => g.select('.domain').remove())
// add grid lines
// references https://observablehq.com/@d3/connected-scatterplot
.call(g => g.selectAll('.tick line')
.clone()
.attr('stroke', '#d3d3d3')
.attr('y1', -(height - margin.top - margin.bottom))
.attr('y2', 0))
// add label
.append('text')
.attr('x', margin.left + (width - margin.left - margin.right) / 2)
.attr('y', 40)
.attr('fill', 'black')
.attr('text-anchor', 'middle')
.text(label)
Insert cell
yAxis = (g, scale, label) =>
// add axis
g.attr('transform', `translate(${margin.left})`)
.call(d3.axisLeft(scale))
// remove baseline
.call(g => g.select('.domain').remove())
// add grid lines
// refernces https://observablehq.com/@d3/connected-scatterplot
.call(g => g.selectAll('.tick line')
.clone()
.attr('stroke', '#d3d3d3')
.attr('x1', 0)
.attr('x2', width - margin.left - margin.right))
// add label
.append('text')
.attr('x', -40)
.attr('y', margin.top + (height - margin.top - margin.bottom) / 2)
.attr('fill', 'black')
.attr('dominant-baseline', 'middle')
.text(label)
Insert cell
Insert cell
carData = (await datasets['cars.json']()).map(d => ({
horsepower: d['Horsepower'],
weight: d['Weight_in_lbs'],
origin: d['Origin'],
displacement: d['Displacement'],
acceleration: d['Acceleration'],
mpg: d['Miles_per_Gallon'],
name: d['Name']
})).filter(d => Object.values(d).every(d => d !== null))
Insert cell
origins = Array.from(new Set(carData.map(d => d.origin)))
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