Roundabouts across the world
Distribution of roundabouts based on type and number of approaches.
By Manish Datt
TidyTuesday dataset of 2025-12-16
Plotting code
<script src="https://d3js.org/d3.v7.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@observablehq/plot@0.6.17/dist/plot.umd.min.js" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.4/css/jquery.dataTables.min.css">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.datatables.net/1.13.4/js/jquery.dataTables.min.js"></script>
<div id="controls"></div>
<div id="barplot"></div>
<div id="scatterplot"></div>
<div id="table"></div>
<script>
d3.csv('https://raw.githubusercontent.com/rfordatascience/tidytuesday/main/data/2025/2025-12-16/roundabouts_clean.csv').then(data => {
// Parse numbers
data.forEach(d => {
d.approaches = +d.approaches;
});
// Filter for existing status
data = data.filter(d => d.status === "Existing");
// Get min and max approaches
const minApp = d3.min(data, d => d.approaches);
const maxApp = d3.max(data, d => d.approaches);
// Aggregate by type
const counts = d3.rollup(data, v => v.length, d => d.type);
const sumApproaches = d3.rollup(data, v => d3.sum(v, d => d.approaches), d => d.type);
const aggregated = Array.from(counts, ([type, count]) => ({type, count, sumApp: sumApproaches.get(type)}));
// Sort by count descending
aggregated.sort((a, b) => b.count - a.count);
const types = aggregated.map(d => d.type);
// Create checkboxes
const controls = document.getElementById('controls');
controls.style.display = 'flex';
controls.style.flexWrap = 'wrap';
// All checkbox
const allCheckbox = document.createElement('input');
allCheckbox.type = 'checkbox';
allCheckbox.id = 'all';
allCheckbox.onchange = () => {
const checked = allCheckbox.checked;
types.forEach(type => {
document.getElementById(`type-${type}`).checked = checked;
});
updatePlots();
};
const allLabel = document.createElement('label');
allLabel.htmlFor = 'all';
allLabel.textContent = 'All';
allLabel.style.marginRight = '20px';
controls.appendChild(allCheckbox);
controls.appendChild(allLabel);
types.forEach((type, i) => {
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = `type-${type}`;
checkbox.checked = true; // default all checked
checkbox.onchange = updatePlots;
const label = document.createElement('label');
label.htmlFor = `type-${type}`;
label.textContent = type;
label.style.marginRight = '10px';
controls.appendChild(checkbox);
controls.appendChild(label);
});
// Add slider for max approaches
const sliderDiv = document.createElement('div');
sliderDiv.style.marginTop = '20px';
const slider = document.createElement('input');
slider.type = 'range';
slider.id = 'approachesSlider';
slider.min = 4;
slider.max = maxApp;
slider.value = maxApp;
sliderDiv.appendChild(slider);
const sliderLabel = document.createElement('label');
sliderLabel.htmlFor = 'approachesSlider';
sliderLabel.textContent = 'Max Approaches: ';
const sliderValue = document.createElement('span');
sliderValue.id = 'sliderValue';
sliderValue.textContent = maxApp;
sliderLabel.appendChild(sliderValue);
sliderDiv.appendChild(sliderLabel);
const scatterplotEl = document.getElementById('scatterplot');
scatterplotEl.parentNode.insertBefore(sliderDiv, scatterplotEl);
slider.addEventListener('input', () => {
document.getElementById('sliderValue').textContent = slider.value;
updatePlots();
});
function updatePlots() {
const selectedTypes = types.filter(type => document.getElementById(`type-${type}`).checked);
const maxApproaches = +document.getElementById('approachesSlider').value;
const filteredData = data.filter(d => selectedTypes.includes(d.type) && d.approaches <= maxApproaches);
const filteredAggregated = d3.rollup(filteredData, v => v.length, d => d.type);
const aggregatedArray = Array.from(filteredAggregated, ([type, count]) => ({type, count}));
aggregatedArray.sort((a, b) => b.count - a.count);
// Bar plot
const barplot = Plot.plot({
x: { label: "Total Count" },
y: { label: "Type", domain: aggregatedArray.map(d => d.type), padding: 0 },
marginLeft: 150,
marginBottom: 40,
height: 200,
marks: [
Plot.barX(aggregatedArray, { x: "count", y: "type" })
]
});
const barplotDiv = document.getElementById('barplot');
while (barplotDiv.firstChild) {
barplotDiv.removeChild(barplotDiv.firstChild);
}
// barplotDiv.appendChild(barplot);
// Scatter plot
const countMap = new Map();
filteredData.forEach(d => {
const key = `${d.type}-${d.approaches}`;
countMap.set(key, (countMap.get(key) || 0) + 1);
});
const maxApp = d3.max(filteredData, d => d.approaches) || 10;
// console.log("Y-tick labels:", filteredAggregated.map(d => d.type));
const scatterplot = Plot.plot({
title: `Distribution of types of roundabouts based on the number of approaches. There are ${data.filter(d => d.type === "Roundabout" && d.approaches === 4).length.toLocaleString()} roundabouts with four approaches.`,
x: { label: "Number of Approaches", tickSize: 0, ticks: d3.range(0, maxApp + 1), tickFormat: d => d.toFixed(0), labelOffset: 35 },
y: { label: "", domain: aggregatedArray.map(d => d.type), tickSize: 0 },
color: { scheme: "plasma", reverse: true },
marginLeft: 200,
marginBottom: 45,
height: 200,
style: "background-color: lightgray; font-size: 14px;",
marks: [
Plot.dot(filteredData, { x: "approaches", y: "type", fill: d => countMap.get(`${d.type}-${d.approaches}`), r: 5, stroke: "none", tip: {format: {x: false}} })
]
});
const scatterplotDiv = document.getElementById('scatterplot');
while (scatterplotDiv.firstChild) {
scatterplotDiv.removeChild(scatterplotDiv.firstChild);
}
const legend = Plot.legend({color: scatterplot.scale("color"), label: "Count"});
// legend.style.transform = "rotate(90deg)";
// legend.style.transformOrigin = "left top";
legend.style.backgroundColor = "transparent";
legend.style.width = "150px";
legend.style.position = "relative";
legend.style.top = "240px";
legend.style.left = "10px";
legend.style.fontSize = "14px";
scatterplotDiv.appendChild(legend);
scatterplotDiv.appendChild(scatterplot);
}
// Create table
const tableDiv = document.getElementById('table');
tableDiv.style.width = '100vw';
tableDiv.style.overflowX = 'auto';
const table = document.createElement('table');
table.id = 'dataTable';
table.style.width = '100%';
const thead = document.createElement('thead');
const headerRow = document.createElement('tr');
Object.keys(data[0]).forEach(key => {
const th = document.createElement('th');
th.textContent = key;
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);
const tbody = document.createElement('tbody');
data.slice(0, 100).forEach(row => {
const tr = document.createElement('tr');
Object.values(row).forEach(val => {
const td = document.createElement('td');
td.textContent = val;
tr.appendChild(td);
});
tbody.appendChild(tr);
});
table.appendChild(tbody);
tableDiv.appendChild(table);
// Initialize DataTable
$('#dataTable').DataTable({
pageLength: 5,
lengthMenu: [5, 10, 25]
});
updatePlots(); // initial render
});
</script>
<style>
figure {
background-color: lightgray;
margin: 0;
width: 640px;
}
figure h2 {
padding-top: 10px;
padding-left: 10px;
padding-right: 10px;
margin: 0;
margin-bottom: -10px;
font-size: 20px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-weight: 500;
}
</style>