Skip to content

Instantly share code, notes, and snippets.

@chringel21
Last active October 4, 2023 17:32
Show Gist options
  • Save chringel21/ba386d71b5d0157e2195da02a3d4453b to your computer and use it in GitHub Desktop.
Save chringel21/ba386d71b5d0157e2195da02a3d4453b to your computer and use it in GitHub Desktop.
Mapbox GL + D3 overlay

Mapbox GL and geojson overlay using D3

Display geojson data as an overlay on a Mapbox GL map using D3, and switch between geographic and topologic view. It shows the Berlin metro and underground rail system (S-Bahn and U-Bahn). This is heavily inspired by Jordi Tosts mapbox-gl-d3-playground

Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3-interpolate')) :
typeof define === 'function' && define.amd ? define(['exports', 'd3-interpolate'], factory) :
(factory((global.d3 = global.d3 || {}),global.d3));
}(this, (function (exports,d3Interpolate) { 'use strict';
var _extends = Object.assign || function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
/**
* List of params for each command type in a path `d` attribute
*/
var typeMap = {
M: ['x', 'y'],
L: ['x', 'y'],
H: ['x'],
V: ['y'],
C: ['x1', 'y1', 'x2', 'y2', 'x', 'y'],
S: ['x2', 'y2', 'x', 'y'],
Q: ['x1', 'y1', 'x', 'y'],
T: ['x', 'y'],
A: ['rx', 'ry', 'xAxisRotation', 'largeArcFlag', 'sweepFlag', 'x', 'y']
};
/**
* Convert to object representation of the command from a string
*
* @param {String} commandString Token string from the `d` attribute (e.g., L0,0)
* @return {Object} An object representing this command.
*/
function commandObject(commandString) {
// convert all spaces to commas
commandString = commandString.trim().replace(/ /g, ',');
var type = commandString[0];
var args = commandString.substring(1).split(',');
return typeMap[type.toUpperCase()].reduce(function (obj, param, i) {
// parse X as float since we need it to do distance checks for extending points
obj[param] = param === 'x' ? parseFloat(args[i]) : args[i];
return obj;
}, { type: type });
}
/**
* Converts a command object to a string to be used in a `d` attribute
* @param {Object} command A command object
* @return {String} The string for the `d` attribute
*/
function commandToString(command) {
var type = command.type;
var params = typeMap[type.toUpperCase()];
return '' + type + params.map(function (p) {
return command[p];
}).join(',');
}
/**
* Converts command A to have the same type as command B.
*
* e.g., L0,5 -> C0,5,0,5,0,5
*
* Uses these rules:
* x1 <- x
* x2 <- x
* y1 <- y
* y2 <- y
* rx <- 0
* ry <- 0
* xAxisRotation <- read from B
* largeArcFlag <- read from B
* sweepflag <- read from B
*
* @param {Object} aCommand Command object from path `d` attribute
* @param {Object} bCommand Command object from path `d` attribute to match against
* @return {Object} aCommand converted to type of bCommand
*/
function convertToSameType(aCommand, bCommand) {
var conversionMap = {
x1: 'x',
y1: 'y',
x2: 'x',
y2: 'y'
};
var readFromBKeys = ['xAxisRotation', 'largeArcFlag', 'sweepFlag'];
// convert (but ignore M types)
if (aCommand.type !== bCommand.type && bCommand.type.toUpperCase() !== 'M') {
(function () {
var aConverted = {};
Object.keys(bCommand).forEach(function (bKey) {
var bValue = bCommand[bKey];
// first read from the A command
var aValue = aCommand[bKey];
// if it is one of these values, read from B no matter what
if (aValue === undefined) {
if (readFromBKeys.includes(bKey)) {
aValue = bValue;
} else {
// if it wasn't in the A command, see if an equivalent was
if (aValue === undefined && conversionMap[bKey]) {
aValue = aCommand[conversionMap[bKey]];
}
// if it doesn't have a converted value, use 0
if (aValue === undefined) {
aValue = 0;
}
}
}
aConverted[bKey] = aValue;
});
// update the type to match B
aConverted.type = bCommand.type;
aCommand = aConverted;
})();
}
return aCommand;
}
/**
* Extends an array of commands to the length of the second array
* inserting points at the spot that is closest by X value. Ensures
* all the points of commandsToExtend are in the extended array and that
* only numPointsToExtend points are added.
*
* @param {Object[]} commandsToExtend The commands array to extend
* @param {Object[]} referenceCommands The commands array to match
* @return {Object[]} The extended commands1 array
*/
function extend(commandsToExtend, referenceCommands, numPointsToExtend) {
// map each command in B to a command in A by counting how many times ideally
// a command in A was in the initial path (see https://github.com/pbeshai/d3-interpolate-path/issues/8)
var initialCommandIndex = void 0;
if (commandsToExtend.length > 1 && commandsToExtend[0].type === 'M') {
initialCommandIndex = 1;
} else {
initialCommandIndex = 0;
}
var counts = referenceCommands.reduce(function (counts, refCommand, i) {
// skip first M
if (i === 0 && refCommand.type === 'M') {
counts[0] = 1;
return counts;
}
var minDistance = Math.abs(commandsToExtend[initialCommandIndex].x - refCommand.x);
var minCommand = initialCommandIndex;
// find the closest point by X position in A
for (var j = initialCommandIndex + 1; j < commandsToExtend.length; j++) {
var distance = Math.abs(commandsToExtend[j].x - refCommand.x);
if (distance < minDistance) {
minDistance = distance;
minCommand = j;
// since we assume sorted by X, once we find a value farther, we can return the min.
} else {
break;
}
}
counts[minCommand] = (counts[minCommand] || 0) + 1;
return counts;
}, {});
// now extend the array adding in at the appropriate place as needed
var extended = [];
var numExtended = 0;
for (var i = 0; i < commandsToExtend.length; i++) {
// add in the initial point for this A command
extended.push(commandsToExtend[i]);
for (var j = 1; j < counts[i] && numExtended < numPointsToExtend; j++) {
var commandToAdd = _extends({}, commandsToExtend[i]);
// don't allow multiple Ms
if (commandToAdd.type === 'M') {
commandToAdd.type = 'L';
} else {
// try to set control points to x and y
if (commandToAdd.x1 !== undefined) {
commandToAdd.x1 = commandToAdd.x;
commandToAdd.y1 = commandToAdd.y;
}
if (commandToAdd.x2 !== undefined) {
commandToAdd.x2 = commandToAdd.x;
commandToAdd.y2 = commandToAdd.y;
}
}
extended.push(commandToAdd);
numExtended += 1;
}
}
return extended;
}
/**
* Interpolate from A to B by extending A and B during interpolation to have
* the same number of points. This allows for a smooth transition when they
* have a different number of points.
*
* Ignores the `Z` character in paths unless both A and B end with it.
*
* @param {String} a The `d` attribute for a path
* @param {String} b The `d` attribute for a path
*/
function interpolatePath(a, b) {
// remove Z, remove spaces after letters as seen in IE
var aNormalized = a == null ? '' : a.replace(/[Z]/gi, '').replace(/([MLCSTQAHV])\s*/gi, '$1');
var bNormalized = b == null ? '' : b.replace(/[Z]/gi, '').replace(/([MLCSTQAHV])\s*/gi, '$1');
var aPoints = aNormalized === '' ? [] : aNormalized.split(/(?=[MLCSTQAHV])/gi);
var bPoints = bNormalized === '' ? [] : bNormalized.split(/(?=[MLCSTQAHV])/gi);
// if both are empty, interpolation is always the empty string.
if (!aPoints.length && !bPoints.length) {
return function nullInterpolator() {
return '';
};
}
// if A is empty, treat it as if it used to contain just the first point
// of B. This makes it so the line extends out of from that first point.
if (!aPoints.length) {
aPoints.push(bPoints[0]);
// otherwise if B is empty, treat it as if it contains the first point
// of A. This makes it so the line retracts into the first point.
} else if (!bPoints.length) {
bPoints.push(aPoints[0]);
}
// convert to command objects so we can match types
var aCommands = aPoints.map(commandObject);
var bCommands = bPoints.map(commandObject);
// extend to match equal size
var numPointsToExtend = Math.abs(bPoints.length - aPoints.length);
if (numPointsToExtend !== 0) {
// B has more points than A, so add points to A before interpolating
if (bCommands.length > aCommands.length) {
aCommands = extend(aCommands, bCommands, numPointsToExtend);
// else if A has more points than B, add more points to B
} else if (bCommands.length < aCommands.length) {
bCommands = extend(bCommands, aCommands, numPointsToExtend);
}
}
// commands have same length now.
// convert A to the same type of B
aCommands = aCommands.map(function (aCommand, i) {
return convertToSameType(aCommand, bCommands[i]);
});
var aProcessed = aCommands.map(commandToString).join('');
var bProcessed = bCommands.map(commandToString).join('');
// if both A and B end with Z add it back in
if ((a == null || a[a.length - 1] === 'Z') && (b == null || b[b.length - 1] === 'Z')) {
aProcessed += 'Z';
bProcessed += 'Z';
}
var stringInterpolator = d3Interpolate.interpolateString(aProcessed, bProcessed);
return function pathInterpolator(t) {
// at 1 return the final value without the extensions used during interpolation
if (t === 1) {
return b == null ? '' : b;
}
return stringInterpolator(t);
};
}
exports.interpolatePath = interpolatePath;
Object.defineProperty(exports, '__esModule', { value: true });
})));
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Berlin S-Bahn and U-Bahn with Mapbox GL and D3</title>
<!-- Mapbox GL -->
<link href="https://api.tiles.mapbox.com/mapbox-gl-js/v0.37.0/mapbox-gl.css" rel="stylesheet" />
<script src="https://api.tiles.mapbox.com/mapbox-gl-js/v0.37.0/mapbox-gl.js"></script>
<!-- D3 -->
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="d3-interpolate-path.js" charset="utf-8"></script>
<script src="https://d3js.org/queue.v1.min.js"></script>
<link rel="stylesheet" href="style.css">
</head>
<body>
<button id="toggle-view" name="toggle-view" onclick="toggleViews()">Toggle View</button>
<div id="map"></div>
<script type="text/javascript">
var view = "map";
//
// Mapbox stuff
//Mapbox initialization
mapboxgl.accessToken = 'pk.eyJ1IjoiZGVlZ2dlIiwiYSI6ImNqM2Jmb29wYjAwN3kycXFrcW03YWlzdXAifQ.lRrvz11b3PTPopgJ888MMQ'; //public key
var map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/dark-v9',
zoom: 9.5,
center: [13.4026, 52.5100]
});
//
//Mapbox + D3 connection
//Get mapbox map canvas container
var canvas = map.getCanvasContainer();
//Overlay D3 on the map
var svg = d3.select(canvas).append("svg");
//Projection function
var transform = d3.geoTransform({point:projectPoint});
var path = d3.geoPath().projection(transform);
//Load data
queue()
.defer(d3.json, "berlin_s_u_bahn.geojson")
.await(drawData);
//Project geojson coordinate to the map's current state
function project(d) {
return map.project(new mapboxgl.LngLat(+d[0], +d[1]));
}
//Project any point to map's current state
function projectPoint(lon, lat) {
var point = map.project(new mapboxgl.LngLat(lon, lat));
this.stream.point(point.x, point.y);
}
//
// D3 stuff
//Draw geojson data with d3
var lines;
var tooltip = d3.select('body')
.append('div')
.attr('class', 'hidden tooltip');
//Function for drawing the data
function drawData(err, data1) {
geojsonData = data1
lines = svg.selectAll("path")
.data(geojsonData.features)
.enter()
.append("path")
.attr("class", function(d) {return d.properties.routes_r_1;})
.attr("d", path)
.on('mousemove', function(d) {
var mouse = d3.mouse(svg.node()).map(function(d) {
return parseInt(d);
});
tooltip.classed('hidden', false)
.attr('style', 'left:' + (mouse[0] + 15) +
'px; top:' + (mouse[1] - 35) + 'px')
.html(d.properties.routes_r_1);
})
.on('mouseout', function() {
tooltip.classed('hidden', true);;
});
update(500);
map.on("viewreset", update);
map.on("move", update);
map.on("moveend", update);
}
//update D3 shapes' positions to the map's current state
function update() {
console.log("update");
if (view === "map") {
lines.attr("d", path);
} else if (view === "grid") {
lines.attr("d", function(d) { return d.properties.cartogram_geom});
}
//stops.attr("cx", function(d) { return project(d.geometry.coordinates).x })
// .attr("cy", function(d) { return project(d.geometry.coordinates).y });
}
// Toggle function
function toggle(transitionTime) {
var windowWidth = Math.max(document.documentElement.clientWidth, window.innerWidth || 0),
windowHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0)
console.log(windowWidth, windowHeight)
// Default value = 0
transitionTime = (typeof transitionTime !== 'undefined') ? transitionTime : 0;
// Map view
if (view === "map") {
console.log(geojsonData)
svg.attr("viewBox", "0 0 " + windowWidth + " " + windowHeight)
.selectAll("path")
.data(geojsonData.features)
.attr("d", path)
.transition()
.duration(transitionTime)
.attrTween("d", function(d) {
var previous = d.properties.cartogram_geom;
var current = d3.select(this).attr("d");
return d3.interpolatePath(previous, current);
})
// Grid view
} else if (view === "grid") {
svg.attr("preserveAspectRatio", "xMinYMin meet")
.attr("width", windowWidth)
.attr("height", windowHeight)
.attr("viewBox", "0 40 1920 1080")
.selectAll("path")
.transition()
.duration(transitionTime)
.attrTween("d", function(d) {
var previous = d3.select(this).attr("d");
var current = d.properties.cartogram_geom;
return d3.interpolatePath(previous, current);
})
}
}
function setMapOpacity(value) {
d3.selectAll(".mapboxgl-canvas")
.transition()
.duration(500)
.style("opacity", value);
d3.selectAll(".mapboxgl-control-container")
.transition()
.duration(500)
.style("opacity", value);
}
function showMap() {
setMapOpacity(1);
// Enable map interaction
map.doubleClickZoom.enable();
map.scrollZoom.enable();
map.dragPan.enable();
}
function hideMap() {
setMapOpacity(0.1);
// Disable map interaction
map.doubleClickZoom.disable();
map.scrollZoom.disable();
map.dragPan.disable();
}
//
//Toggle views
//
function toggleViews() {
// Toggle active view
if (view == "map") {
view = "grid";
hideMap();
} else if (view == "grid") {
view = "map";
showMap();
}
toggle(2000);
}
</script>
</body>
</html>
@media screen {
body {
margin:0;
padding:0;
}
#map {
position:absolute;
top:0;
bottom:0;
width:100%;
}
svg {
position: absolute;
width: 100%;
height: 100%;
}
path {
stroke: #e55e5e;
stroke-width: 4;
stroke-opacity: 0;
fill: none;
cursor: pointer;
transition: 0.5s fill, 0.5s stroke-width;
}
path:hover {
stroke-width: 8;
}
circle {
fill: #ffffff;
stroke: #000000;
stroke-width: 1;
cursor: pointer;
transition: 0.5s fill, 0.5s stroke-width;
}
circle:hover {
fill: #F8FF7B;
stroke-width: 2;
}
.S1 {
stroke: #EF49A1;
stroke-opacity: 1;
}
.S2 {
stroke: #005B28;
stroke-opacity: 1;
}
.S25 {
stroke: #005B28;
stroke-opacity: 1;
}
.S3 {
stroke: #074A96;
stroke-opacity: 1;
}
.S41 {
stroke: #A93B1F;
stroke-opacity: 1;
}
.S42 {
stroke: #A93B1F;
stroke-opacity: 1;
}
.S45 {
stroke: #C9843A;
stroke-opacity: 1;
}
.S46 {
stroke: #C9843A;
stroke-opacity: 1;
}
.S47 {
stroke: #C9843A;
stroke-opacity: 1;
}
.S5 {
stroke: #FF5B03;
stroke-opacity: 1;
}
.S7 {
stroke: #764C9A;
stroke-opacity: 1;
}
.S75 {
stroke: #764C9A;
stroke-opacity: 1;
}
.S8 {
stroke: #4EA425;
stroke-opacity: 1;
}
.S85 {
stroke: #4EA425;
stroke-opacity: 1;
}
.S9 {
stroke: #950A2F;
stroke-opacity: 1;
}
.U1 {
stroke: #7DAD4C;
stroke-opacity: 1;
}
.U2 {
stroke: #DA421E;
stroke-opacity: 1;
}
.U3 {
stroke: #16683D;
stroke-opacity: 1;
}
.U4 {
stroke: #F0D722;
stroke-opacity: 1;
}
.U5 {
stroke: #7E5330;
stroke-opacity: 1;
}
.U55 {
stroke: #7E5330;
stroke-opacity: 1;
}
.U6 {
stroke: #8C6DAB;
stroke-opacity: 1;
}
.U7 {
stroke: #528DBA;
stroke-opacity: 1;
}
.U8 {
stroke: #224F86;
stroke-opacity: 1;
}
.U9 {
stroke: #F3791D;
stroke-opacity: 1;
}
.hidden {
display: none;
}
div.tooltip {
color: #222;
background-color: #fff;
padding: .5em;
text-shadow: #f5f5f5 0 1px 0;
border-radius: 0;
opacity: 0.9;
position: absolute;
font: normal 14px/1.3 Arial;
}
#toggle-view {
position: fixed;
left: 0px;
top: 50%;
margin-top: -50px;
z-index: 9;
border: none;
appearance: none;
cursor: pointer;
display: block;
width: 100px;
height: 100px;
outline: none;
font: 18px/1.3 Arial;
font-weight: bold;
background-color: #33839c;
color: white;
transition: 0.5s all;
}
#toggle-view:hover {
background-color: #3b9bb9;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment