for loop executing only for first iteration - javascript

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

Related

D3: Passing extra arguments to attr functions inside a selection.join()

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!

How to call array elements in axios after promises complete

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 ...
}))

D3 - Add event to bars

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>

calling a function as data in D3.js

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.

Use jquery to dynamically number table columns diagonally

Hi there fellow coders,
I'm looking to find a way to fill a pre-built dynamic blank table with numbering (and colouring if possible) like so:
As you can see the numbering is ascending order diagonally. I know there's probably some way to calculate the number based on the tables td index but can't quite figure out how to do that for every column diagonally. Any help would be appreciated.
Update: Ok back from my Holidays. Thanks to all you clever people for your replies. As I'm sure you've all had to experience the pain in the neck clients can be, I've been told the spec has changed(again). This being the case I've had to put the grid/matrix into a database and output using a pivot table. Every square has to be customizable color-wise.
Nothing is going to waste though I have learnt quite a few nifty new javascript/jquery tricks from your responses I didn't know about before, so thanks, and I'll be sure to pay it forward :)
Here's what I came up with in the end.
Given you said "colouring if possible" I'll provide an example solution that doesn't do colours quite the way you want (it does it in a way that I found easier to code and more attractive to look at) but which does handle all the numbering correctly for varying table sizes.
The function below assumes the table already exists; in this demo I've included code that generates a table to whatever size you specify and then calls the function below to do the numbering and colours.
function numberDiagonally(tableId) {
var rows = document.getElementById(tableId).rows,
numRows = rows.length,
numCols = rows[0].cells.length,
sq = numRows + numCols - 1,
d, x, y,
i = 1,
dc,
c = -1,
colors = ["green","yellow","orange","red"];
diagonalLoop:
for (d = 0; d < sq; d++) {
dc = "diagonal" + d;
for (y = d, x = 0; y >= 0; y--, x++) {
if (x === numCols)
continue diagonalLoop;
if (y < numRows)
$(rows[y].cells[x]).html(i++).addClass(dc);
}
}
for (d = 0; d < sq; d++)
$(".diagonal" + d).css("background-color", colors[c=(c+1)%colors.length]);
}
Demo: http://jsfiddle.net/7NZt3/2
The general idea I came up with was to imagine a square twice as big as whichever of the x and y dimensions is bigger and then use a loop to create diagonals from the left edge of that bounding square going up and to the right - i.e., in the order you want the numbers. EDIT: Why twice as big as longer side? Because that's the first thing that came into my head when I started coding it and it worked (note that the variable i that holds the numbers that get displayed is not incremented for the imaginary cells). Now that I've had time to think, I realise that my sq variable can be set precisely to one less than the number of rows plus the columns - a number that ends up rather smaller for non-square tables. Code above and fiddle updated accordingly.
Note that the background colours could be set directly in the first loop, but instead I opted to assign classes and set the loops for each class later. Seemed like a good idea at the time because it meant individual diagonals could be easily selected in jQuery with a single class selector.
Explaining exactly how the rest works is left as an exercise for the reader...
UPDATE - this version does the colouring more like you asked for: http://jsfiddle.net/7NZt3/1/ (in my opinion not as pretty, but each to his own).
This fiddle populates an existing table with numbers and colors. It is not limited to being a 5x5 table. I didn't understand the logic of 15 being orange rather than yellow, so I simply grouped the diagonal cells into color regions.
// we're assuming the table exists
var $table = $('table'),
// cache the rows for quicker access
$rows = $table.find('tr'),
// determine number of rows
_rows = $rows.length,
// determine number of cells per row
_cols = $rows.first().children().length,
// determine total number of cells
max = _rows * _cols,
// current diagonal offset (for coloring)
d = 1,
// current row
r = 0,
// current cell
c = 0;
for (var i=1; i <= max; i++) {
// identify and fill the cell we're targeting
$rows.eq(r).children().eq(c)
.addClass('d' + d)
.text(i);
if (i < max/2) {
// in the first half we make a "line-break" by
// moving one row down and resetting to first cell
if (!r) {
r = c + 1;
c = 0;
d++;
continue;
}
} else {
// in the second half our "line-break" changes to
// moving to the last row and one cell to the right
if (c + 1 == _cols) {
c = 1 + r;
r = _rows -1;
d++;
continue;
}
}
r--;
c++;
}
Here's a jsFiddle that does what you asked for - http://jsfiddle.net/jaspermogg/MzNr8/8/
I took the liberty of making it a little bit user-customisable; it's interesting to see how long it takes the browser to render a 1000x1000 table using this method :-D
Assuming that each cell has an id of [column]x[row], here are teh codez for how to fill in the numbers of a square table of side length sidelength.
//populate the cells with numbers according to the spec
function nums(){
var xpos = 0
var ypos = 0
var cellval = 1
for(i=0;i<2*sidelength;i++){
if(i >= sidelength){
ypos = sidelength - 1
xpos = 1 + i - sidelength
$('td#' + xpos + 'x' + ypos).text(cellval)
cellval = cellval + 1
while(xpos + 1 < sidelength){
ypos = ypos-1
xpos = xpos+1
$('td#' + xpos + 'x' + ypos).text(cellval)
cellval = cellval + 1
}
} else {
ypos = i
xpos = 0
$('td#' + xpos + 'x' + ypos).text(cellval)
cellval = cellval + 1
while(!(ypos-1 < 0)){
ypos = ypos-1
xpos = xpos+1
$('td#' + xpos + 'x' + ypos).text(cellval)
cellval = cellval + 1
}
}
}
}
And here they are for how to colour the bugger.
// color the cells according to the spec
function cols(){
if(+$('td#0x0').text() === 99){
return false
} else {
$('td').each(function(index, element){
if(+$(this).text() > 22)
{
$(this).attr("bgcolor", "red")
}
if(+$(this).text() <= 22)
{
$(this).attr("bgcolor", "orange")
}
if(+$(this).text() <= 14)
{
$(this).attr("bgcolor", "yellow")
}
if(+$(this).text() <= 6)
{
$(this).attr("bgcolor", "green")
}
})
}
}
Enjoy, eh? :-)

Categories