Snippet 1
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>D3 Test</title>
<script type="text/javascript" src="http://d3js.org/d3.v3.js"></script>
</head>
<body>
<script type="text/javascript">
var data = [];
for (i = 0; i < 3; i += 1) {
data.push(i);
}
d3.select('body')
// .selectAll('p')
.data(data)
.enter()
.append('p')
.text(function(d) {
return d;
});
</script>
</body>
</html>
Snippet 2
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>D3 Test</title>
<script type="text/javascript" src="http://d3js.org/d3.v3.js"></script>
</head>
<body>
<script type="text/javascript">
var data = [];
for (i = 0; i < 3; i += 1) {
data.push(i);
}
d3.select('body')
.selectAll('p')
.data(data)
.enter()
.append('p')
.text(function(d) {
return d;
});
</script>
</body>
</html>
The code .selectAll('p') is commented in the first snippet. I could not figure why the first data is ignored due to that reason.
I am a newbie to d3.js and what I understood ,as I don't have a p tag in my HTML the .selectAll('p') returns an empty selection and p are appended as per my data count.
Thanks in advance.
To understand what happened, you have to understand what is an "enter" selection. The example at the bottom of this post briefly explains both the "enter" selection and why you have to selectAll("p") in your second snippet, even if you don't have any <p> in the document.
Now, let's see your snippets:
Your first snippet:
In your first snippet, data is [0, 1, 2]. So, you have 3 elements. When you select the body, there is clearly a body in the DOM. So, you associate the first datum (0) to this element (body). Now, you have 2 data not associated to any DOM element: 1 and 2. These two data are your "enter" selection. When you append the <p>, your enter selection has only those two numbers.
Your second snippet
In your second snippet, data is again [0, 1, 2]. The difference is that now you select all <p>... but there is none. This is the "placeholder" in the example I linked. As there is no <p> in the DOM to associate with the data, your enter selection has all 3 data: 0, 1 and 2.
As I wrote in the example:
If in your "enter" selection you select something that doesn't exist, your "enter" selection will always contain all your data.
The role of placeholders in "enter" selections
What is an enter selection?
In D3.js, when one binds data to DOM elements, three situations are possible:
The number of elements and the number of data points are the same;
There are more elements than data points;
There are more data points than elements;
In the situation #3, all the data points without a corresponding DOM element belong to the enter selection. Thus, In D3.js, enter selections are selections that, after joining elements to the data, contains all the data that don't match any DOM element. If we use an append function in an enter selection, D3 will create new elements binding that data for us.
This is a Venn diagram explaining the possible situations regarding number of data points/number of DOM elements:
As we can see, the enter selection is the blue area at the left: data points without corresponding DOM elements.
The structure of the enter selection
Typically, an enter selection has these 4 steps:
selectAll: Select elements in the DOM;
data: Counts and parses the data;
enter: Comparing the selection with the data, creates new elements;
append: Append the actual elements in the DOM;
This is a very basic example (look at the 4 steps in the var divs):
var data = [40, 80, 150, 160, 230, 260];
var body = d3.select("body");
var divs = body.selectAll("div")
.data(data)
.enter()
.append("div");
divs.style("width", function(d) { return d + "px"; })
.attr("class", "divchart")
.text(function(d) { return d; });
And this is the result (jsfiddle here):
Notice that, in this case, we used selectAll("div") as the first line in our "enter" selection variable. We have a dataset with 6 values, and D3 created 6 divs for us.
The role of placeholders
But suppose that we already have a div in our document, something like <div>This is my chart</div> at the top. In that case, when we write:
body.selectAll("div")
we are selecting that existent div. So, our enter selection will have only 5 datum without matching elements. For instance, in this jsfiddle, where there is already a div in the HTML ("This is my chart"), this will be the outcome:
We don't see the value "40" anymore: our first "bar" disappeared, and the reason for that is that our "enter" selection now has only 5 elements.
What we have to understand here is that in the first line of our enter selection variable, selectAll("div"), those divs are just placeholders. We don't have to select all the divs if we are appending divs, or all the circle if we are appending circle. We can select different things. And, if we don't plan to have an "update" or an "exit" selection, we can select anything:
var divs = body.selectAll(".foo")//this class doesn't exist, and never will!
.data(data)
.enter()
.append("div");
Doing this way, we are selecting all the ".foo". Here, "foo" is a class that not only doesn't exist, but also it's never created anywhere else in the code! But it doesn't matter, this is only a placeholder. The logic is this:
If in your "enter" selection you select something that doesn't exist, your "enter" selection will always contain all your data.
Now, selecting .foo, our "enter" selection have 6 elements, even if we already have a div in the document:
And here is the corresponding jsfiddle.
Selecting null
By far, the best way to guarantee that you are selecting nothing is selecting null. Not only that, but this alternative is way faster than any other.
Thus, for an enter selection, just do:
selection.selectAll(null)
.data(data)
.enter()
.append(element);
Here is a demo fiddle: https://jsfiddle.net/gerardofurtado/th6s160p/
Conclusion
When dealing with "enter" selections, take extra care to do not select something that already exists. You can use anything in your selectAll, even things that don't exist and will never exist (if you don't plan to have an "update" or an "exit" selection).
The code in the examples is based on this code by Mike Bostock: https://bl.ocks.org/mbostock/7322386
Related
I am parsing an xml string and display it as a tree in d3.js. I can build the whole tree in the 'enter' step, but now I want to make it interactive and pull out the configuration into an update step. I'm following https://stackoverflow.com/a/24912466 to implement the general update pattern but I can't seem to set the attribute to the g element outside the enter step:
const svg = d3.select("#canvas");
const xmlAsText = `
<root attrib1="foo" att2="foO">
<child1 attrib1="ba">zwei</child1>
<child2>eins</child2>
</root>`;
treeDataXml = (new DOMParser()).parseFromString(xmlAsText, "text/xml");
let hierarchy = d3.hierarchy(treeDataXml.children[0], d => d.children);
nodesData = d3.tree()(hierarchy);
var myGroups = svg.selectAll("g").data(nodesData.descendants());
myGroupsEnter = myGroups.enter().append("g")
// 1) works
//myGroupsEnter .attr("class", "x");
// 2) doesn't work
myGroups.select("g").attr("class", "x");
console.log(document.getElementById("canvas").childNodes[0].attributes[0])
alert(document.getElementById("canvas")
.childNodes[0].attributes.length)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg id="canvas"></svg>
with 1) I get
<body>
<svg id="canvas">
<g class="x"></g>
<g class="x"></g>
<g class="x"></g>
</svg>
</body>
but with 2) it's just:
<body>
<svg id="canvas">
<g></g>
<g></g>
<g></g>
</svg>
</body>
I would expect that with myGroups.select("g").attr("class", "x") the previously entered <g>s will all be selected and have their attribute class set to "x". Why doesn't this work? How can I fix this?
Selections are immutable, myGroups is an empty selection: it doesn't contain any DOM elements yet. As this selection is the update selection, this makes sense, there is nothing to update.
The selection myGroupsEnter contains the newly created elements. Entering elements does not modify myGroups, this remains an empty selection. This explains why myGroupsEnter.attr("class", "x"); works and myGroups.select("g").attr("class", "x"); does not.
D3 v3 and earlier added newly entered elements in the update selection, which is why some examples might be misleading, but as this behavior was not explicit it was removed
Often you want to combine the entered and updated elements into one selection, you can use:
let combined = myGroups.merge(myGroupsEnter);
This way regardless of whether you are entering all elements, updating all elements, or entering some and updating others you can modify all elements that exist (that are not exited).
I've seen some D3 codes with a pattern like this for appending elements:
var circles = svg.selectAll(null)
.data(data)
.enter()
.append("circle");
I really don't get this snippet. Why selecting null?
The way I understand D3, if one is appending circles, it should be:
var circles = svg.selectAll("circle")
.data(data)
.enter()
.append("circle");
The same way, if one is appending HTML paragraphs it should be:
var circles = svg.selectAll("p")
.data(data)
.enter()
.append("p");
The same goes for classes: if one is appending elements with a class foo, it should be selectAll(".foo").
However, selectAll(null) does work! The elements get appended.
So, what's the meaning of that null? What am I missing here?
Note: this is a self-answered question, trying to provide a "canonical" Q&A on a subject that has been touched on by many previous questions and not explained by the API. Most of the answer below is from an example I wrote in the extinct StackOverflow Documentation.
tl;dr
The objective of using selectAll(null) is to guarantee that the "enter" selection always corresponds to the elements in the data array, containing one element for every element in the data.
The "enter" selection
To answer your question, we have to briefly explain what is an "enter" selection in D3.js. As you probably know, one of the main features of D3 is the ability of binding data to DOM elements.
In D3.js, when one binds data to DOM elements, three situations are possible:
The number of elements and the number of data points are the same;
There are more elements than data points;
There are more data points than elements;
In the situation #3, all the data points without a corresponding DOM element belong to the "enter" selection.
Thus, In D3.js, "enter" selections are selections that, after joining elements to the data, contains all the data that don't match any DOM element. If we use an append function in an "enter" selection, D3 will create new elements, binding that data for us.
This is a Venn diagram explaining the possible situations regarding number of data points/number of DOM elements:
Binding data to already existing DOM elements
Let's break your proposed snippet for appending circles.
This...
var circles = svg.selectAll("circle")
.data(data)
... binds the data to a selection containing all circles. In D3 lingo, that's the "update" selection.
Then, this...
.enter()
.append("circle");
... represents the "enter" selection, creating a circle for each data point that doesn't match a selected element.
Sure, when there is no element (or a given class) in the selection, using that element (or that class) in the selectAll method will work as intended. So, in your snippet, if there is no <circle> element in the svg selection, selectAll("circle") can be used to append a circle for each data point in the data array.
Here is a simple example. There is no <p> in the <body>, and our "enter" selection will contain all the elements in the data array:
var body = d3.select("body");
var data = ["red", "blue", "green"];
var p = body.selectAll("p")
.data(data)
.enter()
.append("p")
.text(d=> "I am a " + d + " paragraph!")
.style("color", String)
<script src="https://d3js.org/d3.v4.min.js"></script>
But what happens if we already have a paragraph in that page? Let's have a look:
var body = d3.select("body");
var data = ["red", "blue", "green"];
var p = body.selectAll("p")
.data(data)
.enter()
.append("p")
.text(d=> "I am a " + d + " paragraph!")
.style("color", String)
<script src="https://d3js.org/d3.v4.min.js"></script>
<p>Look Ma, I'm a paragraph!</p>
The result is clear: the red paragraph disappeared! Where is it?
The first data element, "red", was bound to the already existing paragraph. Then, just two paragraphs were created (our "enter" selection), the blue one and the green one.
That happened because, when we used selectAll("p"), we selected, well, <p> elements! And there was already one <p> element in that page.
Selecting null
However, if we use selectAll(null), nothing will be selected! It doesn't matter that there is already a paragraph in that page, our "enter" selection will always have all the elements in the data array.
Let's see it working:
var body = d3.select("body");
var data = ["red", "blue", "green"];
var p = body.selectAll(null)
.data(data)
.enter()
.append("p")
.text(d=> "I am a " + d + " paragraph!")
.style("color", String)
<script src="https://d3js.org/d3.v4.min.js"></script>
<p>Look Ma, I'm a paragraph!</p>
And that's the purpose of selecting null: we guarantee that there is no match between the selected elements and the data array.
Selecting null and performance
Since we are not selecting anything, selectAll(null) is by far the fastest way to append new elements: we don't have to traverse the DOM searching for anything.
Here is a comparison, using jsPerf:
https://jsperf.com/selecting-null/1
In this very simple scenario, selectAll(null) was substantially faster. In a real page, full of DOM elements, the difference may be even bigger.
When NOT to use selectAll(null)
As we just explained, selectAll(null) won't match any existing DOM element. It's a nice pattern for a fast code that always append all the elements in the data array.
However, if you plan to update your elements, that is, if you plan to have an "update" (and an "exit") selection, do not use selectAll(null). In that case, select the element (or the class) you plan to update.
So, if you want to update circles according to a changing data array, you would do something like this:
//this is the "update" selection
var circles = svg.selectAll("circle")
.data(data);
//this is the "enter" selection
circles.enter()
.append("circle")
.attr("foo", ...
//this is the "exit" selection
circles.exit().remove();
//updating the elements
circles.attr("foo", ...
In that case, if you use selectAll(null), the circles will be constantly appended to the selection, piling up, and no circle will be removed or updated.
PS: Just as a historical curiosity, the creation of the selectAll(null) pattern can be traced back to these comments by Mike Bostock and others: https://github.com/d3/d3-selection/issues/79
I clone my mainSection like this (I have to clone it because, there are new elements added to #main over AJAX, and I don't want to search through them):
$mainSection = $('#main').clone(true);
then i search through the cloned main section for an element:
var searchTermHtml = 'test';
$foundElement = $mainSection.filter(":contains('"+searchTermHtml+"')");
When I find the string 'test' in the #mainSection I want to get the original element from it in the $mainSection so I can scroll to it via:
var stop = $foundElementOriginal.offset().top;
window.scrollTo(0, stop);
The question is: how do I get the $foundElementOriginal?
Since you're changing the content of #main after cloning it, using structural things (where child elements are within their parents and such) won't be reliable.
You'll need to put markers of some kind on the elements in #main before cloning it, so you can use those markers later to relate the cloned elements you've found back to the original elements in #main. You could mark all elements by adding a data-* attribute to them, but with greater knowledge of the actual problem domain, I expect you can avoid being quite that profligate.
Here's a complete example: Live Copy
<!DOCTYPE html>
<html>
<head>
<script src="http://code.jquery.com/jquery-1.11.0.min.js"></script>
<meta charset="utf-8">
<title>Example</title>
</head>
<body>
<div id="main">
<p>This is the main section</p>
<p>It has three paragraphs in it</p>
<p>We'll find the one with the number word in the previous paragraph after cloning and highlight that paragraph.</p>
</div>
<script>
(function() {
"use strict";
// Mark all elements within `#main` -- again, this may be
// overkill, better knowledge of the problem domain should
// let you narrow this down
$("#main *").each(function(index) {
this.setAttribute("data-original-position", String(index));
});
// Clone it -- be sure not to append this to the DOM
// anywhere, since it still has the `id` on it (and
// `id` values have to be unique within the DOM)
var $mainSection = $("#main").clone(true);
// Now add something to the real main
$("#main").prepend("<p>I'm a new first paragraph, I also have the word 'three' but I won't be found</p>");
// Find the paragraph with "three" in it, get its original
// position
var originalPos = $mainSection.find("*:contains(three)").attr("data-original-position");
// Now highlight it in the real #main
$("#main *[data-original-position=" + originalPos + "]").css("background-color", "yellow");
})();
</script>
</body>
</html>
I want to be able to click on a specific element, and have it send a value to a textarea. However, I want it to append to a specific row/line of the textarea.
What I am trying to build is very similar to what happens when you click the notes of the fret board on this site: http://www.guitartabcreator.com/version2/ In fact, i want it almost exactly the same as this.
But right now I am really just trying to see how I can target the specific row, as it seems doable based on this website.
Currently I am using javascript to send a value based on clicking a specific element.
Here is the js:
<script type="text/javascript">
function addNote0(text,element_id) {
document.getElementById(element_id).value += text;
}
</script>
This is the HTML that represents the clickable element:
<td> x </td>
This is the textarea:
<textarea rows="6" cols="24" id="tabText" name="text">-
-
-
-
-
-</textarea>
This works fine for sending the value. But it obviously just goes to the next available space. I am a total newb when it comes to javascript, so I am just not sure where to begin with trying to target a specific line.
What I have currently can be viewed here: http://aldentec.com/tab/
Working code:
After some help, here is the final code that made this work:
<script>
function addNote0(text,element_id) {
document.getElementById(element_id).value += text;
var tabTextRows = ['','','','','',''];
$('td').click(function(){
var fret = $(this).index() - 1;
var line = $(this).parent().index() -1;
updateNote(fret, line);
});
function updateNote(fret, line){
var i;
for(i=0;i<tabTextRows.length;i++){
if(i == line) tabTextRows[i]+='-'+fret+'-';
else tabTextRows[i]+='---';
$('#tabText').val(tabTextRows.join('\n'));
}
}}
window.onload = function() {
addNote0('', 'tabText');
};
</script>
Tried to solve this only in JS.
What I did here is use an array to model each row of the textfield (note the array length is 6).
Then I used a jQuery selector to trigger any time a <td> element is clicked which calculates the fret and string that was clicked relative to the HTML tree then calls the updateNote function. (If you change the table, the solution will probably break).
In the update note function, I iterate through the tabTextRows array, adding the appropriate note. Finally, I set the value of the <textarea> to the array joined by '\n' (newline char).
Works for me on the site you linked.
This solution is dependant on jQuery however, so make sure that's included.
Also you should consider using a monospaced font so the spacing doesn't get messed up.
var tabTextRows = ['','','','','',''];
$('td').click(function(){
var fret = $(this).index() - 1;
var line = $(this).parent().index() -1;
updateNote(fret, line);
});
function updateNote(fret, line){
var i;
for(i=0;i<tabTextRows.length;i++){
if(i == line) tabTextRows[i]+='-'+fret+'-';
else tabTextRows[i]+='---';
$('#tabText').val(tabTextRows.join('\n'));
}
}
I wrote the guitartabcreator website. Jacob Mattison is correct - I am using the text area for display purposes. Managing the data occurs in the backend. After seeing your site, it looks like you've got the basics of my idea down.
This is a simple page that demonstrates some basic functionality of d3. I made a dataset var dataset = [3,1,4,1,5]; and would like to output it as well as some paragraphs. The data is appearing, but after the body! Strange ...
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title> demo project</title>
<script type="text/javascript" src="d3/d3.v2.js"></script>
</head>
<body>
<script type="text/javascript">
d3.select("body").append("p").text("hello!");
d3.select("p").append("text").text(" hello!!");
d3.select("body").append("p").text("hello2!");
d3.select("p:nth-child(3)").append("text").text(" hello2!!");
var dataset = [3,1,4,1,5];
d3.select("p:nth-child(3n+1)")
.data(dataset)
.enter()
.append("p")
.text(function(d) { return d; });
d3.select("p:nth-child(7n + 1)").append("text").text("hello againss?");
</script>
</body>
</html>
the page looks like this:
and the DOM looks like this (note the data shows up after the body close tag):
Also note that the line d3.select("p:nth-child(7n + 1)").append("text").text("hello againss?"); was intended to be printed after all my data, but it does not show up.
The short answer is that in your particular case the enter() selection's parentNode is the document (and not the body).
Let's take a simple example to see what an enter() selection looks like. Assuming we have a document with a body without any p elements.
var ps = d3.select("body").selectAll("p")
.data([0, 1, 2]);
Since no p elements existed yet, the enter() selection will have three elements. Let's inspect the enter selection:
You see that the inner array has a property named parentNode. When you add new elements using selection.append() or selection.insert() the new elements will be created as children of that parentNode.
So, inspecting ps.enter()[0].parentNode will reveal the body element. It now becomes clear that, in a data join, the selection before the selectAll specifies the parentNode; in the above case that was d3.select("body").
What if we had omitted the select("body") part in the data join?
// example of bad data join
var ps2 = d3.selectAll("p")
.data([0, 1, 2]);
It turns out that in this case ps2.enter()[0].parentNode is the #document! That means that if you add elements using this enter() selection, they will become the document's direct children. The append method will add them to the end of the document; i.e. after the body.
The last case is basically what you've encountered. Your data join and enter expression is not correct; it should follow this pattern:
d3.select(parent).selectAll(element)
.data(data)
.enter().append(element);
BTW, there is no HTML text element. So, append("text") doesn't seem meaningful.