D3 and updating elements of a multidimensional array - javascript

Let's say that I have a 2d array called state that looks like [[0,1,0],[1,1,0],[1,2,1]]. The members of this array are constantly updating. I have D3 successfully rendering each member of the array with the following code:
function view(state)
const vis = d3.select('body')
.selectAll('div')
.data(state)
.enter()
.append('div')
.attr('style', 'display: flex')
.selectAll('span')
.data((d,i) => d)
.enter()
.append('span')
.text(d => d)
return vis
}
Now consider that this view function is called every time my state changes. How can I get D3 to re-render only the elements that have changed since the previous render? Currently, the page renders initially but D3 never re-renders any of the elements, despite the view function being called at each state change.
If you'd like to see the full source, I posted it here

Yeah to handle such a case you need to make use of enter which you are doing to create new elements and exit to remove any element not in the current array.
I ll recommend you to do it this way:
function view(state)
var visData = d3.select('body')
.selectAll('div')
.data(state, function(d){/*uniquely identify each element in array*/return d;});
const vis = visData.enter()
.append('div')
.attr('style', 'display: flex')
.selectAll('span')
.data((d,i) => d)
.enter()
.append('span')
.text(d => d);
//remove the unnecessary data divs.
visData.exit().remove();
return vis
}
Further on enter exit read

You have 2 cases when you want to render your data: when you add new view elements (divs, spans and text), and when you update your data (text). In your code you've done the first step only. You should do the second one.
d3.js provides selections for both cases. When you write
var div = d3.select('body').selectAll('div');
you get what already exists on your view. But if you add .enter(), i.e.
var div = d3.select('body').selectAll('div').enter();
it is already the second case. You are getting what doesn't exist yet, and should be created.
Since your data are changing, you should divide your code into two parts: view creation, view updating.
Here is a JSFiddle example how it could be done for you code. The example can be improved, and the create/update code sections can be separated and put to corresponding functions, see there.
Anyway, I encourage you to read the general update pattern for d3.js.

Related

Adding event listeners into dynamically created elements

I am currently experiencing some issues when trying to add a click event listener for some foreignObjects rendered using vanilla JS.
It works when I use the built in d3 on click functions, but I would prefer to have it done using the javascript code.
However, the function never triggers for these elements and I can't understand why.
The code example is not complete, but should highlight what I am trying to do.
var nodes = g.selectAll("foreignObject")
.data(response.nodes)
.enter()
.append("foreignObject")
.attr("x", function(d) {
return d.x - nodeWidth / 2;
})
.attr("y", function(d) {
return d.y - nodeHeight / 2;
})
.attr("width", nodeWidth)
.attr("height", nodeHeight)
.append("xhtml:div")
.attr("class", "outer")
.html(function(d) {
var nodeHtml = createNodeElement(d);
return nodeHtml.outerHTML;
})
// If I append the img like this, it works, but ends up in the wrong "element scope"
.append("img")
.attr("class", "optionsImg")
.attr("src","/images/options-squares.svg")
.on("click", function(d) {
currentTooltipObject = d;
renderTooltipDiv();
});
function createNodeElement(d) {
let nodeElement = document.createElement("div");
nodeElement.className = "nodeElement";
let nodeOptionsImg = document.createElement("img");
nodeOptionsImg.className = "nodeOptionsImg";
nodeOptionsImg.src = "/images/options-squares.svg";
nodeOptionsImg.addEventListener("click", function() {
console.log("Clicked on optionsImg for this object: "+d);
});
nodeElement.appendChild(nodeOptionsImg);
return nodeElement;
}
The main problem with your approach is the fact that using outerHTML and innerHTML (which is used internally by .html()) for creating / moving / copying elements is kind of like serializing and de-serializing your HTML DOM tree. This works for the HTML elements themselves, however, it does not preserve event listener. Hence, the listeners you attached to the elements in your function createNodeElement are lost during this process. This is a variation of other questions like, e.g. "Is it possible to append to innerHTML without destroying descendants' event listeners?".
If you take a step back and re-read the D3 API docs you will realize that you are in fact almost there: D3 provides means to append native DOM nodes to a selection:
selection.append(type) <>
If the specified type is a function, it is evaluated 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]). This function should return an element to be appended.
If you would like to stick to your implementation of createNodeElement to create an element by using native JS methods, you can simply pass that function to selection.append() as it returns a newly created node along with its <img> child node. Your code can thus be simplified to:
var nodes = g.selectAll("foreignObject")
/* styling omitted */
.append("xhtml:div")
.attr("class", "outer")
.append(createNodeElement);
Because only references to DOM nodes are passed around all event listeners attached to the elements will also be preserved.
In the createNodeElement function nodeOptionsImg element is not yet rendered when you do addEventListener. Listeners can be added only to rendered elements.

d3js stacked bar chart not updating

I am experimenting with a stacked bar chart in d3js and ran into enter exit selection difficulties. I used the d3.stack to get two arrays organized by keys, then I tried to follow the general update pattern. The problem I'm having now is the data is not getting updated when I click a different state in the dropdown menu. Here's the problem code and a link to the full project: http://plnkr.co/edit/8N8b2yUYRF9zqRkjkIiO?p=preview
var series = g.append("g")
var seriesready =
series.selectAll("g")
.data(stack(data))
.enter().append("g")
.attr("fill",function(d){console.log(d); return z(d.key)}) //not logging
when I update the bar chart
var rectangles =
seriesready.selectAll("rect")
.data(function(d){return d})
rectangles.exit().remove()
rectangles.enter().append("rect")
.attr("width", x.bandwidth())
.transition()
.duration(1500)
.attr("transform", function(d) {console.log(d); return "translate(" + x(d.data.Date) + ",0)"; })
.attr("height", function(d) {
return height - y(d[1]-d[0]);
})
.attr("y", function(d) {
return y(d[1]-d[0]);
});
I also think I'm getting confused as to what selections should be removed or added. Would really appreciate any pointers. Data viz is fun to work with, but I still haven't fully grasped data binding yet.
I have not made the switch to version 4 yet, but the data binding methodology is the same i think.
You need to define a key function as the second parameter to the .data() function.
A key function may be specified to control which datum is assigned to
which element, replacing the default join-by-index.
https://github.com/d3/d3-selection/blob/master/README.md#selection_data
Your updated code
http://plnkr.co/edit/wwdjJEflZtyACr6w9LiS?p=preview
The changed code:
var seriesUpdate = series.selectAll("g")
.data(stack(data),d=>d)
var seriesready = seriesUpdate.enter().append("g")
.attr("fill",function(d){return z(d.key)})
seriesUpdate.exit().remove()
When binding data to elements, D3 calculates what data is new/existing/removed in relation to the selection. By default it does this by data index - the size of the input array. Since the computed stack data for michigan and ohio both return 2 sets of data (injured and killed), D3 views this as "same" data, thus it's an update.
If you define a key function, D3 recognizes the computed stack data for michigan and ohio as being "different" data, thus it's an enter.
With a key function, when you select Ohio first, the enter selection is size 2 with Ohio. If you then select Michigan, the enter selection is size 2 with Michigan, and the exit selection is size 2 with Ohio.

d3 enter().append() not appending all elements of my array

I'm working on my first mid-scale d3 project right now after having run through the tutorials. I understand scales, enter, update, and exit, so I'm pretty confused about a problem I'm running into.
I have an array of JSON objects with two fields, year and number. I am creating a bar chart with this array with the following code:
var bar = chart.selectAll('g')
.data(yearData)
.enter().append('g')
.attr('transform', function(d, i) {
console.log(i);
return 'translate(' + i * barWidth + ',0)'; });
My confusion stems from the fact that the console.log statement in this code block outputs 27 as its first value. In other words, d3 is skipping elements 0 - 26 of my array. Why could this be??
Thanks for your help.
This is most likely because you already have g elements on your page (e.g. from adding an axis). These are selected and matched with data, so the enter selection doesn't contain everything you expect.
One solution is to assign a class to these elements and select accordingly:
var bar = chart.selectAll('g.bar')
.data(yearData)
.enter().append('g')
.attr("class", "bar")
// ...
Much more detail on this in the second half of this tutorial.

Inline D3 sparklines in a table alongside text

Say if there is a table like this:
var data = [
['Orange', 'Orange', [6,3,3,2,5]],
['Apple', 'Red', [6,2,6,5,5]],
['Grape', 'Purple', [9,1,2,3,1]]
]
I'd like the strings to represented as strings, but the number array represented as a D3 line chart. If it's just text that I care about, I can selectAll td elements and insert some text.
var tcells = trows
.selectAll("td")
.data(function(d, i) { return d; })
.enter()
.append("td")
.text(function(d, i) { return d; });
The 3rd column is text so I'd like to update it with graphs or alternatively append another column of graphs. Say lines is a function that creates a line graph, I would like to call lines() as it passes data from the 3rd column for each row.
trows.selectAll("td")
.data(function(d) {return d[2]}) // scope out data from 3rd column
.enter()
.append("td")
//.call(lines([3,8,2,5,8,4])); // this works
.call(lines); // this doesn't
My D3 knowledge is spotty so I am not clear on how the data and the selection are passed.
Here is the full code:
jsfiddle link
You're confusing the selection.call(func) method with the selection.each(func) method.
call is a convenience method for calling functions that accept the selection as a whole as a parameter. trows.call(lines) is equivalent to lines(trows), except that it can be part of a method chain.
each is used to call a function once for each element in the selection, passing the data and index as parameters.
As for why trows.call(lines([3,8,2,5,8,4])) "works", well it only sort-of works. You're calling the function lines, with the hard-coded data array, but that function doesn't return anything, so there is nothing to call on the selection itself. (If you don't understand the difference, you might find this answer useful.)
Your method also currently only appends the line graph to the body, it doesn't insert it into the table. You have the following code that you acknowledge doesn't work:
// dynamic selection doesn't
this.append("td").append('svg')
.attr('width', width).attr('height', height)
.append('path').attr('class','line')
.datum(data).attr('d', line);
There are a few reasons that is failing:
in the context you're calling the function, with lines([3,8,2,5,8,4]) being called directly, the this value will not be set, so will point to the window object;
even if you were calling the function with an each method (which sets the this value), this would point to the DOM node, not a selection that you can call d3 methods for;
except in your specific case, it wouldn't even point to a DOM node, because you're calling it from an enter() selection in which you haven't appended the new nodes yet.
In summary, to get things working:
In the main program, use each to call your lines function, after creating your new <td> elements:
trows.selectAll("td.graph")
//use a class so you don't re-select the existing <td> elements
.data(function(d) {return [d[2]];})
// the value returned by a data function must always be an array
// containing data objects for each new element you create --
// you only want one element containing the entire data array,
// not individual elements for each number, so wrap it in another array
.enter()
.append("td")
.attr("class", "graph")
.each(lines);
In the lines function, re-select the this element (which will now point to the just-created <td>), and append your sparkline SVG to it:
d3.select(this).append('svg')
.attr('width', width)
.attr('height', height)
.append('path')
.attr('class','line')
.datum(data)
.attr('d', line);
http://jsfiddle.net/Lgq6ct9f/9/
(with some other clean-up so you don't have a function definition in the middle of your main program)

Dynamically updating in d3 works for circles but not external SVGs

Suppose I want to dynamically update the position and number of circles on a page using d3. I can do this, using the .data(), .enter(), .exit() pattern. Here is a working example.
http://jsfiddle.net/csaid/MFBye/6/
function updatePositions(data) {
var circles = svg.selectAll("circle").data(data);
circles.enter().append("circle");
circles.exit().remove();
circles.attr("r", 6)
.attr("cx", 50)
.attr("cy", function (d) {
return 20 * d
});
}
However, when I try to do the same thing with external SVGs instead of circles, many of the new data points after the first update do not appear on the page. Example:
http://jsfiddle.net/csaid/bmdQz/8/
function updatePositions(data) {
var gs = svg.selectAll("g")
.data(data);
gs.enter().append("g");
gs.exit().remove();
gs.attr("transform", function (d, i) {
return "translate(50," + d * 20 + ")";
})
.each(function (d, i) {
var car = this.appendChild(importedNode.cloneNode(true));
d3.select(car).select("path")
});
}
I suspect this has something to do with the .each() used to append the external SVG objects, but I am at a loss for how to get around this. Also, the "cx" and "cy" attributes are specific for circles, and so I can't think how they could be used for external SVGs.
Thanks in advance!
There are two problems with your code. The first problem, and reason why you're not seeing all the data points, is that your external SVGs contain g elements, which you are selecting. What this means is that after you first appended the elements, any subsequent .selectAll("g") selections will contain elements from those external SVGs. This in turn means that the data you pass to .data() gets matched to those and hence your selections do not contain what you expect. This is easily fixed by adding a class to the g elements you add explicitly and selecting accordingly.
The second problem is that you're executing the code that appends the external SVGs as part of the update selection. This means that those elements get added multiple times -- not something you would notice (as they overlap), but not desirable either. This is easily fixed by moving the call to clone the nodes to the .enter() selection.
Complete jsfiddle here. As for your question about cx and cy, you don't really need them. You can set the position of any elements you append using the transform attribute, as you are doing already in your code.

Categories