I'm building a Voronoi map inspired by http://chriszetter.com/blog/2014/06/15/building-a-voronoi-map-with-d3-and-leaflet/. I'd like to make an option to turn off the background map as the location of the data may not be relevant in all my use cases. Furthermore, it would be great if the visualization could work offline this way. After toggling the switch, the entire background would be white. The Voronoi overlap would be the same. How do I do that? Here's the code (zip-file contains the csv-files): https://www.dropbox.com/s/i8vtfh8mkxazfr0/voronoi-maps-master.zip?dl=0
EDIT: There's two layer variables in the original code as I was trying to split the visualization into two. However, that attempt was unsuccessful and only mapLayer is used. This may not have been clear in the original question.
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link href="base.css" rel="stylesheet" />
<link href='https://api.tiles.mapbox.com/mapbox.js/v1.6.3/mapbox.css' rel='stylesheet' />
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div id='map'>
</div>
<div id='loading'>
</div>
<!-- <div id='selected'>
<h1>...</h1>
</div> -->
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.8/d3.min.js"></script>
<script src="https://api.tiles.mapbox.com/mapbox.js/v1.6.3/mapbox.js"></script>
<!-- <script src="/voronoi-map/lib/voronoi_map.js"></script> -->
<script type="text/javascript" src="voronoi_map.js"></script>
<script>
map = L.mapbox.map('map', 'zetter.i73ka9hn') // <- dur ikke!
.fitBounds([[59.355596 , -9.052734], [49.894634 , 3.515625]]);
url = 'supermarkets.csv';
initialSelection = d3.set(['Tesco', 'Sainsburys']);
voronoiMap(map, url, initialSelection);
</script>
</body>
</html>
voronoi_map.js
voronoiMap = function(map, url, initialSelections) {
var pointTypes = d3.map(),
points = [],
lastSelectedPoint;
var voronoi = d3.geom.voronoi()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; });
var selectPoint = function() {
d3.selectAll('.selected').classed('selected', false);
var cell = d3.select(this),
point = cell.datum();
lastSelectedPoint = point;
cell.classed('selected', true);
d3.select('#selected h1')
.html('')
.append('a')
.text( /*point.name*/ "8 interactions from this cell")
/* .attr('href', point.url)
.attr('target', '_blank') */
}
var drawWithLoading = function(e){
d3.select('#loading').classed('visible', true);
if (e && e.type == 'viewreset') {
d3.select('#overlay').remove();
}
setTimeout(function(){
draw();
d3.select('#loading').classed('visible', false);
}, 0);
}
var draw = function() {
d3.select('#overlay').remove();
var bounds = map.getBounds(),
topLeft = map.latLngToLayerPoint(bounds.getNorthWest()),
bottomRight = map.latLngToLayerPoint(bounds.getSouthEast()),
existing = d3.set(),
drawLimit = bounds.pad(0.4);
filteredPoints = points.filter(function(d) {
var latlng = new L.LatLng(d.latitude, d.longitude);
if (!drawLimit.contains(latlng)) { return false };
var point = map.latLngToLayerPoint(latlng);
key = point.toString();
if (existing.has(key)) { return false };
existing.add(key);
d.x = point.x;
d.y = point.y;
return true;
});
voronoi(filteredPoints).forEach(function(d) { d.point.cell = d; });
var svg = d3.select(map.getPanes().overlayPane).append("svg")
.attr('id', 'overlay')
.attr("class", "leaflet-zoom-hide")
.style("width", map.getSize().x + 'px')
.style("height", map.getSize().y + 'px')
.style("margin-left", topLeft.x + "px")
.style("margin-top", topLeft.y + "px");
var g = svg.append("g")
.attr("transform", "translate(" + (-topLeft.x) + "," + (-topLeft.y) + ")");
var svgPoints = g.attr("class", "points")
.selectAll("g")
.data(filteredPoints)
.enter().append("g")
.attr("class", "point");
var buildPathFromPoint = function(point) {
return "M" + point.cell.join("L") + "Z";
}
svgPoints.append("path")
.attr("class", "point-cell")
.attr("d", buildPathFromPoint)
//.style('fill', function(d) { return '#' + d.color } )
.on('click', selectPoint)
.classed("selected", function(d) { return lastSelectedPoint == d} );
/* svgPoints.append("circle")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
.style('fill', function(d) { return '#' + d.color } )
.attr("r", 2); */
}
/* function interactionGradient() {
color =
if
return
} */
var mapLayer = {
onAdd: function(map) {
map.on('viewreset moveend', drawWithLoading);
drawWithLoading();
}
};
var voronoiLayer = {
onAdd: function(map) {
map.on('viewreset moveend', drawWithLoading);
drawWithLoading();
}
};
map.on('ready', function() {
d3.csv(url, function(csv) {
points = csv;
points.forEach(function(point) {
pointTypes.set(point.type, {type: point.type, color: point.color});
})
map.addLayer(mapLayer);
})
});
}
You need to load the tileLayer seperately so you can create a reference to it which you can then in turn use to create a layercontrol which can easily enable/disable layers:
L.mapbox.accessToken = 'pk.eyJ1IjoicGF1bC12ZXJob2V2ZW4iLCJhIjoiZ1BtMWhPSSJ9.wQGnLyl1DiuIQuS0U_PtiQ';
// Create the tileLayer.
var tileLayer = L.mapbox.tileLayer('examples.map-i86nkdio');
var map = L.mapbox.map('mapbox', null, { // Do not add as parameter
'center': [0, 0],
'zoom': 1,
// Add here so it still gets added to the map initially
// You could skip this so it won't be added and you can
// turn it on via the layercontrol
'layers': [tileLayer]
});
// Create layer control
var layerControl = L.control.layers(null, {
'Tilelayer': tileLayer // Add tile layer to overlays
}).addTo(map);
Here's a working example on Plunker: http://plnkr.co/edit/5re3o6qnyCwAqXNYXrkP?p=preview
L.mapbox.tileLayer reference: https://www.mapbox.com/mapbox.js/api/v2.1.5/l-mapbox-tilelayer/
L.control.layers reference: http://leafletjs.com/reference.html#control-layers
Edit because of the comments:
You're already working with separate layers, the tilelayer (background) gets added upon map initalization L.mapbox.map('map', 'zetter.i73ka9hn') that in fact calls: L.mapbox.tileLayer('zetter.i73ka9hn').addTo(map). You'll need to do it that way because you'll need a reference to the layer so you can add it to L.control.layers like shown above. Your voronoi layer gets added in the voronoiMap method in the ready handler of the map: map.addLayer(mapLayer);
Thus as you can see they are already separated. Now if you also want to be able to toggle the voronoi layer in your layer control you'll need to add it to the layer control:
map.on('ready', function() {
d3.csv(url, function(csv) {
points = csv;
points.forEach(function(point) {
pointTypes.set(point.type, {
type: point.type,
color: point.color
});
});
map.addLayer(mapLayer);
layerControl.addOverlay(mapLayer, 'Voronoi'); // Here
})
});
But that in itself is not enough in your case because your layer doesn't have a onRemove method as prescribed by the ILayer interface:
http://leafletjs.com/reference.html#ilayer
Now if we add a onRemove method to your layer like this:
var mapLayer = {
onAdd: function(map) {
map.on('viewreset moveend', drawWithLoading);
drawWithLoading();
},
onRemove: function (map) {
d3.select('#overlay').remove();
}
};
It works: http://plnkr.co/edit/3z3pCAo0gGuA7xqnqiqb?p=preview (note i've commented out the ready handler because the map is ready before the function call so it wouldn't fire and changed some colors to make things more clear.) Hope this helps.
Related
I'm pretty much a noob trying to get a feel for some of the things that seem interesting and a bit out of my grasp in d3.js. Of course I mess around with some code off of bl.ocks.org and stuff breaks without my understanding of why.
This may be a bigger problem than it appears or me missing something obvious, but I can't get the rotation animation to stop as supposed to when the globe is selected or when the animate box is unchecked. Everything else seems to work as intended.
Here's what I have done with d3.v4:
var feature;
var projection = d3.geoOrthographic()
.scale(380)
.rotate([71.03,-42.37])
.clipAngle(90)
.translate([400, 400]);
var path = d3.geoPath()
.projection(projection);
var svg = d3.select("#body").append("svg:svg")
.attr("width", "100%")
.attr("height", "100%")
.on("mousedown", mousedown);
if (frameElement) frameElement.style.height = '800px';
d3.json("https://gist.githubusercontent.com/phil-pedruco/10447085/raw/426fb47f0a6793776a044f17e66d17cbbf8061ad/countries.geo.json", function(collection) {
feature = svg.selectAll("path")
.data(collection.features)
.enter().append("svg:path")
.attr("d",clip);
feature.append("svg:title")
.text(function(d) { return d.properties.name; });
startAnimation();
d3.select('#animate').on('click', function () {
if (done) startAnimation(); else stopAnimation();
});
});
function stopAnimation() {
done = true;
d3.select('#animate').node().checked = false;
}
function startAnimation() {
done = false;
d3.timer(function() {
var rotate = projection.rotate();
rotate = [rotate[0] + 0.1, rotate[1]];
projection.rotate(rotate);
refresh();
return done;
});
}
function animationState() {
return 'animation: '+ (done ? 'off' : 'on');
}
d3.select(window)
.on("mousemove", mousemove)
.on("mouseup", mouseup);
var m0
, o0
, done
;
function mousedown() {
stopAnimation();
m0 = [d3.event.pageX, d3.event.pageY];
o0 = projection.rotate();
d3.event.preventDefault();
}
function mousemove() {
if (m0) {
var m1 = [d3.event.pageX, d3.event.pageY]
, o1 = [o0[0] - (m0[0] - m1[0]) / 8, o0[1] - (m1[1] - m0[1]) / 8];
projection.rotate(o1);
refresh();
}
}
function mouseup() {
if (m0) {
mousemove();
m0 = null;
}
}
function refresh(duration) {
(duration ? feature.transition().duration(duration) : feature).attr("d",clip);
}
function clip(d) {
return path(d);
}
function reframe(css) {
for (var name in css)
frameElement.style[name] = css[name] + 'px';
}
The original code can be found at http://bl.ocks.org/johan/1392488 for reference.
Your linked example is using a really old version of d3 (version 2). I believe the animation stopped in that version because your done variable is set to false, the timer function then returns false and it stops executing. In version 4, though, you need an explicit call to .stop().
Here's the code patched up:
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#4.0.0" data-semver="4.0.0" src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
<div class="tip">drag to rotate the origin</div>
<div><label for="animate">animate:</label>
<input id="animate" type="checkbox" checked>
</div>
<div id="body" style="width:800px;height:800px"></div>
<script>
var feature;
var projection = d3.geoOrthographic()
.scale(380)
.rotate([71.03,-42.37])
.clipAngle(90)
.translate([400, 400]);
var path = d3.geoPath()
.projection(projection);
var svg = d3.select("#body").append("svg:svg")
.attr("width", "100%")
.attr("height", "100%")
.on("mousedown", mousedown);
if (frameElement) frameElement.style.height = '800px';
d3.json("https://gist.githubusercontent.com/phil-pedruco/10447085/raw/426fb47f0a6793776a044f17e66d17cbbf8061ad/countries.geo.json", function(collection) {
feature = svg.selectAll("path")
.data(collection.features)
.enter().append("svg:path")
.attr("d",clip);
feature.append("svg:title")
.text(function(d) { return d.properties.name; });
startAnimation();
d3.select('#animate').on('click', function () {
if (done) startAnimation(); else stopAnimation();
});
});
function stopAnimation() {
done = true;
d3.select('#animate').node().checked = false;
timer.stop();
}
function startAnimation() {
done = false;
timer = d3.timer(function() {
var rotate = projection.rotate();
rotate = [rotate[0] + 0.1, rotate[1]];
projection.rotate(rotate);
refresh();
});
}
d3.select(window)
.on("mousemove", mousemove)
.on("mouseup", mouseup);
var m0
, o0
, done
, timer
;
function mousedown() {
stopAnimation();
m0 = [d3.event.pageX, d3.event.pageY];
o0 = projection.rotate();
d3.event.preventDefault();
}
function mousemove() {
if (m0) {
var m1 = [d3.event.pageX, d3.event.pageY]
, o1 = [o0[0] - (m0[0] - m1[0]) / 8, o0[1] - (m1[1] - m0[1]) / 8];
projection.rotate(o1);
refresh();
}
}
function mouseup() {
if (m0) {
mousemove();
m0 = null;
}
}
function refresh(duration) {
(duration ? feature.transition().duration(duration) : feature).attr("d",clip);
}
function clip(d) {
return path(d);
}
function reframe(css) {
for (var name in css)
frameElement.style[name] = css[name] + 'px';
}
</script>
</body>
</html>
so I have this Angular application I'm working on.
I have a controller called visualization and a directive called force-layout.
In the HTML template of the directive I'm creating three buttons and I attach to them three corresponding functions that I define in the directive code:
<div class="panel smallest controls">
<span ng-click="centerNetwork()"><i class="fa fa-arrows-alt" aria-hidden="true"></i></span>
<span ng-click="zoomIn()"><i class="fa fa-search-plus" aria-hidden="true"></i></span>
<span ng-click="zoomOut()"><i class="fa fa-search-minus" aria-hidden="true"></i></span>
</div>
<force-layout ng-if=" config.viewMode == 'individual-force' || config.viewMode == 'individual-concentric' "></force-layout>
The function are defined in the directive like this:
scope.centerNetwork = function() {
console.log("Recenter");
var sourceNode = nodes.filter(function(d) { return (d.id == sourceId)})[0];
svg.transition().duration(750).call(zoom.transform, d3.zoomIdentity.translate(width/2-sourceNode.x, height/2-sourceNode.y));
}
var zoomfactor = 1;
scope.zoomIn = function() {
console.log("Zoom In")
svg.transition().duration(500).call(zoom.scaleBy, zoomfactor + .5);
}
scope.zoomOut = function() {
console.log("Zoom Out")
svg.transition().duration(500).call(zoom.scaleBy, zoomfactor - .25);
}
It does not trigger any error.
It was working before, not it is not and I cannot understand what is causing the problem, any help?
UPDATE: full directive code.
'use strict';
/**
* #ngdoc directive
* #name redesign2017App.directive:forceLayout
* #description
* # forceLayout
*/
angular.module('redesign2017App')
.directive('forceLayout', function() {
return {
template: '<svg width="100%" height="100%"></svg>',
restrict: 'E',
link: function postLink(scope, element, attrs) {
console.log('drawing network the first time');
// console.log(scope.data);
var svg = d3.select(element[0]).select('svg'),
width = +svg.node().getBoundingClientRect().width,
height = +svg.node().getBoundingClientRect().height,
nodes,
links,
degreeSize,
sourceId,
confidenceMin = scope.config.confidenceMin,
confidenceMax = scope.config.confidenceMax,
dateMin = scope.config.dateMin,
dateMax = scope.config.dateMax,
complexity = scope.config.networkComplexity;
var durationTransition = 500;
// A function to handle click toggling based on neighboring nodes.
function toggleClick(d, newLinks, selectedElement) {
// Some code for handling selections cutted out from here
}
svg.append('rect')
.attr('width', '100%')
.attr('height', '100%')
.attr('fill', 'transparent')
.on('click', function() {
// Clear selections on nodes and labels
d3.selectAll('.node, g.label').classed('selected', false);
// Restore nodes and links to normal opacity. (see toggleClick() below)
d3.selectAll('.link')
.classed('faded', false)
d3.selectAll('.node')
.classed('faded', false)
// Must select g.labels since it selects elements in other part of the interface
d3.selectAll('g.label')
.classed('hidden', function(d) {
return (d.distance < 2) ? false : true;
});
// reset group bar
d3.selectAll('.group').classed('active', false);
d3.selectAll('.group').classed('unactive', false);
// update selction and trigger event for other directives
scope.currentSelection = {};
scope.$apply(); // no need to trigger events, just apply
});
// HERE ARE THE FUNCTIONS I ASKED ABOUT
// Zooming function translates the size of the svg container.
function zoomed() {
container.attr("transform", "translate(" + d3.event.transform.x + ", " + d3.event.transform.y + ") scale(" + d3.event.transform.k + ")");
}
var zoom = d3.zoom();
// Call zoom for svg container.
svg.call(zoom.on('zoom', zoomed)); //.on("dblclick.zoom", null);
//Functions for zoom and recenter buttons
scope.centerNetwork = function() {
console.log("Recenter");
var sourceNode = nodes.filter(function(d) {
return (d.id == sourceId) })[0];
svg.transition().duration(750).call(zoom.transform, d3.zoomIdentity.translate(width / 2 - sourceNode.x, height / 2 - sourceNode.y));
// svg.transition().duration(750).call(zoom.transform, d3.zoomIdentity);
}
var zoomfactor = 1;
scope.zoomIn = function() {
console.log("Zoom In")
svg.transition().duration(500).call(zoom.scaleBy, zoomfactor + .5);
}
scope.zoomOut = function() {
console.log("Zoom Out")
svg.transition().duration(500).call(zoom.scaleBy, zoomfactor - .25);
}
// TILL HERE
var container = svg.append('g');
// Toggle for ego networks on click (below).
var toggle = 0;
var link = container.append("g")
.attr("class", "links")
.selectAll(".link");
var node = container.append("g")
.attr("class", "nodes")
.selectAll(".node");
var label = container.append("g")
.attr("class", "labels")
.selectAll(".label");
var loading = svg.append("text")
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.attr('x', width / 2)
.attr('y', height / 2)
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.text("Simulating. One moment pleaseā¦");
var t0 = performance.now();
var json = scope.data;
// graph = json.data.attributes;
nodes = json.included;
links = [];
json.data.attributes.connections.forEach(function(c) { links.push(c.attributes) });
sourceId = json.data.attributes.primary_people;
// d3.select('.legend .size.min').text('j')
var simulation = d3.forceSimulation(nodes)
// .velocityDecay(.5)
.force("link", d3.forceLink(links).id(function(d) {
return d.id;
}))
.force("charge", d3.forceManyBody().strength(-75)) //.distanceMax([500]))
.force("center", d3.forceCenter(width / 2, height / 2))
.force("collide", d3.forceCollide().radius(function(d) {
if (d.id == sourceId) {
return 26;
} else {
return 13;
}
}))
// .force("x", d3.forceX())
// .force("y", d3.forceY())
.stop();
for (var i = 0, n = Math.ceil(Math.log(simulation.alphaMin()) / Math.log(1 - simulation.alphaDecay())); i < n; ++i) {
simulation.tick();
}
loading.remove();
var t1 = performance.now();
console.log("Graph took " + (t1 - t0) + " milliseconds to load.")
function positionCircle(nodelist, r) {
var angle = 360 / nodelist.length;
nodelist.forEach(function(n, i) {
n.fx = (Math.cos(angle * (i + 1)) * r) + (width / 2);
n.fy = (Math.sin(angle * (i + 1)) * r) + (height / 2);
});
}
function update(confidenceMin, confidenceMax, dateMin, dateMax, complexity, layout) {
// some code for visualizing a force layout cutted out from here
}
// Trigger update automatically when the directive code is executed entirely (e.g. at loading)
update(confidenceMin, confidenceMax, dateMin, dateMax, complexity, 'individual-force');
// update triggered from the controller
scope.$on('Update the force layout', function(event, args) {
console.log('ON: Update the force layout')
confidenceMin = scope.config.confidenceMin;
confidenceMax = scope.config.confidenceMax;
dateMin = scope.config.dateMin;
dateMax = scope.config.dateMax;
complexity = scope.config.networkComplexity;
update(confidenceMin, confidenceMax, dateMin, dateMax, complexity, args.layout);
});
}
};
});
So I think I found the problem.
The directive was invoked by the <force-layout> tag, which presented the ng-if='some-condition' directive.
For some reason, while the controller code is evaluated, that condition doesn't prove to be true, and the directive as a whole, meaning its HTML and its JS, is simply skipped: the ng-click is then obviously not able to attach the click event to a function that does not exists yet.
Replacing ng-if with ng-show solved the issue: apparently in this case the code is executed immediately and the directive exists hidden, with all its declared properties.
I am using markerCluster for Leaflet with the showCoverageOnHover option set to true. However, in Firefox (v 46.0.1), the event showCoverageOnHover is not triggered correctly, meaning that the cluster area is shown not only when the mouse is hovered over the cluster, but also if the mouse is far away from that cluster.
Basically, I am using the standard procedure to create a markerClusterGroup, but with a customized icon creation function (Using d3 to draw a Pie chart). My code looks as follows:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style type="text/css">
#mapid {
height: 60vh;
}
</style>
<script src='https://api.mapbox.com/mapbox.js/v2.4.0/mapbox.js'></script>
<link href='https://api.mapbox.com/mapbox.js/v2.4.0/mapbox.css' rel='stylesheet' />
<script src='https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/0.5.0/leaflet.markercluster.js'></script>
<link href='https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/0.5.0/MarkerCluster.css' rel='stylesheet' />
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<title>WorldMap</title>
<script type="text/javascript">
function defineClusterIcon(cluster) {
var color = d3.scale.category20b();
//some dummy json
var myjson = '[{ "label":"Monday", "count": 15 },{ "label":"Tuesday", "count": 20 }]';
var dataset = JSON.parse( myjson );
var size = 40;
var radius= size / 2;
var svgres = document.createElementNS(d3.ns.prefix.svg, 'svg');
var svg = d3.select(svgres).append('svg')
.attr('width', size)
.attr('height', size)
.append('g')
.attr('transform','translate(' + (size / 2) + ',' + (size / 2) + ')'); //center g
var arc = d3.svg.arc().outerRadius(radius);
var pie = d3.layout.pie().value(function(d) {
return d.count;
});
//create final chart
svg.selectAll('path').data(pie(dataset)) //fill dataset into path
.enter() //create placeholder for data
.append('path') //fill placeholder with data in path
.attr('d', arc) //define an attribute d
.attr('fill', function(d, i) {
return color(d.data.label);
});
var html = serializeXmlNode(svgres);
var myIcon = new L.DivIcon({
html : html,
className : 'mycluster',
iconSize : new L.Point(size, size)
});
return myIcon;
}
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>
</head>
<body>
<div id="mapid"></div>
<script type="text/javascript">
var map = L.map('mapid', {
center: [40, 40],
maxZoom : 10,
zoom: 2
});
//create markercluster
var markers = new L.markerClusterGroup({
showCoverageOnHover: true,
iconCreateFunction: defineClusterIcon
});
//some example markers
var marker = new L.marker([40.0,10.0]);
markers.addLayer(marker);
var marker = new L.marker([42.0,-12.0]);
markers.addLayer(marker);
var marker = new L.marker([50.0,30.0]);
markers.addLayer(marker);
map.addLayer(markers);
</script>
</body>
</html>
Any ideas why the showCoverageOnHover event is not triggered correctly in Firefox?
Thanks!
Looks like the SVG element you created overflows the Leaflet icon.
Simply setting overflow: hidden CSS rule on your icon class seems to solve your issue.
.mycluster {
overflow: hidden;
}
Updated JSFiddle: https://jsfiddle.net/sqeypmrn/1/
Note: question also posted on Leaflet.markercluser GitHub page as issue #677.
I understand it should be as simple as selecting the element you need and applying the call to it, but in my case, nothing is happening when I do so.
In my case, I need to re draw circles based on a zoom level of a map.
If the zoom level is < a certain number, use dataset A for the circles.
If the zoom level is > the number use dataset B to draw the circles.
I can draw the circles fine, and they do change on changing of the zoom level, but when I add an .on("click") event to these though, nothing happens.
Here is a Codepen link showing the lack of click event working CODEPEN LINK
Here is the code I am using, I have a feeling I am doing something wrong in the update() function, and the way I am using the .remove() function:
L.mapbox.accessToken = 'pk.eyJ1Ijoic3RlbmluamEiLCJhIjoiSjg5eTMtcyJ9.g_O2emQF6X9RV69ibEsaIw';
var map = L.mapbox.map('map', 'mapbox.streets')
.setView([53.4072, -2.9821], 14);
var data = {
"largeRadius": [{
"coords": [53.3942, -2.9785],
"name": "Jamaica Street"
}, {
"coords": [53.4073, -2.9824],
"name": "Hood Street"
}],
"smallRadius": [{
"coords": [53.4075, -2.9936],
"name": "Chapel Street"
}, {
"coords": [53.4073, -2.9824],
"name": "Hood Street"
}]
};
// Sort data for leaflet LatLng conversion
data.largeRadius.forEach(function(d) {
d.LatLng = new L.LatLng(d.coords[0], d.coords[1]);
});
data.smallRadius.forEach(function(d) {
d.LatLng = new L.LatLng(d.coords[0], d.coords[1]);
});
var svg = d3.select(map.getPanes().overlayPane).append("svg");
var g = svg.append("g").attr("class", "leaflet-zoom-hide");
var circles = g.selectAll("circle")
.data(data.smallRadius)
.enter().append("circle");
function update() {
circles.remove();
translateSVG();
var dataInstance;
var radius;
if (map.getZoom() < 17) {
dataInstance = data.largeRadius;
radius = 0.008;
} else {
dataInstance = data.smallRadius;
radius = 0.001;
}
dataInstance.forEach(function(d) {
d.LatLng = new L.LatLng(d.coords[0], d.coords[1]);
});
circles = g.selectAll("circle")
.data(dataInstance)
.enter().append("circle")
.attr("id", function(d) {
return d.name
})
.attr("cx", function(d) {
return map.latLngToLayerPoint(d.LatLng).x
})
.attr("cy", function(d) {
return map.latLngToLayerPoint(d.LatLng).y
})
.attr("r", function() {
return radius * Math.pow(2, map.getZoom())
});
}
function translateSVG() {
var width = window.innerWidth;
var height = window.innerHeight;
var regExp = /\(([^)]+)\)/;
var translateString = regExp.exec(document.querySelector(".leaflet-map-pane").attributes[1].nodeValue);
var translateX = parseInt(translateString[1].split(" ")[0]);
var translateY = parseInt(translateString[1].split(" ")[1]);
if (translateX < 0) {
translateX = Math.abs(translateX);
} else {
translateX = -translateX;
}
if (translateY < 0) {
translateY = Math.abs(translateY);
} else {
translateY = -translateY;
}
svg.attr("width", width);
svg.attr("height", height);
svg.attr("viewBox", function() {
return translateX + " " + translateY + " " + width + " " + height;
});
svg.attr("style", function() {
return "transform: translate3d(" + translateX + "px, " + translateY + "px, 0px);";
});
}
// THIS IS THE CLICK EVENT THAT DOES NOT WORK
circles.on("click", function () {
alert("clicked");
})
map.on("moveend", update);
update();
I'm not sure if this fixes your issue completely, mostly because I'm not sure I fully understand what you're trying to achieve, but if you move the 'click' code:
circles.on("click", function () {
alert("clicked");
});
Inside your update, then you'll rebind that after you've done the destroy and re-create, so your update function becomes this:
function update() {
circles.remove();
translateSVG();
var dataInstance;
var radius;
if (map.getZoom() < 17) {
dataInstance = data.largeRadius;
radius = 0.008;
} else {
dataInstance = data.smallRadius;
radius = 0.001;
}
dataInstance.forEach(function(d) {
d.LatLng = new L.LatLng(d.coords[0], d.coords[1]);
});
circles = g.selectAll("circle")
.data(dataInstance)
.enter().append("circle")
.attr("id", function(d) {
return d.name
})
.attr("cx", function(d) {
return map.latLngToLayerPoint(d.LatLng).x
})
.attr("cy", function(d) {
return map.latLngToLayerPoint(d.LatLng).y
})
.attr("r", function() {
return radius * Math.pow(2, map.getZoom())
});
circles.on("click", function () {
alert("clicked");
});
}
and you then remove the circles.on("click") part from the bottom. It may also be worth making sure you're releasing that bind each time, i'm unsure if it'll be overwriting the memory or adding to it each update.
Here's a fork of yours where it seems to be working as I imagine it: http://codepen.io/anon/pen/waqJqB?editors=101
I have set up this jsfiddle : http://jsfiddle.net/386er/dhzq6q6f/14/
var moveCell = function(direction) {
var cellToBeMoved = pickRandomCell();
var currentX = cellToBeMoved.x.baseVal.value;
var currentY = cellToBeMoved.y.baseVal.value;
var change = getPlusOrMinus() * (cellSize + 1 );
var newX = currentX + change;
var newY = currentY + change;
var selectedCell = d3.select(cellToBeMoved);
if (direction === 'x') {
selectedCell.transition().duration(1500)
.attr('x', newX );
} else {
selectedCell.transition().duration(1500)
.attr('y', newY );
}
}
In the moveCell function, I pick a random cell, request its current x and y coordinates and then add or subtract its width or height, to move it to an adjacent cell.
What I am wondering about: If you watch the cells move, some will only move partially to the next cell. Can anoyne tell me, why this is so ?
The first thing to do in this situation is put .each("interrupt", function() { console.log("interrupted!") }); on your transitions. Then you will see the problem.
Its supposed to fix it if you name the transitions like selection.transition("name"), but that doesn't fix it.
That means you have to do as suggested by #jcuenod and exclude the ones that are moving. One way to do that which is idiomatic is like this...
if (direction === 'x') {
selectedCell.transition("x").duration(1500)
.attr('x', newX)
.each("start", function () { lock.call(this, "lockedX") })
.each("end", function () { unlock.call(this, "lockedX") });
} else {
selectedCell.transition("y").duration(1500)
.attr('y', newY)
.each("start", function () { lock.call(this, "lockedX") })
.each("end", function () { unlock.call(this, "lockedX") });
}
function lock(lockClass) {
var c = { cell: false }; c[lockClass] = true;
d3.select(this).classed(c)
};
function unlock(lockClass) {
var c = { cell: this.classList.length == 1 }; c[lockClass] = false;
d3.select(this).classed(c);
};
Here is a fiddle to prove the concept.
Pure and idiomatic d3 version
Just for completeness here is the d3 way to do it.
I've tried to make it as idiomatic as possible. The main points being...
Purely data-driven
The data is updated and the viz manipulation left entirely to d3 declarations.
Use d3 to detect and act on changes to svg element attributes
This is done by using a composite key function in the selection.data() method and by exploiting the fact that changed nodes (squares where the x, y or fillattributes don't match the updated data) are captured by the exit selection.
Splice changed elements into the data array so d3 can detect changes
Since a reference to the data array elements is bound to the DOM elements, any change to the data will also be reflected in the selection.datum(). d3 uses a key function to compare the data values to the datum, in order to classify nodes as update, enter or exit. If a key is made, that is a function of the data/datum values, changes to data will not be detected. By splice-ing changes into the data array, the value referenced by selection.datum() will be different from the data array, so data changes will flag exit nodes.
By simply manipulating attributes and putting transitions on the exit selection and not removing it, it essentially becomes a 'changed' selection.
this only works if the data values are objects.
Concurrent transitions
Named transitions are used to ensure x and y transitions don't interrupt each other, but it was also necessary to use tag class attributes to lock out transitioning elements. This is done using transition start and end events.
Animation frames
d3.timer is used to smooth animation and marshal resources. d3Timer calls back to update the data before the transitions are updated, before each animation frame.
Use d3.scale.ordinal() to manage positioning
This is great because you it works every time and you don't even have to thin about it.
$(function () {
var container,
svg,
gridHeight = 800,
gridWidth = 1600,
cellSize, cellPitch,
cellsColumns = 100,
cellsRows = 50,
squares,
container = d3.select('.svg-container'),
svg = container.append('svg')
.attr('width', gridWidth)
.attr('height', gridHeight)
.style({ 'background-color': 'black', opacity: 1 }),
createRandomRGB = function () {
var red = Math.floor((Math.random() * 256)).toString(),
green = Math.floor((Math.random() * 256)).toString(),
blue = Math.floor((Math.random() * 256)).toString(),
rgb = 'rgb(' + red + ',' + green + ',' + blue + ')';
return rgb;
},
createGrid = function (width, height) {
var scaleHorizontal = d3.scale.ordinal()
.domain(d3.range(cellsColumns))
.rangeBands([0, width], 1 / 15),
rangeHorizontal = scaleHorizontal.range(),
scaleVertical = d3.scale.ordinal()
.domain(d3.range(cellsRows))
.rangeBands([0, height]),
rangeVertical = scaleVertical.range(),
squares = [];
rangeHorizontal.forEach(function (dh, i) {
rangeVertical.forEach(function (dv, j) {
var indx;
squares[indx = i + j * cellsColumns] = { x: dh, y: dv, c: createRandomRGB(), indx: indx }
})
});
cellSize = scaleHorizontal.rangeBand();
cellPitch = {
x: rangeHorizontal[1] - rangeHorizontal[0],
y: rangeVertical[1] - rangeVertical[0]
}
svg.selectAll("rect").data(squares, function (d, i) { return d.indx })
.enter().append('rect')
.attr('class', 'cell')
.attr('width', cellSize)
.attr('height', cellSize)
.attr('x', function (d) { return d.x })
.attr('y', function (d) { return d.y })
.style('fill', function (d) { return d.c });
return squares;
},
choseRandom = function (options) {
options = options || [true, false];
var max = options.length;
return options[Math.floor(Math.random() * (max))];
},
pickRandomCell = function (cells) {
var l = cells.size(),
r = Math.floor(Math.random() * l);
return l ? d3.select(cells[0][r]).datum().indx : -1;
};
function lock(lockClass) {
var c = { cell: false }; c[lockClass] = true;
d3.select(this).classed(c)
};
function unlock(lockClass) {
var c = { cell: this.classList.length == 1 }; c[lockClass] = false;
d3.select(this).classed(c);
};
function permutateColours() {
var samples = Math.min(50, Math.max(~~(squares.length / 50),1)), s, ii = [], i, k = 0,
cells = d3.selectAll('.cell');
while (samples--) {
do i = pickRandomCell(cells); while (ii.indexOf(i) > -1 && k++ < 5 && i > -1);
if (k < 10 && i > -1) {
ii.push(i);
s = squares[i];
squares.splice(i, 1, { x: s.x, y: s.y, c: createRandomRGB(), indx: s.indx });
}
}
}
function permutatePositions() {
var samples = Math.min(20, Math.max(~~(squares.length / 100),1)), s, ss = [], d, m, p, k = 0,
cells = d3.selectAll('.cell');
while (samples--) {
do s = pickRandomCell(cells); while (ss.indexOf(s) > -1 && k++ < 5 && s > -1);
if (k < 10 && s > -1) {
ss.push(s);
d = squares[s];
m = { x: d.x, y: d.y, c: d.c, indx: d.indx };
m[p = choseRandom(["x", "y"])] = m[p] + choseRandom([-1, 1]) * cellPitch[p];
squares.splice(s, 1, m);
}
}
}
function updateSquares() {
//use a composite key function to transform the exit selection into
// an attribute update selection
//because it's the exit selection, d3 doesn't bind the new data
// that's done manually with the .each
var changes = svg.selectAll("rect")
.data(squares, function (d, i) { return d.indx + "_" + d.x + "_" + d.y + "_" + d.c; })
.exit().each(function (d, i, j) { d3.select(this).datum(squares[i]) })
changes.transition("x").duration(1500)
.attr('x', function (d) { return d.x })
.each("start", function () { lock.call(this, "lockedX") })
.each("end", function () { unlock.call(this, "lockedX") })
changes.transition("y").duration(1500)
.attr('y', function (d) { return d.y })
.each("start", function () { lock.call(this, "lockedY") })
.each("end", function () { unlock.call(this, "lockedY") });
changes.attr("stroke", "white")
.style("stroke-opacity", 0.6)
.transition("fill").duration(800)
.style('fill', function (d, i) { return d.c })
.style("stroke-opacity", 0)
.each("start", function () { lock.call(this, "lockedFill") })
.each("end", function () { unlock.call(this, "lockedFill") });
}
squares = createGrid(gridWidth, gridHeight);
d3.timer(function tick() {
permutateColours();
permutatePositions();
updateSquares();
});
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<div class="svg-container"></div>
NOTE: requires d3 version 3.5.5 for the position transitions to run.
EDIT: fixed a problem with lock and un-lock. Would probably better to tag the data rather than write classes to the DOM but, anyway... this way is fun.