I've got a fairly simple reusable chart built in D3.js -- some circles and some text.
I'm struggling to figure out how to update the chart with new data, without redrawing the entire chart.
With the current script, I can see that the new data is bound to the svg element, but none of the data-driven text or attributes is updating. Why isn't the chart updating?
Here's a fiddle: http://jsfiddle.net/rolfsf/em5kL/1/
I'm calling the chart like this:
d3.select('#clusters')
.datum({
Name: 'Total Widgets',
Value: 224,
Clusters: [
['Other', 45],
['FooBars', 30],
['Foos', 50],
['Bars', 124],
['BarFoos', 0]
]
})
.call( clusterChart() );
When the button is clicked, I'm simply calling the chart again, with different data:
$("#doSomething").on("click", function(){
d3.select('#clusters')
.datum({
Name: 'Total Widgets',
Value: 122,
Clusters: [
['Other', 14],
['FooBars', 60],
['Foos', 22],
['Bars', 100],
['BarFoos', 5]
]
})
.call( clusterChart() );
});
The chart script:
function clusterChart() {
var width = 450,
margin = 0,
radiusAll = 72,
maxRadius = radiusAll - 5,
r = d3.scale.linear(),
padding = 1,
height = 3 * (radiusAll*2 + padding),
startAngle = Math.PI / 2,
onTotalMouseOver = null,
onTotalClick = null,
onClusterMouseOver = null,
onClusterClick = null;
val = function(d){return d};
function chart(selection) {
selection.each(function(data) {
var cx = width / 2,
cy = height / 2,
stepAngle = 2 * Math.PI / data.Clusters.length,
outerRadius = 2*radiusAll + padding;
r = d3.scale.linear()
.domain([0, d3.max(data.Clusters, function(d){return d[1];})])
.range([50, maxRadius]);
var svg = d3.select(this).selectAll("svg")
.data([data])
.enter().append("svg");
//enter
var totalCircle = svg.append("circle")
.attr("class", "total-cluster")
.attr('cx', cx)
.attr('cy', cy)
.attr('r', radiusAll)
.on('mouseover', onTotalMouseOver)
.on('click', onTotalClick);
var totalName = svg.append("text")
.attr("class", "total-name")
.attr('x', cx)
.attr('y', cy + 16);
var totalValue = svg.append("text")
.attr("class", "total-value")
.attr('x', cx)
.attr('y', cy + 4);
var clusters = svg.selectAll('circle.cluster')
.data(data.Clusters)
.enter().append('circle')
.attr("class", "cluster");
var clusterValues = svg.selectAll("text.cluster-value")
.data(data.Clusters)
.enter().append('text')
.attr('class', 'cluster-value');
var clusterNames = svg.selectAll("text.cluster-name")
.data(data.Clusters)
.enter().append('text')
.attr('class', 'cluster-name');
clusters .attr('cx', function(d, i) { return cx + Math.cos(startAngle + stepAngle * i) * outerRadius; })
.attr('cy', function(d, i) { return cy + Math.sin(startAngle + stepAngle * i) * outerRadius; })
.attr("r", "10")
.on('mouseover', function(d, i, j) {
if (onClusterMouseOver != null) onClusterMouseOver(d, i, j);
})
.on('mouseout', function() { /*do something*/ })
.on('click', function(d, i){ onClusterClick(d); });
clusterNames
.attr('x', function(d, i) { return cx + Math.cos(startAngle + stepAngle * i) * outerRadius; })
.attr('y', function(d, i) { return cy + Math.sin(startAngle + stepAngle * i) * outerRadius + 16; });
clusterValues
.attr('x', function(d, i) { return cx + Math.cos(startAngle + stepAngle * i) * outerRadius; })
.attr('y', function(d, i) { return cy + Math.sin(startAngle + stepAngle * i) * outerRadius - 4; });
//update with data
svg .selectAll('text.total-value')
.text(val(data.Value));
svg .selectAll('text.total-name')
.text(val(data.Name));
clusters
.attr('class', function(d, i) {
if(d[1] === 0){ return 'cluster empty'}
else {return 'cluster'}
})
.attr("r", function (d, i) { return r(d[1]); });
clusterValues
.text(function(d) { return d[1] });
clusterNames
.text(function(d, i) { return d[0] });
$(window).resize(function() {
var w = $('.cluster-chart').width(); //make this more generic
svg.attr("width", w);
svg.attr("height", w * height / width);
});
});
}
chart.width = function(_) {
if (!arguments.length) return width;
width = _;
return chart;
};
chart.onClusterClick = function(_) {
if (!arguments.length) return onClusterClick;
onClusterClick = _;
return chart;
};
return chart;
}
I have applied the enter/update/exit pattern across all relevant svg elements (including the svg itself). Here is an example segment:
var clusterValues = svg.selectAll("text.cluster-value")
.data(data.Clusters,function(d){ return d[1];});
clusterValues.exit().remove();
clusterValues
.enter().append('text')
.attr('class', 'cluster-value');
...
Here is a complete FIDDLE with all parts working.
NOTE: I tried to touch your code as little as possible since you have carefully gone about applying a re-usable approach. This the reason why the enter/update/exit pattern is a bit different between the total circle (and text), and the other circles (and text). I might have gone about this using a svg:g element to group each circle and associated text.
Related
I have a D3 chart but only want to show 3 ticks for the y-axis.
Depending on the data, I sometimes get 3, 4 or 5 ticks which makes it difficult for me to style with CSS.
Here is the full code:
// Create a new d3
var chart = d3.select('#analytics-chart').append('div').attr('class', 'chart');
chart.append('div').attr('class', 'y-axis');
chart.append('div').attr('class', 'bars-and-x-axis');
var barMargin = '0 2px',
min = 0,
max = d3.max(data, function(d) {
return parseInt(d.value, 10);
});
var bars = d3.selectAll('.bars-and-x-axis').append('div').attr('class', 'bars'),
xaxis = d3.selectAll('.bars-and-x-axis').append('div').attr('class', 'x-axis'),
yaxis = d3.selectAll('.y-axis'),
xScale = d3.scale.linear().domain([1, data.length]),
yScale = d3.scale.linear().range(0, 100).domain([min, max]),
barWrapper = bars.selectAll()
.data(data.map(function(d) {
return d.value;
}))
.enter()
.append('div')
.attr('class', function(d, i) {
if (d == 0) {
return 'chart-data-wrapper empty';
} else {
return 'chart-data-wrapper';
}
}).style('margin', barMargin);
var bar = barWrapper.append('div').attr('class', 'chart-data-bar')
.style('height', function(d) {
return Math.ceil((d - min) / (max - min) * 100) + 'px';
})
.append('div')
.attr('class', 'tooltip')
.attr('style', function(d, i) {
return 'left: ' + Math.ceil(i / data.length * 100) + '%; transform: translateX(-' + Math.ceil(i / data.length * 100) + '%); ';
})
.append('p')
.text(function(d, i) {
return data[i].date;
})
.append('p')
.attr('class', 'data')
.text(function(d, i) {
return data[i].content;
});
xaxis.selectAll()
.data(xScale.ticks(12))
.enter()
.append('div')
.attr('class', 'x-axis-mark');
yaxis.selectAll()
.data(yScale.ticks(3))
.enter()
.insert('small', ':first-child')
.attr('class', 'label')
.text(function(d, i) {
if (d > 999) {
d = d / 1000 + 'k';
}
return d;
});
var tick = d3.selectAll('.x-axis-mark')
.append('div')
.attr('class', function(d, i) {
if (i % 3 == 1) {
return 'x-axis-tick-with-label';
} else {
return 'x-axis-tick';
}
});
var label = d3.selectAll('.x-axis-mark')
.append('small')
.attr('class', 'label')
.text(function(d, i) {
var format = d3.time.format('%b');
return data[i].xlabel;
})
.attr('class', function(d, i) {
if (i % 3 != 1) {
d3.select(this).remove();
}
});
Edit - I've added the full code and attached an image of the working example below:
I was able to replace yScale.ticks(3) in .data(yScale.ticks(3)) with my own math function that returns an array of values to create the tick values.
I've been looking at this example of a beeswarm plot in d3.js and I'm trying to figure out how to change the size of the dots and without getting the circles to overlap. It seems if the radius of the dots change, it doesn't take this into account when running the calculations of where to place the dots.
This is a cool visualization.
I've made a plunk of it here: https://plnkr.co/edit/VwyXfbc94oXp6kXQ7JFx?p=preview and modified it to work a bit more like you're looking for (I think). The real key is changing the call to handle collision to vary based on the radius of the circles (in the original post it's hard coded to 4, which works well when r === 3 but fails as r grows). The changes:
Make the circle radius into a variable (line 7 of script.js, var r = 3;)
Change the d3.forceCollide call to use that radius and a multiplier - line 110 (.force("collide", d3.forceCollide(r * 1.333)))
Change the .enter() call to use that radius as well (line 130: .attr("r", r))
This works reasonably well for reasonable values of r - but you'll need to adjust the height, and it might even be nice to just change the whole thing so that r is based on height (e.g. var r = height * .01). You'll notice that as is now, the circles go off the bottom and top of the graph area.
This post might be of interest as well: Conflict between d3.forceCollide() and d3.forceX/Y() with high strength() value
Here's the whole of script.js for posterity:
var w = 1000, h = 280;
var padding = [0, 40, 34, 40];
var r = 5;
var xScale = d3.scaleLinear()
.range([ padding[3], w - padding[1] ]);
var xAxis = d3.axisBottom(xScale)
.ticks(10, ".0s")
.tickSizeOuter(0);
var colors = d3.scaleOrdinal()
.domain(["asia", "africa", "northAmerica", "europe", "southAmerica", "oceania"])
.range(['#e41a1c','#377eb8','#4daf4a','#984ea3','#ff7f00','#ffff33']);
d3.select("#africaColor").style("color", colors("africa"));
d3.select("#namericaColor").style("color", colors("northAmerica"));
d3.select("#samericaColor").style("color", colors("southAmerica"));
d3.select("#asiaColor").style("color", colors("asia"));
d3.select("#europeColor").style("color", colors("europe"));
d3.select("#oceaniaColor").style("color", colors("oceania"));
var formatNumber = d3.format(",");
var tt = d3.select("#svganchor").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var svg = d3.select("#svganchor")
.append("svg")
.attr("width", w)
.attr("height", h);
var xline = svg.append("line")
.attr("stroke", "gray")
.attr("stroke-dasharray", "1,2");
var chartState = {};
chartState.variable = "totalEmission";
chartState.scale = "scaleLinear";
chartState.legend = "Total emissions, in kilotonnes";
d3.csv("co2bee.csv", function(error, data) {
if (error) throw error;
var dataSet = data;
xScale.domain(d3.extent(data, function(d) { return +d.totalEmission; }));
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (h - padding[2]) + ")")
.call(xAxis);
var legend = svg.append("text")
.attr("text-anchor", "middle")
.attr("x", w / 2)
.attr("y", h - 4)
.attr("font-family", "PT Sans")
.attr("font-size", 12)
.attr("fill", "darkslategray")
.attr("fill-opacity", 1)
.attr("class", "legend");
redraw(chartState.variable);
d3.selectAll(".button1").on("click", function(){
var thisClicked = this.value;
chartState.variable = thisClicked;
if (thisClicked == "totalEmission"){
chartState.legend = "Total emissions, in kilotonnes";
}
if (thisClicked == "emissionPerCap"){
chartState.legend = "Per Capita emissions, in metric tons";
}
redraw(chartState.variable);
});
d3.selectAll(".button2").on("click", function(){
var thisClicked = this.value;
chartState.scale = thisClicked;
redraw(chartState.variable);
});
d3.selectAll("input").on("change", filter);
function redraw(variable){
if (chartState.scale == "scaleLinear"){ xScale = d3.scaleLinear().range([ padding[3], w - padding[1] ]);}
if (chartState.scale == "scaleLog"){ xScale = d3.scaleLog().range([ padding[3], w - padding[1] ]);}
xScale.domain(d3.extent(dataSet, function(d) { return +d[variable]; }));
var xAxis = d3.axisBottom(xScale)
.ticks(10, ".0s")
.tickSizeOuter(0);
d3.transition(svg).select(".x.axis").transition().duration(1000)
.call(xAxis);
var simulation = d3.forceSimulation(dataSet)
.force("x", d3.forceX(function(d) { return xScale(+d[variable]); }).strength(2))
.force("y", d3.forceY((h / 2)-padding[2]/2))
.force("collide", d3.forceCollide(r * 1.333))
.stop();
for (var i = 0; i < dataSet.length; ++i) simulation.tick();
var countriesCircles = svg.selectAll(".countries")
.data(dataSet, function(d) { return d.countryCode});
countriesCircles.exit()
.transition()
.duration(1000)
.attr("cx", 0)
.attr("cy", (h / 2)-padding[2]/2)
.remove();
countriesCircles.enter()
.append("circle")
.attr("class", "countries")
.attr("cx", 0)
.attr("cy", (h / 2)-padding[2]/2)
.attr("r", r)
.attr("fill", function(d){ return colors(d.continent)})
.merge(countriesCircles)
.transition()
.duration(2000)
.attr("cx", function(d) { console.log(d); return d.x; })
.attr("cy", function(d) { return d.y; });
legend.text(chartState.legend);
d3.selectAll(".countries").on("mousemove", function(d) {
tt.html("Country: <strong>" + d.countryName + "</strong><br>"
+ chartState.legend.slice(0, chartState.legend.indexOf(",")) + ": <strong>" + formatNumber(d[variable]) + "</strong>" + chartState.legend.slice(chartState.legend.lastIndexOf(" ")))
.style('top', d3.event.pageY - 12 + 'px')
.style('left', d3.event.pageX + 25 + 'px')
.style("opacity", 0.9);
xline.attr("x1", d3.select(this).attr("cx"))
.attr("y1", d3.select(this).attr("cy"))
.attr("y2", (h - padding[2]))
.attr("x2", d3.select(this).attr("cx"))
.attr("opacity", 1);
}).on("mouseout", function(d) {
tt.style("opacity", 0);
xline.attr("opacity", 0);
});
d3.selectAll(".x.axis, .legend").on("mousemove", function(){
tt.html("This axis uses SI prefixes:<br>m: 10<sup>-3</sup><br>k: 10<sup>3</sup><br>M: 10<sup>6</sup>")
.style('top', d3.event.pageY - 12 + 'px')
.style('left', d3.event.pageX + 25 + 'px')
.style("opacity", 0.9);
}).on("mouseout", function(d) {
tt.style("opacity", 0);
});
//end of redraw
}
function filter(){
function getCheckedBoxes(chkboxName) {
var checkboxes = document.getElementsByName(chkboxName);
var checkboxesChecked = [];
for (var i=0; i<checkboxes.length; i++) {
if (checkboxes[i].checked) {
checkboxesChecked.push(checkboxes[i].defaultValue);
}
}
return checkboxesChecked.length > 0 ? checkboxesChecked : null;
}
var checkedBoxes = getCheckedBoxes("continent");
var newData = [];
if (checkedBoxes == null){
dataSet = newData;
redraw();
return;
};
for (var i = 0; i < checkedBoxes.length; i++){
var newArray = data.filter(function(d){
return d.continent == checkedBoxes[i];
});
Array.prototype.push.apply(newData, newArray);
}
dataSet = newData;
redraw(chartState.variable);
//end of filter
}
//end of d3.csv
});
I am trying to create a radar chart similar to the link here (
http://www.larsko.org/v/euc/).
I was able to create axes (my work so far), but I am having a problem to draw lines in it.
For instance, if I have a list of values something like below, how can I draw a line in the radar chart?
var tempData = [56784, 5.898, 3417, 0, 0, 0]
Edit: I have included code. I am having a problem finding XY coordinates and I think XY value has to be derived from "scales".
var width = 1000,
height = 960,
r = (960 / 2) - 160;
var svg = d3.select("#radar")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + ", " + height / 2 + ")");
d3.csv("data/results.csv", function(data) {
var headerNames = d3.keys(data[0]);
headerNames.splice(0, 1); //Remove 'scenario'
var minList = $.map(headerNames, function(h) {
return d3.min($.map(data, function(d) {
return d[h];
}));
}),
maxList = $.map(headerNames, function(h) {
return d3.max($.map(data, function(d) {
return d[h];
}));
}),
scales = $.map(headerNames, function(h, i) {
return d3.scale.linear()
.domain([minList[i], maxList[i]])
.range([50, r]);
}),
axes = $.map(headerNames, function(h, i) {
return d3.svg.axis()
.scale(scales[i])
.tickSize(4);
});
function angle(i) {
return i * (2 * Math.PI / headerNames.length) + Math.PI / headerNames.length;
}
var line = d3.svg.line()
.interpolate("cardinal-closed")
/* computing X and Y: I am having a problem here
.x(function(d){ return scales(d); })
.y(function(d){ return scales(d); }); */
$.each(axes, function(i, a) {
svg.append("g")
.attr("transform", "rotate(" + Math.round(angle(i) * (180 / Math.PI)) + ")")
.call(a)
.selectAll("text")
.attr("text-anchor", "middle")
.attr("transform", function(d) {
return "rotate(" + -angle(i) * (180 / Math.PI) + ")";
})
//Drawing line
svg.selectAll(".layer")
.data(data)
.enter()
.append("path")
.attr("class", "layer")
.attr("d", function(d) {
return line(d);
})
}) // End CSV
Example results.csv
scenario,n_dead_oaks,percent_dead_oaks,infected_area_ha,money_spent,area_treated_ha,price_per_oak
baseline,56784,5.898,3417,0,0,0
scen2,52725,5.477,3294,382036,35,94.12071939
RS_1,58037,6.028,3407,796705,59,-635.8379888
RS_2,33571,3.487,2555,1841047,104,79.31103261
RS_3,46111,4.79,2762,1176461,61,110.227771
As Squeegy suggested, you should share some code showing your current progress and how you have achieved to create the axes.
Anyways, this is how I would go about this:
For a given list of values that you want to represent as a line, find the [x,y] coordinates of every point of the line, i.e. place your data-points on each axis. If you have a scale system in place already to draw your axes, this shouldn't be too hard.
Use d3.svg.line to draw a line that goes through all these points.
The code would end up looking like this:
var tempData = [56784, 5.898, 3417, 0, 0, 0];
/** compute tempPoints from tempData **/
var tempPoints = [[123, 30], [12, 123], [123, 123], [0,0], [0,0], [0,0]];
var line = d3.svg.line();
d3.select('svg').append('path').attr('d', line(tempPoints) + 'Z'); // the trailing Z closes the path
I think I have a solution for now and I appreciate all of your response! Here is my current solution for my posting.
function getRowValues(data) {
return $.map(data, function(d, i) {
if (i != "scenario") {
return d;
}
});
}
function getCoor(data) {
console.log(data);
var row = getRowValues(data),
x,
y,
coor = [];
for (var i = 0; i < row.length; i++) {
x = Math.round(Math.cos(angle(i)) * scales[i](row[i]));
y = Math.round(Math.sin(angle(i)) * scales[i](row[i]));
coor.push([x, y]);
}
return coor;
}
var line = d3.svg.line()
.interpolate("cardinal-closed")
.tension(0.85);
svg.selectAll(".layer")
.data(data)
.enter()
.append("path")
.attr("class", "layer")
.attr("d", function(d) { return line(getCoor(d)) + "Z"; })
.style("stroke", function(d, i){ return colors[i]; })
.style("fill", "none");
I've built a chart with a series of "small multiples", each representing a percentage of completion, arranged in rows and columns. Each row has a different number that represents "complete".
You can view the chart in this fiddle: http://jsfiddle.net/rolfsf/Lhnm9a9m/
I'm using transitions to grow the bars from 0 to x% width.
I also want to use a transition to show the actual count of each measure (e.g. 10 out of 17) incrementing from 0.
I've got all the numbers incrementing from 0 using a text tween function, but all of the measures stop at the same count, rather than their correct count.
I must be either using the wrong data in the tween, or placing the tween in the wrong part of the script... but I can't figure out where the problem is.
How do I get the numbers to increment properly??
My data looks like this:
var sets = [
{"title": "Set-1", "count": 17, "measures": [10, 13, 16, 14]},
{"title": "Set-2", "count": 23, "measures": [12, 18, 19, 23]},
{"title": "Set-3", "count": 25, "measures": [19, 22, 23, 20]},
{"title": "Set-4", "count": 4, "measures": [4, 4, 4, 4]},
{"title": "Set-5", "count": 8, "measures": [5, 7, 8, 6]}
];
The chart is called like this:
d3 .select('#overview-graph')
.datum(sets)
.call(relativeCompletionChart()
//options
);
and here's the reusable chart script:
function relativeCompletionChart() {
var width = 1200,
margin = {top: 16, right: 16, bottom: 16, left: 16},
onSetMouseOver = null,
onSetClick = null;
function chart(selection) {
selection.each(function(data) {
var titleColWidth = 0.3*width,
setHeight = 24,
barHeight = 22,
setCount = data.length,
colCount = data[0].measures.length,
colWidth = (width - titleColWidth)/colCount,
rangeWidth = colWidth - 4,
height = ((setHeight * setCount) + margin.top + margin.bottom);
var svg = d3.select(this)
.append("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", "0 0 " + width + " " + height )
.attr("preserveAspectRatio", "xMidYMin meet");
svg .append('rect')
.attr("x", 0)
.attr('y', 0)
.attr('height', height)
.attr('width', width)
.attr('class', 'chart-bg');
/**
* Tween functions
*/
function tweenText( newValue ) {
return function() {
// get current value as starting point for tween animation
var currentValue = +this.textContent;
// create interpolator and do not show nasty floating numbers
var i = d3.interpolateRound( currentValue, newValue );
return function(t) {
this.textContent = i(t);
};
}
}
function update(data) {
var set = svg.selectAll("g.set")
.data(data);
set.exit().remove();
var setEnter = set
.enter().append("g")
.attr("class", "set")
.attr('transform', function (d, i) {
return 'translate(0, ' + (margin.top + i*setHeight) + ')';
});
set.append("text")
.attr("class", "title")
.attr("x", (titleColWidth - 80))
.attr("y", 16)
.attr("text-anchor", "end")
.text(function (d){return d.title;});
set.append("text")
.attr("class", "count")
.attr("x", (titleColWidth - 32))
.attr("y", 16)
.attr("text-anchor", "end")
.text(function (d){return d.count;});
var ranges = set.selectAll("rect.range")
.data(function(d, i){return d.measures})
.enter().append('rect')
.attr("class", "range")
.attr("x",2)
.attr("y",0)
.attr('rx', 2)
.attr('ry', 2)
.attr("width", rangeWidth)
.attr("height",barHeight)
.attr("fill", "#CCCCCC")
.attr('transform', function (d, i) {
return 'translate(' + (titleColWidth + i*colWidth) + ', 0)';
});
var measures = set.selectAll("rect.measure")
.data(function(d, i){return d.measures})
.enter().append('rect')
.attr("class", "measure")
.attr('rx', 2)
.attr('ry', 2)
.attr("x",2)
.attr("y",0)
.attr("width", 1)
.attr("height",barHeight)
.attr('transform', function (d, i) {
return 'translate(' + (titleColWidth + i*colWidth) + ', 0)';
});
var markers = set.selectAll("line.marker")
.data(function(d, i){return d.measures})
.enter().append('line')
.attr("class", "marker")
.attr('x1', 2)
.attr('y1', 0)
.attr("x2",2)
.attr("y2",barHeight)
.attr('transform', function (d, i) {
return 'translate(' + (titleColWidth + i*colWidth) + ', 0)';
});
var values = set.selectAll("text.value")
.data(function(d, i){return d.measures})
.enter().append('text')
.text('0')
.attr("class", "value")
.attr("x", 8)
.attr("y", 16)
.attr("text-anchor", "start")
.attr('transform', function (d, i) {
return 'translate(' + (titleColWidth + i*colWidth) + ', 0)';
});
//update widths
set.selectAll("rect.measure")
.transition()
.duration(1000)
.delay(function(d, i) { return i * 20; })
.attr("width", function(d, i) { return d3.round((d/d3.select(this.parentNode).datum().count)*rangeWidth);});
set.selectAll("line.marker")
.transition()
.duration(1000)
.delay(function(d, i) { return i * 20; })
.attr("x1", function(d, i) { return d3.round((d/d3.select(this.parentNode).datum().count)*rangeWidth + 1);})
.attr("x2", function(d, i) { return d3.round((d/d3.select(this.parentNode).datum().count)*rangeWidth + 1);});
set.selectAll('text.value')
.transition()
.duration(1000)
.tween( 'text', function() {
// get current value as starting point for tween animation
var currentValue = +this.textContent;
// create interpolator and do not show nasty floating numbers
var interpolator = d3.interpolateRound( currentValue, 10 );
// this returned function will be called a couple
// of times to animate anything you want inside
// of your custom tween
return function( t ) {
// set new value to current text element
this.textContent = interpolator(t) + '/' + d3.select(this.parentNode).datum().count;
};
});
}
update(data);
});
}
chart.width = function(_) {
if (!arguments.length) return width;
width = _;
return chart;
};
chart.onSetClick = function(_) {
if (!arguments.length) return onSetClick;
onSetClick = _;
return chart;
};
chart.onSetMouseOver = function(_) {
if (!arguments.length) return onSetMouseOver;
onSetMouseOver = _;
return chart;
};
return chart;
}
The relevant code for the tweening is pulled out here:
set.selectAll('text.value')
.transition()
.duration(1000)
.tween( 'text', function() {
// get current value as starting point for tween animation
var currentValue = +this.textContent;
// create interpolator and do not show nasty floating numbers
var interpolator = d3.interpolateRound( currentValue, 10 );
// this returned function will be called a couple
// of times to animate anything you want inside
// of your custom tween
return function( t ) {
// set new value to current text element
this.textContent = interpolator(t) + '/' + d3.select(this.parentNode).datum().count;
};
});
Though I've also got an unused helper function in the script that I couldn't get to work:
/**
* Tween functions
*/
function tweenText( newValue ) {
return function() {
// get current value as starting point for tween animation
var currentValue = +this.textContent;
// create interpolator and do not show nasty floating numbers
var i = d3.interpolateRound( currentValue, newValue );
return function(t) {
this.textContent = i(t);
};
}
}
I've got a simple donut chart in d3.js, which will only be used to compare 2 or 3 items.
http://jsfiddle.net/Ltqu2/
I want to combine the legend and values as text in the center of the chart.
In the current implementation, the text alignment is ok for 3 items, but for 2 items it doesn't adjust. The alignment is somewhat hard coded:
var l = svg.selectAll('.legend')
.data(data)
.enter().append('g')
.attr('class', 'legend');
l.append('text')
.attr('x', 0)
.attr('y', function(d, i) { return i * 40 - radius / 2 + 10; })
.attr('class', function(d, i){return 'legend-label data-label value-' + (i+1)})
.text(function(d, i) { return d + '%'; });
l.append('text')
.attr('x', 0)
.attr('y', function(d, i) { return i * 40 - radius / 2 + 22; })
.attr('class', function(d, i){return 'legend-label units-label value-' + (i+1)})
.text(function(d, i) { return legend[i]; });
How can I make the text alignment more flexible, so it distributes evenly for both 2 and 3 items? Is there something I can use .rangeround for?
Here's the full script
/**
* Donut chart for d3.js
*/
function donutChart() {
var width = 420,
height = 420,
radius = 0,
factor = 0.7;
var legend = ['Low', 'Medium', 'High'];
function chart(selection) {
selection.each(function(data) {
if (radius == 0) {
radius = Math.min(width, height) / 2 - 10;
}
var arc = d3.svg.arc()
.innerRadius(radius * factor)
.outerRadius(radius);
var pie = d3.layout.pie()
.sort(null)
.value(function(d) { return d; });
var svg = d3.select(this).append('svg')
.attr('width', width)
.attr('height', height)
.attr('class', 'donut')
.append('g')
.attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');
var g = svg.selectAll('.arc')
.data(pie(data))
.enter().append('g')
.attr('class', 'arc');
g.append('path')
.attr('d', arc)
.attr('class', function(d, i){return 'value-' + (i+1)})
.style('stroke', '#fff');
var l = svg.selectAll('.legend')
.data(data)
.enter().append('g')
.attr('class', 'legend');
l.append('text')
.attr('x', 0)
.attr('y', function(d, i) { return i * 40 - radius / 2 + 10; })
.attr('class', function(d, i){return 'legend-label data-label value-' + (i+1)})
.text(function(d, i) { return d + '%'; });
l.append('text')
.attr('x', 0)
.attr('y', function(d, i) { return i * 40 - radius / 2 + 22; })
.attr('class', function(d, i){return 'legend-label units-label value-' + (i+1)})
.text(function(d, i) { return legend[i]; });
});
}
chart.width = function(_) {
if (!arguments.length) return width;
width = _;
return chart;
};
chart.height = function(_) {
if (!arguments.length) return height;
height = _;
return chart;
};
chart.radius = function(_) {
if (!arguments.length) return radius;
radius = _;
return chart;
};
chart.factor = function(_) {
if (!arguments.length) return factor;
factor = _;
return chart;
};
chart.legend = function(_) {
if (!arguments.length) return legend;
legend = _;
return chart;
};
return chart;
}
d3.select('#chart')
.datum([78, 20])
.call(donutChart()
.width(220)
.height(220)
.legend(['This Thing', 'That Thing'])
);
d3.select('#chart2')
.datum([63, 20, 15])
.call(donutChart()
.width(220)
.height(220)
.legend(['This Thing', 'That Thing', 'Another Thing'])
);
I would put all the legend text elements in a common <g> element which can then be translated to center vertically. Determining actual text sizes in SVG is always a pain, you can look into getBBox() for more information on that. But using a predetermined text height will work fine. Here's a bit of calculation (separated into many steps for clarity):
// hardcoding 36 here, you could use getBBox to get the actual SVG text height using sample text if you really wanted.
var legendItemHeight = 36;
// calculated height of all legend items together
var actualLegendHeight = data.length * legendItemHeight;
// inner diameter
var availableLegendHeight = radius * factor * 2;
// y-coordinate of first legend item (relative to the center b/c the main svg <g> element is translated
var legendOffset = (availableLegendHeight - actualLegendHeight) / 2 - (radius*factor);
And then create a <g> element to hold the <text> elements:
// append all the legend items to a common group which is translated
var l = svg.selectAll('.legend')
.data(data)
.enter().append('g')
.attr('class', 'legend')
.attr('transform', function(d, i) {
return 'translate(0, '+ (legendOffset + i * legendHeight)+')';
});
Modified jsFiddle: http://jsfiddle.net/rshKs/2/