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

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