/* eslint-disable */
/*!
 * d3pie
 * @author Ben Keen
 * @version 0.1.9
 * @date June 17th, 2015
 * @repo http://github.com/benkeen/d3pie
 * SPDX-License-Identifier: MIT
 */
// 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",
                speed: 1000
            },
            pullOutSegmentOnClick: {
                effect: "none",
                speed: 300,
                size: 10
            },
            highlightSegmentOnMouseover: false,
            highlightLuminosity: -0.2
        },
        tooltips: {
            enabled: false,
            type: "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;
}));
//# sourceMappingURL=d3pie-0.2.1-netdata-3.js.map