/**
* With this demo, you are able to get the idea of transition and intepolate with D3
*/
// the 3 sets of data we would like to visualize
var data = [1, 1, 1, 6, 7, 8, 5, 5, 20, 25, 9, 8, 2, 2, 2, 20];
var theStatusNameMapping = ['Running', 'Stopping', 'Paused', 'Halt', 'Terminated', 'Stopped', 'Bootstraping', 'InSync', 'Out-Of-Sync', 'Impaired', 'Degraded', 'Unreachabled', 'Destroyed', 'Error1', 'Error2', 'Error3', 'Error4']
var dataScaleName = d3.scale.ordinal()
.range(theStatusNameMapping)
var arcOuterRadius = 130;
var arcTextOffsetTimes = 1.3;
var textMinSpaceHeight = 13;
var charMinSpaceWidth = 10; // px
var pathStrokeWidth = 5;
// surely, create a svg workspace first
var theSvgG = d3.select('body').append('svg').attr({
width: 600,
height: 600
}).append('g').attr('transform', 'translate(300, 300)');
// the color of the 3 annuluses we will assign later
var colorScale = d3.scale.category20();
var pieLayoutData = d3.layout.pie().sort(null);
// the arc Path d generator
var arc = d3.svg.arc()
.innerRadius(100)
.outerRadius(arcOuterRadius)
// we will generate startAngle value based on last sector's endAngle
.startAngle(function (d, index) {
return d.startAngle
})
// its startAngle + its radian = its endAngle
.endAngle(function (d, index) {
return d.endAngle
});
// our main utils/data handler class
var DataHandler = function () {
// let's follow CommonJS a little bit
var exports = this;
exports.data = [];
var percentageOfCircle = [];
var endAngleOf = [];
var updateDatum = function (datum, attr, newValue) {
datum[attr] = newValue;
}
var pieData = function (i, value) {
if (typeof value === 'undefined') {
return exports.data [i];
}
exports.data [i] = value;
}
// sum the array elements as a single circle radian
var sumData = function(){
return exports.data .reduce(function (a, b) {
return a + b;
});
}
/**
* percentage of each array element
* @param i Int this is the index of the given element of the array
*/
var percentageOfCircle = function (i) {
return percentageOfCircle[i] || (percentageOfCircle[i] = exports.data [i] / sumData())
}
// store end angle for all array elements
var endAngleOfDataI = function (i, value) {
if (typeof (value) !== 'undefined') {
endAngleOf[i] = value;
} else {
return endAngleOf[i] || 0
}
}
// get radian of an array element
var getRadianByDataI = function (index, radian) {
if (typeof radian === 'undefined') {
// if param radian is not provided, we assume it's going to get its percentage of whole circle
return 2 * Math.PI * percentageOfCircle(index)
}
return radian * percentageOfCircle(index)
}
// init data
var initData = function (data) {
exports.data = pieLayoutData(data);
percentageOfCircle = [];
endAngleOf = [];
// calc text x and y attr and store back to data
var updateTextOffset = function (datum, index) {
var offset;
// adjust text anchor
var textAnchor = function (textObj) {
textObj.textAnchor = datum.textObj.x > 0 ? 'start' : 'end';
}
offset = arc.centroid(datum)
updateDatum(datum.textObj, 'x', offset[0] * arcTextOffsetTimes);
updateDatum(datum.textObj, 'y', offset[1] * arcTextOffsetTimes);
textAnchor(datum.textObj);
}
exports.data.forEach(function (e, i) {
e.textObj = e.textObj || {};
updateTextOffset(e, i)
});
// reposition label to prevent overlapping
var reduceOverlappingOfTextLabel = function (data) {
data.forEach(function (datum, index, array) {
// if current (x, y) is too closed to previous (x, y)
// then we adjust current x, y
if (index === 0) {
return;
}
var x1 = datum.textObj.x;
var y1 = datum.textObj.y;
var x0 = array[index - 1 ].textObj.x;
var y0 = array[index - 1 ].textObj.y;
var previousYChang = array[index - 1].textObj.yChange;
if (previousYChang) {
y1 += previousYChang;
// datum.textObj.yChange = previousYChang;
// datum.textObj.x *= (1 + datum.textObj.yChange/datum.textObj.y) ;
// datum.textObj.y = y1;
}
var xD = x1 - x0;
var yD = y1 - y0;
var textSpace = dataScaleName(index).length * charMinSpaceWidth;
if (Math.abs(yD) < textMinSpaceHeight) {
if (Math.abs(xD) < textSpace) {
y1 = y0 - textMinSpaceHeight;
datum.textObj.yChange = datum.textObj.yChange || 0;
datum.textObj.yChange += y1 - datum.textObj.y;
// datum.textObj.x *= (1 + datum.textObj.yChange/datum.textObj.y) ;
datum.textObj.y = y1;
}
}
});
}
reduceOverlappingOfTextLabel(exports.data)
};
exports = {
endAngleOfDataI: endAngleOfDataI,
getRadianByDataI: getRadianByDataI,
percentageOfCircle: percentageOfCircle,
pieData: pieData,
initData: initData,
updateData: initData,
getData: function(){
return exports.data;
}
}
return exports;
}
// instantiate
var dataHandler = new DataHandler();
dataHandler.updateData(data);
var updatePath = function(theDonutG) {
theDonutG.selectAll('path').remove();
theDonutG.selectAll('text').remove();
// append a Path into each group
theDonutG.append('path')
.attr({
'stroke': '#ff00ff',
'stroke-width': pathStrokeWidth,
'fill': 'white'
})
.attr('d', function (d, i) {
return arc(d, i)
})
.transition()
.delay(2000)
.duration(2000)
// dynamically change Path d with d3 interpolate
.attrTween('d', function (d, index) {
// get the interpolator to generate value from 0 to 2 PI radian
// in accordance to the t (from 0 to 1) passed in later
var i = d3.interpolateObject({startAngle: 0, endAngle: 0}, {startAngle: d.startAngle, endAngle: d.endAngle})
return function (t) {
var interpolateOutput = arc(i(t), index);
return interpolateOutput;
}
})
.attr('fill', function (d) {
// style it with animation as well
return colorScale(d.data)
});
theDonutG.append('text')
.text(function (d, i) {
return dataScaleName(i);
})
.style('opacity', 0)
.attr({
fill: "#000000",
"font-size": "15px"
})
.attr('x', function (d, i) {
return dataHandler.pieData(i).textObj.x
})
.attr('y', function (d, i) {
return dataHandler.pieData(i).textObj.y
})
.attr('text-anchor', function (d) {
return d.textObj.textAnchor;
})
.transition()
.delay(2000)
.style('opacity', 1)
// add text
}
// the donut group
var theDonutG = theSvgG.selectAll('.donut-sector')
.data(dataHandler.getData())
// enter state
.enter()
.append('g')
.on('mouseenter', function (d) {
d3.select(this).transition().attr('opacity', 0.5);
})
.on('mouseleave', function (d) {
d3.select(this).transition().attr('opacity', 1);
})
.on('click', function (d, i) {
alert('Remove ' + dataScaleName(i));
data.splice(i, 1);
theStatusNameMapping.splice(i - 1, 1);
dataScaleName.range(theStatusNameMapping)
dataHandler.updateData(data);
var myDonutG = theSvgG.selectAll('.donut-sector').data(dataHandler.getData());
myDonutG.enter().append('g');
myDonutG.exit().remove();
updatePath(theSvgG.selectAll('.donut-sector'));
})
.style('cursor', 'hand')
.attr('class', 'donut-sector');
updatePath(theDonutG);