Source: tooltip.js

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

    const d3Format = require('d3-format');
    const d3Selection = require('d3-selection');
    const d3Transition = require('d3-transition');
    const d3TimeFormat = require('d3-time-format');

    const {
      axisTimeCombinations
    } = require('./helpers/constants');

    const {
        formatIntegerValue,
        formatDecimalValue
    } = require('./helpers/formatHelpers');

    const {
        isInteger
    } = require('./helpers/common');

    /**
     * Tooltip Component reusable API class that renders a
     * simple and configurable tooltip element for Britechart's
     * line chart or stacked area chart.
     *
     * @module Tooltip
     * @tutorial tooltip
     * @requires d3-array, d3-axis, d3-dispatch, d3-format, d3-scale, d3-selection, d3-transition
     *
     * @example
     * var lineChart = line(),
     *     tooltip = tooltip();
     *
     * tooltip
     *     .title('Tooltip title');
     *
     * lineChart
     *     .width(500)
     *     .on('customMouseOver', function() {
     *          tooltip.show();
     *     })
     *     .on('customMouseMove', function(dataPoint, topicColorMap, dataPointXPosition) {
     *          tooltip.update(dataPoint, topicColorMap, dataPointXPosition);
     *     })
     *     .on('customMouseOut', function() {
     *          tooltip.hide();
     *     });
     *
     * d3Selection.select('.css-selector')
     *     .datum(dataset)
     *     .call(lineChart);
     *
     * d3Selection.select('.metadata-group .hover-marker')
     *     .datum([])
     *     .call(tooltip);
     *
     */
    return function module() {

        let margin = {
                top: 2,
                right: 2,
                bottom: 2,
                left: 2
            },
            width = 250,
            height = 45,

            title = 'Tooltip title',

            // tooltip
            tooltip,
            tooltipOffset = {
                y: -55,
                x: 0
            },
            tooltipMaxTopicLength = 170,
            tooltipTextContainer,
            tooltipDivider,
            tooltipBody,
            tooltipTitle,
            tooltipWidth = 250,
            tooltipHeight = 48,
            ttTextX = 0,
            ttTextY = 37,
            textSize,
            entryLineLimit = 3,

            circleYOffset = 8,

            colorMap,
            bodyFillColor = '#FFFFFF',
            borderStrokeColor = '#D2D6DF',
            titleFillColor = '#6D717A',
            textFillColor = '#282C35',
            tooltipTextColor = '#000000',

            dateLabel = 'date',
            valueLabel = 'value',
            topicLabel = 'topics',

            defaultAxisSettings = axisTimeCombinations.DAY_MONTH,
            forceAxisSettings = null,
            forceOrder = [],

            // formats
            monthDayYearFormat = d3TimeFormat.timeFormat('%b %d, %Y'),
            monthDayHourFormat = d3TimeFormat.timeFormat('%b %d, %I %p'),

            chartWidth, chartHeight,
            data,
            svg;


        /**
         * 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 {Object} _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 = _data;

                buildSVG(this);
            });
        }

        /**
         * Builds containers for the tooltip
         * Also applies the Margin convention
         * @private
         */
        function buildContainerGroups() {
            var container = svg.append('g')
                .classed('tooltip-container-group', true)
                .attr('transform', `translate( ${margin.left}, ${margin.top})`);

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

        /**
         * 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('g')
                    .classed('britechart britechart-tooltip', true);

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

            // Hidden by default
            exports.hide();
        }

        /**
         * Resets the tooltipBody content
         * @return void
         */
        function cleanContent(){
            tooltipBody.selectAll('text').remove();
            tooltipBody.selectAll('circle').remove();
        }

        /**
         * Draws the different elements of the Tooltip box
         * @return void
         */
        function drawTooltip(){
            tooltipTextContainer = svg.selectAll('.tooltip-group')
                .append('g')
                .classed('tooltip-text', true);

            tooltip = tooltipTextContainer
                .append('rect')
                .classed('tooltip-text-container', true)
                .attr('x', -tooltipWidth / 4 + 8)
                .attr('y', 0)
                .attr('width', tooltipWidth)
                .attr('height', tooltipHeight)
                .attr('rx', 3)
                .attr('ry', 3)
                .style('fill', bodyFillColor)
                .style('stroke', borderStrokeColor)
                .style('stroke-width', 1);

            tooltipTitle = tooltipTextContainer
                .append('text')
                .classed('tooltip-title', true)
                .attr('x', -tooltipWidth / 4 + 17)
                .attr('dy', '.35em')
                .attr('y', 16)
                .style('fill', titleFillColor);

            tooltipDivider = tooltipTextContainer
                .append('line')
                .classed('tooltip-divider', true)
                .attr('x1', -tooltipWidth / 4 + 15)
                .attr('y1', 31)
                .attr('x2', 265)
                .attr('y2', 31)
                .style('stroke', borderStrokeColor);

            tooltipBody = tooltipTextContainer
                .append('g')
                .classed('tooltip-body', true)
                .style('transform', 'translateY(8px)')
                .style('fill', textFillColor);
        }

        /**
         * Formats the value depending on its characteristics
         * @param  {Number} value Value to format
         * @return {Number}       Formatted value
         */
        function getFormattedValue(value) {
            if (!value) {
                return 0;
            }

            if (isInteger(value)) {
                value = formatIntegerValue(value);
            } else {
                value = formatDecimalValue(value);
            }

            return value;
        }

        /**
         * Extracts the value from the data object
         * @param  {Object} data Data value containing the info
         * @return {String}      Value to show
         */
        function getValueText(data) {
            let value = data[valueLabel];
            let valueText;

            if (data.missingValue) {
                valueText = '-';
            } else {
                valueText = getFormattedValue(value).toString();
            }

            return valueText;
        }

        /**
         * Resets the height of the tooltip and the pointer for the text
         * position
         */
        function resetSizeAndPositionPointers() {
            tooltipHeight = 48;
            ttTextY = 37;
            ttTextX = 0;
        }

        /**
         * Draws the data entries inside the tooltip for a given topic
         * @param  {Object} topic Topic to extract data from
         * @return void
         */
        function updateContent(topic){
            let name = topic.name,
                tooltipRight,
                tooltipLeftText,
                tooltipRightText,
                elementText;

            tooltipLeftText = topic.topicName || name;
            tooltipRightText = getValueText(topic);

            elementText = tooltipBody
                .append('text')
                .classed('tooltip-left-text', true)
                .attr('dy', '1em')
                .attr('x', ttTextX - 20)
                .attr('y', ttTextY)
                .style('fill', tooltipTextColor)
                .text(tooltipLeftText)
                .call(textWrap, tooltipMaxTopicLength, -25);

            tooltipRight = tooltipBody
                .append('text')
                .classed('tooltip-right-text', true)
                .attr('dy', '1em')
                .attr('x', ttTextX + 8)
                .attr('y', ttTextY)
                .style('fill', tooltipTextColor)
                .text(tooltipRightText);

            textSize = elementText.node().getBBox();
            tooltipHeight += textSize.height + 5;

            // Not sure if necessary
            tooltipRight.attr('x', tooltipWidth - tooltipRight.node().getBBox().width - 10 - tooltipWidth / 4)

            tooltipBody
                .append('circle')
                .classed('tooltip-circle', true)
                .attr('cx', 23 - tooltipWidth / 4)
                .attr('cy', (ttTextY + circleYOffset))
                .attr('r', 5)
                .style('fill', colorMap[name])
                .style('stroke-width', 1);

            ttTextY += textSize.height + 7;
        }

        /**
         * Updates size and position of tooltip depending on the side of the chart we are in
         * @param  {Object} dataPoint DataPoint of the tooltip
         * @param  {Number} xPosition DataPoint's x position in the chart
         * @return void
         */
        function updatePositionAndSize(dataPoint, xPosition){
            tooltip
                .attr('width', tooltipWidth)
                .attr('height', tooltipHeight + 10);

            // show tooltip to the right
            if ((xPosition - tooltipWidth) < 0) {
                // Tooltip on the right
                tooltipTextContainer
                    .attr('transform', 'translate(' + (tooltipWidth - 185) + ',' + tooltipOffset.y + ')');
            } else {
                // Tooltip on the left
                tooltipTextContainer
                    .attr('transform', 'translate(' + (-205) + ',' + tooltipOffset.y + ')');
            }

            tooltipDivider
                .attr('x2', tooltipWidth - 60);
        }

        /**
         * Updates value of tooltipTitle with the data meaning and the date
         * @param  {Object} dataPoint Point of data to use as source
         * @return void
         */
        function updateTitle(dataPoint) {
            var date = new Date(dataPoint[dateLabel]),
                tooltipTitleText = title + ' - ' + formatDate(date);

            tooltipTitle.text(tooltipTitleText);
        }

        /**
         * Figures out which date format to use when showing the date of the current data entry
         * @return {Function} The proper date formatting function
         */
        function formatDate(date) {
            let settings = forceAxisSettings || defaultAxisSettings;
            let format = null;

            if (settings === axisTimeCombinations.DAY_MONTH || settings === axisTimeCombinations.MONTH_YEAR) {
                format = monthDayYearFormat;
            } else if (settings === axisTimeCombinations.HOUR_DAY || settings === axisTimeCombinations.MINUTE_HOUR) {
                format = monthDayHourFormat;
            }

            return format(date);
        }

        /**
         * Helper method to sort the passed topics array by the names passed int he order arary
         * @param  {Object[]} topics    Topics data, retrieved from datapoint passed by line chart
         * @param  {Object[]} order     Array of names in the order to sort topics by
         * @return {Object[]}           sorted topics object
         */
        function _sortByForceOrder(topics, order=forceOrder) {
            return forceOrder.map((orderName) => topics.filter(({name}) => name === orderName)[0]);
        }

        /**
         * Updates tooltip title, content, size and position
         *
         * @param  {lineChartPointByDate} dataPoint  Current datapoint to show info about
         * @param  {Number} xPosition           Position of the mouse on the X axis
         * @return void
         */
        function updateTooltip(dataPoint, xPosition) {
            var topics = dataPoint[topicLabel];

            // sort order by forceOrder array if passed
            if (forceOrder.length) {
                topics = _sortByForceOrder(topics);
            }

            cleanContent();
            resetSizeAndPositionPointers();
            updateTitle(dataPoint);
            topics.forEach(updateContent);
            updatePositionAndSize(dataPoint, xPosition);
        }

        /**
         * Wraps a text given the text, width, x position and textFormatter function
         * @param  {D3Selection} text  Selection with the text to wrap inside
         * @param  {Number} width Desired max width for that line
         * @param  {Number} xpos  Initial x position of the text
         *
         * REF: http://bl.ocks.org/mbostock/7555321
         * More discussions on https://github.com/mbostock/d3/issues/1642
         */
        function textWrap(text, width, xpos) {
            xpos = xpos || 0;

            text.each(function() {
                var words,
                    word,
                    line,
                    lineNumber,
                    lineHeight,
                    y,
                    dy,
                    tspan;

                text = d3Selection.select(this);

                words = text.text().split(/\s+/).reverse();
                line = [];
                lineNumber = 0;
                lineHeight = 1.2;
                y = text.attr('y');
                dy = parseFloat(text.attr('dy'));
                tspan = text
                    .text(null)
                    .append('tspan')
                    .attr('x', xpos)
                    .attr('y', y)
                    .attr('dy', dy + 'em');

                while ((word = words.pop())) {
                    line.push(word);
                    tspan.text(line.join(' '));

                    if (tspan.node().getComputedTextLength() > width) {
                        line.pop();
                        tspan.text(line.join(' '));

                        if (lineNumber < entryLineLimit - 1) {
                            line = [word];
                            tspan = text.append('tspan')
                                .attr('x', xpos)
                                .attr('y', y)
                                .attr('dy', ++lineNumber * lineHeight + dy + 'em')
                                .text(word);
                        }
                    }
                }
            });
        }

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

            return this;
        };

        /**
         * Gets or Sets the valueLabel of the data
         * @param  {Number} _x Desired valueLabel
         * @return { valueLabel | 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 topicLabel of the data
         * @param  {Number} _x Desired topicLabel
         * @return { topicLabel | module} Current topicLabel or Chart module to chain calls
         * @public
         */
        exports.topicLabel = function(_x) {
            if (!arguments.length) {
                return topicLabel;
            }
            topicLabel = _x;

            return this;
        };

        /**
         * Hides the tooltip
         * @return {Module} Tooltip module to chain calls
         * @public
         */
        exports.hide = function() {
            svg.style('display', 'none');

            return this;
        };

        /**
         * Shows the tooltip
         * @return {Module} Tooltip module to chain calls
         * @public
         */
        exports.show = function() {
            svg.style('display', 'block');

            return this;
        };

        /**
         * Gets or Sets the title of the tooltip
         * @param  {string} _x Desired title
         * @return { string | module} Current title or module to chain calls
         * @public
         */
        exports.title = function(_x) {
            if (!arguments.length) {
                return title;
            }
            title = _x;

            return this;
        };

        /**
         * Pass an override for the ordering of your tooltip
         * @param  {Object[]} _x    Array of the names of your tooltip items
         * @return { overrideOrder | module} Current overrideOrder or Chart module to chain calls
         * @public
         */
        exports.forceOrder = function(_x) {
            if (!arguments.length) {
                return forceOrder;
            }
            forceOrder = _x;

            return this;
        }

        /**
         * Updates the position and content of the tooltip
         * @param  {Object} dataPoint    Datapoint to represent
         * @param  {Object} colorMapping Color scheme of the topics
         * @param  {Number} position     X-scale position in pixels
         * @return {Module} Tooltip module to chain calls
         * @public
         */
        exports.update = function(dataPoint, colorMapping, position) {
            colorMap = colorMapping;
            updateTooltip(dataPoint, position);

            return this;
        };

        /**
         * Exposes the ability to force the tooltip to use a certain date format
         * @param  {String} _x Desired format
         * @return { (String|Module) }    Current format or module to chain calls
         */
        exports.forceDateRange = function(_x) {
            if (!arguments.length) {
              return forceAxisSettings || defaultAxisSettings;
            }
            forceAxisSettings = _x;
            return this;
        };

        /**
         * constants to be used to force the x axis to respect a certain granularity
         * current options: HOUR_DAY, DAY_MONTH, MONTH_YEAR
         * @example tooltip.forceDateRange(tooltip.axisTimeCombinations.HOUR_DAY)
         */
        exports.axisTimeCombinations = axisTimeCombinations;


        return exports;
    };
});