Public
Edited
Mar 19, 2024
Insert cell
Insert cell
chart = {
// Specify the map’s dimensions and projection.
const width = 928;
const height = 581;
const projection = d3.geoAlbersUsa().scale(4 / 3 * width).translate([width / 2, height / 2]);

// Create the container SVG.
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.attr("width", width)
.attr("height", height)
.attr("style", "max-width: 100%; height: auto;");

// Create a function to calculate weighted age based on proximity to older stores.
function calculateWeightedAge(d, bins) {
const radius = 50; // Specify the radius within which to consider proximity
const nearbyStores = bins.filter(b => Math.sqrt((b.x - d.x) ** 2 + (b.y - d.y) ** 2) < radius);
const totalWeightedAge = nearbyStores.reduce((acc, store) => acc + (2024 - store.date.getFullYear()), 0);
return totalWeightedAge / nearbyStores.length; // Return the average weighted age
}

// Create the bins.
const hexbin = d3.hexbin()
.extent([[0, 0], [width, height]])
.radius(10)
.x(d => d.xy[0])
.y(d => d.xy[1]);
const bins = hexbin(walmarts.map(d => ({xy: projection([d.longitude, d.latitude]), date: d.date})))
.map(d => (d.date = new Date(d3.median(d, d => d.date)), d))
.sort((a, b) => b.length - a.length);

// Append the state mesh.
svg.append("path")
.datum(stateMesh)
.attr("fill", "none")
.attr("stroke", "#777")
.attr("stroke-width", 0.5)
.attr("stroke-linejoin", "round")
.attr("d", d3.geoPath(projection));

// Define color scale for legend.
const colorScale = d3.scaleLinear()
.domain([0, 20, 40, 60, 80, 100]) // Define age ranges
.range(['#f7fbff', '#ccece6', '#66c2a4', '#2ca25f', '#006d2c', '#00441b']); // Define corresponding colors

// Append legend.
const legend = svg.append('g')
.attr('class', 'legend')
.attr('transform', 'translate(20,20)');

const legendRectSize = 18;
const legendSpacing = 4;

const legendData = [0, 20, 40, 60, 80, 100]; // Data for legend

legend.selectAll('.legend-item')
.data(legendData)
.enter()
.append('rect')
.attr('class', 'legend-item')
.attr('width', legendRectSize)
.attr('height', legendRectSize)
.attr('x', 0)
.attr('y', (d, i) => i * (legendRectSize + legendSpacing))
.attr('fill', d => colorScale(d));

legend.selectAll('.legend-text')
.data(legendData)
.enter()
.append('text')
.attr('class', 'legend-text')
.attr('x', legendRectSize + legendSpacing)
.attr('y', (d, i) => i * (legendRectSize + legendSpacing) + legendRectSize / 2)
.text(d => `${d}%`);

// Append the circles with size based on weighted age.
svg.append("g")
.selectAll("circle")
.data(bins)
.join("circle")
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", d => Math.sqrt(d.length) * 2) // Size based on number of stores
.attr("fill", d => colorScale(calculateWeightedAge(d, bins))) // Color based on weighted age
.attr("opacity", 0.7) // Adjust opacity for better visualization
.append("title")
.text(d => `${d.length.toLocaleString()} stores\nWeighted Age: ${calculateWeightedAge(d, bins).toFixed(2)}%`);

return svg.node();
}
Insert cell
walmarts = {
const parseDate = d3.utcParse("%m/%d/%Y");
return FileAttachment("walmart.tsv").tsv()
.then(data => data.map((d) => ({
longitude: +d[0],
latitude: +d[1],
date: parseDate(d.date)
})));
}
Insert cell
stateMesh = FileAttachment("us-counties-10m.json").json().then(us => topojson.mesh(us, us.objects.states))
Insert cell
d3 = require("d3@7", "d3-hexbin@0.2")
Insert cell
import {legend} from "@d3/color-legend"
Insert cell
Insert cell
Plot.plot({
projection: "albers-usa",
color: {scheme: "Spectral"},
marks: [
Plot.geo(stateMesh, {strokeOpacity: 0.25}),
Plot.dot(walmarts, Plot.hexbin(
{fill: "median", r: "count"},
{x: "longitude", y: "latitude", binWidth: 14, fill: "date", stroke: "currentColor", strokeWidth: 0.5}
))
]
})
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