How can I get the value of the previous element of the dataset passed to .data() in d3?
I know in a callback I cant do something like
function(d,i) { console.log(d, i); })
for example, to print the data element and its index to the console. But how can i reference the previous element?
Like d[i-1] or something?
You can get the value of previous element like this.
var texts = svg.selectAll("text")
.data(data)
.enter()
.append("text")
.attr("x",function(d){ return d.x; })
.attr("y",function(d){ return d.y; })
.text(function(d){ return d.name; });
texts.attr(...,function(d,i){
......
var prevData = texts.data()[i-1]; //check whether i>0 before this code
.......
});
Here is a small example JSFiddle , Mouse over the texts to see the functionality.
There is no built in way to do it, but you can achieve it in all kinds of ways, including
Scope:
var data = ['a','b','c','d']
d3.select('.parent').selectAll('.child')
.data(data)
.enter()
.append('div')
.attr('class', 'child')
.text(function(d,i) {
return "previous letter is " + data[i-1];
});
Linking (works even if they're Strings, as in this example):
var data = ['a','b','c','d']
for(var i = 0; i < data.length; i++) { data[i].previous = data[i-1]; }
d3.select('.parent').selectAll('.child')
.data(data)
...
.text(function(d,i) {
return "previous letter is " + d.previous
});
Via the parent node (Experimental):
var data = ['a','b','c','d']
d3.select('.parent').selectAll('.child')
.data(data)
...
.text(function(d,i) {
var parentData = d3.select(this.parentNode).selectAll('.child').data();
// NOTE: parentData === data is false, but parentData still deep equals data
return "previous letter is " + parentData[i-1];
});
Related to the last example, you can even try to find the sibling DOM node immediately preceding this node. Something like
...
.text(function(d,i) {
var previousChild = d3.select(this.parentNode).select('.child:nth-child(' + i + ')')
return "previous letter is " + previousChild.datum();
})
but the last two can fail in all kinds of ways, like if the DOM nodes aren't ordered the same as data, or if there are other unrelated DOM nodes within the parent.
Related
Using D3, I'm trying to implement a word wrap plugin for each quotation displayed from myData. The call function on the last line is that.
The problem is that it only works for the first quote. It makes the all other quotes not appear. I'm not sure how to structure the call to happen while enter() would typically renders thing.
var quote=svg.selectAll("text.quote").data(myData);
quote.exit().remove()
quote.enter().append("text")
quote
.attr("class","quote")
.attr("x", function (d,i){ return xScale(i);})
.attr("y", function(d){ return yScale(d.y);})
.text(function(d, i){return d.quote;})
.call(d3.util.wrap(125))
You want selection.each() rather than selection.call(). Selection.call will will invoke a function just once, while .each will invoke it for each datum:
selection.each(function) <>
Invokes the specified function 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
(nodes[i]). This method can be used to invoke arbitrary code for each
selected element...
Compare to:
selection.call(function[, arguments…]) <>
Invokes the specified function exactly once, passing in this selection
along with any optional arguments.
(API Documentation (v4, but both methods exist in v3))
See the following snippet for a comparison of both:
var data = [10,20,30,40];
var selection = d3.select("body").selectAll(null)
.data(data)
.enter()
.append("p")
.each(function(d) {
console.log("each: " + d); // d is datum
})
.call(function(d) {
console.log("call: ")
console.log(d.data()); // d is selection
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
To call this function once on each item, you could use .call within the .each. I've used a g to place the text, so that the tspans this utility creates are positioned correctly (otherwise they overlap). The following snippet otherwise uses your code (the word wrap utility is on top as I could not find a cdn for it quickly enough):
d3.util = d3.util || {};
d3.util.wrap = function(_wrapW){
return function(d, i){
var that = this;
function tspanify(){
var lineH = this.node().getBBox().height;
this.text('')
.selectAll('tspan')
.data(lineArray)
.enter().append('tspan')
.attr({
x: 0,
y: function(d, i){ return (i + 1) * lineH; }
})
.text(function(d, i){ return d.join(' '); })
}
function checkW(_text){
var textTmp = that
.style({visibility: 'hidden'})
.text(_text);
var textW = textTmp.node().getBBox().width;
that.style({visibility: 'visible'}).text(text);
return textW;
}
var text = this.text();
var parentNode = this.node().parentNode;
var textSplitted = text.split(' ');
var lineArray = [[]];
var count = 0;
textSplitted.forEach(function(d, i){
if(checkW(lineArray[count].concat(d).join(' '), parentNode) >= _wrapW){
count++;
lineArray[count] = [];
}
lineArray[count].push(d)
});
this.call(tspanify)
}
};
var wrap = d3.util.wrap(11);
var svg = d3.select("body")
.append("svg")
.attr("height",400)
.attr("width",400);
var myData = ["text 1","text 2","text 3"]
var quote = svg.selectAll("text.quote").data(myData);
quote.enter().append("g")
quote.attr("class","quote")
.attr("transform", function (d,i){ return "translate(20," + (i * 40 + 20) + ")" })
.append("text")
.text(function(d, i){return d})
.each(function() {
d3.select(this).call(d3.util.wrap(11));
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
Is there an array containing those?
Also, is it also possible to add nodes in an existing path?
You need to edit the path. No, there's no array, just the path string. You can of course write higher level code that keeps tracks of nodes in arrays from which you generate path strings.
A simple implementation would be something like:
function Polygon (nodes) {
if (nodes instanceof Array) this.nodes = nodes;
else this.nodes = [];
}
Polygon.prototype.draw = function () {
var path = "M" + this.nodes[0][0] + "," + this.nodes[0][1];
for (var i=1; i<this.nodes.length; i++) {
path += "L" + this.nodes[i][0] + "," + this.nodes[i][1];
}
path += "Z";
return path;
}
Then you would do:
var p = new Polygon([[100,100],[100,200],[200,200],[200,100]]);
var pp = paper.path(p.draw());
// Modify node:
p.nodes[2] = [300,300];
pp.attr('path',p.draw());
// Add node:
p.nodes.push([250,150]);
pp.attr('path',p.draw());
Of course you can be more creative with the API you implement. For example, polygon.draw() can automatically update a linked Raphael Element. The code above is just a simple example of the basic idea.
I want to set up a d3 quadtree using data which does not have columns labelled x and y, but with some other labels. Reading the quadtree docs I thought I could do it like this:
var data = [{"a":6,"b":99.0},{"a":12,"b":227.0},{"a":2,"b":43.0},{"a":23,"b":32.0}];
var xname = "a";
var yname = "b";
var quadtree = d3.geom.quadtree(data)
.x(function(d) {return d[xname]; })
.y(function(d) {return d[yname]; });
But when I run this code I get an error:
TypeError: d3.geom.quadtree(data).x is not a function. (In 'd3.geom.quadtree(data).x(function(d) {return d[xname]; })', 'd3.geom.quadtree(data).x' is undefined)
How can I set up the quadtree to use different x and y labels? (Obviously I could change the data to use "x" and "y" but there are reasons I don't want to do that.)
Don't create your quadtree passing it data
e.g :
var data = [{"a":6,"b":99.0},{"a":12,"b":227.0},{"a":2,"b":43.0},{"a":23,"b":32.0}];
var xname = "a";
var yname = "b";
var quadtree = d3.geom.quadtree(/* nothing here */)
.x(function(d) {return d[xname]; })
.y(function(d) {return d[yname]; });
data binding must append later
d3.selectAll('circle').data(quadtree(data))
.append('circle');
I'm a newbie in javascript but a few weeks back just delved into d3.js trying
to create a spatio-temporal visualisation.
What I want to achieve is something like this (https://jsfiddle.net/dmatekenya/mz5fxd44/) based on code shown below:
var w1 = ["Dunstan","Mercy","Lara","Khama"];
var w2 =["August 1,2013","August 2,2013","August 3,2013"]
var text = d3.select("body")
.attr("fill","red")
.text("Dunstan")
.attr("font-size", "50px");
var j = 0;
var transition = function() {
return text.transition().duration(500).tween("text", function() {
var i = d3.interpolateString(w1[j], w1[j + 1]);
return function(t) {
this.textContent = i(t);
};
}).each('end', function() {
j += 1;
if (!(j > w1.length)) return transition();
});
};
transition();
However, instead I want to use date string ( like w2 in the code snippet above). When I do this d3 interpolates the numbers embedded in the string as well and the output isn't what I'm looking for. I need help on how I can somehow create a custom interpolator which can interpolate date string while ignoring numbers in them. I know from d3 documentation (https://github.com/mbostock/d3/wiki/Transitions#tween) thats its possible to push a custom interpolator into the d3 array of interpolators but I have tried and still cannot get it to work.
Kindly need your help.
Maybe you need d3.scaleQuantize().
For instance:
var color = d3.scaleQuantize()
.domain([0, 1])
.range(["brown", "steelblue"]);
color(0.49); // "brown"
color(0.51); // "steelblue"
I created a multiple-line chart using nvd3, but was unable to modify it in some important ways. I would like to roll my own using straight d3js, but I'm struggling with thinking in joins.
I need to create a path for each d.key with its own corresponding d.values.
My data is formatted for nvd3 as follows (abridged).
[
{
"key":"brw-a",
"values":[
["2012-07-11T00:00:00", 0.0 ],
["2012-07-11T23:59:59", 0.0 ],
["2012-07-05T06:31:47", 0.0 ],
["2012-07-05T23:59:59", 0.0 ]
]
},
{
"key":"brw-c",
"values":[
["2012-07-11T00:00:00", 0.0 ],
["2012-07-07T00:00:00", 2.0 ],
["2012-07-05T23:59:59", 4.0 ]
]
}
]
I seem to need an inner loop to access the array stored in each d.values. I have a working example that demonstrates how d.values comes out in one big glob of uselessness.
var p = d3.select("body").selectAll("p")
.data(data)
.enter().append("p")
.text(function(d) {return d.key +": " + '[' + d.values + ']'})
I feel like I'm close, and it has something to do with:
.data(data, function(d) { return d.key; })
Update: I was able to manually loop over the data to create the desired effect. Perhaps there is not a way of doing this with joins? Save for using the wonderful nvd3 lib, of course. See the comment below for the link.
var body = d3.select("body")
for (i=0; i < data.length; i++) {
var key = data[i].key
var values = data[i].values
body.append("h3")
.text(key)
for (j=0; j < values.length; j++) {
body.append("p")
.text(values[j][0] + " -- " + values[j][1])
}
}
You were right about .data() function. But because you need to loop through elements in values, that's what you need to pass as data for a nested selection:
.data(function(d) { return d.values})
You could try this:
var p = d3.select("body").selectAll("p")
.data(data)
.enter().append("p")
.attr("id", function(d) { return d.key})
.text(function(d) {return d.key})
.selectAll("span")
.data(function(d) { return d.values})
.enter().append("span")
.text(function(d) {return d})
Produces:
<p id="brw-a">brw-a
<span>2012-07-05T00:00:00,0</span>
<span>2012-07-06T23:59:59,1</span>
<span>2012-07-07T06:31:47,0</span>
<span>2012-07-08T23:59:59,3</span>
</p>
<p id="brw-c">brw-c
<span>2012-07-11T00:00:00,0</span>
<span>2012-07-07T00:00:00,2</span>
<span>2012-07-05T23:59:59,4</span>
</p>