Source: mini-tooltip.js

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

    const d3Array = require('d3-array');
    const d3Ease = require('d3-ease');
    const d3Format = require('d3-format');
    const d3Selection = require('d3-selection');
    const d3Transition = require('d3-transition');


    /**
     * Mini Tooltip Component reusable API class that renders a
     * simple and configurable tooltip element for Britechart's
     * bar and step chart.
     *
     * @module Mini-tooltip
     * @tutorial bar
     * @requires d3
     *
     * @example
     * var barChart = line(),
     *     miniTooltip = miniTooltip();
     *
     * barChart
     *     .width(500)
     *     .height(300)
     *     .on('customMouseHover', miniTooltip.show)
     *     .on('customMouseMove', miniTooltip.update)
     *     .on('customMouseOut', miniTooltip.hide);
     *
     * d3Selection.select('.css-selector')
     *     .datum(dataset)
     *     .call(barChart);
     *
     * d3Selection.select('.metadata-group .mini-tooltip-container')
     *     .datum([])
     *     .call(miniTooltip);
     *
     */
    return function module() {

        let margin = {
                top: 12,
                right: 12,
                bottom: 12,
                left: 12
            },
            width = 100,
            height = 100,

            // Optional Title
            title = '',

            // Data Format
            valueLabel = 'value',
            nameLabel = 'name',

            // Animations
            mouseChaseDuration = 100,
            ease = d3Ease.easeQuadInOut,

            // tooltip
            tooltipBackground,
            backgroundBorderRadius = 1,
            tooltipTextContainer,
            tooltipOffset = {
                y: 0,
                x: 20
            },

            // Fonts
            textSize = 14,
            textLineHeight = 1.5,
            valueTextSize = 27,
            valueTextLineHeight = 1.18,

            // Colors
            bodyFillColor = '#FFFFFF',
            borderStrokeColor = '#D2D6DF',
            titleFillColor = '#666a73',
            nameTextFillColor = '#666a73',
            valueTextFillColor = '#45494E',
            valueTextWeight = 200,

            // formats
            tooltipValueFormat = d3Format.format('.2f'),

            chartWidth,
            chartHeight,
            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 {Array} _data The data to attach and generate the chart (usually an empty array)
         */
        function exports(_selection) {
            _selection.each(function(_data){
                chartWidth = width - margin.left - margin.right;
                chartHeight = height - margin.top - margin.bottom;

                buildSVG(this);
                drawTooltip();
            });
        }

        /**
         * Builds containers for the tooltip
         * Also applies the Margin convention
         * @private
         */
        function buildContainerGroups() {
            let 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-mini-tooltip', true);

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

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

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

            tooltipBackground = tooltipTextContainer
              .append('rect')
                .classed('tooltip-background', true)
                .attr('width', width)
                .attr('height', height)
                .attr('rx', backgroundBorderRadius)
                .attr('ry', backgroundBorderRadius)
                .attr('y', - margin.top)
                .attr('x', - margin.left)
                .style('fill', bodyFillColor)
                .style('stroke', borderStrokeColor)
                .style('stroke-width', 1)
                .style('pointer-events', 'none')
                .style('opacity', 0.9);
        }

        /**
         * Figures out the max length of the tooltip lines
         * @param  {D3Selection[]} texts    List of svg elements of each line
         * @return {Number}                 Max size of the lines
         */
        function getMaxLengthLine(...texts) {
            let textSizes = texts.filter(x => !!x)
                .map(x => x.node().getBBox().width);

            return d3Array.max(textSizes);
        }

        /**
         * Calculates the desired position for the tooltip
         * @param  {Number} mouseX             Current horizontal mouse position
         * @param  {Number} mouseY             Current vertical mouse position
         * @param  {Number} parentChartWidth   Parent's chart width
         * @param  {Number} parentChartHeight  Parent's chart height
         * @return {Number[]}                  X and Y position
         * @private
         */
        function getTooltipPosition([mouseX, mouseY], [parentChartWidth, parentChartHeight]) {
            let tooltipX, tooltipY;

            if (hasEnoughHorizontalRoom(parentChartWidth, mouseX)) {
                tooltipX = mouseX + tooltipOffset.x;
            } else {
                tooltipX = mouseX - chartWidth - tooltipOffset.x - margin.right;
            }

            if (hasEnoughVerticalRoom(parentChartHeight, mouseY)) {
                tooltipY = mouseY + tooltipOffset.y;
            } else {
                tooltipY = mouseY - chartHeight - tooltipOffset.y - margin.bottom;
            }

            return [tooltipX, tooltipY];
        }

        /**
         * Checks if the mouse is over the bounds of the parent chart
         * @param  {Number}  chartWidth Parent's chart
         * @param  {Number}  positionX  Mouse position
         * @return {Boolean}            If the mouse position allows space for the tooltip
         */
        function hasEnoughHorizontalRoom(parentChartWidth, positionX) {
            return (parentChartWidth - margin.left - margin.right - chartWidth) - positionX > 0;
        }

        /**
         * Checks if the mouse is over the bounds of the parent chart
         * @param  {Number}  chartWidth Parent's chart
         * @param  {Number}  positionX  Mouse position
         * @return {Boolean}            If the mouse position allows space for the tooltip
         */
        function hasEnoughVerticalRoom(parentChartHeight, positionY) {
            return (parentChartHeight - margin.top - margin.bottom - chartHeight) - positionY > 0;
        }

        /**
         * Hides the tooltip
         * @return {void}
         */
        function hideTooltip() {
            svg.style('display', 'none');
        }

        /**
         * Shows the tooltip updating it's content
         * @param  {Object} dataPoint Data point from the chart
         * @return {void}
         */
        function showTooltip(dataPoint) {
            updateContent(dataPoint);
            svg.style('display', 'block');
        }

        /**
         * Draws the data entries inside the tooltip for a given topic
         * @param  {Object} topic Topic to extract data from
         * @return void
         */
        function updateContent(dataPoint = {}){
            let value = dataPoint[valueLabel] || '',
                name = dataPoint[nameLabel] || '',
                lineHeight = textSize * textLineHeight,
                valueLineHeight = valueTextSize * valueTextLineHeight,
                defaultDy = '1em',
                temporalHeight = 0,
                tooltipValue,
                tooltipName,
                tooltipTitle;

            tooltipTextContainer.selectAll('text')
                .remove();

            if (title) {
                tooltipTitle = tooltipTextContainer
                  .append('text')
                    .classed('mini-tooltip-title', true)
                    .attr('dy', defaultDy)
                    .attr('y', 0)
                    .style('fill', titleFillColor)
                    .style('font-size', textSize)
                    .text(title);

                temporalHeight = lineHeight + temporalHeight;
            }

            if (name) {
                tooltipName = tooltipTextContainer
                  .append('text')
                    .classed('mini-tooltip-name', true)
                    .attr('dy', defaultDy)
                    .attr('y', temporalHeight || 0)
                    .style('fill', nameTextFillColor)
                    .style('font-size', textSize)
                    .text(name);

                temporalHeight = lineHeight + temporalHeight;
            }

            if (value) {
                tooltipValue = tooltipTextContainer
                  .append('text')
                    .classed('mini-tooltip-value', true)
                    .attr('dy', defaultDy)
                    .attr('y', temporalHeight || 0)
                    .style('fill', valueTextFillColor)
                    .style('font-size', valueTextSize)
                    .style('font-weight', valueTextWeight)
                    .text(tooltipValueFormat(value));

                temporalHeight = valueLineHeight + temporalHeight;
            }

            chartWidth = getMaxLengthLine(tooltipName, tooltipTitle, tooltipValue);
            chartHeight = temporalHeight;
        }

        /**
         * Updates size and position of tooltip depending on the side of the chart we are in
         * @param  {Object} dataPoint DataPoint of the tooltip
         * @return void
         */
        function updatePositionAndSize(mousePosition, parentChartSize) {
            let [tooltipX, tooltipY] = getTooltipPosition(mousePosition, parentChartSize);

            svg.transition()
                .duration(mouseChaseDuration)
                .ease(ease)
                .attr('height', chartHeight + margin.top + margin.bottom)
                .attr('width', chartWidth + margin.left + margin.right)
                .attr('transform', `translate(${tooltipX},${tooltipY})`);

            tooltipBackground
                .attr('height', chartHeight + margin.top + margin.bottom)
                .attr('width', chartWidth + margin.left + margin.right);
        }

        /**
         * Updates tooltip content, size and position
         *
         * @param  {Object} dataPoint Current datapoint to show info about
         * @return void
         */
        function updateTooltip(dataPoint, position, chartSize) {
            updateContent(dataPoint);
            updatePositionAndSize(position, chartSize);
        }

        /**
         * Hides the tooltip
         * @return {Module} Tooltip module to chain calls
         * @public
         */
        exports.hide = function() {
            hideTooltip();

            return this;
        };

        /**
         * Gets or Sets data's nameLabel
         * @param  {text} _x Desired nameLabel
         * @return { text | module} nameLabel or Step Chart module to chain calls
         * @public
         */
        exports.nameLabel = function(_x) {
            if (!arguments.length) {
                return nameLabel;
            }
            nameLabel = _x;
            return this;
        };

        /**
         * Shows the tooltip
         * @return {Module} Tooltip module to chain calls
         * @public
         */
        exports.show = function() {
            showTooltip();

            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;
        };

        /**
         * Updates the position and content of the tooltip
         * @param  {Object} dataPoint       Datapoint of the hovered element
         * @param  {Array} mousePosition    Mouse position relative to the parent chart [x, y]
         * @param  {Array} chartSize        Parent chart size [x, y]
         * @return {module}                 Current component
         */
        exports.update = function(dataPoint, mousePosition, chartSize) {
            updateTooltip(dataPoint, mousePosition, chartSize);

            return this;
        };

        return exports;
    };
});