outer selectAll: what is selected? - javascript

I am learning D3, and how to nest or append elements to the page using D3's data binding mechanism.
I have modified code found on http://www.recursion.org/d3-for-mere-mortals/ . I understand how to set up the svg canvas and I also understand the loops binding data to the rect, text and line elements.
What I don't understand are the calls to selectAll('Anything1/2/3/4') below. They are clearly necessary, but what exactly am I selecting, and how do they fit in the data binding mechanism? Thank you.
<html>
<head>
<title>D3 Test</title>
<script type="text/javascript" src="d3/d3.v2.js"></script>
</head>
<body>
<script type="text/javascript">
var dat = [ { title:"A", subtitle:"a", year: 2006, books: 54, avg:10 },
{ title:"B", subtitle:"b", year: 2007, books: 43, avg:10 },
{ title:"C", subtitle:"c", year: 2008, books: 41, avg:10 },
{ title:"D", subtitle:"d", year: 2009, books: 44, avg:10 },
{ title:"E", subtitle:"e", year: 2010, books: 35, avg:10 } ];
var width = 560,
height = 500,
margin = 20,
innerBarWidth = 20,
outerBarWidth = 40;
var x = d3.scale.linear().domain([0, dat.length]).range([0, width]);
var y = d3.scale.linear()
.range([0, height - 2 * margin])
.domain([ 0 , 100 ]);
var z = d3.scale.category10();
var n = d3.format(",d"),
p = d3.format("%");
var canvas = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + 2 * margin + "," + margin + ")");
// outerbars
var outerBars = d3.select("svg")
.selectAll("Anything1").data(dat).enter().append("rect")
.attr("x", function(datum, index) { return x(index); })
.attr("y", function(datum) { return height - y(datum.books); })
.attr("height", function(datum) { return y(datum.books); })
.attr("width", outerBarWidth)
.attr("fill", "blue")
// innerbars
var innterBars = d3.select("svg")
.selectAll("Anything2").data(dat).enter().append("rect")
.attr("x", function(datum, index) { return x(index)+innerBarWidth/2; })
.attr("y", function(datum) { return height - y(datum.books)/2; })
.attr("height", function(datum) { return y(datum.books); })
.attr("width", innerBarWidth)
.attr("fill", "red");
// avg references
var barlabels = d3.select("svg")
.selectAll("Anything3").data(dat).enter().append("line")
.attr("x1", function(datum, index) { return x(index); })
.attr("x2", function(datum, index) { return x(index)+outerBarWidth; })
.attr("y1", function(datum) { return height - y(datum.books)/2; })
.attr("y2", function(datum) { return height - y(datum.books)/2; })
.style("stroke", "#ccc");
// titles
var barlabels = d3.select("svg")
.selectAll("Anything4").data(dat).enter().append("text")
.attr("x", function(datum, index) { return x(index)+innerBarWidth/2; })
.attr("y", height )
.attr("text-anchor", "end")
.text(function (d) {return d.title} );
</script>
</body>
</html>

Perhaps the most important, yet most difficult concept to understand in d3 is the selection (I highly recommend you bookmark and familiarize yourself with the API). On the surface, selections provide similar functionality to many other JavaScript libraries, such as jQuery:
jQuery:
var paragraphs = $("p");
d3:
var paragraphs = d3.selectAll("p");
Both these lines create "selection objects", which are essentially DOM elements which have been grouped into a single object which gives you better control over the elements. Like other libraries, you can manipulate these "selected" elements in d3 using functions that are provided in the library.
jQuery:
var paragraphs = $("p").css("color", "red");
d3:
var paragraphs = d3.selectAll("p").style("color", "red");
Again, on the surface this is fairly easy to understand. What makes d3 so powerful is that it lets you take this a step further by allowing you to bind arbitrary data to the selected elements.
Let's say you have a blank document and you want to add a couple paragraphs of text - and you have each paragraph of text stored in individual elements in an array:
var text = ["First", "Second", "Third", "Fourth"];
Since we haven't yet created these paragraphs, the following call will return an empty selection:
var paragraphs = d3.selectAll("p");
console.log(paragraphs.empty()); // true
Note that paragraphs is still a selection, it is just empty. This is a fundamental point in d3. You can bind data to an empty selection, and then use the data to add new elements using the entering selection. Let's start over from our previous example and walk through this process. First, create your empty selection and bind the text array to it:
var paragraphs = d3.select("body").selectAll("p").data(text);
Then, using the entering selection, append the <p> elements to the body:
paragraphs.enter().append("p").text(function(d) { return d; });
Your DOM will now have:
<body>
<p>First</p>
<p>Second</p>
<p>Third</p>
<p>Fourth</p>
</body>
There's a lot that could definitely confuse you at this point, but I think this should give you a good start.
See also: Thinking with Joins.

Here are some readings to get you started:
Understanding selectAll, data, enter, append sequence
Binding Data: Scott Murray D3 Tutorials
From the second link its explained:
The answer lies with enter(), a truly magical method. Here’s our final code for this example, which I’ll explain:
d3.select("body").selectAll("p")
.data(dataset)
.enter()
.append("p")
.text("New paragraph!");
.selectAll("p") — Selects all paragraphs in the DOM. Since none exist yet, this returns an empty selection. Think of this empty selection as representing the paragraphs that will soon exist.
Basically, you are selecting DOM elements that do not exist yet and then appending data to these non-existent elements and then appending them after the data is bound.

Related

Appended bars wont draw on svg element

I am trying to write a transitioning bar graph that uses two CVS files. I know that both of the files are loading properly because it shows in the console that the first one loads with the page and the second one loads when you click the update button.
The only thing that I have really thought of trying was changing the svg select to group instead of selecting all rectangles incase there was something screwed up there.
This block is creating the svg element, bringing in the first CSV file, and appending the rectangles onto the chart. My only thought for what the problem could be is that it is inside a function, but if I take it out of the function how do I bind the data to them?
//Creating SVG Element
var chart_w = 1000,
chart_h = 500,
chart_pad_x = 40,
chart_pad_y = 20;
var svg = d3.select('#chart')
.append('svg')
.attr('width', chart_w)
.attr('height', chart_h);
//Defining Scales
var x_scale = d3.scaleBand().range([chart_pad_x, chart_w -
chart_pad_x]).padding(0.2);
var y_scale = d3.scaleLinear().range([chart_pad_y, chart_h -
chart_pad_y]);
//Data-------------------------------------------------------------------
d3.csv('data.csv').then(function(data){
console.log(data);
generate(data); });
function generate(data){
//Scale domains
x_scale.domain(d3.extent(data, function(d){ return d }));
y_scale.domain([0, d3.max(data, function(d){ return d })]);
//Create Bars
svg.select('rect')
.data(data)
.enter()
.append('rect')
.attr('x', function(d, i){
return x_scale(i);
})
.attr('y', function(d){
return y_scale(d);
})
.attr('width', x_scale.bandwidth())
.attr('height', function(d){
return y_scale(d);
})
.attr('transform',
"translate(0,0)")
.attr('fill', '#03658C')
'''
The results I have experienced is a blank window with just the update button. As previously stated I know that the data is being generated because I can see it in the console.
Try using the following:
svg.selectAll('rect')
.data(data)
If you use svg.select this will only make the data binding with the first element found.
d3.select(selector): Selects the first element that matches the specified selector string. If no elements match the selector, returns an empty selection. If multiple elements match the selector, only the first matching element (in document order) will be selected. For example, to select the first anchor element:
This should be clear if you inspect the DOM nodes.
To fix the issue lets change some things in your code:
Lets create a dummy fetch function:
(function simulateCSVFetch() {
const data = [1,2,3,4,5];
generate(data);
})();
You are also using a scaleBand with an incomplete domain by using the extent function:
d3.extent(): Returns the minimum and maximum value in the given iterable using natural order. If the iterable contains no comparable values, returns [undefined, undefined]. An optional accessor function may be specified, which is equivalent to calling Array.from before computing the extent.
x_scale.domain(d3.extent(data, function(d) { // cant use extent since we are using a scaleBand, we need to pass the whole domain
return d;
}));
console.log(x_scale.domain()) // [min, max]
The scaleBand needs the whole domain to be mapped
Band scales are typically used for bar charts with an ordinal or categorical dimension. The unknown value of a band scale is effectively undefined: they do not allow implicit domain construction.
If we continue using that scale we will be only to get two values for our x scale. Lets fix that with the correct domain:
x_scale.domain(data);
Lastly use the selectAll to create the data bind:
svg.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('x', function(d, i) {
return x_scale(d);
})
.attr('y', function(d) {
return chart_h - y_scale(d); // fix y positioning
})
.attr('width', x_scale.bandwidth())
.attr('height', function(d) {
return y_scale(d);
})
.attr('fill', '#03658C');
This should do the trick.
Complete code:
var chart_w = 1000,
chart_h = 500,
chart_pad_x = 40,
chart_pad_y = 20;
var svg = d3
.select('#chart')
.append('svg')
.style('background', '#f9f9f9')
.style('border', '1px solid #cacaca')
.attr('width', chart_w)
.attr('height', chart_h);
//Defining Scales
var x_scale = d3.scaleBand()
.range([chart_pad_x, chart_w - chart_pad_x])
.padding(0.2);
var y_scale = d3.scaleLinear()
.range([chart_pad_y, chart_h - chart_pad_y]);
//Data-------------------------------------------------------------------
(function simulateCSVFetch() {
const data = [1,2,3,4,5];
generate(data);
})();
function generate(data) {
console.log(d3.extent(data, function(d) { return d }));
//Scale domains
x_scale.domain(d3.extent(data, function(d) { // cant use extent since we are using a scaleBand, we need to pass the whole domain
return d;
}));
// Band scales are typically used for bar charts with an ordinal or categorical dimension. The unknown value of a band scale is effectively undefined: they do not allow implicit domain construction.
x_scale.domain(data);
y_scale.domain([0, d3.max(data, function(d) {
return d
})]);
//Create Bars
svg.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('x', function(d, i) {
return x_scale(d);
})
.attr('y', function(d) {
return chart_h - y_scale(d); // fix y positioning
})
.attr('width', x_scale.bandwidth())
.attr('height', function(d) {
return y_scale(d);
})
.attr('fill', '#03658C');
}
Working jsfiddle here

Why does separating a d3 method chain on enter alter outcome?

I am learning D3.js and curious on the chaining of methods
This script works:
var data = [32, 57, 112, 250]
var svg = d3.select("svg")
svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cy", 60)
.attr("cx", function(d, i) { return i * 100 + 30 })
.attr("r", function(d) { return Math.sqrt(d); })
But this script results in nothing:
var data = [32, 57, 112, 250]
var circles = d3.select("svg").selectAll("circle");
circles.data(data);
var circlesEnter = circles
.enter()
.append("circle")
.attr("cy", 60)
.attr("cx", function(d, i) { return i * 100 + 30})
.attr("r", function (d) { return Math.sqrt(d)})
I don't see the different effects on these two different approaches. Can anyone tell me the difference between these?
Thanks in advance!
The issue is that selection.data() doesn't modify an existing selection, it returns a new selection:
[selection.data] Binds the specified array of data with the selected elements,
returning a new selection that represents the update selection: the
elements successfully bound to data. Also defines the enter and exit
selections on the returned selection, which can be used to add or
remove elements to correspond to the new data. (from the docs)
Also,
Selections are immutable. All selection methods that affect which
elements are selected (or their order) return a new selection rather
than modifying the current selection. However, note that elements are
necessarily mutable, as selections drive transformations of the
document! (link)
As is, circles contains an empty selection of circles (size: 0) with no associated data array. Because it is immutable, calling circles.data(data) won't change that selection, and circles.enter() will remain empty. Meanwhile the selection created by circles.data() is lost as it isn't assigned to a variable.
We can chain methods together as in the first code block of yours because the returned selection in the chain is a new selection when using .data(), .enter(), or selectAll(). Each method in the method chain uses the selection returned by the previous line, which is the correct one.
In order to break .data() from the chain, we would need to create a new intermediate selection with selection.data() to access the enter selection:
var circles = d3.select("svg").selectAll("circle");
var circlesData = circles.data(data);
var circlesEnter = circlesData
.enter()
...
var data = [32, 57, 112, 250]
var circles = d3.select("svg").selectAll("circle");
var circlesData = circles.data(data);
var circlesEnter = circlesData
.enter()
.append("circle")
.attr("cy", 60)
.attr("cx", function(d, i) { return i * 100 + 30})
.attr("r", function (d) { return Math.sqrt(d)})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
But this would be a bit of an odd approach.

apply a transition on each object in a D3 selection

I'm having troubles in understanding how to get each D3 object in a selection to apply a transition.
Consider the follwoing code (here on jsfiddle):
var svg = d3.select('svg');
var dataSet = [10, 20, 30, 40];
var circle = svg.selectAll('circle')
.data(dataSet)
.enter()
.append('circle')
.attr("r",function(d){ return d })
.attr("cx", function(d, i){ return i * 100 + Math.random()*50 })
.attr("cy",50)
.attr("fill",'red')
;
circle.each(function(d,i) {
this
.transition()
.duration(1000)
.attr("cx",this.cx+100);
})
My use of this is wrong. I've also tried with d3.select(this) but I get the dom object corresponding to D3 object.
I'm unable to get the D3 object to apply transition.
The missing part is that you can supply a function to .attr('cx', function (d,i) { ... }) when using a transition, and inside that function you can access the cx attribute using this.getAttribute('cx').
Of course, you also want to make sure to turn it into a number using parseInt(), otherwise it will do string concatenation (because JS, sigh).
So change your final line to:
circle.transition().duration(1000).attr('cx', function(d, i) {
return parseInt(this.getAttribute('cx')) + 100;
});

Understanding D3 with an example - Mouseover, Mouseup with multiple arguments

I am reading the code from http://bl.ocks.org/diethardsteiner/3287802
But I dont understand why and how the mouse-up piece of code works:
var arcs = vis.selectAll("g.slice")
.data(pie)
.enter()
.append("svg:g")
.attr("class", "slice")
.on("mouseover", mouseover)
.on("mouseout", mouseout)
.on("click", up)
;
...
function up(d, i) {
updateBarChart(d.data.category, color(i));
updateLineChart(d.data.category, color(i));
}
I can see that "up" is a mouse-event handler, but what are the "d" and "i" here?
I mean, how does it know what variable it need to pass on as the function Argument when we are calling "on("click", up)? It seems that "d" and "i" are refering to the data associated with "g.slice" and its index, but istn't a mouse-up Event handle supposed to take an Event object as Default Argument?
Moreover, regarding the "d.data.category", I dont see any data of such structure in the code, although there is a dataset variable declared. But how come that "d.data" would refer to the data of a Person in the dataset?
Thank you guys!!!
For someone that has knowledge of JavaScript but is not familiar with D3, this seems strange indeed, but these arguments (or parameters) are already expected by D3:
When a specified event is dispatched on a selected node, the specified listener will be evaluated for each selected element, being passed the current datum (d), the current index (i), and the current group (nodes), with this as the current DOM element.
These are the famous 3 arguments when you use a function in a D3 selection:
the function is evaluated for each selected element, in order, being passed the current datum (d), the current index (i), and the current group (nodes), with this as the current DOM element.
So, when you do something like this in a D3 selection:
function(d,i,n){
You have the 3 arguments:
d, named like this for "datum", is the datum of the element.
i, for "index", is the index of the element;
n is the group of the element.
Of course, you can name them anything you want ("foo", "bar", "a", "r2d2" etc...), the important here is just the order of the arguments.
Here is a demo to show you this, click the circles:
var width = 400,
height = 150;
var data = [3,19,6,12,23];
var scale = d3.scaleLinear()
.range([10, width - 10])
.domain([0,30]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var circles = svg.selectAll("circle").data(data)
.enter()
.append("circle")
.attr("r", 8)
.attr("fill", "teal")
.attr("cy", 50)
.attr("cx", function(d) {
return scale(d)
})
.on("click", up);
var axis = d3.axisBottom(scale);
var gX = svg.append("g")
.attr("transform", "translate(0,100)")
.call(axis);
function up(d,i){
alert("datum is " + d + "; index is " + i);
}
<script src="https://d3js.org/d3.v4.min.js"></script>
Regarding the d.data.category, it's all well commented in the code: dataset has both "category" and "measure", and it's bound to the SVG. When you use d3.layout.pie() on dataset, it returns an array of objects like this:
{"data": 42, "value": 42, "startAngle": 42, "endAngle": 42, "padAngle": 42}
That's where the d.data comes from.

d3.js force-directed issues, significance of "d"?

I'm having a bit of trouble getting a something to work with D3.js. Namely, I'm trying to make a tree of nodes, using the basic code from http://bl.ocks.org/mbostock/1021953.
I switched it to load the data inline, as opposed to loading from file, because I'm using it with a Rails application and don't want to have repetitive information. I switched the line so that you could see the format of my data.
Anyways, here's the bulk of my code:
<%= javascript_tag do %>
var nodes = [{"title":"Duncan's Death","id":"265"},{"title":"Nature Thrown Off","id":"266"},{"title":"Cows Dead","id":"267"},{"title":"Weather Bad","id":"268"},{"title":"Lighting kills man","id":"269"},{"title":"Macbeth's Rise","id":"270"}];
var links = [{"source":"265","target":"266","weight":"1"},{"source":"266","target":"267","weight":"1"},{"source":"266","target":"268","weight":"1"},{"source":"268","target":"269","weight":"1"}];
var firstelement = +links[0].source;
links.forEach(function(l) {
l.source = +l.source;
l.source = l.source-firstelement;
l.target = +l.target
l.target = l.target-firstelement;
});
var width = 960,
height = 500;
var color = d3.scale.category20();
var force = d3.layout.force()
.charge(-1000)
.linkDistance(300)
.size([width, height]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
force
.nodes(nodes)
.links(links)
.start();
var link = svg.selectAll(".link")
.data(links)
.enter().append("line")
.attr("class", "link")
.style("stroke-width", function(d) { return Math.sqrt(d.weight); });
var node = svg.selectAll(".node")
.data(nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("circle")
.attr("class", "circle_node")
.attr("r", 50)
.style("fill", function(d) { return color(d.id); })
node.append("title")
.text(function(d) { return d.title; });
node.append("text")
.attr("x", function(d) { return d.x; } )
.attr("y", function(d) { return d.y; })
.text(function(d) { return d.title; });
force.on("tick", function() {
link.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
node.attr("x", function(a) { return a.x; })
.attr("y", function(a) { return a.y; });
});
<% end %>
This seems like it should work to me, but I can seem to manage it. The links work, but the nodes all remain in the top left corner. I've tried just entering the circles directly and appending the text to them (staying close to the source code I listed above,) but while the circles behave properly, it doesn't display the text. I'd like the title to be centered in the nodes.
More generally, I'm kind of confused by how this is working. What does "d" refer to within lines like
function(d) { return d.source.x; }
It seems to be declaring a function and calling it simultaneously. I know that it doesn't have to be specifically the character "d," (for instance, switching the "d" to an "a" seems to make no difference as long as it's done both in the declaration and within the function.) But what is it referring to? The data entered into the object that's being modified? For instance, if I wanted to print that out, (outside of the attribute,) how would I do it?
Sorry, I'm new to D3 (and fairly new to JavaScript in general,) so I have a feeling the answer is obvious, but I've been looking it up and through tutorials and I'm still lost. Thanks in advance.
First, there's a simple problem with your code that is causing all your nodes to stay in the top left corner. You are trying to position each node using the code:
node.attr("x", function(a) { return a.x; })
.attr("y", function(a) { return a.y; });
However, node is a selection of gs which do not take x and y attributes. Instead, you can move each node using translate transform, e.g.
node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
Making this change should allow the nodes to move around.
Next, moving to your question about "d", I think the first thing you need to understand is what you can do with a selection of elements in D3. From the docs: a selection (such as nodes) "is an array of elements pulled from the current document." Once you have a selection of elements, you can apply operators to change the attributes or style of the elements. You can also bind data to each element.
In your case, you are binding data to a selection of gs (nodes):
var node = svg.selectAll(".node")
.data(nodes)
.enter().append("g")
You are then using attr to change the position of each node. However, instead of setting the x and y attributes of each element to the same value, you are passing attr an anonymous function that will return a (presumably different) position for each node:
node.attr("x", function(a) { return a.x; })
.attr("y", function(a) { return a.y; });
This behavior is also explained in the docs for attr:
Attribute values and such are specified as either constants or
functions; the latter are evaluated for each element.
Thus, d represents an individual element (Object) in nodes.
So going back to your code, on each tick two things are happening:
The position of each node (data) is being recalculated by force.
Each corresponding element is then being moved to its new location by the anonymous function you pass to force.on.

Categories

Resources