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)
Related
While studying animated line chart of d3.
I ran into two different ways of making line charts and still can't differentiate the difference between the two.
Two cases are as below.
1) case one - drawing line by .attr(d, line(data))
2) case two - drawing line by .attr (d, function(d){return line(d)})
Why some cases line is drawn by just calling the line function,
while the other case asks me to make a anonymous function and put the line function within that?
Here are the sample cases I ran into.
1) case one example
https://bl.ocks.org/pjsier/28d1d410b64dcd74d9dab348514ed256
2) case two example
https://bl.ocks.org/phvaillant/53b90038b9c5ac5f6b817a4f63fbc2af
Whenever you see function(d) {...} as a parameter of .attr(), .style(), .data(), .datum(), .each() and a few others, d refers to the bound datum of each element in the selection.
But if you see a line drawn as in the first approach:
.attr("d", line(data))`
Every element in the selection will be given the same line: line(data) will return the same value for every element in the selection. This approach might be taken if you haven't bound any data to the selection. To draw multiple different lines we'd need to use a loop of some sort and change the value of data. If binding data to the selection (which is a key purpose of D3), you should use that bound data as in the second approach - it'll be easier if you decide to have more than one line.
In the second approach:
.attr("d", function(d) { return line(d); })
The bound datum each element in the selection is passed to line(), as the bound datum for each line can be different, you can have different lines without an explicit loop. For multiple lines, this would certainly be the idiomatic method, though for single lines, the difference is, honestly, fairly negligible.
I didn't actually see .attr("d", line(data)) in the first example's link, the first example's link seems to have instead .attr("d", line), which is equivalent to the second example:
In the second example, this:
.attr("d", function(d) { return line(d); })
Is equivalent to:
.attr("d", line);
In the simpler line the bound datum is passed to line and line is executed for each item in the selection. Here's the same block updated to demonstrate.
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.
I'm following the General Update Pattern but having an issue with regards to layering.
Using a circle-pack layout, I pack the new data, update, enter and exit the circle elements. However, when new elements enter, they overlap the updated circles.
Data key function is based on element name:
.data(nodes, function(d, i) { return d.name; });
So my circle pack has a spot for the updated circle (of the correct location and size) but it's hidden behind its newly entered parent circle.
Is there a way to send these updated nodes to the front or redraw them over the entered circles?
--UPDATE--
As suggested by the person who closed this issue, I've tried implementing the linked to solution using moveToFront.
I added the following code in my update section (which didn't change anything) and then tried adding it after the enter and exit code, which also didn't make any difference.
.each("end", function(d){ d3.select(this).moveToFront(); });
d3.selection.prototype.moveToFront = function() {
return this.each(function(){
this.parentNode.appendChild(this);
});
};
For clarity, this is what the selection and update looks like:
// Load data into svg, join new data with old elements, if any.
var nodes = pack.nodes(postData);
node = root = postData;
groupNodes = svg.selectAll("g")
.data(nodes, function(d, i) { return d.name; });
// Update and transition existing elements
groupNodes.select("circle")
.transition()
.duration(duration)
.attr('transform', function(d) { return 'translate(' + d.x + ',' + d.y + ')'; })
.attr('r', function(d) { return d.r; })
.each("end", function(d){ d3.select(this).moveToFront(); });
This moveToFront code does not make a difference to my output, and the updated circles remain behind the entered selection circles.
To summarize: the issue seems to be caused by a hierarchy layout (circle-packing) which expects the circles to be drawn in the order of the data's hierarchy. The d3 update pattern (using enter, update and exit selections) causes selected update elements to remain in the svg when the hierarchy is re-drawn, and the new layers are drawn over it. The parents of those nodes are already correctly set, so parentNode.appendChild doesn't do anything in this case, because it's not the cause of the issue.
Here is a fiddle to demonstrate my issue. I've tried putting the moveToFront code in various places, with no visible difference.
When you hit the "Change Data" button, it'll redraw the circles, but any circles whose names overlap between the two data sets are not nested properly in the circle-pack. Children of "Group A" are hidden behind one of the parent circles. You can verify the nodes are there via Inspect Element.
Another pic from the updated fiddle:
D3 provides a way to reorder elements based on the data bound to them with the .sort() function. In your case, the condition to check is the .depth attribute of the elements -- "deeper" elements should appear in front:
svg.selectAll("g")
.sort(function (a, b) {
if (a.depth < b.depth) return -1;
else return 1;
});
Complete demo here.
I'm creating a bunch of Objects (using a pseudo-class, so they all have the same structure) with the properties "name", "type", "status", "poxitionX" and "positionY".
Then I activate my SVG drawing area using d3.js
var svg = d3.select("#d3canvas")
.append("svg")
.attr("width", 600)
.attr("height", 400);
and draw my Objects as circles on the canvas
function drawCircle (objectHandle) {
var tempDrawVar = svg.append("circle")
.style("stroke", "white")
.style("fill", "orange")
.attr("r", 20)
.attr("cx", objectHandle.positionX)
.attr("cy", objectHandle.positionY)
.on("click", function(d){
objectHandle.doStuff();
});
}
doStuff() is a method / prototype function that is supposed to ask the user for input and react to the user input by changing attributes of some of the previously created circles.
The problem is, that I don't know how to "target" those circles. I can update the properties in the Objects just fine, but I really don't think completely deleting the canvas area and creating new circles with every "update" is anywhere near a decent solution.
I can't use tempDrawVar outside the function, and even if I could it would be overwritten each time a new circle is draw anyway (or is it? I'm not sure, I admit).
I tried creating an global Array, using the draw function's parameter as index and using that as the variable instead of tempDrawVar. The drawing function works, but the array stays empty...
var circleArray = new Array();
function drawCircle (objectHandle) {
circleArray[objectHandle] = svg.append("circle")
...
Can anybody point me in the right direction? (In a nutshell: How can I create a function that targets a specific "item" created with d3.js and change one or more of its attributes?)
There are a few options for identifying specific elements. You can use a DOM selector, which means that you would need something like an ID or a class attached to the element to target it.
d3.selectAll("circle").data(data).enter()
.append("circle")
.attr("id", function(d) { return d.id; })
.attr("class", function(d) { return d.class; });
// select DOM element for first data element based on ID
d3.select("#" + data[0].id);
// select by class
d3.select("circle." + data[0].class);
Alternatively, you can use D3's data binding to do the matching. This relies on having a matching function that tells D3 how to match data to DOM elements.
// second argument to .data() is the matching function
d3.selectAll("circle")
.data(data, function(d) { return d.id; })
.enter()
.append("circle");
// select the DOM element for the first data element
d3.selectAll("circle")
.data([data[0]], function(d) { return d.id; });
The latter is probably the more D3 way to do it, but more code (and not particularly nice code at that) to select the element. You also have to remember to pass the same matching function to .data() all the time.
In the example code you've posted, it doesn't look as if you're binding any data to the created DOM elements, so you would have to assign an ID or a class to be able to identify them.
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.