Source: charts/number-display.js

import {format} from 'd3-format';
import {easeQuad} from 'd3-ease';
import {interpolateNumber} from 'd3-interpolate';

import {BaseMixin} from '../base/base-mixin';

const SPAN_CLASS = 'number-display';

/**
 * A display of a single numeric value.
 *
 * Unlike other charts, you do not need to set a dimension. Instead a group object must be provided and
 * a valueAccessor that returns a single value.
 *
 * If the group is a {@link https://github.com/crossfilter/crossfilter/wiki/API-Reference#crossfilter_groupAll groupAll}
 * then its `.value()` will be displayed. This is the recommended usage.
 *
 * However, if it is given an ordinary group, the `numberDisplay` will show the last bin's value, after
 * sorting with the {@link https://dc-js.github.io/dc.js/docs/html/dc.baseMixin.html#ordering__anchor ordering}
 * function. `numberDisplay` defaults the `ordering` function to sorting by value, so this will display
 * the largest value if the values are numeric.
 * @mixes BaseMixin
 */
export class NumberDisplay extends BaseMixin {
    /**
     * Create a Number Display widget.
     *
     * @example
     * // create a number display under #chart-container1 element using the default global chart group
     * var display1 = new NumberDisplay('#chart-container1');
     * @param {String|node|d3.selection} parent - Any valid
     * {@link https://github.com/d3/d3-selection/blob/master/README.md#select d3 single selector} specifying
     * a dom block element such as a div; or a dom element or d3 selection.
     * @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in.
     * Interaction with a chart will only trigger events and redraws within the chart's group.
     */
    constructor (parent, chartGroup) {
        super();

        this._formatNumber = format('.2s');
        this._html = {one: '', some: '', none: ''};
        this._lastValue = undefined;
        this._ariaLiveRegion = false;

        // dimension not required
        this._mandatoryAttributes(['group']);

        // default to ordering by value, to emulate old group.top(1) behavior when multiple groups
        this.ordering(kv => kv.value);

        this.data(group => {
            const valObj = group.value ? group.value() : this._maxBin(group.all());
            return this.valueAccessor()(valObj);
        });

        this.transitionDuration(250); // good default
        this.transitionDelay(0);

        this.anchor(parent, chartGroup);
    }

    /**
     * Gets or sets an optional object specifying HTML templates to use depending on the number
     * displayed.  The text `%number` will be replaced with the current value.
     * - one: HTML template to use if the number is 1
     * - zero: HTML template to use if the number is 0
     * - some: HTML template to use otherwise
     * @example
     * numberWidget.html({
     *      one:'%number record',
     *      some:'%number records',
     *      none:'no records'})
     * @param {{one:String, some:String, none:String}} [html={one: '', some: '', none: ''}]
     * @returns {{one:String, some:String, none:String}|NumberDisplay}
     */
    html (html) {
        if (!arguments.length) {
            return this._html;
        }
        if (html.none) {
            this._html.none = html.none;//if none available
        } else if (html.one) {
            this._html.none = html.one;//if none not available use one
        } else if (html.some) {
            this._html.none = html.some;//if none and one not available use some
        }
        if (html.one) {
            this._html.one = html.one;//if one available
        } else if (html.some) {
            this._html.one = html.some;//if one not available use some
        }
        if (html.some) {
            this._html.some = html.some;//if some available
        } else if (html.one) {
            this._html.some = html.one;//if some not available use one
        }
        return this;
    }

    /**
     * Calculate and return the underlying value of the display.
     * @returns {Number}
     */
    value () {
        return this.data();
    }

    _maxBin (all) {
        if (!all.length) {
            return null;
        }
        const sorted = this._computeOrderedGroups(all);
        return sorted[sorted.length - 1];
    }

    _doRender () {
        const newValue = this.value();
        let span = this.selectAll(`.${SPAN_CLASS}`);

        if (span.empty()) {
            span = span.data([0])
                .enter()
                .append('span')
                .attr('class', SPAN_CLASS)
                .classed('dc-tabbable', this._keyboardAccessible)
                .merge(span);

            if (this._keyboardAccessible) {
                span.attr('tabindex', '0');
            }

            if (this._ariaLiveRegion) {
                this.transitionDuration(0);
                span.attr('aria-live', 'polite');
            }
        }

        {
            const chart = this;
            span.transition()
                .duration(chart.transitionDuration())
                .delay(chart.transitionDelay())
                .ease(easeQuad)
                .tween('text', function () {
                    // [XA] don't try and interpolate from Infinity, else this breaks.
                    const interpStart = isFinite(chart._lastValue) ? chart._lastValue : 0;
                    const interp = interpolateNumber(interpStart || 0, newValue);
                    chart._lastValue = newValue;

                    // need to save it in D3v4
                    const node = this;
                    return t => {
                        let html = null;
                        const num = chart.formatNumber()(interp(t));
                        if (newValue === 0 && (chart._html.none !== '')) {
                            html = chart._html.none;
                        } else if (newValue === 1 && (chart._html.one !== '')) {
                            html = chart._html.one;
                        } else if (chart._html.some !== '') {
                            html = chart._html.some;
                        }
                        node.innerHTML = html ? html.replace('%number', num) : num;
                    };
                });
        }
    }

    _doRedraw () {
        return this._doRender();
    }

    /**
     * Get or set a function to format the value for the display.
     * @see {@link https://github.com/d3/d3-format/blob/master/README.md#format d3.format}
     * @param {Function} [formatter=d3.format('.2s')]
     * @returns {Function|NumberDisplay}
     */
    formatNumber (formatter) {
        if (!arguments.length) {
            return this._formatNumber;
        }
        this._formatNumber = formatter;
        return this;
    }

    /**
     * If set, the Number Display widget will have its aria-live attribute set to 'polite' which will
     * notify screen readers when the widget changes its value. Note that setting this method will also
     * disable the default transition between the old and the new values. This is to avoid change
     * notifications spoken out before the new value finishes re-drawing. It is also advisable to check
     * if the widget has appropriately set accessibility description or label. 
     * @param {Boolean} [ariaLiveRegion=false]
     * @returns {Boolean|NumberDisplay}
     */
    ariaLiveRegion (ariaLiveRegion) {
        if (!arguments.length) {
            return this._ariaLiveRegion;
        }
        this._ariaLiveRegion = ariaLiveRegion;
        return this;
    }

}

export const numberDisplay = (parent, chartGroup) => new NumberDisplay(parent, chartGroup);