D3: How do I chain transitions for nested data? - d3.js

I'm attempting to make a visualization with a multistage animation. Here's a contrived fiddle illustrating my problem (code below).
In this visualization the boxes in each row should turn green when the entire group has finished moving to the right column. IOW, when the first row (containing 3 boxes) is entirely in the right column, all the boxes should turn from black to green, but the second row, having only partially moved to the right column at this point, would remain black until it, too, is completely in the right column.
I'm having a hard time designing this transition.
Basic chaining without a delay immediately turns each box green once its finished moving (this is how it's working currently). Not good enough.
On the other hand creating a delay for the chain is difficult, since the effective delay per group is based on the number of boxes it has and I don't think this count is available to me.
It's like I need the transition to happen at mixed levels of granularity.
How should I go about doing this?
The fiddle (code below)
var data = [
["x", "y", "z"],
["a", "b", "c", "d", "e"]
];
var svg = d3.select("svg");
var group = svg.selectAll("g").data(data)
.enter()
.append("g")
.attr("transform", function(d, i) {
return "translate(0, " + (40 * i) + ")";
});
var box = group.selectAll("rect")
.data(function(d) { return d; });
box.enter()
.append("rect")
.attr("width", 30)
.attr("height", 30)
.attr("x", function(d, i) { return 60 + 30 * i; })
.transition()
.delay(function(d, i) { return 250 + 500 * i; })
.attr("x", function(d, i) { return 300 + 30 * i; })
.transition()
.attr("style", "fill:green");
// I probably need a delay here but it'd be based off the
// number of elements in the nested data and I don't know
// how to get that count
.attr("style", "fill:green");

I manage to get the effect you want, it's a little tricky though. You can customize the behavior of a transition at the begining and end of a transition. If you add a function to the end of the transition that detects if the transitioned element is the last in the group, you select all the rectangles in the group and apply the change to them.
box.enter()
.append("rect")
.attr("width", 30)
.attr("height", 30)
.attr("x", function(d, i) { return 60 + 30 * i; })
.transition()
.delay(function(d, i) { return 250 + 500 * i; })
.attr("x", function(d, i) { return 300 + 30 * i; })
.each('end', function(d, i) {
var g = d3.select(d3.select(this).node().parentNode),
n = g.selectAll('rect')[0].length;
if (i === n - 1) {
g.selectAll('rect').attr('fill', 'green');
}
});
More details in the transitions here, a working fiddle here.

Related

Legend transition not working properly

I have a graph with a legend, and the legend is always the same. The only thing that changes with the transitions is the size of the legend tiles, that are the same as this example.
When I update my graph, its size changes, and so does the legend's. Here is what I have for the legend :
var couleurs = ["#ffffb2", "#fed976", "#feb24c", "#fd8d3c", "#fc4e2a", "#e31a1c", "#b10026"];
var legende = ["0-15", "15-30", "30-45", "45-60", "60-75", "75-90", "90-100"];
canevas.append("g").selectAll(".legende").data(legende).enter()
.append("rect")
.attr("class", "legende")
.attr("width", cellPosX / 7)
.attr("height", largeurCellule / 2)
.attr("x", (d, i) => i * (cellPosX / 7))
.attr("y", cellPosY + 10)
.attr("fill", ((d, i) => couleurs[i]));
canevas.selectAll(".legende").data(legende).transition()
.duration(transitionTime)
.attr("width", cellPosX / 7)
.attr("x", (d, i) => i * (cellPosY / 7))
.attr("y", cellPosY + 10)
.attr("fill", ((d, i) => couleurs[i]));
canevas.selectAll(".legende").data(legende).exit()
.remove();
This works fine, except that when the graph is updated, for a while there are 2 legends at the same time. One that goes from the old position to the new one, which is expected, but there is also one that instantly appears to the new position. Here is a very low fps gif that I quickly made to show an example.
How would I go about having the legend going from its initial position to the other one, without it also appearing instantly at the new position?

How to draw vertical text as labels in D3

I'm trying to draw vertical labels for the heatmap that I'm working. I'm using the example from http://bl.ocks.org/tjdecke/5558084. Here is the part of the code that I've changed:
var timeLabels = svg.selectAll(".timeLabel")
.data(ife_nr)
.enter().append("text")
.text(function(d) {
return d;
})
.attr("x", function(d, i) {
return (i * gridSize);
})
.attr("y", 0)
//.style("text-anchor", "middle")
//.attr("transform", "translate(" + gridSize / 2 + '-5' + ")")
.attr("transform", "translate(" + gridSize/2 + '-8' + "), rotate(-90)")
.attr("class", function(d, i) {
return ((i >= 0) ? "timeLabel mono axis axis-worktime" : "timeLabel mono axis");
});
But it appears the labels seems to be stacked on top one another on top of the first grid. How can I edit this code to get the labels correctly displayed?
Two problems: first, the translate should have a comma separating the values:
"translate(" + gridSize/2 + ",-8), rotate(-90)")
Assuming that -8 is the y value for the translate. If you don't have a comma, the value inside the parenthesis should be just the x translation (If y is not provided, it is assumed to be zero). But even if there is actually no comma and all that gridSize/2 + '-8' is just the x value you still have a problem, because number plus string is a string. You'll have to clarify this point.
Besides that, for rotating the texts over their centres, you'll have to set the cx and cy of the rotate. Have a look at this demo:
var svg = d3.select("body")
.append("svg")
.attr("width", 400)
.attr("height", 100);
var texts = svg.selectAll(".texts")
.data(["foo", "bar", "baz"])
.enter()
.append("text");
texts.attr("y", 50)
.attr("x", function(d,i){ return 50 + 80*i})
.text(function(d){ return d});
texts.attr("transform", function(d,i){
return "rotate(-90 " + (50 + 80*i) + " 50)";
});
<script src="https://d3js.org/d3.v4.js"></script>

Displaying two strings in a table using d3.js

I've been trying to use d3.js to display a table using two input strings, I've added the code below. When displaying the second string, only the characters with indices that are greater than the length of the string x are displayed.
I think it's something related to the anonymous functions, when iterating through the second string, i begins with the index value it finished off with in the first string e.g., only "fo" is displayed from the second string instead of "fresihnfo". Can anyone give me some pointers on how to fix this?
Thanks!
var x = ["a", "e", "d", "i", "r", "z"];
var y = ["f", "r", "s", "i", "h", "n", "f", "o"];
var w = (x.length + 1) * 50;
var h = (y.length + 1) * 50;
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
/*Displays the first string*/
svg.selectAll("rect")
.data(x)
.enter()
.append("rect")
.attr("x", function(d, i) {
return (i * 45) + 45;
})
.attr("y", "0px")
.attr("width", "40px")
.attr("height", "40px")
.attr("fill", "rgb(0, 0, 102)");
svg.selectAll("text")
.data(x)
.enter()
.append("text")
.text(function(d) {
return d;
})
.attr("text-anchor", "middle")
.attr("x", function(d, i) {
return (i * 45) + 65;
})
.attr("y", "27px")
.attr("font-family", "sans-serif")
.attr("font-size", "20px")
.attr("fill", "white");
/*Displays the second string*/
svg.selectAll("rect")
.data(y)
.enter()
.append("rect")
.attr("x", "0px")
.attr("y", function(d, i) {
return ((i - x.length) * 45) + 45;
})
.attr("width", "40px")
.attr("height", "40px")
.attr("fill", "rgb(0, 0, 102)");
svg.selectAll("text")
.data(y)
.enter()
.append("text")
.text(function(d) {
return d;
})
.attr("text-anchor", "middle")
.attr("x", "20px")
.attr("y", function(d, i) {
return ((i - x.length) * 45) + 70;
})
.attr("font-family", "sans-serif")
.attr("font-size", "20px")
.attr("fill", "white");
Current output:
Desired output would be to have the rest of the string y displayed in the left column.
As Marc said, the problem is that your second selection each time is being interpretted as an update to your first selection, not as a new set of elements. And since you only work on the enter() part of the update, you don't even see the fact that you've changed the data for your first set of rectangles.
To confirm that the first set of rectangles have been given the data from your second array, right-click on one, choose "Inspect Element" and then open up the properties tab in your inspector -- the __data__ property holds the element's D3 data object.
So how do you fix it? You need a way of distinguishing between the two groups of rectangles in your select statement. There are two options:
Option 1: Use sub-selections on SVG group elements (<g>)
You've got two groups of rectangles, so it makes sense to use the svg grouping element to keep them organized. Instead of adding your rectangles directly to your svg, add a group element to the svg and then add the rectangles/text to it.
svg.append("g").selectAll("rect") //etc.
Do the same for the second set of rectangles, and they'll all be nicely arranged in the second group, so long as your select statement is always called from the group selection, it will only select elements that are part of that group.
Option 2: Use a class attribute to distinguish the two element types
You've got two types of values, x and y, so you should distinguish which type your svg elements belong to by setting a corresponding class attribute. Then, in your select statement, make sure you only select elements of the correct class. The CSS selector format for an element of a certain class is elementName.ClassName, so your code would look like:
.selectAll("rect.x")
.data(x)
enter()
.append("rect")
.classed("x", true)
// etc.
Or, Option 3: Use both
If you're going to want to update the rectangles in the future, just putting them in two groups isn't good enough -- you need a way to distinguish the groups, too. So add an x or y class when you append the <g> elements and use a "g.x" or "g.y" selector when you create them.
I highly recommend you read up on selections, selectors, nested selections, and the update process if you want to keep your D3 code straight. There's a list of tutorials on the wiki.
P.S. The i values that you create as named parameters of your anonymous functions are always limited in scope to that function. You could give them different names if you wanted, their value will always be the value that D3 passes in to them -- the data object for the first parameter, and the index within the current selection for the second parameter.
the main goal of selectAll() is to select exist elements in the container, when worked with data()
it will join data with elements so we can
use enter() to add new element for new data items;
use update() to update elements which match the data.
use exit() to remove elements without data to match
when you use svg.selectAll("rect").data(x).enter() to append rect selectAll() will return 0 element
data(x).enter() will produce placeholder for each data so you can append all data in x
but when use svg.selectAll("rect").data(y).enter() to append rect for y. selectAll() will return 6
rects, so data(y).enter() will product placeholder for 'f' and 'o' this is why you only get two
elements
solution: use different selector for x and y, such as different class name
svg.selectAll(".x").data(x).enter().append("rect").attr("class", "x"); // other operation
svg.selectAll(".y").data(x).enter().append("rect").attr("class", "y"); // other operation
you can get deep understand of d3 selection and data join with these articles:
http://bost.ocks.org/mike/join/
http://bost.ocks.org/mike/selection/

d3 line chart labels overlap

I've created a line chart based on the example found here:
http://bl.ocks.org/mbostock/3884955
However, with my data the line labels (cities) end up overlapping because the final values on the y-axis for different lines are frequently close together. I know that I need to compare the last value for each line and move the label up or down when the values differ by 12 units or less. My thought is to look at the text labels that are written by this bit of code
city.append("text")
.datum(function(d) { return {name: d.name, value: d.values[d.values.length - 1]}; })
.attr("transform", function(d) { return "translate(" + x(d.value.date) + "," + y(d.value.temperature) + ")"; })
.attr("x", 3)
.attr("dy", ".35em")
.text(function(d) { return d.name; });
If the y(d.value.temperature) values differ by 12 or less, move the values apart until they have at least 12 units between them. Any thoughts on how to get this done? This is my first d3 project and the syntax is still giving me fits!
You're probably better off passing in all the labels at once -- this is also more in line with the general d3 idea. You could then have code something like this:
svg.selectAll("text.label").data(data)
.enter()
.append("text")
.attr("transform", function(d, i) {
var currenty = y(d.value.temperature);
if(i > 0) {
var previousy = y(data[i-1].value.temperature),
if(currenty - previousy < 12) { currenty = previousy + 12; }
}
return "translate(" + x(d.value.date) + "," + currenty + ")";
})
.attr("x", 3)
.attr("dy", ".35em")
.text(function(d) { return d.name; });
This does not account for the fact that the previous label may have been moved. You could get the position of the previous label explicitly and move the current one depending on that. The code would be almost the same except that you would need to save a reference to the current element (this) such that it can be accessed later.
All of this will not prevent the labels from being potentially quite far apart from the lines they are labelling in the end. If you need to move every label, the last one will be pretty far away. A better course of action may be to create a legend separately where you can space labels and lines as necessary.
Consider using a D3 force layout to place the labels. See an example here: https://bl.ocks.org/wdickerson/bd654e61f536dcef3736f41e0ad87786
Assuming you have a data array containing objects with a value property, and a scale y:
// Create some nodes
const labels = data.map(d => {
return {
fx: 0,
targetY: y(d.value)
};
});
// Set up the force simulation
const force = d3.forceSimulation()
.nodes(labels)
.force('collide', d3.forceCollide(10))
.force('y', d3.forceY(d => d.targetY).strength(1))
.stop();
// Execute thte simulation
for (let i = 0; i < 300; i++) force.tick();
// Assign values to the appropriate marker
labels.sort((a, b) => a.y - b.y);
data.sort((a, b) => b.value - a.value);
data.forEach((d, i) => d.y = labels[i].y);
Now your data array will have a y property representing its optimal position.
Example uses D3 4.0, read more here: https://github.com/d3/d3-force

D3.js ignores duplicate values

I'm exploring D3.js. I primarily used their tutorial to get what I have. But I have made some adjustments.
What I'm doing is counting the number of active & inactive items in a specific table. It then displays a graph with those values. Most everything works fines. I have 2 issues with it though:
It doesn't update automatically with my AJAX call when items are deleted. Only updates when items are added. But I'm not concerned about this for this post.
My primary issue: duplicate values aren't being treated as individual numbers. Instead it sees [10,10] and outputs it as a single bar in the graph as 10 (instead of 2 bars).
Looking at the D3 docs, it would seem the issue lies with .data. Documentation mentions that it joins data.
$(document).on("DOMSubtreeModified DOMNodeRemoved", ".newsfeed", function() {
var columns = ['created','deleted'];
var data = [numN, numD];
var y = d3.scale.linear()
.domain([0, d3.max(data)])
.range([0, 420]);
var x = d3.scale.ordinal()
.domain(columns)
.rangeBands([0, 120]);
chart.selectAll("rect")
.data(data)
//.enter().append("rect")
.attr("x", x)
.attr("height", y)
.attr("width", x.rangeBand())
.attr("rx", 10)
.attr("ry", 10);
chart.selectAll("text")
.data(data)
//.enter().append("text")
.attr("y", y)
.attr("x", function(d) {
return x(d) + x.rangeBand() / 2;
}) //offset
.attr("dy", -10) // padding-right
.attr("dx", ".35em") // vertical-align: middle
.attr("text-anchor", "end") // text-align: right
.text(String);
});
How can I make each value to display? If I pass in two different values, the chart displays as it should.
Your problem is at .attr("x", x). So the way you're doing it assigns the same x coordinate for both rects.
So try offsetting x coordinate.
.attr("x", function(d, i) { x + i * width_of_your_rect); })

Resources