I'm rendering data with d3. I have a nodes list from which I construct a bunch of SVG groups using .enter(), like this (simplified):
nodes = [{name: "Fred"}, {name: "Barney"}];
var node = vis.selectAll(".node")
.data(nodes)
.enter().append("svg:g")
.attr("class", "node");
var circ = node.append("svg:circle")
.attr("x", 0)
.attr("y", 0)
.attr("r", 10)
node.append("svg:text")
.attr("dx", 0)
.attr("dy", 0)
.text(function(d) { return d.name });
However, if I edit a listitem's name, the SVG text element does not update (because its text attr is only called on creation). How do I tell d3 to "recreate" the SVG group corresponding to a changed listitem? I'm doing this as part of a force layout, so I could set the text attr on every tick, but that's rather inelegant.
What's needed is to feed the data back into the nodes again. So, when I've updated the nodes list, I do this:
vis.selectAll(".node text")
.data(nodes)
.text(function(d) { return d.name });
and that recalculates.
Related
I'm new to D3 and am trying to build a table like structure out of rectangles. I would like the header to be a different color than the rest of the rectangles. I've written the following code:
table = svgContainer.selectAll('rect')
.data([managedObj])
.enter()
.append('rect')
.attr("width", 120)
.attr("height", 20)
.attr("fill", "blue")
.text(function(d) {
return d.name;
});
// create table body
table.selectAll('rect')
.data(managedObj.data)
.enter()
.append('rect')
.attr("y", function() {
shift += 20;
return shift;
})
.attr("width", 120)
.attr("height", 20)
.attr("fill", "red")
.text(function(d) {
return d.name;
});
This is producing the following results:
This is almost what I intended except it is nesting the second group of rectangles inside the first rectangle. This causes only the first blue rectangle to be visible. I'm assuming this has something to do with calling the data method twice. How can I fix this issue?
I think I understand the intended result, so I'll give it a go:
This line :
table.selectAll('rect')
is selecting the rectangle just created here:
table = svgContainer.selectAll('rect')....append('rect')....
You don't want to append rectangles to that rectangle (or any rectangle for that matter) because this won't work, but you do want to append them to the SVG itself.
So instead of table.selectAll you should be using svgContainer.selectAll, but there are two other issues:
if you use svgContainer.selectAll('rect') you will be selecting the rect you have already appended, when you actually want an empty selection. See the answer here.
you cannot place text in a rect (See answer here), instead you could append g elements and then append text and rect elements to those. And, for ease of positioning, you could translate the g elements so that positioning the rectangles and text is more straight forward.
So, your code could look like:
var data = ["test1","test2","test3","test4"];
var svgContainer = d3.select('body').append('svg').attr('width',900).attr('height',400);
var header = svgContainer.selectAll('g')
.data([data])
.enter()
.append('g')
.attr('transform','translate(0,0)');
header.append('rect')
.attr("width", 120)
.attr("height", 20)
.attr("fill", "blue");
header.append('text')
.attr('y',15)
.attr('x',5)
.text(function(d) {
return "header";
});
// create table body
var boxes = svgContainer.selectAll('.box')
.data(data)
.enter()
.append('g')
.attr('class','box')
.attr('transform',function(d,i) { return 'translate(0,'+((i+1)*20)+')'; });
boxes.append('rect').attr("width", 120)
.attr("height", 20)
.attr("fill", "red");
boxes.append('text')
.attr('y',15)
.attr('x',5)
.text(function(d) {
return d;
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
I have a table with filtered data that's working properly and now I'm trying to make a corresponding barchart. The barchart consists of a group for each bar, with two text elements and a rect inside of it. The exit selection successfully removes the g element but the internal rect and text somehow ends up in another g.
function updateSvg(data) {
parameters.svg = d3.select(".svg")
.attr("height", data.length * parameters.barHeight)
var life_expectancy = d3.extent(data.map(getter('life_expectancy')));
var min = life_expectancy[0];
var max = life_expectancy[1];
var x = d3.scale.linear()
.domain([0, max])
.range([0, parameters.svgWidth])
// Data join.
var groups = parameters.svg.selectAll("g")
.data(data)
// Enter.
var groupsEnter = groups.enter().append("g").attr("transform", function(d, i) { return "translate(0," + i * parameters.barHeight + ")"; })
// Update.
var bars = groups.append("rect")
.attr("width", function(d) { return x(d.life_expectancy)})
.attr("height", parameters.barHeight - 1)
var labels = groups.append("text")
.attr("x", 20)
.attr("y", parameters.barHeight / 2)
.attr("dy", ".35em")
.text(function(d) { return d.name; })
var values = groups.append("text")
.attr("x", function(d) { return x(d.life_expectancy) - 50; })
.attr("y", parameters.barHeight / 2)
.attr("dy", ".35em")
.text(function(d) { return d.life_expectancy})
// Exit.
groups.exit().remove()
}
Here's what I have working so far: http://chrisdaly.github.io/D3/World%20Countries%20Rank/table.html. If you untick all the continents except Oceania for example and inspect the bars, it shows a tonne of different rects etc hidden underneath the correct one. Any guidance is appreciated!
The problem is here
groups.exit().remove()
On slider motion the values with in the country array will change but none of the g group DOM will get removed because the array still has the same number of array elements. So on that g group you go on appending rect and text.
groups.append("rect")
.attr("width", function(d) { return x(d.life_expectancy)})
.attr("height", parameters.barHeight - 1)
Now when you tick off Americas the g tag for USA will go which is what exit function does. Reason: your array is filtered has no record for USA.
But the g for Asia countries and others you append the text and rect again thus it keeps growing.
Best way out is when you update do this to remove all rect and text:
groups.selectAll("text").remove();
groups.selectAll("rect").remove();
I'm trying to learn d3 library and include a list to a tree. Something like:
JSFiddle: link
I don't think I understand the concept of adding data to a node well. Did go through this link: How selection work in d3?
You can do this with a subselection:
// keep reference to appended text elements
var someText = nodeEnter.append("text")
.attr("class", "properties")
.attr("text-anchor", "middle")
.attr("y", 52)
.style("fill-opacity", 1);
// sub-select them with properties array
someText.selectAll('tspan')
.data(function(d) {
return d.properties || []; // catch situation where child has no properties
})
.enter()
.append('tspan')
.attr("x", 0)
.attr('dy', "0.9em")
.text(function(d) {
return d;
});
Updated fiddle.
I am just starting to learn D3 and currently working through understanding this example.
I am trying to get text to display next to each node in the graph. In the JSON data, there is a key named name, which contains the name of a character from Les Misérables. I am trying to display this text next to each node using this code:
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", 5)
.style("fill", "black")
.text(function(d) { return d.name;}).style("font-family", "Arial").style("font-size", 12)
.call(force.drag);
I've basically just altered the .text to try and display text. Can anyone explain how I can get text to display? Thanks in advance.
The SVG <circle> element will not display any text that you place inside it using the .text function. A way to display the text would be to make the nodes of the graph SVG <g> elements. These can then contain a <circle> and a <text> child elements.
This could look like this :
\\create graph nodes using groups
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("g").call(force.drag);
node.append("circle")
.attr("class", "node")
.attr("r", 5)
.style("fill", function (d) { return color(d.group); });
node.append("text")
.text(function (d) { return d.name; });
You would also need to change the force so that it transforms the group <g> on a tick.
force.on("tick", function () {
...
node.attr("transform", function (d) { return "translate(" + d.x + "," + d.y + ")"; });
});
You can see it in action for a small graph at http://jsfiddle.net/vrWDc/19/. This method may not be very efficient when applied to a large graph due to the amount of text that needs to be rendered.
In this code, http://enjalot.com/inlet/4124664/
of which the main part is:
function render(data) {
var nodz = svg.selectAll(".node")
.data(data);
nodz.enter().append("g")
.attr("class", "node")
.attr("id", function(d) { return "id"+d[0]+d[1]; })
.attr("x",0)
.attr("y", 0)
.attr("transform", function(d) {
return "translate(" + x(d[0]) + "," + y(d[1]) + ")";
})
.append("text")
.attr("x", 0)
.attr("y", 0)
.attr("stroke", "black")
.text(function(d) {return d[2]; });
// update
nodz.selectAll("text")
.text(function(d) {
return d[2]; });
// another go
d3.selectAll("text")
.text(function(d) {
return d[2]; });
}
// data in form [x pos, y pos, value to display]
var nodes = [[0,0,0],[1,1,0],[1, -1,0],[2,0,0], [2,2,0]];
render(nodes);
nodes2 = [[0,0,1],[1,1,1],[1, -1,1],[2,0,1], [2,2,1], [2, -2,1]];
render(nodes2);
I call the code to draw some nodes twice.
I expect it to draw five nodes with a value of zero in the first pass,
Then I add another item to the list and update all the values to 1 so expect to see all the values change to 1 and a new node appear. Instead, I'm only seeing the last one being set to 1. I've tried adding a unique id to bind the node to the data but this isn't working. Also tried reselecting to see if the data is now bound. In all the tutorials I've been through, just calling the selectAll().data() part updates the data, what am I missing?
The second optional argument to .data() is a function that tells d3 how to match elements. That's where you need to compare your IDs, see the documentation. That said, it should work without IDs in your case as it matches by index by default.
The problem with updating the text is that after calling .selectAll() you need to call .data() again to let d3 know what you want to match to that selection (i.e. that the new data should be bound to the old data).