Skip to content

Commit

Permalink
cap default take from front but allow back
Browse files Browse the repository at this point in the history
for #1269
add cap mixin tests too
  • Loading branch information
gordonwoodhull committed Jan 28, 2017
1 parent 533748c commit fc69f54
Show file tree
Hide file tree
Showing 2 changed files with 197 additions and 19 deletions.
152 changes: 152 additions & 0 deletions spec/cap-mixin-spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/* global loadDateFixture */
describe('dc.capMixin', function () {
var data, dimension, group;
var mixin, total;

beforeEach(function () {
data = crossfilter(loadDateFixture());
dimension = data.dimension(function (d) { return +d.value; });
group = dimension.group().reduceSum(function (d) {return Math.abs(+d.nvalue);});
total = d3.sum(group.all().map(dc.pluck('value')));

mixin = dc.capMixin(dc.baseMixin({}));

mixin.dimension(dimension)
.group(group);
});

describe('with no capping and default ordering', function () {
it('should include everything', function () {
expect(mixin.data().length).toBe(5);
});
it('should be in key order', function () {
expect(mixin.data().map(dc.pluck('key'))).toEqual([22, 33, 44, 55, 66]);
});
it('should sum to total', function () {
expect(d3.sum(mixin.data().map(dc.pluck('value')))).toEqual(total);
});
});

describe('with no capping and descending key ordering', function () {
beforeEach(function () {
mixin.ordering(function (kv) { return -kv.key; });
});
it('should include everything', function () {
expect(mixin.data().length).toBe(5);
});
it('should be in reverse key order', function () {
expect(mixin.data().map(dc.pluck('key'))).toEqual([66, 55, 44, 33, 22]);
});
});

describe('with descending value ordering', function () {
beforeEach(function () {
mixin.ordering(function (kv) { return -kv.value; });
});
describe('and no capping', function () {
it('should include everything', function () {
expect(mixin.data().length).toBe(5);
});
it('should be in reverse key order', function () {
expect(mixin.data().map(dc.pluck('key'))).toEqual([22, 44, 55, 66, 33]);
});
it('should sum to total', function () {
expect(d3.sum(mixin.data().map(dc.pluck('value')))).toEqual(total);
});
});
describe('and cap(3)', function () {
beforeEach(function () {
mixin.cap(3);
});
it('should have 3 front elements plus others element', function () {
expect(mixin.data().length).toBe(4);
expect(mixin.data().map(dc.pluck('key'))).toEqual([22, 44, 55, 'Others']);
});
it('should sum to total', function () {
expect(d3.sum(mixin.data().map(dc.pluck('value')))).toEqual(total);
});

describe('and taking from back', function () {
beforeEach(function () {
mixin.takeFront(false);
});
it('should have 3 back elements plus others element', function () {
expect(mixin.data().length).toBe(4);
expect(mixin.data().map(dc.pluck('key'))).toEqual([55, 66, 33, 'Others']);
});
it('should sum to total', function () {
expect(d3.sum(mixin.data().map(dc.pluck('value')))).toEqual(total);
});
});

describe('and no othersGrouper', function () {
beforeEach(function () {
mixin.othersGrouper(null);
});
it('should have 3 front elements', function () {
expect(mixin.data().length).toBe(3);
expect(mixin.data().map(dc.pluck('key'))).toEqual([22, 44, 55]);
});
it('should sum to correct number', function () {
expect(d3.sum(mixin.data().map(dc.pluck('value')))).toEqual(29);
});
});
});
});

describe('with ascending value ordering', function () {
beforeEach(function () {
mixin.ordering(function (kv) { return kv.value; });
});
describe('and no capping', function () {
it('should include everything', function () {
expect(mixin.data().length).toBe(5);
});
it('should be in reverse key order', function () {
expect(mixin.data().map(dc.pluck('key'))).toEqual([33, 66, 55, 44, 22]);
});
it('should sum to total', function () {
expect(d3.sum(mixin.data().map(dc.pluck('value')))).toEqual(total);
});
});
describe('and cap(3)', function () {
beforeEach(function () {
mixin.cap(3);
});
it('should have 3 front elements plus others element', function () {
expect(mixin.data().length).toBe(4);
expect(mixin.data().map(dc.pluck('key'))).toEqual([33, 66, 55, 'Others']);
});
it('should sum to total', function () {
expect(d3.sum(mixin.data().map(dc.pluck('value')))).toEqual(total);
});

describe('and taking from back', function () {
beforeEach(function () {
mixin.takeFront(false);
});
it('should have 3 back elements plus others element', function () {
expect(mixin.data().length).toBe(4);
expect(mixin.data().map(dc.pluck('key'))).toEqual([55, 44, 22, 'Others']);
});
it('should sum to total', function () {
expect(d3.sum(mixin.data().map(dc.pluck('value')))).toEqual(total);
});
});

describe('and no othersGrouper', function () {
beforeEach(function () {
mixin.othersGrouper(null);
});
it('should have 3 front elements', function () {
expect(mixin.data().length).toBe(3);
expect(mixin.data().map(dc.pluck('key'))).toEqual([33, 66, 55]);
});
it('should sum to correct number', function () {
expect(d3.sum(mixin.data().map(dc.pluck('value')))).toEqual(14);
});
});
});
});
});

64 changes: 45 additions & 19 deletions src/cap-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@
* @returns {dc.capMixin}
*/
dc.capMixin = function (_chart) {

var _cap = Infinity;

var _cap = Infinity, _takeFront = true;
var _othersLabel = 'Others';

var _othersGrouper = function (topRows) {
Expand Down Expand Up @@ -50,23 +48,28 @@ dc.capMixin = function (_chart) {
return _chart.valueAccessor()(d, i);
};

// return N biggest groups, where N is the cap, sorted in ascending order.
// 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 topRows = group.all(); // in key order
topRows = _chart._computeOrderedGroups(topRows); // re-order using ordering (defaults to key)
var items = group.all();
items = _chart._computeOrderedGroups(items); // sort by baseMixin.ordering

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

if (_othersGrouper) {
return _othersGrouper(topRows);
return _othersGrouper(items);
}
return topRows;
return items;
}
});

Expand All @@ -75,19 +78,24 @@ dc.capMixin = function (_chart) {
* {@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}.
*
* Up through dc.js 2.0.*, capping uses
* 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 sorts the items according to {@link dc.baseMixin#ordering baseMixin.ordering()}.
* So the two values essentially have to agree, but if the former is incorrect (it's easy to
* forget about `group.order()`), the latter will mask the problem. This also makes
* {@link https://github.com/dc-js/dc.js/wiki/FAQ#fake-groups fake groups} difficult to
* implement.
* 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.
*
* In dc.js 2.1 and forward, only
* {@link https://github.com/crossfilter/crossfilter/wiki/API-Reference#group_all group.all()}
* and `baseMixin.ordering()` are used.
* If your chart previously relied on `group.order()`, use `chart.ordering()` instead. If you
* actually want to cap by size but e.g. sort alphabetically by key, please
* [file an issue](https://github.com/dc-js/dc.js/issues/new) - it's still possible but we'll
* need to work up an example.
* @method cap
* @memberof dc.capMixin
* @instance
Expand All @@ -102,6 +110,24 @@ dc.capMixin = function (_chart) {
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
Expand Down

1 comment on commit fc69f54

@macyabbey
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for cleaning this up Gordon.

Please sign in to comment.