Skip to content

Instantly share code, notes, and snippets.

@dcasciotti
Last active August 29, 2015 14:10
Show Gist options
  • Save dcasciotti/637cfaccc66c0fbe2c2f to your computer and use it in GitHub Desktop.
Save dcasciotti/637cfaccc66c0fbe2c2f to your computer and use it in GitHub Desktop.
#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-BURGLARY{
fill: #F88;
stroke: #800;
background: #F88;
border-color: #800;
}
.category-ROBBERY{
fill: #0C9;
stroke: #B60;
background: #0C9;
border-color: #B60;
}
.category-LARCENY{
fill: #FF3;
stroke: #D80;
background: #FF3;
border-color: #D80;
}
.category-AGGRAVATED_ASSAULT{
fill: #0CF;
stroke: #D80;
background: #0CF;
border-color: #D80;
}
.category-MV_LARCENY{
fill: #F0F;
stroke: #D80;
background: #F0F;
border-color: #D80;
}
/*marker icons*/
.icon-AGGRAVATED_ASSAULT{
background-image: url('http://webmappingevents.com/wp-content/uploads/2014/12/police.RMS_.violence.assaultAggravated12.png');
background-repeat: no-repeat;
background-position: 0px 1px;
}
.icon-BURGLARY{
background-image:url('http://webmappingevents.com/wp-content/uploads/2014/12/police.RMS_.property.burglaryResidential252.png');
background-repeat: no-repeat;
background-position: 1px -2px;
}
.icon-LARCENY{
background-image: url('http://webmappingevents.com/wp-content/uploads/2014/12/police.RMS_.property.theftOther251.png');
background-repeat: no-repeat;
background-position: 1px -2px;
}
.icon-ROBBERY{
background-image: url('http://webmappingevents.com/wp-content/uploads/2014/12/police.RMS_.property.robberyOther252.png');
background-repeat: no-repeat;
background-position: 1px -2px;
}
.icon-MV_LARCENY{
background-image: url('http://webmappingevents.com/wp-content/uploads/2014/12/police.RMS_.property.vehicleTheft252.png');
background-repeat: no-repeat;
background-position: 1px -2px;
}
/*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" type="text/css" href="http://cdn.leafletjs.com/leaflet-0.7.2/leaflet.css" />
<link rel="stylesheet" type="text/css" href="https:////cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/0.4.0/MarkerCluster.Default.css" />
<link rel="stylesheet" type="text/css" href="clusterpies_ucr.css" />
</head>
<body>
<script src="http://cdn.leafletjs.com/leaflet-0.7.2/leaflet.js" charset="utf-8"></script>
<script src="http://leaflet.github.io/Leaflet.markercluster/dist/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 = 'points_rand.json',
categoryField = 'UCR_NAME', //This is the fieldname for marker category (used in the pie and legend)
iconField = 'UCR_NAME', //This is the fieldame for marker icon
popupFields = ['UCR_NAME','OCCUR_BEGI','REPORT_DAT'], //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([42.56, -73.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
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment