import {sum} from 'd3-array';
/**
* Cap is a mixin that groups small data elements below a _cap_ into an *others* grouping for both the
* Row and Pie Charts.
*
* The top ordered elements in the group up to the cap amount will be kept in the chart, and the rest
* will be replaced with an *others* element, with value equal to the sum of the replaced values. The
* keys of the elements below the cap limit are recorded in order to filter by those keys when the
* others* element is clicked.
* @mixin CapMixin
* @param {Object} Base
* @returns {CapMixin}
*/
export const CapMixin = Base => class extends Base {
constructor () {
super();
this._cap = Infinity;
this._takeFront = true;
this._othersLabel = 'Others';
this._othersGrouper = (topItems, restItems) => {
const restItemsSum = sum(restItems, this.valueAccessor()),
restKeys = restItems.map(this.keyAccessor());
if (restItemsSum > 0) {
return topItems.concat([{
others: restKeys,
key: this.othersLabel(),
value: restItemsSum
}]);
}
return topItems;
};
// emulate old group.top(N) ordering
this.ordering(kv => -kv.value);
// return N "top" groups, where N is the cap, sorted by baseMixin.ordering
// whether top means front or back depends on takeFront
this.data(group => {
if (this._cap === Infinity) {
return this._computeOrderedGroups(group.all());
} else {
let items = group.all(), rest;
items = this._computeOrderedGroups(items); // sort by baseMixin.ordering
if (this._cap) {
if (this._takeFront) {
rest = items.slice(this._cap);
items = items.slice(0, this._cap);
} else {
const start = Math.max(0, items.length - this._cap);
rest = items.slice(0, start);
items = items.slice(start);
}
}
if (this._othersGrouper) {
return this._othersGrouper(items, rest);
}
return items;
}
});
}
cappedKeyAccessor (d, i) {
if (d.others) {
return d.key;
}
return this.keyAccessor()(d, i);
}
cappedValueAccessor (d, i) {
if (d.others) {
return d.value;
}
return this.valueAccessor()(d, i);
}
/**
* Get or set the count of elements to that will be included in the cap. If there is an
* {@link CapMixin#othersGrouper othersGrouper}, any further elements will be combined in an
* extra element with its name determined by {@link CapMixin#othersLabel othersLabel}.
*
* As of dc.js 2.1 and onward, the capped charts use
* {@link https://github.com/crossfilter/crossfilter/wiki/API-Reference#group_all group.all()}
* and {@link BaseMixin#ordering BaseMixin.ordering()} to determine the order of
* elements. Then `cap` and {@link CapMixin#takeFront takeFront} determine how many elements
* to keep, from which end of the resulting array.
*
* **Migration note:** Up through dc.js 2.0.*, capping used
* {@link https://github.com/crossfilter/crossfilter/wiki/API-Reference#group_top group.top(N)},
* which selects the largest items according to
* {@link https://github.com/crossfilter/crossfilter/wiki/API-Reference#group_order group.order()}.
* The chart then sorted the items according to {@link BaseMixin#ordering baseMixin.ordering()}.
* So the two values essentially had to agree, but if the `group.order()` was incorrect (it's
* easy to forget about), the wrong rows or slices would be displayed, in the correct order.
*
* If your chart previously relied on `group.order()`, use `chart.ordering()` instead. As of
* 2.1.5, the ordering defaults to sorting from greatest to least like `group.top(N)` did.
*
* If you want to cap by one ordering but sort by another, you can still do this by
* specifying your own {@link BaseMixin#data `.data()`} callback. For details, see the example
* {@link https://dc-js.github.io/dc.js/examples/cap-and-sort-differently.html Cap and Sort Differently}.
* @memberof CapMixin
* @instance
* @param {Number} [count=Infinity]
* @returns {Number|CapMixin}
*/
cap (count) {
if (!arguments.length) {
return this._cap;
}
this._cap = count;
return this;
}
/**
* Get or set the direction of capping. If set, the chart takes the first
* {@link CapMixin#cap cap} elements from the sorted array of elements; otherwise
* it takes the last `cap` elements.
* @memberof CapMixin
* @instance
* @param {Boolean} [takeFront=true]
* @returns {Boolean|CapMixin}
*/
takeFront (takeFront) {
if (!arguments.length) {
return this._takeFront;
}
this._takeFront = takeFront;
return this;
}
/**
* Get or set the label for *Others* slice when slices cap is specified.
* @memberof CapMixin
* @instance
* @param {String} [label="Others"]
* @returns {String|CapMixin}
*/
othersLabel (label) {
if (!arguments.length) {
return this._othersLabel;
}
this._othersLabel = label;
return this;
}
/**
* Get or set the grouper function that will perform the insertion of data for the *Others* slice
* if the slices cap is specified. If set to a falsy value, no others will be added.
*
* The grouper function takes an array of included ("top") items, and an array of the rest of
* the items. By default the grouper function computes the sum of the rest.
* @memberof CapMixin
* @instance
* @example
* // Do not show others
* chart.othersGrouper(null);
* // Default others grouper
* chart.othersGrouper(function (topItems, restItems) {
* var restItemsSum = d3.sum(restItems, _chart.valueAccessor()),
* restKeys = restItems.map(_chart.keyAccessor());
* if (restItemsSum > 0) {
* return topItems.concat([{
* others: restKeys,
* key: _chart.othersLabel(),
* value: restItemsSum
* }]);
* }
* return topItems;
* });
* @param {Function} [grouperFunction]
* @returns {Function|CapMixin}
*/
othersGrouper (grouperFunction) {
if (!arguments.length) {
return this._othersGrouper;
}
this._othersGrouper = grouperFunction;
return this;
}
onClick (d) {
if (d.others) {
this.filter([d.others]);
}
super.onClick(d);
}
};