|
<!doctype html> |
|
<html> |
|
<head> |
|
<title>Many many many Points with D3 and leaflet</title> |
|
<meta charset="utf-8"> |
|
|
|
<style> |
|
body { |
|
font-size: 12px; |
|
} |
|
|
|
text { |
|
fill: white; |
|
} |
|
|
|
svg { |
|
position: relative; |
|
} |
|
|
|
path { |
|
fill: #ffd800; |
|
fill-opacity: .2; |
|
} |
|
|
|
path:hover { |
|
fill: brown; |
|
fill-opacity: .7; |
|
} |
|
|
|
#map { |
|
position: absolute; |
|
height: 100%; |
|
width: 100%; |
|
background-color: #333; |
|
|
|
} |
|
|
|
|
|
</style> |
|
|
|
</head> |
|
<body> |
|
<div id="map"></div> |
|
<div id="tooltip"> |
|
<svg xmlns="http://www.w3.org/2000/svg" width="100px" height="100px" /> |
|
</div> |
|
|
|
<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.7.2/leaflet.css" /> |
|
<script src="http://cdn.leafletjs.com/leaflet-0.7.2/leaflet.js"></script> |
|
|
|
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script> |
|
|
|
|
|
<script> |
|
d3.csv('testDataCZ.csv', function (error, incidents) { |
|
|
|
function reformat(array) { |
|
var data = []; |
|
array.map(function (d, i) { |
|
|
|
data.push({ |
|
id: i, |
|
type: "Feature", |
|
geometry: { |
|
coordinates: [+d.longitude, +d.latitude], |
|
type: "Point" |
|
} |
|
|
|
|
|
}); |
|
}); |
|
return data; |
|
} |
|
var geoData = { type: "FeatureCollection", features: reformat(incidents) }; |
|
|
|
|
|
|
|
var qtree = d3.geom.quadtree(geoData.features.map(function (data, i) { |
|
return { |
|
x: data.geometry.coordinates[0], |
|
y: data.geometry.coordinates[1], |
|
all: data |
|
}; |
|
} |
|
) |
|
); |
|
|
|
|
|
// Find the nodes within the specified rectangle. |
|
function search(quadtree, x0, y0, x3, y3) { |
|
var pts = []; |
|
var subPixel = false; |
|
var subPts = []; |
|
var scale = getZoomScale(); |
|
console.log(" scale: " + scale); |
|
var counter = 0; |
|
quadtree.visit(function (node, x1, y1, x2, y2) { |
|
var p = node.point; |
|
var pwidth = node.width * scale; |
|
var pheight = node.height * scale; |
|
|
|
// -- if this is too small rectangle only count the branch and set opacity |
|
if ((pwidth * pheight) <= 1) { |
|
// start collecting sub Pixel points |
|
subPixel = true; |
|
} |
|
// -- jumped to super node large than 1 pixel |
|
else { |
|
// end collecting sub Pixel points |
|
if (subPixel && subPts && subPts.length > 0) { |
|
|
|
subPts[0].group = subPts.length; |
|
pts.push(subPts[0]); // add only one todo calculate intensity |
|
counter += subPts.length - 1; |
|
subPts = []; |
|
} |
|
subPixel = false; |
|
} |
|
|
|
if ((p) && (p.x >= x0) && (p.x < x3) && (p.y >= y0) && (p.y < y3)) { |
|
|
|
if (subPixel) { |
|
subPts.push(p.all); |
|
} |
|
else { |
|
if (p.all.group) { |
|
delete (p.all.group); |
|
} |
|
pts.push(p.all); |
|
} |
|
|
|
} |
|
// if quad rect is outside of the search rect do nto search in sub nodes (returns true) |
|
return x1 >= x3 || y1 >= y3 || x2 < x0 || y2 < y0; |
|
}); |
|
console.log(" Number of removed points: " + counter); |
|
return pts; |
|
|
|
} |
|
|
|
|
|
function updateNodes(quadtree) { |
|
var nodes = []; |
|
quadtree.depth = 0; // root |
|
|
|
quadtree.visit(function (node, x1, y1, x2, y2) { |
|
var nodeRect = { |
|
left: MercatorXofLongitude(x1), |
|
right: MercatorXofLongitude(x2), |
|
bottom: MercatorYofLatitude(y1), |
|
top: MercatorYofLatitude(y2), |
|
} |
|
node.width = (nodeRect.right - nodeRect.left); |
|
node.height = (nodeRect.top - nodeRect.bottom); |
|
|
|
if (node.depth == 0) { |
|
console.log(" width: " + node.width + "height: " + node.height); |
|
} |
|
nodes.push(node); |
|
for (var i = 0; i < 4; i++) { |
|
if (node.nodes[i]) node.nodes[i].depth = node.depth + 1; |
|
} |
|
}); |
|
return nodes; |
|
} |
|
|
|
//------------------------------------------------------------------------------------- |
|
MercatorXofLongitude = function (lon) { |
|
return lon * 20037508.34 / 180; |
|
} |
|
|
|
MercatorYofLatitude = function (lat) { |
|
return (Math.log(Math.tan((90 + lat) * Math.PI / 360)) / (Math.PI / 180)) * 20037508.34 / 180; |
|
} |
|
var cscale = d3.scale.linear().domain([1, 3]).range(["#ff0000", "#ff6a00", "#ffd800", "#b6ff00", "#00ffff", "#0094ff"]);//"#00FF00","#FFA500" |
|
var leafletMap = L.map('map').setView([49.19, 16.60], 11); |
|
|
|
L.tileLayer("http://{s}.sm.mapstack.stamen.com/(toner-lite,$fff[difference],$fff[@23],$fff[hsl-saturation@20])/{z}/{x}/{y}.png").addTo(leafletMap); |
|
|
|
|
|
var svg = d3.select(leafletMap.getPanes().overlayPane).append("svg"); |
|
var g = svg.append("g").attr("class", "leaflet-zoom-hide"); |
|
|
|
|
|
// Use Leaflet to implement a D3 geometric transformation. |
|
function projectPoint(x, y) { |
|
var point = leafletMap.latLngToLayerPoint(new L.LatLng(y, x)); |
|
this.stream.point(point.x, point.y); |
|
} |
|
|
|
var transform = d3.geo.transform({ point: projectPoint }); |
|
var path = d3.geo.path().projection(transform); |
|
|
|
|
|
updateNodes(qtree); |
|
|
|
leafletMap.on('moveend', mapmove); |
|
|
|
mapmove(); |
|
|
|
|
|
|
|
|
|
function getZoomScale() { |
|
var mapWidth = leafletMap.getSize().x; |
|
var bounds = leafletMap.getBounds(); |
|
var planarWidth = MercatorXofLongitude(bounds.getEast()) - MercatorXofLongitude(bounds.getWest()); |
|
var zoomScale = mapWidth / planarWidth; |
|
return zoomScale; |
|
|
|
} |
|
|
|
function redrawSubset(subset) { |
|
path.pointRadius(3);// * scale); |
|
|
|
var bounds = path.bounds({ type: "FeatureCollection", features: subset }); |
|
var topLeft = bounds[0]; |
|
var bottomRight = bounds[1]; |
|
|
|
|
|
svg.attr("width", bottomRight[0] - topLeft[0]) |
|
.attr("height", bottomRight[1] - topLeft[1]) |
|
.style("left", topLeft[0] + "px") |
|
.style("top", topLeft[1] + "px"); |
|
|
|
|
|
g.attr("transform", "translate(" + -topLeft[0] + "," + -topLeft[1] + ")"); |
|
|
|
var start = new Date(); |
|
|
|
|
|
var points = g.selectAll("path") |
|
.data(subset, function (d) { |
|
return d.id; |
|
}); |
|
points.enter().append("path"); |
|
points.exit().remove(); |
|
points.attr("d", path); |
|
|
|
points.style("fill-opacity", function (d) { |
|
if (d.group) { |
|
return (d.group * 0.1) + 0.2; |
|
} |
|
}); |
|
|
|
|
|
console.log("updated at " + new Date().setTime(new Date().getTime() - start.getTime()) + " ms "); |
|
|
|
} |
|
function mapmove(e) { |
|
var mapBounds = leafletMap.getBounds(); |
|
var subset = search(qtree, mapBounds.getWest(), mapBounds.getSouth(), mapBounds.getEast(), mapBounds.getNorth()); |
|
console.log("subset: " + subset.length); |
|
|
|
redrawSubset(subset); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}); |
|
</script> |
|
</body> |
|
</html> |
This is a very good implementation. I was trying to migrate this implementation with Latest D3 V4 version but finding difficulty as the QuadTree implementation has been changed. I am able to plot the points but the problem that I am facing is in Zoom-out operation the Points that are plotting is the previous zoom options. If I do a slight pan I am getting all the Points.
Also in some of the cases I am getting stack overflow when d3 is creating the QuadTree as the old implementation of QuadTree is recursive and thus we are getting stack overflow and that is the reason why I am planning to migrate to new D3 v4 Quad implemetation as they have remove the recursion.
Can you let me know what changes are required for this implementation to work with D3 V4?
Thanks
Mahender