This block uses d3-geo-scale-bar with d3-zoom to make a map with a scale bar that updates as the user zooms in and out.
<!DOCTYPE html>
<html>
<head>
<style>
body {
display: table;
margin: 0 auto;
}
svg {
overflow: visible;
}
.state {
fill: #eee;
stroke: #ccc;
}
</style>
</head>
<body>
<script src="https://d3js.org/d3.v6.min.js"></script>
<script src="https://unpkg.com/topojson-client@3"></script>
<script src="https://unpkg.com/d3-geo-scale-bar@1"></script>
<script>
const projection = d3.geoAlbers();
const path = d3.geoPath(projection);
const scaleBar = d3.geoScaleBar()
.projection(projection)
.left(.1)
.top(.9)
.units(d3.geoScaleMiles)
.tickFormat(d => d)
.zoomClamp(false);
const zoom = d3.zoom()
.scaleExtent([1, 20])
.on("zoom", event => {
const { transform } = event;
g.attr("transform", transform);
scaleBar.zoomFactor(transform.k);
bar.call(scaleBar);
});
const svg = d3.select("body").append("svg");
const g = svg.append("g");
const bar = svg.append("g");
d3.json("ne_50m_admin_1_states_provinces_lakes.json")
.then(data => {
const geojson = topojson.feature(data, data.objects.ne_50m_admin_1_states_provinces_lakes);
const states = g.selectAll(".state")
.data(geojson.features)
.enter().append("path")
.attr("class", "state");
draw();
addEventListener("resize", draw);
function draw(){
const width = Math.min(innerWidth, 800);
const height = width * .63;
projection.fitSize([width, height], geojson);
scaleBar.size([width, height]);
zoom.translateExtent([[0, 0], [width, height]]);
svg
.attr("width", width)
.attr("height", height)
.call(zoom);
bar.call(scaleBar);
states
.attr("d", path)
.on("mouseover", function(){
d3.select(this).style("stroke", "black").raise();
})
.on("mouseout", function(){
d3.select(this).style("stroke", "#ccc");
});
}
});
</script>
</body>
</html>