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