Transition.each in d3 doesn't interpolate attributes - javascript

I'm missing something in my understanding here rather than a bug I think...
In d3, I'm trying to update multiple attribute values within a transition. Using multiple transition.attr declarations works fine. But I'd like to use .each as in the real world I have a big calculation I don't want to do twice or more... I tried using transition.each but the attribute values don't interpolate.
Is it the d3.select within the each? I tried d3.active but that didn't do anything.
(There is a https://github.com/d3/d3-selection-multi library that supplied .attrs that worked nicely up to v5, but not with the new v6...)
See https://jsfiddle.net/f679a4yj/3/ - what I can do to get the animation but through the .each, what am I not getting?

The problem here is a misconception regarding transition.each: you cannot use it to wrap several transition.attr or transition.style methods. In fact, it is used only to invoke arbitrary code to each element in that transition, just like selection.each.
Therefore, when you do...
.each (function (d, i) {
// complicated stuff with d I don't want to do twice
d3.select(this)
.attr("x", ((zz+i)%4) *20)
.attr("y", ((zz+i)%4) *40)
})
... you are simply changing the attribute in the DOM element, and the element will immediately jump to the new x and y position, as expected. In other words, that d3.select(this).attr(... in your code is a selection.attr, not a transition.attr.
You have several alternatives (for avoiding redundant complex calculations). If you want to stick with transition.each, you can use it to create a new local variable, or maybe a new property. For that to work, we need to have objects as the data elements in the array, and then we just use transition.attr as usual:
.each(function(d, i) {
d.complexValue = ((zz + i) % 4) * 20;
})
.attr("x", function(d) {
return d.complexValue;
})
//etc...
Here is a demo:
const sel = d3.select("svg")
.selectAll(null)
.data([{
color: "red"
}, {
color: "blue"
}, {
color: "green"
}, {
color: "yellow"
}])
.enter()
.append("rect")
.attr("width", 30)
.attr("height", 30)
.attr("x", function(d, i) {
return i * 20;
})
.attr("y", function(d, i) {
return i * 40;
})
.style("fill", function(d) {
return d.color;
});
let zz = 0;
function each() {
zz++;
d3.selectAll("rect")
.transition()
.duration(1000)
.each(function(d, i) {
d.complexValue = ((zz + i) % 4) * 20;
})
.attr("x", function(d) {
return d.complexValue;
})
.attr("y", function(d) {
return d.complexValue;
})
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="demo"><svg width="360px"></svg></div>
<button onclick="each()">In an Each</button>

Related

Edit svgs instead of recreating

I have an apparently simple misunderstanding of d3, and can't find a matching explanation, although lots of stuff I look for seems close.
Basically, I have an array of data, and I want to render a in an for each item. I want each to have its own children elements (a and a text />) that depends on the datum bound to each .
My problem is that when the data updates, additional and elements are added to each , rather than replacing it.
A minimal reproduction codepen is attached. Any assistance would be greatly appreciated!
https://codepen.io/kamry-bowman/pen/GaLvKJ
html:
<body>
<svg />
<button>Increment</button>
</body>
js:
let state = [];
for (let i = 0; i < 5; i++) {
state.push({ id: i, value: (i + 1) * 2 });
}
function render() {
console.log('running', state)
const svg = d3
.select("svg")
.attr("width", "1000")
.attr("height", "200");
const gGroup = svg
.selectAll("g")
.data(state, d => d.id)
.join("g")
.attr("transform", (d, i) => `translate(${100 * i})`);
gGroup
.append("circle")
.attr("cx", "50")
.attr("cy", "50")
.attr("r", "50")
.attr("fill", "none")
.attr("stroke", "black");
gGroup
.append("text")
.text(d => d.value)
.attr("stroke", "black")
.attr("x", "40")
.attr("y", "55")
.attr("style", "font: bold 30px sans-serif");
}
function clickHandler() {
state = state.map(obj => ({ ...obj, value: obj.value + 1 }))
render()
}
document.querySelector('button').addEventListener('click', clickHandler)
render()
I have found a hacky fix of calling gGroup.html(''), but that seems like I'm missing something with how the library is supposed to work.
Add this line directly after you create your svg in the render method:
svg.selectAll("*").remove();
This will remove all children of the root svg. Otherwise you'll just keep drawing on top of the same svgs.
Try it here
And the docs:
d3.selectAll()
selection.remove()
EDIT
To update just the text values themselves, call this update() function in the click handler instead:
function update() {
const svg = d3.select("svg")
d3.selectAll("svg text").each(function(d, i) {
d3.select(this).text(state[i].value);
});
}
The codepen has been updated to use this method
EDIT AGAIN
If you want to bind the values to their id, just access it from d in .each() method instead of i (which is the index in the collection):
function update() {
const svg = d3.select("svg")
d3.selectAll("svg text").each(function(d, i) {
d3.select(this).text(
state.find((e) => e.id === d.id).value
);
});
}
Since the entries can be in any order, we'll have to use Array.find() to find the element with the matching id. Here's another codepen for this.

JavaScript/d3 - Assign Attribute within Another Attribute [duplicate]

For example, I need to calculate a Math.sqrt of my data for each attr, how can I calculate only one time the Math.sqrt(d)?
var circle = svgContainer.data(dataJson).append("ellipse")
.attr("cx", function(d) {
return Math.sqrt(d) + 1
})
.attr("cy", function(d) {
return Math.sqrt(d) + 2
})
.attr("rx", function(d) {
return Math.sqrt(d) + 3
})
.attr("ry", function(d) {
return Math.sqrt(d) + 4
});
Has any elegant/performative mode? I'm thinking this way:
var aux;
var circle = svgContainer.data(dataJson).append("ellipse")
.attr("cx", function(d) {
aux = Math.sqrt(d);
return aux + 1
})
.attr("cy", function(d) {
return aux + 2
})
.attr("rx", function(d) {
return aux + 3
})
.attr("ry", function(d) {
return aux + 4
});
An underestimated feature of D3 is the concept of local variables which were introduced with version 4. These variables allow you to store information on a node (that is the reason why it is called local) independent of the data which might have been bound to that node. You don't have to bloat your data to store additional information.
D3 locals allow you to define local state independent of data.
Probably the major advantage of using local variables over other approaches is the fact that it smoothly fits into the classic D3 approach; there is no need to introduce another loop whereby keeping the code clean.
Using local variables to just store a pre-calculated value is probably the simplest use case one can imagine. On the other hand, it perfectly illustrates what D3's local variables are all about: Store some complex information, which might require heavy lifting to create, locally on a node, and retrieve it for later use further on in your code.
Shamelessly copying over and adapting the code from Gerardo's answer the solution can be implemented like this:
var svg = d3.select("svg");
var data = d3.range(100, 1000, 100);
var roots = d3.local(); // This is the instance where our square roots will be stored
var ellipses = svg.selectAll(null)
.data(data)
.enter()
.append("ellipse")
.attr("fill", "gainsboro")
.attr("stroke", "darkslateblue")
.attr("cx", function(d) {
return roots.set(this, Math.sqrt(d)) * 3; // Calculate and store the square root
})
.attr("cy", function(d) {
return roots.get(this) * 3; // Retrieve the previously stored root
})
.attr("rx", function(d) {
return roots.get(this) + 3; // Retrieve the previously stored root
})
.attr("ry", function(d) {
return roots.get(this) + 4; // Retrieve the previously stored root
});
<script src="//d3js.org/d3.v4.min.js"></script>
<svg></svg>
Probably, the most idiomatic way for doing this in D3 is using selection.each, which:
Invokes the specified function for each selected element, in order, being passed the current datum (d), the current index (i), and the current group (nodes), with this as the current DOM element (nodes[i]).
So, in your case:
circle.each(function(d){
//calculates the value just once for each datum:
var squareRoot = Math.sqrt(d)
//now use that value in the DOM element, which is 'this':
d3.select(this).attr("cx", squareRoot)
.attr("cy", squareRoot)
//etc...
});
Here is a demo:
var svg = d3.select("svg");
var data = d3.range(100, 1000, 100);
var ellipses = svg.selectAll(null)
.data(data)
.enter()
.append("ellipse")
.attr("fill", "gainsboro")
.attr("stroke", "darkslateblue")
.each(function(d) {
var squareRoot = Math.sqrt(d);
d3.select(this)
.attr("cx", function(d) {
return squareRoot * 3
})
.attr("cy", function(d) {
return squareRoot * 3
})
.attr("rx", function(d) {
return squareRoot + 3
})
.attr("ry", function(d) {
return squareRoot + 4
});
})
<script src="//d3js.org/d3.v4.min.js"></script>
<svg></svg>
Another common approach in D3 codes is setting a new data property in the first attr method, and retrieving it latter:
.attr("cx", function(d) {
//set a new property here
d.squareRoot = Math.sqrt(d.value);
return d.squareRoot * 3
})
.attr("cy", function(d) {
//retrieve it here
return d.squareRoot * 3
})
//etc...
That way you also perform the calculation only once per element.
Here is the demo:
var svg = d3.select("svg");
var data = d3.range(100, 1000, 100).map(function(d) {
return {
value: d
}
});
var ellipses = svg.selectAll(null)
.data(data)
.enter()
.append("ellipse")
.attr("fill", "gainsboro")
.attr("stroke", "darkslateblue")
.attr("cx", function(d) {
d.squareRoot = Math.sqrt(d.value);
return d.squareRoot * 3
})
.attr("cy", function(d) {
return d.squareRoot * 3
})
.attr("rx", function(d) {
return d.squareRoot + 3
})
.attr("ry", function(d) {
return d.squareRoot + 4
});
<script src="//d3js.org/d3.v4.min.js"></script>
<svg></svg>
PS: by the way, your solution with var aux will not work. Try it and you'll see.

d3.js: Confusion about the order in which the code is executed

I am trying to make an interactive bar chart in D3.js
I uploaded everything to github for easy reference. I also included index.html at the end of my question.
My starting point is data.json containing an array of 7 items (i.e. countries). Each country has an attribute 'name' and four other attributes. These represent the exposition of private banks and the state to Greek debt for the years 2009 and 2014.
My goal is to create a bar chart that starts by showing the exposition of each country's banks and public sector in 2009 (so two bars for each country) and that changes to the year 2014 once the user clicks on the appropriate button.
I had managed to make it all work nicely! However, I had to create manually separate lists for each (sub-)dataset I needed to use. For example I created one called y2009 which included the exposition of bank and state for country 1, then the same for country 2, an so on..
(I left one of the list and commented it out on line 43)
I wanted to make my code more flexible so I created a for loop that extracts the data and creates the lists for me. (see lines 46-60). This did not work because the for loops would start before the data was actually loaded. Hence I would end up with empty lists.
So I grouped the for loops into a function (prepare()) and executed that function within the function that loads the data (lines 18-30). This fixed that issue...
..and created a new one! The two functions that should set the scales (see lines 67-73) do not work because their calculations require on one of the lists created by the for loops (namely 'total').
(I assume this is due to the list being created after the scale methods are called.)
The curious thing is that if I run the script, then copy in the console the xScale and yScale functions, and then copy the draw function (lines 101-212) everything works.
Hence I tried to group everything into functions (e.g. setScales, draw) so that I would call them in the order I want at the end of the script (lines 214-215) but this creates problem because certain variables (e.g. xScale and yScale) need to be global.
I also tried to first create them in the global space and then modify them through setScales. This did not work either.
Summing up, wait I don't understand is:
In which order should I write the code to make things work(again)? Is it a good idea to wrap operations within functions (e.g. setting the scales, drawing bars and labels) and then calling the function in the right order?
Which type of object is created with the scale method? I am confused on whether they are actual functions.
I hope this was not too much of a pain to read and thank everyone who made it through!
Fede
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="d3.min.js"></script>
</head>
<body>
<p>Introductory text here!</p>
<p>
<button id="change2009"> 2009 </button>
<button id="change2014"> 2014 </button>
</p>
<div id="country"></div>
<script type="text/javascript">
d3.json("data.json", function(error, json) {
if (error) {
console.log(error);
} else{
console.log(json);
dataset=json;
}
prepare (dataset);
});
//load data
var dataset;
var bank09=[];
var state09=[];
var bank14=[];
var state14=[];
var y2009=[];
var y2014=[];
var total=[];
var xScale;
var yScale;
//var total = [4.76, 0, 0.12, 6.36, 4.21, 0, 0.04, 7.96, 78.82, 0, 1.81, 46.56, 45, 0, 13.51, 61.74, 6.86, 0, 1.06, 40.87, 12.21, 0, 1.22, 13.06, 1.21, 0, 0.39, 27.35];
function prepare (dataset){
for (i in dataset) {bank09.push(dataset[i].bank09);
state09.push(dataset[i].state09);
bank14.push(dataset[i].bank14);
state14.push(dataset[i].state14);
y2009.push(dataset[i].bank09);
y2009.push(dataset[i].state09);
y2014.push(dataset[i].bank14);
y2014.push(dataset[i].state14);
total.push(dataset[i].bank09);
total.push(dataset[i].state09);
total.push(dataset[i].bank14);
total.push(dataset[i].state14);
}
}
//overwrite dataset
dataset2=y2009;
//scales
function setScales () {
var xScale = d3.scale.ordinal()
.domain(d3.range(total.length/2))
.rangeRoundBands([0, w], 0.1);
var yScale = d3.scale.linear()
.domain([0, d3.max(total)])
.range([0, h]);
console.log(yScale(89));
}
//layout
var w = 600;
var h = 600;
var barPadding = 1;
//coountry names
var country = ["Austria", "Belgium", "France", "Germany", "Italy", "Holland", "Spain"];
d3.select("#country")
.data(country)
.enter()
.append("rect")
.attr("class", "country")
//.append("text")
//.text(function(d){
// return d;
// })
//draw svg
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
function draw () {
//draw bars
svg.selectAll("rect")
.data(dataset2)
.enter()
.append("rect")
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", function(d){
return h - yScale(d);
})
.attr("width", xScale.rangeBand)
.attr("height", function(d) {
return yScale(d);
})
.attr("fill", "black");
//add labels
svg.selectAll("text")
.data(dataset2)
.enter()
.append("text")
.text(function(d){
return d;
})
.attr("text-anchor", "middle")
.attr("font-family", "sans-serif")
.attr("font-size", "12px")
.attr("fill", "red")
.attr("x", function(d, i){
return xScale(i) + xScale.rangeBand() / 2;
})
.attr("y", function(d) {
if (d<3) {
return h - 15;
} else {
return h - yScale(d) + 15;}
})
//interactivity
d3.select("#change2014")
.on("click", function() {
//update data
dataset2=y2014;
//update bars
svg.selectAll("rect")
.data(dataset2)
.transition()
.duration(3000)
.attr("y", function(d){
return h - yScale(d);
})
.attr("height", function(d) {
return yScale(d);
})
//update labels
svg.selectAll("text")
.data(dataset2)
.transition()
.duration(3000)
.text(function(d){
return d;
})
.attr("x", function(d, i){
return xScale(i) + xScale.rangeBand() / 2;
})
.attr("y", function(d) {
if (d<3) {
return h - 15;
} else {
return h - yScale(d) + 15;}
})
})
d3.select("#change2009")
.on("click", function() {
//update data
dataset2=y2009;
//update bars
svg.selectAll("rect")
.data(dataset2)
.transition()
.duration(3000)
.attr("y", function(d){
return h - yScale(d);
})
.attr("height", function(d) {
return yScale(d);
})
//update labels
svg.selectAll("text")
.data(dataset2)
.transition()
.duration(3000)
.text(function(d){
return d;
})
.attr("x", function(d, i){
return xScale(i) + xScale.rangeBand() / 2;
})
.attr("y", function(d) {
if (d<3) {
return h - 15;
} else {
return h - yScale(d) + 15;}
})
})
}
setScales ();
draw();
</script>
In which order should I write the code to make things work(again)? Is
it a good idea to wrap operations within functions (e.g. setting the
scales, drawing bars and labels) and then calling the function in the
right order?
As Lars pointed out, you can put everything inside the d3.json callback. This is because you only want to start rendering with D3 once you have the data. The d3.json method is asynchronous, which means that after you call d3.json(), the code afterwards will execute first before the function inside the d3.json method has finished. Check out http://rowanmanning.com/posts/javascript-for-beginners-async/ for more on asynchronous behavior in Javascript.
Given that you only want to start rendering when the d3.json method has completed, you could also just organize the other parts of your code into smaller functions and call some sort of initializer function from within the d3.json success callback, sort of like what you are doing with the prepare function. This is a cleaner approach and starts taking you towards a model-view paradigm.
Which type of object is created with the scale method? I am confused
on whether they are actual functions.
The scale method does return a function, but with additional functions added to its prototype. Try printing out "someScale.prototype" to see all of the various methods you can use. I'd also highly recommend Scott Murray's tutorial on D3. Here is the chapter on scales: http://alignedleft.com/tutorials/d3/scales

d3 stack appears to be working but getting NaN when values are pushed to svg rect

I created this chart initially which is basically what I am trying to recreate except I want the height of the bars to be dynamically populated based on the data in the threshold array:
http://jsfiddle.net/featherita/Lw7KZ/
So, if the values are 1,20,40, it would scale the colored rectangles accordingly.
I'm attempting to use the stack function:
var layers = d3.layout.stack()(dataChartData.summaryData.threshold.map(function () {
return dataChartData.summaryData.threshold.map(function (d) {
return { x: 0, y: +d, y0: 0 };
});
}));
This will console log the data (even though my y0 value is currently incorrect), the data is in the right format. However, when I try to take these values and pull them to the rectangle values I get an error: Error: Invalid value for attribute y="NaN". I'm calling it with "layers":
svg.selectAll("rect")
.data(layers, function(d){return d;})
.enter()
.append("rect")
.attr("width", 7)
.attr("x", function (d) { return xScale(d.x); })
.attr("y", function (d) { return -yScale(d.y0) - yScale(d.y); })
.attr("height", function (d) { return yScale(d.y); })
.style("fill", function (d) { return color(d); });
Any help is appreciated!!!
Your layers array is not formatted the way you think it is. Right now it is an array of 3 arrays with values you created with the d3.layout.stack function call. With that said and the current format, you can access the values with the following:
.attr("y", function (d, i) { return yScale(d[i].y); })
is it because your setting the attributes to be Funciton object's instead of Number objects?
Try adding parenthesis with variables to pass in to the function definitions, like so:
- .attr("x", function (d) { return xScale(d.x); }) // Current
+ .attr("x", function (d) { return xScale(d.x); }(d)) // Suggested

Get width of d3.js SVG text element after it's created

I'm trying to get the widths of a bunch of text elements I have created with d3.js
This is how I'm creating them:
var nodesText = svg.selectAll("text")
.data(dataset)
.enter()
.append("text")
.text(function(d) {
return d.name;
})
.attr("x", function(d, i) {
return i * (w / dataset.length);
})
.attr("y", function(d) {
return 45;
});
I'm then using the width to create rectangles the same size as the text's boxes
var nodes = svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", function(d, i) {
return i * (w / dataset.length);
})
.attr("y", function(d) {
return 25;
})
.attr("width", function(d, i) {
//To Do: find width of each text element, after it has been generated
var textWidth = svg.selectAll("text")
.each(function () {
return d3.select(this.getComputedTextLength());
});
console.log(textWidth);
return textWidth;
})
.attr("height", function(d) {
return 30;
})
I tried using the Bbox method from here but I don't really understand it. I think selecting the actual element is where I'm going wrong really.
I would make the length part of the original data:
var nodesText = svg.selectAll("text")
.data(dataset)
.enter()
.append("text")
.text(function(d) {
return d.name;
})
.attr("x", function(d, i) {
return i * (w / dataset.length);
})
.attr("y", function(d) {
return 45;
})
.each(function(d) {
d.width = this.getBBox().width;
});
and then later
var nodes = svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("width", function(d) { return d.width; });
You can use getBoundingClientRect()
Example:
.style('top', function (d) {
var currElemHeight = this.getBoundingClientRect().height;
}
edit: seems like its more appropriate for HTML elements. for SVG elements you can use getBBbox() instead.
d3.selectAll returns a selection. You can get each of the elements by navigating through the array in the _groups property. When you are determining the width of a rectangle, you can use its index to get the corresponding text element:
.attr('width', function (d, i) {
var textSelection = d3.selectAll('text');
return textSelection._groups[0][i].getComputedTextLength();
});
The _groups property of d3's selection has a list of nodes at [0]. This list contains all of the selected elements, which you can access by index. It's important that you get the SVG element so that you can use the getComputedTextLength method.
You may also want to consider creating the rect elements first, then the text elements, and then going back to the rectangles to edit the width attribute, so that the text elements are on top of the rectangles (in case you want to fill the rectangles with color).
Update:
It's typically preferred that you don't access _groups, though, so a safer way to get the matching text element's width would be:
.attr('width', function (d, i) {
return d3.selectAll('text').filter(function (d, j) { return i === j; })
.node().getComputedTextLength();
});
Using node safely retrieves the element, and filter will find the text element which matches index.

Categories