Public
Edited
Oct 13, 2022
Fork of Homework 2
Insert cell
Insert cell
Insert cell
jsonCarData = FileAttachment("cars.json").json()
Insert cell
Insert cell
Insert cell
margin = ({top: 10, right: 20, bottom: 50, left: 105});
Insert cell
visWidth = 400;
Insert cell
visHeight = 400;
Insert cell
Insert cell
origins = Array.from(new Set(carData.map(d => d.origin)));
Insert cell
carColor = d3.scaleOrdinal().domain(origins).range(d3.schemeCategory10);
Insert cell
d3.extent(Array.from(new Set(carData.map(d => d.weight))))
Insert cell
// x = ?
x = d3.scaleLinear()
.domain(d3.extent(carData, d => d.horsepower)).nice()
.range([0, visWidth])
Insert cell
// y = ?
y = d3.scaleLinear()
.domain(d3.extent(carData, d => d.weight)).nice()
.range([visHeight, 0])
Insert cell
Insert cell
xAxis = (g, scale, label) =>
g.attr('transform', `translate(0, ${visHeight})`)
// 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', -visHeight)
.attr('y2', 0))
// add label
.append('text')
.attr('x', visWidth / 2)
.attr('y', 40)
.attr('fill', 'black')
.attr('text-anchor', 'middle')
.text(label)
Insert cell
yAxis = (g, scale, label) =>
// add axis
g.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', visWidth))
// add label
.append('text')
.attr('x', -40)
.attr('y', visHeight / 2)
.attr('fill', 'black')
.attr('dominant-baseline', 'middle')
.text(label)
Insert cell
Insert cell
scatterplot()
Insert cell
function scatterplot() {
// set up
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})`);
// axes
g.append("g").call(xAxis, x, 'horsehpower');
g.append("g").call(yAxis, y, 'weight (lbs)');
// draw points
g.selectAll('circle')
// filter data to only contain selected car origins
.data(carData.filter(d => 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
cars
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', visWidth + margin.left + margin.right)
.attr('height', visHeight + margin.top + margin.bottom)
.property('value', initialValue);

const g = svg.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
// axes
g.append("g").call(xAxis, x, 'horsehpower');
g.append("g").call(yAxis, y, 'weight (lbs)');
// 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);
// ********** brushing 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
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
{
const bar = barChart();
bar.update(carData);
return bar;
}
Insert cell
function barChart() {
// set up
const margin = {top: 10, right: 20, bottom: 50, left: 50};
const visWidth = 300;
const visHeight = 200;

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()
.range([0, visWidth]);
const y = d3.scaleBand()
.domain(carColor.domain())
.range([0, visHeight])
.padding(0.2);
// create and add axes
const xAxis = d3.axisBottom(x).tickSizeOuter(0);
const xAxisGroup = g.append("g")
.attr("transform", `translate(0, ${visHeight})`);
xAxisGroup.append("text")
.attr("x", visWidth / 2)
.attr("y", 40)
.attr("fill", "black")
.attr("text-anchor", "middle")
.text("Count");
const yAxis = d3.axisLeft(y);
const yAxisGroup = g.append("g")
.call(yAxis)
// remove baseline from the axis
.call(g => g.select(".domain").remove());
let barsGroup = g.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", 0)
.attr("y", ([origin, count]) => y(origin))
.transition(t)
.attr("width", ([origin, count]) => x(count))
}
return Object.assign(svg.node(), { update });;
}
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
Insert cell
d3 = require("d3")
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