Skip to content

Instantly share code, notes, and snippets.

@Andrew-Reid
Last active October 13, 2016 01:17
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save Andrew-Reid/7e2096170ae87ce85ee0455fe959e376 to your computer and use it in GitHub Desktop.
Basic Map Scale Bar

This is an attempt at a basic scale that could be easily modified. As it stands, it could be dropped into any map. The method used to create a round distance to base the scale off of is just a first attempt, there are likely much better ways to arrive at similar or better outcomes.

At it's most basic, this example aims to make a scale bar 1/4 the width of the map. It begins by measuring the distance between two points on either side of the center of the map (along the x axis), which are 1/4 of the width of the map apart. Then, it adjusts pixel distance between those points, a little at a time, until the physical distance is a nice round number. Once this is achieved, then the pixel distance is used to set the scale width, while the physical distance is used to label the scale.

As scale varies across a projection, the center is the most logical point to measure as it is likely the most representative area to measure.

The scale is based on the distance calculator found at Moveable Type. In both this calculator and the eventual scale there is what I've only seen to be a tolerable degree of error. The distance calculator uses a spherical earth (the equitorial radius of the earth is only 0.3% more than the polar radius), while the indicated scale distance is cleaned by rounding off excess significant digits, which is why it attempts to find a distance to represent that is relatively round prior to rounding.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<style>
svg {
background: #9ecae1;
}
.boundary {
fill:none;
stroke: white;
}
.land {
fill: #41ab5d;
}
.scaleText {
font-family: Arial;
font-size: 14px;
}
</style>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-geo-projection.v1.min.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
</head>
<body>
<script type="text/javascript">
var width = 960,
height = 500;
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var projection = d3.geoConicEqualArea()
projection.rotate([-132,0]).center([0,-28]).parallels([-18,-36]).scale(750);
var path = d3.geoPath().projection(projection);
var g = svg.append("g");
d3.json("world.json", function(error, world) {
g.insert("path", ".land")
.datum(topojson.feature(world, world.objects.land))
.attr("class", "land")
.attr("d", path);
g.insert("path", ".boundary")
.datum(topojson.mesh(world, world.objects.countries, function(a, b) { return a !== b; }))
.attr("class", "boundary")
.attr("d", path);
// Start Scale ---------------------------------------------------------
// baseWidth refers to ideal scale width on the screen it also is the width of the initial measurement point
var baseWidth = width / 4;
var p1 = projection.invert([width/2 - baseWidth/2, height / 2]);
var p2 = projection.invert([width/2 + baseWidth/2, height / 2]);
var distance = getDistance(p1,p2);
var unit = "m";
var multiply = 1;
var bestFit = 1;
var increment = 0.1; // This could be scaled to map width maybe width/10000;
var scaleDistance = 0;
var scaleWidth = 0;
if ( distance > 1000 ) {
unit = "km"; multiply = 0.001;
}
// Adjust distance to a round(er) number
var i = 0;
while (i < 400) {
var temp = getDistance( projection.invert([ width/2 - (baseWidth / 2) + (increment * i), height / 2 ]), projection.invert([ width/2 + baseWidth/2 - (increment * i), height / 2 ]));
var ratio = temp / temp.toPrecision(1);
// If the second distance is moving away from a cleaner number, reverse direction.
if (i == 1) {
if (Math.abs(1 - ratio) > bestFit) { increment = - increment; }
}
// If we are moving away from a best fit after that, break
else if (i > 2) {
if (Math.abs(1 - ratio) > bestFit) { break }
}
// See if the current distance is the cleanest number
if (Math.abs(1-ratio) < bestFit) {
bestFit = Math.abs(1 - ratio);
scaleDistance = temp;
scaleWidth = (baseWidth) - (2 * increment * i);
}
i++;
}
// Now to build the scale
var bars = [];
var smallBars = 10;
var bigBars = 4;
var odd = true;
var label = false;
// Populate an array to represent the bars on the scale
for (i = 0; i < smallBars; i++) {
if (smallBars - 1 > i ) { label = false; } else { label = true; }
bars.push( {width: 1 / (smallBars * (bigBars + 1)), offset: i / (smallBars * (bigBars + 1)), label: label, odd: odd } );
odd = !odd;
}
for (i = 0; i < bigBars; i++) {
bars.push( {width: 1 / (bigBars + 1), offset: (i + 1) / (bigBars + 1), label: true, odd: odd } );
odd = !odd;
}
// Append the scale
g.selectAll(".scaleBar")
.data(bars).enter()
.append("rect")
.attr("x", function(d) { return d.offset * scaleWidth + 20 })
.attr("y", height - 30)
.attr("width", function(d) { return d.width * scaleWidth})
.attr("height", 10)
.attr("fill", function (d) { if (d.odd) { return "#eee"; } else { return "#222"; } });
g.selectAll(".scaleText")
.data(bars).enter()
.filter( function (d) { return d.label == true })
.append("text")
.attr("class","scaleText")
.attr("x",0)
.attr("y",0)
.style("text-anchor","start")
.text(function(d) { return d3.format(",")(((d.offset + d.width) * scaleDistance).toPrecision(2) * multiply); })
.attr("transform", function(d) { return "translate("+ ((d.offset + d.width) * scaleWidth + 20 )+","+ (height - 35) +") rotate(-45)" });
g.append("text")
.attr("x", scaleWidth/2 + 20)
.attr("y", height - 5)
.text( function() { if(unit == "km") { return "kilometers"; } else { return "metres";} })
.style("text-anchor","middle")
.attr("class","scaleText");
// End Scale -----------------------------------------
});
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/* Latitude/longitude spherical geodesy tools (c) Chris Veness 2002-2016 */
/* MIT Licence */
/* www.movable-type.co.uk/scripts/latlong.html */
/* www.movable-type.co.uk/scripts/geodesy/docs/module-latlon-spherical.html */
function getDistance(p1,p2) {
var lat1 = p1[1];
var lat2 = p2[1];
var lon1 = p1[0];
var lon2 = p2[0];
var R = 6371e3; // metres
var φ1 = lat1* Math.PI / 180;
var φ2 = lat2* Math.PI / 180;
var Δφ = (lat2-lat1)* Math.PI / 180;
var Δλ = (lon2-lon1)* Math.PI / 180;
var a = Math.sin(Δφ/2) * Math.sin(Δφ/2) +
Math.cos(φ1) * Math.cos(φ2) *
Math.sin(Δλ/2) * Math.sin(Δλ/2);
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
var distance = R * c;
return distance;
}
</script>
</body>
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment