Skip to content

Instantly share code, notes, and snippets.

@seutje
Last active October 8, 2015 13:19
Show Gist options
  • Save seutje/77e7e85b1e88016d0dd0 to your computer and use it in GitHub Desktop.
Save seutje/77e7e85b1e88016d0dd0 to your computer and use it in GitHub Desktop.
Example map pin clustering based on PX distance.
var foo = {
init: function(i, el) {
this.wrapper = el;
this.baseMarkerRadius = 2.5; // Base size of a cluster.
this.markerRadiusExpand = 1.5; // Added radius based on cluster size (e.g. 4 in a cluster, radius = 2.5 + 4 * 1.5)
this.clusterDistance = 10; // Distance in pixels for clustering.
this.offset = 268435456; // Half the circumference of the earth at zoom level 21 and in pixels.
this.radius = this.offset / Math.PI; // Used for clustering logic.
var that = this; // Scope us a reference.
var width = 940,
height = 620;
var projection = d3.geo.mercator()
.scale((width) / 2 / Math.PI)
.center([0, 45]) // lol arbitrary center point.
.translate([width / 2, height / 2])
.precision(0);
var path = d3.geo.path()
.projection(projection);
that.projection = projection;
var svg = d3.select(el).append('svg')
.attr('width', width)
.attr('height', height);
that.svg = svg;
},
addPins: function() {
var that = this,
projection = that.projection,
svg = that.svg;
d3.json('/some/path', function(error, data) {
if (error) throw error;
var clustered = that.cluster.bind(that)(data.events, that.clusterDistance, 2);
svg.selectAll('.event-pin')
.data(clustered)
.enter()
.append('circle')
.attr('class', 'event-pin')
.attr('r', function(d) {
return that.baseMarkerRadius + (d.length * that.markerRadiusExpand);
})
.attr('transform', function(d) {
return 'translate(' + projection([d[0].long, d[0].lat]) + ')';
})
.attr('style', 'cursor: pointer;')
.on('click', function(d) {
svg.selectAll('.map-tooltip').remove();
svg.append('foreignObject')
.attr('width', 250)
.attr('height', 200)
.attr('class', 'map-tooltip')
.attr('transform', function() {
var proj = projection([d[0].long, d[0].lat]);
proj[0] += 30;
proj[1] -= 175;
if (proj[1] < 0) {
proj[1] = 0;
}
return 'translate(' + proj + ')';
})
.append('xhtml:div')
.attr('class', 'map-tooltip-wrapper')
.append('xhtml:div')
.attr('class', 'map-tooltip-inner')
.html(function() {
var content = '';
for (var i = 0, max = d.length; i < max; i++) {
content += d[i].event;
}
return content;
});
svg.selectAll('.tooltip-arrow').remove();
svg.append('path')
.attr('d', lineFunction(lineData))
.attr('class', 'tooltip-arrow')
.attr('transform', function() {
var proj = projection([d[0].long, d[0].lat]);
proj[1] -= 20;
return 'translate(' + proj + ')';
});
});
});
},
cluster: function(markers, distance, zoom) {
var clustered = [],
marker,
target,
cluster,
pixels;
for (var i = 0, max = markers.length; i < max; i++) {
marker = markers[i];
cluster = [];
if (!marker['done']) {
marker['done'] = true;
for (var j = 0, end = markers.length; j < end; j++) {
if (markers[j] !== markers[i] && !markers[j]['done']) {
target = markers[j];
pixels = this.pixelDistance(marker['lat'], marker['long'], target['lat'], target['long'], zoom);
if (distance > pixels) {
target['done'] = true;
cluster.push(target);
}
}
}
cluster.push(marker);
clustered.push(cluster);
}
}
return clustered;
},
lonToX: function(lon) {
return Math.round(this.offset + this.radius * lon * Math.PI / 180);
},
latToY: function(lat) {
return Math.round(this.offset - this.radius * Math.log((1 + Math.sin(lat * Math.PI / 180)) / (1 - Math.sin(lat * Math.PI / 180))) / 2);
},
pixelDistance: function (lat1, lon1, lat2, lon2, zoom) {
var x1 = this.lonToX(lon1),
y1 = this.latToY(lat1),
x2 = this.lonToX(lon2),
y2 = this.latToY(lat2);
return Math.sqrt(Math.pow((x1 - x2), 2) + Math.pow((y1 - y2), 2)) >> (21 - zoom);
}
}
@seutje
Copy link
Author

seutje commented Oct 8, 2015

An obvious flaw here is that it clusters towards the first pin it finds instead of to the middle of all the pins in that cluster.

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