Source: cap-mixin.js

/**
 * 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.
 * @name capMixin
 * @memberof dc
 * @mixin
 * @param {Object} _chart
 * @returns {dc.capMixin}
 */
dc.capMixin = function (_chart) {
    var _cap = Infinity, _takeFront = true;
    var _othersLabel = 'Others';

    // emulate old group.top(N) ordering
    _chart.ordering(function (kv) {
        return -kv.value;
    });

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

    _chart.cappedKeyAccessor = function (d, i) {
        if (d.others) {
            return d.key;
        }
        return _chart.keyAccessor()(d, i);
    };

    _chart.cappedValueAccessor = function (d, i) {
        if (d.others) {
            return d.value;
        }
        return _chart.valueAccessor()(d, i);
    };

    // return N "top" groups, where N is the cap, sorted by baseMixin.ordering
    // whether top means front or back depends on takeFront
    _chart.data(function (group) {
        if (_cap === Infinity) {
            return _chart._computeOrderedGroups(group.all());
        } else {
            var items = group.all(), rest;
            items = _chart._computeOrderedGroups(items); // sort by baseMixin.ordering

            if (_cap) {
                if (_takeFront) {
                    rest = items.slice(_cap);
                    items = items.slice(0, _cap);
                } else {
                    var start = Math.max(0, items.length - _cap);
                    rest = items.slice(0, start);
                    items = items.slice(start);
                }
            }

            if (_othersGrouper) {
                return _othersGrouper(items, rest);
            }
            return items;
        }
    });

    /**
     * Get or set the count of elements to that will be included in the cap. If there is an
     * {@link dc.capMixin#othersGrouper othersGrouper}, any further elements will be combined in an
     * extra element with its name determined by {@link dc.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 dc.baseMixin#ordering baseMixin.ordering()} to determine the order of
     * elements. Then `cap` and {@link dc.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 dc.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 dc.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}.
     * @method cap
     * @memberof dc.capMixin
     * @instance
     * @param {Number} [count=Infinity]
     * @returns {Number|dc.capMixin}
     */
    _chart.cap = function (count) {
        if (!arguments.length) {
            return _cap;
        }
        _cap = count;
        return _chart;
    };

    /**
     * Get or set the direction of capping. If set, the chart takes the first
     * {@link dc.capMixin#cap cap} elements from the sorted array of elements; otherwise
     * it takes the last `cap` elements.
     * @method takeFront
     * @memberof dc.capMixin
     * @instance
     * @param {Boolean} [takeFront=true]
     * @returns {Boolean|dc.capMixin}
     */
    _chart.takeFront = function (takeFront) {
        if (!arguments.length) {
            return _takeFront;
        }
        _takeFront = takeFront;
        return _chart;
    };

    /**
     * Get or set the label for *Others* slice when slices cap is specified.
     * @method othersLabel
     * @memberof dc.capMixin
     * @instance
     * @param {String} [label="Others"]
     * @returns {String|dc.capMixin}
     */
    _chart.othersLabel = function (label) {
        if (!arguments.length) {
            return _othersLabel;
        }
        _othersLabel = label;
        return _chart;
    };

    /**
     * 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.
     * @method othersGrouper
     * @memberof dc.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|dc.capMixin}
     */
    _chart.othersGrouper = function (grouperFunction) {
        if (!arguments.length) {
            return _othersGrouper;
        }
        _othersGrouper = grouperFunction;
        return _chart;
    };

    dc.override(_chart, 'onClick', function (d) {
        if (d.others) {
            _chart.filter([d.others]);
        }
        _chart._onClick(d);
    });

    return _chart;
};