How do I create a <dl> using d3.js - javascript

I'd like to create a series of dl tags in a list from some data using d3.js.
The code I came up with is this:
var x=d3.select("body")
.append('ol')
.selectAll('li')
.data(data)
.enter()
.append('li')
.append('dl')
.selectAll()
.data(d=>Object.entries(d.volumeInfo)).enter();
x.append('dt')
.text(d=>d[0]);
x.append('dd')
.text(d=>d[1]);
where data is an array of objects. Everything works except the elements are not in the correct order.
Here is the order I manage to get:
<dl>
<dt>key1</dt>
<dt>key2</dt>
<dd>value1</dd>
<dd>value2</dd>
</dl>
But it should be like this:
<dl>
<dt>key1</dt>
<dd>value1</dd>
<dt>key2</dt>
<dd>value2</dd>
</dl>
I've done a fair amount of googling and nothing answers the question, at least not in a way that works in v5 or not with more than one dt/dd pair.
This seems like something basic that d3.js should be able to do.

In your solution:
x.append('dt')
.text(d=>d[0]);
x.append('dd')
.text(d=>d[1]);
All elements appended with an enter().append() cycle are appended to the parent, in the order they are appended, which for you runs like this: first all the dts, then all the dds, as you have seen. The placeholder nodes (these are not the appended elements) created by the enter statement do not nest children in a manner it appears you might expect them to.
Despite the fact that d3 doesn't include methods to achieve what you are looking for with methods as easy as a simple selection.append() method, the desired behavior can be achieved fairly easily with standard d3 methods and an extra step or two. Alternatively, we can build that functionality into d3.selection ourselves.
For my answer I'll finish with an example that uses your data structure and enter pattern, but to start I'll simplify the nesting here a bit - rather than a nested append I'm just demonstrating several possible methods for appending ordered siblings. To start I've also simplified the data structure, but the principle remains the same.
The first method might be the most straightforward: using a selection.each() function. With the enter selection (either with a parent or the entered placeholders), use the each method to append two separate elements:
var data = [
{name:"a",description:"The first letter"},
{name:"b",description:"The second letter"}
];
d3.select("body")
.selectAll(null)
.data(data)
.enter()
.each(function(d) {
var selection = d3.select(this);
// append siblings:
selection.append("dt")
.html(function(d) { return d.name; });
selection.append("dd")
.html(function(d) { return d.description; })
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
But, perhaps a more elegant option is to dig into d3.selection() and toy with it to give us some new behaivor. Below I've added a selection.appendSibling() method which lets you append a paired sibling element immediately below each item in a selection:
d3.selection.prototype.appendSibling = function(type) {
var siblings = this.nodes().map(function(n) {
return n.parentNode.insertBefore(document.createElement(type), n.nextSibling);
})
return d3.selectAll(siblings).data(this.data());
}
It takes each node in a selection, creates a new paired sibling node (each one immediately after the original node in the DOM) of a specified type, and then places the new nodes in a d3 selection and binds the data. This allows you to chain methods onto it to style the element etc and gives you access to the bound datum. See it in action below:
// modify d3.selection so that we can append a sibling
d3.selection.prototype.appendSibling = function(type) {
var siblings = this.nodes().map(function(n) {
return n.parentNode.insertBefore(document.createElement(type), n.nextSibling);
})
return d3.selectAll(siblings).data(this.data());
}
var data = [
{name:"a",description:"The first letter"},
{name:"b",description:"The second letter"}
];
d3.select("body")
.selectAll(null)
.data(data)
.enter()
.append("dt")
.html(function(d) { return d.name; })
.appendSibling("dd") // append siblings
.html(function(d) { return d.description; }) // modify the siblings
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
Of course it is probably wise to keep the siblings in separate selections so you can manage each one for updates/entering/exiting etc.
This method is very easily applied to your example, here's a nested solution using data that is structured like you expect and the appendSibling method:
// modify d3.selection so that we can append a sibling
d3.selection.prototype.appendSibling = function(type) {
var siblings = this.nodes().map(function(n) {
return n.parentNode.insertBefore(document.createElement(type), n.nextSibling);
})
return d3.selectAll(siblings).data(this.data());
}
var data = [
{volumeInfo: {"a":1,"b":2,"c":3}},
{volumeInfo: {"α":1,"β":2}}
]
var items = d3.select("body")
.append('ol')
.selectAll('li')
.data(data)
.enter()
.append('li')
.append('dl')
.selectAll()
.data(d=>Object.entries(d.volumeInfo)).enter();
var dt = items.append("dt")
.text(function(d) { return d[0]; })
var dd = dt.appendSibling("dd")
.text(function(d) { return d[1]; })
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>

Here is a possibility using the .html appender (instead of .append):
var data = [
{ "volumeInfo": { "key1": "value1", "key2": "value2" }, "some": "thing" },
{ "volumeInfo": { "key3": "value3", "key4": "value4", "key5": "value5" } }
];
d3.select("body")
.append('ol')
.selectAll('li')
.data(data)
.enter()
.append('li')
.append('dl')
.html( function(d) {
// Produces: <dt>key1</dt><dd>value1</dd><dt>key2</dt><dd>value2</dd>
return Object.entries(d.volumeInfo).map(r => "<dt>" + r[0] + "</dt><dd>" + r[1] + "</dd>").join("");
});
dt { float: left; width: 100px; }
dd { margin-left: 100px; }
<script src="https://d3js.org/d3.v5.min.js"></script>
which produces this tree:
<ol>
<li>
<dl>
<dt>key1</dt>
<dd>value1</dd>
<dt>key2</dt>
<dd>value2</dd>
</dl>
</li>
<li>
<dl>
<dt>key3</dt>
...
</dl>
</li>
</ol>
Note that this is not exactly in the spirit of d3 and makes it difficult to work with appended children (adding class, style, other children, ...).

Example for data would be useful. But instead of appending to x, have you tried appending directly to dt itself?
x.append('dt').text(d=>d[0]).append('dd')

Related

Why is D3.js data only available to child nodes when enter() is chained, not invoked separately

I'm trying to follow what Mike Bostock seems to indicate is a best practice, namely assigning your selectAll() to a variable and then separating out the update, the enter() and the exit(), but I'm noticing a discrepancy in V4 that I can't explain.
Consider the following working code:
// Bind an array of users to the #users div
var userNodes = d3.select("#users").selectAll("li")
.data(d3.values(users))
// Add LIs for any new users
.enter()
.append("li");
var userMessageGraph = userNodes.selectAll("span")
.data(function(d){ return [d.name]; })
.enter().append("span")
.text(function(d){ return d; });
Which creates an empty LI and then appends a SPAN inside it with the user name. (By the way, if this is all I wanted I'm sure there's a better way, but this is just a reduction of something else to illustrate my point. Bear with me if you will.)
Now I try to adapt it to my interpretatioin of said best practice by running the enter() against the stored variable, and the child elements lose their link to the parent data. I get a list of empty LIs.
// Bind an array of users to the #users div
var userNodes = d3.select("#users").selectAll("li")
.data(d3.values(users));
// Add LIs for any new users
userNodes.enter()
.append("li");
var userMessageGraph = userNodes.selectAll("span")
.data(function(d){ return [d.name]; })
.enter().append("span")
.text(function(d){ return d; });
Update 1
Well, after reading [~Gerardo Furtado]'s response, I thought I had it. Clearly I'm missing a fundamental principle here.
Here's my code that attempts to use merge() to make sure the data is carried over to the child elements, with no joy:
var userNodes = d3.select("#users").selectAll("li")
.data(d3.values(users));
// Add LIs for any new users
userNodes.enter()
.append("li")
// New for V4, merge back the original set to get the data
.merge(userNodes);
var userMessageGraph = userNodes.selectAll("span")
.data(function(d){ return [d.name]; })
.enter().append("span")
.text(function(d){ return d; });
I suppose you're using d3 v4.x. In that case, that's the expected behaviour.
This is what's happening: userNodes is the data binding variable:
var userNodes = d3.select("#users").selectAll("li")
.data(d3.values(users));
Then, you write:
userNodes.enter()
.append("li");
And that's the "enter" selection.
In d3 v3.x that enter selection magically modifies your original variable, turning it into this:
var userNodes = d3.select("#users").selectAll("li")
.data(d3.values(users));
.enter()
.append("li");
However, in d3 v4.x, your original variable remains just a data binding selection:
var userNodes = d3.select("#users").selectAll("li")
.data(d3.values(users));
So, if we rewrite your userMessageGraph taking into account the chaining, this is what it really is:
var userMessageGraph = d3.select("#users")//here userNode starts
.selectAll("li")
.data(d3.values(users))//this is where userNode ends
.selectAll("span")
.data(function(d){ return [d.name]; })
.enter().append("span")
.text(function(d){ return d; });
You can see that the "enter" selection for the <li>, which is...
.enter()
.append("li")
...is missing.
EDIT: This edit addresses the OP's new code:
var userNodes = d3.select("#users").selectAll("li")
.data(d3.values(users));
// Add LIs for any new users
userNodes.enter()
.append("li")
// New for V4, merge back the original set to get the data
.merge(userNodes);
That won't work for a reason: the merge function...
...returns a new selection merging this selection with the specified other selection. The returned selection has the same number of groups and the same parents as this selection. (emphasis mine)
As userNodes is an empty selection, this will not work. You can invert the logic:
var userNodes = d3.select("#users").selectAll("li")
.data(d3.values(users));
var userNodesEnter = userNodes.enter()
.append("li");
var userNodesUpdate = userNodesEnter.merge(userNodes);
var userMessageGraph = userNodesUpdate.selectAll("span")
.data(function(d) {
return [d.name];
})
.enter().append("span")
.text(function(d) {
return d;
});

d3.js iterate order changes

As project to get to know d3.js, I’m displaying tweets on a map in real-time. Everything has worked this far, and I’m very, very pleased with the library.
On the page, I’m trying to list all languages. On hover, I want all tweets of that language to pop up. All of this is done, except some items in the list pops up the tweets of another language. A wrong one, I might add.
This is how I project the dots on the map:
points.selectAll('circle')
.data(tweets)
.enter()
.append('circle')
// Attach node to tweet, so I can use refer to the nodes later
.each(function(d) {
d.node = this;
})
.attr('r', 1);
This is how I create the list:
var data = d3.nest()
// Group by language code
.key(function(d) { return d.lang.code; })
.entries(tweets)
// Sort list by amount of tweets in that language
.sort(function(a, b) {
return b.values.length - a.values.length;
});
var items = languages_dom
// Add items
.selectAll('li')
.data(data)
.enter()
.append('li');
// Used for debugging
.attr('data-lang', function(d) {
return d.key; // Group key = language code
})
// Set text
.text(function(d) {
var dt = d.values[0];
return dt.lang.name;
})
// Mouseover handler
.on('mouseover', function(d) {
// Compare attribute with
// These values are actually different
var attr = d3.select(this).attr('data-lang');
console.log(attr, d.key);
// Pop up each node
d.values.forEach(function(d) {
d = d3.select(d.node);
d.transition()
.duration(200)
.attr('opacity', 0.5)
.attr('r', 8);
});
});
Note that the script above is run several times. d.key refers to another value later in the chain, while I’m not modifying data in that chain.
Edit 22:08
Things seems to work fine when I’m not sorting the data. At least it’s a lead.
As noted in the comments, you're overwriting d in your forEach function. Instead, you should try something like
d.values.forEach(function(p) {
d3.select(p.node)
.transition()
.duration(200)
.attr('opacity', 0.5)
.attr('r', 8);
});
Notice the forEach variable is named p instead of d.
As the data changed, the old data seems to be kept somehow.
Either way, I simply deleted the list before applying the new data:
languages_dom
.selectAll('li')
.remove();
Can’t say this is graceful, nor performant, but it gets the job done :)

Referencing different sets of elements using d3 js

This has to be simple, but it's been a while since I was using d3.js and I can't figure out a good solution.
I have a single set of data and I'm using it to create two sets of elements
circles = svg.selectAll('.highcircles')
.data(data)
.enter()
.append('circle');
and
list.selectAll('.states-list')
.data(data)
.enter()
.append('p');
I'd like to be able to have on mouseover of the <p> tags, to have the related circle animate. I can't though think of the way to link the two. Is it through a data-state attribute? Is there a better solution?
selection.filter can be used to filter down a selection based on data. You can use the datum from the <p> event target to filter down a <circle> selection like this:
var circleMatch = svg.selectAll(".highcircles")
.filter(function(d) {
return d.key === targetDatum.key; // 'key' is some datum-unique property
});
You can add "id" attributes to your circles, and then reference those ids in your mouseover function. Something like this:
circles.attr("id", function(d) { return "id" + d; })
list.on('mouseover', function(d) {
d3.select("#id" + d)
.style("fill", "yellow")
})
http://jsfiddle.net/woodedlawn/7ZqZx/

D3.js - Retrieving DOM subset given data subset

I'm using d3.js to create a large number of svg:ellipse elements (~5000). After the initial rendering some of the data items can be updated via the backend (I'll know which ones) and I want to change the color of those ellipses (for example).
Is there a fast way to recover the DOM element or elements associated with a data item or items? Other than the obvious technique if recomputing a join over the full set of DOM elements with the subset of data?
var myData = [{ id: 'item1'}, { id: 'item2' }, ... { id: 'item5000' }];
var create = d3.selectAll('ellipse).data(myData, function(d) { return d.id; });
create.enter().append('ellipse').each(function(d) {
// initialize ellipse
});
// later on
// this works, but it seems like it would have to iterate over all 5000 elements
var subset = myData.slice(1200, 1210); // just an example
var updateElements = d3.selectAll('ellipse').data(subset, function(d) { return d.id; });
updateElements.each(function(d) {
// this was O(5000) to do the join, I _think_
// change color or otherwise update
});
I'm rendering updates multiple times per second (as fast as possible, really) and it seems like O(5000) to update a handful of elements is a lot.
I was thinking of something like this:
create.enter().append('ellipse').each(function(d) {
d.__dom = this;
// continue with initialization
});
// later on
// pull the dom nodes back out
var subset = myData.slice(1200, 1210).map(function(d) { return d.__dom; });
d3.selectAll(subset).each(function(d) {
// now it should be O(subset.length)
});
This works. But it seems like this would be a common pattern, so I'm wondering if there is a standard way to solve this problem? I actually want to use my data in multiple renderings, so I would need to be more clever so they don't trip over each other.
Basically, I know that d3 provides a map from DOM -> data via domElement.__data__. Is there a fast and easy way to compute the reverse map, other than caching the values myself manually?
I need to get from data -> DOM.
As long as you keep the d3 selection reference alive (create in your example), D3 is using a map to map the data keys to DOM nodes in the update so it's actually O(log n).
We can do some testing with the D3 update /data operator method vs a loop method over the subset:
var d3UpdateMethod = function() {
svg.selectAll("ellipse").data(subset, keyFunc)
.attr("style", "fill:green");
}
var loopMethod = function() {
for (var i=0; i < subset.length; i++) {
svg.selectAll(".di" + i)
.attr("style", "fill:green");
}
}
var timedTest = function(f) {
var sumTime=0;
for (var i=0; i < 10; i++) {
var startTime = Date.now();
f();
sumTime += (Date.now() - startTime);
}
return sumTime / 10;
};
var nextY = 100;
var log = function(text) {
svg.append("text")
.attr("x", width/2)
.attr("y", nextY+=100)
.attr("text-anchor", "middle")
.attr("style", "fill:red")
.text(text);
};
log("d3UpdateMethod time:" + timedTest(d3UpdateMethod));
log("loopMethod time:" + timedTest(loopMethod));
I also created a fiddle to demonstrate what I understand you're trying to do here.
Another method to make it easy to track the nodes that are in your subset is by adding a CSS class to the subset. For example:
var ellipse = svg.selectAll("ellipse").data(data, keyFunc).enter()
.append("ellipse")
.attr("class", function (d) {
var cl = "di" + d.i;
if (d.i % 10 == 0)
cl+= " subset"; //<< add css class for those nodes to be updated later
return cl;
})
...
Note how the "subset" class would be added only to those nodes that you know are in your subset to be updated later. You can then select them later for an update with the following:
svg.selectAll("ellipse.subset").attr("style", "fill:yellow");
I updated the fiddle to include this test too and it's nearly as fast as the directMethod.

Understanding how D3.js binds data to nodes

I'm reading through the D3.js documentation, and am finding it hard to understand the selection.data method from the documentation.
This is the example code given in the documentation:
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; });
I understand most of this, but what is going on with the .data(function(d) { return d; }) section of the var td statement?
My best guess is as follows:
The var tr statement has bound a four-element array to each tr node
The var td statement then uses that four-element array as its data, somehow
But how does .data(function(d) { return d; }) actually get that data, and what does it return?
When you write:
….data(someArray).enter().append('foo');
D3 creates a bunch of <foo> elements, one for each entry in the array. More importantly, it also associates the data for each entry in the array with that DOM element, as a __data__ property.
Try this:
var data = [ {msg:"Hello",cats:42}, {msg:"World",cats:17} ];
d3.select("body").selectAll("q").data(data).enter().append("q");
console.log( document.querySelector('q').__data__ );
What you will see (in the console) is the object {msg:"Hello",cats:42}, since that was associated with the first created q element.
If you later do:
d3.selectAll('q').data(function(d){
// stuff
});
the value of d turns out to be that __data__ property. (At this point it's up to you to ensure that you replace // stuff with code that returns a new array of values.)
Here's another example showing the data bound to the HTML element and the ability to re-bind subsets of data on lower elements:
The key to understanding what this code is doing is to recognize that selections are arrays of arrays of DOM elements. The outer-most array is called a 'selection', the inner array(s) are called 'groups' and those groups contain the DOM elements. You can test this by going into the console at d3js.org and making a selection like d3.selectAll('p'), you will see an array containing an array containing 'p' elements.
In your example, when you first call selectAll('tr') you get a selection with a single group that contains all the 'tr' elements. Then each element of matrix is matched to each 'tr' element.
But when you call selectAll('td') on that selection, the selection already contains a group of 'tr' elements. This time each of those elements will each become a group of 'td' elements. A group is just an array, but it also has a parentNode property that references the old selection, in this case the 'tr' elements.
Now when you call data(function(d) { return d; }) on this new selection of 'td' elements, d represents the data bound to each group's parent node. So in the example, the 'td's in the first group will be bound with the array [11975, 5871, 8916, 2868]. The second group of 'td's are bound with [ 1951, 10048, 2060, 6171].
You can read mike bostock's own excellent explanation of selections and data binding here: http://bost.ocks.org/mike/selection/
Use the counter i to show the index of the data being used.
var tr = d3.select("body").append("table").selectAll("tr")
.data(matrix)
.enter().append("tr") //create a row for each data entry, first index
.text(function(d, i) { return i}); // show the index i.e. d[0][] then d[1][] etc.
var td = tr.selectAll("td")
.data(function(d) { return d; })
.enter().append("td")
.style("background-color", "yellow") //show each cell
.text(function(d,i) { return i + " " + d; }); // i.e d[from the tr][0] then d[from the tr][1]...

Categories

Resources