/*! * d3pie * @author Ben Keen * @version 0.1.8 * @date May 1st, 2015 * @repo http://github.com/benkeen/d3pie */ // UMD pattern from https://github.com/umdjs/umd/blob/master/returnExports.js (function(root, factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module define([], factory); } else if (typeof exports === 'object') { // Node. Does not work with strict CommonJS, but only CommonJS-like environments that support module.exports, // like Node module.exports = factory(require()); } else { // browser globals (root is window) root.d3pie = factory(root); } }(this, function() { var _scriptName = "d3pie"; var _version = "0.1.6"; // used to uniquely generate IDs and classes, ensuring no conflict between multiple pies on the same page var _uniqueIDCounter = 0; // this section includes all helper libs on the d3pie object. They're populated via grunt-template. Note: to keep // the syntax highlighting from getting all messed up, I commented out each line. That REQUIRES each of the files // to have an empty first line. Crumby, yes, but acceptable. //// --------- _default-settings.js -----------/** /** * Contains the out-the-box settings for the script. Any of these settings that aren't explicitly overridden for the * d3pie instance will inherit from these. This is also included on the main website for use in the generation script. */ var defaultSettings = { header: { title: { text: "", color: "#333333", fontSize: 18, font: "arial" }, subtitle: { text: "", color: "#666666", fontSize: 14, font: "arial" }, location: "top-center", titleSubtitlePadding: 8 }, footer: { text: "", color: "#666666", fontSize: 14, font: "arial", location: "left" }, size: { canvasHeight: 500, canvasWidth: 500, pieInnerRadius: "0%", pieOuterRadius: null }, data: { sortOrder: "none", ignoreSmallSegments: { enabled: false, valueType: "percentage", value: null }, smallSegmentGrouping: { enabled: false, value: 1, valueType: "percentage", label: "Other", color: "#cccccc" }, content: [] }, labels: { outer: { format: "label", hideWhenLessThanPercentage: null, pieDistance: 30 }, inner: { format: "percentage", hideWhenLessThanPercentage: null }, mainLabel: { color: "#333333", font: "arial", fontSize: 10 }, percentage: { color: "#dddddd", font: "arial", fontSize: 10, decimalPlaces: 0 }, value: { color: "#cccc44", font: "arial", fontSize: 10 }, lines: { enabled: true, style: "curved", color: "segment" }, truncation: { enabled: false, truncateLength: 30 }, formatter: null }, effects: { load: { effect: "default", speed: 1000 }, pullOutSegmentOnClick: { effect: "bounce", speed: 300, size: 10 }, highlightSegmentOnMouseover: true, highlightLuminosity: -0.2 }, tooltips: { enabled: false, type: "placeholder", // caption|placeholder string: "", placeholderParser: null, styles: { fadeInSpeed: 250, backgroundColor: "#000000", backgroundOpacity: 0.5, color: "#efefef", borderRadius: 2, font: "arial", fontSize: 10, padding: 4 } }, misc: { colors: { background: null, segments: [ "#2484c1", "#65a620", "#7b6888", "#a05d56", "#961a1a", "#d8d23a", "#e98125", "#d0743c", "#635222", "#6ada6a", "#0c6197", "#7d9058", "#207f33", "#44b9b0", "#bca44a", "#e4a14b", "#a3acb2", "#8cc3e9", "#69a6f9", "#5b388f", "#546e91", "#8bde95", "#d2ab58", "#273c71", "#98bf6e", "#4daa4b", "#98abc5", "#cc1010", "#31383b", "#006391", "#c2643f", "#b0a474", "#a5a39c", "#a9c2bc", "#22af8c", "#7fcecf", "#987ac6", "#3d3b87", "#b77b1c", "#c9c2b6", "#807ece", "#8db27c", "#be66a2", "#9ed3c6", "#00644b", "#005064", "#77979f", "#77e079", "#9c73ab", "#1f79a7" ], segmentStroke: "#ffffff" }, gradient: { enabled: false, percentage: 95, color: "#000000" }, canvasPadding: { top: 5, right: 5, bottom: 5, left: 5 }, pieCenterOffset: { x: 0, y: 0 }, cssPrefix: null }, callbacks: { onload: null, onMouseoverSegment: null, onMouseoutSegment: null, onClickSegment: null } }; //// --------- validate.js ----------- var validate = { // called whenever a new pie chart is created initialCheck: function(pie) { var cssPrefix = pie.cssPrefix; var element = pie.element; var options = pie.options; // confirm d3 is available [check minimum version] if (!window.d3 || !window.d3.hasOwnProperty("version")) { console.error("d3pie error: d3 is not available"); return false; } // confirm element is either a DOM element or a valid string for a DOM element if (!(element instanceof HTMLElement || element instanceof SVGElement)) { console.error("d3pie error: the first d3pie() param must be a valid DOM element (not jQuery) or a ID string."); return false; } // confirm the CSS prefix is valid. It has to start with a-Z and contain nothing but a-Z0-9_- if (!(/[a-zA-Z][a-zA-Z0-9_-]*$/.test(cssPrefix))) { console.error("d3pie error: invalid options.misc.cssPrefix"); return false; } // confirm some data has been supplied if (!helpers.isArray(options.data.content)) { console.error("d3pie error: invalid config structure: missing data.content property."); return false; } if (options.data.content.length === 0) { console.error("d3pie error: no data supplied."); return false; } // clear out any invalid data. Each data row needs a valid positive number and a label var data = []; for (var i=0; i giveupIterationCount) { clearInterval(interval); } inc++; }, 1); }, whenElementsExist: function(els, callback) { var inc = 1; var giveupIterationCount = 1000; var interval = setInterval(function() { var allExist = true; for (var i=0; i giveupIterationCount) { clearInterval(interval); } inc++; }, 1); }, shuffleArray: function(array) { var currentIndex = array.length, tmpVal, randomIndex; while (0 !== currentIndex) { randomIndex = Math.floor(Math.random() * currentIndex); currentIndex -= 1; // and swap it with the current element tmpVal = array[currentIndex]; array[currentIndex] = array[randomIndex]; array[randomIndex] = tmpVal; } return array; }, processObj: function(obj, is, value) { if (typeof is === 'string') { return helpers.processObj(obj, is.split('.'), value); } else if (is.length === 1 && value !== undefined) { obj[is[0]] = value; return obj[is[0]]; } else if (is.length === 0) { return obj; } else { return helpers.processObj(obj[is[0]], is.slice(1), value); } }, getDimensions: function(id) { var el = document.getElementById(id); var w = 0, h = 0; if (el) { var dimensions = el.getBBox(); w = dimensions.width; h = dimensions.height; } else { console.log("error: getDimensions() " + id + " not found."); } return { w: w, h: h }; }, /** * This is based on the SVG coordinate system, where top-left is 0,0 and bottom right is n-n. * @param r1 * @param r2 * @returns {boolean} */ rectIntersect: function(r1, r2) { var returnVal = ( // r2.left > r1.right (r2.x > (r1.x + r1.w)) || // r2.right < r1.left ((r2.x + r2.w) < r1.x) || // r2.top < r1.bottom ((r2.y + r2.h) < r1.y) || // r2.bottom > r1.top (r2.y > (r1.y + r1.h)) ); return !returnVal; }, /** * Returns a lighter/darker shade of a hex value, based on a luminance value passed. * @param hex a hex color value such as “#abc” or “#123456″ (the hash is optional) * @param lum the luminosity factor: -0.1 is 10% darker, 0.2 is 20% lighter, etc. * @returns {string} */ getColorShade: function(hex, lum) { // validate hex string hex = String(hex).replace(/[^0-9a-f]/gi, ''); if (hex.length < 6) { hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2]; } lum = lum || 0; // convert to decimal and change luminosity var newHex = "#"; for (var i=0; i<3; i++) { var c = parseInt(hex.substr(i * 2, 2), 16); c = Math.round(Math.min(Math.max(0, c + (c * lum)), 255)).toString(16); newHex += ("00" + c).substr(c.length); } return newHex; }, /** * Users can choose to specify segment colors in three ways (in order of precedence): * 1. include a "color" attribute for each row in data.content * 2. include a misc.colors.segments property which contains an array of hex codes * 3. specify nothing at all and rely on this lib provide some reasonable defaults * * This function sees what's included and populates this.options.colors with whatever's required * for this pie chart. * @param data */ initSegmentColors: function(pie) { var data = pie.options.data.content; var colors = pie.options.misc.colors.segments; // TODO this needs a ton of error handling var finalColors = []; for (var i=0; i 99) ? 99 : percent; percent = (percent < 0) ? 0 : percent; var smallestDimension = (w < h) ? w : h; // now factor in the label line size if (pie.options.labels.outer.format !== "none") { var pieDistanceSpace = parseInt(pie.options.labels.outer.pieDistance, 10) * 2; if (smallestDimension - pieDistanceSpace > 0) { smallestDimension -= pieDistanceSpace; } } outerRadius = Math.floor((smallestDimension / 100) * percent) / 2; } else { outerRadius = parseInt(size.pieOuterRadius, 10); } } // inner radius if (/%/.test(size.pieInnerRadius)) { percent = parseInt(size.pieInnerRadius.replace(/[\D]/, ""), 10); percent = (percent > 99) ? 99 : percent; percent = (percent < 0) ? 0 : percent; innerRadius = Math.floor((outerRadius / 100) * percent); } else { innerRadius = parseInt(size.pieInnerRadius, 10); } pie.innerRadius = innerRadius; pie.outerRadius = outerRadius; }, getTotalPieSize: function(data) { var totalSize = 0; for (var i=0; i b.label.toLowerCase()) ? 1 : -1; }); break; case "label-desc": data.sort(function(a, b) { return (a.label.toLowerCase() < b.label.toLowerCase()) ? 1 : -1; }); break; } return data; }, // var pieCenter = math.getPieCenter(); getPieTranslateCenter: function(pieCenter) { return "translate(" + pieCenter.x + "," + pieCenter.y + ")"; }, /** * Used to determine where on the canvas the center of the pie chart should be. It takes into account the * height and position of the title, subtitle and footer, and the various paddings. * @private */ calculatePieCenter: function(pie) { var pieCenterOffset = pie.options.misc.pieCenterOffset; var hasTopTitle = (pie.textComponents.title.exists && pie.options.header.location !== "pie-center"); var hasTopSubtitle = (pie.textComponents.subtitle.exists && pie.options.header.location !== "pie-center"); var headerOffset = pie.options.misc.canvasPadding.top; if (hasTopTitle && hasTopSubtitle) { headerOffset += pie.textComponents.title.h + pie.options.header.titleSubtitlePadding + pie.textComponents.subtitle.h; } else if (hasTopTitle) { headerOffset += pie.textComponents.title.h; } else if (hasTopSubtitle) { headerOffset += pie.textComponents.subtitle.h; } var footerOffset = 0; if (pie.textComponents.footer.exists) { footerOffset = pie.textComponents.footer.h + pie.options.misc.canvasPadding.bottom; } var x = ((pie.options.size.canvasWidth - pie.options.misc.canvasPadding.left - pie.options.misc.canvasPadding.right) / 2) + pie.options.misc.canvasPadding.left; var y = ((pie.options.size.canvasHeight - footerOffset - headerOffset) / 2) + headerOffset; x += pieCenterOffset.x; y += pieCenterOffset.y; pie.pieCenter = { x: x, y: y }; }, /** * Rotates a point (x, y) around an axis (xm, ym) by degrees (a). * @param x * @param y * @param xm * @param ym * @param a angle in degrees * @returns {Array} */ rotate: function(x, y, xm, ym, a) { a = a * Math.PI / 180; // convert to radians var cos = Math.cos, sin = Math.sin, // subtract midpoints, so that midpoint is translated to origin and add it in the end again xr = (x - xm) * cos(a) - (y - ym) * sin(a) + xm, yr = (x - xm) * sin(a) + (y - ym) * cos(a) + ym; return { x: xr, y: yr }; }, /** * Translates a point x, y by distance d, and by angle a. * @param x * @param y * @param dist * @param a angle in degrees */ translate: function(x, y, d, a) { var rads = math.toRadians(a); return { x: x + d * Math.sin(rads), y: y - d * Math.cos(rads) }; }, // from: http://stackoverflow.com/questions/19792552/d3-put-arc-labels-in-a-pie-chart-if-there-is-enough-space pointIsInArc: function(pt, ptData, d3Arc) { // Center of the arc is assumed to be 0,0 // (pt.x, pt.y) are assumed to be relative to the center var r1 = d3Arc.innerRadius()(ptData), // Note: Using the innerRadius r2 = d3Arc.outerRadius()(ptData), theta1 = d3Arc.startAngle()(ptData), theta2 = d3Arc.endAngle()(ptData); var dist = pt.x * pt.x + pt.y * pt.y, angle = Math.atan2(pt.x, -pt.y); // Note: different coordinate system angle = (angle < 0) ? (angle + Math.PI * 2) : angle; return (r1 * r1 <= dist) && (dist <= r2 * r2) && (theta1 <= angle) && (angle <= theta2); } }; //// --------- labels.js ----------- var labels = { /** * Adds the labels to the pie chart, but doesn't position them. There are two locations for the * labels: inside (center) of the segments, or outside the segments on the edge. * @param section "inner" or "outer" * @param sectionDisplayType "percentage", "value", "label", "label-value1", etc. * @param pie */ add: function(pie, section, sectionDisplayType) { var include = labels.getIncludes(sectionDisplayType); var settings = pie.options.labels; // group the label groups (label, percentage, value) into a single element for simpler positioning var outerLabel = pie.svg.insert("g", "." + pie.cssPrefix + "labels-" + section) .attr("class", pie.cssPrefix + "labels-" + section); var labelGroup = outerLabel.selectAll("." + pie.cssPrefix + "labelGroup-" + section) .data(pie.options.data.content) .enter() .append("g") .attr("id", function(d, i) { return pie.cssPrefix + "labelGroup" + i + "-" + section; }) .attr("data-index", function(d, i) { return i; }) .attr("class", pie.cssPrefix + "labelGroup-" + section) .style("opacity", 0); var formatterContext = { section: section, sectionDisplayType: sectionDisplayType }; // 1. Add the main label if (include.mainLabel) { labelGroup.append("text") .attr("id", function(d, i) { return pie.cssPrefix + "segmentMainLabel" + i + "-" + section; }) .attr("class", pie.cssPrefix + "segmentMainLabel-" + section) .text(function(d, i) { var str = d.label; // if a custom formatter has been defined, pass it the raw label string - it can do whatever it wants with it. // we only apply truncation if it's not defined if (settings.formatter) { formatterContext.index = i; formatterContext.part = 'mainLabel'; formatterContext.value = d.value; formatterContext.label = str; str = settings.formatter(formatterContext); } else if (settings.truncation.enabled && d.label.length > settings.truncation.truncateLength) { str = d.label.substring(0, settings.truncation.truncateLength) + "..."; } return str; }) .style("font-size", settings.mainLabel.fontSize + "px") .style("font-family", settings.mainLabel.font) .style("fill", settings.mainLabel.color); } // 2. Add the percentage label if (include.percentage) { labelGroup.append("text") .attr("id", function(d, i) { return pie.cssPrefix + "segmentPercentage" + i + "-" + section; }) .attr("class", pie.cssPrefix + "segmentPercentage-" + section) .text(function(d, i) { var percentage = segments.getPercentage(pie, i, pie.options.labels.percentage.decimalPlaces); if (settings.formatter) { formatterContext.index = i; formatterContext.part = "percentage"; formatterContext.value = d.value; formatterContext.label = percentage; percentage = settings.formatter(formatterContext); } else { percentage += "%"; } return percentage; }) .style("font-size", settings.percentage.fontSize + "px") .style("font-family", settings.percentage.font) .style("fill", settings.percentage.color); } // 3. Add the value label if (include.value) { labelGroup.append("text") .attr("id", function(d, i) { return pie.cssPrefix + "segmentValue" + i + "-" + section; }) .attr("class", pie.cssPrefix + "segmentValue-" + section) .text(function(d, i) { formatterContext.index = i; formatterContext.part = "value"; formatterContext.value = d.value; formatterContext.label = d.value; return settings.formatter ? settings.formatter(formatterContext, d.value) : d.value; }) .style("font-size", settings.value.fontSize + "px") .style("font-family", settings.value.font) .style("fill", settings.value.color); } }, /** * @param section "inner" / "outer" */ positionLabelElements: function(pie, section, sectionDisplayType) { labels["dimensions-" + section] = []; // get the latest widths, heights var labelGroups = d3.selectAll("." + pie.cssPrefix + "labelGroup-" + section); labelGroups.each(function(d, i) { var mainLabel = d3.select(this).selectAll("." + pie.cssPrefix + "segmentMainLabel-" + section); var percentage = d3.select(this).selectAll("." + pie.cssPrefix + "segmentPercentage-" + section); var value = d3.select(this).selectAll("." + pie.cssPrefix + "segmentValue-" + section); labels["dimensions-" + section].push({ mainLabel: (mainLabel.node() !== null) ? mainLabel.node().getBBox() : null, percentage: (percentage.node() !== null) ? percentage.node().getBBox() : null, value: (value.node() !== null) ? value.node().getBBox() : null }); }); var singleLinePad = 5; var dims = labels["dimensions-" + section]; switch (sectionDisplayType) { case "label-value1": d3.selectAll("." + pie.cssPrefix + "segmentValue-" + section) .attr("dx", function(d, i) { return dims[i].mainLabel.width + singleLinePad; }); break; case "label-value2": d3.selectAll("." + pie.cssPrefix + "segmentValue-" + section) .attr("dy", function(d, i) { return dims[i].mainLabel.height; }); break; case "label-percentage1": d3.selectAll("." + pie.cssPrefix + "segmentPercentage-" + section) .attr("dx", function(d, i) { return dims[i].mainLabel.width + singleLinePad; }); break; case "label-percentage2": d3.selectAll("." + pie.cssPrefix + "segmentPercentage-" + section) .attr("dx", function(d, i) { return (dims[i].mainLabel.width / 2) - (dims[i].percentage.width / 2); }) .attr("dy", function(d, i) { return dims[i].mainLabel.height; }); break; } }, computeLabelLinePositions: function(pie) { pie.lineCoordGroups = []; d3.selectAll("." + pie.cssPrefix + "labelGroup-outer") .each(function(d, i) { return labels.computeLinePosition(pie, i); }); }, computeLinePosition: function(pie, i) { var angle = segments.getSegmentAngle(i, pie.options.data.content, pie.totalSize, { midpoint: true }); var originCoords = math.rotate(pie.pieCenter.x, pie.pieCenter.y - pie.outerRadius, pie.pieCenter.x, pie.pieCenter.y, angle); var heightOffset = pie.outerLabelGroupData[i].h / 5; // TODO check var labelXMargin = 6; // the x-distance of the label from the end of the line [TODO configurable] var quarter = Math.floor(angle / 90); var midPoint = 4; var x2, y2, x3, y3; // this resolves an issue when the if (quarter === 2 && angle === 180) { quarter = 1; } switch (quarter) { case 0: x2 = pie.outerLabelGroupData[i].x - labelXMargin - ((pie.outerLabelGroupData[i].x - labelXMargin - originCoords.x) / 2); y2 = pie.outerLabelGroupData[i].y + ((originCoords.y - pie.outerLabelGroupData[i].y) / midPoint); x3 = pie.outerLabelGroupData[i].x - labelXMargin; y3 = pie.outerLabelGroupData[i].y - heightOffset; break; case 1: x2 = originCoords.x + (pie.outerLabelGroupData[i].x - originCoords.x) / midPoint; y2 = originCoords.y + (pie.outerLabelGroupData[i].y - originCoords.y) / midPoint; x3 = pie.outerLabelGroupData[i].x - labelXMargin; y3 = pie.outerLabelGroupData[i].y - heightOffset; break; case 2: var startOfLabelX = pie.outerLabelGroupData[i].x + pie.outerLabelGroupData[i].w + labelXMargin; x2 = originCoords.x - (originCoords.x - startOfLabelX) / midPoint; y2 = originCoords.y + (pie.outerLabelGroupData[i].y - originCoords.y) / midPoint; x3 = pie.outerLabelGroupData[i].x + pie.outerLabelGroupData[i].w + labelXMargin; y3 = pie.outerLabelGroupData[i].y - heightOffset; break; case 3: var startOfLabel = pie.outerLabelGroupData[i].x + pie.outerLabelGroupData[i].w + labelXMargin; x2 = startOfLabel + ((originCoords.x - startOfLabel) / midPoint); y2 = pie.outerLabelGroupData[i].y + (originCoords.y - pie.outerLabelGroupData[i].y) / midPoint; x3 = pie.outerLabelGroupData[i].x + pie.outerLabelGroupData[i].w + labelXMargin; y3 = pie.outerLabelGroupData[i].y - heightOffset; break; } /* * x1 / y1: the x/y coords of the start of the line, at the mid point of the segments arc on the pie circumference * x2 / y2: if "curved" line style is being used, this is the midpoint of the line. Other * x3 / y3: the end of the line; closest point to the label */ if (pie.options.labels.lines.style === "straight") { pie.lineCoordGroups[i] = [ { x: originCoords.x, y: originCoords.y }, { x: x3, y: y3 } ]; } else { pie.lineCoordGroups[i] = [ { x: originCoords.x, y: originCoords.y }, { x: x2, y: y2 }, { x: x3, y: y3 } ]; } }, addLabelLines: function(pie) { var lineGroups = pie.svg.insert("g", "." + pie.cssPrefix + "pieChart") // meaning, BEFORE .pieChart .attr("class", pie.cssPrefix + "lineGroups") .style("opacity", 0); var lineGroup = lineGroups.selectAll("." + pie.cssPrefix + "lineGroup") .data(pie.lineCoordGroups) .enter() .append("g") .attr("class", pie.cssPrefix + "lineGroup"); var lineFunction = d3.svg.line() .interpolate("basis") .x(function(d) { return d.x; }) .y(function(d) { return d.y; }); lineGroup.append("path") .attr("d", lineFunction) .attr("stroke", function(d, i) { return (pie.options.labels.lines.color === "segment") ? pie.options.colors[i] : pie.options.labels.lines.color; }) .attr("stroke-width", 1) .attr("fill", "none") .style("opacity", function(d, i) { var percentage = pie.options.labels.outer.hideWhenLessThanPercentage; var segmentPercentage = segments.getPercentage(pie, i, pie.options.labels.percentage.decimalPlaces); var isHidden = (percentage !== null && segmentPercentage < percentage) || pie.options.data.content[i].label === ""; return isHidden ? 0 : 1; }); }, positionLabelGroups: function(pie, section) { if (pie.options.labels[section].format === "none") { return; } d3.selectAll("." + pie.cssPrefix + "labelGroup-" + section) .style("opacity", 0) .attr("transform", function(d, i) { var x, y; if (section === "outer") { x = pie.outerLabelGroupData[i].x; y = pie.outerLabelGroupData[i].y; } else { var pieCenterCopy = extend(true, {}, pie.pieCenter); // now recompute the "center" based on the current _innerRadius if (pie.innerRadius > 0) { var angle = segments.getSegmentAngle(i, pie.options.data.content, pie.totalSize, { midpoint: true }); var newCoords = math.translate(pie.pieCenter.x, pie.pieCenter.y, pie.innerRadius, angle); pieCenterCopy.x = newCoords.x; pieCenterCopy.y = newCoords.y; } var dims = helpers.getDimensions(pie.cssPrefix + "labelGroup" + i + "-inner"); var xOffset = dims.w / 2; var yOffset = dims.h / 4; // confusing! Why 4? should be 2, but it doesn't look right x = pieCenterCopy.x + (pie.lineCoordGroups[i][0].x - pieCenterCopy.x) / 1.8; y = pieCenterCopy.y + (pie.lineCoordGroups[i][0].y - pieCenterCopy.y) / 1.8; x = x - xOffset; y = y + yOffset; } return "translate(" + x + "," + y + ")"; }); }, fadeInLabelsAndLines: function(pie) { // fade in the labels when the load effect is complete - or immediately if there's no load effect var loadSpeed = (pie.options.effects.load.effect === "default") ? pie.options.effects.load.speed : 1; setTimeout(function() { var labelFadeInTime = (pie.options.effects.load.effect === "default") ? 400 : 1; // 400 is hardcoded for the present d3.selectAll("." + pie.cssPrefix + "labelGroup-outer") .transition() .duration(labelFadeInTime) .style("opacity", function(d, i) { var percentage = pie.options.labels.outer.hideWhenLessThanPercentage; var segmentPercentage = segments.getPercentage(pie, i, pie.options.labels.percentage.decimalPlaces); return (percentage !== null && segmentPercentage < percentage) ? 0 : 1; }); d3.selectAll("." + pie.cssPrefix + "labelGroup-inner") .transition() .duration(labelFadeInTime) .style("opacity", function(d, i) { var percentage = pie.options.labels.inner.hideWhenLessThanPercentage; var segmentPercentage = segments.getPercentage(pie, i, pie.options.labels.percentage.decimalPlaces); return (percentage !== null && segmentPercentage < percentage) ? 0 : 1; }); d3.selectAll("g." + pie.cssPrefix + "lineGroups") .transition() .duration(labelFadeInTime) .style("opacity", 1); // once everything's done loading, trigger the onload callback if defined if (helpers.isFunction(pie.options.callbacks.onload)) { setTimeout(function() { try { pie.options.callbacks.onload(); } catch (e) { } }, labelFadeInTime); } }, loadSpeed); }, getIncludes: function(val) { var addMainLabel = false; var addValue = false; var addPercentage = false; switch (val) { case "label": addMainLabel = true; break; case "value": addValue = true; break; case "percentage": addPercentage = true; break; case "label-value1": case "label-value2": addMainLabel = true; addValue = true; break; case "label-percentage1": case "label-percentage2": addMainLabel = true; addPercentage = true; break; } return { mainLabel: addMainLabel, value: addValue, percentage: addPercentage }; }, /** * This does the heavy-lifting to compute the actual coordinates for the outer label groups. It does two things: * 1. Make a first pass and position them in the ideal positions, based on the pie sizes * 2. Do some basic collision avoidance. */ computeOuterLabelCoords: function(pie) { // 1. figure out the ideal positions for the outer labels pie.svg.selectAll("." + pie.cssPrefix + "labelGroup-outer") .each(function(d, i) { return labels.getIdealOuterLabelPositions(pie, i); }); // 2. now adjust those positions to try to accommodate conflicts labels.resolveOuterLabelCollisions(pie); }, /** * This attempts to resolve label positioning collisions. */ resolveOuterLabelCollisions: function(pie) { if (pie.options.labels.outer.format === "none") { return; } var size = pie.options.data.content.length; labels.checkConflict(pie, 0, "clockwise", size); labels.checkConflict(pie, size-1, "anticlockwise", size); }, checkConflict: function(pie, currIndex, direction, size) { var i, curr; if (size <= 1) { return; } var currIndexHemisphere = pie.outerLabelGroupData[currIndex].hs; if (direction === "clockwise" && currIndexHemisphere !== "right") { return; } if (direction === "anticlockwise" && currIndexHemisphere !== "left") { return; } var nextIndex = (direction === "clockwise") ? currIndex+1 : currIndex-1; // this is the current label group being looked at. We KNOW it's positioned properly (the first item // is always correct) var currLabelGroup = pie.outerLabelGroupData[currIndex]; // this one we don't know about. That's the one we're going to look at and move if necessary var examinedLabelGroup = pie.outerLabelGroupData[nextIndex]; var info = { labelHeights: pie.outerLabelGroupData[0].h, center: pie.pieCenter, lineLength: (pie.outerRadius + pie.options.labels.outer.pieDistance), heightChange: pie.outerLabelGroupData[0].h + 1 // 1 = padding }; // loop through *ALL* label groups examined so far to check for conflicts. This is because when they're // very tightly fitted, a later label group may still appear high up on the page if (direction === "clockwise") { i = 0; for (; i<=currIndex; i++) { curr = pie.outerLabelGroupData[i]; // if there's a conflict with this label group, shift the label to be AFTER the last known // one that's been properly placed if (helpers.rectIntersect(curr, examinedLabelGroup)) { labels.adjustLabelPos(pie, nextIndex, currLabelGroup, info); break; } } } else { i = size - 1; for (; i >= currIndex; i--) { curr = pie.outerLabelGroupData[i]; // if there's a conflict with this label group, shift the label to be AFTER the last known // one that's been properly placed if (helpers.rectIntersect(curr, examinedLabelGroup)) { labels.adjustLabelPos(pie, nextIndex, currLabelGroup, info); break; } } } labels.checkConflict(pie, nextIndex, direction, size); }, // does a little math to shift a label into a new position based on the last properly placed one adjustLabelPos: function(pie, nextIndex, lastCorrectlyPositionedLabel, info) { var xDiff, yDiff, newXPos, newYPos; newYPos = lastCorrectlyPositionedLabel.y + info.heightChange; yDiff = info.center.y - newYPos; if (Math.abs(info.lineLength) > Math.abs(yDiff)) { xDiff = Math.sqrt((info.lineLength * info.lineLength) - (yDiff * yDiff)); } else { xDiff = Math.sqrt((yDiff * yDiff) - (info.lineLength * info.lineLength)); } if (lastCorrectlyPositionedLabel.hs === "right") { newXPos = info.center.x + xDiff; } else { newXPos = info.center.x - xDiff - pie.outerLabelGroupData[nextIndex].w; } pie.outerLabelGroupData[nextIndex].x = newXPos; pie.outerLabelGroupData[nextIndex].y = newYPos; }, /** * @param i 0-N where N is the dataset size - 1. */ getIdealOuterLabelPositions: function(pie, i) { var labelGroupNode = d3.select("#" + pie.cssPrefix + "labelGroup" + i + "-outer").node(); if (!labelGroupNode) { return; } var labelGroupDims = labelGroupNode.getBBox(); var angle = segments.getSegmentAngle(i, pie.options.data.content, pie.totalSize, { midpoint: true }); var originalX = pie.pieCenter.x; var originalY = pie.pieCenter.y - (pie.outerRadius + pie.options.labels.outer.pieDistance); var newCoords = math.rotate(originalX, originalY, pie.pieCenter.x, pie.pieCenter.y, angle); // if the label is on the left half of the pie, adjust the values var hemisphere = "right"; // hemisphere if (angle > 180) { newCoords.x -= (labelGroupDims.width + 8); hemisphere = "left"; } else { newCoords.x += 8; } pie.outerLabelGroupData[i] = { x: newCoords.x, y: newCoords.y, w: labelGroupDims.width, h: labelGroupDims.height, hs: hemisphere }; } }; //// --------- segments.js ----------- var segments = { /** * Creates the pie chart segments and displays them according to the desired load effect. * @private */ create: function(pie) { var pieCenter = pie.pieCenter; var colors = pie.options.colors; var loadEffects = pie.options.effects.load; var segmentStroke = pie.options.misc.colors.segmentStroke; // we insert the pie chart BEFORE the title, to ensure the title overlaps the pie var pieChartElement = pie.svg.insert("g", "#" + pie.cssPrefix + "title") .attr("transform", function() { return math.getPieTranslateCenter(pieCenter); }) .attr("class", pie.cssPrefix + "pieChart"); var arc = d3.svg.arc() .innerRadius(pie.innerRadius) .outerRadius(pie.outerRadius) .startAngle(0) .endAngle(function(d) { return (d.value / pie.totalSize) * 2 * Math.PI; }); var g = pieChartElement.selectAll("." + pie.cssPrefix + "arc") .data(pie.options.data.content) .enter() .append("g") .attr("class", pie.cssPrefix + "arc"); // if we're not fading in the pie, just set the load speed to 0 var loadSpeed = loadEffects.speed; if (loadEffects.effect === "none") { loadSpeed = 0; } g.append("path") .attr("id", function(d, i) { return pie.cssPrefix + "segment" + i; }) .attr("fill", function(d, i) { var color = colors[i]; if (pie.options.misc.gradient.enabled) { color = "url(#" + pie.cssPrefix + "grad" + i + ")"; } return color; }) .style("stroke", segmentStroke) .style("stroke-width", 1) .transition() .ease("cubic-in-out") .duration(loadSpeed) .attr("data-index", function(d, i) { return i; }) .attrTween("d", function(b) { var i = d3.interpolate({ value: 0 }, b); return function(t) { return pie.arc(i(t)); }; }); pie.svg.selectAll("g." + pie.cssPrefix + "arc") .attr("transform", function(d, i) { var angle = 0; if (i > 0) { angle = segments.getSegmentAngle(i-1, pie.options.data.content, pie.totalSize); } return "rotate(" + angle + ")"; } ); pie.arc = arc; }, addGradients: function(pie) { var grads = pie.svg.append("defs") .selectAll("radialGradient") .data(pie.options.data.content) .enter().append("radialGradient") .attr("gradientUnits", "userSpaceOnUse") .attr("cx", 0) .attr("cy", 0) .attr("r", "120%") .attr("id", function(d, i) { return pie.cssPrefix + "grad" + i; }); grads.append("stop").attr("offset", "0%").style("stop-color", function(d, i) { return pie.options.colors[i]; }); grads.append("stop").attr("offset", pie.options.misc.gradient.percentage + "%").style("stop-color", pie.options.misc.gradient.color); }, addSegmentEventHandlers: function(pie) { var arc = d3.selectAll("." + pie.cssPrefix + "arc,." + pie.cssPrefix + "labelGroup-inner,." + pie.cssPrefix + "labelGroup-outer"); arc.on("click", function() { var currentEl = d3.select(this); var segment; // mouseover works on both the segments AND the segment labels, hence the following if (currentEl.attr("class") === pie.cssPrefix + "arc") { segment = currentEl.select("path"); } else { var index = currentEl.attr("data-index"); segment = d3.select("#" + pie.cssPrefix + "segment" + index); } var isExpanded = segment.attr("class") === pie.cssPrefix + "expanded"; segments.onSegmentEvent(pie, pie.options.callbacks.onClickSegment, segment, isExpanded); if (pie.options.effects.pullOutSegmentOnClick.effect !== "none") { if (isExpanded) { segments.closeSegment(pie, segment.node()); } else { segments.openSegment(pie, segment.node()); } } }); arc.on("mouseover", function() { var currentEl = d3.select(this); var segment, index; if (currentEl.attr("class") === pie.cssPrefix + "arc") { segment = currentEl.select("path"); } else { index = currentEl.attr("data-index"); segment = d3.select("#" + pie.cssPrefix + "segment" + index); } if (pie.options.effects.highlightSegmentOnMouseover) { index = segment.attr("data-index"); var segColor = pie.options.colors[index]; segment.style("fill", helpers.getColorShade(segColor, pie.options.effects.highlightLuminosity)); } if (pie.options.tooltips.enabled) { index = segment.attr("data-index"); tt.showTooltip(pie, index); } var isExpanded = segment.attr("class") === pie.cssPrefix + "expanded"; segments.onSegmentEvent(pie, pie.options.callbacks.onMouseoverSegment, segment, isExpanded); }); arc.on("mousemove", function() { tt.moveTooltip(pie); }); arc.on("mouseout", function() { var currentEl = d3.select(this); var segment, index; if (currentEl.attr("class") === pie.cssPrefix + "arc") { segment = currentEl.select("path"); } else { index = currentEl.attr("data-index"); segment = d3.select("#" + pie.cssPrefix + "segment" + index); } if (pie.options.effects.highlightSegmentOnMouseover) { index = segment.attr("data-index"); var color = pie.options.colors[index]; if (pie.options.misc.gradient.enabled) { color = "url(#" + pie.cssPrefix + "grad" + index + ")"; } segment.style("fill", color); } if (pie.options.tooltips.enabled) { index = segment.attr("data-index"); tt.hideTooltip(pie, index); } var isExpanded = segment.attr("class") === pie.cssPrefix + "expanded"; segments.onSegmentEvent(pie, pie.options.callbacks.onMouseoutSegment, segment, isExpanded); }); }, // helper function used to call the click, mouseover, mouseout segment callback functions onSegmentEvent: function(pie, func, segment, isExpanded) { if (!helpers.isFunction(func)) { return; } var index = parseInt(segment.attr("data-index"), 10); func({ segment: segment.node(), index: index, expanded: isExpanded, data: pie.options.data.content[index] }); }, openSegment: function(pie, segment) { if (pie.isOpeningSegment) { return; } pie.isOpeningSegment = true; // close any open segments if (d3.selectAll("." + pie.cssPrefix + "expanded").length > 0) { segments.closeSegment(pie, d3.select("." + pie.cssPrefix + "expanded").node()); } d3.select(segment).transition() .ease(pie.options.effects.pullOutSegmentOnClick.effect) .duration(pie.options.effects.pullOutSegmentOnClick.speed) .attr("transform", function(d, i) { var c = pie.arc.centroid(d), x = c[0], y = c[1], h = Math.sqrt(x*x + y*y), pullOutSize = parseInt(pie.options.effects.pullOutSegmentOnClick.size, 10); return "translate(" + ((x/h) * pullOutSize) + ',' + ((y/h) * pullOutSize) + ")"; }) .each("end", function(d, i) { pie.currentlyOpenSegment = segment; pie.isOpeningSegment = false; d3.select(this).attr("class", pie.cssPrefix + "expanded"); }); }, closeSegment: function(pie, segment) { d3.select(segment).transition() .duration(400) .attr("transform", "translate(0,0)") .each("end", function(d, i) { d3.select(this).attr("class", ""); pie.currentlyOpenSegment = null; }); }, getCentroid: function(el) { var bbox = el.getBBox(); return { x: bbox.x + bbox.width / 2, y: bbox.y + bbox.height / 2 }; }, /** * General helper function to return a segment's angle, in various different ways. * @param index * @param opts optional object for fine-tuning exactly what you want. */ getSegmentAngle: function(index, data, totalSize, opts) { var options = extend({ // if true, this returns the full angle from the origin. Otherwise it returns the single segment angle compounded: true, // optionally returns the midpoint of the angle instead of the full angle midpoint: false }, opts); var currValue = data[index].value; var fullValue; if (options.compounded) { fullValue = 0; // get all values up to and including the specified index for (var i=0; i<=index; i++) { fullValue += data[i].value; } } if (typeof fullValue === 'undefined') { fullValue = currValue; } // now convert the full value to an angle var angle = (fullValue / totalSize) * 360; // lastly, if we want the midpoint, factor that sucker in if (options.midpoint) { var currAngle = (currValue / totalSize) * 360; angle -= (currAngle / 2); } return angle; }, getPercentage: function(pie, index, decimalPlaces) { var relativeAmount = pie.options.data.content[index].value / pie.totalSize; if (decimalPlaces <= 0) { return Math.round(relativeAmount * 100); } else { return (relativeAmount * 100).toFixed(decimalPlaces); } } }; //// --------- text.js ----------- var text = { offscreenCoord: -10000, addTitle: function(pie) { var title = pie.svg.selectAll("." + pie.cssPrefix + "title") .data([pie.options.header.title]) .enter() .append("text") .text(function(d) { return d.text; }) .attr({ id: pie.cssPrefix + "title", class: pie.cssPrefix + "title", x: text.offscreenCoord, y: text.offscreenCoord }) .attr("text-anchor", function() { var location; if (pie.options.header.location === "top-center" || pie.options.header.location === "pie-center") { location = "middle"; } else { location = "left"; } return location; }) .attr("fill", function(d) { return d.color; }) .style("font-size", function(d) { return d.fontSize + "px"; }) .style("font-family", function(d) { return d.font; }); }, positionTitle: function(pie) { var textComponents = pie.textComponents; var headerLocation = pie.options.header.location; var canvasPadding = pie.options.misc.canvasPadding; var canvasWidth = pie.options.size.canvasWidth; var titleSubtitlePadding = pie.options.header.titleSubtitlePadding; var x; if (headerLocation === "top-left") { x = canvasPadding.left; } else { x = ((canvasWidth - canvasPadding.right) / 2) + canvasPadding.left; } // add whatever offset has been added by user x += pie.options.misc.pieCenterOffset.x; var y = canvasPadding.top + textComponents.title.h; if (headerLocation === "pie-center") { y = pie.pieCenter.y; // still not fully correct if (textComponents.subtitle.exists) { var totalTitleHeight = textComponents.title.h + titleSubtitlePadding + textComponents.subtitle.h; y = y - (totalTitleHeight / 2) + textComponents.title.h; } else { y += (textComponents.title.h / 4); } } pie.svg.select("#" + pie.cssPrefix + "title") .attr("x", x) .attr("y", y); }, addSubtitle: function(pie) { var headerLocation = pie.options.header.location; pie.svg.selectAll("." + pie.cssPrefix + "subtitle") .data([pie.options.header.subtitle]) .enter() .append("text") .text(function(d) { return d.text; }) .attr("x", text.offscreenCoord) .attr("y", text.offscreenCoord) .attr("id", pie.cssPrefix + "subtitle") .attr("class", pie.cssPrefix + "subtitle") .attr("text-anchor", function() { var location; if (headerLocation === "top-center" || headerLocation === "pie-center") { location = "middle"; } else { location = "left"; } return location; }) .attr("fill", function(d) { return d.color; }) .style("font-size", function(d) { return d.fontSize + "px"; }) .style("font-family", function(d) { return d.font; }); }, positionSubtitle: function(pie) { var canvasPadding = pie.options.misc.canvasPadding; var canvasWidth = pie.options.size.canvasWidth; var x; if (pie.options.header.location === "top-left") { x = canvasPadding.left; } else { x = ((canvasWidth - canvasPadding.right) / 2) + canvasPadding.left; } // add whatever offset has been added by user x += pie.options.misc.pieCenterOffset.x; var y = text.getHeaderHeight(pie); pie.svg.select("#" + pie.cssPrefix + "subtitle") .attr("x", x) .attr("y", y); }, addFooter: function(pie) { pie.svg.selectAll("." + pie.cssPrefix + "footer") .data([pie.options.footer]) .enter() .append("text") .text(function(d) { return d.text; }) .attr("x", text.offscreenCoord) .attr("y", text.offscreenCoord) .attr("id", pie.cssPrefix + "footer") .attr("class", pie.cssPrefix + "footer") .attr("text-anchor", function() { var location = "left"; if (pie.options.footer.location === "bottom-center") { location = "middle"; } else if (pie.options.footer.location === "bottom-right") { location = "left"; // on purpose. We have to change the x-coord to make it properly right-aligned } return location; }) .attr("fill", function(d) { return d.color; }) .style("font-size", function(d) { return d.fontSize + "px"; }) .style("font-family", function(d) { return d.font; }); }, positionFooter: function(pie) { var footerLocation = pie.options.footer.location; var footerWidth = pie.textComponents.footer.w; var canvasWidth = pie.options.size.canvasWidth; var canvasHeight = pie.options.size.canvasHeight; var canvasPadding = pie.options.misc.canvasPadding; var x; if (footerLocation === "bottom-left") { x = canvasPadding.left; } else if (footerLocation === "bottom-right") { x = canvasWidth - footerWidth - canvasPadding.right; } else { x = canvasWidth / 2; // TODO - shouldn't this also take into account padding? } pie.svg.select("#" + pie.cssPrefix + "footer") .attr("x", x) .attr("y", canvasHeight - canvasPadding.bottom); }, getHeaderHeight: function(pie) { var h; if (pie.textComponents.title.exists) { // if the subtitle isn't defined, it'll be set to 0 var totalTitleHeight = pie.textComponents.title.h + pie.options.header.titleSubtitlePadding + pie.textComponents.subtitle.h; if (pie.options.header.location === "pie-center") { h = pie.pieCenter.y - (totalTitleHeight / 2) + totalTitleHeight; } else { h = totalTitleHeight + pie.options.misc.canvasPadding.top; } } else { if (pie.options.header.location === "pie-center") { var footerPlusPadding = pie.options.misc.canvasPadding.bottom + pie.textComponents.footer.h; h = ((pie.options.size.canvasHeight - footerPlusPadding) / 2) + pie.options.misc.canvasPadding.top + (pie.textComponents.subtitle.h / 2); } else { h = pie.options.misc.canvasPadding.top + pie.textComponents.subtitle.h; } } return h; } }; //// --------- validate.js ----------- var tt = { addTooltips: function(pie) { // group the label groups (label, percentage, value) into a single element for simpler positioning var tooltips = pie.svg.insert("g") .attr("class", pie.cssPrefix + "tooltips"); tooltips.selectAll("." + pie.cssPrefix + "tooltip") .data(pie.options.data.content) .enter() .append("g") .attr("class", pie.cssPrefix + "tooltip") .attr("id", function(d, i) { return pie.cssPrefix + "tooltip" + i; }) .style("opacity", 0) .append("rect") .attr({ rx: pie.options.tooltips.styles.borderRadius, ry: pie.options.tooltips.styles.borderRadius, x: -pie.options.tooltips.styles.padding, opacity: pie.options.tooltips.styles.backgroundOpacity }) .style("fill", pie.options.tooltips.styles.backgroundColor); tooltips.selectAll("." + pie.cssPrefix + "tooltip") .data(pie.options.data.content) .append("text") .attr("fill", function(d) { return pie.options.tooltips.styles.color; }) .style("font-size", function(d) { return pie.options.tooltips.styles.fontSize; }) .style("font-family", function(d) { return pie.options.tooltips.styles.font; }) .text(function(d, i) { var caption = pie.options.tooltips.string; if (pie.options.tooltips.type === "caption") { caption = d.caption; } return tt.replacePlaceholders(pie, caption, i, { label: d.label, value: d.value, percentage: segments.getPercentage(pie, i, pie.options.labels.percentage.decimalPlaces) }); }); tooltips.selectAll("." + pie.cssPrefix + "tooltip rect") .attr({ width: function (d, i) { var dims = helpers.getDimensions(pie.cssPrefix + "tooltip" + i); return dims.w + (2 * pie.options.tooltips.styles.padding); }, height: function (d, i) { var dims = helpers.getDimensions(pie.cssPrefix + "tooltip" + i); return dims.h + (2 * pie.options.tooltips.styles.padding); }, y: function (d, i) { var dims = helpers.getDimensions(pie.cssPrefix + "tooltip" + i); return -(dims.h / 2) + 1; } }); }, showTooltip: function(pie, index) { var fadeInSpeed = pie.options.tooltips.styles.fadeInSpeed; if (tt.currentTooltip === index) { fadeInSpeed = 1; } tt.currentTooltip = index; d3.select("#" + pie.cssPrefix + "tooltip" + index) .transition() .duration(fadeInSpeed) .style("opacity", function() { return 1; }); tt.moveTooltip(pie); }, moveTooltip: function(pie) { d3.selectAll("#" + pie.cssPrefix + "tooltip" + tt.currentTooltip) .attr("transform", function(d) { var mouseCoords = d3.mouse(this.parentNode); var x = mouseCoords[0] + pie.options.tooltips.styles.padding + 2; var y = mouseCoords[1] - (2 * pie.options.tooltips.styles.padding) - 2; return "translate(" + x + "," + y + ")"; }); }, hideTooltip: function(pie, index) { d3.select("#" + pie.cssPrefix + "tooltip" + index) .style("opacity", function() { return 0; }); // move the tooltip offscreen. This ensures that when the user next mouseovers the segment the hidden // element won't interfere d3.select("#" + pie.cssPrefix + "tooltip" + tt.currentTooltip) .attr("transform", function(d, i) { // klutzy, but it accounts for tooltip padding which could push it onscreen var x = pie.options.size.canvasWidth + 1000; var y = pie.options.size.canvasHeight + 1000; return "translate(" + x + "," + y + ")"; }); }, replacePlaceholders: function(pie, str, index, replacements) { // if the user has defined a placeholderParser function, call it before doing the replacements if (helpers.isFunction(pie.options.tooltips.placeholderParser)) { pie.options.tooltips.placeholderParser(index, replacements); } var replacer = function() { return function(match) { var placeholder = arguments[1]; if (replacements.hasOwnProperty(placeholder)) { return replacements[arguments[1]]; } else { return arguments[0]; } }; }; return str.replace(/\{(\w+)\}/g, replacer(replacements)); } }; // -------------------------------------------------------------------------------------------- // our constructor var d3pie = function(element, options) { // element can be an ID or DOM element this.element = element; if (typeof element === "string") { var el = element.replace(/^#/, ""); // replace any jQuery-like ID hash char this.element = document.getElementById(el); } var opts = {}; extend(true, opts, defaultSettings, options); this.options = opts; // if the user specified a custom CSS element prefix (ID, class), use it if (this.options.misc.cssPrefix !== null) { this.cssPrefix = this.options.misc.cssPrefix; } else { this.cssPrefix = "p" + _uniqueIDCounter + "_"; _uniqueIDCounter++; } // now run some validation on the user-defined info if (!validate.initialCheck(this)) { return; } // add a data-role to the DOM node to let anyone know that it contains a d3pie instance, and the d3pie version d3.select(this.element).attr(_scriptName, _version); // things that are done once this.options.data.content = math.sortPieData(this); if (this.options.data.smallSegmentGrouping.enabled) { this.options.data.content = helpers.applySmallSegmentGrouping(this.options.data.content, this.options.data.smallSegmentGrouping); } this.options.colors = helpers.initSegmentColors(this); this.totalSize = math.getTotalPieSize(this.options.data.content); _init.call(this); }; d3pie.prototype.recreate = function() { // now run some validation on the user-defined info if (!validate.initialCheck(this)) { return; } this.options.data.content = math.sortPieData(this); if (this.options.data.smallSegmentGrouping.enabled) { this.options.data.content = helpers.applySmallSegmentGrouping(this.options.data.content, this.options.data.smallSegmentGrouping); } this.options.colors = helpers.initSegmentColors(this); this.totalSize = math.getTotalPieSize(this.options.data.content); _init.call(this); }; d3pie.prototype.redraw = function() { this.element.innerHTML = ""; _init.call(this); }; d3pie.prototype.destroy = function() { this.element.innerHTML = ""; // clear out the SVG d3.select(this.element).attr(_scriptName, null); // remove the data attr }; /** * Returns all pertinent info about the current open info. Returns null if nothing's open, or if one is, an object of * the following form: * { * element: DOM NODE, * index: N, * data: {} * } */ d3pie.prototype.getOpenSegment = function() { var segment = this.currentlyOpenSegment; if (segment !== null && typeof segment !== "undefined") { var index = parseInt(d3.select(segment).attr("data-index"), 10); return { element: segment, index: index, data: this.options.data.content[index] }; } else { return null; } }; d3pie.prototype.openSegment = function(index) { index = parseInt(index, 10); if (index < 0 || index > this.options.data.content.length-1) { return; } segments.openSegment(this, d3.select("#" + this.cssPrefix + "segment" + index).node()); }; d3pie.prototype.closeSegment = function() { var segment = this.currentlyOpenSegment; if (segment) { segments.closeSegment(this, segment); } }; // this let's the user dynamically update aspects of the pie chart without causing a complete redraw. It // intelligently re-renders only the part of the pie that the user specifies. Some things cause a repaint, others // just redraw the single element d3pie.prototype.updateProp = function(propKey, value) { switch (propKey) { case "header.title.text": var oldVal = helpers.processObj(this.options, propKey); helpers.processObj(this.options, propKey, value); d3.select("#" + this.cssPrefix + "title").html(value); if ((oldVal === "" && value !== "") || (oldVal !== "" && value === "")) { this.redraw(); } break; case "header.subtitle.text": var oldValue = helpers.processObj(this.options, propKey); helpers.processObj(this.options, propKey, value); d3.select("#" + this.cssPrefix + "subtitle").html(value); if ((oldValue === "" && value !== "") || (oldValue !== "" && value === "")) { this.redraw(); } break; case "callbacks.onload": case "callbacks.onMouseoverSegment": case "callbacks.onMouseoutSegment": case "callbacks.onClickSegment": case "effects.pullOutSegmentOnClick.effect": case "effects.pullOutSegmentOnClick.speed": case "effects.pullOutSegmentOnClick.size": case "effects.highlightSegmentOnMouseover": case "effects.highlightLuminosity": helpers.processObj(this.options, propKey, value); break; // everything else, attempt to update it & do a repaint default: helpers.processObj(this.options, propKey, value); this.destroy(); this.recreate(); break; } }; // ------------------------------------------------------------------------------------------------ var _init = function() { // prep-work this.svg = helpers.addSVGSpace(this); // store info about the main text components as part of the d3pie object instance this.textComponents = { headerHeight: 0, title: { exists: this.options.header.title.text !== "", h: 0, w: 0 }, subtitle: { exists: this.options.header.subtitle.text !== "", h: 0, w: 0 }, footer: { exists: this.options.footer.text !== "", h: 0, w: 0 } }; this.outerLabelGroupData = []; // add the key text components offscreen (title, subtitle, footer). We need to know their widths/heights for later computation if (this.textComponents.title.exists) { text.addTitle(this); } if (this.textComponents.subtitle.exists) { text.addSubtitle(this); } text.addFooter(this); // the footer never moves. Put it in place now var self = this; helpers.whenIdExists(this.cssPrefix + "footer", function() { text.positionFooter(self); var d3 = helpers.getDimensions(self.cssPrefix + "footer"); self.textComponents.footer.h = d3.h; self.textComponents.footer.w = d3.w; }); // now create the pie chart and position everything accordingly var reqEls = []; if (this.textComponents.title.exists) { reqEls.push(this.cssPrefix + "title"); } if (this.textComponents.subtitle.exists) { reqEls.push(this.cssPrefix + "subtitle"); } if (this.textComponents.footer.exists) { reqEls.push(this.cssPrefix + "footer"); } helpers.whenElementsExist(reqEls, function() { if (self.textComponents.title.exists) { var d1 = helpers.getDimensions(self.cssPrefix + "title"); self.textComponents.title.h = d1.h; self.textComponents.title.w = d1.w; } if (self.textComponents.subtitle.exists) { var d2 = helpers.getDimensions(self.cssPrefix + "subtitle"); self.textComponents.subtitle.h = d2.h; self.textComponents.subtitle.w = d2.w; } // now compute the full header height if (self.textComponents.title.exists || self.textComponents.subtitle.exists) { var headerHeight = 0; if (self.textComponents.title.exists) { headerHeight += self.textComponents.title.h; if (self.textComponents.subtitle.exists) { headerHeight += self.options.header.titleSubtitlePadding; } } if (self.textComponents.subtitle.exists) { headerHeight += self.textComponents.subtitle.h; } self.textComponents.headerHeight = headerHeight; } // at this point, all main text component dimensions have been calculated math.computePieRadius(self); // this value is used all over the place for placing things and calculating locations. We figure it out ONCE // and store it as part of the object math.calculatePieCenter(self); // position the title and subtitle text.positionTitle(self); text.positionSubtitle(self); // now create the pie chart segments, and gradients if the user desired if (self.options.misc.gradient.enabled) { segments.addGradients(self); } segments.create(self); // also creates this.arc labels.add(self, "inner", self.options.labels.inner.format); labels.add(self, "outer", self.options.labels.outer.format); // position the label elements relatively within their individual group (label, percentage, value) labels.positionLabelElements(self, "inner", self.options.labels.inner.format); labels.positionLabelElements(self, "outer", self.options.labels.outer.format); labels.computeOuterLabelCoords(self); // this is (and should be) dumb. It just places the outer groups at their calculated, collision-free positions labels.positionLabelGroups(self, "outer"); // we use the label line positions for many other calculations, so ALWAYS compute them labels.computeLabelLinePositions(self); // only add them if they're actually enabled if (self.options.labels.lines.enabled && self.options.labels.outer.format !== "none") { labels.addLabelLines(self); } labels.positionLabelGroups(self, "inner"); labels.fadeInLabelsAndLines(self); // add and position the tooltips if (self.options.tooltips.enabled) { tt.addTooltips(self); } segments.addSegmentEventHandlers(self); }); }; return d3pie; }));