Source: base/cap-mixin.js

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