Pie Chart

Pie charts are used to show percentage or proportional data as a series of slices that make up a whole.

This pie chart widget is one of the original widgets I created to show off the capabilities of svidget.js. The widget will highlight the slice as you hover over it, and display its data in a small popup. The chart is mobile friendly, as its slices can be selected via a mouseover or by tapping on a touch device.

To get started, play around with the controls to change the data, appearance, and settings of the pie chart. Use the generated HTML snippet to embed this pie chart in your website.

Data Params
data
maxSlices
startAngle (0-359)
sort
 
Appearance Params
colors
fontSize
fontName
labelColor
labelType
showLabels
showPopups

Actions
Events
 

Here is the HTML snippet based on the params above. Embed this snippet in your page.

<object id="pieChart" role="svidget" data="piechart.svg" type="image/svg+xml" width="400" height="400">
	<param name="data" value="[['banana', 7], ['apple', 6], ['cherry', 2], ['orange', 10], ['grape', 3.5]]" />
	<param name="maxSlices" value="-1" />
	<param name="startAngle" value="0" />
	<param name="sort" value="0" />
	<param name="colors" value="['#cf1f1f', '#1f1fcf', '#cf9f1f', '#1f9f1f', '#9f1f9f']" />
	<param name="fontSize" value="15" />
	<param name="fontName" value="Helvetica" />
	<param name="labelColor" value="white" />
	<param name="labelType" value="percentage" />
	<param name="showLabels" value="1" />
	<param name="showPopups" value="1" />
</object>

				
[View Code]

Here is the code for the widget:

<?xml version="1.0" encoding="utf-8" ?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svidget="http://www.svidget.org/svidget"
		 width="400" height="400" viewBox="0 0 400 400" style="background:transparent" svidget:version="0.2.0">
	<title>Pie Chart</title>
	<desc>
		A simple pie chart. 
		This pie chart takes in an array of data elements and displays them as a pie chart.
		This widget was developed by Joe Agster for svidget.com and is licensed under the Creative Commons Attribution 4.0 License.
	</desc>

	<svidget:params>
		<!-- data params -->
		<svidget:param name="data" shortname="d" type="array" coerce="true" description="An array of name, value arrays. Example: [['Steelers', 0.14], ['Broncos', 0.38], ['Colts', 0.21], ['Raiders', 0.06]]. Note that the values do not need to sum to 1 or 100. The widget will auto sum the values to construct percentages." onset="handleDataParamSet" />
		<svidget:param name="maxSlices" shortname="max" type="number" subtype="integer" defvalue="0" coerce="true" description="The max number of slices to allow. If there are more data for slices, the lowest valued slices will be grouped together as one slice. Default is unlimited." onset="handleDataParamSet" />
		<svidget:param name="startAngle" shortname="angle" type="number" coerce="true" defvalue="0" description="The angle where the first slice will be located." onset="handleDataParamSet" />
		<svidget:param name="sort" shortname="sort" type="bool" coerce="true" defvalue="0" description="Whether to sort the data. When true sorted by greatet to least value." onset="handleDataParamSet" />
		<!-- style params -->
		<svidget:param name="colors" type="array" coerce="true" description="An array of colors that correspond to each pie slice. If there are more slices than colors then colors will be reused." onset="handleColorsParamSet" />
		<svidget:param name="fontSize" shortname="fsize" type="number" coerce="true" defvalue="15" binding="#testtext@font-size" description="The font size for the labels." onset="handleDataParamSet" />
		<svidget:param name="fontName" shortname="fname" type="string" defvalue="Helvetica" binding="#testtext@font-family" description="The font name for the labels." onset="handleDataParamSet" />
		<svidget:param name="labelColor" shortname="lcolor" type="string" subtype="color" defvalue="white" description="The label color on the slices." onset="handleLabelParamsSet" />
		<svidget:param name="labelType" shortname="lt" type="string" subtype="choice" typedata="percentage|value|text" coerce="true" defvalue="percentage" description="The label value to display. Values are 'percentage', 'value', or 'text'. Default is text." onset="handleDataParamSet" />
		<svidget:param name="showLabels" shortname="sl" type="bool" coerce="true" defvalue="true" description="Whether to show the labels in the pie slices." onset="handleLabelParamsSet" />
		<svidget:param name="showPopups" shortname="sp" type="bool" coerce="true" defvalue="true" description="Whether to show popups when hovering or clicking on pie slices." onset="handleShowPopupsParamSet" />
	</svidget:params>

	<svidget:actions>
		<svidget:action name="animate" binding="animate" description="Animates the transition from 0-360 on the chart." />
	</svidget:actions>

	<svidget:events>
		<svidget:event name="sliceSelect" description="Triggered when a slice is selected either by mousing over or touching (on non-mouse devices)." />
		<svidget:event name="sliceActivate" description="Triggered when a slice is activated due to a click or touch after slice was selected." />
	</svidget:events>

	<style>
		<![CDATA[
		a.footer { fill: #afafaf; }
		.nonhover { opacity: 0.5; -webkit-transition: opacity 0.2s ease-in; transition: opacity 0.1s ease-in; }
		.hover { cursor: pointer; opacity: 1.0; }
		.unhover { opacity: 1.0; }
		.popupbox { fill: #fff; border: 1px solid #3f3f3f; }
		]]>
	</style>
		
	<defs>
		<clipPath id="popupboxclip">
			<rect x="0" y="0" width="120" height="50" stroke-width="2" />
		</clipPath>
	</defs>

	<g>
		<text id="footer" font-size="10" font-family="Helvetica" x="395" y="395" fill="#7f7f7f" text-anchor="end">
			<a xlink:href="http://www.svidget.com" class="footer">Powered by Svidget.js</a>.
		</text>
	</g>

	<g id="test" transform="translate(-1000 -1000)">
		<text id="testtext" font-size="15" font-family="Helvetica" x="0" y="0" />
	</g>
	
	<g id="pie">
		<g class="" transform="matrix(1 0 0 1 0 0)" tabindex="0">
			<path style="stroke-linejoin: round; stroke-width: 2;" fill="#cf1f1f" stroke="#ffffff" d="M 200 10 A 190 190 0 0 1 358.384 95.05 L 200 200 Z" />
			<text style="font-family: Helvetica; font-size: 15px; text-anchor: middle;" fill="#ffffff" x="279.1013" y="56.4448">15.7%</text>
		</g>
		<g class="" transform="matrix(1 0 0 1 0 0)" tabindex="1">
			<path style="stroke-linejoin: round; stroke-width: 2;" fill="#1f1fcf" stroke="#ffffff" d="M 358.384 95.0507 A 190 190 0 0 1 319.115 348.025 L 200 200 Z" />
			<text style="font-family: Helvetica; font-size: 15px; text-anchor: middle;" fill="#ffffff" x="361.4663" y="228.8142">23.5%</text>
		</g>
		<g class="" transform="matrix(1 0 0 1 0 0)" tabindex="2">
			<path style="stroke-linejoin: round; stroke-width: 2;" fill="#cf9f1f" stroke="#ffffff" d="M 319.116 348.025 A 190 190 0 0 1 234.912 386.764 L 200 200 Z" />
			<text style="font-family: Helvetica; font-size: 15px; text-anchor: middle;" fill="#ffffff" x="271.4712" y="359.0976">7.8%</text>
		</g>
		<g class="" transform="matrix(1 0 0 1 0 0)" tabindex="3">
			<path style="stroke-linejoin: round; stroke-width: 2;" fill="#1f9f1f" stroke="#ffffff" d="M 234.912 386.765 A 190 190 0 0 1 55.713 76.382 L 200 200 Z" />
			<text style="font-family: Helvetica; font-size: 15px; text-anchor: middle;" fill="#ffffff" x="58.4915" y="285.45">39.2%</text>
		</g>
		<g class="" transform="matrix(1 0 0 1 0 0)" tabindex="4">
			<path style="stroke-linejoin: round; stroke-width: 2;" fill="#9f1f9f" stroke="#ffffff" d="M 55.7131 76.3825 A 190 190 0 0 1 200 10 L 200 200 Z" />
			<text style="font-family: Helvetica; font-size: 15px; text-anchor: middle;" fill="#ffffff" x="129.3229" y="50.1285">13.7%</text>
		</g>
	</g>
	
	<g id="popupbox" clip-path="url(#popupboxclip)" transform="translate(0 0)" display="none">
		<rect x="0" y="0" width="120" height="50" class="popupbox" stroke="#3f3f3f" stroke-width="2" />
		<text id="popupboxlabel" font-family="Helvetica" font-size="15" fill="#000000" font-weight="bold" stroke="none" stroke-width="0" text-anchor="start" x="5" y="20">Item 1</text>
		<text id="popupboxvalue" font-family="Helvetica" font-size="15" fill="#000000" stroke="none" stroke-width="0" text-anchor="start" x="5" y="40">454</text>
	</g>

	<script type="application/javascript" xlink:href="../scripts/svidget.min.js"></script>
	<script type="application/javascript" xlink:href="../scripts/snap.svg-min.js"></script>
	<script type="application/javascript" xml:lang="javascript">
		<![CDATA[

		// constants
		var DEFAULT_COLORS = ['#cf1f1f', '#1f1fcf', '#cf9f1f', '#1f9f1f', '#9f1f9f'];
		var EDGE = 10; // space between pie and edge
		var CENTER_SPACE = 0; // space between slice tip and center
		var SIZE = 400;
		var FULL_RADIUS = SIZE / 2;
		
		// param variables
		var _data = [];
		var _maxSlices = -1;
		var _startAngle = 0;
		var _sort = false;
		var _colors = DEFAULT_COLORS;
		var _fontSize = 15; //pixels
		var _fontName = 'Helvetica';
		var _labelColor = '#000';
		var _labelType = 'percentage';
		var _showLabels = true;
		var _showPopups = true;

		// general variables
		var _slices = null;
		var _activeSlice = null;
		var _loaded = false;
		var _runAnimation = true;
		
		
		/* Loading */
		
		// entry point
		function init() {
			//debugger;
			console.log('init');
			// for demo purposes only, in case no data is passes
			_data = [['banana', 4], ['apple', 6], ['cherry', 2], ['orange', 10], ['grape', 3.5]];
			initParams();
			initEvents();

			drawIfStandalone();
			_loaded = true;
		}
		
		function initParams() {
			loadParams();
		}
		
		function initEvents() {
			var surface = Snap(document.documentElement);
			surface.click(handleSurfaceClick);
			//var pie = Snap("#pie"); //Snap(document.documentElement); //"#pie");
			//pie.mouseout(handlePieOut);
			var widget = svidget.$;
			widget.onpagepopulate(handlePagePopulate);
		}
		
		function drawIfStandalone() {
			// this is temp hack to give chance for page to pass params, if no page then we are in standalone mode
			window.setTimeout(function () {
				if (svidget.$.populatedFromPage()) return;
				draw();
			}, 100);
		}
		
		window.addEventListener('load', init, false);
		
		
		/* Param Events */
		
		function loadParams() {
			var widget = svidget.$;
			_data = checkNull(widget.param("data").value(), _data);
			_maxSlices = checkNull(widget.param("maxSlices").value(), _maxSlices);
			_startAngle = checkNull(widget.param("startAngle").value(), _startAngle);
			_sort = checkNull(widget.param("sort").value(), _sort);
			_colors = checkNull(widget.param("colors").value(), _colors);
			_fontSize = checkNull(widget.param("fontSize").value(), _fontSize);
			_fontName = checkNull(widget.param("fontName").value(), _fontName); //'Arial';
			_labelColor = checkNull(widget.param("labelColor").value(), _labelColor); //'#000';
			_labelType = checkNull(widget.param("labelType").value(), _labelType); //'percentage';
			_showLabels = checkNull(widget.param("showLabels").value(), _showLabels);
			_showPopups = checkNull(widget.param("showPopups").value(), _showPopups);
		}
		
		// this handler is invoked once the params from page are passed to widget and populated onto params
		function handlePagePopulate(e) {
			loadParams();
			draw();
		}
		
		// this handler is invoked when these params are set: data, maxSlices, startAngle, sort, fontSize, fontName, labelType
		function handleDataParamSet(e) {
			if (!_loaded) return;
			//console.log('handleDataParamSet');
			var widget = svidget.$;
			if (widget.connected() && !widget.populatedFromPage()) return; // not all params have been populated, so defer until loadParams() is called
			//console.log('populated from page: ' + widget.populatedFromPage());

			_runAnimation = _runAnimation || e.target.name() == "data";
			loadParams();
			draw();
		}
		
		// this handler is invoked when the colors params is set
		function handleColorsParamSet(e) {
			if (!_loaded) return;
			//console.log('handleColorsParamSet');
			var widget = svidget.$;
			if (widget.connected() && !widget.populatedFromPage()) return; // not all params have been populated, so defer until loadParams() is called
			_colors = widget.param("colors").value();
			var i = 0;
			//debugger;
			iterateSliceContainers(function (snapEle) {
				var sliceEle = snapEle.select("path");
				var color = _colors[i];
				sliceEle.attr({ fill: color });
				i = (i + 1) % _colors.length;
			});
		}
		
		// this handler is invoked when the labelColor or showLabels params is set
		function handleLabelParamsSet(e) {
			if (!_loaded) return;
			//console.log('handleLabelParamsSet');
			var widget = svidget.$;
			if (widget.connected() && !widget.populatedFromPage()) return; // not all params have been populated, so defer until loadParams() is called
			_labelColor = widget.param("labelColor").value() || _labelColor;
			_showLabels = widget.param("showLabels").value();

			iterateSliceContainers(function (snapEle) {
				var labelEle = snapEle.select("text");
				labelEle.attr({
					fill: _labelColor,
					'font-family': _fontName,
					'font-size': _fontSize,
					'display': _showLabels ? "" : "none"
				});
			});
		}
		
		// this handler is invoked when the showPopups params is set
		function handleShowPopupsParamSet(e) {
			if (!_loaded) return;
			//console.log('handleShowPopupsParamSet');
			var widget = svidget.$;
			if (widget.connected() && !widget.populatedFromPage()) return; // not all params have been populated, so defer until loadParams() is called
			_showPopups = widget.param("showPopups").value();
			if (!_showPopups) hidePopup(); // just in case
		}
		
		
		/* Action Events */
		
		function animate(e) {
			_runAnimation = true;
			redraw();
		}
		
		
		/* UI Events */
		
		function handleSliceOver(e) {
			iterateSliceContainers(function (snapEle) {
				snapEle.removeClass("hover");
				snapEle.addClass("nonhover");
			});
			var target = Snap(e.currentTarget);
			target.removeClass("nonhover");
			target.addClass("hover");
			var index = target.data("index");
			//debugger;
			var currentSlice = _slices[index];
			if (_showPopups) showPopup(currentSlice);
			if (_activeSlice != currentSlice) e.preventDefault(); // in case of touchstart, prevents "click" from firing (needs test)
			_activeSlice = currentSlice;
			var val = toEventValue(currentSlice);
			svidget.$.event("sliceSelect").trigger(val);
			//console.log('handleSliceOver index=' + index);
		}
		
		function handleSliceOut(e) {
			iterateSliceContainers(function (snapEle) {
				snapEle.removeClass("hover");
				snapEle.removeClass("nonhover");
			});

			hidePopup();
			_activeSlice = null;
			//console.log('handleSliceOut');
			//console.log('handleSliceOut ' + e.target.nodeName + " " + e.target.id);
		}
		
		function handleSliceClick(e) {
			//alert('clicked');
			var target = Snap(e.currentTarget);
			var index = target.data("index");
			var currentSlice = _slices[index];
			var val = toEventValue(currentSlice);
			svidget.$.event("sliceActivate").trigger(val);
			e.stopPropagation();
		}
		
		function handleSurfaceClick(e) {
			handleSliceOut(e);
		}
		
		function toEventValue(slice) {
			return { name: slice.label, value: slice.value, ratio: slice.ratio };
		}
		
		
		/* Popup */
		
		function showPopup(slice) {
			document.getElementById("popupboxlabel").textContent = slice.label;
			document.getElementById("popupboxvalue").textContent = slice.toPctString() + " (" + slice.value + ")";
			var box = Snap("#popupbox");
			var rect = box.select("rect");
			//box.attr({ display: ""});
			positionBox(slice.centerAngle());
			setPopupVisible(true);
			
			function positionBox(angle) {
				var w = rect.node.width.baseVal.value;
				var h = rect.node.height.baseVal.value;
				var x = 0;
				var y = 0;
				angle = angle % 360;
				if (angle <= 90)
					x = SIZE - w;
				else if (angle <= 180) {
					x = SIZE - w;
					y = SIZE - h;
				}
				else if (angle <= 270)
					y = SIZE - h;
				box.transform(translate(x, y));
			}
			
			function translate(x, y) {
				return "translate(" + x + " " + y + ")";
			}
		}
		
		function hidePopup() {
			setPopupVisible(false);
		}
		
		function setPopupVisible(visible) {
			var d = visible ? "" : "none";
			document.getElementById("popupbox").setAttribute("display", d);
		}
		
		
		/* Drawing */

		function draw() {
			//debugger;
			clearPie();
			loadSlices();
			drawSlices();
			_runAnimation = false;
		}
		
		function redraw() {
			clearPie();
			drawSlices();
			_runAnimation = false;
		}
		
		function clearPie() {
			var pie = Snap("#pie");
			pie.clear();
		}
		
		function loadSlices() {
			_slices = buildSlices(_data);
		}
		
		function drawSlices() {
			if (!_slices || !_slices.length) return; // no data yet
			for (var i=0; i<_slices.length; i++) {
				var slice = _slices[i];
				drawSlice(slice);
			}
		}
		
		function drawSlice(slice) { //startAngle, endAngle, color) {
			var pie = Snap("#pie");
			var sliceContainer = pie.group();
			// wire events
			sliceContainer.data("index", slice.index);
			sliceContainer.mouseover(handleSliceOver);
			sliceContainer.touchstart(handleSliceOver); // for touch devices
			sliceContainer.mouseout(handleSliceOut);
			sliceContainer.click(handleSliceClick);
			
			sliceContainer.attr({ 'tabindex': slice.index });
			// set up animation
			//var r = "r-" + slice.angle + ",200,200";
			//sliceContainer.transform("r-180,200,200");
			if (_runAnimation) {
				animateSlices(sliceContainer, slice);
			}
			// build sub-elements for slice
			var sliceEle = buildSliceElement(sliceContainer, slice.path, slice.color); //g, startAngle, endAngle, color);
			var labelEle = buildLabelElement(sliceContainer, slice.getLabelString(), slice.textcolor, slice.textX, slice.textY);
		}
		
		function buildSliceElement(container, path, color) { //, source, startAngle, endAngle, color) {
			//var d = generateSlicePath(startAngle, endAngle);
			var sliceEle = container.path(path);
			sliceEle.attr({
				fill: color,
				stroke: "#fff",
				strokeWidth: 2,
				'stroke-linejoin': 'round'
			});
			return sliceEle;
		}
		
		function buildLabelElement(container, text, color, x, y) {
			if (x < 0 || y < 0) return; // there is no room for label, so exit
			var yOffset = _fontSize / 4.0; // this is for positioning text along baseline
			var labelEle = container.text(x, y, text);
			labelEle.attr({
				fill: color,
				x: x,
				y: y + yOffset,
				'font-family': _fontName,
				'font-size': _fontSize,
				'text-anchor': 'middle',
				'display': _showLabels ? "" : "none"
			});
			return labelEle;
		}
		
		function iterateSliceContainers(action) {
			var pie = Snap("#pie");
			var groups = pie.selectAll("g");
			for (var i=0; i<groups.length; i++) action(groups[i]);
		}
		
		function animateSlices(sliceContainer, slice) {
			sliceContainer.transform("rotate(-" + parseInt(slice.angle) + " 200 200)");
			sliceContainer.animate({ transform: "rotate(0 200 200)" }, 500, mina.easein);
		}
		
		
		/* Slice Building */
		
		function buildSlices(data) {
			if (!data || !data.length) return;
			// ex: [['Steelers', 0.14], ['Broncos', 0.38]]
			data = data.slice(); // clone array
			var slices = [];
			var sum = 0.0;
			var remSlice = { name: "Other", value: 0 };
			// sort by value so that smaller slices are grouped together
			if (_sort) sortData(data);
			
			// builds a collection of slice items to be use to build the actual slice objects below
			// also sums the total of all values for each item
			for (var i=0; i<data.length; i++) {
				var item = data[i];
				
				if (item.length && item.length >= 2) {
					if (_maxSlices > 0 && i >= _maxSlices-1 && data.length > _maxSlices) {
						remSlice.value += item[1];
					}
					else {
						var slice = { name: item[0], value: item[1] };
						slices.push(slice);
					}
					sum += +item[1];
				}
			}
			// append remaining slice if maxSlices exceeded
			if (remSlice.value > 0) slices.push(remSlice);
			
			// calc ratios
			for (var i=0; i<slices.length; i++) {
				slices[i].ratio = slices[i].value / sum;
			}
			
			// build slice objects
			var result = [];
			var startAngle = _startAngle;
			for (var i=0; i<slices.length; i++) {
				var colorIndex = i % _colors.length;
				var item = new Slice(startAngle, slices[i].ratio, _colors[colorIndex], slices[i].value, slices[i].name, _labelColor, i);
				var angleLength = item.ratio * 360;
				result.push(item);
				startAngle += angleLength;
			}
			
			return result;
		}

		function sortData(data) {
			data.sort(function (a,b) {
				return b[1] - a[1]; //sort descending by value (index 1 in subarray)
			});
		}
		
		
		/* Misc */
		
		function checkNull(item, backupItem) {
			return item != null ? item : backupItem;
		}


		/* Models */

		// Slice object
		function Slice(angle, ratio, color, value, label, labelColor, index) {
			this.angle = angle;
			this.ratio = ratio;
			this.angleLength = ratio * 360;
			this.color = color;
			this.textcolor = labelColor;
			this.value = value;
			this.label = label;
			this.index = index;
			this.path = generateSlicePath(angle, angle + this.angleLength);
			this.textX = -1;
			this.textY = -1;
			
			//console.log('new Slice populateTextXY');
			populateTextXY.call(this);
			//console.log('new Slice populateTextXY complete');
			
			function populateTextXY() {
				//debugger;
				var textRect = getTextRectXY(this.angle, this.angleLength, this.path, this.getLabelString());
				if (textRect == null) return;
				var textX = textRect.x + (textRect.width / 2);
				var textY = textRect.y + (textRect.height / 2);
				this.textX = textX;
				this.textY = textY;
			}
			
			// returns { x: 0, y: 0, width: 0, height: 0 }
			function getTextRectXY(angle, angleLen, path, label) {
				// measure text length and height, this is the text bounding rect
				var PAD = 6;
				//var YOFFSET = 4;
				var textEle = document.getElementById("testtext");
				textEle.textContent = label;
				var rectWidth = textEle.getComputedTextLength() + PAD; 
				var rectHeight = _fontSize + PAD;
				//var rectSize = { width: width, height: _fontSize };
				var arcRadius = FULL_RADIUS - EDGE;
				// given imaginary line between center of pie and middle angle of edge
				//   determine the furthest away point from center of pie such that the text rect is fully inside the slice
				var C = FULL_RADIUS; // 200
				var centerAngle = (angle + angle + angleLen) / 2;
				var centerAnglePoint = { x: getAngleX(centerAngle, arcRadius), y: getAngleY(centerAngle, arcRadius) };
				var rect = { x: centerAnglePoint.x - (rectWidth / 2), y: centerAnglePoint.y - (rectHeight / 2), width: rectWidth, height: rectHeight };

				var startX = rect.x;
				var startY = rect.y;
				var ccx = startX < 200 ? -1 : 1;
				var ccy = startY < 200 ? -1 : 1;
				var endX = C - (rectWidth / 2); //+ (centerAnglePoint.x - startX * ccx);
				var endY = C - (rectHeight / 2); //+ (centerAnglePoint.y - startY * ccy);

				// in this loop we work our way from edge of circle to center, in incremenetal units divisible by 100
				// if we get halfway there (50) and still the rect doesn't fit, then it will never fit
				for (var i=1; i<50; i++) {
					var curX = startX + (((endX - startX) / 100) * i);
					var curY = startY + (((endY - startY) / 100) * i);
					var curRect = { x: curX, y: curY, width: rect.width, height: rect.height };
					if (isRectInPath(curRect, path)) return curRect;
				}
				
				return null;
			}
			
			function isRectInPath(rect, path) {
				return Snap.path.isPointInside(path, rect.x, rect.y) && 
								Snap.path.isPointInside(path, rect.x + rect.width, rect.y) &&
								Snap.path.isPointInside(path, rect.x + rect.width, rect.y + rect.height) &&
								Snap.path.isPointInside(path, rect.x, rect.y + rect.height);
			}
			
			function generateSlicePath(startAngle, endAngle) {
				//M 340 251 L 300.142 399.753 A 154 154 0 0 1 231.106 359.894 L 340 251 A 0 0 0 0 0 340 251
				var path = "";
				var outerLength = FULL_RADIUS - EDGE;
				var startX = getAngleX(startAngle, outerLength);
				var startY = getAngleY(startAngle, outerLength);
				path += moveTo(startX, startY);
			
				path += generateSlicePathArc(startAngle, endAngle);
				var endX = getAngleX(endAngle, CENTER_SPACE);
				var endY = getAngleY(endAngle, CENTER_SPACE);
				path += lineTo(endX, endY);
				path += " Z";
			
				return path;
			}
		
			// startAngle: start angle (in degrees)
			// endAngle: end angle (in degrees)
			function generateSlicePathArc(startAngle, endAngle) {
				var arcRadius = FULL_RADIUS - EDGE;
				if (endAngle < startAngle) endAngle += 360;
				var angleDiff = endAngle - startAngle;
				var largeArc = angleDiff > 180 ? 1 : 0; // if greater than 180 deg we need to use large arc in path
				var endX = getAngleX(endAngle, arcRadius);
				var endY = getAngleY(endAngle, arcRadius);
				if (endX == 100 && pct > 0) endX = 99.999; // this corrects issue with path if start and end are same it won't draw an arc
				//debugger;
			
				// A 73,73 0 1,1 27,100
				var path = arcTo(arcRadius, largeArc, 1, endX, endY);
			
				return path;
			}
		
			function getAngleX(angle, fromCenter) {
				var rad = Snap.rad(angle);
				return (Math.sin(rad) * fromCenter) + FULL_RADIUS;
			}
		
			function getAngleY(angle, fromCenter) {
				var rad = Snap.rad(angle);
				return (-Math.cos(rad) * fromCenter) + FULL_RADIUS;
			}
		
			function lineTo(x, y) {
				return "L " + x + "," + y + " ";
			}
		
			function moveTo(x, y) {
				return "M " + x + "," + y + " ";
			}
		
			function arcTo(radius, largeArcFlag, sweepFlag, endX, endY) {
				return "A " + radius + "," + radius + " 0 " + largeArcFlag + "," + sweepFlag + " " + round3(endX) + "," + round3(endY) + " ";
			}
		
			function round3(val) {
				return parseInt(val * 1000) / 1000.0;
			}
			
			function slope(x1, x2, y1, y2) {
				if (x1 == x2) return NaN;
				return (y2 - y1) / (x2 - x1);
			}
		}
		
		Slice.prototype = {
		
			toPctString: function () {
				return (Math.round(this.ratio * 1000) / 10.0) + "%";
			},
			
			getLabelString: function(type) {
				// percentage|value|text
				type = type || _labelType;
				if (type == "value")
					return this.value + '';
				else if (type == "text")
					return this.label;
				else
					return this.toPctString();
			},
			
			centerAngle: function () {
				return (this.angle + this.angle + this.angleLength) / 2;
			}
		
		}
		
	
	  ]]>
	</script>
</svg>
[View Code]

Download this widget to place on your website. The zip file contains version 0.3.4 of svidget.js but you can also download the latest version.

Fork me on GitHub