diff options
Diffstat (limited to 'web/lib/d3pie-0.2.1-netdata-3.js')
-rw-r--r-- | web/lib/d3pie-0.2.1-netdata-3.js | 2123 |
1 files changed, 0 insertions, 2123 deletions
diff --git a/web/lib/d3pie-0.2.1-netdata-3.js b/web/lib/d3pie-0.2.1-netdata-3.js deleted file mode 100644 index e6ef064af..000000000 --- a/web/lib/d3pie-0.2.1-netdata-3.js +++ /dev/null @@ -1,2123 +0,0 @@ -/*! - * d3pie - * @author Ben Keen - * @version 0.1.9 - * @date June 17th, 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(); - } else { - // browser globals (root is window) - root.d3pie = factory(root); - } -}(this, function() { - - var _scriptName = "d3pie"; - var _version = "0.2.1"; - - // 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, - fontWeight: "bold", - font: "arial" - }, - subtitle: { - text: "", - color: "#666666", - fontSize: 14, - fontWeight: "bold", - font: "arial" - }, - location: "top-center", - titleSubtitlePadding: 8 - }, - footer: { - text: "", - color: "#666666", - fontSize: 14, - fontWeight: "bold", - 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", - fontWeight: "normal", - fontSize: 10 - }, - percentage: { - color: "#dddddd", - font: "arial", - fontWeight: "bold", - fontSize: 10, - decimalPlaces: 0 - }, - value: { - color: "#cccc44", - fontWeight: "bold", - font: "arial", - fontSize: 10 - }, - lines: { - enabled: true, - style: "curved", - color: "segment" - }, - truncation: { - enabled: false, - truncateLength: 30 - }, - formatter: null - }, - effects: { - load: { - effect: "none", // "default", commented in the code - speed: 1000 - }, - pullOutSegmentOnClick: { - effect: "none", // "bounce", commented in the code - speed: 300, - size: 10 - }, - highlightSegmentOnMouseover: false, - 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", - fontWeight: "bold", - 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<options.data.content.length; i++) { - if (typeof options.data.content[i].value !== "number" || isNaN(options.data.content[i].value)) { - console.log("not valid: ", options.data.content[i]); - continue; - } - if (options.data.content[i].value <= 0) { - console.log("not valid - should have positive value: ", options.data.content[i]); - continue; - } - data.push(options.data.content[i]); - } - pie.options.data.content = data; - - // labels.outer.hideWhenLessThanPercentage - 1-100 - // labels.inner.hideWhenLessThanPercentage - 1-100 - - return true; - } -}; - - //// --------- helpers.js ----------- -var helpers = { - - // creates the SVG element - addSVGSpace: function(pie) { - var element = pie.element; - var canvasWidth = pie.options.size.canvasWidth; - var canvasHeight = pie.options.size.canvasHeight; - var backgroundColor = pie.options.misc.colors.background; - - var svg = d3.select(element).append("svg:svg") - .attr("width", canvasWidth) - .attr("height", canvasHeight); - - if (backgroundColor !== "transparent") { - svg.style("background-color", function() { return backgroundColor; }); - } - - return svg; - }, - - 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(el) { - if(typeof el === 'string') - el = document.getElementById(el); - - 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<data.length; i++) { - if (data[i].hasOwnProperty("color")) { - finalColors.push(data[i].color); - } else { - finalColors.push(colors[i]); - } - } - - return finalColors; - }, - - applySmallSegmentGrouping: function(data, smallSegmentGrouping) { - var totalSize; - if (smallSegmentGrouping.valueType === "percentage") { - totalSize = math.getTotalPieSize(data); - } - - // loop through each data item - var newData = []; - var groupedData = []; - var totalGroupedData = 0; - for (var i=0; i<data.length; i++) { - if (smallSegmentGrouping.valueType === "percentage") { - var dataPercent = (data[i].value / totalSize) * 100; - if (dataPercent <= smallSegmentGrouping.value) { - groupedData.push(data[i]); - totalGroupedData += data[i].value; - continue; - } - data[i].isGrouped = false; - newData.push(data[i]); - } else { - if (data[i].value <= smallSegmentGrouping.value) { - groupedData.push(data[i]); - totalGroupedData += data[i].value; - continue; - } - data[i].isGrouped = false; - newData.push(data[i]); - } - } - - // we're done! See if there's any small segment groups to add - if (groupedData.length) { - newData.push({ - color: smallSegmentGrouping.color, - label: smallSegmentGrouping.label, - value: totalGroupedData, - isGrouped: true, - groupedData: groupedData - }); - } - - return newData; - }, - - // for debugging - showPoint: function(svg, x, y) { - svg.append("circle").attr("cx", x).attr("cy", y).attr("r", 2).style("fill", "black"); - }, - - isFunction: function(functionToCheck) { - var getType = {}; - return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]'; - }, - - isArray: function(o) { - return Object.prototype.toString.call(o) === '[object Array]'; - } -}; - - -// taken from jQuery -var extend = function() { - var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {}, - i = 1, - length = arguments.length, - deep = false, - toString = Object.prototype.toString, - hasOwn = Object.prototype.hasOwnProperty, - class2type = { - "[object Boolean]": "boolean", - "[object Number]": "number", - "[object String]": "string", - "[object Function]": "function", - "[object Array]": "array", - "[object Date]": "date", - "[object RegExp]": "regexp", - "[object Object]": "object" - }, - - jQuery = { - isFunction: function (obj) { - return jQuery.type(obj) === "function"; - }, - isArray: Array.isArray || - function (obj) { - return jQuery.type(obj) === "array"; - }, - isWindow: function (obj) { - return obj !== null && obj === obj.window; - }, - isNumeric: function (obj) { - return !isNaN(parseFloat(obj)) && isFinite(obj); - }, - type: function (obj) { - return obj === null ? String(obj) : class2type[toString.call(obj)] || "object"; - }, - isPlainObject: function (obj) { - if (!obj || jQuery.type(obj) !== "object" || obj.nodeType) { - return false; - } - try { - if (obj.constructor && !hasOwn.call(obj, "constructor") && !hasOwn.call(obj.constructor.prototype, "isPrototypeOf")) { - return false; - } - } catch (e) { - return false; - } - var key; - for (key in obj) {} - return key === undefined || hasOwn.call(obj, key); - } - }; - if (typeof target === "boolean") { - deep = target; - target = arguments[1] || {}; - i = 2; - } - if (typeof target !== "object" && !jQuery.isFunction(target)) { - target = {}; - } - if (length === i) { - target = this; - --i; - } - for (i; i < length; i++) { - if ((options = arguments[i]) !== null) { - for (name in options) { - src = target[name]; - copy = options[name]; - if (target === copy) { - continue; - } - if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))) { - if (copyIsArray) { - copyIsArray = false; - clone = src && jQuery.isArray(src) ? src : []; - } else { - clone = src && jQuery.isPlainObject(src) ? src : {}; - } - // WARNING: RECURSION - target[name] = extend(deep, clone, copy); - } else if (copy !== undefined) { - target[name] = copy; - } - } - } - } - return target; -}; - //// --------- math.js ----------- -var math = { - - toRadians: function(degrees) { - return degrees * (Math.PI / 180); - }, - - toDegrees: function(radians) { - return radians * (180 / Math.PI); - }, - - computePieRadius: function(pie) { - var size = pie.options.size; - var canvasPadding = pie.options.misc.canvasPadding; - - // outer radius is either specified (e.g. through the generator), or omitted altogether - // and calculated based on the canvas dimensions. Right now the estimated version isn't great - it should - // be possible to calculate it to precisely generate the maximum sized pie, but it's fussy as heck. Something - // for the next release. - - // first, calculate the default _outerRadius - var w = size.canvasWidth - canvasPadding.left - canvasPadding.right; - var h = size.canvasHeight - canvasPadding.top - canvasPadding.bottom; - - // now factor in the footer, title & subtitle - if (pie.options.header.location !== "pie-center") { - h -= pie.textComponents.headerHeight; - } - - if (pie.textComponents.footer.exists) { - h -= pie.textComponents.footer.h; - } - - // for really teeny pies, h may be < 0. Adjust it back - h = (h < 0) ? 0 : h; - - var outerRadius = ((w < h) ? w : h) / 3; - var innerRadius, percent; - - // if the user specified something, use that instead - if (size.pieOuterRadius !== null) { - if (/%/.test(size.pieOuterRadius)) { - percent = parseInt(size.pieOuterRadius.replace(/[\D]/, ""), 10); - percent = (percent > 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<data.length; i++) { - totalSize += data[i].value; - } - return totalSize; - }, - - sortPieData: function(pie) { - var data = pie.options.data.content; - var sortOrder = pie.options.data.sortOrder; - - switch (sortOrder) { - case "none": - // do nothing - break; - case "random": - data = helpers.shuffleArray(data); - break; - case "value-asc": - data.sort(function(a, b) { return (a.value < b.value) ? -1 : 1; }); - break; - case "value-desc": - data.sort(function(a, b) { return (a.value < b.value) ? 1 : -1; }); - break; - case "label-asc": - data.sort(function(a, b) { return (a.label.toLowerCase() > 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 = pie.__labels[section] = 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("font-weight", settings.mainLabel.fontWeight) - .style("fill", function(d, i) { - return (settings.mainLabel.color === "segment") ? pie.options.colors[i] : 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 = d.percentage; - if (settings.formatter) { - formatterContext.index = i; - formatterContext.part = "percentage"; - formatterContext.value = d.value; - formatterContext.label = d.percentage; - percentage = settings.formatter(formatterContext); - } else { - percentage += "%"; - } - return percentage; - }) - .style("font-size", settings.percentage.fontSize + "px") - .style("font-family", settings.percentage.font) - .style("font-weight", settings.percentage.fontWeight) - .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("font-weight", settings.value.fontWeight) - .style("fill", settings.value.color); - } - }, - - /** - * @param section "inner" / "outer" - */ - positionLabelElements: function(pie, section, sectionDisplayType) { - labels["dimensions-" + section] = []; - - // get the latest widths, heights - var labelGroups = pie.__labels[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": - pie.svg.selectAll("." + pie.cssPrefix + "segmentValue-" + section) - .attr("dx", function(d, i) { return dims[i].mainLabel.width + singleLinePad; }); - break; - case "label-value2": - pie.svg.selectAll("." + pie.cssPrefix + "segmentValue-" + section) - .attr("dy", function(d, i) { return dims[i].mainLabel.height; }); - break; - case "label-percentage1": - pie.svg.selectAll("." + pie.cssPrefix + "segmentPercentage-" + section) - .attr("dx", function(d, i) { return dims[i].mainLabel.width + singleLinePad; }); - break; - case "label-percentage2": - pie.svg.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 = []; - pie.__labels.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", 1); - - var lineGroup = lineGroups.selectAll("." + pie.cssPrefix + "lineGroup") - .data(pie.lineCoordGroups) - .enter() - .append("g") - .attr("class", pie.cssPrefix + "lineGroup"); - - var lineFunction = d3.line() - .curve(d3.curveBasis) - .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 isHidden = (percentage !== null && d.percentage < percentage) || pie.options.data.content[i].label === ""; - return isHidden ? 0 : 1; - }); - }, - - positionLabelGroups: function(pie, section) { - if (pie.options.labels[section].format === "none") - return; - - pie.__labels[section] - .style("opacity", function(d, i) { - var percentage = pie.options.labels[section].hideWhenLessThanPercentage; - return (percentage !== null && d.percentage < percentage) ? 0 : 1; - }) - .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 + ")"; - }); - }, - - - 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.__labels.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 (!labels.isLabelHidden(pie, i) && 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 (!labels.isLabelHidden(pie, i) && helpers.rectIntersect(curr, examinedLabelGroup)) { - labels.adjustLabelPos(pie, nextIndex, currLabelGroup, info); - break; - } - } - } - labels.checkConflict(pie, nextIndex, direction, size); - }, - - isLabelHidden: function(pie, index) { - var percentage = pie.options.labels.outer.hideWhenLessThanPercentage; - return (percentage !== null && d.percentage < percentage) || pie.options.data.content[index].label === ""; - }, - - // 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 = pie.svg.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 = { - - effectMap: { - "none": d3.easeLinear, - "bounce": d3.easeBounce, - "linear": d3.easeLinear, - "sin": d3.easeSin, - "elastic": d3.easeElastic, - "back": d3.easeBack, - "quad": d3.easeQuad, - "circle": d3.easeCircle, - "exp": d3.easeExp - }, - - /** - * 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.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(d3.easeCubicInOut) - //.duration(loadSpeed) - .attr("data-index", function(d, i) { return i; }) - .attr("d", arc); -/* - .attrTween("d", function(b) { - var i = d3.interpolate({ value: 0 }, b); - return function(t) { - var ret = pie.arc(i(t)); - console.log(ret); - return ret; - }; - }); -*/ - 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 = pie.svg.selectAll("." + pie.cssPrefix + "arc"); - arc = arc.merge(pie.__labels.inner.merge(pie.__labels.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; - - segments.maybeCloseOpenSegment(pie); - - d3.select(segment) - .transition() - .ease(segments.effectMap[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) + ")"; - }) - .on("end", function(d, i) { - pie.currentlyOpenSegment = segment; - pie.isOpeningSegment = false; - d3.select(segment).attr("class", pie.cssPrefix + "expanded"); - }); - }, - - maybeCloseOpenSegment: function(pie) { - if (typeof pie !== 'undefined' && pie.svg.selectAll("." + pie.cssPrefix + "expanded").size() > 0) { - segments.closeSegment(pie, pie.svg.select("." + pie.cssPrefix + "expanded").node()); - } - }, - - closeSegment: function(pie, segment) { - d3.select(segment) - .transition() - .duration(400) - .attr("transform", "translate(0,0)") - .on("end", function(d, i) { - d3.select(segment).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; - } - -}; - - //// --------- text.js ----------- -var text = { - offscreenCoord: -10000, - - addTitle: function(pie) { - pie.__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") - .attr("class", pie.cssPrefix + "title") - .attr("x", text.offscreenCoord) - .attr("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-weight", function(d) { return d.fontWeight; }) - .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.__title - .attr("x", x) - .attr("y", y); - }, - - addSubtitle: function(pie) { - var headerLocation = pie.options.header.location; - - pie.__subtitle = 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-weight", function(d) { return d.fontWeight; }) - .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.__subtitle - .attr("x", x) - .attr("y", y); - }, - - addFooter: function(pie) { - pie.__footer = 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-weight", function(d) { return d.fontWeight; }) - .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.__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) - .attr("ry", pie.options.tooltips.styles.borderRadius) - .attr("x", -pie.options.tooltips.styles.padding) - .attr("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-weight", function(d) { return pie.options.tooltips.styles.fontWeight; }) - .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: d.percentage - }); - }); - - 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); - }) - .attr("height", function (d, i) { - var dims = helpers.getDimensions(pie.cssPrefix + "tooltip" + i); - return dims.h + (2 * pie.options.tooltips.styles.padding); - }) - .attr("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 - _setupData.call(this); - _init.call(this); - }; - - d3pie.prototype.recreate = function() { - // now run some validation on the user-defined info - if (!validate.initialCheck(this)) { - return; - } - - _setupData.call(this); - _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() { - segments.maybeCloseOpenSegment(this); - }; - - // 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 _setupData = function () { - 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); - - var dp = this.options.labels.percentage.decimalPlaces; - - // add in percentage data to content - for (var i=0; i<this.options.data.content.length; i++) { - this.options.data.content[i].percentage = _getPercentage(this.options.data.content[i].value, this.totalSize, dp); - } - - // adjust the final item to ensure the percentage always adds up to precisely 100%. This is necessary - var totalPercentage = 0; - for (var j=0; j<this.options.data.content.length; j++) { - if (j === this.options.data.content.length - 1) { - this.options.data.content[j].percentage = (100 - totalPercentage).toFixed(dp); - } - totalPercentage += parseFloat(this.options.data.content[j].percentage); - } - }; - - 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); - - // console.log(this); - - // the footer never moves. Put it in place now - var self = this; - text.positionFooter(self); - var d3 = helpers.getDimensions(self.__footer.node()); - self.textComponents.footer.h = d3.h; - self.textComponents.footer.w = d3.w; - - if (self.textComponents.title.exists) { - var d1 = helpers.getDimensions(self.__title.node()); - self.textComponents.title.h = d1.h; - self.textComponents.title.w = d1.w; - } - - if (self.textComponents.subtitle.exists) { - var d2 = helpers.getDimensions(self.__subtitle.node()); - 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 - - self.__labels = {}; - 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"); - - if (helpers.isFunction(self.options.callbacks.onload)) { - try { - self.options.callbacks.onload(); - } catch (e) { } - } - - // add and position the tooltips - if (self.options.tooltips.enabled) { - tt.addTooltips(self); - } - - segments.addSegmentEventHandlers(self); - }; - - var _getPercentage = function(value, total, decimalPlaces) { - var relativeAmount = value / total; - if (decimalPlaces <= 0) { - return Math.round(relativeAmount * 100); - } else { - return (relativeAmount * 100).toFixed(decimalPlaces); - } - }; - - return d3pie; -})); |