D3 arc tweening not working - javascript

I am new to programming so apologies if the answer to this is obvious but after hours of searching I can't find out what's wrong.
I simply want to tween an arc in D3.js (in this case change the endAngle to 0). I've been through lots of examples but I must be missing something. I have built a function to change arc colour on clicking which works but it is the second function 'arcTween' to change the arc endAngle of the outermost arcs that doesn't work. Can you help?
Many thanks
Full JS fiddle here: http://jsfiddle.net/vaaa052h/
Extracts below
var chartArea = d3.select("#chart")
.append("svg") // d3 SVG function
.attr("width", 210)
.attr("height", 210);
var arcGroup = chartArea.append("g") // d3 g grouping function
.attr("transform", "translate(" + transX + "," + transY + ")")
.attr("class", "arc");
var arc = d3.svg.arc()
.innerRadius(function (d) {
return radius[level];
})
.outerRadius(function (d) {
return radius[level + 1];
})
.startAngle(function (d) {
return minAngArc;
})
.endAngle(function (d) {
return maxAngArc;
});
//////// chart building ///////////////
arcGroup.append("path")
.attr("d", arc)
.attr("fill", color(0, random, 0, i, j, k))
.attr("opacity", opacity(rating))
.on("click", arcTween());
////// click functions //////////
function arcTween(d) {
d3.select(this).transition().duration(1000)
.attrTween("d", function (d) {
var interpolate = d3.interpolate(d.endAngle, 0);
return function (t) {
d.endAngle = interpolate(t);
return arc(d);
};
});
};

I made a couple of changes in this updated fiddle: http://jsfiddle.net/henbox/a8r326m5/1/
First, when you set up the click handler, avoid calling it on page load by using:
.on("click", arcTween);
instead of
.on("click", arcTween());
as per Lars' explanation here. This will stop you getting "Object [object global] has no method 'getAttribute'" errors in the console
Second, bind some data to the path elements so we can manipulate it later:
arcGroup.append("path")
.datum({endAngle:maxAngArc, startAngle:minAngArc})
....
And thirdly, use this data in the arcTween function. By setting maxAngArc and minAngArc, and then tweening the value of maxAngArc to minAngArc (I've asumed you mean to do this rather than tweening to 0), you should get the behaviour you want. The tween function:
function arcTween(d) {
maxAngArc = d.endAngle;
minAngArc = d.startAngle;
d3.select(this).transition().duration(1000)
.attrTween("d", function (d) {
var interpolate = d3.interpolate(d.endAngle, d.startAngle);
return function (t) {
maxAngArc = interpolate(t);
return arc(d);
};
});
};

Related

D3 chart can't update -- enter and exit property of selection both empty

I'm trying to make a scatter plot using a .json file. It will let the user to select which group of data in the json file to be displayed. So I'm trying to use the update pattern.
The following code will make the first drawing, but every time selectGroup() is called(the code is in the html file), nothing got updated. The console.log(selection) did come back with a new array each time, but the enter and exit property of that selection is always empty.
Can anyone help me take a look? Thanks a lot!
var margin = {
top: 30,
right: 40,
bottom: 30,
left: 40
}
var width = 640 - margin.right - margin.left,
height = 360 - margin.top - margin.bottom;
var dataGroup;
var groupNumDefault = "I";
var maxX, maxY;
var svg, xAxis, xScale, yAxis, yScale;
//select and read data by group
function init() {
d3.json("data.json", function (d) {
maxX = d3.max(d, function (d) {
return d.x;
});
maxY = d3.max(d, function (d) {
return d.y;
});
console.log(maxY);
svg = d3.select("svg")
.attr("id", "scatter_plot")
.attr("width", 960)
.attr("height", 500)
.append("g")
.attr("id", "drawing_area")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
//x-axis
xScale = d3.scale.linear().range([0, width]).domain([0, maxX]);
xAxis = d3.svg.axis()
.scale(xScale).orient("bottom").ticks(6);
//y-axis
yScale = d3.scale.linear().range([0, height]).domain([maxY, 0]);
yAxis = d3.svg.axis().scale(yScale).orient("left").ticks(6);
svg.append("g")
.attr("class", "x_axis")
.attr("transform", "translate(0," + (height) + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y_axis")
.call(yAxis);
});
selectGroup(groupNumDefault);
}
//update data
function selectGroup(groupNum) {
d3.json("/data.json", function (d) {
dataGroup = d.filter(function (el) {
return el.group == groupNum;
});
console.log(dataGroup);
drawChart(dataGroup);
});
}
//drawing function
function drawChart(data) {
var selection = d3.select("svg").selectAll("circle")
.data(data);
console.log(selection);
selection.enter()
.append("circle")
.attr("class", "dots")
.attr("cx", function (d) {
console.log("updating!");
return xScale(d.x);
})
.attr("cy", function (d) {
return yScale(d.y);
})
.attr("r", function (d) {
return 10;
})
.attr("fill", "red");
selection.exit().remove();
}
init();
The problem here is on two fronts:
Firstly, your lack of a key function in your data() call means data is matched by index (position in data array) by default, which will mean no enter and exit selections if the old and current datasets sent to data() are of the same size. Instead, most (perhaps all) of the data will be put in the update selection when d3 matches by index (first datum in old dataset = first datum in new dataset, second datum in old dataset = second datum in new dataset etc etc)
var selection = d3.select("svg").selectAll("circle")
.data(data);
See: https://bl.ocks.org/mbostock/3808221
Basically, you need your data call adjusted to something like this (if your data has an .id property or anything else that can uniquely identify each datum)
var selection = d3.select("svg").selectAll("circle")
.data(data, function(d) { return d.id; });
This will generate enter() and exit() (and update) selections based on the data's actual contents rather than just their index.
Secondly, not everything the second time round is guaranteed be in the enter or exit selections. Some data may be just an update of existing data and not in either of those selections (in your case it may be intended to be completely new each time). However, given the situation just described above it's pretty much guaranteed most of your data will be in the update selection, some of it by mistake. To show updates you will need to alter the code like this (I'm assuming d3 v3 here, apparently it's slightly different for v4)
selection.enter()
.append("circle")
.attr("class", "dots")
.attr("r", function (d) {
return 10;
})
.attr("fill", "red");
// this new bit is the update selection (which includes the just added enter selection
// now, the syntax is different in v4)
selection // v3 version
// .merge(selection) // v4 version (remove semi-colon off preceding enter statement)
.attr("cx", function (d) {
console.log("updating!");
return xScale(d.x);
})
.attr("cy", function (d) {
return yScale(d.y);
})
selection.exit().remove();
Those two changes should see your visualisation working, unless of course the problem is something as simple as an empty set of data the second time around which would also explain things :-)

How to achieve this transition effect in d3?

I'm very much new to d3 and was wondering how to achieve this effect, where multiple lines remain tangent to the perimeter of a circle at all times.
This is what I have thus far: http://jsfiddle.net/tz5KT/181/
function transition() {
svg.selectAll(".lines")
.attr("x2", function (d) {
var tangent = findTangent(d.x, d.y);
return tangent.x;
})
.attr("y2", function (d) {
var tangent = findTangent(d.x, d.y);
return tangent.y;
});
circleX = getRandom(0, width),
circleY = getRandom(0, height);
svg.select(".circle").transition()
.duration(1500)
.attr("cx", circleX)
.attr("cy", circleY)
.each("end", transition);
}
I'm just not sure how to transition the lines from one position to the next, all while keeping them tangent to the circle. Any advice on how to do this? Much appreciated.
I think I got it
http://jsfiddle.net/tz5KT/219/
Check out my use of attrTween, it might be helpful
you need to add the transition to the line points in the transition function, like so: http://jsfiddle.net/tz5KT/177/
here it's a bit delayed tho
This is what you'll want in the transition function:
function transition() {
circleX = getRandom(0, width),
circleY = getRandom(0, height);
svg.select(".circle").transition()
.duration(1500)
.attr("cx", circleX)
.attr("cy", circleY)
.each("end", transition);
svg.selectAll(".lines")
.transition()
.duration(1500)
.attr("x2", function (d) {
var tangent = findTangent(d.x, d.y);
return tangent.x;
})
.attr("y2", function (d) {
var tangent = findTangent(d.x, d.y);
return tangent.y;
});
}

arc.centroid returning (NaN, NaN) in D3

Fair warning: I'm a D3 rookie here. I'm building a donut chart using D3 and all is well so far, except that the labels on the slices aren't aligning with the slices. Using the code below, the labels for each slice are rendered in the middle of the chart, stacked on top of each other so they're unreadable. I've dropped the arc.centroid in my transform attribute, but it's returning "NaN,NaN" instead of actual coordinates, and I can't understand where it's reading from that it's not finding a number. My innerRadius and outerRadius are defined in the arc variable. Any help?
(pardon the lack of a jsfiddle but I'm pulling data from a .csv here)
var width = 300,
height = 300,
radius = Math.min(width, height) / 2;
var color = ["#f68b1f", "#39b54a", "#2772b2"];
var pie = d3.layout.pie()
.value(function(d) { return d.taskforce1; })
.sort(null);
var arc = d3.svg.arc()
.innerRadius(radius - 85)
.outerRadius(radius);
var svg = d3.select("#pieplate").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
d3.csv("data.csv", type, function(error, data) {
var path = svg.datum(data).selectAll("path")
.data(pie)
.enter().append("path")
.attr("fill", function(d, i) { return color[i]; })
.attr("d", arc)
.each(function(d) { this._current = d; }); // store the initial angles
var text = svg.selectAll("text")
.data(data)
.enter()
.append("text")
.attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; })
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text( function (d) { return d.taskforce1; })
.attr("font-family", "sans-serif")
.attr("font-size", "20px")
.attr("fill", "black");
d3.selectAll("a")
.on("click", switcher);
function switcher() {
var value = this.id;
var j = value + 1;
pie.value(function(d) { return d[value]; }); // change the value function
path = path.data(pie); // compute the new angles
path.transition().duration(750).attrTween("d", arcTween); // redraw the arcs
textLabels = text.text( function (d) { return d[value]; });
}
});
function type(d) {
d.taskforce1 = +d.taskforce1;
d.taskforce2 = +d.taskforce2;
d.taskforce3 = +d.taskforce3;
return d;
}
// Store the displayed angles in _current.
// Then, interpolate from _current to the new angles.
// During the transition, _current is updated in-place by d3.interpolate.
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) {
return arc(i(t));
};
}
Finally got it. The arc.centroid function expects data with precomputed startAngle and endAngle which is the result of pie(data). So the following helped me:
var text = svg.selectAll("text")
.data(pie(data))
followed by the rest of the calls. Note that you might have to change the way to access the text data that you want to display. You can always check it with
// while adding the text elements
.text(function(d){ console.log(d); return d.data.textAttribute })

update angles of multiple pie on map in d3.js

I'm building a very complex visualization with pie charts on a Choropleth world map. You can cycle through the years and the data gets updated. I managed to update the size of the pies but I have difficulties updating the angles because my data structure is quite different from this example: http://bl.ocks.org/mbostock/1346410. I don't know how the data update would work in my case.
Thank you for any help
here is my code to build the pies:
var arc = d3.svg.arc()
(some more code here ...)
function ready(error, world, water_pc,cotton_pc,water_total,cotton_total) {
(some more code here ...)
var pie = d3.layout.pie();
var piegroup = g.selectAll(".pie")
.data(water_pc)
.enter().append("g")
.attr("class","pie")
.attr("transform", function (d) {
return ("translate(" + projection([d.lon,d.lat])[0] + "," +
projection([d.lon,d.lat])[1] + ")");
});
arc
.outerRadius(function (d,i) {
return radius(data_water_pc()[this.parentNode.__data__.id][0]+data_water_pc()[this.parentNode.__data__.id][1]);
})
.innerRadius(0);
var arcs = piegroup.selectAll("path")
.data(function(d) {return pie(data_water_pc()[d.id]); })
.enter().append("svg:path")
.attr("d", arc)
.each(function(d) { this._current = d; }) // store the initial angles
.attr("class","slice")
.style("fill", function(d, i) { return colorWater(i); });
(some more code here ...)
and this is my update function
var update_total = function() {
var piegroup = g.selectAll(".pie")
.transition()
arc
.outerRadius(function (d,i) {
return radius_total(data_water_total()[this.parentNode.__data__.id][0]+data_water_total()[this.parentNode.__data__.id][1]);
})
var arcs = piegroup.selectAll("path")
.transition().duration(150)
.attr("d", arc)
//.attrTween("d", arcTween); // when I use this the size of the pies is not calculated anymore
}
(some more code here ...)
and at the end I have my arc tween function:
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) {
return arc(i(t));
};
}

Animate path (line) from last known point to new added point (d3)

I'm still learning to program and I'm currently trying out the d3 library.
So far I'm pretty happy with the result. fiddle
Q: If you check out the link (or part of the code under this question) you should try to plot a point. This is only possible on the x-axis ticks. You'll see it animates but it's not exactly what I want. I just want it to animate the newly added line. I have checked out .enter() and .append() but I was getting errors. I might be doing something wrong.
function lines(x, y) {
this.x = x;
this.y = y+h;
}
var lineArray = [{x: 0, y: h}, {x: 1, y: h}];
var lineArrayPrevious = lineArray[lineArray.length -1].x;
var d3line = d3.svg.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.interpolate("monotone");
var path = svg.append("path").attr("d", d3line(lineArray)).attr("class", "line");
canPlot = true;
function plot() {
var m = d3.mouse(this);
if (m[0]-20 > lineArray[lineArray.length - 1].x) {
var lineX = lineArray.push(new lines(m[0], m[1]));
svg.selectAll("path")
.data(lineArray)
.attr("d", d3line(lineArray));
var point = svg.append("circle")
.attr("cx", function(d, i) { return m[0]; })
.attr("cy", function(d, i) { return m[1]+h; })
.attr("r", 0).transition().delay(150).attr("r", 6);
var totalLength = path.node().getTotalLength();
console.log();
path.attr("stroke-dasharray", totalLength + " " + totalLength)
.attr("stroke-dashoffset", totalLength)
.transition().duration(700).ease("linear").attr("stroke-dashoffset", 0).delay(200);
canPlot = true;
} else { console.log("error"); canPlot = false; }
}
Excuse my bad code, I'm learning and will clean it up eventually.
Q2: How hard would it be to make a circle that follows the mouse's y-position and moves on the ticks when you get near one?
Q3: If we solve my first question, would it be easy to get the lines to animate/update automatically when we do question 2?
Thanks in advance.
I've updated your jsfiddle here to include the points that you're asking for.
Regarding question 1, I've changed the way the line is drawn such that it can be interpolated from the previous to the current point in a transition. The relevant code is this.
svg.select("path.line")
.attr("d", d3line(lineArray))
.transition().duration(700)
.attrTween('d', pathTween)
.each("end", function() {
var lineX = lineArray.push(new lines(m[0], m[1]));
});
var last = lineArray[lineArray.length-1];
function pathTween() {
var xi = d3.interpolate(last.x, m[0]),
yi = d3.interpolate(last.y, m[1] + h);
return function(t) {
return d3line(lineArray.concat([{x: xi(t), y: yi(t)}]));
};
}
Note that the new data point is only added to the array of points once the transition finishes.
Regarding your second question, this is taken care of by attaching handlers to all tick marks and append a marker on mouse over:
d3.selectAll(".xaxis > .tick").on("mouseenter", mousein)
.on("mousemove", mousemove)
.on("mouseleave", mouseout);
function mousein() {
svg.append("circle").attr("class", "marker").attr("r", 3)
.attr("pointer-events", "none");
}
function mousemove() {
d3.select("circle.marker")
.attr("transform", d3.select(this).attr("transform"))
.attr("cy", d3.mouse(this)[1] + h);
}
function mouseout() {
d3.select("circle.marker").remove();
}

Categories