/** * 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);