How to apply force repulsion on map's labels so they find their right places automatically ?
Bostock' "Let's Make a Map"
Mike Bostock's Let's Make a Map (screenshot below). By default, labels are put at the point's coordinates and polygons/multipolygons's path.centroid(d) + a simple left or right align, so they frequently enter in conflict.
Handmade label placements
One improvement I met requires to add an human made IF fixes, and to add as many as needed, such :
.attr("dy", function(d){ if(d.properties.name==="Berlin") {return ".9em"} })
The whole become increasingly dirty as the number of labels to reajust increase :
//places's labels: point objects
svg.selectAll(".place-label")
.data(topojson.object(de, de.objects.places).geometries)
.enter().append("text")
.attr("class", "place-label")
.attr("transform", function(d) { return "translate(" + projection(d.coordinates) + ")"; })
.attr("dy", ".35em")
.text(function(d) { if (d.properties.name!=="Berlin"&&d.properties.name!=="Bremen"){return d.properties.name;} })
.attr("x", function(d) { return d.coordinates[0] > -1 ? 6 : -6; })
.style("text-anchor", function(d) { return d.coordinates[0] > -1 ? "start" : "end"; });
//districts's labels: polygons objects.
svg.selectAll(".subunit-label")
.data(topojson.object(de, de.objects.subunits).geometries)
.enter().append("text")
.attr("class", function(d) { return "subunit-label " + d.properties.name; })
.attr("transform", function(d) { return "translate(" + path.centroid(d) + ")"; })
.attr("dy", function(d){
//handmade IF
if( d.properties.name==="Sachsen"||d.properties.name==="Thüringen"|| d.properties.name==="Sachsen-Anhalt"||d.properties.name==="Rheinland-Pfalz")
{return ".9em"}
else if(d.properties.name==="Brandenburg"||d.properties.name==="Hamburg")
{return "1.5em"}
else if(d.properties.name==="Berlin"||d.properties.name==="Bremen")
{return "-1em"}else{return ".35em"}}
)
.text(function(d) { return d.properties.name; });
Need for better solution
That's just not manageable for larger maps and sets of labels. How to add force repulsions to these both classes: .place-label and .subunit-label?
This issue is quite a brain storming as I haven't deadline on this, but I'am quite curious about it. I was thinking about this question as a basic D3js implementation of Migurski/Dymo.py. Dymo.py's README.md documentation set a large set of objectives, from which to select the core needs and functions (20% of the work, 80% of the result).
Initial placement: Bostock give a good start with left/right positionning relative to the geopoint.
Inter-labels repulsion: different approach are possible, Lars & Navarrc proposed one each,
Labels annihilation: A label annihilation function when one label's overall repulsion is too intense, since squeezed between other labels, with the priority of annihilation being either random or based on a population data value, which we can get via NaturalEarth's .shp file.
[Luxury] Label-to-dots repulsion: with fixed dots and mobile labels. But this is rather a luxury.
I ignore if label repulsion will work across layers and classes of labels. But getting countries labels and cities labels not overlapping may be a luxury as well.
In my opinion, the force layout is unsuitable for the purpose of placing labels on a map. The reason is simple -- labels should be as close as possible to the places they label, but the force layout has nothing to enforce this. Indeed, as far as the simulation is concerned, there is no harm in mixing up labels, which is clearly not desirable for a map.
There could be something implemented on top of the force layout that has the places themselves as fixed nodes and attractive forces between the place and its label, while the forces between labels would be repulsive. This would likely require a modified force layout implementation (or several force layouts at the same time), so I'm not going to go down that route.
My solution relies simply on collision detection: for each pair of labels, check if they overlap. If this is the case, move them out of the way, where the direction and magnitude of the movement is derived from the overlap. This way, only labels that actually overlap are moved at all, and labels only move a little bit. This process is iterated until no movement occurs.
The code is somewhat convoluted because checking for overlap is quite messy. I won't post the entire code here, it can be found in this demo (note that I've made the labels much larger to exaggerate the effect). The key bits look like this:
function arrangeLabels() {
var move = 1;
while(move > 0) {
move = 0;
svg.selectAll(".place-label")
.each(function() {
var that = this,
a = this.getBoundingClientRect();
svg.selectAll(".place-label")
.each(function() {
if(this != that) {
var b = this.getBoundingClientRect();
if(overlap) {
// determine amount of movement, move labels
}
}
});
});
}
}
The whole thing is far from perfect -- note that some labels are quite far away from the place they label, but the method is universal and should at least avoid overlap of labels.
One option is to use the force layout with multiple foci. Each foci must be located in the feature's centroid, set up the label to be attracted only by the corresponding foci. This way, each label will tend to be near of the feature's centroid, but the repulsion with other labels may avoid the overlapping issue.
For comparison:
M. Bostock's "Lets Make a Map" tutorial (resulting map),
my gist for an Automatic Labels Placement version (resulting map) implementing the foci's strategy.
The relevant code:
// Place and label location
var foci = [],
labels = [];
// Store the projected coordinates of the places for the foci and the labels
places.features.forEach(function(d, i) {
var c = projection(d.geometry.coordinates);
foci.push({x: c[0], y: c[1]});
labels.push({x: c[0], y: c[1], label: d.properties.name})
});
// Create the force layout with a slightly weak charge
var force = d3.layout.force()
.nodes(labels)
.charge(-20)
.gravity(0)
.size([width, height]);
// Append the place labels, setting their initial positions to
// the feature's centroid
var placeLabels = svg.selectAll('.place-label')
.data(labels)
.enter()
.append('text')
.attr('class', 'place-label')
.attr('x', function(d) { return d.x; })
.attr('y', function(d) { return d.y; })
.attr('text-anchor', 'middle')
.text(function(d) { return d.label; });
force.on("tick", function(e) {
var k = .1 * e.alpha;
labels.forEach(function(o, j) {
// The change in the position is proportional to the distance
// between the label and the corresponding place (foci)
o.y += (foci[j].y - o.y) * k;
o.x += (foci[j].x - o.x) * k;
});
// Update the position of the text element
svg.selectAll("text.place-label")
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; });
});
force.start();
While ShareMap-dymo.js may work, it does not appear to be very well documented. I have found a library that works for the more general case, is well documented and also uses simulated annealing: D3-Labeler
I've put together a usage sample with this jsfiddle.The D3-Labeler sample page uses 1,000 iterations. I have found this is rather unnecessary and that 50 iterations seems to work quite well - this is very fast even for a few hundred data points. I believe there is room for improvement both in the way this library integrates with D3 and in terms of efficiency, but I wouldn't have been able to get this far on my own. I'll update this thread should I find the time to submit a PR.
Here is the relevant code (see the D3-Labeler link for further documentation):
var label_array = [];
var anchor_array = [];
//Create circles
svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("id", function(d){
var text = getRandomStr();
var id = "point-" + text;
var point = { x: xScale(d[0]), y: yScale(d[1]) }
var onFocus = function(){
d3.select("#" + id)
.attr("stroke", "blue")
.attr("stroke-width", "2");
};
var onFocusLost = function(){
d3.select("#" + id)
.attr("stroke", "none")
.attr("stroke-width", "0");
};
label_array.push({x: point.x, y: point.y, name: text, width: 0.0, height: 0.0, onFocus: onFocus, onFocusLost: onFocusLost});
anchor_array.push({x: point.x, y: point.y, r: rScale(d[1])});
return id;
})
.attr("fill", "green")
.attr("cx", function(d) {
return xScale(d[0]);
})
.attr("cy", function(d) {
return yScale(d[1]);
})
.attr("r", function(d) {
return rScale(d[1]);
});
//Create labels
var labels = svg.selectAll("text")
.data(label_array)
.enter()
.append("text")
.attr("class", "label")
.text(function(d) {
return d.name;
})
.attr("x", function(d) {
return d.x;
})
.attr("y", function(d) {
return d.y;
})
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("fill", "black")
.on("mouseover", function(d){
d3.select(this).attr("fill","blue");
d.onFocus();
})
.on("mouseout", function(d){
d3.select(this).attr("fill","black");
d.onFocusLost();
});
var links = svg.selectAll(".link")
.data(label_array)
.enter()
.append("line")
.attr("class", "link")
.attr("x1", function(d) { return (d.x); })
.attr("y1", function(d) { return (d.y); })
.attr("x2", function(d) { return (d.x); })
.attr("y2", function(d) { return (d.y); })
.attr("stroke-width", 0.6)
.attr("stroke", "gray");
var index = 0;
labels.each(function() {
label_array[index].width = this.getBBox().width;
label_array[index].height = this.getBBox().height;
index += 1;
});
d3.labeler()
.label(label_array)
.anchor(anchor_array)
.width(w)
.height(h)
.start(50);
labels
.transition()
.duration(800)
.attr("x", function(d) { return (d.x); })
.attr("y", function(d) { return (d.y); });
links
.transition()
.duration(800)
.attr("x2",function(d) { return (d.x); })
.attr("y2",function(d) { return (d.y); });
For a more in depth look at how D3-Labeler works, see "A D3 plug-in for automatic label placement using simulated
annealing"
Jeff Heaton's "Artificial Intelligence for Humans, Volume 1" also does an excellent job at explaining the simulated annealing process.
You might be interested in the d3fc-label-layout component (for D3v5) that is designed exactly for this purpose. The component provides a mechanism for arranging child components based on their rectangular bounding boxes. You can apply either a greedy or simulated annealing strategy in order to minimise overlaps.
Here's a code snippet which demonstrates how to apply this layout component to Mike Bostock's map example:
const labelPadding = 2;
// the component used to render each label
const textLabel = layoutTextLabel()
.padding(labelPadding)
.value(d => d.properties.name);
// a strategy that combines simulated annealing with removal
// of overlapping labels
const strategy = layoutRemoveOverlaps(layoutGreedy());
// create the layout that positions the labels
const labels = layoutLabel(strategy)
.size((d, i, g) => {
// measure the label and add the required padding
const textSize = g[i].getElementsByTagName('text')[0].getBBox();
return [textSize.width + labelPadding * 2, textSize.height + labelPadding * 2];
})
.position(d => projection(d.geometry.coordinates))
.component(textLabel);
// render!
svg.datum(places.features)
.call(labels);
And this is a small screenshot of the result:
You can see a complete example here:
http://bl.ocks.org/ColinEberhardt/389c76c6a544af9f0cab
Disclosure: As discussed in the comment below, I am a core contributor of this project, so clearly I am somewhat biased. Full credit to the other answers to this question which gave us inspiration!
For 2D case
here are some examples that do something very similar:
one http://bl.ocks.org/1691430
two http://bl.ocks.org/1377729
thanks Alexander Skaburskis who brought this up here
For 1D case
For those who search a solution to a similar problem in 1-D i can share my sandbox JSfiddle where i try to solve it. It's far from perfect but it kind of doing the thing.
Left: The sandbox model, Right: an example usage
Here is the code snippet which you can run by pressing the button in the end of the post, and also the code itself. When running, click on the field to position the fixed nodes.
var width = 700,
height = 500;
var mouse = [0,0];
var force = d3.layout.force()
.size([width*2, height])
.gravity(0.05)
.chargeDistance(30)
.friction(0.2)
.charge(function(d){return d.fixed?0:-1000})
.linkDistance(5)
.on("tick", tick);
var drag = force.drag()
.on("dragstart", dragstart);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.on("click", function(){
mouse = d3.mouse(d3.select(this).node()).map(function(d) {
return parseInt(d);
});
graph.links.forEach(function(d,i){
var rn = Math.random()*200 - 100;
d.source.fixed = true;
d.source.px = mouse[0];
d.source.py = mouse[1] + rn;
d.target.y = mouse[1] + rn;
})
force.resume();
d3.selectAll("circle").classed("fixed", function(d){ return d.fixed});
});
var link = svg.selectAll(".link"),
node = svg.selectAll(".node");
var graph = {
"nodes": [
{"x": 469, "y": 410},
{"x": 493, "y": 364},
{"x": 442, "y": 365},
{"x": 467, "y": 314},
{"x": 477, "y": 248},
{"x": 425, "y": 207},
{"x": 402, "y": 155},
{"x": 369, "y": 196},
{"x": 350, "y": 148},
{"x": 539, "y": 222},
{"x": 594, "y": 235},
{"x": 582, "y": 185}
],
"links": [
{"source": 0, "target": 1},
{"source": 2, "target": 3},
{"source": 4, "target": 5},
{"source": 6, "target": 7},
{"source": 8, "target": 9},
{"source": 10, "target": 11}
]
}
function tick() {
graph.nodes.forEach(function (d) {
if(d.fixed) return;
if(d.x<mouse[0]) d.x = mouse[0]
if(d.x>mouse[0]+50) d.x--
})
link.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; });
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
function dblclick(d) {
d3.select(this).classed("fixed", d.fixed = false);
}
function dragstart(d) {
d3.select(this).classed("fixed", d.fixed = true);
}
force
.nodes(graph.nodes)
.links(graph.links)
.start();
link = link.data(graph.links)
.enter().append("line")
.attr("class", "link");
node = node.data(graph.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", 10)
.on("dblclick", dblclick)
.call(drag);
.link {
stroke: #ccc;
stroke-width: 1.5px;
}
.node {
cursor: move;
fill: #ccc;
stroke: #000;
stroke-width: 1.5px;
opacity: 0.5;
}
.node.fixed {
fill: #f00;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<body></body>
Related
There are many cases online how to plot couple of lines in d3 if you add svg object only once, such as
svg.selectAll("line")
.data(dataset)
.enter().append("line")
.style("stroke", "black") // colour the line
.attr("x1", function(d) { console.log(d); return xScale(d.x1); })
.attr("y1", function(d) { return yScale(d.y1); })
.attr("x2", function(d) { return xScale(d.x2); })
.attr("y2", function(d) { return yScale(d.y2); });
This plot create one line. I want to create many different lines in an array smth like
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
for (a_ind=1; a_ind<3; a_ind++){
dataset_a=dataset.filter(function(d) { return (d.a==a_ind)})
svg.selectAll("line")
.data(dataset_a) - //!!! using new dataset in each cycle
.enter().append("line")
.style("stroke", "black") // colour the line
.attr("x1", function(d) { console.log(d); return xScale(d.x1); })
.attr("y1", function(d) { return yScale(d.y1); })
.attr("x2", function(d) { return xScale(d.x2); })
.attr("y2", function(d) { return yScale(d.y2); });
}
I was told it's impossible. Or maybe there is the way? And also how to access then line from dataset_a if i want to delete it with the click of the mouse?
Well, if you want to plot lines, I suggest that you append...<line>s!
The thing with a D3 enter selection is quite simple: the number of appended elements is the number of objects in the data array that doesn't match any element.
So, you just need a data array with several objects. For instance, let's create 50 of them:
var data = d3.range(50).map(function(d) {
return {
x1: Math.random() * 300,
x2: Math.random() * 300,
y1: Math.random() * 150,
y2: Math.random() * 150,
}
});
And, as in the below demo I'm selecting null, all of them will be in the enter selection. Here is the demo:
var svg = d3.select("svg");
var data = d3.range(50).map(function(d) {
return {
x1: Math.random() * 300,
x2: Math.random() * 300,
y1: Math.random() * 150,
y2: Math.random() * 150,
}
});
var color = d3.scaleOrdinal(d3.schemeCategory20);
var lines = svg.selectAll(null)
.data(data)
.enter()
.append("line")
.attr("x1", function(d) {
return d.x1
})
.attr("x2", function(d) {
return d.x2
})
.attr("y1", function(d) {
return d.y1
})
.attr("y2", function(d) {
return d.y2
})
.style("stroke", function(_, i) {
return color(i)
})
.style("stroke-width", 1);
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
Finally, a tip: as this is JavaScript you can use for loops anywhere you want. However, do not use for loops to append elements in a D3 code. It's unnecessary and not idiomatic.
That being said, whoever told you that it is impossible was wrong, it's clearly possible. Here is a demo (but don't do that, it's a very cumbersome and ugly code):
var svg = d3.select("svg");
var data = d3.range(50).map(function(d, i) {
return {
x1: Math.random() * 300,
x2: Math.random() * 300,
y1: Math.random() * 150,
y2: Math.random() * 150,
id: "id" + i
}
});
var color = d3.scaleOrdinal(d3.schemeCategory20);
for (var i = 0; i < data.length; i++) {
var filteredData = data.filter(function(d) {
return d.id === "id" + i
});
var lines = svg.selectAll(null)
.data(filteredData)
.enter()
.append("line")
.attr("x1", function(d) {
return d.x1
})
.attr("x2", function(d) {
return d.x2
})
.attr("y1", function(d) {
return d.y1
})
.attr("y2", function(d) {
return d.y2
})
.style("stroke", function() {
return color(i)
})
.style("stroke-width", 1);
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
I would do something like this. Make each data set (1 data set per line), an array inside the final data array .enter().append() will then work properly. To remove the line on click, I added an event handler that will select the line just clicked and remove it.
var data = [[dataset_a], [dataset_b], [dataset_c], [dataset_d], [dataset_e]];
var xValue = function(d){return d.x;}
var yValue = function(d){return d.y;}
var lineFunction = d3.line()
.x(function(d) { return xScale(xValue(d)); })
.y(function(d) { return yScale(yValue(d)); });
var lines = d3.select("svg").selectAll("path")
lines.data(data)
.enter().append("path")
.attr("d", lineFunction)
.on("click", function(d){
d3.select(this).remove();
});
I have rewritten most of my d3 code to v4, but the new update pattern is throwing me off. The example below is for a force diagram. A duplicate circle is created within the first container upon every update. The data in my example does not actually change, but it's irrelevant. If I use new data, the same issue (a duplicate circle) occurs.
var w = 800,
h = 500;
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h);
var dataset = {};
function setData() {
dataset.nodes = [{
value: 200
}, {
value: 100
}, {
value: 50
}];
}
setData();
var rScale = d3.scaleSqrt()
.range([0, 100])
.domain([0, d3.max(dataset.nodes.map(function(d) {
return d.value;
}))]);
var node = svg.append("g")
.attr("class", "nodes")
.attr("transform", "translate(" + w / 2 + "," + h / 2 + ")")
.selectAll(".node");
var simulation = d3.forceSimulation(dataset.nodes)
.force("charge", d3.forceManyBody().strength(-1600))
.force("x", d3.forceX())
.force("y", d3.forceY())
.alphaDecay(.05)
.on("tick", ticked);
function ticked() {
node.selectAll("circle")
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
}
function restart() {
// Apply the general update pattern to the nodes.
node = node.data(dataset.nodes, function(d) {
return d.id;
});
node.exit().remove();
node = node.enter().append("g")
.attr("class", "node")
.merge(node);
node.append("circle")
.attr("r", function(d) {
return rScale(d.value);
});
// Update and restart the simulation.
simulation.nodes(dataset.nodes);
simulation.alpha(1).restart();
}
restart();
function update() {
setData();
restart();
}
d3.select("#update").on("click", update);
If you click the Update button in this codepen (https://codepen.io/cplindem/pen/wpQbQe), you will see all three circles animate as the simulation restarts, but behind the largest circle, there is another, identical circle that does not animate. You can also see the new circle appear in the html if you inspect it.
What am I doing wrong?
Your first problem seems to be that you are keying the data on an 'id' field, but your data doesn't have any ids, so that needs changed or you just keep adding new groups:
function setData() {
dataset.nodes = [{
value: 200,
id: "A"
}, {
value: 100,
id: "B"
}, {
value: 50,
id: "C"
}];
console.log("dataset", dataset);
}
The second problem is you merge the new and updated selection and then append new circles to all of them, even the existing ones (so you have multiple circles per group on pressing update). I got it to work by doing this: make the new nodes, merge with existing selection, add circles to just the new nodes, update the circles in all the nodes:
node.exit().remove();
var newNodes = node.enter().append("g");
node = newNodes
.attr("class", "node")
.merge(node);
newNodes.append("circle");
node.select("circle")
.attr("r", function(d) {
return rScale(d.value);
});
Whether that 2nd bit is optimal I don't know, I'm still more anchored in v3 myself...
https://codepen.io/anon/pen/WdLexR
I'm trying to conditionally color these voronoi segments based on the 'd.lon' value. If it's positive, I want it to be green, if it's negative I want it to be red. However at the moment it's returning every segment as green.
Even if I swap my < operand to >, it still returns green.
Live example here: https://allaffects.com/world/
Thank you :)
JS
// Stating variables
var margin = {top: 20, right: 40, bottom: 30, left: 45},
width = parseInt(window.innerWidth) - margin.left - margin.right;
height = (width * .5) - 10;
var projection = d3.geo.mercator()
.center([0, 5 ])
.scale(200)
.rotate([0,0]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var path = d3.geo.path()
.projection(projection);
var voronoi = d3.geom.voronoi()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.clipExtent([[0, 0], [width, height]]);
var g = svg.append("g");
// Map data
d3.json("/world-110m2.json", function(error, topology) {
// Cities data
d3.csv("/cities.csv", function(error, data) {
g.selectAll("circle")
.data(data)
.enter()
.append("a")
.attr("xlink:href", function(d) {
return "https://www.google.com/search?q="+d.city;}
)
.append("circle")
.attr("cx", function(d) {
return projection([d.lon, d.lat])[0];
})
.attr("cy", function(d) {
return projection([d.lon, d.lat])[1];
})
.attr("r", 5)
.style("fill", "red");
});
g.selectAll("path")
.data(topojson.object(topology, topology.objects.countries)
.geometries)
.enter()
.append("path")
.attr("d", path)
});
var voronoi = d3.geom.voronoi()
.clipExtent([[0, 0], [width, height]]);
d3.csv("/cities.csv", function(d) {
return [projection([+d.lon, +d.lat])[0], projection([+d.lon, +d.lat]) [1]];
}, function(error, rows) {
vertices = rows;
console.log(vertices);
drawV(vertices);
}
);
function polygon(d) {
return "M" + d.join("L") + "Z";
}
function drawV(d) {
svg.append("g")
.selectAll("path")
.data(voronoi(d), polygon)
.enter().append("path")
.attr("class", "test")
.attr("d", polygon)
// This is the line I'm trying to get to conditionally fill the segment.
.style("fill", function(d) { return (d.lon < 0 ? "red" : "green" );} )
.style('opacity', .7)
.style('stroke', "pink")
.style("stroke-width", 3);
}
JS EDIT
d3.csv("/static/cities.csv", function(data) {
var rows = [];
data.forEach(function(d){
//Added third item into my array to test against for color
rows.push([projection([+d.lon, +d.lat])[0], projection([+d.lon, +d.lat]) [1], [+d.lon]])
});
console.log(rows); // data for polygons and lon value
console.log(data); // data containing raw csv info (both successfully log)
svg.append("g")
.selectAll("path")
.data(voronoi(rows), polygon)
.enter().append("path")
.attr("d", polygon)
//Trying to access the third item in array for each polygon which contains the lon value to test
.style("fill", function(data) { return (rows[2] < 0 ? "red" : "green" );} )
.style('opacity', .7)
.style('stroke', "pink")
.style("stroke-width", 3)
});
This is what's happening: your row function is modifying the objects of rows array. At the time you get to the function for filling the polygons there is no d.lon anymore, and since d.lon is undefined the ternary operator is evaluated to false, which gives you "green".
Check this:
var d = {};
console.log(d.lon < 0 ? "red" : "green");
Which also explains what you said:
Even if I swap my < operand to >, it still returns green.
Because d.lon is undefined, it doesn't matter what operator you use.
That being said, you have to keep your original rows structure, with the lon property in the objects.
A solution is getting rid of the row function...
d3.csv("cities.csv", function(data){
//the rest of the code
})
... and creating your rows array inside the callback:
var rows = [];
data.forEach(function(d){
rows.push([projection([+d.lon, +d.lat])[0], projection([+d.lon, +d.lat]) [1]])
});
Now you have two arrays: rows, which you can use to create the polygons just as you're using now, and data, which contains the lon values.
Alternatively, you can keep everything in just one array (just changing your row function), which is the best solution because it would make easier to get the d.lon values inside the enter selection for the polygons. However, it's hard providing a working answer without testing it with your actual code (it normally ends up with the OP saying "it's not working!").
Original Code and Visualization is located at: http://bl.ocks.org/Guerino1/6aa3861bcbe96c343103
I am trying to chain transitions for rectangles. When I transition, I believe I am overwriting the "x" attribute, using the code:
rectangle.transition()
.ease("linear")
.duration(1500)
.attr("x", function(){
if(x == orig_x){
var retVal = dataSet.svgWidth-Number(width);
return retVal;
}
else{
var retVal = Number(orig_x);
return retVal;
}
})
The issue seems to be that the above code does not overwrite the "x" value, when the transition is executed, the first time. As I step through the debugger, the next time I step through the flip() function, "x" is still set to its original value, even though it appears that retValue returned a different value the last time through (for that specific rectangle). NOTE: I use different colors to be sure I'm working with consistent rectangles.
This code is wrapped in a function called "flip()" that is called by a while loop that is intended to flip the value of "x" back and forth between the original value of "x" (stored in "orig_x") and the width of the svg canvas minus the original width of the rectangle. The intent is a visualization that causes the rectangles to keep shifting "horizontally," back and forth.
The original data set is:
var dataSet7 = [];
dataSet7.svgWidth = 400;
dataSet7.svgHeight = 95;
dataSet7.r1 = {"x": 0, "y": 0, "w": 50, "h": 30, "color": "Red"};
dataSet7.r2 = {"x": 10, "y": 30, "w": 150, "h": 30, "color": "Yellow"};
dataSet7.r3 = {"x": 20, "y": 60, "w": 90, "h": 30, "color": "Blue"};
The HTML div that gets replaced with the chart is:
<td class="td_tableBody" colspan="1">
<div class="div_RootBody">
<h3 class="h3_Body">Continuous Transition</h3>
<p>Transitions the x-axis continuously.</p>
<div id="simple_rectangle9"></div>
</div>
</td>
The for the function that gets called is:
function drawRectangle9( dataSet, selectString ) {
function flip(){
var rectangle = d3.select(this);
var width = rectangle.attr("width");
var x = rectangle.attr("x");
var orig_x = rectangle.attr("orig_x");
// Just for debug info...
var y = rectangle.attr("y");
var height = rectangle.attr("height");
var color = rectangle.attr("color");
rectangle.transition()
.ease("linear")
.duration(1500)
.attr("x", function(){
if(x == orig_x){
var retVal = dataSet.svgWidth-Number(width);
return retVal;
}
else{
var retVal = Number(orig_x);
return retVal;
}
})
};
// Extract Rectangles from dataSet
var rectangles = [];
rectangles[0] = dataSet.r1;
rectangles[1] = dataSet.r2;
rectangles[2] = dataSet.r3;
var svgContainer = d3.select(selectString).append("svg:svg")
.attr("width", dataSet.svgWidth)
.attr("height", dataSet.svgHeight);
var arrayOfRectangles = svgContainer.selectAll("rect")
.data(rectangles)
.enter().append("svg:rect")
.attr("class", "rect_flip1")
.attr("x", function(d){ return d.x; })
.attr("orig_x", function(d){ return d.x; })
.attr("y", function(d){ return d.y; })
.attr("width", function(d){ return d.w; })
.attr("height", function(d){ return d.h; })
.attr("color", function(d){ return d.color; })
.style("fill", function(d){ return d.color; });
var i = 0;
while(i++ < 10){
var rectangles = d3.selectAll(".rect_flip1")
rectangles.each(flip);
}
}
The function call that executes the above function is:
drawRectangle9(dataSet7, "#simple_rectangle9");
My Question: What's the best way to properly transition the rectangles, back and forth horizontally, indefinitely?
The advice from ee2Dev was good but not specific. Below is the specific code that corrects the problem.
The solution is to create a function that loops back on itself using the ".each("end", flip)" method. Then, trigger the function with one single call of that function (e.g. "flip();").
function drawRectangle9( dataSet, selectString ) {
// Extract Rectangles from dataSet
var rectangles = [];
rectangles[0] = dataSet.r1;
rectangles[1] = dataSet.r2;
rectangles[2] = dataSet.r3;
var svgContainer = d3.select(selectString).append("svg:svg")
.attr("width", dataSet.svgWidth)
.attr("height", dataSet.svgHeight);
var arrayOfRectangles = svgContainer.selectAll("rect")
.data(rectangles)
.enter().append("svg:rect")
.attr("class", "rect_flip1")
.attr("x", function(d){ return d.x; })
.attr("orig_x", function(d){ return d.x; })
.attr("y", function(d){ return d.y; })
.attr("width", function(d){ return d.w; })
.attr("height", function(d){ return d.h; })
.attr("color", function(d){ return d.color; })
.style("fill", function(d){ return d.color; });
var flip = function(){
var selectedRectangles = d3.selectAll(".rect_flip1");
selectedRectangles.transition()
.ease("linear")
.duration(1500)
.attr("x", function(d,i){
var rect = d3.select(this)
var x = rect.attr("x")
var orig_x = rect.attr("orig_x")
var width = rect.attr("width")
if(x == orig_x){
var retVal = dataSet.svgWidth-width;
return retVal;
}
else{
var retVal = orig_x;
return retVal;
}
})
.each("end", flip);
};
flip();
}
I'm trying to construct a directed force graph in d3.js that has a click listener on it that changes the the underlying data and redraws the graph. I belive I'm following Mr. Bostock's update pattern, but the issue that I'm having is that when I run the update triggered by the click listener the nodes disappear off the bottom of the screen leaving the links and labels behind.
This update seems to run, updates the existing nodes (turns them green in this case) then ignores the "enter" and "exit" sections (which is the desired behaviour) then hits the tick() function which send the nodes south.
I can get this working by removing the "g" tag on the node and thus decoupling the labels and the node, which is obviously not desirable.
I can't help feeling I'm missing something obvious! Or perhaps I should be tacking this a different way?
Here's the code:
var width = 960,
height = 500,
links,
nodes,
root;
var force = d3.layout.force()
.size([width, height])
.charge(-200)
.linkDistance(50)
.on("tick", tick);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var link = svg.selectAll(".link"),
node = svg.selectAll(".node");
d3.json("test.json", function(json) {
root = json;
update();
});
function update() {
nodes = root.nodes
links = root.links
// Restart the force layout.
force
.nodes(nodes)
.links(links)
.start();
svg.append("svg:defs").append("marker")
.attr("id", "end")
.attr("refX", 15)
.attr("refY", 2)
.attr("markerWidth", 6)
.attr("markerHeight", 4)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M 0,0 V 4 L8,2 Z");
// Update the links…
//link = link.data(links, function(d) { return d.target.name; });
link = link.data(links)
// Exit any old links.
link.exit().remove();
// Enter any new links.
link.enter().insert("svg:line", ".node")
.attr("class", "link")
.attr("marker-end", "url(#end)")
.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; });
// Update the nodes…
node = svg.selectAll("g").select(".node").data(nodes, function(d) { return d.name; });
node.style("fill", "green")
// Exit any old nodes.
node.exit().remove();
// Enter any new nodes.
node.enter().append("g")
.append("svg:circle")
.attr("class", "node")
.attr("id", function(d) {return "node" + d.index; })
.attr("r", 12)
.style("fill", "#BBB")
.on("click", click)
.call(force.drag);
node.append("svg:text")
.attr("dx", 16)
.attr("dy", ".15em")
.attr("class", "nodelabel")
.text(function(d) { return d.name });
}
function tick() {
link.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; });
node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
}
function click(d) {
if (!d3.event.defaultPrevented) {
// DO ANYTHING
update()
}
}
and the contents of test.json is:
{
"nodes": [
{"name" : "John"},
{"name" : "Alison"},
{"name" : "Phil"},
{"name" : "Jim"},
{"name" : "Jane"},
{"name" : "Mary"},
{"name" : "Joe"}
],
"links": [
{"source": 1, "target": 0},
{"source": 2, "target": 0},
{"source": 3, "target": 0},
{"source": 4, "target": 0},
{"source": 5, "target": 1},
{"source": 6, "target": 1}
]
}
OK, so I figured out the problem. When I was selecting the nodes to update I was selecting the nodes (that is, the elements that had the class "node") and they were being updated:
node = svg.selectAll("g").select(".node").data(nodes, function(d) { return d.name; });
Then in the tick function I was updating those nodes:
node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
so that was quite right, however, the nodes were encapsulated in the "g" tag along with the text label, but the tick() function was only acting on the node. The fix was to force the transform attribute in tick() to update the whole group rather than just the node:
svg.selectAll("g").attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
All works now!