How do I count how many nodes were matched by a selectAll? (without joined data)
Or if there's data, how to count the data from the selection? (suppose I've set it with "data(function...)" so I don't know the length in advance)
Just use d3.selectAll(data).size().Hope this example help you:
var matrix = [
[11975, 5871, 8916, 2868],
[ 1951, 10048, 2060, 6171],
[ 8010, 16145, 8090, 8045],
[ 1013, 990, 940, 6907]
];
var tr = d3.select("body").append("table").selectAll("tr")
.data(matrix)
.enter().append("tr");
var td = tr.selectAll("td")
.data(function(d) { return d; })
.enter().append("td")
.text(function(d) { return d; });
var tdSize=tr.selectAll("td").size();
Complete jsfiddle here.
If you want the length conveniently from a callback function, such as setting an element attribute, it seems that you can get it from the third argument, like so:
node
.attr('some-property', function(datum, index, array) {
// d/datum = the individual data point
// index = the index of the data point (0, 1, 2, 3, etc)
// array = full array of data points
// array.length = the size/length of the data points / dataset
// some calculation involving array.length or whatever
return someValue;
});
Similar to the call signature of the JavaScript forEach/filter/etc. array functions.
Seems like most of the d3 functions support this:
https://github.com/d3/d3-selection
...current datum (d), the current index (i), and the current group (nodes), with this as the current DOM element (nodes[i])
...is a repeated phrase throughout the docs. So if you see a d3 function where you'd use d, you can probably also get index and array.
One way I have done this previously is to pass that information into the data function by making a new object.
.data(function(d) {
return d.Objects.map(function(obj) {
return {
Object: obj,
TotalObjects: d.Objects.length
}
});
Then in your update portions you use Object and still have the count available.
.attr("x", function(d) {
return d.Object.X;
})
.attr("y", function(d) {
return d.TotalObjects;
})
To get the data count, then after .selectAll() and .data(), it appears that .enter() is needed before .size():
legend_count = legendBoxG.selectAll("legend.box")
.data(curNodesData, (d) -> d.id)
.enter()
.size()
Without the .enter(), the result is 0. The .enter() makes it return the data count. (Code above is shown in Coffee dialect.)
I need to get the count before adding attributes to my svg objects (in order to scale them properly), and none of the preceding examples did that. However I can't seem to add more attributes after stripping out the count into a variable as above. So while the above approach demonstrates the operation of data() and enter() it isn't really a practical solution. What I do instead is to get the length of the data array itself before doing the selectAll(). I can do that most simply with the length property (not a function) on the data array itself:
legend_count = curNodesData.length
Related
even if all the values are right before it reaches to the drawing part,
the code below keeps returrning error message
Expected number, "MNaN,NaNLNaN,NaNL…".
my lineRadial function is as below.
var radarLine = d3.lineRadial()
.radius(function(d) {
return rscale(d.value)
})
.angle(function(i) { return i * angleSlice })
And i called this function to draw radial chart like below.
var blobWrapper = g.selectAll(".radarWrapper")
.data(data)
.join('g')
.attr("class", "radarWrapper")
blobWrapper.append('path')
.attr('class', 'radarArea')
.attr('d', function(d) { return radarLine(d); })
.style('fill', function(d, i) { return cfg.color[i] })
.style('fill-opacity', cfg.opacityArea)
I fixed this error by putting one dummy parameter in the line function.
var radarLine = d3.radialLine()
.radius(function(d) {
return rscale(d.value)
})
.angle(function(d, i) { return i * angleSlice })
just by adding 'd' parameter to the angle function, it started working.
why is it?
That's not a dummy parameter (or an argument, from the callback function point of view).
The angle function's...
accessor will be invoked for each defined element in the input data array, being passed the element d, the index i, and the array data as three arguments. (source)
Therefore, if you use..
angle(function(i) { return i * angleSlice })
That i is not the index, no matter if you call it i, d, α or whatever. The index, which is what you want, will be always the second parameter.
Finally, there is a Javascript convention that says we use _ to indicate that the parameter is not used. That being said, you'd have:
angle(function(_, i) { return i * angleSlice })
The name of the parameters doesn't matter, only their order.
The first parameter passed to the accessor functions of d3.line or d3.lineRadial is the point being plotted, conventionally labelled as d, but it doesn't matter what you use.
The second parameter passed is the index of the current point. Conventionally labelled i.
The third parameter is the entire data array of all the points, rarely seen, so there is not really a conventional name for it in the d3 idiom.
So, in your non-working code:
.angle(function(i) { return i * angleSlice })
i, as the first parameter, represents an item in the data array, which is an object, and doesn't produce a number when multiplied by a constant.
This is why you need to use:
.angle(function(d,i) { return i * angleSlice })
As the 2nd parameter is the index you're looking for. Sometimes you'll see an underscore used for an unused parameter, rather than use d.
I've created a set of d3js elements based on an array of 3 elements:
var data = [[0,0,2],[0,23,5],[2,12,5]];
circleSet = svg.selectAll()
.data(data)
.enter().append('circle');
edit:
How can I select the second element by index?
The most natural way to manipulate just one element is using the filter function:
var data = [[0,0,2],[0,23,5],[2,12,5]];
var circleSet = svg.selectAll()
.data(data)
.enter()
.append('circle');
var filteredCircleSet = circleSet
.filter(function (d, i) { return i === 1;})
// put all your operations on the second element, e.g.
.append('h1').text('foo');
Note that depending on what you do with the other elements you might use one of the two variants of this approach:
variant a): use the filter in the data function (to reduce the data and the appended elements)
variant b): use the filter to exclude instead of to include in order to remove the other elements at the end
See also Filter data in d3 to draw either circle or square
One other way to do it is to use the selection.each method: https://github.com/mbostock/d3/wiki/Selections#wiki-each
By using an if statement with the corresponding index you can create a block for one element.
E.g.
var data = [[0,0,2],[0,23,5],[2,12,5]];
var circleSet = svg.selectAll()
.data(data)
.enter()
.append('circle')
.each(function (d, i) {
if (i === 1) {
// put all your operations on the second element, e.g.
d3.select(this).append('h1').text(i);
}
});
In d3 v4 and above, you can use Selection.nodes(). Assuming i is the index number you want:
d3.select(someSelection.nodes()[i])
It's a natural one-liner, and it's arguably more readable: you're obviously just getting the node at i in the order, as a D3 selection.
It looks like it'd be more efficient than the alternatives, which involve looping through the entire selection with .each(). So, you might think this is O(1), while the other options are O(n).
Unfortunately, Selection.nodes() itself includes an each loop, so it's also O(n) (not that it's likely to matter in real life unless you call this thousands of times on selections with thousands of nodes):
var nodes = new Array(this.size()), i = -1;
this.each(function() { nodes[++i] = this; });
return nodes;
However, this way you can separate the looping from the getting, which could be useful if efficiency is a major concern.
For example, if you want to loop through each() in selection A and get the item in the same position from selection B, and you want to avoid loops-within-loops because those selections can be huge and you call this many times, you could structure it like this, which would be O(2n) instead of O(n^2):
var selectionBArray = selectionB.nodes()
selectionA.each(function(d, i) {
var iFromSelectionA = this
var iFromSelectionB = d3.select(selectionBArray[i])
})
...or if you're using arrow functions to preserve this context:
var selectionBArray = selectionB.nodes()
selectionA.each((d, i, nodes) => {
var iFromSelectionA = d3.select(nodes[i])
var iFromSelectionB = d3.select(selectionBArray[i])
})
You could even (ab)use Selection._groups, but I wouldn't recommend using a private property like that since it'll break if a D3 update renamed the _groups property, like this update did.
Use the preset function i variable, which references the index of the array object.
var data = [[0,0,2],[0,23,5],[2,12,5]];
circleSet = svg.selectAll()
.data(data)
.enter()
.append('circle')
.attr('fill',function(d,i){i === 1 ? return 'red' : return 'black' };
Find more on array structure references in d3.js at this tutorial
You can also encode each element you append by utilizing the count of the i index when assigning a class.
var data = [[0,0,2],[0,23,5],[2,12,5]];
circleSet = svg.selectAll()
.data(data)
.enter()
.append('circle')
.attr("class",function(d,i){ return "yourclass item" + i })
var theSecondElement = d3.select(".item1")
Last, you could use the .each method and a conditional to target a specific element
circleSet = svg.selectAll()
.data(data)
.enter()
.append('circle')
.each(function (d, i) {
if (i === 1) {
var that = this;
(function textAdd() {
d3.select(that).append('h1').text(i);
)();
}
});
I'm having a hard time understanding selection.filter. I tried this, in a blank document:
d3.selectAll('rect')
.data([1, 2, 3])
.filter(function(d) { return d>1; })
// [ Array[0] ]
I expected the selection to have 2 elements, but it has 0. Maybe it's because I'm working with an empty update selection...
d3.selectAll('rect')
.data([1, 2, 3])
.enter()
.filter(function(d) { return d>1; })
// []
Now it seems I don't even have a selection.
If the DOM elements exist
d3.selectAll('rect').data([1, 2, 3])
.enter().append('rect')
and then I select and filter
d3.selectAll('rect').filter(function(d) { return d>1; })
// [ Array[2] ]
it seems to work. So what's going on here? Filter seems to work in this example on selections that don't correspond to any DOM elements, so I'd expect it to work in my first example above.
The .filter() function only works on selections that have data bound to them already, i.e. not after you've called .data() on them. The example you've linked to doesn't actually work the way it seems to do -- let me reindent for clarity:
var node = svg.selectAll(".node")
.data(bubble.nodes(classes(root))
.filter(function(d) { return !d.children; })
)
.enter().append("g")
The .filter() function isn't actually applied to a selection here, but to the array that bubble.nodes() returns. This is then passed to .data(). And this is exactly what you would do in a case like yours -- there's no need to filter the selection if you can filter the data that determines it.
I am using NVD3 to visualise data on economic inequality. The chart for the US is here: http://www.chartbookofeconomicinequality.com/inequality-by-country/USA/
These are two lineCharts on top of each other. The problem I have is that there are quite a lot of missing values and this causes two problems:
If I would not make sure that the missing values are not visualised the line Chart would connect all shown values with the missing values. Therefore I used the following to not have the missing values included in the line chart:
chart = nv.models.lineChart()
.x(function(d) { return d[0] })
.y(function(d) { return d[1]== 0 ? null : d[1]; })
But still if you hover over the x-axis you see that the missing values are shown in the tooltip on mouseover. Can I get rid of them altogether? Possibly using remove in NVD3?
The second problem is directly related to that. Now the line only connects values of the same series when there is no missing values in between. That means there are many gaps in the lines. Is it possible to connect the dots of one series even if there are missing values in between?
Thank you for your help!
As Lars showed, getting the graph to look the way you want is just a matter of removing the missing values from your data arrays.
However, you wouldn't normally want to do that by hand, deleting all the rows with missing values. You need to use an array filter function to remove the missing values from your data arrays.
Once you have the complete data array as an array of series objects, each with an array of values, this code should work:
//to remove the missing values, so that the graph
//will just connect the valid points,
//filter each data array:
data.forEach(function(series) {
series.values = series.values.filter(
function(d){return d.y||(d.y === 0);}
);
//the filter function returns true if the
//data has a valid y value
//(either a "true" value or the number zero,
// but not null or NaN)
});
Updated fiddle here: http://jsfiddle.net/xammamax/8Kk8v/
Of course, when you are constructing the data array from a csv where each series is a separate column, you can do the filtering at the same time as you create the array:
var chartdata = [];//initialize as empty array
d3.csv("top_1_L-shaped.csv", function(error, csv) {
if (error)
return console.log("there was an error loading the csv: " + error);
var columndata = ["Germany", "Switzerland", "Portugal",
"Japan", "Italy", "Spain", "France",
"Finland", "Sweden", "Denmark", "Netherlands"];
for (var i = 0; i < columndata.length; i++) {
chartdata[i].key = columndata[i];
chartdata[i].values = csv.map(function(d) {
return [+d["year"], +d[ columndata[i] ] ];
})
.filter(function(d){
return d[1]||(d[1] === 0);
});
//the filter is applied to the mapped array,
//and the results are assigned to the values array.
}
});
the pie chart update example on the bl.ocks site doesn't update the elements 'in place':
http://bl.ocks.org/j0hnsmith/5591116
function change() {
clearTimeout(timeout);
path = path.data(pie(dataset[this.value])); // update the data
// set the start and end angles to Math.PI * 2 so we can transition
// anticlockwise to the actual values later
path.enter().append("path")
.attr("fill", function (d, i) {
return color(i);
})
.attr("d", arc(enterAntiClockwise))
.each(function (d) {
this._current = {
data: d.data,
value: d.value,
startAngle: enterAntiClockwise.startAngle,
endAngle: enterAntiClockwise.endAngle
};
}); // store the initial values
path.exit()
.transition()
.duration(750)
.attrTween('d', arcTweenOut)
.remove() // now remove the exiting arcs
path.transition().duration(750).attrTween("d", arcTween); // redraw the arcs
}
Instead, it just treats the new array of value as brand new data and resizes the chart accordingly.
I've created a fiddle demonstrating the issue very simply:
http://jsfiddle.net/u9GBq/23/
If you press 'add', it add a random int to the array: this works as intended.
If you press 'remove', the only element getting transitioned out is always the last element to have entered the pie. In short, it behaves like a LIFO stack.
The expected behaviour is for the relevant pie arc to get transitioned out instead.
Is it possible to apply object consistency to pies? I've also tried adding a key function (not demonstrated on the fiddle) but that just breaks (oddly enough it works fine with my stacked graphs).
Thank you.
The easiest solution to this problem is to set missing values to zero, rather than removing them entirely, as in Part III of the Pie Chart Update series of examples. Then you get object constancy for free: you have the same number of elements, in the same order, across updates.
Alternatively, if you want a data join as in Part IV, you have to tell D3 where the entering arcs should enter from, and where the exiting arcs should exit to. A reasonable strategy is to find the closest neighboring arc from the opposite data: for a given entering arc, find the closest neighboring arc in the old data (pre-transition); likewise for a given exiting arc, find the closest neighboring arc in the new data (post-transition).
To continue the example, say you’re showing sales of apples in different regions, and want to switch to show oranges. You could use the following key function to maintain object constancy:
function key(d) {
return d.data.region;
}
(This assumes you’re using d3.layout.pie, which wraps your original data and exposes it as d.data.)
Now say when you transition to oranges, you have the following old data and new data:
var data0 = path.data(), // retrieve the old data
data1 = pie(region.values); // compute the new data
For each entering arc at index i (where d is data1[i]), you can step sequentially through preceding data in data1, and see if you can find a match in data0:
var m = data0.length;
while (--i >= 0) {
var k = key(data1[i]);
for (var j = 0; j < m; ++j) {
if (key(data0[j]) === k) return data0[j]; // a match!
}
}
If you find a match, your entering arcs can start from the matching arc’s end angle. If you don’t find a preceding match, you can then look for a following matching arc instead. If there are no matches, then there’s no overlap between the two datasets, so you might enter the arcs from angle 0°, or do a crossfade. You can likewise apply this technique to exiting arcs.
Putting it all together, here’s Part V:
Ok, found the solution.
The trick was to pass the key this way:
path = path.data(pie(dataset), function (d) {return d.data}); // this is good
as opposed to not passing it, or passing it the wrong way:
path = path.data(pie(dataset, function (d) {return d.data})); // this is bad
And here's an updated fiddle with a working transition on the right arc! :)
http://jsfiddle.net/StephanTual/PA7WD/1/