D3.js: how to follow general update pattern to transition nested elements? - d3.js

I am working with D3.js, and learning the general update pattern.
I understand it for simple data structures (I think!), but I would like to create a nested set of DOM elements: groups which contain paths and text elements. I'm not clear on how to access the update/enter/exit selection for the nested elements.
In summary, this is the SVG structure that I want to end up with:
<g class="team">
<path class="outer" d="..."></path>
<text class="legend" x="890" dy=".2em" y="23">Red Sox</text>
</g>
<g class="team">
<path class="outer" d="..."></path>
<text class="legend" x="890" dy=".2em" y="23">Yankees</text>
</g>
And I'd like to be able to access the update/enter/select selection explicitly on each element. My data looks like this:
[
{ "name": "Red Sox", "code": "RED", "results": [...] },
{ "name": "Yankees", "code": "YAN", "results": [...] },
]
And this is my code - see it in full at the jsfiddle: http://jsfiddle.net/e2vdt/6/
function update(data) {
// Trying to follow the general update pattern:
// http://bl.ocks.org/mbostock/3808234
// Join new data with old elements, if any.
var g = vis.selectAll("g.team").data(data, function(d) {
return d.code;
});
// Update old elements as needed.
// QUESTION: How to get the update selection for the text elements?
// Currently the text element is not moving to the right position.
g.selectAll("text.legend").transition()
.duration(750)
.attr("y", function(d, i) { return i * 32 + 100; });
// Create new elements as needed.
var gEnter = g.enter()
.append("g").attr("class","team");
gEnter.append("text").attr("class", "legend")
.attr("dy", ".35em")
.attr("y", function(d, i) { return i * 32 + 100; })
.attr("x", "20")
.style("fill-opacity", 1e-6)
.text(function(d) { return d.name; })
.transition()
.duration(750)
.style("fill-opacity", 1);
// TBA: Add path element as well.
// Remove old elements as needed.
g.exit()
.transition()
.duration(750)
.attr("y", "0")
.style("fill-opacity", 1e-6)
.remove();
}
The enter and exit selections are both working fine, but I don't know how to get the update selection so I can move the text labels to the correct position. The "Red Sox" entry should be moving down the page, but it isn't.

It's working fine, you just have to change how you position the g element.
g.transition()
.duration(750)
.attr("transform", function(d, i) {
return "translate(0,"+(i * 32 + 100)+")";
});
The <g> element has no x & y attributes, so you must position it using svg transforms.
Here's some more information about it: https://developer.mozilla.org/en-US/docs/SVG/Element/g
The adjusted example: http://jsfiddle.net/e2vdt/10/

Related

D3.js get data index of shape on mouseover event

I am attempting to access the data index of a shape on mouseover so that I can control the behavior of the shape based on the index.
Lets say that this block of code lays out 5 rect in a vertical line based on some data.
var g_box = svg
.selectAll("g")
.data(controls)
.enter()
.append("g")
.attr("transform", function (d,i){
return "translate("+(width - 100)+","+((controlBoxSize+5)+i*(controlBoxSize+ 5))+")"
})
.attr("class", "controls");
g_box
.append("rect")
.attr("class", "control")
.attr("width", 15)
.attr("height", 15)
.style("stroke", "black")
.style("fill", "#b8b9bc");
When we mouseover rect 3, it transitions to double size.
g_box.selectAll("rect")
.on("mouseover", function(d){
d3.select(this)
.transition()
.attr("width", controlBoxSize*2)
.attr("height", controlBoxSize*2);
var additionalOffset = controlBoxSize*2;
g_box
.attr("transform", function (d,i){
if( i > this.index) { // want to do something like this, what to use for "this.index"?
return "translate("+(width - 100)+","+((controlBoxSize+5)+i*(controlBoxSize+5)+additionalOffset)+")"
} else {
return "translate("+(width - 100)+","+((controlBoxSize+5)+i*(controlBoxSize+5))+")"
}
})
})
What I want to do is move rect 4 and 5 on mouseover so they slide out of the way and do not overlap rect 3 which is expanding.
So is there a way to detect the data index "i" of "this" rect in my mouseover event so that I could implement some logic to adjust the translate() of the other rect accordingly?
You can easily get the index of any selection with the second argument of the anonymous function.
The problem here, however, is that you're trying to get the index in an anonymous function which is itself inside the event handler, and this won't work.
Thus, get the index in the event handler...
selection.on("mouseover", function(d, i) {
//index here ---------------------^
... and, inside the inner anonymous function, get the index again, using different parameter name, comparing them:
innerSelection.attr("transform", function(e, j) {
//index here, with a different name -----^
This is a simple demo (full of magic numbers), just to show you how to do it:
var svg = d3.select("svg");
var data = d3.range(5);
var groups = svg.selectAll("foo")
.data(data)
.enter()
.append("g");
var rects = groups.append("rect")
.attr("y", 10)
.attr("x", function(d) {
return 10 + d * 20
})
.attr("width", 10)
.attr("height", 100)
.attr("fill", "teal");
groups.on("mouseover", function(d, i) {
d3.select(this).select("rect").transition()
.attr("width", 50);
groups.transition()
.attr("transform", function(e, j) {
if (i < j) {
return "translate(40,0)"
}
})
}).on("mouseout", function() {
groups.transition().attr("transform", "translate(0,0)");
rects.transition().attr("width", 10);
})
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
PS: don't do...
g_box.selectAll("rect").on("mouseover", function(d, i){
... because you won't get the correct index that way (which explain your comment). Instead of that, attach the event to the groups, and get the rectangle inside it.
I'm pretty sure d3 passes in the index as well as the data in the event listener.
So try
.on("mouseover", function(d,i)
where i is the index.
Also you can take a look at a fiddle i made a couple months ago, which is related to what you're asking.
https://jsfiddle.net/guanzo/h1hdet8d/1/
You can find the index usign indexOf(). The second argument in the event mouseover it doesn't show the index in numbers, it shows the data info you're working, well, you can pass this info inside indexOf() to find the number of the index that you need.
.on("mouseover", (event, i) => {
let index = data.indexOf(i);
console.log(index); // will show the index number
})

d3.js trying to call data method twice on same selector produces strange results

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>

Appending another element when a sibling element's transition ends

I have a bar chart, which I am using transitions to animate the heights of rect elements like so:
//Create a layer for each category of data that exists, as per dataPointLegend values
//e.g. DOM will render <g class="successful"><g>
layers = svg.selectAll('g.layer')
.data(stacked, function(d) {
return d.dataPointLegend;
})
.enter()
.append('g')
.attr('class', function(d) {
return d.dataPointLegend;
})
//transform below is used to shift the entire layer up by one pixel to allow
//x-axis to appear clearly, otherwise bars inside layer appear over the top.
.attr('transform', 'translate(0,-1)');
//Create a layer for each datapoint object
//DOM will render <g class="successful"><g></g><g>
barLayers = layers.selectAll('g.layer')
.data(function(d) {
return d.dataPointValues;
})
.enter()
.append('g');
//Create rect elements inside each of our data point layers
//DOM will render <g class="successful"><g><rect></rect></g></g>
barLayers
.append('rect')
.attr('x', function(d) {
return x(d.pointKey);
})
.attr('width', x.rangeBand())
.attr('y', height - margin.bottom - margin.top)
.attr('height', 0)
.transition()
.delay(function(d, i) {
return i * transitionDelayMs;
})
.duration(transitionDurationMs)
.attr('y', function(d) {
return y(d.y0 + d.pointValue);
})
.attr('height', function(d) {
return height - margin.bottom - margin.top - y(d.pointValue)
});
I then have a further selection used for appending text elements
//Render any point labels if present
//DOM will render <g><g><rect></rect><text></text></g></g>
if (width > miniChartWidth) {
barLayers
.append('text')
.text(function(d) {
return d.pointLabel
})
.attr('x', function(d) {
return x(d.pointKey) + x.rangeBand() / 2;
})
.attr('y', function(d) {
var textHeight = d3.select(this).node().getBoundingClientRect().height;
//Position the text so it appears below the top edge of the corresponding data bar
return y(d.y0 + d.pointValue) + textHeight;
})
.attr('class', 'data-value')
.attr('fill-opacity', 0)
.transition()
.delay(function(d, i) {
return i * transitionDelayMs + transitionDurationMs;
})
.duration(transitionDurationMs)
.attr('fill-opacity', 1);
}
This fades in the text elements nicely after all the rects have finished growing in height. What I wondered, was whether its possible to append a text element to the corresponding layer as each bar finishes its transition?
I have seen the answer on this SO - Show text only after transition is complete d3.js
Which looks to be along the lines of what I am after, I tried adding an .each('end',...) in my rect rendering cycle like so
.each('end', function(d){
barLayers
.append('text')
.text(function() {
return d.pointLabel
})
.attr('x', function() {
return x(d.pointKey) + x.rangeBand() / 2;
})
.attr('y', function() {
var textHeight = d3.select(this).node().getBoundingClientRect().height;
//Position the text so it appears below the top edge of the corresponding data bar
return y(d.y0 + d.pointValue) + textHeight;
})
.attr('class', 'data-value')
.attr('fill-opacity', 0)
.transition()
.delay(function(d, i) {
return i * transitionDelayMs + transitionDurationMs;
})
.duration(transitionDurationMs)
.attr('fill-opacity', 1);
});
But I end up with lots of text elements for each of my g that holds a single rect for each of my datapoints.
I feel like I'm close, but need some assistance from you wise people :)
Thanks
whateverTheSelectionIs
.each('end', function(d){
barLayers
.append('text')
.each runs separately for every element in your selection, and inside the each you're adding text elements to every barLayer (barLayers). So you're going to get a (barLayers.size() * selection.size()) number of text elements added overall. You need to add only one text element in the each to the right bar / g.
The below is a fudge that might work. It's tricky because the text you want to add is a sibling of the rects in the selection that calls the .each function..., d3.select(this.parentNode) should move you up to the parent of the rect, which would be the right barLayer.
whateverTheSelectionIs
.each('end', function(d,i){
d3.select(this.parentNode)
.append('text')

D3 how to make dynamic sequential transitions work using transform/translate and offset coordinates?

I am trying to create an interactive org chart such that when I click on a box that box is repositioned in the centre of the SVG container and all other elements transition as well but remain in the same relative position. So if you click the top box in the list, they all move down together. Then if you click one of the lower boxes they all move up together but always so the selected box is in the centre. If you click on a box which is already in the middle it should not move but at the moment they are flying all over the place.
I have got this working for the first click but on each subsequent click the boxes start flying all over the place. I am using the mouse listener to get the current position and calculate an offset to centre the selected box that I feed into transform/translate. I think this is where the strange behaviour is coming from because the offset is calculating correctly (viewed through console.log) but the applied transition is not equal to this calculation.
I have read many posts about transform/translate but they all seem to apply to a single transition, not multiple sequential transitions. I have tried using .attr(transform, null) before each new transition but this didn't work. I have also tried to dynamically extract the current x,y of the selected component and then update these attributes with the offset value but this didn't work either. Am really stuck with this and any help is greatly appreciated!
Thanks,
SD
<script type="text/javascript">
var cwidth = 1000;
var cheight = 500;
var bwidth = 100;
var bheight = 50;
// container definition
var svgContainer = d3.select("body").append("svg")
.attr("width",cwidth)
.attr("height",cheight)
.on("mousemove", mousemove);
// Background gray rectangle
svgContainer.append("svg:rect")
.attr("x",0)
.attr("y",0)
.attr("width",cwidth)
.attr("height",cheight)
.style("fill", "lightgrey");
// data
var secondData = [
{ "idx": 1, "name": "Commercial" },
{ "idx": 2, "name": "Finance" },
{ "idx": 3, "name": "Operations" },
{ "idx": 4, "name": "Business Services" }
];
var secondElements = secondData.length;
// group definition
var secondNodes = svgContainer.append("g")
.attr("class", "nodes")
.selectAll("rect")
.data(secondData)
.enter()
.append("g")
.attr("transform", function(d, i) {
d.x = 300;
d.y = ((cheight/secondElements)*d.idx)-bheight;
return "translate(" + d.x + "," + d.y + ")";
});
// Add elements to the previously added g element.
secondNodes.append("rect")
.attr("class", "node")
.attr("height", bheight)
.attr("width", bwidth)
.style("stroke", "gray")
.style("fill", "white")
.attr("y", function() {return -(bheight/2);})
.on("mouseover", function(){d3.select(this).style("fill", "aliceblue");})
.on("mouseout", function(){d3.select(this).style("fill", "white");})
.on("mousedown", center);
// Add a text element to the previously added g element.
secondNodes.append("text")
.attr("text-anchor", "left")
.attr("x", 15)
.attr("y",5)
.text(function(d) {return d.name;});
// gets current coordinates for transition
var current = [0,0];
var xshift = 0;
var yshift = 0;
// get offset to centre from current mouse location
function mousemove() {
//console.log(d3.mouse(this));
current = d3.mouse(this);
xshift = 500 - current[0];
yshift = 250 - current[1];
}
//applies transitions
function center(d) {
secondNodes.selectAll("rect")
.transition()
.delay(0)
.duration(500)
.attr("transform", "translate(" + xshift + "," + yshift + ")")
.each("end", function() {
secondNodes.selectAll("text")
.transition()
.delay(0)
.duration(0)
.attr("transform", null);
});
}
</script>
If you want everything to keep its relative position, it seems to me that something far easier to do would be to include everything in a g element that you can set the transform attribute one. That is, instead of moving many elements, you would have to move just the top-level container. The code you have for handling clicks would remain pretty much the same except that you only need to set the transform attribute on that one element.

Display text in D3

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.

Resources