Slippy (zoomable/draggable) map and tile management, with data layer comprised of semitransparent circles. Also demonstrates an info overlay activated when the user is hovering over a data item. The visualization does not use any other library than D3.js.
See the script in action here
xxxxxxxxxx
<html lang="en">
<!-- Based on Map examples by Mike Bostock, MapBox, etc. -->
<head>
<meta charset="UTF-8">
<title>Death of Migrants</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js" charset="utf-8"></script>
<style>
body {
margin: 10px;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
#title {
margin-bottom: 10px;
}
p, a {
font-size: 12px;
line-height: 12px;
margin: 0px
margin-bottom: 10px;
}
a {
margin-left: 10px;
}
circle {
cursor: crosshair;
stroke: gray;
stroke-width: 1px;
stroke-opacity: 0.0;
fill-opacity: 0.4;
}
.cursorLocation {
padding: 3px;
position: absolute;
bottom: 10px;
left: 10px;
min-width: 300px;
min-height: 15px;
opacity: 0.5;
background-color1: white;
}
.cursorLocation label {
font-size: 12px;
font-weight1: bold;
opacity: 1;
}
.attribution {
padding: 3px;
position: absolute;
bottom: 0px;
right: 0px;
opacity: 0.5;
background-color: white;
}
.attribution label {
font-size: 10px;
font-weight: bold;
}
.attribution a {
color: #404040;
text-decoration: none;
}
.attribution a:hover {
text-decoration: underline;
}
.attribution a:link {
-webkit-tap-highlight-color: rgba(0,0,0,0);
}
#viz {
border: 1px solid gray;
overflow: scroll;
}
label {
font-size: 12px;
}
#overlay {
padding: 5px;
position: absolute;
min-width: 200px;
max-width: 400px;
top: 10px;
left: 10px;
border: 1px solid gray;
border-radius: 5px;
background-color: white;
opacity: 0.8;
display: none;
z-index: 10001;
}
#overlay p {
margin: 5px;
}
.migrantsOnly {
position: absolute;
top: 10px;
right: 10px;
}
#main {
position: absolute;
top: 10px;
left: 10px;
opacity: 0.8;
background-color: white;
border-radius: 5px;
padding-left: 20px;
padding-right: 20px;
z-index: 10000;
}
</style>
<body>
<div style="position: relative">
<div id="main">
<h3 id="title">Death of Migrants</h3>
<p><b>Source: </b><a href="https://www.themigrantsfiles.com" target="_blank">The Migrants Files</a> <a href="https://docs.google.com/spreadsheets/d/1YNqIzyQfEn4i_be2GGWESnG2Q80E_fLASffsXdCOftI/edit?usp=sharing" target="_blank">Data</a>
</div>
</div>
<div id="viz" style="position: relative"></div>
<script>
'use strict';
var width = 960,
height = 500,
mapCenter = [15, 40], // lng, lat
center,
svg,
tile,
projection,
zoom,
tileGroup,
eventGroup,
eventElements,
data,
overlay;
// get the data
d3.json("migrantDeaths.json", function(_) {
data = _;
console.log("data.length", data.length)
// remove items with missing dates;
data = data.filter(function(d) { if (d.date != "") return true; })
// convert text to numbers
data.forEach(function(d, i) {
d.latitude = +d.latitude;
d.longitude = +d.longitude;
d.Year = +d.Year;
d.dead = +d.dead;
d.missing = +d.missing;
d.dead_and_missing = +d.dead_and_missing;
d.origOrder = i;
})
// append date range to DOM
var dateRange = d3.extent(data, function(d) { return new Date(d.date) })
var earliestDate = new Date(dateRange[0]).toISOString().substring(0, 10);
var latestDate = new Date(dateRange[1]).toISOString().substring(0, 10);
d3.select("#main")
.append("p")
.style("margin-bottom", "20px")
.html("<b>Date Range:</b> " + earliestDate + " - " + latestDate + " (in this visualization)")
// sort events in decending order (so "smaller" events are drawn on top of "larger" events)
data.sort(function(a, b) { return b.dead_and_missing - a.dead_and_missing; })
console.log("data", data);
// create the map
createMap();
})
// create the map
function createMap() {
// setup tile processing
tile = d3.geo.tile().size([width, height]);
// define mercator projection
projection = d3.geo.mercator()
.scale((1 << 12) / 2 / Math.PI)
.translate([width / 2, height / 2]); // translate to center of svg
// get pixel location of map center
center = projection(mapCenter);
// define zoom behavior
zoom = d3.behavior.zoom()
.scale(projection.scale() * 2 * Math.PI)
.translate([width - center[0], height - center[1]])
.on("zoom", redraw);
// set dimension of container
d3.select("#viz")
.style("max-width", width + "px")
.style("max-height", height + "px")
// create the svg
svg = d3.select("#viz")
.append("svg")
.style("width", width + "px")
.style("height", height + "px")
//.style("border", "1px solid gray")
.call(zoom)
.on("mousemove", function() {
// get current zoom scale
var zoomScale = zoom.scale();
// get current mouse location
var location = projection.invert(d3.mouse(this));
// format location and show
var text = formatLocation(location, zoomScale);
d3.select("#cursorLocation").html("Cursor: " + text)
})
// add overlay div
overlay = d3.select("#viz")
.append("div")
.attr("id", "overlay");
overlay.append("p").attr("id", "deadText").text("Dead: X")
overlay.append("p").attr("id", "dateText").text("Date: X")
overlay.append("p").attr("id", "descriptionText").text("Event: X")
// add div attribution (as per Mapbox' requirements)
d3.select("#viz")
.append("div")
.attr("class", "attribution")
.append("label")
.html("<a href='https://www.mapbox.com/about/maps/' target='_blank'>© MapBox © OpenStreetMap</a> <a href='https://www.mapbox.com/map-feedback/'>Improve this map</a>")
// add div for current cursor location
d3.select("#viz")
.append("div")
.attr("class", "cursorLocation")
.append("label")
.attr("id", "cursorLocation")
.html("")
// append checkbox
d3.select("#viz")
.append("label")
.attr("class", "migrantsOnly")
.text("Missing migrants")
.append("input")
.attr("type", "checkbox")
.property("checked", false)
.on("change", function(d) {
var state = d3.select(this).property("checked")
showMissing(state);
})
// append tile and event groups
tileGroup = svg.append("g").attr("id", "tiles"); // tiles get filled in redraw function
eventGroup = svg.append("g").attr("id", "events");
// add events to map
eventElements = eventGroup
.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("cx", function(d) { return projection([d.longitude, d.latitude])[0] })
.attr("cy", function(d) { return projection([d.longitude, d.latitude])[1] })
.attr("r", function(d) { return calcRadius(d) })
.style("fill", function(d) { return "darkred" })
.on("mouseover", function() {
// set overlay text items
var item = d3.select(this).data()[0];
var deadText = "<b>Dead: " + item.dead_and_missing + (item.missing > 0 ? " (" + item.missing + " missing) " : "") + "</b>"
d3.select("#deadText").html(deadText);
d3.select("#dateText").html("<b>Date:</b> " + item.date);
d3.select("#descriptionText").html("<b>Event:</b> " + item.description);
// get current size of overlay
overlay.style("display", "block");
var overlayWidth = overlay.node().offsetWidth;
var overlayHeight = overlay.node().offsetHeight;
// compute position of overlay
var mouse = d3.mouse(this);
var x = mouse[0] - overlayWidth / 2;
x = (x < 0) ? 10 : (x + overlayWidth) > width ? width - overlayWidth - 10 : x; // adjust x position if needed
var y = mouse[1] - 20 - overlayHeight;
y = (y < 0) ? mouse[1] + 20 : y; // adjust y position if needed
// set position of overlay
overlay.style("left", x + "px")
overlay.style("top", y + "px")
})
.on("mouseout", function() {
overlay.style("display", "none")
});
// redraws viz after drag or zoom
function redraw() {
// get the tiles for this zoom level
var tiles = tile
.scale(zoom.scale())
.translate(zoom.translate())();
// scale and translate tile group and bind new tiles
var image = tileGroup
.attr("transform", "scale(" + tiles.scale + ")translate(" + tiles.translate + ")")
.selectAll("image")
.data(tiles, function(d) { return d; });
// remove prior tiles
image.exit().remove();
// append the new tiles
image.enter().append("image")
.attr("xlink:href", function(d) {
var url = "https://" + ["a", "b", "c", "d"][Math.random() * 4 | 0] + ".tiles.mapbox.com/v4/boeric.naal7ngd/" + d[2] + "/" + d[0] + "/" + d[1] + ".png" + "?access_token=pk.eyJ1IjoiYm9lcmljIiwiYSI6IkZEU3BSTjQifQ.XDXwKy2vBdzFEjndnE4N7Q";
return url;
})
.attr("width", 1)
.attr("height", 1)
.attr("x", function(d) { return d[0]; })
.attr("y", function(d) { return d[1]; });
// update projection
projection
.scale(zoom.scale() / 2 / Math.PI)
.translate(zoom.translate());
// update object positions
eventElements
.attr("cx", function(d) { return projection([d.longitude, d.latitude])[0] })
.attr("cy", function(d) { return projection([d.longitude, d.latitude])[1] });
}
// initial draw
redraw();
// toggles between missing and found dead migrants
// radius is set to 0 for those elements not to be shown (easier than reloading the DOM)
function showMissing(show) {
if (show) {
eventElements.attr("r", function(d) {
if (d.missing > 0) { return calcRadius(d) }
else return 0;
})
}
else eventElements.attr("r", function(d) { return calcRadius(d) });
}
// calculates the radius of each circle
function calcRadius(d) {
// scale radius to value (sqrt), then compress dynamic range (log), then scale
var r = Math.log(Math.sqrt(Math.max(d.dead_and_missing))) * 5;
// ensure minumum size
r = Math.max(3, r);
return r;
}
// formats the geo location string (zoom-in generates more decimals)
function formatLocation(p, k) {
var format = d3.format("." + Math.floor(Math.log(k) / 2 - 2) + "f");
return (p[1] < 0 ? format(-p[1]) + "°S" : format(p[1]) + " °N") + " " + (p[0] < 0 ? format(-p[0]) + "°W" : format(p[0]) + "°E");
}
} // end createMap function
// d3 tile generator
// https://github.com/d3/d3-plugins/blob/master/geo/tile/tile.js
d3.geo.tile = function() {
var size = [960, 500],
scale = 256,
translate = [size[0] / 2, size[1] / 2],
zoomDelta = 0;
function tile() {
var z = Math.max(Math.log(scale) / Math.LN2 - 8, 0),
z0 = Math.round(z + zoomDelta),
k = Math.pow(2, z - z0 + 8),
origin = [(translate[0] - scale / 2) / k, (translate[1] - scale / 2) / k],
tiles = [],
cols = d3.range(Math.max(0, Math.floor(-origin[0])), Math.max(0, Math.ceil(size[0] / k - origin[0]))),
rows = d3.range(Math.max(0, Math.floor(-origin[1])), Math.max(0, Math.ceil(size[1] / k - origin[1])));
rows.forEach(function(y) {
cols.forEach(function(x) {
tiles.push([x, y, z0]);
});
});
tiles.translate = origin;
tiles.scale = k;
return tiles;
}
tile.size = function(_) {
if (!arguments.length) return size;
size = _;
return tile;
};
tile.scale = function(_) {
if (!arguments.length) return scale;
scale = _;
return tile;
};
tile.translate = function(_) {
if (!arguments.length) return translate;
translate = _;
return tile;
};
tile.zoomDelta = function(_) {
if (!arguments.length) return zoomDelta;
zoomDelta = +_;
return tile;
};
return tile;
} // end tile function
</script>
https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js