D3 data() vs. datum() - javascript

In Mike Bostock’s ‘Towards Reusable Charts’ why the initial data link to <p> element is made with datum(data):
d3.csv("sp500.csv", function(data) {
var formatDate = d3.time.format("%b %Y");
d3.select("#example")
.datum(data)
.call(timeSeriesChart()
.x(function(d) { return formatDate.parse(d.date); })
.y(function(d) { return +d.price; }));
});
while further link of data to <svg> element inside the chart() function is made with data([data]):
// Select the svg element, if it exists.
var svg = d3.select(this).selectAll("svg").data([data]);
// Otherwise, create the skeletal chart.
var gEnter = svg.enter().append("svg").append("g");
gEnter.append("path").attr("class", "area");
gEnter.append("path").attr("class", "line");
gEnter.append("g").attr("class", "x axis");
In his own answer Mike says that these two approaches are interchangeable, except the former doesn’t compute a join. So why to use data([data]) here?
Also I don’t quite get what happens if there is already an <svg> element as the comment line suggests. For me the enter selection is empty in this case and no further append will work…
I must be misunderstanding something…
Thanks for your help!

The advantage of using .data([data]) in the second case is that if there's no SVG, the handling of the enter selection adds it. If the SVG exists, the code is exactly equivalent to .datum(data) -- the data bound to the element is changed and nothing else happens.

Related

d3.js insert line path by "datum" or line (data)

I was trying to plot a time-pressure line chart.
The data is an array of objects, named "res"
[
{Time: ,
Psi:
},
...
]
I defined the x, y axis, and line function like these
var x = d3.scaleTime().domain(d3.extent(res, d => d.Time)).range([0, width]),
y = d3.scaleLinear().domain([0,d3.max(res, d=>d.psi)]).range([height, 0]),
var line = d3.line()
.x(function(d) { return x(d.Time) })
.y(function(d) { return y(d.psi) });
Every thing was very standard set-up.
When I insert line element to the chart, I found two ways to insert them.
Method 1, with "datum"
svg.append("path")
.datum(res) //"datum"
.attr("class", "line")
.attr("d", line);
Method 2, with by line(res)
svg.insert("path")
.attr("class", "line")
.attr("d", line(res)); //line(res), like a function
Both methods work, just wondering are there any difference between these two methods?
Thanks,
The difference between the two methods is that by method one, you have assigned res as the "datum" object of the node. That means that if you were to store it in a variable - or I think even if you would re-select them (not sure though) - you should be able to reliably access the current value using .attr('...', function(d) { });. That can be useful if you want to do stuff to it, like animations or styling, and the value might update often - so it's a hassle to carry it around.
Other than that, there is no real difference. One of the things I like to use .datum() for is when I have a container for every shape and I want to add a node to every container, then it might be useful to use container.select('text').datum((d) => d) to feed the datum object from the container to its text child.

ColorBrewer in D3 does not work

I overlay a leaflet map with the d3 library. The points get displayed, as well as the map. However, the Colorbrewer does not work...
Its supposed to color the dots on the map according to their value, instead, they stay black. I could hardcode that with something like if value == 0.1 but thats not what I want...
Thats my code, the structure of cities.json can be seen here, the colorbrewer is this one
...
// add colorbrewer
var colorScale = d3.scale.quantize()
.domain([extent[0], extent[1]])
.range(colorbrewer.YlGn[n]);
// uses d3 data join method
// for each data point a "path" is created
var feature = g.selectAll("path")
.data(collection.features)
.enter()
.append("path")
.style("fill", function(d) {
colorScale(d.properties.pop_max);
});
...
Any ideas what is going wrong?!
There are negative values in my d.properties.pop_max. Could that be the problem?
You're missing a return in the fill function.
...
...
.style("fill", function(d) {
// add a 'return' here.
return colorScale(d.properties.pop_max);
});
Also, you can just write .domain(extent) directly when initializing your colorScale since d3.extent returns a two element [min, max] array
var colorScale = d3.scale.quantize()
.domain(extent) // instead of .domain([extent[0], extent[1]])
.range(colorbrewer.YlGn[n]);

D3 update circle-pack data new nodes overlap existing nodes

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.

d3.js How to target and change elements' attributes

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.

d3.js: How to add a data key?

I'm learning D3.js and trying to get my head around data keys used with streamgraphs. I would like to adapt the official streamgraph example:
...so that each path has an explicit data key, and so that the mouseover logs the data key.
The official example adds paths as follows:
var area = d3.svg.area()
.x(function(d) { console.log('x', d.data); return d.x * w / mx; })
.y0(function(d) { return h - d.y0 * h / my; })
.y1(function(d) { return h - (d.y + d.y0) * h / my; });
vis.selectAll("path")
.data(data0)
.enter().append("path")
.style("fill", function() { return color(Math.random()); })
.attr("d", area);
I tried adapting the code as follows, but I'm not sure how to change the structure of data0 (currently an array of arrays) to achieve what I want:
vis.selectAll("path")
.data(data0, function(d) { return d.name }) // Add key function
.enter().append("path")
.style("fill", function() { return color(Math.random()); })
.attr("d", area)
.on("mouseover", function (d,i) {
console.log("mouseover", d.name); // Log name property on mouseover
});
As it stands, without my having made any changes to the structure of data0, it unsurprisingly does not work. How can I add a name property to data0 without also messing up the area and .data() functions?
UPDATE: To be a bit clearer: the D3 docs say that the area function is expecting a two-dimensional array. So if I change data0 from a two-dimensional array, to an array of objects, each with a name key and a data key, how can I also change what I pass to area?
The data in the example doesn't have a "name" property, so you would need to add that to the data to use it. The data keys you refer to are used when merging/updating data, i.e. you have drawn some paths already and then update (some of them). The .data() function will try to figure out what data is updated and what data is new. If that doesn't work for you, you can use the data key to help it, i.e. in your case tell it that things with the same name are the same data.
If what you mean by data keys are "data legends", then you might want to take a look at the following examples where I've completely separated the placement of magnitudes, legend bullets and legend text in different areas of the charts.
Multiple D3 Pie Charts Mixed In With HTML Layout Constructs
Multiple D3 Horizontal Bar Charts Mixed In With HTML Layout Constructs
In each of the examples, you'll clearly see how the data is labeled, structured, passed in, and used.
I also tied them together through mouseover and mouseout events so that mousing over or out of any element causes all elements in a set to change color simultaneously.
I hope this helps.
Frank

Categories