Public
Edited
Oct 11, 2022
4 forks
1 star
Insert cell
Insert cell
Insert cell
jsonCarData = FileAttachment("cars.json").json()
Insert cell
carData = jsonCarData.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 => d.horsepower !== null && d.weight !== null && d.origin !== null && d.name !== null)
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
x = d3.scaleLinear()
.domain(d3.extent(carData, d => d.horsepower)).nice()
.range([0, visWidth])
Insert cell
y = d3.scaleLinear()
.domain(d3.extent(carData, d => d.weight)).nice()
.range([visHeight, 0])
Insert cell
Insert cell
Insert cell
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

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