Skip to content

Instantly share code, notes, and snippets.

@sarah37
Last active March 14, 2019 11:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sarah37/17796e35d845d500f55d8a4255f4777b to your computer and use it in GitHub Desktop.
Save sarah37/17796e35d845d500f55d8a4255f4777b to your computer and use it in GitHub Desktop.
Zoomable Dot Map
location lat lon
China 35.86166 104.195397
Australia -25.274398 133.775136
France 44.734828 0.408447
The Netherlands 52.120831 4.51657
North Atlantic Ocean 48.978078 -24.896729
China 23.12911 113.264385
USA 42.2842 -74.375914
New Zealand -38.059426 175.437557
USA 33.233858 -84.054441
Brazil -14.54628 -52.794109
USA 39.400183 -101.291831
Zimbabwe -19.015438 29.154857
USA 40.680428 -122.370842
Argentina -38.416097 -63.616672
Vietnam 10.103407 105.501645
India 20.593684 78.96288
Ethiopia 9.145 40.489673
Japan 35.961719 137.546605
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Zoomable dot map</title>
<link rel="stylesheet" href="style.css">
<script src="https://d3js.org/d3.v5.min.js"></script>
</head>
<body>
<div id="mapDiv"></div>
<script type="text/javascript" src="map.js"></script>
</body>
</html>
// set width and height
var w = 960
var h = 500
// create svg
var svg = d3.select("#mapDiv")
.append("svg")
.attr("width", w)
.attr("height", h);
// initial scale and translation (makes mercator projection fit screen)
scaleInit = h/(2*Math.PI) * 1.3
transInit = [w/2, h/2]
// define projection
var projection = d3.geoMercator()
.scale(scaleInit)
.translate(transInit)
// define path generator
var path = d3.geoPath()
.projection(projection);
// g's for different parts of the map
var mapG = svg.append("g")
var dotG = svg.append("g")
// load world map geojson
d3.json("https://gist.githubusercontent.com/sarah37/dcca42b936545d9ee9f0bc8052e03dbd/raw/550cfee8177df10e515d82f7eb80bce4f72c52de/world-110m.json").then(function(world) {
mapG
.append("path")
.attr("d", path(world))
.classed("land", true)
// load dataset for dots
d3.csv('example_locations.csv').then(function(data) {
// initialise zoom
// has to be within recall of dataset because "zoomEnd" function triggers reloading the dots
var zoom = d3.zoom()
.on("start", zoomStart)
.on("zoom", zooming)
.on("end", zoomEnd)
svg.call(zoom)
updateDots()
function zoomStart() {
dotG.classed("hidden", true)
}
function zooming() {
// zoom map
mapG.style("stroke-width", 1.5 / d3.event.transform.k + "px");
mapG.attr("transform", d3.event.transform);
}
function zoomEnd() {
dotG.classed("hidden", false)
// update projection
projection
.translate([d3.event.transform.x + d3.event.transform.k*transInit[0], d3.event.transform.y + d3.event.transform.k*transInit[1]])
.scale(d3.event.transform.k * scaleInit)
// re-plot dots
updateDots()
}
function updateDots() {
// get current bbox
var bbox = getBoundingBox()
// filter data for the visible dots only
var visible = data.filter(function(d) {
return filterBBox(bbox, d.lon, d.lat)
})
// bind new data to circles
var circle = dotG
.selectAll(".dot")
.data(visible)
// remove surplus circles
circle.exit().remove()
// add new ones
circle
.enter()
.append("circle")
.classed("dot", true)
.merge(circle)
.attr("cx", function(d) {
d.loc = projection([d.lon, d.lat])
return d.loc[0]
})
.attr("cy", function(d) {return d.loc[1]})
.attr("r", 3)
}
})
.catch(function(error){
throw error;
})
})
.catch(function(error){
throw error;
})
function getBoundingBox() {
// computes bounding box of currently visible part of the map in terms of lat/lon
var mapBounds = mapG.node().getBBox()
var mapTrans = d3.zoomTransform(svg.node())
var l = mapTrans.x + mapBounds.x * mapTrans.k
var r = l + mapBounds.width * mapTrans.k
var t = mapTrans.y + mapBounds.y * mapTrans.k
var b = t + mapBounds.height * mapTrans.k
// compare to bounds of svg (visible part)
l = (l > 0 ? -180 : projection.invert([0, NaN])[0])
r = (r < w ? 180 : projection.invert([w, NaN])[0])
t = (t > 0 ? 85 : projection.invert([NaN, 0])[1])
b = (b < h ? -85 : projection.invert([NaN, h])[1])
return {l: l, r: r, t: t, b: b}
}
function filterBBox(bbox, lon, lat) {
// check if point at lon/lat is within the visible part of the map
return (bbox.l < lon && lon < bbox.r && bbox.b < lat && lat < bbox.t)
}
.land {
fill: #eee;
stroke: #bbb;
}
circle {
fill: #D94726;
}
.hidden {
display: none;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment