I am trying to implement this example. but I am using d3 v4 and leaflet version 1. In the mouse move on the svg function, I am styling the lines but it throws me an error Uncaught TypeError: Cannot read property 'style' of null I could form a lasso but it is all black which means neither the circles are getting styled nor the lines. I don't know why is the line variable null? This is my code--
svgLayer = L.svg({clickable:true});
svgLayer.addTo(map)
// assigning SVG
svg = d3.select('#map').select('svg').attr("pointer-events", "auto");
pointsG = svg.select('g').attr('class', 'leaflet-zoom-hide');
map.dragging.disable();
map.touchZoom.disable();
map.doubleClickZoom.disable();
map.scrollWheelZoom.disable();
if (map.tap) map.tap.disable();
function project(ll) {
//console.log(ll);
var point = map.latLngToLayerPoint(ll.LatLng);
//console.log(point)
return point;
}
d3.queue()
.defer(d3.csv, 'dots.csv', function(row) {
return {LatLng: [+row['lat'], +row['lng']]};
})
.await(readyToDraw);
function readyToDraw(error,data){
//console.log(data);
var points = pointsG.selectAll(".points")
.data(data);
var pointsEnter = points.enter().append("circle")
.attr("class", "points")
.attr("r", 6)
.style("fill-opacity", 0.4)
.style("fill","black")
.attr("pointer-events","visible");
var lassoPoints = [];
var lassoClosed = false;
var dragging = false;
svg.on("click.lasso", function() {
if(dragging) return;
var p = d3.mouse(this);
//console.log(p)
var ll = map.containerPointToLatLng(L.point([p[0],p[1]]))
//console.log(ll)
if(lassoPoints.length) {
var fp = project(lassoPoints[0])
// console.log(lassoPoints[0])
var dist2 = (fp.x - p[0])*(fp.x - p[0]) + (fp.y - p[1])*(fp.y-p[1])
if(dist2 < 100) {
lassoClosed = true;
renderLasso();
pointsG.selectAll("line.lasso").remove();
return;
}
}
if(lassoClosed) {
/*
lassoClosed = false;
g.selectAll(".lasso").remove();
lassoPoints = [];
return render();
*/
return;
};
lassoPoints.push(ll);
updateLayers();
});
svg.on("mousemove", function() {
// we draw a guideline for where the next point would go.
var lastPoint = lassoPoints[lassoPoints.length-1];
var p = d3.mouse(this);
var ll = map.containerPointToLatLng(L.point([p[0],p[1]]));
//console.log(lastPoint)
var line = pointsG.selectAll("line.lasso").data([lastPoint])
//console.log(line)
line.enter().append("line").classed("lasso", true)
if(lassoPoints.length && !lassoClosed) {
//console.log(project(lastPoint))
line.attr('x1', project(lastPoint).x)
.attr('y1', project(lastPoint).y)
.attr('x2', project(ll).x)
.attr('y2', project(ll).y)
.style('stroke', "#111")
.style("stroke-dasharray", "5 5");
} else {
line.remove();
}
})
var path = d3.line()
.x(function(d) { return project(d).x})
.y(function(d) { return project(d).y})
function renderLasso() {
// render our lasso
//console.log(lassoPoints)
var lassoPath = pointsG.selectAll("path.lasso").data([lassoPoints])
lassoPath.enter().append("path").classed("lasso", true)
.on("click", function() {
if(lassoClosed) {
lassoClosed = false;
pointsG.selectAll(".lasso").remove();
lassoPoints = [];
d3.event.stopPropagation();
return updateLayers();
};
})
//console.log(lassoPath)
lassoPath.attr("d", function(d) {
var str = path(d)
if(lassoClosed) str += "Z"
return str;
})
.style('stroke', '#010')
.style('fill', "#010")
.style("fill-opacity", 0.1);
var drag = d3.drag()
.on("drag", function(d) {
if(!lassoClosed) return;
dragging = true;
var p = d3.mouse(svg.node())
var ll = map.containerPointToLatLng(L.point([p[0],p[1]]));
d.lat = ll.lat;
d.lng = ll.lng;
renderLasso();
}).on("end", function() {
setTimeout(function() {
dragging = false;
}, 100)
})
//console.log(lassoPoints)
var lasso = pointsG.selectAll("circle.lasso")
.data(lassoPoints)
lasso.enter().append("circle").classed("lasso", true)
.call(drag);
//console.log(lasso)
lasso.attr('cx', function(d) { return project(d).x;})
.attr('cy', function(d) { return project(d).y;})
.attr('r',8)
.style('stroke','#010')
.style('fill','#b7feb7')
.style("fill-opacity",0.9);
var projected = lassoPoints.map(function(d){
return project(d)
})
//console.log(projected)
pointsG.selectAll(".points").style('fill', function(d) {
//console.log(d);
var isInside = inside(project(d), projected);
//console.log(project(d), isInsid
//console.log(isInside)
if(isInside) {
return "#ff8eec";
} else {
return "#0082a3";
}
})
}
function updateLayers(){
pointsG.selectAll('.points')
.attr('cx', function(d){ return map.latLngToLayerPoint(d.LatLng).x})
.attr('cy', function(d){return map.latLngToLayerPoint(d.LatLng).y})
renderLasso();
};
map.on('zoomend', updateLayers);
updateLayers();
}
function inside(point, vs) {
var x = point.x, y = point.y;
var inside = false;
for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) {
var xi = vs[i].x, yi = vs[i].y;
var xj = vs[j].x, yj = vs[j].y;
var intersect = ((yi > y) != (yj > y))
&& (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
if (intersect) inside = !inside;
}
return inside;
};
Without the line where the error is it is hard to give an explanation.
Better to put all the style stuff in the style tag.
You are inconsistent in the use of the bound data to the points
Here you use d
pointsG.selectAll(".points").style({
fill: function(d) {
var isInside = inside(project(d), projected);
//console.log(project(d), isInside)
if(isInside) {
return "#ff8eec";
} else {
return "#0082a3"
}
}
})
Here you use d.LatLng
function updateLayers(){
pointsG.selectAll('.points')
.attr('cx', function(d){ return map.latLngToLayerPoint(d.LatLng).x})
.attr('cy', function(d){ return map.latLngToLayerPoint(d.LatLng).y})
renderLasso();
};
Related
I am having a similar problem to the one described here, but the solution offered there doesn't seem to work for me. I am trying to create a force-directed graph with force-based label placement using my own json data. I'm running into an unspecified uncaught javascript error at the point of reading in my data. My data is in the following format:
{"nodes": [{"name": Bob, "id": 1}, {"name": Suzy, "id": 2}],
"links": [{"source": 1, "target":2}]}
My script is below. Any insight would be enormously helpful!
<script type="text/javascript" charset="utf-8">
var w = 960, h = 500;
var labelDistance = 0;
var vis = d3.select("body").append("svg:svg").attr("width", w).attr("height", h);
var nodes = [];
var labelAnchors = [];
var labelAnchorLinks = [];
var links = [];
d3.json("data.json", function(error, graph) {
if (error) throw error;
for (var i = 0; i < graph.nodes.length; i++) {
var node = graph.nodes[i];
nodes.push(node);
labelAnchors.push({
node : node
});
labelAnchors.push({
node : node
});
}
graph.links.forEach(function(link, i) {
links.push({
source: link.source,
target: link.target,
weight: Math.random()
});
labelAnchorLinks.push({
source: link.source * 2,
target: link.target * 2 + 1,
weight: 1
});
});
var force = d3.layout.force().size([w, h]).nodes(nodes).links(links).gravity(1).linkDistance(50).charge(-3000).linkStrength(function(x) {
return x.weight * 10
});
force.start();
var force2 = d3.layout.force().nodes(labelAnchors).links(labelAnchorLinks).gravity(0).linkDistance(0).linkStrength(8).charge(-100).size([w, h]);
force2.start();
var link = vis.selectAll("line.link").data(links).enter().append("svg:line").attr("class", "link").style("stroke", "#CCC");
var node = vis.selectAll("g.node").data(force.nodes()).enter().append("svg:g").attr("class", "node");
node.append("svg:circle").attr("r", 5).style("fill", "#555").style("stroke", "#FFF").style("stroke-width", 3);
node.call(force.drag);
var anchorLink = vis.selectAll("line.anchorLink").data(labelAnchorLinks)//.enter().append("svg:line").attr("class", "anchorLink").style("stroke", "#999");
var anchorNode = vis.selectAll("g.anchorNode").data(force2.nodes()).enter().append("svg:g").attr("class", "anchorNode");
anchorNode.append("svg:circle").attr("r", 0).style("fill", "#FFF");
anchorNode.append("svg:text").text(function(d, i) {
return i % 2 == 0 ? "" : d.node.label
}).style("fill", "#555").style("font-family", "Arial").style("font-size", 12);
var updateLink = function() {
this.attr("x1", function(d) {
return d.source.x;
}).attr("y1", function(d) {
return d.source.y;
}).attr("x2", function(d) {
return d.target.x;
}).attr("y2", function(d) {
return d.target.y;
});
}
var updateNode = function() {
this.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
force.on("tick", function() {
force2.start();
node.call(updateNode);
anchorNode.each(function(d, i) {
if(i % 2 == 0) {
d.x = d.node.x;
d.y = d.node.y;
} else {
var b = this.childNodes[1].getBBox();
var diffX = d.x - d.node.x;
var diffY = d.y - d.node.y;
var dist = Math.sqrt(diffX * diffX + diffY * diffY);
var shiftX = b.width * (diffX - dist) / (dist * 2);
shiftX = Math.max(-b.width, Math.min(0, shiftX));
var shiftY = 5;
this.childNodes[1].setAttribute("transform", "translate(" + shiftX + "," + shiftY + ")");
}
});
anchorNode.call(updateNode);
link.call(updateLink);
anchorLink.call(updateLink);
});
});
</script>
You can't read a file without specifying a correct path to your file system or to an external resource.
That means that you have to swap this line:
d3.json("/data/", function(error, graph) {
With something like:
d3.json("./data.json", function(error, graph) {
Or:
d3.json("http://www.somesite.com/data.json", function(error, graph) {
Also, make sure you are running a local server on your machine otherwise that call will fail.
I have a sunburst chart made in D3. Each 'petal' represents a subset of data. When a user clicks on one of the 'petals', I would like it to transition, fanning out to only show that subset (see image):
I'm having trouble getting the code to properly transition.
On click, all 'petals' (besides the selected one) should disappear and the remain paths should animate along the circle (using attrTween, arcTween, and interpolate?). The primary value that would be changing is the angleSize (var angleSize = (2 * Math.PI) / theData.length;).
I've tried using this, this, this, and this as reference without much success. What's the best way to handle the animation?
Thanks for your time!
--> See Plunker Here. <--
Code is below:
var colors = {
'Rank1' : '#3FA548',
'Rank2' : '#00B09E',
'Rank3' : '#8971B3',
'Rank4' : '#DFC423',
'Rank5' : '#E74341'
};
var $container = $('.chart'),
m = 40,
width = $container.width() - m,
height = $container.height() - m,
r = Math.min(width, height) / 2;
var study = null;
var arc = d3.svg.arc();
d3.csv('text.csv', ready);
function ready(err, data) {
if (err) console.warn('Error', err);
var svg = d3.select('.chart')
.append('svg')
.attr({
'width' : (r + m) * 2,
'height' : (r + m) * 2,
'class' : 'container'
})
.append('g')
.attr('transform', 'translate(' + (width / 4) + ', ' + (height / 2) + ' )');
var slice = svg.selectAll('.slice');
function updateChart(study) {
if (study) {
var theData = data.filter(function(d) {
return d.study_name === study;
});
} else {
var theData = data;
}
slice = slice.data(theData);
slice.enter()
.append('g')
.attr('class', 'slice');
var angleSize = (2 * Math.PI) / theData.length;
var startRadArr = [],
endRadArr = [];
for ( var i = 0; i < data.length; i++ ) {
var startRadius = (width / 20),
endRadius = startRadius;
for ( var x = 0; x < 4; x++ ) {
startRadArr.push(startRadius);
if ( x == 0 ) {
endRadius += Number(data[i].group1_score) * (width / 500);
} else if ( x == 1 ) {
endRadius += Number(data[i].group2_score) * (width / 500);
} else if ( x == 2 ) {
endRadius += Number(data[i].group3_score) * (width / 500);
} else {
endRadius += Number(data[i].group4_score) * (width / 500);
}
endRadArr.push(endRadius);
startRadius = endRadius + 0.3;
}
}
var startRadGroup = [],
endRadGroup = [];
for (i = 0; i < startRadArr.length; i += 4) {
startRadGroup.push(startRadArr.slice(i, i + 4));
}
for (i = 0; i < endRadArr.length; i += 4) {
endRadGroup.push(endRadArr.slice(i, i + 4));
}
slice.selectAll('path')
.remove();
for ( var x = 0; x < 4; x++ ) {
slice.append('path')
.attr({
'class' : function(d, i) {
if ( x == 0 ) {
return d.group1_class;
} else if ( x == 1 ) {
return d.group2_class;
} else if ( x == 2 ) {
return d.group3_class;
} else {
return d.group4_class;
}
},
'company' : function(d, i) {
return d.brand_name;
},
'cat' : function(d, i) {
if ( x == 0 ) {
return 'Group1';
} else if ( x == 1 ) {
return 'Group2';
} else if ( x == 2 ) {
return 'Group3';
} else {
return 'Group4';
}
},
'study' : function(d, i) {
return d.study_name;
},
'companyid' : function(d, i) {
return d.brand_id;
},
'startradius' : function(d, i) {
return startRadGroup[i][x];
},
'endradius' : function(d, i) {
return endRadGroup[i][x];
},
'startangle' : function(d, i) {
return angleSize * i;
},
'endangle' : function(d, i) {
return angleSize * (i + 1);
}
})
.on('click', selectStudy);
}
slice.exit()
.remove();
slice.selectAll('path')
.attr({
'd' : function(d) {
return arc({
innerRadius : +d3.select(this)[0][0].attributes.startradius.nodeValue,
outerRadius : +d3.select(this)[0][0].attributes.endradius.nodeValue,
startAngle : +d3.select(this)[0][0].attributes.startangle.nodeValue,
endAngle : +d3.select(this)[0][0].attributes.endangle.nodeValue
})
}
});
}
function selectStudy(d) {
study = $(this).attr('study');
updateChart(study);
}
updateChart();
}
EDIT
Updated the code (based on this) to include a properly working enter, update, and exit pattern. Still unsure about the transition however. Most of the examples I've linked to use something similar to d3.interpolate(this._current, a);, tweening between differing data.
In this chart, this._current and a are the same, angleSize (var angleSize = (2 * Math.PI) / theData.length;), startAngle, and endAngle are the only thing that changes.
Your problem is that you are not really binding data to the elements, and therefore the transition is not possible. I mangled your code a little bit so the data contains all the nested information about the starting and ending angles, so that it can be bound to the paths inside each slice.
Take a look at this Plunker: https://plnkr.co/edit/a7cxRplzy66Pc1arM2a9?p=preview
Here's the listing of the modified version:
var colors = {
Rank1: '#3FA548',
Rank2: '#00B09E',
Rank3: '#8971B3',
Rank4: '#DFC423',
Rank5: '#E74341'
};
// Configuration
var $container = $('.chart'),
m = 40,
width = $container.width() - m,
height = $container.height() - m,
r = Math.min(width, height) / 2;
var study = null;
var arc = d3.svg.arc();
// Load data
d3.csv('text.csv', ready);
// Data loaded callback
function ready(err, data) {
if (err) console.warn('Error', err);
var svg = d3.select('.chart')
.append('svg')
.attr({
'width': (r + m) * 2,
'height': (r + m) * 2,
'class': 'container'
})
.append('g')
.attr('transform', 'translate(' + (width / 4) + ', ' + (height / 2) + ' )');
var slices = svg.selectAll('.slice');
function updateChart(study) {
var theData = data;
if (study) {
theData = data.filter(function (d) {
return d.study_name === study;
});
}
var angleSize = (2 * Math.PI) / theData.length;
theData.forEach(function (item, i) {
var startRadius = (width / 20),
endRadius = startRadius,
groupName;
item.paths = [];
for (var g = 0; g < 4; g++) {
item.paths[g] = {};
item.paths[g].startRadius = startRadius;
groupName = 'group' + (g + 1) + '_score';
endRadius += Number(item[groupName]) * (width / 500);
item.paths[g].endRadius = endRadius;
startRadius = endRadius + 0.3;
}
});
// Set the data
slices = slices.data(theData);
// Enter
slices.enter()
.append('g')
.attr('class', 'slice');
// Exit
slices.exit()
.remove();
// Update
slices
.transition()
.duration(750)
.each(function (dSlice, iSlice) {
var slice = d3.select(this);
var paths = slice.selectAll('path');
// Set data
paths = paths.data(dSlice.paths);
// Exit
paths.exit()
.remove();
// Enter
paths.enter()
.append('path')
.attr('class', 'path');
// Update
paths
.transition()
.attr({
'class': function (d, i) {
return dSlice['group' + (i + 1) + '_class'];
},
'company': dSlice.brand_name,
'cat': function (d, i) {
return 'Group' + (i + 1);
},
'study': function (d, i) {
return dSlice.study_name;
},
'companyid': function (d, i) {
return dSlice.brand_id;
},
'startradius': function (d, i) {
return d.startRadius;
},
'endradius': function (d, i) {
return d.endRadius;
},
'startangle': function (d, i) {
return angleSize * iSlice;
},
'endangle': function (d, i) {
return angleSize * (iSlice + 1);
},
'd': function (d) {
return arc({
innerRadius: +d.startRadius,
outerRadius: +d.endRadius,
startAngle: +angleSize * iSlice,
endAngle: +angleSize * (iSlice + 1)
})
}
})
.duration(750);
paths.on('click', selectStudy);
});
function selectStudy(d, i) {
study = $(this).attr('study');
updateChart(study);
}
}
updateChart();
}
As you can see, the key is correctly preparing the data (let's say the format in your example .tsv file is not the best choice, but sometimes we can't choose our data sources...)
Then afterwards, by putting the code for the paths generation inside the .each call on the slices, the data can be accessed from the function (d, i) { ... } callbacks and every element happens to receive the corresponding data.
Another trick is using the slices data (accessed inside the .each function via the dSlice and iSlice vars) on the paths' callbacks. This way the paths can consume this data for their own purposes. In this case, the company and study_name properties.
Now in order to tweak the transition and make it more accurate, the starting attributes can change. You can try by setting up some attributes for the paths in the .enter() phase.
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.
I'm working with d3. I create a globe of countries from a json file. The globe has svg paths, and each path has an id. I want to select a path with a particular ID. How would I do that, please?
handleGlobe();
$('#panel div').click(function(){
if (this.className == 'represented') {
thisID = $(this).attr('id');
focusedCountry = d3.select('path') //??? not sure how to say this
p = d3.geo.centroid(focusedCountry);
}
...
handleGlobe() {
var feature;
var projection = d3.geo.azimuthal()
.scale(380)
.origin([-71.03,42.37])
.mode("orthographic")
.translate([380, 400]);
var circle = d3.geo.greatCircle()
.origin(projection.origin());
// TODO fix d3.geo.azimuthal to be consistent with scale
var scale = {
orthographic: 380,
stereographic: 380,
gnomonic: 380,
equidistant: 380 / Math.PI * 2,
equalarea: 380 / Math.SQRT2
};
var path = d3.geo.path()
.projection(projection);
var svg = d3.select("#globe").append("svg:svg")
.attr("width", 800)
.attr("height", 800)
.on("mousedown", mousedown);
d3.json("world-countries.json", function(collection) {
feature = svg.selectAll("path")
.data(collection.features)
.enter().append("svg:path")
.attr("d", clip)
.attr("id", function(d) { return d.id; })
.on("mouseover", pathOver)
.on("mouseout", pathOut)
.on("click", click);
feature.append("svg:title")
.text(function(d) { return d.properties.name; });
feature.each(function(){
for (var i=0; i<unrepresented.length; i++){
if ($(this).attr('id') == unrepresented[i]) {
d3.select(this).style("fill", "#ededed");
}
}
if (($(this).attr('id') == 'GRL') || ($(this).attr('id') == 'ATA')) { //Greenland and Antarctica are shapes, but not countries
d3.select(this).style("fill", "#ededed");
}
});
});
d3.select(window)
.on("mousemove", mousemove)
.on("mouseup", mouseup)
;
d3.select("select").on("change", function() {
projection.mode(this.value).scale(scale[this.value]);
refresh(750);
});
var m0,
o0;
function mousedown() {
m0 = [d3.event.pageX, d3.event.pageY];
o0 = projection.origin();
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.origin(o1);
circle.origin(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(circle.clip(d));
}
function click() {
}
function pathOver() {
}
function pathOut() {
}
//end globe
}
You can select an element by ID by prefixing the ID with "#" and using that as a selector:
d3.select("#ID");
or to select a path with that ID
d3.select("path#ID");