I am trying to get font awesome icons to draw in d3 by filling an array with axios calls to an api. I'm having an issue with the what i believe is the asynchronous nature of axios. When i log the array i get all of the elements to display although when i call a single element it returns undefined.
I think i saw a reason for calling console.log on the whole array works because console.log() is also asynchronous so when i call the whole array it waits until all elements are finished loading. Then when i call a single element it calls immediately. This is why i believe i can see elements when i call the whole array.
The main issue i have is getting my weathericons array elements to be defined when i make a call to an element in the .then function of the axios.all.
This is currently what i have...
axios.all(promises)
.then(axios.spread((...args) => {
for (let i = 0; i < args.length; i++) {
weathericons[i] = args[i].data.fa;
}
}))
// After everything is loaded from the server can we append our data
.then(
svg.append('svg:foreignObject') //d3 chart
.data(options.labels)
.attr('alt', "")
.attr("height", xScale.bandwidth() > 100 ? 100 : xScale.bandwidth())
.attr("width", xScale.bandwidth() > 100 ? 100 : xScale.bandwidth())
.attr("transform", function(d, j) {
var height_adj = 60 * (xScale.bandwidth() > 1 ? 1 : xScale.bandwidth()) / 50;
return "translate(" + (xScale(options.labels[j]) + (xScale.bandwidth() / 2) - ((xScale.bandwidth() > 100 ? 100 : xScale.bandwidth()) / 2))
+ "," + (yScale(0) - height_adj) + ") scale(" + (xScale.bandwidth() > 100 ? 100 : xScale.bandwidth()) / 50 + ")"
})
.append('i')
.attr('class', function(d, j){
return 'fa fa-' + weathericons[j] + '-o"'; //This is what i need to work
})
)
Im trying to have the weathericons array elements call for all the data so i get an icon with every data point. I just can't seem to figure out a good fix to this.
Is there a way to have the array fill completely before calling the .then statement?
You definitely make the obvious mistake that the entire expression starting with svg.append() is the first argument to the 2nd then() but the argument to then() should be a function.
Change it like so
axios.all(promises)
.then(axios.spread((...args) => {
for (let i = 0; i < args.length; i++) {
weathericons[i] = args[i].data.fa;
}
}))
.then(() => {
svg.append('svg:foreignObject')
...
})
But since you don't return a promise (or anything for that matter) from the first then(), the second is unnecessary. You could just write
axios.all(promises)
.then(axios.spread((...args) => {
for (let i = 0; i < args.length; i++) {
weathericons[i] = args[i].data.fa;
}
svg.append ...
}))
Related
I've some code inside a selection.join() pattern:
const nodeWidth = (node) => node.getBBox().width;
const toolTip = selection
.selectAll('g')
.data(data)
.join(
(enter) => {
const g = enter
.append('g')
g.append('text')
.attr('x', 17.5)
.attr('y', 10)
.text((d) => d.text);
let offset = 0;
g.attr('transform', function (d) {
let x = offset;
offset += nodeWidth(this) + 10;
return `translate(${x}, 0)`;
});
selection.attr('transform', function (d) {
return `translate(${
(0 - nodeWidth(this)) / 2
},${129.6484} )`;
});
},
(update) => {
update
.select('text')
.text((d) => d.text);
let offset = 0;
update.attr('transform', function (d) {
let x = offset;
offset += nodeWidth(this) + 10;
return `translate(${x}, 0)`;
});
selection.attr('transform', function (d) {
return `translate(${
(0 - nodeWidth(this)) / 2
},${129.6484} )`;
});
}
);
as you can see, in the enter and update section I need to call a couple of functions to calculate several nodes transformations. In particular, the code stores in the accumulation var offset the length of the previous text element. This properly spaces text elements (ie, text0 <- 10 px -> text1 <- 10 px -> ...).
As you can see, the "transform functions" in the enter and update section are identical. I'm trying to define them just in one place and call them where I need. E.g.,
(update) => {
update.attr('transform', foo);
selection.attr('transform', bar);
}
However, I cannot refactor the code this way because it looks like I cannot pass in neither the offset value nor this to the function passed to attr().
Is there a way to do it?
EDIT:
As per Gerardo Furtado's hint (if I got it right), you can define foo as follows:
const foo = function(d, i, n, offset) {
let x = offset;
offset += nodeWidth(n[i]) + 10;
return `translate(${x}, 0)`;
}
then in the selection.join¡ you have to call foo this way:
(update) => {
let offset = 0;
update.attr('transform', (d, i, n) => foo(d, i, n, offset));
}
However, refactoring this way, offset is ever equal to 0. A possibile solution here: https://stackoverflow.com/a/21978425/4820341
Have a look at Function.prototype.bind().
const doSomething = (d) => {
return `translate(${
(0 - nodeWidth(this)) / 2
},${129.6484} )`;
}
Calling the function inside (enter) and (update)
selection.attr('transform', doSomething.bind(d));
This way the function gets executed in the current scope.
I guess this is what you are looking for. Please be aware that I could not test my code!
I'm trying to add an event to the bars in my graph. I tried this function bellow, but with this function it doesn't matter what bar I click, it will always return the last key in the array.
My guess would be this has to do with asynchronization, because it returns the last key value.
for (var key in data) {
bar = bars.append('rect')
.attr('class', 'bar')
.attr('x', (dimensions.width / data.length) * currentId + 41)
.attr('y', 100 - data[key] + 10).attr('height', data[key])
.attr('width', dimensions.width / data.length)
.attr('fill', '#4682B4')
.on('click', (bar = key) => {
console.log(key) //Always returns the same key
});
currentId++;
}
I have also tried to copy one of the keys the array contains and make a if-statement like this:
console.log(key === 1 ? true : false);
This will return true and false, exactly as it should. Another reason why I think this has to do with async.
My essential question is;
How can I make a click-event on this bar which will return the correct key
Before anything else: this is not the idiomatic way of adding events in D3. As a general rule, when you write a D3 code, you normally don't need any kind of loop. Of course, we use loops sometimes, but in very specific situations and to solve very specific problems. Thus, 98.57% of the loops found in D3 codes are unnecessary (source: FakeData Inc.), be it a for...in, a for...of or a simple for loop.
That being said, let's see what's happening here.
Your real problem has nothing to do with D3 or with async code. Actually, your problem can be explained by this excellent answer: JavaScript closure inside loops – simple practical example (I'll avoid closing this as a duplicate, though).
After reading the answer in the link above, let's see two demos.
The first one, using var. Please click the circles:
var data = [{
name: "foo",
value: 1
}, {
name: "bar",
value: 2
}, {
name: "baz",
value: 3
}];
var svg = d3.select("svg");
for (var key in data) {
var foo = key;//look at the var here
circle = svg.append("circle")
.attr("cy", 50)
.attr("fill", "teal")
.attr("cx", d=> 20 + key*50)
.attr("r", 15)
.on('click', () => {
console.log(foo)
});
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
Now another one, using let, please click the circles and compare the results:
var data = [{
name: "foo",
value: 1
}, {
name: "bar",
value: 2
}, {
name: "baz",
value: 3
}];
var svg = d3.select("svg");
for (var key in data) {
let foo = key;//look at the let here
circle = svg.append("circle")
.attr("cy", 50)
.attr("fill", "teal")
.attr("cx", d=> 20 + key*50)
.attr("r", 15)
.on('click', () => {
console.log(foo)
});
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
I have the following for loop which is appending a rect to a group element:
var svg_container = d3.select(element[0]).append('svg')
.attr('width', width)
.attr('height', 50)
var legend_group = svg_container.append('g');
for(var i = 0; i < 5; i++) {
legend_group.append('rect')
.attr('x', i += 10)
.attr('y', 5)
.attr('width', 10)
.attr('height', 5)
.attr('fill', 'red')
}
But it is running only for i = 0 and there are no errors. However, when I remove the attr chaining, it works as follows:
for(var i = 0; i < 5; i++) {
legend_group.append('rect');
}
Why is the for loop not executing for each iteration?
That is because in this line you are chainging i itself to more than 5.
.attr('x', i += 10)
You should have it, readonly like .attr('x', i + 10).
Because you are adding +10 to i in this line .attr('x', i += 10).I guess you need something like i+10
The for loop's second field is a condition that is checked before every loop run. Inside your loop you're adding 10 to i and assigning it at the same time. after the first run the loop logic sees that i is 10 and exits because the condition (i < 5) is not met anymore. Simply change i += 10 to i + 10 or i * 10.
That is because you add 10 to your increment counter in the first loop:
.attr('x', i += 10)
After that, in the first iteration, i is 10, which is greater that your limit.
Most likely you are using the same variable by mistake, but there is not enough information here. But if you do, change the i inside your loop, or the i in the for loop conditions
I'm trying to understand how it is that a D3 generated HTML table can be transitioned without the explicit use of a D3 "transition()" call.
The original code is Mike Bostock's sortable table: http://bl.ocks.org/mbostock/3719724
The bulk of the code reads data from a CSV file, maps it to different column heading names and renders an HTML table:
d3.csv("readme-states-age.csv", function(states) {
var ages = d3.keys(states[0]).filter(function(key) {
return key != "State" && key != "Total";
});
d3.selectAll("thead td").data(ages).on("click", function(k) {
tr.sort(function(a, b) { return (b[k] / b.Total) - (a[k] / a.Total); });
});
var tr = d3.select("tbody").selectAll("tr")
.data(states)
.enter().append("tr");
tr.append("th")
.text(function(d) { return d.State; });
tr.selectAll("td")
.data(function(d) { return ages.map(function(k) { return d[k] / d.Total; }); })
.enter().append("td").append("svg")
.attr("width", 71)
.attr("height", 12)
.append("rect")
.attr("height", 12)
.attr("width", function(d) { return d * 71; });
});
It appears that the sorting of the table happens with this "on click" callback:
d3.selectAll("thead td").data(ages).on("click", function(k) {
tr.sort(function(a, b) { return (b[k] / b.Total) - (a[k] / a.Total); });
});
I see the "sort()" function being called on the resulting selection of data records (tr) but I don't understand how the transition is actually being applied to resort the table. It just seems to happen.
Can someone please explain how and why the sorting can be achieved without an explicit call of the d3.transition() function?
There are 50 states - the below code creates 50 rows and binds each one of the 50 lines to 50 rows individually (Each row contains the value for value for the different age buckets)
var tr = d3.select("tbody").selectAll("tr")
.data(states)
.enter().append("tr");
The onclick function gets passed in the name of the column header ( 5-13,14-17 ,18-24...) - the sort function returns the value in descending order for the column clicked which inturn sorts the entire row.
d3.selectAll("thead td").data(ages).on("click", function(k) {
tr.sort(function(a, b) { return (b[k] / b.Total) - (a[k] / a.Total); });
});
Im just getting started on d3.js and was going through Nick's source code on github here and got stuck at the part where he is passing a function as data into d3.js.
The var x in the function assigned to next var gets incremented from 0 to the loop counter as i show in the jsbin link below. I cant quite wrap my head around how x gets incremented automatically and how does it know the loop counter that it needs to get incremented upto everytime.
the next variable is called from >> newdata from the >>render function ?
I just setup a jsbin here
This part:
.data(newData);
is simply going to call the newData function and bind the return to the selection.
So each call to render in the setInterval simply pushes the next function into his data array.
This part then:
selection.attr("class", "v-bar")
.style("height", function (d, i) {
return d(i) + "px"; // <- E
})
.select("span")
.text(function(d, i){
return d(i); // <- F
});
Calls d which is the next function for each element in the data array. It's passing the index position in the data array.
So the first render call is:
15 + 0 * 0;
Second is:
15 + 0 * 0;
15 + 1 * 1;
Third is:
15 + 0 * 0;
15 + 1 * 1;
15 + 2 * 2;
First, for simplification, this
var selection = d3.select("#container").selectAll("div")
.data(newData); // <- D
is just like writing
var arrayOfFunctions = newData();
var selection = d3.select("#container").selectAll("div")
.data(arrayOfFunctions); // <- D
So, for example, calling this code 3 times (via setInterval) builds up arrayOfFunctions like this:
arrayOfFunctions = [
function (x) { return 15 + x * x; },
function (x) { return 15 + x * x; },
function (x) { return 15 + x * x; }
]
(Note: it's not literally like that, because in actuality they're just pointers to the same function next)
So nothing about that increments x. But once it binds those functions to DOM elements (via data(arrayOfFunctions) and runs through this bit:
selection.attr("class", "v-bar")
.style("height", function (d, i) {
return d(i) + "px"; // <- E
})
d is function (x) { return 15 + x * x; } and i (which is 0, 1, or 2) is passed in as x to that function when it calls d(i).
And that's what essentially increments x.