I am trying to use some user defined svg icons for markers on leaflet but I think the whole task gets too heavy for my browser.
Until now I was using L.circleMarker
but I now have to use markers like asterisks, arrows, stars etc instead so I decided to do them as svg path and then plug them in instead of my circleMarkers. To make things more complicated I have more than 300K points. With the circleMarkers I was able to make a workable chart, not lightning fast but quite acceptable especially when a fairly deep zoom was used to be able to distinguish individual points (otherwise everything was like a big blob and useless to study).
With the svg markers however the chart becomes so computationally heavy that the browser just hangs. I have played around with 100, 1000 and 10000 points and even with 1000 points the difference becomes apparent. Is there any solution to this please, has anyone used svg markers with lots of data points? I think canvas is properly employed in my code, especially for the circleMarkers
but I might be mistaken. Any help highly appreciated. Code in the snippet, comment/uncomment the few lines towards the bottom:
return L.circleMarker(p, style(feature));
or
console.log("Starting markers.")
return L.marker(p, {
renderer: myRenderer,
icon: makeIcon('6-pointed-star', style(feature).color),
});
to switch from the circleMarkers
to svg
markers.
Many thanks!
PS. With svg markers the code breaks with the highlight event but I have quite understood whats wrong..it works fine with circleMarkers
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Chart</title>
<style>
#tooltip {
position:absolute;
background-color: #2B292E;
color: white;
font-family: sans-serif;
font-size: 15px;
pointer-events: none; /*dont trigger events on the tooltip*/
padding: 15px 20px 10px 20px;
text-align: center;
opacity: 0;
border-radius: 4px;
}
html, body {
height: 100%;
margin: 0;
}
#map {
width: 600px;
height: 600px;
}
</style>
<!-- Reference style.css -->
<!-- <link rel="stylesheet" type="text/css" href="style.css">-->
<!-- Reference minified version of D3 -->
<script src='https://d3js.org/d3.v4.min.js' type='text/javascript'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'></script>
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
</head>
<body>
<div id="map"></div>
<script>
var data = [];
var NumOfPoints = 100
for (let i = 0; i < NumOfPoints; i++) {
data.push({
num: i,
x: Math.random(),
y: Math.random(),
year: Math.floor(100*Math.random())
})
}
renderChart(data);
function make_dots(data) {
var arr = [];
var nest = d3.nest()
.key(function (d) {
return Math.floor(d.year / 10);;
})
.entries(data);
for (var k = 0; k < nest.length; ++k) {
arr[k] = helper(nest[k].values);
}
return arr;
}
function helper(data) {
dots = {
type: "FeatureCollection",
features: []
};
for (var i = 0; i < data.length; ++i) {
x = data[i].x;
y = data[i].y;
var g = {
"type": "Point",
"coordinates": [x, y]
};
//create feature properties
var p = {
"id": i,
"popup": "Dot_" + i,
"year": parseInt(data[i].year),
"size": 30 // Fixed size
};
//create features with proper geojson structure
dots.features.push({
"geometry": g,
"type": "Feature",
"properties": p
});
}
return dots;
}
//////////////////////////////////////////////////////////////////////////////////////////////
//styling and displaying the data as circle markers//
//////////////////////////////////////////////////////////////////////////////////////////////
//create color ramp
function getColor(y) {
return y > 90 ? '#6068F0' :
y > 80 ? '#6B64DC' :
y > 70 ? '#7660C9' :
y > 60 ? '#815CB6' :
y > 50 ? '#8C58A3' :
y > 40 ? '#985490' :
y > 30 ? '#A3507C' :
y > 20 ? '#AE4C69' :
y > 10 ? '#B94856' :
y > 0 ? '#C44443' :
'#D04030';
}
//calculate radius so that resulting circles will be proportional by area
function getRadius(y) {
r = Math.sqrt(y / Math.PI)
return r;
}
// This is very important! Use a canvas otherwise the chart is too heavy for the browser when
// the number of points is too high, as in this case where we have around 300K points to plot
var myRenderer = L.canvas({
padding: 0.5
});
//create style, with fillColor picked from color ramp
function style(feature) {
return {
radius: getRadius(feature.properties.size),
fillColor: getColor(feature.properties.year),
color: "#000",
weight: 0,
opacity: 1,
fillOpacity: 0.9,
renderer: myRenderer
};
}
//create highlight style, with darker color and larger radius
function highlightStyle(feature) {
return {
radius: getRadius(feature.properties.size) + 1.5,
fillColor: "#FFCE00",
color: "#FFCE00",
weight: 1,
opacity: 1,
fillOpacity: 0.9
};
}
//attach styles and popups to the marker layer
function highlightDot(e) {
var layer = e.target;
dotStyleHighlight = highlightStyle(layer.feature);
layer.setStyle(dotStyleHighlight);
if (!L.Browser.ie && !L.Browser.opera) {
layer.bringToFront();
}
}
function resetDotHighlight(e) {
var layer = e.target;
dotStyleDefault = style(layer.feature);
layer.setStyle(dotStyleDefault);
}
function onEachDot(feature, layer) {
layer.on({
mouseover: highlightDot,
mouseout: resetDotHighlight
});
var popup = '<table style="width:110px"><tbody><tr><td><div><b>Marker:</b></div></td><td><div>' + feature.properties.popup +
'</div></td></tr><tr class><td><div><b>Group:</b></div></td><td><div>' + feature.properties.year +
'</div></td></tr><tr><td><div><b>X:</b></div></td><td><div>' + feature.geometry.coordinates[0] +
'</div></td></tr><tr><td><div><b>Y:</b></div></td><td><div>' + feature.geometry.coordinates[1] +
'</div></td></tr></tbody></table>'
layer.bindPopup(popup);
}
function makeIcon(name, color) {
if (name == "diamond") {
// here's the SVG for the marker
var icon = "<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='20' height='20'> " +
"<path stroke=" + "'" + color + "'" + " stroke-width='3' fill='none' " +
" d='M10,1 5,10 10,19, 15,10Z'/></svg>";
}
// Based on http://www.smiffysplace.com/stars.html
if (name == "6-pointed-star") {
// here's the SVG for the marker
var icon = "<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='28' height='28'> " +
"<path stroke=" + "'" + color + "'" + " stroke-width='3' fill='none' " +
" d='m13 13m0 5l5 3.6599999999999966l-0.6700000000000017 -6.159999999999997l5.670000000000002 -2.5l-5.670000000000002 -2.5l0.6700000000000017 -6.159999999999997l-5 3.6599999999999966l-5 -3.6599999999999966l0.6700000000000017 6.159999999999997l-5.670000000000002 2.5l5.670000000000002 2.5l-0.6700000000000017 6.159999999999997z'/></svg>";
}
// here's the trick, base64 encode the URL
var svgURL = "data:image/svg+xml;base64," + btoa(icon);
// create icon
var svgIcon = L.icon({
iconUrl: svgURL,
iconSize: [20, 20],
shadowSize: [12, 10],
iconAnchor: [5, 5],
popupAnchor: [5, -5]
});
return svgIcon
}
function renderChart(data) {
var myDots = make_dots(data);
var minZoom = 0,
maxZoom = 15;
var map = L.map('map', {
minZoom: minZoom,
maxZoom: maxZoom
}).setView([0.5, 0.5], 10);
L.tileLayer("http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
continuousWorld: false,
minZoom: 0,
noWrap: true
}).addTo(map);
var myRenderer = L.canvas({
padding: 0.5
});
// Define an array to keep layerGroups
var dotlayer = [];
//create marker layer and display it on the map
for (var i = 0; i < myDots.length; i += 1) {
dotlayer[i] = L.geoJson(myDots[i], {
pointToLayer: function (feature, latlng) {
var p = latlng;
// return L.circleMarker(p, style(feature));
console.log("Starting markers.")
return L.marker(p, {
renderer: myRenderer,
icon: makeIcon('6-pointed-star', style(feature).color),
});
},
onEachFeature: onEachDot
}).addTo(map);
}
var cl = L.control.layers(null, {}).addTo(map);
for (j = 0; j < dotlayer.length; j += 1) {
var name = "Group " + j + "0-" + j + "9";
cl.addOverlay(dotlayer[j], name);
}
}
</script>
</body>
</html>
Your svg 6-point markers are not rendered by the Canvas. Have a look with the DevTools and you see they are img
tags with a base-64 encoded svg as source. If you have a large number of markers this will slow down the HTML renderer.
The CircleMarkers are rendered on the canvas.
By creating a new L.Path
subclass you can draw any marker you want on the canvas and let leaflet do what it does best. Make these leaflet modifications before any other JS code otherwise it will complain that it is not a constructor.
L.Canvas.include({
_updateMarker6Point: function (layer) {
if (!this._drawing || layer._empty()) { return; }
var p = layer._point,
ctx = this._ctx,
r = Math.max(Math.round(layer._radius), 1);
this._drawnLayers[layer._leaflet_id] = layer;
ctx.beginPath();
ctx.moveTo(p.x + r , p.y );
ctx.lineTo(p.x + 0.43*r, p.y + 0.25 * r);
ctx.lineTo(p.x + 0.50*r, p.y + 0.87 * r);
ctx.lineTo(p.x , p.y + 0.50 * r);
ctx.lineTo(p.x - 0.50*r, p.y + 0.87 * r);
ctx.lineTo(p.x - 0.43*r, p.y + 0.25 * r);
ctx.lineTo(p.x - r, p.y );
ctx.lineTo(p.x - 0.43*r, p.y - 0.25 * r);
ctx.lineTo(p.x - 0.50*r, p.y - 0.87 * r);
ctx.lineTo(p.x , p.y - 0.50 * r);
ctx.lineTo(p.x + 0.50*r, p.y - 0.87 * r);
ctx.lineTo(p.x + 0.43*r, p.y - 0.25 * r);
ctx.closePath();
this._fillStroke(ctx, layer);
}
});
var Marker6Point = L.CircleMarker.extend({
_updatePath: function () {
this._renderer._updateMarker6Point(this);
}
});
You use it the same as for the circleMarker
return new Marker6Point(p, style(feature));
In the code there are 2 L.canvas
instances and there are 2 variables myRenderer
. I have kept the global variable but assign it only when there is an L.map() constructed in the renderChart()
function.
For the demo I have used a larger area with markers and used 10000 markers. My browser has no problems with that. I have increased the size
in the properties to 500 so we get a marker with radius 13 pixels, so you can see the star clearly.
I have used the latest leaflet version 1.3.3.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Chart</title>
<style>
#tooltip {
position:absolute;
background-color: #2B292E;
color: white;
font-family: sans-serif;
font-size: 15px;
pointer-events: none; /*dont trigger events on the tooltip*/
padding: 15px 20px 10px 20px;
text-align: center;
opacity: 0;
border-radius: 4px;
}
html, body {
height: 100%;
margin: 0;
}
#map {
width: 600px;
height: 600px;
}
</style>
<!-- Reference style.css -->
<!-- <link rel="stylesheet" type="text/css" href="style.css">-->
<!-- Reference minified version of D3 -->
<script src='https://d3js.org/d3.v4.min.js' type='text/javascript'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'></script>
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
</head>
<body>
<div id="map"></div>
<script>
L.Canvas.include({
_updateMarker6Point: function (layer) {
if (!this._drawing || layer._empty()) { return; }
var p = layer._point,
ctx = this._ctx,
r = Math.max(Math.round(layer._radius), 1);
this._drawnLayers[layer._leaflet_id] = layer;
ctx.beginPath();
ctx.moveTo(p.x + r , p.y );
ctx.lineTo(p.x + 0.43*r, p.y + 0.25 * r);
ctx.lineTo(p.x + 0.50*r, p.y + 0.87 * r);
ctx.lineTo(p.x , p.y + 0.50 * r);
ctx.lineTo(p.x - 0.50*r, p.y + 0.87 * r);
ctx.lineTo(p.x - 0.43*r, p.y + 0.25 * r);
ctx.lineTo(p.x - r, p.y );
ctx.lineTo(p.x - 0.43*r, p.y - 0.25 * r);
ctx.lineTo(p.x - 0.50*r, p.y - 0.87 * r);
ctx.lineTo(p.x , p.y - 0.50 * r);
ctx.lineTo(p.x + 0.50*r, p.y - 0.87 * r);
ctx.lineTo(p.x + 0.43*r, p.y - 0.25 * r);
ctx.closePath();
this._fillStroke(ctx, layer);
}
});
var Marker6Point = L.CircleMarker.extend({
_updatePath: function () {
this._renderer._updateMarker6Point(this);
}
});
var data = [];
var NumOfPoints = 10000;
for (let i = 0; i < NumOfPoints; i++) {
data.push({
num: i,
x: Math.random()*60,
y: Math.random()*60,
year: Math.floor(100*Math.random())
})
}
renderChart(data);
function make_dots(data) {
var arr = [];
var nest = d3.nest()
.key(function (d) {
return Math.floor(d.year / 10);
})
.entries(data);
for (var k = 0; k < nest.length; ++k) {
arr[k] = helper(nest[k].values);
}
return arr;
}
function helper(data) {
dots = {
type: "FeatureCollection",
features: []
};
for (var i = 0; i < data.length; ++i) {
x = data[i].x;
y = data[i].y;
var g = {
"type": "Point",
"coordinates": [x, y]
};
//create feature properties
var p = {
"id": i,
"popup": "Dot_" + i,
"year": parseInt(data[i].year),
"size": 500 // Fixed size circle radius=~13
};
//create features with proper geojson structure
dots.features.push({
"geometry": g,
"type": "Feature",
"properties": p
});
}
return dots;
}
//////////////////////////////////////////////////////////////////////////////////////////////
//styling and displaying the data as circle markers//
//////////////////////////////////////////////////////////////////////////////////////////////
//create color ramp
function getColor(y) {
return y > 90 ? '#6068F0' :
y > 80 ? '#6B64DC' :
y > 70 ? '#7660C9' :
y > 60 ? '#815CB6' :
y > 50 ? '#8C58A3' :
y > 40 ? '#985490' :
y > 30 ? '#A3507C' :
y > 20 ? '#AE4C69' :
y > 10 ? '#B94856' :
y > 0 ? '#C44443' :
'#D04030';
}
//calculate radius so that resulting circles will be proportional by area
function getRadius(y) {
r = Math.sqrt(y / Math.PI)
return r;
}
// This is very important! Use a canvas otherwise the chart is too heavy for the browser when
// the number of points is too high, as in this case where we have around 300K points to plot
var myRenderer;
// = L.canvas({
// padding: 0.5
// });
//create style, with fillColor picked from color ramp
function style(feature) {
return {
radius: getRadius(feature.properties.size),
fillColor: getColor(feature.properties.year),
color: "#000",
weight: 0,
opacity: 1,
fillOpacity: 0.9,
renderer: myRenderer
};
}
//create highlight style, with darker color and larger radius
function highlightStyle(feature) {
return {
radius: getRadius(feature.properties.size) + 1.5,
fillColor: "#FFCE00",
color: "#FFCE00",
weight: 1,
opacity: 1,
fillOpacity: 0.9
};
}
//attach styles and popups to the marker layer
function highlightDot(e) {
var layer = e.target;
dotStyleHighlight = highlightStyle(layer.feature);
layer.setStyle(dotStyleHighlight);
if (!L.Browser.ie && !L.Browser.opera) {
layer.bringToFront();
}
}
function resetDotHighlight(e) {
var layer = e.target;
dotStyleDefault = style(layer.feature);
layer.setStyle(dotStyleDefault);
}
function onEachDot(feature, layer) {
layer.on({
mouseover: highlightDot,
mouseout: resetDotHighlight
});
var popup = '<table style="width:110px"><tbody><tr><td><div><b>Marker:</b></div></td><td><div>' + feature.properties.popup +
'</div></td></tr><tr class><td><div><b>Group:</b></div></td><td><div>' + feature.properties.year +
'</div></td></tr><tr><td><div><b>X:</b></div></td><td><div>' + feature.geometry.coordinates[0] +
'</div></td></tr><tr><td><div><b>Y:</b></div></td><td><div>' + feature.geometry.coordinates[1] +
'</div></td></tr></tbody></table>'
layer.bindPopup(popup);
}
function makeIcon(name, color) {
if (name == "diamond") {
// here's the SVG for the marker
var icon = "<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='20' height='20'> " +
"<path stroke=" + "'" + color + "'" + " stroke-width='3' fill='none' " +
" d='M10,1 5,10 10,19, 15,10Z'/></svg>";
}
// Based on http://www.smiffysplace.com/stars.html
if (name == "6-pointed-star") {
// here's the SVG for the marker
var icon = "<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='28' height='28'> " +
"<path stroke=" + "'" + color + "'" + " stroke-width='3' fill='none' " +
" d='m13 13m0 5l5 3.66l-0.67 -6.16l5.67 -2.5l-5.67 -2.5l0.67 -6.16l-5 3.66l-5 -3.66l0.67 6.16l-5.67 2.5l5.67 2.5l-0.67 6.16z'/></svg>";
}
// here's the trick, base64 encode the URL
var svgURL = "data:image/svg+xml;base64," + btoa(icon);
// create icon
var svgIcon = L.icon({
iconUrl: svgURL,
iconSize: [20, 20],
shadowSize: [12, 10],
iconAnchor: [5, 5],
popupAnchor: [5, -5]
});
return svgIcon
}
function renderChart(data) {
var myDots = make_dots(data);
var minZoom = 0,
maxZoom = 15;
var map = L.map('map', {
minZoom: minZoom,
maxZoom: maxZoom
}).setView([0.5, 0.5], 5);
L.tileLayer("http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
continuousWorld: false,
minZoom: 0,
noWrap: true
}).addTo(map);
myRenderer = L.canvas({ padding: 0.5 });
// Define an array to keep layerGroups
var dotlayer = [];
//create marker layer and display it on the map
for (var i = 0; i < myDots.length; i += 1) {
dotlayer[i] = L.geoJson(myDots[i], {
pointToLayer: function (feature, latlng) {
var p = latlng;
// return L.circleMarker(p, style(feature));
// console.log("Starting markers.")
// return L.marker(p, {
// renderer: myRenderer,
// icon: makeIcon('6-pointed-star', style(feature).color),
// });
return new Marker6Point(p, style(feature));
},
onEachFeature: onEachDot
}).addTo(map);
}
var cl = L.control.layers(null, {}).addTo(map);
for (j = 0; j < dotlayer.length; j += 1) {
var name = "Group " + j + "0-" + j + "9";
cl.addOverlay(dotlayer[j], name);
}
}
</script>
</body>
</html>
If this is not fast enough you can always set up your own tile server and bake the markers into the tiles at the different zoom levels (in transparent PNG files). And use this as a separate tile layer on top of the terrain tiles. You don't have easy popups but the rendering of 300K markers is pretty fast. You can make a tile overlay for each layer/group you want to show. All serviced from the same tile server.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With