I'm attempting to upgrade an old v3 chart to v4 and got stuck at trying to append clipPath.
At first I thought that the way to append clip path was changed in v4 somehow but It appears to be the same as before.
My reference is Mike Bostocks chart.
And here's the progress I've made so far.
One thing i've tried was basically to copy/paste the line path logic and change:
line(d.values) to area(d.values)
I get no errors from this so I'm not sure why it isn't working.
The issue was that the y scale was not being calculated within the same environment as the clip path - giving the clip path a height of 0. I moved the clip path to the update function. You can probably get a more elegant update of the clip path, but I added a line at the top to remove the existing clip path so a new one could be appended: https://plnkr.co/edit/KQC1A70b4O5fNtHrQEkn?p=preview
function update() {
d3.selectAll('clipPath').remove();
VALUE = d3.select('#selectbox').property('value');
d3.csv("data.csv", function(d, _, columns) {
d.date = parseDate(d.date);
for (var i = 1, n = columns.length, c; i < n; ++i)
d[c = columns[i]] = +d[c];
return d;
}, function(error, data) {
if (error) throw error;
baseValue = data[0]["Category" + VALUE];
console.log(baseValue)
var keys = data.columns.slice(1,2);
var copy = [];
keys.forEach(function(t) {
t = t.slice(0, -2) // Slice last two letters
copy.push(t) // Push sliced strings into copy array
});
var cities = copy.map(function(id) {
return {
id: id,
values: data.map(function(d) {
return {date: d.date, city: d[id+VALUE] / baseValue};
})
};
});
x.domain(d3.extent(data, function(d) { return d.date; }));
y.domain([
d3.min(cities, function(c) {
return d3.min(c.values, function(d) {
return d.city;
});
}),
d3.max(cities, function(c) {
return d3.max(c.values, function(d) {
return d.city;
});
})
]);
defs.append("clipPath")
.attr("id", "clip-above")
.append("rect")
.attr("width", width)
.attr("height", y(1));
area.y0(y(1));
yAxis.tickValues(d3.scaleLinear()
.domain(y.domain())
.ticks(20));
gY.transition().duration(durations).call(yAxis);
gY.selectAll(".tick")
.classed("tick--one", function(d) { return Math.abs(d - 1) < 1e-6; });
g.selectAll(".axis.axis--x").transition()
.duration(durations)
.call(xAxis);
// ========= Above Clip =========
var above = g.selectAll(".above")
.data(cities);
above = above
.enter()
.append("path")
.attr("clip-path", "url(#clip-above)")
.attr("class", "area area--above above")
.merge(above);
above.transition()
.duration(durations)
.attr("d", function(d) {return area(d.values)} );
// ========= Line Path =========
var cityLine = g.selectAll(".cities")
.data(cities);
cityLine = cityLine
.enter()
.append("path")
.attr("class", "line cities")
.merge(cityLine);
cityLine.transition()
.duration(durations)
.attr("d", function(d) { return line(d.values) });
afterLoad();
});
Related
I created this chart using D3 V5. Also, I have attached the sample data on the fiddle you can view by clicking here.
I've included the tick function code block which appends new domains for x and y scales and line/data on the path to slide left:
When the tick function executes, the line sort of rebuilds which makes it look like it bounces.
How can it be smooth, without a bounce at all when it rebuilds the line?
var tr = d3
.transition()
.duration(obj.tick.duration)
.ease(d3.easeLinear);
function tick() {
return setInterval(function() {
var newData = [];
var tickFunction = obj.tick.fnTickData;
if (tickFunction !== undefined && typeof tickFunction === "function") {
newData = tickFunction();
for (var i = 0; i < newData.length; i++) {
obj.data.push(newData[i]);
}
}
if (newData.length > 0) {
var newMaxDate, newMinDate, newDomainX;
if (isKeyXDate) {
newMaxDate = new Date(
Math.max.apply(
null,
obj.data.map(function(e) {
return new Date(e[obj.dataKeys.keyX]);
})
)
);
newMinDate = new Date(
Math.min.apply(
null,
obj.data.map(function(e) {
return new Date(e[obj.dataKeys.keyX]);
})
)
);
newDomainX = [newMinDate, newMaxDate];
} else {
newDomainX = [
d3.min(obj.data, function(d) {
return d[obj.dataKeys.keyX];
}),
d3.max(obj.data, function(d) {
return d[obj.dataKeys.keyX];
})
];
}
// update the domains
//x.domain([newMinDate, newMaxDate]);
if (obj.tick.updateXDomain) {
newDomainX = obj.tick.updateXDomain;
}
x.domain(newDomainX);
if (obj.tick.updateYDomain) {
y.domain(obj.tick.updateYDomain);
}
path.attr("transform", null);
// slide the line left
if (obj.area.allowArea) {
areaPath.attr("transform", null);
areaPath
.transition()
.transition(tr)
.attr("d", area);
}
path
.transition()
.transition(tr)
.attr("d", line);
svg
.selectAll(".x")
.transition()
.transition(tr)
.call(x.axis);
svg
.selectAll(".y")
.transition()
.transition(tr)
.call(y.axis);
// pop the old data point off the front
obj.data.shift();
}
}, obj.tick.tickDelay);
}
this.interval = tick();
That bounce is actually the expected result when you transition the d attribute, which is just a string.
There are several solutions here. Without refactoring your code too much, a simple one is using the pathTween function written by Mike Bostock in this bl.ocks: https://bl.ocks.org/mbostock/3916621. Here, I'm changing it a little bit so you can pass the datum, like this:
path.transition()
.transition(tr)
.attrTween("d", function(d) {
var self = this;
var thisd = line(d);
return pathTween(thisd, 1, self)()
})
Here is the forked plunker: https://plnkr.co/edit/aAqpdSb9JozwHsErpqa9?p=preview
As Gerardo notes, transitioning the d attribute of the path won't work very well unless you modfiy the approach. Here's a simple example of the sort of wiggle/bouncing that will arise if simply updating the d attribute of the path:
Pᴏɪɴᴛs ᴛʀᴀɴsɪᴛɪᴏɴɪɴɢ ᴀᴄʀᴏss sᴄʀᴇᴇɴ, ᴡɪᴛʜ ᴘᴀᴛʜ ᴛʀᴀɴsɪᴛɪᴏɴɪɴɢ ғʀᴏᴍ ᴏɴᴇ ᴅᴀᴛᴀ sᴇᴛ ᴛᴏ ᴛʜᴇ ɴᴇxᴛ.
The above behavior is noted by Mike Bostock in a short piece here, and here's a snippet reproducing the above animation:
var n = 10;
var data = d3.range(n).map(function(d) {
return {x: d, y:Math.random() }
})
var x = d3.scaleLinear()
.domain(d3.extent(data, function(d) { return d.x; }))
.range([10,490])
var y = d3.scaleLinear()
.range([290,10]);
var line = d3.line()
.x(function(d) { return x(d.x); })
.y(function(d) { return y(d.y); })
var svg = d3.select("body")
.append("svg")
.attr("width",500)
.attr("height", 400)
.append("g");
var path = svg.append("path")
.datum(data)
.attr("d", line);
var points = svg.selectAll("circle")
.data(data, function(d) { return d.x; })
.enter()
.append("circle")
.attr("cx", function(d) { return x(d.x); })
.attr("cy", function(d) { return y(d.y); })
.attr("r", 5);
function tick() {
var transition = d3.transition()
.duration(1000);
var newPoint = {x:n++, y: Math.random() };
data.shift()
data.push(newPoint);
x.domain(d3.extent(data,function(d) { return d.x; }))
points = svg.selectAll("circle").data(data, function(d) { return d.x; })
points.exit()
.transition(transition)
.attr("cx", function(d) { return x(d.x); })
.attr("cy", function(d) { return y(d.y); })
.remove();
points.enter().append("circle")
.attr("cx", function(d) { return x(d.x)+30; })
.attr("cy", function(d) { return y(d.y); })
.merge(points)
.transition(transition)
.attr("cx", function(d) { return x(d.x); })
.attr("r", 5);
path.datum(data)
.transition(transition)
.attr("d", line)
.on("end", tick);
}
tick();
path {
fill: none;
stroke: black;
stroke-width: 2;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
One solution to this wiggle/bounce is:
add an additional point(s) to the data,
redraw the line with the recently added to data array
find out the next extent of the data
transition the line to the left
update the scale and transition the axis
remove the first data point(s)
This is also proposed in Mike's article that I've linked to. Here would be a basic implementation with your code:
I've avoided a setInterval function by recursively calling the function at the end of the last transition:
function slide() {
// Stop any ongoing transitions:
d3.selectAll().interrupt();
// A transition:
var transition = d3.transition()
.duration(2000)
.ease(d3.easeLinear)
// 1. add an additional point(s) to the data
var newData = obj.tick.fnTickData();
obj.data.push(...newData);
// 2. redraw the line with the recently added to data array
path.datum(obj.data)
areaPath.datum(obj.data)
// Redraw the graph, without the translate, with less data:
path.attr("transform","translate(0,0)")
.attr("d", line)
areaPath.attr("transform","translate(0,0)")
.attr("d", area)
// 3. find out the next extent of the data
// Assuming data is in chronological order:
var min = obj.data[newData.length][obj.dataKeys.keyX];
var max = obj.data[obj.data.length-1][obj.dataKeys.keyX];
// 4. transition the line to the left
path.datum(obj.data)
.transition(transition)
.attr("transform", "translate("+(-x(new Date(min)))+",0)");
areaPath.datum(obj.data)
.transition(transition)
.attr("transform", "translate("+(-x(new Date(min)))+",0)");
// 5. update the scale and transition the axis
x.domain([new Date(min),new Date(max)])
// Update the xAxis:
svg.selectAll('.x')
.transition(transition)
.call(x.axis)
.on("end",slide); // Trigger a new transition at the end.
// 6. remove the first data point(s)
obj.data.splice(0,newData.length)
}
slide();
Here's an updated plunkr.
I've uploaded a block (FIXED) where you can toggle a sorting function.
What I want to add now is some kind of if statement when the checkbox is on, and when it is on I want the bars to sort automatically when you change year or category, and when you toggle it again it stops auto-sorting.
I thought a simple
if (document.getElementsByClassName('myCheckbox').checked) {
change();
}
Within the update function would work but nothing happens.
Any help is appreciated!
I started an answer your direct question, but soon realized that your code needed a bit of refactor. You had a bit too much copy/paste going on with redundant code and too many things drawing. When coding with d3 you should try for a single function that does all the drawing.
Here's the code running.
Here's a snippet of the new one update function to rule them all:
function update() {
file = d3.select('#year').property('value') == 'data2017' ? 'data.csv' : 'data2.csv';
catInt = d3.select('#category').property('value');
d3.csv(file, type, function(error,data) {
if(error) throw error;
var sortIndex = data.map(function(d){ return d.month});
// Update domain
y.domain([0, d3.max(data, function(d) {
return d["Category" + catInt]; })
]).nice();
// Update axis
g.selectAll(".axis.axis--y").transition()
.duration(750)
.call(yAxis);
g.selectAll(".axis.grid--y").transition()
.duration(750)
.call(yGrid);
// Sums and averages
let sumOfAll = d3.sum(data, function(d) {
return d["Category" + catInt];
});
let avgValue = d3.sum(data, function(d) {
return d["Category" + catInt];
}) / data.length;
//sort data
data.sort( d3.select("#myCheckbox").property("checked")
? function(a, b) { return b["Category" + catInt] - a["Category" + catInt]; }
: function(a, b) { return sortIndex.indexOf(a.month) - sortIndex.indexOf(b.month);})
// set x domain
x.domain(data.map(function(d) { return d.month; }));
g.selectAll(".axis.axis--x").transition()
.duration(750)
.call(xAxis);
// Update rectangles
let bars = g.selectAll(".barEnter")
.data(data, function(d){
return d.month;
});
bars = bars
.enter()
.append("rect")
.attr("class", "barEnter") // Enter data reference
.attr("width", x.bandwidth())
.merge(bars);
bars.transition()
.duration(750)
.attr("height", function(d) {
return height - y(d["Category" + catInt]);
})
.attr("x", function(d) {
return x(d.month);
})
.attr("y", function(d) {
return y(d["Category" + catInt]);
});
bars.exit().remove();
// Update text on rectangles
let textUpdate = g.selectAll(".textEnter")
.data(data, function(d){
return d.month;
});
textUpdate = textUpdate.enter()
.append("text")
.style("text-shadow","1px 1px #777")
.attr("class", "textEnter") // Enter data reference
.attr("text-anchor","middle")
.attr("font-size",11)
.attr("fill","#fff")
.merge(textUpdate);
textUpdate.transition()
.duration(750)
.attr("y", function(d) {
return y(d["Category" + catInt]) + 15;
})
// Update text value
.text( function(d) {
return d["Category" + catInt];
})
.attr("x", function(d) {
return x(d.month) + x.bandwidth()/2;
})
// Update sum and avg value
g.selectAll("#totalValue").transition()
.duration(750)
.text(sumOfAll + " Category " + catInt)
g.selectAll("#avgValue").transition()
.duration(750)
.text(formatValue(avgValue))
});
}
I am plotting points on a UK map using D3 off a live data stream. When the data points exceed 10,000 the browser becomes sluggish and the animation is no longer smooth. So I modify the dataPoints array to keep only the last 5000 points.
However when I modify the dataPoints the first time using splice() D3 stops rendering any new points. The old points gradually disappear (due to a transition) but there are no new points. I am not sure what I am doing wrong here.
I have simulated the problem by loading data of a CSV as well storing it in memory and plotting them at a rate of 1 point every 100ms. Once the number of dots goes above 10 I splice to retain the last 5 points. I see the same behaviour. Can someone review the code and let me know what I am doing wrong?
Setup and the plotting function:
var width = 960,
height = 1160;
var dataPoints = []
var svg = d3.select("#map").append("svg")
.attr("width", width)
.attr("height", height);
var projection = d3.geo.albers()
.center([0, 55.4])
.rotate([4.4, 0])
.parallels([40, 70])
.scale(5000)
.translate([width / 2, height / 2]);
function renderPoints() {
var points = svg.selectAll("circle")
.data(dataPoints)
points.enter()
.append("circle")
.attr("cx", function (d) {
prj = projection([d.longitude, d.latitude])
return prj[0];
})
.attr("cy", function (d) {
prj = projection([d.longitude, d.latitude])
return prj[1];
})
.attr("r", "4px")
.attr("fill", "blue")
.attr("fill-opacity", ".4")
.transition()
.delay(5000)
.attr("r", "0px")
}
/* JavaScript goes here. */
d3.json("uk.json", function(error, uk) {
if (error) return console.error(error);
console.log(uk);
var subunits = topojson.feature(uk, uk.objects.subunits);
var path = d3.geo.path()
.projection(projection);
svg.selectAll(".subunit")
.data(subunits.features)
.enter().append("path")
.attr("class", function(d) { return "subunit " + d.id })
.attr("d", path);
svg.append("path")
.datum(topojson.mesh(uk, uk.objects.subunits, function(a,b) {return a!== b && a.id !== 'IRL';}))
.attr("d", path)
.attr("class", "subunit-boundary")
svg.append("path")
.datum(topojson.mesh(uk, uk.objects.subunits, function(a,b) {return a=== b && a.id === 'IRL';}))
.attr("d", path)
.attr("class", "subunit-boundary IRL")
svg.selectAll(".place-label")
.attr("x", function(d) { return d.geometry.coordinates[0] > -1 ? 6 : -6; })
.style("text-anchor", function(d) { return d.geometry.coordinates[0] > -1 ? "start": "end"; });
svg.selectAll(".subunit-label")
.data(topojson.feature(uk, uk.objects.subunits).features)
.enter().append("text")
.attr("class", function(d) { return "subunit-label " + d.id })
.attr("transform", function(d) { return "translate(" + path.centroid(d) + ")"; })
.attr("dy", ".35em")
.text(function(d) { return d.properties.name; })
// function applyProjection(d) {
// console.log(d);
// prj = projection(d)
// console.log(prj);
// return prj;
// }
lon = -4.6
lat = 55.45
dataPoints.push([lon,lat])
renderPoints()
});
Function to cleanup old points
var cleanupDataPoints = function() {
num_of_elements = dataPoints.length
console.log("Pre:" + num_of_elements)
if(num_of_elements > 10) {
dataPoints = dataPoints.splice(-5, 5)
}
console.log("Post:" + dataPoints.length)
}
Loading data from CSV and plotting at a throttled rate
var bufferedData = null
var ptr = 0
var renderNext = function() {
d = bufferedData[ptr]
console.log(d)
dataPoints.push(d)
ptr++;
renderPoints()
cleanupDataPoints()
if(ptr < bufferedData.length)
setTimeout(renderNext, 100)
}
d3.csv('test.csv', function (error, data) {
bufferedData = data
console.log(data)
setTimeout(renderNext, 100)
})
In the lines
points = svg.selectAll("circle")
.data(dataPoints)
points.enter() (...)
d3 maps each element in dataPoints (indexed from 0 to 5000) to the circle elements (of which there should be 5000 eventually). So from its point of view, there is no enter'ing data: there are enough circles to hold all your points.
To make sure that the same data point is mapped to the same html element after it changed index in its array, you need to use an id field of some sort attached to each of your data point, and tell d3 to use this id to map the data to elements, instead of their index.
points = svg.selectAll("circle")
.data(dataPoints, function(d){return d.id})
If the coordinates are a good identifier for your point, you can directly use:
points = svg.selectAll("circle")
.data(dataPoints, function(d){return d.longitude+" "+d.latitude})
See https://github.com/mbostock/d3/wiki/Selections#data for more details.
I currently am running a page which generates a graph with default values upon page load. The page takes the data from a TSV generated by a PHP script, modified by GET parameters.
The user can then input options, and update the graph through AJAX.
Currently the page is almost working, but it is is overlaying the new paths with the new data without removing the old paths.
The new data has the same x range and domain but different y coordinate values, sometimes with a different number of values.
Ideally I would like the old paths to fluidly transition from the old paths - how can I make this occur?
I've tried to include the relevant code below. Apologies for its poor quality, I am very new to d3.
...
var line = d3.svg.line()
.interpolate("basis")
.defined(function(d) {
return d.result != 0;
})
.x(function(d) {
return x(d.date);
})
.y(function(d) {
return y(d.result);
});
var svg = d3.select(".chart")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var txtDays = 7;
var txtStartDate = "01/01/2013";
var txtEndDate = "01/01/2014";
var txtInterval = 1;
requestDataURL = //removed for SO
d3.tsv("http://localhost" + requestDataURL, function(error, data) {
var varPolls = d3.keys(data[0]).filter(function(key) {
return key !== "date";
});
data.forEach(function(d) {
d.date = parseDate(d.date);
});
var results = varPolls.map(function(name) {
return {
name: name,
values: data.map(function(d) {
return {
date: d.date,
result: +d[name]
};
})
};
});
x.domain(d3.extent(data, function(d) {
return d.date;
}));
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
var group = svg.selectAll(".group")
.data(results)
.enter().append("g")
.attr("class", "group")
.attr("data-name", function(d) {
return d.name;
});
group.append("path")
.attr("class", "line")
.attr("d", function(d) {
return line(d.values);
})
.style("stroke", function(d) {
return colors[d.name];
});
group.append("text")
.datum(function(d) {
return {
name: d.name,
value: d.values[d.values.length - 1]
};
})
.attr("transform", function(d) {
return "translate(" + x(d.value.date) + "," + y(d.value.result) + ")";
})
.attr("x", 3)
.attr("dy", ".35em")
.text(function(d) {
return Math.round(d.value.result);;
});
d3.select(".submit")
.attr('disabled', null);
});
$(".submit").click(function(event) {
var data = [];
//SORT OUT VALIDATION
var req = $.ajax({
url: requestDataURL,
dataType: 'text',
success: function(response) {
data = response;
}
});
requestDataURL = //new data removed for SO
$.when(req).done(function() {
d3.tsv("http://localhost" + requestDataURL, function(error, data) {
var varPolls = d3.keys(data[0]).filter(function(key) {
return key !== "date";
});
data.forEach(function(d) {
d.date = parseDate(d.date);
});
var results = varPolls.map(function(name) {
return {
name: name,
values: data.map(function(d) {
return {
date: d.date,
result: +d[name]
};
})
};
});
x.domain(d3.extent(data, function(d) {
return d.date;
}));
var group = svg.selectAll(".chart")
.data(results);
group.exit().remove();
group.enter().append("g");
group.attr("class", "group")
.attr("data-name", function(d) {
return d.name;
});
group.append("path")
.attr("class", "line")
.attr("d", function(d) {
return line(d.values);
})
.style("stroke", function(d) {
return colors[d.name];
});
group.transition()
.duration(500)
.ease("linear")
.attr("d", group);
});
});
});
The problem is that you're not handling the enter and update selections correctly. As a rule, append operations should only happen on enter selections, not update. When you're getting new data, you have the following code:
group.enter().append("g");
// ...
group.append("path");
This will append new path elements to the update selection, which is what you're seeing in the graph. The proper way to handle new data would look as follows:
var enterSel = group.enter().append("g");
// set attributes on the g elements
enterSel.append("path"); // append path elements to the new g elements
group.select("path") // select the path elements that are present, this includes the newly appended ones
.attr("d", function(d) { // update the d attribute
return line(d.values);
});
This code will append new elements for data items that have no corresponding elements and update the paths for existing elements.
I created a multi-level pie chart but i am having trouble animate it on load.
Here is the JS that i tryied.The animation works fine on the first circle of the chart , but it hides the other 2.
Any help would be appreciated.Thanks:)
<script>
var dataset = {
final: [7000],
process: [1000, 1000, 1000, 7000],
initial: [10000],
};
var width = 660,
height = 500,
cwidth = 75;
var color = d3.scale.category20();
var pie = d3.layout.pie()
.sort(null);
var arc = d3.svg.arc();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("class","wrapper")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
var gs = svg.selectAll("g.wrapper").data(d3.values(dataset)).enter()
.append("g")
.attr("id",function(d,i){
return Object.keys(dataset)[i];
});
var gsLabels = svg.selectAll("g.wrapper").data(d3.values(dataset)).enter()
.append("g")
.attr("id",function(d,i){
return "label_" + Object.keys(dataset)[i];
});
var count = 0;
var path = gs.selectAll("path")
.data(function(d) { return pie(d); })
.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", function(d, i, j) {
if(Object.keys(dataset)[j] === "final"){
return arc.innerRadius(cwidth*j).outerRadius(cwidth*(j+1))(d);
}
else{
return arc.innerRadius(10+cwidth*j).outerRadius(cwidth*(j+1))(d);
}
})
.transition().delay(function(d, i, j) {
return i * 500;
}).duration(500)
.attrTween('d', function(d,x,y) {
var i = d3.interpolate(d.startAngle+0.1, d.endAngle);
return function(t) {
d.endAngle = i(t);
return arc(d);
}
});
</script>
The main problem is that you're using the same arc generator for all of the different pie segments. That means that after the transition, all the segments will have the same inner and outer radii -- they are there, you just can't see them because they're obscured by the outer blue segment.
To fix this, use different arc generators for the different levels. You also need to initialise the d attribute to zero width (i.e. start and end angle the same) for the animation to work properly.
I've implemented a solution for this here where I'm saving an arc generator for each pie chart segment with the data assigned to that segment. This is a bit wasteful, as a single generator for each level would be enough, but faster to implement. The relevant code is below.
var path = gs.selectAll("path")
.data(function(d) { return pie(d); })
.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", function(d, i, j) {
d._tmp = d.endAngle;
d.endAngle = d.startAngle;
if(Object.keys(dataset)[j] === "final"){
d.arc = d3.svg.arc().innerRadius(cwidth*j).outerRadius(cwidth*(j+1));
}
else{
d.arc = d3.svg.arc().innerRadius(10+cwidth*j).outerRadius(cwidth*(j+1));
}
return d.arc(d);
})
.transition().delay(function(d, i, j) {
return i * 500;
}).duration(500)
.attrTween('d', function(d,x,y) {
var i = d3.interpolate(d.startAngle, d._tmp);
return function(t) {
d.endAngle = i(t);
return d.arc(d);
}
});