Source: donut.js

define(function(require){
    'use strict';

    const d3Dispatch = require('d3-dispatch');
    const d3Ease = require('d3-ease');
    const d3Interpolate = require('d3-interpolate');
    const d3Scale = require('d3-scale');
    const d3Shape = require('d3-shape');
    const d3Selection = require('d3-selection');
    const d3Transition = require('d3-transition');

    const {exportChart} = require('./helpers/exportChart');
    const textHelper = require('./helpers/text');
    const colorHelper = require('./helpers/colors');


    /**
     * @typedef DonutChartData
     * @type {Object[]}
     * @property {Number} quantity     Quantity of the group (required)
     * @property {Number} percentage   Percentage of the total (required)
     * @property {String} name         Name of the group (required)
     * @property {Number} id           Identifier for the group required for legend feature (optional)
     *
     * @example
     * [
     *     {
     *         quantity: 1,
     *         percentage: 50,
     *         name: 'glittering',
     *         id: 1
     *     },
     *     {
     *         quantity: 1,
     *         percentage: 50,
     *         name: 'luminous',
     *         id: 2
     *     }
     * ]
     */

    /**
     * Reusable Donut Chart API class that renders a
     * simple and configurable donut chart.
     *
     * @module Donut
     * @tutorial donut
     * @requires d3-dispatch, d3-ease, d3-interpolate, d3-scale, d3-shape, d3-selection
     *
     * @example
     * var donutChart = donut();
     *
     * donutChart
     *     .externalRadius(500)
     *     .internalRadius(200);
     *
     * d3Selection.select('.css-selector')
     *     .datum(dataset)
     *     .call(donutChart);
     *
     */
    return function module() {

        let margin = {
                top: 0,
                right: 0,
                bottom: 0,
                left: 0
            },
            width = 300,
            height = 300,
            ease = d3Ease.easeCubicInOut,
            arcTransitionDuration = 750,
            pieDrawingTransitionDuration = 1200,
            pieHoverTransitionDuration = 150,
            radiusHoverOffset = 12,
            paddingAngle = 0.016,
            data,
            chartWidth, chartHeight,
            externalRadius = 140,
            internalRadius = 45.5,
            legendWidth = externalRadius + internalRadius,
            layout,
            shape,
            slices,
            svg,

            quantityLabel = 'quantity',
            nameLabel = 'name',
            percentageLabel = 'percentage',

            // colors
            colorScale,
            colorSchema = colorHelper.colorSchemas.britechartsColorSchema,

            // utils
            storeAngle = function(d) {
                this._current = d;
            },
            reduceOuterRadius = d => {
                d.outerRadius = externalRadius - radiusHoverOffset;
            },
            sortComparator = (a, b) => b.quantity - a.quantity,

            // extractors
            getQuantity = ({quantity}) => quantity,
            getSliceFill = ({data}) => colorScale(data.name),

            // events
            dispatcher = d3Dispatch.dispatch('customMouseOver', 'customMouseOut', 'customMouseMove');

        /**
         * This function creates the graph using the selection as container
         *
         * @param {D3Selection} _selection A d3 selection that represents
         *                                  the container(s) where the chart(s) will be rendered
         * @param {DonutChartData} _data The data to attach and generate the chart
         */
        function exports(_selection) {
            _selection.each(function(_data) {
                chartWidth = width - margin.left - margin.right;
                chartHeight = height - margin.top - margin.bottom;
                data = cleanData(_data);

                buildLayout();
                buildColorScale();
                buildShape();
                buildSVG(this);
                drawSlices();
                initTooltip();
            });
        }

        /**
         * Builds color scale for chart, if any colorSchema was defined
         * @private
         */
        function buildColorScale() {
            if (colorSchema) {
                colorScale = d3Scale.scaleOrdinal().range(colorSchema);
            }
        }

        /**
         * Builds containers for the chart, the legend and a wrapper for all of them
         * @private
         */
        function buildContainerGroups() {
            let container = svg
              .append('g')
                .classed('container-group', true)
                .attr('transform', `translate(${width / 2}, ${height / 2})`);

            container.append('g').classed('chart-group', true);
            container.append('g').classed('legend-group', true);
        }

        /**
         * Builds the pie layout that will produce data ready to draw
         * @private
         */
        function buildLayout() {
            layout = d3Shape.pie()
                .padAngle(paddingAngle)
                .value(getQuantity)
                .sort(sortComparator);
        }

        /**
         * Builds the shape function
         * @private
         */
        function buildShape() {
            shape = d3Shape.arc()
                .innerRadius(internalRadius)
                .padRadius(externalRadius);
        }

        /**
         * Builds the SVG element that will contain the chart
         *
         * @param  {HTMLElement} container DOM element that will work as the container of the graph
         * @private
         */
        function buildSVG(container) {
            if (!svg) {
                svg = d3Selection.select(container)
                  .append('svg')
                    .classed('britechart donut-chart', true)
                    .data([data]);  //TO REVIEW

                buildContainerGroups();
            }

            svg
                .transition()
                .ease(ease)
                .attr('width', width + margin.left + margin.right)
                .attr('height', height + margin.top + margin.bottom);
        }

        /**
         * Cleaning data adding the proper format
         * @param  {DonutChartData} data Data
         * @private
         */
        function cleanData(data) {
            return data.map((d) => {
                d.quantity = +d[quantityLabel];
                d.name = String(d[nameLabel]);
                d.percentage = String(d[percentageLabel]);

                return d;
            });
        }

        /**
         * Draws the values on the donut slice inside the text element
         *
         * @param  {Object} obj Data object
         * @private
         */
        function drawLegend(obj) {
            if (obj.data) {
                svg.select('.donut-text')
                    .text(() => `${obj.data.percentage}% ${ obj.data.name}`)
                    .attr('dy', '.2em')
                    .attr('text-anchor', 'middle');

                svg.select('.donut-text').call(wrapText, legendWidth);
            }
        }

        /**
         * Draws the slices of the donut
         * @private
         */
        function drawSlices() {
            if (!slices) {
                slices = svg.select('.chart-group')
                    .selectAll('g.arc')
                    .data(layout(data));

                slices.enter()
                  .append('g')
                    .each(storeAngle)
                    .each(reduceOuterRadius)
                    .classed('arc', true)
                    .on('mouseover', handleMouseOver)
                    .on('mouseout', handleMouseOut)
                .merge(slices)
                  .append('path')
                    .attr('fill', getSliceFill)
                    .on('mouseover', tweenGrowthFactory(externalRadius, 0))
                    .on('mouseout', tweenGrowthFactory(externalRadius - radiusHoverOffset, pieHoverTransitionDuration))
                    .transition()
                    .ease(ease)
                    .duration(pieDrawingTransitionDuration)
                    .attrTween('d', tweenLoading);

            } else {
                slices = svg.select('.chart-group')
                    .selectAll('path')
                    .data(layout(data));

                slices
                    .attr('d', shape);

                // Redraws the angles of the data
                slices
                    .transition()
                    .duration(arcTransitionDuration)
                    .attrTween('d', tweenArc);
            }
        }

        /**
         * Cleans any value that could be on the legend text element
         * @private
         */
        function cleanLegend() {
            svg.select('.donut-text').text('');
        }

        function handleMouseOver(datum) {
            drawLegend(datum);

            dispatcher.call('customMouseOver', this, datum);
        }

        function handleMouseOut() {
            cleanLegend();

            dispatcher.call('customMouseOut', this);
        }

        /**
         * Creates the text element that will hold the legend of the chart
         */
        function initTooltip() {
            svg.select('.legend-group')
                .append('text')
                .attr('class', 'donut-text');
        }

        /**
         * Stores current angles and interpolates with new angles
         * Check out {@link http://bl.ocks.org/mbostock/1346410| this example}
         *
         * @param  {Object}     a   New data for slice
         * @return {Function}       Tweening function for the donut shape
         * @private
         */
        function tweenArc(a) {
            let i = d3Interpolate.interpolate(this._current, a);

            this._current = i(0);

            return function(t) {
                return shape(i(t));
            };
        }

        /**
         * Generates animations with tweens depending on the attributes given
         *
         * @param  {Number} outerRadius Final outer radius value
         * @param  {Number} delay       Delay of animation
         * @return {Function}           Function that when called will tween the element
         * @private
         */
        function tweenGrowthFactory(outerRadius, delay) {
            return function() {
                d3Selection.select(this)
                    .transition()
                    .delay(delay)
                    .attrTween('d', function(d) {
                        let i = d3Interpolate.interpolate(d.outerRadius, outerRadius);

                        return (t) => {
                            d.outerRadius = i(t);

                            return shape(d);
                        };
                    });
            };
        }

        /**
         * Animation for chart loading
         * Check out {@link http://bl.ocks.org/mbostock/4341574| this example}
         *
         * @param  {Object} b Data point
         * @return {Function}   Tween function
         * @private
         */
        function tweenLoading(b) {
            let i;

            b.innerRadius = 0;
            i = d3Interpolate.interpolate({ startAngle: 0, endAngle: 0}, b);

            return function(t) { return shape(i(t)); };
        }

        /**
         * Utility function that wraps a text into the given width
         *
         * @param  {D3Selection} text         Text to write
         * @param  {Number} legendWidth Width of the container
         * @private
         */
        function wrapText(text, legendWidth) {
            let fontSize = externalRadius / 5;

            textHelper.wrapText.call(null, 0, fontSize, legendWidth, text.node());
        }

        /**
         * Gets or Sets the colorSchema of the chart
         * @param  {String[]} _x Desired colorSchema for the graph
         * @return { colorSchema | module} Current colorSchema or Chart module to chain calls
         * @public
         */
        exports.colorSchema = function(_x) {
            if (!arguments.length) {
                return colorSchema;
            }
            colorSchema = _x;
            return this;
        };

        /**
         * Gets or Sets the externalRadius of the chart
         * @param  {Number} _x ExternalRadius number to get/set
         * @return { (Number | Module) } Current externalRadius or Donut Chart module to chain calls
         * @public
         */
        exports.externalRadius = function(_x) {
            if (!arguments.length) {
                return externalRadius;
            }
            externalRadius = _x;
            return this;
        };

        /**
         * Gets or Sets the height of the chart
         * @param  {Number} _x Desired width for the graph
         * @return { (Number | Module) } Current height or Donut Chart module to chain calls
         * @public
         */
        exports.height = function(_x) {
            if (!arguments.length) {
                return height;
            }
            height = _x;
            return this;
        };

        /**
         * Gets or Sets the internalRadius of the chart
         * @param  {Number} _x InternalRadius number to get/set
         * @return { (Number | Module) } Current internalRadius or Donut Chart module to chain calls
         * @public
         */
        exports.internalRadius = function(_x) {
            if (!arguments.length) {
                return internalRadius;
            }
            internalRadius = _x;
            return this;
        };

        /**
         * Gets or Sets the margin of the chart
         * @param  {Object} _x Margin object to get/set
         * @return { (Number | Module) } Current margin or Donut Chart module to chain calls
         * @public
         */
        exports.margin = function(_x) {
            if (!arguments.length) {
                return margin;
            }
            margin = _x;
            return this;
        };

        /**
         * Gets or Sets the width of the chart
         * @param  {Number} _x Desired width for the graph
         * @return { (Number | Module) } Current width or Donut Chart module to chain calls
         * @public
         */
        exports.width = function(_x) {
            if (!arguments.length) {
                return width;
            }
            width = _x;
            return this;
        };

        /**
         * Chart exported to png and a download action is fired
         * @public
         */
        exports.exportChart = function(filename, title) {
            exportChart.call(exports, svg, filename, title);
        };

        /**
         * Exposes an 'on' method that acts as a bridge with the event dispatcher
         * We are going to expose this events:
         * customMouseOver, customMouseMove and customMouseOut
         *
         * @return {module} Bar Chart
         * @public
         */
        exports.on = function() {
            let value = dispatcher.on.apply(dispatcher, arguments);

            return value === dispatcher ? exports : value;
        };

        return exports;
    };
});