Source: stacked-bar.js

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

    const d3Array = require('d3-array');
    const d3Axis = require('d3-axis');
    const d3Color = require('d3-color');
    const d3Collection = require('d3-collection');
    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 assign = require('lodash.assign');
    const d3Transition = require('d3-transition');

    const { exportChart } = require('./helpers/export');
    const colorHelper = require('./helpers/color');
    const { bar: barChartLoadingMarkup } = require('./helpers/load');

    const PERCENTAGE_FORMAT = '%';
    const NUMBER_FORMAT = ',f';
    const uniq = (arrArg) => arrArg.filter((elem, pos, arr) => arr.indexOf(elem) == pos);


    /**
     * @typdef D3Layout
     * @type function
     */

    /**
     * @typedef stackedBarData
     * @type {Object[]}
     * @property {String} name         Name of the entry
     * @property {String} stack        Stack of the entry
     * @property {Number} value        Value of the entry
     *
     * @example
     * [
     *     {
     *         name: "2011-01",
     *         stack: "Direct",
     *         value: 0
     *     }
     * ]
     */

    /**
     * Stacked Area Chart reusable API module that allows us
     * rendering a multi area and configurable chart.
     *
     * @module Stacked-bar
     * @tutorial stacked-bar
     * @requires d3-array, d3-axis, d3-color, d3-collection, d3-dispatch, d3-ease,
     *  d3-interpolate, d3-scale, d3-shape, d3-selection, lodash assign
     *
     * @example
     * let stackedBar = stackedBar();
     *
     * stackedBar
     *     .width(containerWidth);
     *
     * d3Selection.select('.css-selector')
     *     .datum(dataset.data)
     *     .call(stackedBar);
     *
     */
    return function module() {

        let margin = {
                top: 40,
                right: 30,
                bottom: 60,
                left: 70
            },
            width = 960,
            height = 500,
            loadingState = barChartLoadingMarkup,

            xScale,
            xAxis,
            yScale,
            yAxis,

            aspectRatio = null,
            betweenBarsPadding = 0.1,

            yTickTextYOffset = -8,
            yTickTextXOffset = -20,

            locale,

            yTicks = 5,
            xTicks = 5,
            percentageAxisToMaxRatio = 1,

            colorSchema = colorHelper.colorSchemas.britecharts,

            colorScale,
            categoryColorMap,

            layers,

            ease = d3Ease.easeQuadInOut,
            isHorizontal = false,

            svg,
            chartWidth, chartHeight,
            data,
            transformedData,
            stacks,
            layerElements,
            hasReversedStacks = false,

            tooltipThreshold = 480,

            yAxisLabel,
            yAxisLabelEl,
            yAxisLabelOffset = -60,

            baseLine,
            xAxisPadding = {
                top: 0,
                left: 0,
                bottom: 0,
                right: 0
            },
            barOpacity = 0.24,

            animationDelayStep = 20,
            animationDuration = 1000,
            animationDelays,

            grid = null,

            nameLabel = 'name',
            valueLabel = 'value',
            stackLabel = 'stack',
            valueLabelFormat = NUMBER_FORMAT,

            // getters
            getName = (data) =>  data[nameLabel],
            getValue = (data) => data[valueLabel],
            getStack = (data) => data[stackLabel],
            getValOrDefaultToZero = (val) => (isNaN(val) || val < 0) ? 0 : val,
            isAnimated = false,

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

        /**
         * This function creates the graph using the selection and data provided
         * @param {D3Selection} _selection A d3 selection that represents
         * the container(s) where the chart(s) will be rendered
         * @param {stackedBarData} _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);

                prepareData(data);
                buildScales();
                buildLayers();
                buildSVG(this);
                drawGridLines();
                buildAxis();
                drawAxis();
                drawStackedBar();
                addMouseEvents();
            });
        }

        /**
         * Adds events to the container group if the environment is not mobile
         * Adding: mouseover, mouseout and mousemove
         */
        function addMouseEvents() {
            if (shouldShowTooltip()){
                svg
                    .on('mouseover', function(d) {
                        handleMouseOver(this, d);
                    })
                    .on('mouseout', function(d) {
                        handleMouseOut(this, d);
                    })
                    .on('mousemove',  function(d) {
                        handleMouseMove(this, d);
                    })
                    .on('click',  function(d) {
                        handleClick(this, d);
                    });
            }

            svg.selectAll('.bar')
                .on('mouseover', handleBarsMouseOver)
                .on('mouseout', handleBarsMouseOut);
        }

        /**
         * Adjusts the position of the y axis' ticks
         * @param  {D3Selection} selection Y axis group
         * @return void
         */
        function adjustYTickLabels(selection) {
            selection.selectAll('.tick text')
                .attr('transform', `translate(${yTickTextXOffset}, ${yTickTextYOffset})`);
        }

        /**
         * Creates the d3 x and y axis, setting orientations
         * @private
         */
        function buildAxis() {
            if (isHorizontal) {
                xAxis = d3Axis.axisBottom(xScale)
                    .ticks(xTicks, valueLabelFormat);
                yAxis = d3Axis.axisLeft(yScale)
            } else {
                xAxis = d3Axis.axisBottom(xScale)
                yAxis = d3Axis.axisLeft(yScale)
                    .ticks(yTicks, valueLabelFormat)
            }
        }

        /**
         * Builds containers for the chart, the axis and a wrapper for all of them
         * NOTE: The order of drawing of this group elements is really important,
         * as everything else will be drawn on top of them
         * @private
         */
        function buildContainerGroups(){
            let container = svg
                .append('g')
                .classed('container-group', true)
                .attr('transform', `translate(${margin.left},${margin.top})`);

            container
                .append('g').classed('x-axis-group', true)
                .append('g').classed('x axis', true);
            container.selectAll('.x-axis-group')
                .append('g').classed('month-axis', true);
            container
                .append('g').classed('y-axis-group axis', true);
            container
                .append('g').classed('grid-lines-group', true);
            container
                .append('g').classed('chart-group', true);
            container
                .append('g').classed('y-axis-label', true);
            container
                .append('g').classed('metadata-group', true);
        }

        /**
         * Builds the stacked layers layout
         * @return {D3Layout} Layout for drawing the chart
         * @private
         */
        function buildLayers(){
            let stack3 = d3Shape.stack().keys(stacks),
                dataInitial = transformedData.map((item) => {
                        let ret = {};

                        stacks.forEach((key) => {
                            ret[key] = item[key];
                        });

                        return assign({}, item, ret);
                });

            layers = stack3(dataInitial);
        }

        /**
         * Creates the x, y and color scales of the chart
         * @private
         */
        function buildScales() {
            let yMax = getYMax();

            if (isHorizontal) {
                xScale = d3Scale.scaleLinear()
                    .domain([0, yMax])
                    .rangeRound([0, chartWidth - 1]);
                // 1 pix for edge tick

                yScale = d3Scale.scaleBand()
                    .domain(data.map(getName))
                    .rangeRound([chartHeight, 0])
                    .padding(betweenBarsPadding);
            } else {
                xScale = d3Scale.scaleBand()
                    .domain(data.map(getName))
                    .rangeRound([0, chartWidth ])
                    .padding(betweenBarsPadding);

                yScale = d3Scale.scaleLinear()
                    .domain([0,yMax])
                    .rangeRound([chartHeight, 0])
                    .nice();
            }

            colorScale = d3Scale.scaleOrdinal()
                .range(colorSchema)
                .domain(data.map(getStack));

            categoryColorMap = colorScale
                .domain(data.map(getStack))
                .domain()
                .reduce((memo, item) => {
                    memo[item] = colorScale(item)
                    return memo;
                }, {});
        }

        /**
         * @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 stacked-bar', true);

                buildContainerGroups();
            }

            svg
                .attr('width', width)
                .attr('height', height);
        }

        /**
         * Cleaning data casting the values, stacks, names and topic names to the proper type while keeping
         * the rest of properties on the data
         * @param  {stackedBarData} originalData   Raw data from the container
         * @return {stackedBarData}                Parsed data with values and dates
         * @private
         */
        function cleanData(originalData) {
            return originalData.reduce((acc, d) => {
                    d.value = +d[valueLabel];
                    d.stack = d[stackLabel];
                    d.topicName = getStack(d); // for tooltip
                    d.name = d[nameLabel];

                    return [...acc, d];
                }, []);
        }

        /**
         * Draws the x and y axis on the svg object within their
         * respective groups
         * @private
         */
        function drawAxis(){
            if (isHorizontal) {
                svg.select('.x-axis-group .axis.x')
                    .attr('transform', `translate( 0, ${chartHeight} )`)
                    .call(xAxis);

                svg.select('.y-axis-group.axis')
                    .attr('transform', `translate( ${-xAxisPadding.left}, 0)`)
                    .call(yAxis);
            } else {
                svg.select('.x-axis-group .axis.x')
                    .attr('transform', `translate( 0, ${chartHeight} )`)
                    .call(xAxis);

                svg.select('.y-axis-group.axis')
                    .attr('transform', `translate( ${-xAxisPadding.left}, 0)`)
                    .call(yAxis)
                    .call(adjustYTickLabels);
            }

            if (yAxisLabel) {
                if (yAxisLabelEl) {
                    svg.selectAll('.y-axis-label-text').remove();
                }

                yAxisLabelEl = svg.select('.y-axis-label')
                    .append('text')
                        .classed('y-axis-label-text', true)
                            .attr('x', -chartHeight / 2)
                            .attr('y', yAxisLabelOffset)
                            .attr('text-anchor', 'middle')
                            .attr('transform', 'rotate(270 0 0)')
                            .text(yAxisLabel)
            }
        }

        /**
         * Draws grid lines on the background of the chart
         * @return void
         */
        function drawGridLines() {
            let scale = isHorizontal ? xScale : yScale;

            svg.select('.grid-lines-group')
                .selectAll('line')
                .remove();

            if (grid === 'horizontal' || grid === 'full') {
                svg.select('.grid-lines-group')
                    .selectAll('line.horizontal-grid-line')
                    .data(scale.ticks(yTicks).slice(1))
                    .enter()
                      .append('line')
                        .attr('class', 'horizontal-grid-line')
                        .attr('x1', (-xAxisPadding.left + 1 ))
                        .attr('x2', chartWidth)
                        .attr('y1', (d) => yScale(d))
                        .attr('y2', (d) => yScale(d));
            }

            if (grid === 'vertical' || grid === 'full') {
                svg.select('.grid-lines-group')
                    .selectAll('line.vertical-grid-line')
                    .data(scale.ticks(xTicks).slice(1))
                    .enter()
                      .append('line')
                        .attr('class', 'vertical-grid-line')
                        .attr('y1', 0)
                        .attr('y2', chartHeight)
                        .attr('x1', (d) => xScale(d))
                        .attr('x2', (d) => xScale(d));
            }

            if (isHorizontal) {
                drawVerticalExtendedLine();
            } else {
                drawHorizontalExtendedLine();
            }
        }

        /**
         * Draws the bars along the x axis
         * @param  {D3Selection} layersSelection Selection of bars
         * @return {void}
         */
        function drawHorizontalBars(layersSelection) {
            let layerJoin = layersSelection
                .data(layers);

            layerElements = layerJoin
                .enter()
                  .append('g')
                    .attr('fill', (({key}) => categoryColorMap[key]))
                    .classed('layer', true);

            let barJoin = layerElements
                .selectAll('.bar')
                .data((d) => filterOutUnkownValues(d));

            // Enter + Update
            let bars = barJoin
                    .enter()
                      .append('rect')
                        .classed('bar', true)
                        .attr('x', (d) => xScale(d[0]) )
                        .attr('y', (d) => yScale(d.data.key) )
                        .attr('height', yScale.bandwidth());

            if (isAnimated) {
                bars.style('opacity', barOpacity)
                    .transition()
                    .delay((_, i) => animationDelays[i])
                    .duration(animationDuration)
                    .ease(ease)
                    .tween('attr.width', horizontalBarsTween);
            } else {
                bars.attr('width', (d) => xScale(d[1] - d[0]));
            }
        }

        /**
         * Draws a vertical line to extend x-axis till the edges
         * @return {void}
         */
        function drawHorizontalExtendedLine() {
            baseLine = svg.select('.grid-lines-group')
                .selectAll('line.extended-x-line')
                .data([0])
                .enter()
                  .append('line')
                    .attr('class', 'extended-x-line')
                    .attr('x1', (xAxisPadding.left))
                    .attr('x2', chartWidth)
                    .attr('y1', chartHeight)
                    .attr('y2', chartHeight);
        }

        /**
         * Draws the bars along the y axis
         * @param  {D3Selection} layersSelection Selection of bars
         * @return {void}
         */
        function drawVerticalBars(layersSelection) {
            let layerJoin = layersSelection
                .data(layers);

            layerElements = layerJoin
                .enter()
                  .append('g')
                    .attr('fill', (({key}) => categoryColorMap[key]))
                    .classed('layer', true);

            let barJoin = layerElements
                    .selectAll('.bar')
                    .data((d) => filterOutUnkownValues(d));

            // Enter + Update
            let bars = barJoin
                    .enter()
                      .append('rect')
                        .classed('bar', true)
                        .attr('x', (d) => xScale(d.data.key))
                        .attr('y', (d) => yScale(d[1]))
                        .attr('width', xScale.bandwidth );

            if (isAnimated) {
                bars.style('opacity', barOpacity)
                    .transition()
                    .delay((_, i) => animationDelays[i])
                    .duration(animationDuration)
                    .ease(ease)
                    .tween('attr.height', verticalBarsTween);
            } else {
                bars.attr('height', (d) => yScale(d[0]) - yScale(d[1]));
            }
        }

        /**
         * Draws a vertical line to extend y-axis till the edges
         * @return {void}
         */
        function drawVerticalExtendedLine() {
            baseLine = svg.select('.grid-lines-group')
                .selectAll('line.extended-y-line')
                .data([0])
                .enter()
                  .append('line')
                    .attr('class', 'extended-y-line')
                    .attr('y1', (xAxisPadding.bottom))
                    .attr('y2', chartHeight)
                    .attr('x1', 0)
                    .attr('x2', 0);
        }

        /**
         * Draws the different areas into the chart-group element
         * @private
         */
        function drawStackedBar() {
            // Not ideal, we need to figure out how to call exit for nested elements
            if (layerElements) {
                svg.selectAll('.layer').remove();
            }

            let series = svg.select('.chart-group').selectAll('.layer')

            animationDelays = d3Array.range(animationDelayStep, (layers[0].length + 1) * animationDelayStep, animationDelayStep)

            if (isHorizontal) {
                drawHorizontalBars(series)
            } else {
                drawVerticalBars(series)
            }
            // Exit
            series.exit()
                .transition()
                .style('opacity', 0)
                .remove();
        }

        /**
         * Filter out unkown stacks/values in the bar layers
         * @param {Object[]} d
         * @return {Object[]} filteredData
         * @private
         */
        function filterOutUnkownValues(d) {
            return d.map(layerEls => {
                for (let i = 0; i < layerEls.length; i++) {
                    layerEls[i] = getValOrDefaultToZero(layerEls[i]);
                }
                return layerEls;
            });
        }

        /**
         * Gets the yMax, sets it to 1 if all data points are 0
         * @return {number} Calculated yMax
         * @private
         */
        function getYMax() {
            const uniqueDataPoints = new Set(transformedData.map(({total}) => total));
            const isAllZero = uniqueDataPoints.size === 1 && uniqueDataPoints.has(0);

            if (isAllZero) {
                return 1;
            } else {
                return d3Array.max(transformedData.map(({total}) => total));
            }
        }

        /**
         * Extract X position on the chart from a given mouse event
         * @param  {obj} event D3 mouse event
         * @return {Number}       Position on the x axis of the mouse
         * @private
         */
        function getMousePosition(event) {
            return d3Selection.mouse(event);
        }

        /**
         * Finds out the data entry that is closer to the given position on pixels
         * @param  {Number} mouseX  X position of the mouse
         * @return {obj}            Data entry that is closer to that x axis position
         */
        function getNearestDataPoint(mouseX) {
            const adjustedMouseX = mouseX - margin.left;

            const nearest = transformedData.find(({key}) => {
                const barStart = xScale(key);
                const barEnd = barStart + xScale.bandwidth();

                // If mouseX is between barStart & barEnd
                return (adjustedMouseX >= barStart) && (adjustedMouseX < barEnd);
            });

            return nearest;
        }

        /**
         * Finds out the data entry that is closer to the given position on pixels (horizontal)
         * @param  {Number} mouseY  Y position of the mouse
         * @return {obj}            Data entry that is closer to that y axis position
         */
        function getNearestDataPoint2(mouseY) {
            const adjustedMouseY = mouseY - margin.top;

            const nearest = transformedData.find(({key}) => {
                const barStart = yScale(key);
                const barEnd = barStart + yScale.bandwidth();

                // If mouseY is between barStart & barEnd
                return (adjustedMouseY >= barStart) && (adjustedMouseY < barEnd);
            });

            return nearest;
        }

        /**
         * Handles a mouseover event on top of a bar
         * @return {void}
         */
        function handleBarsMouseOver() {
            d3Selection.select(this)
                .attr('fill', () => d3Color.color(d3Selection.select(this.parentNode).attr('fill')).darker())
        }

        /**
         * Handles a mouseout event out of a bar
         * @return {void}
         */
        function handleBarsMouseOut() {
            d3Selection
                .select(this).attr('fill', () => d3Selection.select(this.parentNode).attr('fill'))
        }

        /**
         * MouseMove handler, calculates the nearest dataPoint to the cursor
         * and updates metadata related to it
         * @private
         */
        function handleMouseMove(e){
            let [mouseX, mouseY] = getMousePosition(e),
                dataPoint = isHorizontal ? getNearestDataPoint2(mouseY) : getNearestDataPoint(mouseX),
                x,
                y;

            if (dataPoint) {
                // Move verticalMarker to that datapoint
                if (isHorizontal) {
                    x = mouseX - margin.left;
                    y = yScale(dataPoint.key) + yScale.bandwidth()/2;
                } else {
                    x = xScale(dataPoint.key) + margin.left;
                    y = mouseY - margin.bottom;
                }
                moveTooltipOriginXY(x,y);

                // Emit event with xPosition for tooltip or similar feature
                dispatcher.call('customMouseMove', e, dataPoint, categoryColorMap, x, y);
            }
        }

        /**
         * Click handler, passes the data point of the clicked bar
         * (or it's nearest point)
         * @private
         */

         function handleClick(e) {
            let [mouseX, mouseY] = getMousePosition(e);
            let dataPoint = isHorizontal ? getNearestDataPoint2(mouseY) : getNearestDataPoint(mouseX);

            dispatcher.call('customClick', e, dataPoint, d3Selection.mouse(e));
         }

        /**
         * MouseOut handler, hides overlay and removes active class on verticalMarkerLine
         * It also resets the container of the vertical marker
         * @private
         */
        function handleMouseOut(e, d) {
            svg.select('.metadata-group').attr('transform', 'translate(9999, 0)');
            dispatcher.call('customMouseOut', e, d, d3Selection.mouse(e));
        }

        /**
         * Mouseover handler, shows overlay and adds active class to verticalMarkerLine
         * @private
         */
        function handleMouseOver(e, d) {
            dispatcher.call('customMouseOver', e, d, d3Selection.mouse(e));
        }

        /**
         * Animation tween of horizontal bars
         * @param  {obj} d data of bar
         * @return {void}
         */
        function horizontalBarsTween(d) {
            let node = d3Selection.select(this),
                i = d3Interpolate.interpolateRound(0, xScale(d[1] - d[0])),
                j = d3Interpolate.interpolateNumber(0, 1);

            return function (t) {
                node.attr('width', i(t))
                    .style('opacity', j(t));
            }
        }

        /**
         * Helper method to update the x position of the vertical marker
         * @param  {obj} dataPoint Data entry to extract info
         * @return void
         */
        function moveTooltipOriginXY(originXPosition, originYPosition){
            svg.select('.metadata-group')
                .attr('transform', `translate(${originXPosition},${originYPosition})`);
        }

        /**
         * Prepare data for create chart.
         * @private
         */
        function prepareData(data) {
            stacks = uniq(data.map(({stack}) => stack));

            if (hasReversedStacks) {
                stacks = stacks.reverse();
            }

            transformedData = d3Collection.nest()
                .key(getName)
                .rollup(function(values) {
                    let ret = {};

                    values.forEach((entry) => {
                        if (entry && entry[stackLabel]) {
                            ret[entry[stackLabel]] = getValue(entry);
                        }
                    });
                    ret.values = values; //for tooltip

                    return ret;
                })
                .entries(data)
                .map(function(data){
                    return assign({}, {
                        total:d3Array.sum( d3Array.permute(data.value, stacks) ),
                        key:data.key
                    }, data.value);
                });
        }

        /**
         * Determines if we should add the tooltip related logic depending on the
         * size of the chart and the tooltipThreshold variable value
         * @return {boolean} Should we build the tooltip?
         * @private
         */
        function shouldShowTooltip() {
            return width > tooltipThreshold;
        }

        /**
         * Animation tween of vertical bars
         * @param  {obj} d data of bar
         * @return {void}
         */
        function verticalBarsTween(d) {
            const vertDiff = yScale(d[0]) - yScale(d[1]);

            let node = d3Selection.select(this),
                i = d3Interpolate.interpolateRound(0, getValOrDefaultToZero(vertDiff)),
                j = d3Interpolate.interpolateNumber(0,1);

            return function (t) {
                node
                    .attr('height', i(t))
                    .style('opacity', j(t));
            }
        }

        // API
        /**
         * Gets or Sets the aspect ratio of the chart
         * @param  {Number} _x = null       Desired aspect ratio for the graph
         * @return {Number | module}        Current aspect ratio or Area Chart module to chain calls
         * @public
         */
        exports.aspectRatio = function(_x) {
            if (!arguments.length) {
                return aspectRatio;
            }
            aspectRatio = _x;

            return this;
        };

        /**
         * Gets or Sets the padding of the stacked bar chart
         * The default value is 0.1
         * @param  {Number} _x = 0.1    Padding value to get/set
         * @return {Number | module}    Current padding or Chart module to chain calls
         * @public
         */
        exports.betweenBarsPadding = function (_x) {
            if (!arguments.length) {
                return betweenBarsPadding;
            }
            betweenBarsPadding = _x;

            return this;
        };

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

            return this;
        };

        /**
         * Chart exported to png and a download action is fired
         * @param {String} filename     File title for the resulting picture
         * @param {String} title        Title to add at the top of the exported picture
         * @public
         */
        exports.exportChart = function(filename, title) {
            exportChart.call(exports, svg, filename, title);
        };

        /**
         * Gets or Sets the grid mode.
         *
         * @param  {String} _x Desired mode for the grid ('vertical'|'horizontal'|'full')
         * @return {String | module} Current mode of the grid or Area Chart module to chain calls
         * @public
         */
        exports.grid = function(_x) {
            if (!arguments.length) {
                return grid;
            }
            grid = _x;

            return this;
        };

        /**
         * Gets or Sets the hasPercentage status
         * @param  {Boolean} _x     Should use percentage as value format
         * @return {Boolean | module} Is percentage used or Chart module to chain calls
         * @public
         */
        exports.hasPercentage = function(_x) {
            if (!arguments.length) {
                return valueLabelFormat === PERCENTAGE_FORMAT;
            }
            if (_x) {
                valueLabelFormat = PERCENTAGE_FORMAT;
            } else {
                valueLabelFormat = NUMBER_FORMAT;
            }

            return this;
        };

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

            return this;
        };

        /**
         * Gets or Sets the horizontal direction of the chart
         * @param  {Boolean} _x = false     Desired horizontal direction for the graph
         * @return {Boolean | module}       If it is horizontal or Bar Chart module to chain calls
         * @public
         */
        exports.isHorizontal = function(_x) {
            if (!arguments.length) {
                return isHorizontal;
            }
            isHorizontal = _x;

            return this;
        };

        /**
         * Gets or Sets the hasReversedStacks property of the chart, reversing the order of stacks.
         * @param  {Boolean} _x = false     Desired hasReversedStacks flag
         * @return {Boolean | module}       Current hasReversedStacks or Chart module to chain calls
         * @public
         */
        exports.hasReversedStacks = function(_x) {
            if (!arguments.length) {
                return hasReversedStacks;
            }
            hasReversedStacks = _x;

            return this;
        };

        /**
         * Gets or Sets the isAnimated property of the chart, making it to animate when render.
         * By default this is 'false'
         *
         * @param  {Boolean} _x = false     Desired animation flag
         * @return {Boolean | module}       Current isAnimated flag or Chart module
         * @public
         */
        exports.isAnimated = function(_x) {
            if (!arguments.length) {
                return isAnimated;
            }
            isAnimated = _x;

            return this;
        };

        /**
         * Pass language tag for the tooltip to localize the date.
         * Feature uses Intl.DateTimeFormat, for compatability and support, refer to
         * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat
         * @param  {String} _x          Must be a language tag (BCP 47) like 'en-US' or 'fr-FR'
         * @return {String | module}    Current locale or module to chain calls
         */
        exports.locale = function(_x) {
            if (!arguments.length) {
                return locale;
            }
            locale = _x;

            return this;
        };

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

            return this;
        };

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

            return this;
        };

        /**
         * Gets or Sets the number of ticks of the x axis on the chart
         * (Default is 5)
         * @param  {Number} _x = 5      Desired horizontal ticks
         * @return {Number | module}    Current xTicks or Chart module to chain calls
         * @public
         */
        exports.xTicks = function (_x) {
            if (!arguments.length) {
                return xTicks;
            }
            xTicks = _x;

            return this;
        };

        /**
         * Gets or Sets the number of vertical ticks of the axis on the chart
         * @param  {Number} _x = 5      Desired vertical ticks
         * @return {Number | module}    Current yTicks or Chart module to chain calls
         * @public
         */
        exports.yTicks = function (_x) {
            if (!arguments.length) {
                return yTicks;
            }
            yTicks = _x;

            return this;
        };

        /**
         * Gets or Sets the loading state of the chart
         * @param  {String} markup      Desired markup to show when null data
         * @return {String | module}    Current loading state markup or Chart module to chain calls
         * @public
         */
        exports.loadingState = function(_markup) {
            if (!arguments.length) {
                return loadingState;
            }
            loadingState = _markup;

            return this;
        };

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

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

        /**
         * Configurable extension of the x axis
         * if your max point was 50% you might want to show x axis to 60%, pass 1.2
         * @param  {Number} _x = 1      Ratio to max data point to add to the x axis
         * @return {Number | module}    Current ratio or Bar Chart module to chain calls
         * @public
         */
        exports.percentageAxisToMaxRatio = function(_x) {
            if (!arguments.length) {
                return percentageAxisToMaxRatio;
            }
            percentageAxisToMaxRatio = _x;

            return this;
        };

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

            return this;
        };

        /**
         * Gets or Sets the minimum width of the graph in order to show the tooltip
         * NOTE: This could also depend on the aspect ratio
         *
         * @param  {Number} [_x=480]    Minimum width of the graph
         * @return {Number | module}    Current tooltipThreshold or Area Chart module to chain calls
         * @public
         */
        exports.tooltipThreshold = function(_x) {
            if (!arguments.length) {
                return tooltipThreshold;
            }
            tooltipThreshold = _x;

            return this;
        };

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

            return this;
        };

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

            return this;
        };

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

            return this;
        };

        /**
         * Gets or Sets the y-axis label of the chart
         * @param  {String} _x          Desired label string
         * @return {String | module}    Current yAxisLabel or Chart module to chain calls
         * @public
         * @example stackedBar.yAxisLabel('Ticket Sales')
         */
        exports.yAxisLabel = function (_x) {
            if (!arguments.length) {
                return yAxisLabel;
            }
            yAxisLabel = _x;

            return this;
        };

        /**
         * Gets or Sets the offset of the yAxisLabel of the chart.
         * The method accepts both positive and negative values.
         * The default value is -60
         * @param  {Number} _x = -60        Desired offset for the label
         * @return {Number | module}        Current yAxisLabelOffset or Chart module to chain calls
         * @public
         * @example stackedBar.yAxisLabelOffset(-55)
         */
        exports.yAxisLabelOffset = function (_x) {
            if (!arguments.length) {
                return yAxisLabelOffset;
            }
            yAxisLabelOffset = _x;

            return this;
        }

        return exports;
    };
});