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

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.

Related

d3.js:refresh the chart with new dataset

I have created d3 PCP chart and Here is my code:
var m = [60, 10, 10, 60],
w = 1000 - m[1] - m[3],
h = 270 - m[0] - m[2];
var x=d3.scaleOrdinal()
.range([0, w]),
y = {},
dragging = {};
var line = d3.line(),
background,
foreground;
var svg = d3.select("#test").append("svg:svg")
.attr("width", w + m[1] + m[3])
.attr("height", h + m[0] + m[2])
.append("svg:g")
.attr("transform", "translate(" + m[3] + "," + m[0] + ")");
//let dimensions
// Extract the list of dimensions and create a scale for each.
x.domain(this.dimensions = d3.keys(data[0]).filter(function(d) {
if (d === "name") return false;
if (d === "Plant" || d === "Chemical" || d === "Pathway" || d === "Gene" || d === "Disease") {
y[d] = d3.scaleOrdinal()
.domain(data.map(function(p) {
return p[d];
}))
.range([h, 0]);
} else {
y[d] = d3.scaleLinear().domain([0, d3.max(data.map(function(p) { return p[d]; }))]).range([h, 0]);
}
return true;
}));
//alert(this.dimensions)
// Add grey background lines for context.
background = svg.append("svg:g")
.attr("class", "background")
.selectAll("path")
.data(data)
.enter().append("svg:path")
.style('fill', 'none')
.attr("d", path.bind(this));
// Add blue foreground lines for focus.
foreground = svg.append("svg:g")
.attr("class", "foreground")
.selectAll("path")
.data(data)
.enter().append("svg:path")
.style('fill', 'none')
.style('stroke', 'steelblue')
.attr("d", path.bind(this));
// Add a group element for each dimension.
let g = svg.selectAll(".dimension")
.data(this.dimensions)
.enter().append("svg:g")
.attr("class", "dimension")
.attr("transform", function(d) {
return "translate(" + x(d) + ")";
})
// Add an axis and title.
g.append("svg:g")
.attr("class", "axis")
.each(function(d) {
d3.select(this).call(d3.axisLeft(y[d]));
})
.append("svg:text")
.attr("text-anchor", "middle")
.attr("y", -50)
.attr("x",-10)
.style("fill", "black")
.text(function(d) {return d });
var firstAxis = d3.selectAll('g.dimension g.axis');
firstAxis
.append("svg:image")
.attr('x',-20)
.attr('y',-60)
.attr('width', 40)
.attr('height', 60)
.attr("xlink:href", function(s) {
return "images/" + s+'.png';
});
function position(d) {
var v = dragging[d];
return v == null ? x(d) : v;
}
function transition(g) {
return g.transition().duration(500);
}
// Returns the path for a given data point.
function path(d) {
return line(this.dimensions.map(function(p) {
return [position(p), y[p](d[p])];
}));
}
I have a simple input from on my webpage which displaying some input drop down and based on selected value we are passing each time our new data set to script.Every time a user changes a value on the form a new dataset is sent to my script. I'm struggling to figure out how to update (refresh) the chart with new dataset I have tried to using exit and remove function but not got much success.
svg.exit().remove();
The exit method:
Returns the exit selection: existing DOM elements in the selection for
which no new datum was found. (The exit selection is empty for
selections not returned by selection.data.) (API documentation).
While you have specified data for the selections g, foreground, and background, you have not specified any data for the selection svg, so the exit selection will be empty. Consequently, .remove() can't remove anything.
To use an exit selection, note that the exit selection doesn't remove elements in a selection. It is a selection of a subset of elements in a selection that no longer have corresponding bound data.
If a selection holds 10 DOM elements, and the selection's data array is set to an array with 9 elements, the exit selection will contain one element: the excess element.
We can remove this subset with .exit().remove(). The exit is which elements we no longer need, and remove() gets rid of them. Exit doesn't remove elements because we may want to still do something with them, such as transition them, or hide them, etc.
As noted in the quote above, to populate an exit selection you must use selection.data():
var selection = svg.selectAll("path")
.data(data);
Now we can access the enter selection, the exit selection, and the update selection.
Unless specifying a key, only one of the enter or exit selections can contain elements: if we need more elements we don't need to exit any, if we have excess elements, we don't need any new ones.
To remove excess elements, we can now use:
var selection = svg.selectAll("path")
.data(data);
selection.exit().remove();
Given you are specifying data for other selections such as foreground, background, and g, these are the selections you should be using .exit() on, as part of a complete enter/update/exit cycle. Keep in mind, in d3v4 you need to merge the update and enter selections in order to perform operations on pre-existing and new elements in the selection.
However, if you simply want to get rid of the svg and start fresh (which is what it looks like you want to do - as you are trying to remove the svg) you can simply remove the svg:
d3.select("#test")
.select("svg")
.remove();
However, this won't allow you transition nicely between charts or utilize the enter/update/exit cycle.
Why note use svg.remove()? Because the selection svg holds a g element:
var svg = d3.select("#test") // returns a selection with #test
.append("svg:svg") // returns a selection with the new svg
.attr(...) // returns the selection with the newly created svg again
.append("svg:g") // returns a selection with the new g
.attr(...); // returns the selection with the newly created g again
svg is consequently a selection of a g, and removing it won't remove the svg element. In fact running the above code block after svg.remove() will add a new svg element to the DOM while not removing the old one.

Call function(d){} in .attr()

I tried to set numbers as translate attributes of groups.
But, to set the numbers, I need to access data.
I found it's impossible to access data with function(d){}.
How to access data in .attr()?
var xCol = 'month'
var wraps = g.selectAll('.wrap').data(data);
wraps.enter().append('g')
.attr('class', 'wrap')
.attr('transform', 'translate('+function(d){return xScale(d[xCol])}()+', '+ (-margin.top)+')')
>>>index.js:132 Uncaught TypeError: Cannot read property 'month' of undefined
And I want to make several groups .wrap and draw bars in each groups
But, I have no idea to forward data to child elements.
var bars = wraps.selectAll('.bar').data(function(d){return d});
bars.enter().append('rect')
.attr('class', 'bar')
.attr('x', function(d){return xScale(d.d[xCol])})
...
I think you may just have the idea right but missing one small thing. The second argument to the attr function should be another function. Like below.
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 + 50;})
.attr('cy', 50)
.attr('fill', 'red')
So the function then returns the result. This fiddle shows it in action
http://jsfiddle.net/bdkxgph5/1/
So in your case replace you attr call with
.attr('transform', function(d) { return 'translate('+xScale(d[xCol])+', '+ (-margin.top)+')'})

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.

D3Js updating wrong elements using call

I am new to D3JS and you can find the code at http://jsfiddle.net/3n8wD/
I am a facing an issue and any pointers would help
When I move the line it separates as expected, but as soon as I try to move the circles away it jumps back to the line.
On inspecting the Array, looks like the circle array is updating as I am moving the link, not sure what is causing that.
Any help on this would be highly appreciated. below is the code that i have on jsfiddle
var width = 960,
height = 500;
graph1 = {"nodes":[{"x":444,"y":275},{"x":378,"y":324}],"links":[{"source":1,"target":0}]}
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var onDraggable = function(d1) {
console.log(d1);
d1.x = d3.event.x, d1.y = d3.event.y;
d3.select(this).attr("cx", d1.x).attr("cy", d1.y);
//console.log(d1);
link.filter(function(l) { return l.source === d1; }).attr("x1", d3.event.x).attr("y1", d3.event.y);
link.filter(function(l) { return l.target === d1; }).attr("x2", d3.event.x).attr("y2", d3.event.y);
}
var drag = d3.behavior.drag()
.origin(function(d) { return d; })
.on("drag", onDraggable)
var onDraggable1 = function(d) {
//d.x1 = d3.event.x1, d.y1 = d3.event.y1, d.x2=d3.event.x2, y2=d3.event.y2;
var mouseX = d3.mouse(this)[0];
var mouseY = d3.mouse(this)[1];
var relativeX = mouseX-d.source.x;
var relativeY = mouseY-d.source.y;
console.log(d);
//console.log(d);
// d3.select(this).attr("x1", d3.event.x).attr("y1", d3.event.y).attr("x2", d3.event.x).attr("y2", d3.event.y);
d.source.x= d.source.x+relativeX;
d.source.y = d.source.y+relativeY;
d.target.x= d.target.x+relativeX;
d.target.y = d.target.y+relativeY;
d3.select(this).attr("x1", d.source.x).attr("y1", d.source.y);
d3.select(this).attr("x2", d.target.x).attr("y2", d.target.y);
}
var drag1 = d3.behavior.drag()
.origin(function(d) { return d; })
.on("drag", onDraggable1);
graph1.links.forEach(function(d) {
d.source = graph1.nodes[d.source];
d.target = graph1.nodes[d.target];
});
var node = svg.append("g").attr("class","node")
.selectAll("circle")
.data(graph1.nodes)
.enter().append("circle")
.attr("r", 4)
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.call(drag);
var link = svg.append("g").attr("class","link")
.selectAll("line")
.data(graph1.links)
.enter().append("line")
.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; })
.attr("stroke-width", 2)
.attr("stroke", "black")
.call(drag1);
The reason that your nodes are "jumping" to the end of the line, even after the line has been moved away, is because the data object for the node is the same object as the source/target data object for the line. The d value for the node and the d.source or d.target value for the link are just pointers (references) to the same object in the browser's memory. Everything you do to that data object is reflected in both the link and the node. That's what makes the node-drag function work: you change the data object directly, and then you update the position of the line and the circle to match their already-changed data value.
So, even though you don't update the circle's cx and cy position at the time you move the line, the statements d.source.x = etc. in the line drag method are setting the d.x and d.y values for the nodes. Then in the node drag method, when you access these values to determine the new position, the movement is determined relative to the end of the line, not to the position of the circle on screen.
So how do you get the behaviour you want, to separate the line from the node? You need to create a new data object for the line's source and target when you start to drag it, one that no longer references the nodes' data objects. You only need to do this once, at the start of the drag, so you use the dragstart event of the drag behaviour object:
var drag1 = d3.behavior.drag()
.origin(function(d) { return d; })
.on("dragstart", separateNodes);
.on("drag", onDraggable1);
The separateNodes method is going to be quite short. All we have to do is, for both the target and the source of the link, create a new data object that is a copy of the existing target/source object (but which can be edited independently of the original object). Javascript doesn't have any default methods for copying an object (although various extensions do), but if the object in question just consists of x and y attributes, it is easy to do.
var separateNodes = function(d) {
//create new data objects that are copies of d.source and d.target
var newSource = {x:d.source.x, y:d.source.y};
var newTarget = {x:d.target.x, y:d.target.y};
//set the new objects as the target/source of this link
d.source = newSource;
d.target = newTarget;
}
You could even skip the variable declarations and just merge it all into one line for source and one for target, but I find it easier to understand like this. I should mention, however, that this approach works because x and y are both just simple data types (numbers), not pointers to objects. So when it says x:d.source.x, you are actually getting a new copy of that number which you can change without having it affect the original object that contained that number.
If, however, your nodes and links also have a value property that is a complex data object, then you have to decide what you want to do when the line separates from the node: do you want to create a copy of the entire value data object, or maintain the link to the original object, or replace it with a null (empty/undefined) object? The first case is complicated, and depends on the data structure of the value object; for the second you don't need to do anything (except remember that you've got multiple pointers to the same object); and for the last case you just add a value:null, attribute to the newSource and newTarget object definitions in your separateNodes function.
Hope that all makes sense,
--ABR

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