Public
Edited
Feb 24, 2023
Paused
2 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
carData = cars.filter(d => d.Miles_per_Gallon != null).filter(d => d.Horsepower != null)
Insert cell
originCounts = d3.rollup(carData, group => group.length, d => d.Origin);
Insert cell
origins = Array.from(originCounts.keys()).sort()
Insert cell
Insert cell
baseHisto("Cylinders")
Insert cell
baseHisto("Horsepower")
Insert cell
// pass in the column name to create the histogram
function baseHisto(col) {
// set up
const histoWidth = width * 0.33;
const num_hist = 4;
const histoHeight = (scatter_sq/num_hist)-(6*num_hist);

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

const g = svg.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`);

// create x scale
const x = d3.scaleLinear()
.range([0, histoWidth])
.domain(d3.extent(carData, d => d[col])).nice();
// NEW: compute bins for histogram
// https://www.d3-graph-gallery.com/graph/histogram_basic.html
// https://observablehq.com/@d3/d3-bin
const histogram = d3.bin()
.value(d => d[col]) // specify the values used for the histogram
.domain(x.domain()); // domain is same as x-axis
const bins = histogram(carData);

console.log(bins);
// create y scale
const y = d3.scaleLinear()
.range([histoHeight, 0])
.domain([0, d3.max(bins, d => d.length)]).nice(); // need to know bins first
// create and add axes
const xAxis = d3.axisBottom(x);
const xAxisGroup = g.append("g")
.attr("transform", `translate(0, ${histoHeight})`);
xAxisGroup.append("text")
.attr("x", 0)
.attr("y", 33)
.attr("fill", "black")
.attr("font-weight", "bold")
.attr("text-anchor", "start")
.text(col);
xAxisGroup.call(xAxis);
const yAxis = d3.axisLeft(y).ticks(3);
const yAxisGroup = g.append("g").call(yAxis)
// add grid lines
.call(g => g.selectAll('.tick line')
.clone()
.attr('stroke', '#d3d3d3')
.attr('x1', 0)
.attr('x2', histoWidth))

// https://observablehq.com/@ehs219/d3-histogram
// draw bars
const spacing = 1
g.append("g")
.selectAll("rect")
.data(bins)
.join("rect")
.attr("fill", "steelblue")
.attr("x", d => x(d.x0) + spacing) // spacing allows for some space between the bars
.attr("width", d => x(d.x1) - x(d.x0) - spacing*2) // spacing adds space between the bars
.attr("y", d => y(d.length))
.attr("height", d => histoHeight - y(d.length));
return svg.node();
}
Insert cell
Insert cell
{
const scatter = brushableScatterplot();
const bar = barChart();
const histo_cyl = filteredHisto("Cylinders");
const histo_displ = filteredHisto("Displacement");
const histo_hp = filteredHisto("Horsepower");
const histo_acc = filteredHisto("Acceleration");

// update the bar chart and histograms when the selection changes
d3.select(scatter).on('input', () => {
bar.update(scatter.value);
histo_cyl.update(scatter.value);
histo_displ.update(scatter.value);
histo_hp.update(scatter.value);
histo_acc.update(scatter.value);
});
// intial state of bar chart and histograms
bar.update(scatter.value);
histo_cyl.update(scatter.value);
histo_displ.update(scatter.value);
histo_hp.update(scatter.value);
histo_acc.update(scatter.value);

// build HTML table to place charts in
let table = html`<table><tr><td>${bar}<br/>${scatter}</td><td>${histo_cyl}<br/>${histo_displ}<br/>${histo_hp}<br/>${histo_acc}</td></table>`;
return table;
}
Insert cell
// pass in the column name to create the histogram
function filteredHisto(col) {
// set up
const histoWidth = width * 0.33;
const num_hist = 4;
const histoHeight = (scatter_sq/num_hist)-(6*num_hist);

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

const g = svg.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`);

// create x scale
const x = d3.scaleLinear()
.range([0, histoWidth])
.domain(d3.extent(carData, d => d[col])).nice();
// https://www.d3-graph-gallery.com/graph/histogram_basic.html
// https://observablehq.com/@d3/d3-bin
const histogram = d3.bin()
.value(d => d[col]) // specify the values used for the histogram
.domain(x.domain()); // domain is same as x-axis
const bins = histogram(carData);
// create y scale
const y = d3.scaleLinear()
.range([histoHeight, 0])
.domain([0, d3.max(bins, d => d.length)]).nice(); // need to know bins first
// create and add axes
const xAxis = d3.axisBottom(x);
const xAxisGroup = g.append("g")
.attr("transform", `translate(0, ${histoHeight})`);
xAxisGroup.append("text")
.attr("x", 0)
.attr("y", 33)
.attr("fill", "black")
.attr("font-weight", "bold")
.attr("text-anchor", "start")
.text(col);
xAxisGroup.call(xAxis);
const yAxis = d3.axisLeft(y).ticks(3);
const yAxisGroup = g.append("g").call(yAxis)
// add grid lines
.call(g => g.selectAll('.tick line')
.clone()
.attr('stroke', '#d3d3d3')
.attr('x1', 0)
.attr('x2', histoWidth))

// NEW: draw background gray bars
g.append("g").selectAll("rect")
.data(bins)
.join("rect")
.attr("fill", "lightgray")
.attr("x", d => x(d.x0) + 1)
.attr("width", d => x(d.x1) - x(d.x0)-1)
.attr("y", d => y(d.length))
.attr("height", d => histoHeight - y(d.length));
let barsGroup = g.append("g");

// NEW: added update() that updates bins and redraws bars
function update(data) {
// update bins
const histogram = d3.bin()
.value(d => d[col]) // specify the values used for the histogram
.domain(x.domain()); // domain is same as x-axis
const bins = histogram(data);

// setup transition
const t = svg.transition()
.ease(d3.easeLinear) // easing examples at https://observablehq.com/@d3/easing-animations
.duration(200);
// draw bars
barsGroup.selectAll("rect")
.data(bins)
.join("rect")
.attr("fill", "steelblue")
.attr("x", d => x(d.x0) + 1)
.attr("width", d => x(d.x1) - x(d.x0) - 1)
.transition(t)
.attr("y", d => y(d.length))
.attr("height", d => histoHeight - y(d.length));
}
return Object.assign(svg.node(), { update });
}
Insert cell
function barChart() {
// set up
const barWidth = scatter_sq;
const barHeight = 50;

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

const g = svg.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`);
// create scales
const x = d3.scaleLinear()
.range([0, barWidth])
.domain([0, d3.max(originCounts.values())]).nice(); // keep scale the same
const y = d3.scaleBand()
.domain(origins)
.range([0, barHeight])
.padding(0.2);
// create and add axes
const xAxis = d3.axisBottom(x);
const xAxisGroup = g.append("g")
.attr("transform", `translate(0, ${barHeight})`);
// draw grid lines
xAxisGroup.call(xAxis)
.call(g => g.selectAll('.tick line')
.clone()
.attr('stroke', '#d3d3d3')
.attr('y1', -barHeight)
.attr('y2', 0));
const yAxis = d3.axisLeft(y);
const yAxisGroup = g.append("g").call(yAxis);
yAxisGroup.append("text")
.attr("transform", "rotate(-90)") // now x, y flipped in terms of positioning
.attr("x", -1 * (barHeight / 2))
.attr("y", -50)
.attr("fill", "black")
.attr("dominant-baseline", "middle")
.attr("text-anchor", "middle")
.attr("font-weight", "bold")
.text("Origin");
let barsGroup = g.append("g");

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

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

// 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
function brushableScatterplot() {
const visWidth = 400, visHeight = 400;
// set up
const xCol = "Weight_in_lbs", xLabel = "Weight (lbs)";
const yCol = "Miles_per_Gallon", yLabel = "MPG";
// 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})`);

// set up scales
const x = d3.scaleLinear()
.domain([0, d3.max(carData, d => d[xCol])]).nice()
.range([0, visWidth]);

const y = d3.scaleLinear()
.domain([0, d3.max(carData, d => d[yCol])]).nice()
.range([visHeight, 0]);
// set up axes
const xAxis = d3.axisBottom(x);
const xAxisGroup = g.append('g')
.attr('transform', `translate(0, ${visHeight})`);
xAxisGroup.call(xAxis)
// 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')
.attr('font-weight', 'bold')
.text(xLabel);

const yAxis = d3.axisLeft(y);
const yAxisGroup = g.append('g');
// add grid lines
// refernces https://observablehq.com/@d3/connected-scatterplot
yAxisGroup.call(yAxis)
.call(g => g.selectAll('.tick line')
.clone()
.attr('stroke', '#d3d3d3')
.attr('x1', 0)
.attr('x2', visWidth))
// add label
.append('text')
.attr("transform", "rotate(-90)") // now x, y flipped in terms of positioning
.attr("x", -1 * (visHeight / 2))
.attr("y", -50)
.attr('fill', 'black')
.attr('dominant-baseline', 'middle')
.attr('font-weight', 'bold')
.text(yLabel);
// draw points
const radius = 3;
const dots = g.selectAll('circle')
.data(carData)
.join('circle')
.attr('cx', d => x(d[xCol]))
.attr('cy', d => y(d[yCol]))
.attr('r', radius)
// make dots look like Vega-Lite dots
.attr('opacity', 0.8)
.attr('stroke', d => carColor(d.Origin))
.attr('fill', 'none')
.attr('stroke-width', 2);
// ********** 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[xCol]);
const cy = y(d[yCol])
return cx >= x1 && cx <= x2 && cy >= y1 && cy <= y2;
}
// style the dots
dots.attr('stroke', d => isBrushed(d) ? carColor(d.Origin) : 'gray');
// update the data that appears in the cars variable
svg.property('value', cars.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('stroke', d => carColor(d.Origin));
svg.property('value', initialValue).dispatch('input');
}
}

return svg.node();
}
Insert cell
Insert cell
margin = ({top: 10, right: 20, bottom: 50, left: 80})
Insert cell
scatter_sq = width * 0.45;
Insert cell
carColor = d3.scaleOrdinal()
.domain(origins)
.range(d3.schemeTableau10);
Insert cell
Insert cell
d3 = require('d3@7')
Insert cell
import {aq, op} from "@uwdata/arquero"
Insert cell
import {Swatches} from "@d3/color-legend"
Insert cell
import {visualizeTicks} from "@d3/continuous-scales"
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