I have the following code;
var matrix = [
[ 1, 2, 3, 4],
[ 5, 6, 7, 8]
];
var tr = d3.selectAll("td.sparkline").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; });
What is the correct way to do the following;
<td class="sparkline">matrix[0]</td>
<td class="sparkline">matrix[1]</td>
Can I use the same class for the container?
I keep getting both arrays in both rows.
I read about the nesting here
http://bost.ocks.org/mike/nest/
but I still can't figure it out.
It's not clear what you're trying to accomplish here. Do you have a table with a td element with class "sparkline" already in the DOM, and you're trying to add a new table inside it? Or do you want a new table somewhere else? Do you want one cell per number in your matrix, or one cell per row in the matrix?
This fiddle illustrates the above choices: http://jsfiddle.net/nrabinowitz/ma7Dn/
In the first version, the code is as you've provided it, and there's an existing table in the DOM with a td.sparkline element to append new content to. The result is a new table within that td, with rows and cells corresponding to your matrix structure.
In the second version, we're appending a new table to a different element, with one row per row in the matrix and one cell per row:
var tr2 = d3.select("#v2").append("table").selectAll("tr")
.data(matrix)
.enter().append("tr").append("td")
.attr("class", "sparkline")
.text(function(d) { return d.join(", "); });
The first version works as expected with your own code, so I'm assuming that's not what you want. My guess is that your initial selector td.sparkline is the cause of the confusion; if that's not referencing something already in the DOM, it's an error here.
Figured it out...
d3.selectAll('td.sparkline')
.data(matrix)
.text(function(d, i)
{ return 'Result #' + (i + 1) + ': ' + d; // i is 0-based.
});
gives me the ability to add each index to the correct row.
This link was pretty helpful;
http://code.hazzens.com/d3tut/lesson_1.html
Related
I have made a compound bar chart representing footballers within football teams. The chart is here: http://andybarefoot.com/football/path.html
I used d3 and built the page to work in two stages. Firstly I load the data and create a rectangle for each player. I then update the parameters of the rectangles based on the data assigned to each element depending on which view is chosen. This means that the different navigation options resize and rearrange the rectangles based on existing data mapped to the elements but no additional data is loaded in.
Whilst the resizing of the rectangles works correctly I am unable to reorder the rectangles based on the data.
The vertical position of each rectangle is set simply by "i" multiplied by a set spacing variable. To change the order I thought I could selectAll all elements, sort based on the relevant data, and then set the new vertical position in the same way. (i.e. the value of "i" would have changed). However I can't get this to work.
Here is my (unsuccessful) attempt:
// select all elements and then reorder
svg
.selectAll(".team")
.sort(function(a, b) {
return b.totalClubContractDistance - a.totalClubContractDistance;
})
;
// select all elements and reposition according to new order
svg
.selectAll(".team")
.duration(750)
.attr("transform", function(d,i) {
return "translate(0,"+teamSpacing*i+")";
})
;
In d3 there are 4 core concepts. Join, Update, Enter, Exit. You can read more here: https://bost.ocks.org/mike/join/
Basically, every time you want to update the position of an element, you should change the data, then do a join followed by an update.
So the code would look like this:
function render (data) {
// join
// this joins the new data to the existing data
var teams = svg.selectAll('.team')
.data(data);
// update
// this will update existing teams that have a different location
teams.attr('transform', function (d, i) {
return 'translate(0, ' + teamSpacing * i + ')';
});
// enter
// this will add new teams that were added to the data set
teams.enter()
.attr('transform', function (d, i) {
return 'translate(0, ' + teamSpacing * i + ')';
});
// exit
// this will remove all the teams that are no longer part of the data set
teams.exit()
.remove();
}
Hope this helps
I am working on a d3 plot, where I have multiple elements which might overlap when drawn.
Each element renders a timeline and has multiple graphical units (start circle, line and end circle), something like as below:
O----------O O
O--------------------O
O-------O-----O-------O
For example the third line has two timeline plot elements which are overlapping as start time of the 2nd timeline is before end time of the first timeline. Note that 2nd timeline in the first line has only start time (as end time and start time are same).
Now, the following code brings an element of the timeline to front on mouseover by moving the DOM node to be the last child of its parent.
d3.selection.prototype.moveToFront = function() {
return this.each(function(){
this.parentNode.appendChild(this);
});
};
But the problem is that this is not altering the order of the bound data and is breaking the overall plot.
Each of the plot element has specific order in the dom which is bound to the d3 data in the same order. When the code above changes the order to bring any element to the front it is breaking the order, it still thinks that the order of the children are the same, which is wrong.
Here is a sample JSFiddle to describe the issue:
https://jsfiddle.net/pixelord/g2gt1f03/57/
How can I retain the data order once I have altered the dom elements?
Thanks.
Instead of doing the html update by yourself let d3 do it, remember that d3 stands for data driven documents so rewrite your problem as
On mouseover move the selection's datum to the last position and then rerender the graph
Imagine that your data is [0,1,2,3], when you mouseover on any element that represents the second datum you move the second datum to the last position i.e. [0,2,3,1] and that's pretty much it
.on("mouseover", function() {
var selection = d3.select(this);
var d = selection.datum()
// find d in data, extract it and push it
var index = data.indexOf(d)
var extract = data.splice(index, 1)
data = data.concat(extract)
draw()
});
Next when you bind your data make sure you add a way to distinguish from both states which is done with the second parameter sent to the .data() function which might be an id
var data = [
[5, 8, 6],
[10, 10, 6],
[20, 25, 6],
[23, 27, 6]
].map(function (d, i) {
return {
id: i,
x1: d[0],
y1: d[2],
x2: d[1],
y2: d[2]
}
});
// ...
var itemGroup = maingroup.selectAll(".itemGroup")
.data(data, function (d) { return d.id })
Finally you'll need to tell d3 that we have modified the order of the elements and that it needs to do what you were doing by hand which is reorder the elements
// update
itemGroup.order()
Demo
I like the way Mauricio solved the issue.
However, after some investigation I came to know that I can specify key value while binding the data. So here is my solution without re-ordering the data itself:
I added a key value property for each data object which is unique.
I specify the key value while binding the data like,
data(data_list, funcion(d){return d.keyValue})
the problem was fixed.
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
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);
)();
}
});
With a d3.js join Is there a way to select only the 'updating' elements separately from the 'entering' elements?
updateAndEnter = d3.selectAll('element').data(data);
entering = updateAndEnter.enter();
exiting = updateAndEnter.exit();
updatingOnly = ??;
Yes, the selection just after the data join contains the 'update only' elements. After appending to the enter() selection, it will be expanded to include the entering elements as well.
See General Update Pattern:
// DATA JOIN
// Join new data with old elements, if any.
var text = svg.selectAll("text")
.data(data);
// UPDATE
// Update old elements as needed.
text.attr("class", "update");
// ENTER
// Create new elements as needed.
text.enter().append("text")
.attr("class", "enter")
.attr("x", function(d, i) { return i * 32; })
.attr("dy", ".35em");
// ENTER + UPDATE
// Appending to the enter selection expands the update selection to include
// entering elements; so, operations on the update selection after appending to
// the enter selection will apply to both entering and updating nodes.
text.text(function(d) { return d; });
// EXIT
// Remove old elements as needed.
text.exit().remove();
it's my pleasure
For me ( too ) this is a little bit confusing : it seems that the only available set is actually ENTER+UPDATE ( blended together ) and EXIT.
But what if i want to work or at least identify only updated elements?
I wrote a very simple function ( that follows, simply put wrap it in a script tag at the end of a basic html page ) showing this simple dilemma : how do I highlight updated elements ? Only ENTER and EXIT seem to react "correctly"
To test it, just type in chrome console :
manage_p(['append','a few','paragraph'])
manage_p(['append','a few','new','paragraph'])
manage_p(['append','paragraphs'])
I can get green or red highlighting, i can't get white
Maybe we're missing D3Js specs?
Best regards,
Fabrizio
function join_p(dataSet) {
var el = d3.select('body');
var join = el
.selectAll('p')
.data(dataSet);
join.enter().append('p').style('background-color','white');
join.style('background-color','green').text(function(d) { return d; });
join.exit().text('eliminato').style('background-color','red');
}