diff --git a/README.md b/README.md index d7ac98b..1ec34d1 100644 --- a/README.md +++ b/README.md @@ -7,3 +7,68 @@ Inspired by Chart.js(http://www.chartjs.org/). [Demo on my codepen](http://codepen.io/githiro/details/ICfFE) Licensed under the MIT License. +# jquery.drawDoughnutChart.js + +A SVG doughnut chart with animation and tooltip. +Inspired by Chart.js(http://www.chartjs.org/). +[Demo on my codepen](http://codepen.io/githiro/details/ICfFE) + +## Usage + +EXAMPLE: +```html + + +
+ +' + settings.summaryTitle + '
').appendTo($summary); - $summaryTitle.css('font-size', getScaleFontSize( $summaryTitle, settings.summaryTitle )); // In most of case useless - var $summaryNumber = $('').appendTo($summary).css({opacity: 0}); + //Set up pie segments wrapper + var $pathGroup = $(document.createElementNS('http://www.w3.org/2000/svg', 'g')); + $pathGroup.attr({opacity: 0}).appendTo($svg); - for (var i = 0, len = data.length; i < len; i++) { - segmentTotal += data[i].value; - $paths[i] = $(document.createElementNS('http://www.w3.org/2000/svg', 'path')) - .attr({ - "stroke-width": settings.segmentStrokeWidth, - "stroke": settings.segmentStrokeColor, - "fill": data[i].color, - "data-order": i - }) - .appendTo($pathGroup) - .on("mouseenter", pathMouseEnter) - .on("mouseleave", pathMouseLeave) - .on("mousemove", pathMouseMove) - .on("click", pathClick); - } + //Set up tooltip + var $tip = $('').appendTo('body').hide(), + tipW = $tip.width(), + tipH = $tip.height(); - //Animation start - animationLoop(drawPieSegments); + //Set up center text area + var summarySize = (cutoutRadius - (doughnutRadius - cutoutRadius)) * 2, + $summary = $('') + .appendTo($this) + .css({ + width: summarySize + "px", + height: summarySize + "px", + "margin-left": -(summarySize / 2) + "px", + "margin-top": -(summarySize / 2) + "px" + }); + var $summaryTitle = $('' + settings.summaryTitle + '
').appendTo($summary); + var $summaryNumber = $('').appendTo($summary).css({opacity: 0}); - //Functions - function getHollowCirclePath(doughnutRadius, cutoutRadius) { - //Calculate values for the path. - //We needn't calculate startRadius, segmentAngle and endRadius, because base doughnut doesn't animate. - var startRadius = -1.570,// -Math.PI/2 - segmentAngle = 6.2831,// 1 * ((99.9999/100) * (PI*2)), - endRadius = 4.7131,// startRadius + segmentAngle - startX = centerX + cos(startRadius) * doughnutRadius, - startY = centerY + sin(startRadius) * doughnutRadius, - endX2 = centerX + cos(startRadius) * cutoutRadius, - endY2 = centerY + sin(startRadius) * cutoutRadius, - endX = centerX + cos(endRadius) * doughnutRadius, - endY = centerY + sin(endRadius) * doughnutRadius, - startX2 = centerX + cos(endRadius) * cutoutRadius, - startY2 = centerY + sin(endRadius) * cutoutRadius; - var cmd = [ - 'M', startX, startY, - 'A', doughnutRadius, doughnutRadius, 0, 1, 1, endX, endY,//Draw outer circle - 'Z',//Close path - 'M', startX2, startY2,//Move pointer - 'A', cutoutRadius, cutoutRadius, 0, 1, 0, endX2, endY2,//Draw inner circle - 'Z' - ]; - cmd = cmd.join(' '); - return cmd; - }; - function pathMouseEnter(e) { - var order = $(this).data().order; - if (settings.showTip) { - $tip.text(data[order].title + ": " + data[order].value) - .fadeIn(200); - } - if(settings.showLabel) { - $summaryTitle.text(data[order].title).css('font-size', getScaleFontSize( $summaryTitle, data[order].title)); - var tmpNumber = settings.shortInt ? shortKInt(data[order].value) : data[order].value; - $summaryNumber.html(tmpNumber).css('font-size', getScaleFontSize( $summaryNumber, tmpNumber)); - } - settings.onPathEnter.apply($(this),[e,data]); - } - function pathMouseLeave(e) { - if (settings.showTip) $tip.hide(); - if(settings.showLabel) { - $summaryTitle.text(settings.summaryTitle).css('font-size', getScaleFontSize( $summaryTitle, settings.summaryTitle)); - var tmpNumber = settings.shortInt ? shortKInt(segmentTotal) : segmentTotal; - $summaryNumber.html(tmpNumber).css('font-size', getScaleFontSize( $summaryNumber, tmpNumber)); - } - settings.onPathLeave.apply($(this),[e,data]); - } - function pathMouseMove(e) { - if (settings.showTip) { - $tip.css({ - top: e.pageY + settings.tipOffsetY, - left: e.pageX - $tip.width() / 2 + settings.tipOffsetX - }); - } - } - function pathClick(e){ - var order = $(this).data().order; - if (typeof data[order].action != "undefined") - data[order].action(); - } - function drawPieSegments (animationDecimal) { - var startRadius = -PI / 2,//-90 degree - rotateAnimation = 1; - if (settings.animation && settings.animateRotate) rotateAnimation = animationDecimal;//count up between0~1 + for (var i = 0, len = data.length; i < len; i++) { + segmentTotal += value[i]; + $paths[i] = $(document.createElementNS('http://www.w3.org/2000/svg', 'path')) + .attr({ + "stroke-width": settings.segmentStrokeWidth, + "stroke": settings.segmentStrokeColor, + "fill": data[i].color, + "data-order": i + }) + .appendTo($pathGroup) + .on("mouseenter", pathMouseEnter) + .on("mouseleave", pathMouseLeave) + .on("mousemove", pathMouseMove); + } + segmentTotal = segmentTotal === 0 ? 1:segmentTotal; // avoid denominator equaling to zero - drawDoughnutText(animationDecimal, segmentTotal); + //Animation start + animationLoop(drawPieSegments); - $pathGroup.attr("opacity", animationDecimal); + function pathMouseEnter(e) { + var order = $(this).data().order; + $tip.text(data[order].title.replace(/\$(\d+)/g,function(match,$1){ // replace the pattern of $i in data[j].title with data[i-1].value, but when i equals zero, replace that with data[j].value + if($1 == 0) { + return data[order].value; + } + else if($1 > 0 && $1 <= data.length) { + return data[$1-1].value; + } + else return match; + })).fadeIn(200); + settings.onPathEnter.apply($(this),[e,data]); + } + function pathMouseLeave(e) { + $tip.stop().hide(); + settings.onPathLeave.apply($(this),[e,data]); + } + function pathMouseMove(e) { + $tip.css({ + top: e.pageY + settings.tipOffsetY, + left: e.pageX - $tip.width() / 2 + settings.tipOffsetX + }); + } + function drawPieSegments (animationDecimal) { + var startRadius = -PI / 2,//-90 degree + rotateAnimation = 1; + if (settings.animation && settings.animateRotate) rotateAnimation = animationDecimal;//count up between0~1 - //If data have only one value, we draw hollow circle(#1). - if (data.length === 1 && (4.7122 < (rotateAnimation * ((data[0].value / segmentTotal) * (PI * 2)) + startRadius))) { - $paths[0].attr("d", getHollowCirclePath(doughnutRadius, cutoutRadius)); - return; - } - for (var i = 0, len = data.length; i < len; i++) { - var segmentAngle = rotateAnimation * ((data[i].value / segmentTotal) * (PI * 2)), - endRadius = startRadius + segmentAngle, - largeArc = ((endRadius - startRadius) % (PI * 2)) > PI ? 1 : 0, - startX = centerX + cos(startRadius) * doughnutRadius, - startY = centerY + sin(startRadius) * doughnutRadius, - endX2 = centerX + cos(startRadius) * cutoutRadius, - endY2 = centerY + sin(startRadius) * cutoutRadius, - endX = centerX + cos(endRadius) * doughnutRadius, - endY = centerY + sin(endRadius) * doughnutRadius, - startX2 = centerX + cos(endRadius) * cutoutRadius, - startY2 = centerY + sin(endRadius) * cutoutRadius; - var cmd = [ - 'M', startX, startY,//Move pointer - 'A', doughnutRadius, doughnutRadius, 0, largeArc, 1, endX, endY,//Draw outer arc path - 'L', startX2, startY2,//Draw line path(this line connects outer and innner arc paths) - 'A', cutoutRadius, cutoutRadius, 0, largeArc, 0, endX2, endY2,//Draw inner arc path - 'Z'//Cloth path - ]; - $paths[i].attr("d", cmd.join(' ')); - startRadius += segmentAngle; - } - } - function drawDoughnutText(animationDecimal, segmentTotal) { - $summaryNumber - .css({opacity: animationDecimal}) - .text((segmentTotal * animationDecimal).toFixed(1)); - var tmpNumber = settings.shortInt ? shortKInt(segmentTotal) : segmentTotal; - $summaryNumber.html(tmpNumber).css('font-size', getScaleFontSize( $summaryNumber, tmpNumber)); - } - function animateFrame(cnt, drawData) { - var easeAdjustedAnimationPercent =(settings.animation)? CapValue(easingFunction(cnt), null, 0) : 1; - drawData(easeAdjustedAnimationPercent); - } - function animationLoop(drawData) { - var animFrameAmount = (settings.animation)? 1 / CapValue(settings.animationSteps, Number.MAX_VALUE, 1) : 1, - cnt =(settings.animation)? 0 : 1; - requestAnimFrame(function() { - cnt += animFrameAmount; - animateFrame(cnt, drawData); - if (cnt <= 1) { - requestAnimFrame(arguments.callee); - } else { - settings.afterDrawed.call($this); - } - }); - } - function Max(arr) { - return Math.max.apply(null, arr); - } - function Min(arr) { - return Math.min.apply(null, arr); - } - function isNumber(n) { - return !isNaN(parseFloat(n)) && isFinite(n); - } - function CapValue(valueToCap, maxValue, minValue) { - if (isNumber(maxValue) && valueToCap > maxValue) return maxValue; - if (isNumber(minValue) && valueToCap < minValue) return minValue; - return valueToCap; - } - function shortKInt (int) { - int = int.toString(); - var strlen = int.length; - if(strlen<5) - return int; - if(strlen<8) - return '' + int.substring(0, strlen-3) + 'K'; - return '' + int.substring( 0, strlen-6) + 'M'; - } - function getScaleFontSize(block, newText) { - block.css('font-size', ''); - newText = newText.toString().replace(/(<([^>]+)>)/ig,""); - var newFontSize = block.width() / newText.length * settings.ratioFont; - // Not very good : http://stephensite.net/WordPressSS/2008/02/19/how-to-calculate-the-character-width-accross-fonts-and-points/ - // But best quick way the 1.5 number is to affinate in function of the police - var maxCharForDefaultFont = block.width() - newText.length * block.css('font-size').replace(/px/, '') / settings.ratioFont; - if(maxCharForDefaultFont<0) - return newFontSize+'px'; - else - return ''; - } - /** - function getScaleFontSize(block, newText) { - block.css('font-size', ''); - newText = newText.toString().replace(/(<([^>]+)>)/ig,""); - var newFontSize = block.width() / newText.length; - if(newFontSize