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

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.

Related

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;
});

Assign Class to All Elements of Same Bound Data

I have a scatter plot and a table. Each circle in the scatter plot has a corresponding row in the table. When I apply classes to the circles for CSS purposes, I also want to have that same class be assigned to the corresponding table row. They have the same data value, but are appended to separate elements.
Here is my circle class event:
my_circles.each(function(d,i) {
if (my_bool===true) {
d3.select(this).classed('selected',true);
//d3.selectAll('tr').filter(d===???)
}
});
I was trying to use a filter to select only the table rows of matching d value, but it didn't quite work out, I didn't know how to finish the line. Which got me thinking, maybe there is a better way, like the post title, assign classes to all elements bound to the same data.
If you have another solution aside from any of my ideas, that would be fine too.
Probably the easiest solution will be to check in the .classed() method for the tr selection, if the data bound to that tr matches the one for the selected circle.
my_circles.each(function(d,i) {
if (my_bool===true) {
d3.select(this).classed("selected",true);
d3.selectAll('tr')
.classed("selected", trData => d === trData); // Set class if data matches
}
});
This, however, is a bit clumsy and may be time-consuming because it will iterate over all trs each time this code is called. In case this is in an outer loop for handling multiple selected circles—as mentioned in your comment—things will get even worse.
D3 v4
For a slim approach I would prefer using D3's local variables, which are new to v4, to store the references between circles and table rows. This will require just a one-time setup which will depend on the rest of your code, but might go somewhat along the following lines:
// One-time setup
var tableRows = d3.local();
my_circles.each(function(d) {
var row = d3.selectAll("tr").filter(trData => d === trData);
tableRows.set(this, row); // Store row reference for this circle
});
This creates a new local variable tableRows which is used to store the reference to the corresponding table row for each circle. Later on you are then able to retrieve the reference to the row without the need for further iterations.
my_circles.each(function(d,i) {
if (my_bool===true) {
d3.select(this).classed('selected',true);
tableRows.get(this).classed("selected", true); // Use local variable to get row
}
});
D3 v3
If you are not yet using D3 there are, of course, other ways to achieve the same thing. Personally, I would prefer using a WeakMap to store the references. Because the API of the WeakMap also features get and set methods similar to d3.local, all you need to do is to change the line creating the local reference store while keeping the rest of the above code as is:
// var tableRows = d3.local();
var tableRows = new WeakMap(); // use a WeakMap to hold the references
You can use dataIndex for this purpose. Here is a code snippet for the same.
var data = ["A", "B", "C"];
var color = d3.scale.category10();
var container = d3.select("body")
.append("svg")
.attr("height", 500)
.attr("width", 500);
var my_circles = container.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("name", function(d, i) {
return "circle" + i
})
.attr("r", 10)
.attr("cx", function(d, i) {
return (i + 1) * 50
})
.attr("cy", function(d, i) {
return (i + 1) * 50
})
.style("fill", function(d, i) {
return color(i)
});
container.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("name", function(d, i) {
return "rect" + i
})
.attr("width", 15)
.attr("height", 15)
.attr("x", function(d, i) {
return i * 50 + 200
})
.attr("y", function(d, i) {
return (i + 1) * 50
})
.style("fill", function(d, i) {
return color(i)
});
my_circles.each(function(d, i) {
d3.select(this).classed("selected" + i, true);
container.selectAll("[name=rect" + i + "]").classed("selected" + i, true);
});
svg {
border: 1px solid black;
background: black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

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.

Who defines the arguments in this javascript snippet?

I am just starting out with javascript and have a problem understanding this piece of (svg)code below which uses javascript for defining it's x co-ordinate and radius. I understand how data is bound etc. But my question is - For the function which takes two arguments : d and i, where is it defined that the first argument to the function is the dataset and the second is a counter for the circle, ie 0 for the first circle, 1 for the second and so on.
var dataset = [ 5, 10, 15, 20, 25 ];
var circles = svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle");
circles.attr("cx", function(d, i) {
return (i * 50) + 25;
})
.attr("cy", h/2)
.attr("r", function(d) {
return d;
});
Thanks.
This is d3 and so the d3 documentation defines what the function expects
If value is a constant, then all elements are given the same attribute value; otherwise, if value is a function, then the function is evaluated for each selected element (in order), being passed the current datum d and the current index i, with the this context as the current DOM element.

outer selectAll: what is selected?

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.

Categories

Resources