Skip to content

Instantly share code, notes, and snippets.

@gisminister
Last active April 11, 2024 18:52
Show Gist options
  • Save gisminister/10001728 to your computer and use it in GitHub Desktop.
Save gisminister/10001728 to your computer and use it in GitHub Desktop.
Markercluster pie charts

The map shows traffic accidents recorded in Oslo, Norway, for the year 2013.

The Leaflet Markercluster plugin is wonderful. Since the markerclusters are divIcons you can put whatever you want inside them using the iconCreateFunction. I wanted my clusters to reveal more information than just the marker count and figured a pie chart would do the job. So I told the iconCreateFunction to do some D3 magic and this is the result.

The example is a bit more complicated than necessary due to how my dataset is structured. But if you take a look at the defineClusterIcon() function you'll see that I use d3.nest() to build a dataset for the pie chart based on a given property from all the cluster's children. Then I pass this dataset over to the bakeThePie() function together with instructions on how to style the chart. The function returns svg markup which in turn is placed inside the divIcon.

Feel free to suggest improvements.

#map {
width:960px;
height:500px;
}
#legend {
position: absolute;
top: 3px;
left: 780px;
margin: 10px;
padding: 5px;
border-radius: 5px;
z-index: 100;
font-size: 1em;
font-family: sans-serif;
width: 165px;
background: rgba(255,255,255,0.6);
}
.legendheading {
position: relative;
height: 25px;
padding: 5px 2px 0px 2px;
font-size: larger;
font-weight: bold;
}
.legenditem {
padding: 2px;
margin-bottom: 2px;
}
/*Marker clusters*/
.marker-cluster-pie g.arc{
fill-opacity: 0.5;
}
.marker-cluster-pie-label {
font-size: 14px;
font-weight: bold;
font-family: sans-serif;
}
/*Markers*/
.marker {
width: 18px;
height: 18px;
border-width: 2px;
border-radius:10px;
margin-top: -10px;
margin-left: -10px;
border-style: solid;
fill: #CCC;
stroke: #444;
background: #CCC;
border-color: #444;
}
.marker div{
text-align: center;
font-size: 14px;
font-weight: bold;
font-family: sans-serif;
}
/*marker categories*/
.category-1{
fill: #F88;
stroke: #800;
background: #F88;
border-color: #800;
}
.category-2{
fill: #FA0;
stroke: #B60;
background: #FA0;
border-color: #B60;
}
.category-3{
fill: #FF3;
stroke: #D80;
background: #FF3;
border-color: #D80;
}
.category-4{
fill: #BFB;
stroke: #070;
background: #BFB;
border-color: #070;
}
.category-5{
fill: #9DF;
stroke: #007;
background: #9DF;
border-color: #007;
}
.category-6{
fill: #CCC;
stroke: #444;
background: #CCC;
border-color: #444;
}
/*marker icons*/
.icon-1{
background-image: url('running-16.png');
background-repeat: no-repeat;
background-position: 0px 1px;
}
.icon-2{
background-image: url('bicycle-16.png');
background-repeat: no-repeat;
background-position: 1px 0px;
}
.icon-3{
background-image: url('motorcycle-16.png');
background-repeat: no-repeat;
background-position: 1px 0px;
}
.icon-4{
background-image: url('car-16.png');
background-repeat: no-repeat;
background-position: 1px 0px;
}
/*Popup*/
.map-popup span.heading {
display: block;
font-size: 1.2em;
font-weight: bold;
}
.map-popup span.attribute {
display: block;
}
.map-popup span.label {
font-weight: bold;
}
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<title>ClusterPies</title>
<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.7.2/leaflet.css" />
<link href='https://api.mapbox.com/mapbox.js/plugins/leaflet-markercluster/v0.4.0/MarkerCluster.css' rel='stylesheet' />
<link rel="stylesheet" href="clusterpies.css" />
</head>
<body>
<script src="http://cdn.leafletjs.com/leaflet-0.7.2/leaflet.js" charset="utf-8"></script>
<script src='https://api.mapbox.com/mapbox.js/plugins/leaflet-markercluster/v0.4.0/leaflet.markercluster.js'></script>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<div id="container">
<div id="map" />
</div>
<script>
"use strict"
var geojson,
metadata,
geojsonPath = 'traffic_accidents.geojson',
categoryField = '5074', //This is the fieldname for marker category (used in the pie and legend)
iconField = '5065', //This is the fieldame for marker icon
popupFields = ['5065','5055','5074'], //Popup will display these fields
tileServer = 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
tileAttribution = 'Map data: <a href="http://openstreetmap.org">OSM</a>',
rmax = 30, //Maximum radius for cluster pies
markerclusters = L.markerClusterGroup({
maxClusterRadius: 2*rmax,
iconCreateFunction: defineClusterIcon //this is where the magic happens
}),
map = L.map('map').setView([59.95, 10.78], 8);
//Add basemap
L.tileLayer(tileServer, {attribution: tileAttribution, maxZoom: 15}).addTo(map);
//and the empty markercluster layer
map.addLayer(markerclusters);
//Ready to go, load the geojson
d3.json(geojsonPath, function(error, data) {
if (!error) {
geojson = data;
metadata = data.properties;
var markers = L.geoJson(geojson, {
pointToLayer: defineFeature,
onEachFeature: defineFeaturePopup
});
markerclusters.addLayer(markers);
map.fitBounds(markers.getBounds());
map.attributionControl.addAttribution(metadata.attribution);
renderLegend();
} else {
console.log('Could not load data...');
}
});
function defineFeature(feature, latlng) {
var categoryVal = feature.properties[categoryField],
iconVal = feature.properties[iconField];
var myClass = 'marker category-'+categoryVal+' icon-'+iconVal;
var myIcon = L.divIcon({
className: myClass,
iconSize:null
});
return L.marker(latlng, {icon: myIcon});
}
function defineFeaturePopup(feature, layer) {
var props = feature.properties,
fields = metadata.fields,
popupContent = '';
popupFields.map( function(key) {
if (props[key]) {
var val = props[key],
label = fields[key].name;
if (fields[key].lookup) {
val = fields[key].lookup[val];
}
popupContent += '<span class="attribute"><span class="label">'+label+':</span> '+val+'</span>';
}
});
popupContent = '<div class="map-popup">'+popupContent+'</div>';
layer.bindPopup(popupContent,{offset: L.point(1,-2)});
}
function defineClusterIcon(cluster) {
var children = cluster.getAllChildMarkers(),
n = children.length, //Get number of markers in cluster
strokeWidth = 1, //Set clusterpie stroke width
r = rmax-2*strokeWidth-(n<10?12:n<100?8:n<1000?4:0), //Calculate clusterpie radius...
iconDim = (r+strokeWidth)*2, //...and divIcon dimensions (leaflet really want to know the size)
data = d3.nest() //Build a dataset for the pie chart
.key(function(d) { return d.feature.properties[categoryField]; })
.entries(children, d3.map),
//bake some svg markup
html = bakeThePie({data: data,
valueFunc: function(d){return d.values.length;},
strokeWidth: 1,
outerRadius: r,
innerRadius: r-10,
pieClass: 'cluster-pie',
pieLabel: n,
pieLabelClass: 'marker-cluster-pie-label',
pathClassFunc: function(d){return "category-"+d.data.key;},
pathTitleFunc: function(d){return metadata.fields[categoryField].lookup[d.data.key]+' ('+d.data.values.length+' accident'+(d.data.values.length!=1?'s':'')+')';}
}),
//Create a new divIcon and assign the svg markup to the html property
myIcon = new L.DivIcon({
html: html,
className: 'marker-cluster',
iconSize: new L.Point(iconDim, iconDim)
});
return myIcon;
}
/*function that generates a svg markup for the pie chart*/
function bakeThePie(options) {
/*data and valueFunc are required*/
if (!options.data || !options.valueFunc) {
return '';
}
var data = options.data,
valueFunc = options.valueFunc,
r = options.outerRadius?options.outerRadius:28, //Default outer radius = 28px
rInner = options.innerRadius?options.innerRadius:r-10, //Default inner radius = r-10
strokeWidth = options.strokeWidth?options.strokeWidth:1, //Default stroke is 1
pathClassFunc = options.pathClassFunc?options.pathClassFunc:function(){return '';}, //Class for each path
pathTitleFunc = options.pathTitleFunc?options.pathTitleFunc:function(){return '';}, //Title for each path
pieClass = options.pieClass?options.pieClass:'marker-cluster-pie', //Class for the whole pie
pieLabel = options.pieLabel?options.pieLabel:d3.sum(data,valueFunc), //Label for the whole pie
pieLabelClass = options.pieLabelClass?options.pieLabelClass:'marker-cluster-pie-label',//Class for the pie label
origo = (r+strokeWidth), //Center coordinate
w = origo*2, //width and height of the svg element
h = w,
donut = d3.layout.pie(),
arc = d3.svg.arc().innerRadius(rInner).outerRadius(r);
//Create an svg element
var svg = document.createElementNS(d3.ns.prefix.svg, 'svg');
//Create the pie chart
var vis = d3.select(svg)
.data([data])
.attr('class', pieClass)
.attr('width', w)
.attr('height', h);
var arcs = vis.selectAll('g.arc')
.data(donut.value(valueFunc))
.enter().append('svg:g')
.attr('class', 'arc')
.attr('transform', 'translate(' + origo + ',' + origo + ')');
arcs.append('svg:path')
.attr('class', pathClassFunc)
.attr('stroke-width', strokeWidth)
.attr('d', arc)
.append('svg:title')
.text(pathTitleFunc);
vis.append('text')
.attr('x',origo)
.attr('y',origo)
.attr('class', pieLabelClass)
.attr('text-anchor', 'middle')
//.attr('dominant-baseline', 'central')
/*IE doesn't seem to support dominant-baseline, but setting dy to .3em does the trick*/
.attr('dy','.3em')
.text(pieLabel);
//Return the svg-markup rather than the actual element
return serializeXmlNode(svg);
}
/*Function for generating a legend with the same categories as in the clusterPie*/
function renderLegend() {
var data = d3.entries(metadata.fields[categoryField].lookup),
legenddiv = d3.select('body').append('div')
.attr('id','legend');
var heading = legenddiv.append('div')
.classed('legendheading', true)
.text(metadata.fields[categoryField].name);
var legenditems = legenddiv.selectAll('.legenditem')
.data(data);
legenditems
.enter()
.append('div')
.attr('class',function(d){return 'category-'+d.key;})
.classed({'legenditem': true})
.text(function(d){return d.value;});
}
/*Helper function*/
function serializeXmlNode(xmlNode) {
if (typeof window.XMLSerializer != "undefined") {
return (new window.XMLSerializer()).serializeToString(xmlNode);
} else if (typeof xmlNode.xml != "undefined") {
return xmlNode.xml;
}
return "";
}
</script>
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.
@cnlane
Copy link

cnlane commented Apr 8, 2020

Awesome example! It's just what a needed to show storm severity for multiple rain gauges. Thank you so much!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment