Published
Edited
Jan 2, 2021
Insert cell
md`# Leaftlet + D3.js Map, zoom- and data-dependent map display

I am showing line information on a map but because of the high amount of data (in the real data set, this is a miniature one) I want only lines representing pipes with diameter higher or equal to 12" when the zoom is smaller than 15 and all pipes, regardless of diameter, if zoom is greater or equal to 15. However, when I increase the zoom to 15 the extra lines that were supposed to appear do not and the zoom function stops working altogether. I'm pretty sure the problem is with my enter-update-exit logic, but I don't know what it is. Any ideas? Thanks!`
Insert cell
L = require('leaflet@1.5.0')
Insert cell
html`<link href='${resolve('leaflet@1.5.0/dist/leaflet.css')}' rel='stylesheet' />`
Insert cell
d3 = require("d3@6")
Insert cell
md`# Leaflet map div container`
Insert cell
container = { return DOM.element('div', { style: `width:${width}px;height:${width/1.6}px` });}
Insert cell
md`## Simplyfied map class`
Insert cell
function createMap(map_id, zoomControl, map_center) {
const map = {};

map.map_id = map_id;

let Esri_WorldGrayCanvas = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}', { // Can get others from https://leaflet-extras.github.io/leaflet-providers/preview/
attribution: 'Tiles &copy; Esri &mdash; Esri, DeLorme, NAVTEQ',
maxZoom: 16
});

var Stamen_TopOSMRelief = L.tileLayer('https://stamen-tiles-{s}.a.ssl.fastly.net/toposm-color-relief/{z}/{x}/{y}.{ext}', {
attribution: 'Map tiles by <a href="http://stamen.com">Stamen Design</a>, <a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a> &mdash; Map data &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
subdomains: 'abcd',
minZoom: 0,
maxZoom: 20,
ext: 'jpg',
bounds: [[22, -132], [51, -56]]
});

var Esri_WorldImagery = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: 'Tiles &copy; Esri &mdash; Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'
});

map.map = L.map(container, {
layers: [
Stamen_TopOSMRelief,
Esri_WorldImagery,
Esri_WorldGrayCanvas
],
center: map_center,
zoom: 11,
zoomControl: zoomControl
}); // initializes the map, sets zoom & coordinates

let baseMaps = {
'Stamen_TopOSMRelief': Stamen_TopOSMRelief,
'Esri_WorldImagery': Esri_WorldImagery,
'Esri_WorldGrayCanvas': Esri_WorldGrayCanvas
};
L.control.layers(baseMaps).addTo(map.map);

map.drawMap = function(data, diameter) {
// Initialize svg to add to map
L.svg({clickable:true}).addTo(map.map);
// Create selection using D3
const overlay = d3.select(map.map.getPanes().overlayPane);
const svg = overlay.select('svg').attr("pointer-events", "auto");
// create a group that is hidden during zooming
const g = svg.append('g').attr('class', 'leaflet-zoom-hide');
// Use Leaflets projection API for drawing svg path (creates a stream of projected points)
const projectPoint = function(x, y) {
const point = map.map.latLngToLayerPoint(new L.LatLng(y, x));
this.stream.point(point.x, point.y);
}
// Use d3's custom geo transform method to implement the above
const projection = d3.geoTransform({point: projectPoint});
// creates geopath from projected points (SVG)
const pathCreator = d3.geoPath().projection(projection) ;
let myColor = map.colorInterpolateInteger(data.features, diameter);
let init_diameter = function(d) {
return d.properties[diameter] / 3 + 'px'
}
//map.tooltip = create_tooltip(overlay);

function filterPipes(data) {
let isZommed = map.map.getZoom() >= 15;
return data.features.filter(d => isZommed || d.properties[diameter] >= 12);
}
let filteredData = filterPipes(data)
const lines_bound = g.selectAll('path').data(filteredData).join('path')
drawPipeSelection(lines_bound)

function drawPipeSelection(selection) {
// console.log(sel, selection.data())
selection = selection
.attr('class', 'pipe')
.attr('d', pathCreator)
.attr("stroke-width", d => init_diameter(d))
.attr("stroke", d => myColor(d.properties[diameter]))
.attr("fill", "none")
.attr("style", "pointer-events: auto;")
.on("mouseover", function(e, d) {
d3.select(e.target).attr('stroke-width', '5px');
map.map.dragging.disable();
map.tooltip.tooltipMouseover(e, d);
})
.on('mouseout', function(e, d) {
d3.select(e.target)
.attr('stroke-width', d => init_diameter(d));
map.map.dragging.enable();
map.tooltip.tooltipMouseout(e, d);
});
}

function onZoom() {
console.log('==== NEW ZOOM ===', map.map.getZoom());
filteredData = filterPipes(data);
lines_bound.data(filteredData)
.join(
// enter => drawPipeSelection(enter),
function(enter) {
enter.attr('class', 'pipe')
.attr('d', pathCreator)
.attr("stroke-width", d => init_diameter(d))
.attr("stroke", d => myColor(d.properties[diameter]))
.attr("fill", "none")
.attr("style", "pointer-events: auto;")
.on("mouseover", function(e, d) {
d3.select(e.target).attr('stroke-width', '5px');
map.map.dragging.disable();
map.tooltip.tooltipMouseover(e, d);
})
.on('mouseout', function(e, d) {
d3.select(e.target)
.attr('stroke-width', d => init_diameter(d));
map.map.dragging.enable();
map.tooltip.tooltipMouseout(e, d);
})
},
update => drawPipeSelection(update),
exit => exit.remove()
)
}
// initialize positioning
onZoom();
// // reset whenever map is moved
map.map.on('zoomend', onZoom);
map.map.on('moveend', onZoom);
}

map.colorInterpolateInteger = function(data, param) {
let all_data = data.map(d => d.properties[param])
let unique_data = all_data.filter((v, i, a) => a.indexOf(v) === i);
unique_data = unique_data.sort(function(a, b){return a - b})
if (param.substring(0, 4).toLowerCase() === 'diam')
unique_data = unique_data.filter(x => x > 5.9);
return d3.scaleOrdinal().domain(unique_data).range(d3.schemeYlGnBu[unique_data.length]);
}
map.colorInteger = function(param, data) {
let myColor = map.colorInterpolateInteger(data, param);
d3.select('#map' + map.map_id)
.selectAll('.pipe')
.attr("stroke", d => myColor(d['properties'][param]));
map.legendInteger(data, param);
}

return map;
}
Insert cell
md`## The function below draws the map`
Insert cell
function mapCenter(data) {
let x_max = d3.max(data.features.map(d => d.geometry.coordinates[0][1]));
let x_min = d3.min(data.features.map(d => d.geometry.coordinates[0][1]));
let y_max = d3.max(data.features.map(d => d.geometry.coordinates[0][0]));
let y_min = d3.min(data.features.map(d => d.geometry.coordinates[0][0]));
return [
(x_max + x_min) / 2,
(y_max + y_min) / 2
]
}
Insert cell
mapCenter(test_pipe_data[0])
Insert cell
function drawPipes(data) {
// Get columns containing pipe diameters
let diameter = null;
if (Object.keys(data.features[0].properties).includes('DIAMETER')) {
diameter = 'DIAMETER';
} else if (Object.keys(data.features[0].properties).includes('diameter')) {
diameter = 'diameter';
}

// Calculate map center
let map_center = mapCenter(data);

let map1 = createMap(1, true, map_center);
map1.drawMap(data, diameter);
}
Insert cell
md`## Data in GeoJSON`
Insert cell
test_pipe_data = [{
"type": "FeatureCollection",
"name": "ithaca_pipes_test",
"crs": {
"type": "name",
"properties": {
"name": "urn:ogc:def:crs:OGC:1.3:CRS84"
}
},
"features": [
{
"type": "Feature",
"properties": {
"id": 1,
"diameter": 12,
"material": "CI",
"inst_year": 1950
},
"geometry": {
"type": "LineString",
"coordinates":
[
[
-76.476090116279039,
42.441242732558145
],
[
-76.481758720930202,
42.441133720930239
],
[
-76.483938953488334,
42.441678779069775
],
[
-76.485356104651132,
42.441642441860473
],
[
-76.485210755813924,
42.440370639534891
],
[
-76.487354651162761,
42.440261627906985
],
[
-76.487390988372056,
42.440734011627917
],
[
-76.508321220930199,
42.440188953488381
]
]
}
},
{
"type": "Feature",
"properties": {
"id": 2,
"diameter": 12,
"material": "CI",
"inst_year": 1950
},
"geometry": {
"type": "LineString",
"coordinates":
[
[
-76.477507267441823,
42.441242732558145
],
[
-76.47605377906973,
42.443277616279076
],
[
-76.475763081395314,
42.445239825581403
],
[
-76.485101744186011,
42.445239825581403
]
]
}
},
{
"type": "Feature",
"properties": {
"id": 3,
"diameter": 8,
"material": "DI",
"inst_year": 1960
},
"geometry": {
"type": "LineString",
"coordinates":
[
[
-76.475726744186019,
42.445276162790705
],
[
-76.47409156976741,
42.445276162790705
],
[
-76.471875,
42.445530523255819
],
[
-76.471947674418573,
42.447638081395354
]
]
}
},
{
"type": "Feature",
"properties": {
"id": 4,
"diameter": 8,
"material": "DI",
"inst_year": 1960
},
"geometry": {
"type": "LineString",
"coordinates":
[
[
-76.482594476744154,
42.4453125
],
[
-76.48288517441857,
42.45061773255815
]
]
}
},
{
"type": "Feature",
"properties": {
"id": 5,
"diameter": 8,
"material": "DI",
"inst_year": 1960
},
"geometry": {
"type": "LineString",
"coordinates":
[
[
-76.495675872092988,
42.4405523255814
],
[
-76.4958938953488,
42.444985465116289
]
]
}
},
{
"type": "Feature",
"properties": {
"id": 6,
"diameter": 8,
"material": "DI",
"inst_year": 1965
},
"geometry": {
"type": "LineString",
"coordinates":
[
[
-76.496838662790665,
42.4405523255814
],
[
-76.497056686046477,
42.444985465116289
]
]
}
},
{
"type": "Feature",
"properties": {
"id": 7,
"diameter": 6,
"material": "DI",
"inst_year": 1962
},
"geometry": {
"type": "LineString",
"coordinates":
[
[
-76.4958938953488,
42.445021802325591
],
[
-76.496366279069733,
42.451998546511639
]
]
}
},
{
"type": "Feature",
"properties": {
"id": 8,
"diameter": 6,
"material": "AC",
"inst_year": 1970
},
"geometry": {
"type": "LineString",
"coordinates":
[
[
-76.497129360465081,
42.444985465116289
],
[
-76.497492732558101,
42.452071220930243
]
]
}
},
{
"type": "Feature",
"properties": {
"id": 8,
"diameter": 6,
"material": "AC",
"inst_year": 1963
},
"geometry": {
"type": "LineString",
"coordinates":
[
[
-76.496947674418578,
42.442623546511633
],
[
-76.505813953488342,
42.442441860465124
]
]
}
},
{
"type": "Feature",
"properties": {
"id": 9,
"diameter": 6,
"material": "AC",
"inst_year": 1975
},
"geometry": {
"type": "LineString",
"coordinates":
[
[
-76.49912790697671,
42.442587209302332
],
[
-76.49978197674416,
42.453343023255819
]
]
}
},
{
"type": "Feature",
"properties": {
"id": 10,
"diameter": 6,
"material": "AC",
"inst_year": 1976
},
"geometry": {
"type": "LineString",
"coordinates":
[
[
-76.497420058139497,
42.450726744186056
],
[
-76.499600290697643,
42.450654069767452
]
]
}
}
]
}
]

Insert cell
md`# Calls function to create map`
Insert cell
drawPipes(test_pipe_data[0]);
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more