chart = {
const width = 928;
const height = 581;
const projection = d3.geoAlbersUsa().scale(4 / 3 * width).translate([width / 2, height / 2]);
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.attr("width", width)
.attr("height", height)
.attr("style", "max-width: 100%; height: auto;");
function calculateWeightedAge(d, bins) {
const radius = 50;
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;
}
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);
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();
}