d3 - How to animate over array - javascript

I am pretty new to d3. For the moment I am able to draw circles based on an array of data - wow - I know :-) But now I would like to just draw two circles at one time while I animate the whole array. Let's say I have 1000 elements in my array and I want to draw 1 and 2 at the same time, then draw 2 and 3, 3 and 4 and so on. This should get a very pretty animation :-) I have played with functions i index and with exit().remove() but this does not work.
This is what I have:
var w = 500;
var h = 300;
var padding = 20;
var dataset = [
[5, 20], [480, 90], [250, 50], [100, 33], [330, 95],
[410, 12], [475, 44], [25, 67], [85, 21], [220, 88],
[600, 150]
];
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
//Create scale functions
var xScale = d3.scale.linear()
.domain([0, d3.max(dataset, function(d) { return d[0]; })])
.range([padding, w - padding * 2]);
var yScale = d3.scale.linear()
.domain([0, d3.max(dataset, function(d) { return d[1]; })])
.range([h - padding, padding]);
//Create circles
svg.selectAll("circle")
.data(dataset.slice(0,2))
.enter()
.append("circle")
.attr("cx", function(d) {
return xScale(d[0]);
})
.attr("cy", function(d) {
return yScale(d[1]);
})
.attr("r",10);
for (var i=0; i<4;i++) {
svg.selectAll("circle").data(dataset.slice(i,i+2)).transition().duration(2000).delay(2000)
.attr("cx", function(d) {
return xScale(d[0]);
})
.attr("cy", function(d) {
return yScale(d[1]);
})
.attr("r", 10);
//svg.selectAll("circle").data(dataset.slice(i,i+1)).exit().remove();
console.log(dataset.slice(i,i+2));
}
But I will get only one single animation instead of 4 .. hmm .. what is going wrong?

The delay function accepts callbacks, so there is no need to wrap your selection in a for loop.
.delay( function(d, i) { (2000*i); } )

Looking at the code, you've got a fixed duration (2s) and a fixed delay (2s). The FOR loop will run instantly, thus queueing all four animations up at once, and thus they are probably all playing at the same time - but (probably) only the last will be visible because you've rebound the data.
try something like:
svg.selectAll("circle")
.delay( (2000*i) )
.data(dataset.slice(i,i+2))
.transition()
.duration(2000)
.attr("cx", function(d) {return xScale(d[0]);})
.attr("cy", function(d) {return yScale(d[1]);})
.attr("r", 10);)
Multiplying the delay by the animation counter should offset each animation, and by putting the delay first, it should the data gets rebound just before starting the animation (thereby stopping the final animation step from rebinding it's data before the first animation has run)

Related

D3: issue with legend scaling

I am trying to amend M Bostock's US unemployment choropleth map that applies D3 scale chromatic.
I am now able to amend the bucket-sizes as I please to plot my data, however when I do so the legend seems to grow in size exponentially and I am unable to make it fit to a desired width and scale the intervals appropriately.
Please see the attached jsfiddle where I demonstrate the issue encountered. I would like to amend the legend in two ways:
space between ticks is fixed, e.g. 50px
space between ticks is a function of scale, but legend still fits within desired width (e.g. 500px)
My problem is that I do not seem to be able to amend parameters in the following line of code (I am hoping this is not a default in the scale chromatic script..)
g.call(d3.axisBottom(x)
.tickSize(13)
.tickFormat(function(x, i) { return i ? x : x + "%"; })
.tickValues(color.domain()))
.select(".domain")
.remove();
There are only two problems in your code, both easy to fix.
The first problem is the domain here:
var myDomain = [1, 5, 8, 9, 12, 18, 20, 25]
var x = d3.scaleLinear()
.domain(myDomain)
.rangeRound([600, 860]);
As you can see, you're passing an array with several values. However, for a linear scale with just two values in the range, you have to pass an array with just two values.
Therefore, it should be:
var myDomain = [1, 5, 8, 9, 12, 18, 20, 25]
var x = d3.scaleLinear()
.domain(d3.extent(myDomain))//just two values here
.rangeRound([600, 860]);
The second problem is here:
if (d[1] == null) d[1] = x.domain()[1];
//this is the second element -------^
Since myDomain is an array with several values, you're passing the second value here. But you don't want the second value, you want the last value.
Therefore, it should be:
if (d[1] == null) d[1] = x.domain()[x.domain().length - 1];
//now this is the last element --------------^
Here is the code with those changes (I removed the map, we don't need it for this answer, and also moved the legend to the left, so it better fits S.O. snippet):
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
var myDomain = [1, 5, 8, 9, 12, 18, 20, 25]
var x = d3.scaleLinear()
.domain(d3.extent(myDomain))
.rangeRound([200, 460]);
var color = d3.scaleThreshold()
.domain(myDomain)
.range(d3.schemeBlues[9]);
var g = svg.append("g")
.attr("class", "key");
g.selectAll("rect")
.data(color.range().map(function(d) {
d = color.invertExtent(d);
if (d[0] == null) d[0] = x.domain()[0];
if (d[1] == null) d[1] = x.domain()[x.domain().length - 1];
return d;
}))
.enter().append("rect")
.attr("height", 8)
.attr("x", function(d) { return x(d[0]); })
.attr("width", function(d) { return x(d[1]) - x(d[0]); })
.attr("fill", function(d) { return color(d[0]); });
g.append("text")
.attr("class", "caption")
.attr("x", x.range()[0])
.attr("y", -6)
.attr("fill", "#000")
.attr("text-anchor", "start")
.attr("font-weight", "bold")
.text("Unemployment rate");
g.call(d3.axisBottom(x)
.tickSize(13)
.tickFormat(function(x, i) { return i ? x : x + "%"; })
.tickValues(color.domain()))
.select(".domain")
.remove();
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<svg width="600" height="200"></svg>

D3--How to iterate through an array of colors

I am a very beginner programmer (emphasis on very). I am trying to figure out how to iterate through an array of colors in order to make three rectangles of different colors. Here is my code so far:
var dataArray = [5, 11, 18];
var colors = ["red", "green", "black"];
var svg = d3.select("body").append("svg")
.attr("width","2000").attr("height","400");
svg.selectAll("rect")
.data(dataArray)
.enter()
.append("rect")
.attr("fill", function(d, i) { return colors*i}) //the line of code in question
.attr("x", function(d,i) { return 70*i + 50; })
.attr("y", function(d,i) { return d*15; })
.attr("height", function(d,i) { return 500; })
.attr("width", "50");
//code end
As you can see, I've been trying to use function(d, i) to iterate through array colors, unsuccessfully. Full disclosure: the above code was created for a class, but this particular question is not part of the assignment. I'm trying to go just a tiny bit beyond the assignment.
The other answer is right, colors[i] will give you your colors, so it deserves the checkmark. The fix in that answer will give you:
var dataArray = [5, 11, 18];
var colors = ["red", "green", "black"];
var svg = d3.select("body").append("svg")
.attr("width","2000").attr("height","400");
svg.selectAll("rect")
.data(dataArray)
.enter()
.append("rect")
.attr("fill", function(d, i) { return colors[i]})
.attr("x", function(d,i) { return 70*i + 50; })
.attr("y", function(d,i) { return d*15; })
.attr("height", function(d,i) { return 500; })
.attr("width", "50");
//code end
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
But, looking at the results, the first bar is the tallest, but has the smallest data value. All bars are 500 pixels high, each has a part that is below the edge of the SVG.
I thought I'd just point out a couple potential improvements on your code:
The use of function(d,i) is only necessary when grabbing either the current element in your data array (d) or the current increment (i), so for:
.attr("height", function(d,i) { return 500; }) you can use: .attr('height',500);
But, I doubt you want all items to be 500 pixels tall (especially as your svg is only 400 pixels tall). This will be especially apparent if you have margins.
So, for height, we can use the formula you are currently using for your y coordinate:
.attr("height", function(d,i) { return d*15; })
Now, we have to have the bars end at the same point by manipulating the position of the top of each rectangle:
(go up d*15 pixels from the bottom of the svg (which is at 400), in svg coordinate space 0 is at the top):
.attr("y", function(d,i) { return 400 - d*15; })
Which gives you:
var dataArray = [5, 11, 18];
var colors = ["red", "green", "black"];
var svg = d3.select("body").append("svg")
.attr("width","2000").attr("height","400");
svg.selectAll("rect")
.data(dataArray)
.enter()
.append("rect")
.attr("fill", function(d, i) { return colors[i]})
.attr("x", function(d,i) { return 70*i + 50; })
.attr("y", function(d,i) { return 400-d*15; })
.attr("height", function(d,i) { return d*15; })
.attr("width", "50");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
Lastly,
Quotations enclose strings, but if you are using numbers then you can drop them:
This: .attr("width", "50"); can be .attr("width",50);
Have you tried using colors[i] instead of colors*i?
That will allow you to access the value in place i.
(colors[1] will be 'green', colors[0] will be 'red' etc)

How do I make a scatter plot of lines in D3?

I have a series of paired xy coordinates that create 58 lines. I want to plot them on a Cartesian graph, values are between -5 and 5 on both axis, essentially making a scatter plot of lines. I have made something similar in matplotlib using the quiver function, but I want to be able to do this in D3. I would also like to be able to label each line, or each line that meets a length threshold. The code I have come up with below. Thanks.
var lisa = [["Eloy",0.0169808,-0.695317,-0.0510301,-0.6995938],
["Florence",-0.3465685,-0.6790588,-0.5869514,-0.6762134],
["Phoenix",0.677068,-0.5754814,-0.6052215,-0.6158059],
["Tucson",-0.663848,0.4111043,-0.6722116,0.011639]]
var w = 200;
var h = 200;
//create the svg element and set the height and width parameters
var svg = d3.select("div").select("div")
.append("svg")
.attr("height",h)
.attr("width", w)
.style("border", "1px solid black");
//Create the scale for the scatter plot
var xScale = d3.scale.linear()
.domain([d3.min(dataset, function(d) { return d[0];}),d3.max(dataset, function(d) { return d[0];})])
.range([-1,1]);
var yScale = d3.scale.linear()
.domain([d3.min(dataset, function(d) { return d[1];}),d3.max(dataset, function(d) { return d[1];})])
.range([-1,1]);
//This is the function that creates the SVG lines
var line = svg.selectAll("line")
.data(lisa)
.enter()
.append("line");
//This gets the cooresponding x,y cordinates from the dataset
line.attr("x1", function(d) {
return xScale(d[0]);
})
.attr("y1", function(d) {
return yScale(d[1]);
})
.attr("x2", function(d) {
return xScale(d[2]);
})
.attr("y2", function(d) {
return yScale(d[3]);
})
.attr("stroke", "black");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
Your code has some problems:
First, your range right now ([-1, 1]) makes no sense. This should be the domain instead (I changed the ranges to [0, w] and [0, h]).
In your real code, the domain should be [-5, 5] and the range should be the limits of the plot, something like [leftLimit, rightLimit] and [topLimit, bottomLimit] (have in mind that, in an SVG, the 0 position for the y axis is the top, not the bottom).
Second, given this array:
["Tucson",-0.663848,0.4111043,-0.6722116,0.011639]
your x and y positions should be the indices 1,2,3 and 4, not 0, 1, 2 and 3.
Besides that changes, I added the labels:
var text = svg.selectAll(".text")
.data(dataset)
.enter()
.append("text");
text.attr("font-size", 10)
.attr("x", function(d) {
return xScale(d[1]);
})
.attr("y", function(d) {
return yScale(d[2]);
})
.text(d => d[0]);
Here is the demo with the corrections:
var dataset = [["Eloy",0.0169808,-0.695317,-0.0510301,-0.6995938],
["Florence",-0.3465685,-0.6790588,-0.5869514,-0.6762134],
["Phoenix",0.677068,-0.5754814,-0.6052215,-0.6158059],
["Tucson",-0.663848,0.4111043,-0.6722116,0.011639]];
var color = d3.scale.category10();
var w = 400;
var h = 300;
//create the svg element and set the height and width parameters
var svg = d3.select("body")
.append("svg")
.attr("height",h)
.attr("width", w)
.style("border", "1px solid black");
//Create the scale for the scatter plot
var xScale = d3.scale.linear()
.domain([-1,1])
.range([0,w]);
var yScale = d3.scale.linear()
.domain([-1,1])
.range([0,h]);
//This is the function that creates the SVG lines
var line = svg.selectAll("line")
.data(dataset)
.enter()
.append("line");
//This gets the cooresponding x,y cordinates from the dataset
line.attr("x1", function(d) {
return xScale(d[1]);
})
.attr("y1", function(d) {
return yScale(d[2]);
})
.attr("x2", function(d) {
return xScale(d[3]);
})
.attr("y2", function(d) {
return yScale(d[4]);
})
.attr("stroke-width", 2)
.attr("stroke", (d,i)=>color(i));
var text = svg.selectAll(".text")
.data(dataset)
.enter()
.append("text");
text.attr("font-size", 10)
.attr("x", function(d) {
return xScale(d[1])+2;
})
.attr("y", function(d) {
return yScale(d[2]) + 4;
})
.text(d=>d[0]);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

How to change the data that appear on the axis using D3.js

I am having problems on setting the data that appear on the xAxis. At this momment I am using the Time in seconds, at this point everything is correct and the data is set aright but in the xAxis I would like to have the time in minutes from the first one, instead of the seconds on I calculate the position. I am wondering if it is posible or I have to re-do the xScale.
var xScale = d3.scale.linear()
.domain([
d3.min(ds, function(d, i){ return d.Seconds - 100;}),
d3.max(ds, function(d){ return d.Seconds})
])
.range([
w - padding,
padding + 100,
]);
var yScale = d3.scale.linear()
.domain([
1 ,
d3.max(ds, function(d){ return d.Place + 2; })
])
.range([ padding, h - padding ]);
Another question I have is that now, I set a function that makes the name and the dots appear bigger on mouseover, Is it possible to make it both at the same time? this is the piece of code on I have it set, but as I did the dots and the labels separetly I cannot see how to connect them.
var dots = svg.selectAll("circle")
.data(ds)
.enter()
.append("circle")
.attr({
cx: function(d, i){ return xScale(d.Seconds); },
cy: function(d){ return yScale(d.Place); },
r: 5,
fill: function(d) { return doping(d.Doping);}
})
.on("mouseover", function(d){
d3.select(this).attr({
r: 7
});
})
.on("mouseout", function(d){
d3.select(this).attr({
r: 5
})
});
/* - - - Labeling the chart - - - */
var labels = svg.selectAll(".label")
.data(ds)
.enter()
.append("text")
.text(function(d){ return d.Name; })
.attr({
x: function(d){ return xScale(d.Seconds) + 20; },
y: function(d){ return yScale(d.Place)+5;},
"class": "label",
"font-size": "10px",
"font-family": "sans-serif",
"text-anchor": "start",
"fill": "#666666"
})
.on("mouseover", function(d){
d3.select(this).attr({
"font-size": "14px"
});
})
.on("mouseout", function(d){
d3.select(this).attr({
"font-size": "10px"
})
})
}
CodePen: http://codepen.io/DiazPedroAbel/pen/xEgNrR
There are several ways for achieving the effect that you ask in your second question ("I set a function that makes the name and the dots appear bigger on mouseover, Is it possible to make it both at the same time?"), such as using groups. I particularly like a different approach, using classes to select all elements I want at once:
First, we set the same class for the circles and the texts:
class: function(d, i){ return (d.Name).split(" ").join("") + i}
Then, inside the mouseover, we retrieve the class:
var thisClass = d3.select(this).attr("class");
And use it for changing both circles and texts.
For your first question, if I understand it correctly (maybe not), you are already showing the time in seconds. So, all you need is to divide it by 60:
var xAxisGen = d3.svg.axis()
.scale(xScale)
.orient("bottom")
.ticks(8)
.tickFormat(function(d){ return d/60});
Here is your codepen: http://codepen.io/anon/pen/dpvxxb?editors=1010

Selecting a subset of circles using D3

When creating circles using D3, is it possible to create a group such that they can be selected at a later stage? For example, if circles are created using the following approach:
var dataset = [ [ 30, 50, 20],
[ 100, 50, 20],
[ 150, 50, 30]];
//Create SVG element
var svg = d3.select("#chart")
.append("svg")
.attr("width", 200)
.attr("height", 200);
// generate circles
svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function(d){
return d[0];
})
.attr("cy", function(d){
return d[1];
})
.attr("r", function(d){
return d[2];
});
Can I tag the circle created from the first array element with circle1 and the second two circles as circle2?
absolutely - update the class attribute dynamically based on the data index:
.attr("class", function(d,i) {return i == 0 ? "circle1" : "circle2";});
then use the assigned classes for selecting elements:
d3.select(".circle1"); //first circle
d3.selectAll(".circle2"); //second and third circles

Categories

Resources