dc
Version:
A multi-dimensional charting library built to work natively with crossfilter and rendered using d3.js
760 lines (686 loc) • 28.8 kB
JavaScript
/* global appendChartID, loadDateFixture, loadIrisFixture, makeDate */
describe('dc.bubbleChart', function () {
var id, chart, data;
var dateFixture;
var dimension, group;
var countryDimension;
var width = 900, height = 350;
beforeEach(function () {
dateFixture = loadDateFixture();
data = crossfilter(dateFixture);
dimension = data.dimension(function (d) {
return d.status;
});
group = dimension.group()
.reduce(
//add
function (p, v) {
++p.count;
p.value += +v.value;
return p;
},
//remove
function (p, v) {
--p.count;
p.value -= +v.value;
return p;
},
//init
function () {
return {count: 0, value: 0};
}
)
.order(function (d) {return d.value;});
countryDimension = data.dimension(function (d) {
return d.countrycode;
});
id = 'bubble-chart';
appendChartID(id);
chart = dc.bubbleChart('#' + id);
chart.dimension(dimension).group(group)
.width(width).height(height)
.colors(['#a60000', '#ff0000', '#ff4040', '#ff7373', '#67e667', '#39e639', '#00cc00'])
.colorDomain([0,220])
.colorAccessor(function (p) {
return p.value.value;
})
.keyAccessor(function (p) {
return p.value.value;
})
.valueAccessor(function (p) {
return p.value.count;
})
.radiusValueAccessor(function (p) {
return p.value.count;
})
.x(d3.scaleLinear().domain([0, 300]))
.y(d3.scaleLinear().domain([0, 10]))
.r(d3.scaleLinear().domain([0, 30]))
.maxBubbleRelativeSize(0.3)
.transitionDuration(0)
.renderLabel(true)
.renderTitle(true)
.title(function (p) {
return p.key + ': {count:' + p.value.count + ',value:' + p.value.value + '}';
});
});
it('assigns colors', function () {
expect(chart.colors()).not.toBeNull();
});
it('sets the radius scale', function () {
expect(chart.r()).not.toBeNull();
});
it('sets the radius value accessor', function () {
expect(chart.radiusValueAccessor()).not.toBeNull();
});
it('sets the x units', function () {
expect(chart.xUnits()).toBe(dc.units.integers);
});
it('creates the x axis', function () {
expect(chart.xAxis()).not.toBeNull();
});
it('creates the y axis', function () {
expect(chart.yAxis()).not.toBeNull();
});
describe('render', function () {
beforeEach(function () {
chart.render();
});
it('generates right number of bubbles', function () {
expect(chart.selectAll('circle.bubble').nodes().length).toBe(2);
});
it('calculates right cx for each bubble', function () {
chart.selectAll('g.node').each(function (d, i) {
if (i === 0) {
expect(d3.select(this).attr('transform')).toMatchTranslate(601.3333333333334, 155, 3);
}
if (i === 1) {
expect(d3.select(this).attr('transform')).toMatchTranslate(541.2, 155);
}
});
});
it('generates opaque groups and circles for each bubble', function () {
chart.selectAll('g.node').each(function (d, i) {
expect(d3.select(this).attr('opacity')).toBeNull();
expect(d3.select(this).select('circle').attr('opacity')).toBe('1');
});
});
it('calculates right r for each bubble', function () {
chart.selectAll('circle.bubble').each(function (d, i) {
if (i === 0) {
expect(Number(d3.select(this).attr('r'))).toBeCloseTo(49.33333333333333, 3);
}
if (i === 1) {
expect(Number(d3.select(this).attr('r'))).toBeCloseTo(49.33333333333333, 3);
}
});
});
it('attaches each bubble with index based class', function () {
chart.selectAll('circle.bubble').each(function (d, i) {
if (i === 0) {
expect(d3.select(this).attr('class')).toBe('bubble _0');
}
if (i === 1) {
expect(d3.select(this).attr('class')).toBe('bubble _1');
}
});
});
it('generates right number of labels', function () {
expect(chart.selectAll('g.node text').nodes().length).toBe(2);
});
it('creates correct label for each bubble', function () {
chart.selectAll('g.node text').each(function (d, i) {
if (i === 0) {
expect(d3.select(this).text()).toBe('F');
}
if (i === 1) {
expect(d3.select(this).text()).toBe('T');
}
});
});
it('generates right number of titles', function () {
expect(chart.selectAll('g.node title').nodes().length).toBe(2);
});
it('creates correct title for each bubble', function () {
chart.selectAll('g.node title').each(function (d, i) {
if (i === 0) {
expect(d3.select(this).text()).toBe('F: {count:5,value:220}');
}
if (i === 1) {
expect(d3.select(this).text()).toBe('T: {count:5,value:198}');
}
});
});
it('fills bubbles with correct colors', function () {
chart.selectAll('circle.bubble').each(function (d, i) {
if (i === 0) {
expect(d3.select(this).attr('fill')).toMatch(/#00cc00/i);
}
if (i === 1) {
expect(d3.select(this).attr('fill')).toMatch(/#00cc00/i);
}
});
});
});
describe('bubble chart w/o label & title', function () {
beforeEach(function () {
chart.renderLabel(false).renderTitle(false).render();
});
it('generates right number of labels', function () {
expect(chart.selectAll('g.node text').nodes().length).toBe(0);
});
it('generates right number of titles', function () {
expect(chart.selectAll('g.node title').nodes().length).toBe(0);
});
});
describe('with filter', function () {
beforeEach(function () {
chart.filter('F').render();
});
it('deselects bubble based on filter value', function () {
chart.selectAll('g.node').each(function (d, i) {
if (i === 0) {
expect(d3.select(this).attr('class')).toBe('node selected');
}
if (i === 1) {
expect(d3.select(this).attr('class')).toBe('node deselected');
}
});
});
it('handles multi-selection highlight', function () {
chart.filter('T');
chart.redraw();
chart.selectAll('g.node').each(function (d, i) {
expect(d3.select(this).attr('class')).toBe('node selected');
});
});
});
describe('update', function () {
beforeEach(function () {
chart.render();
countryDimension.filter('CA');
chart.redraw();
});
it('creates correct label for each bubble', function () {
chart.selectAll('g.node title').each(function (d, i) {
if (i === 0) {
expect(d3.select(this).text()).toBe('F: {count:0,value:0}');
}
if (i === 1) {
expect(d3.select(this).text()).toBe('T: {count:2,value:77}');
}
});
});
it('fills bubbles with correct colors', function () {
chart.selectAll('circle.bubble').each(function (d, i) {
if (i === 0) {
expect(d3.select(this).attr('fill')).toMatch(/#a60000/i);
}
if (i === 1) {
expect(d3.select(this).attr('fill')).toMatch(/#ff4040/i);
}
});
});
describe('with bubble sorting', function () {
beforeEach(function () {
chart
.sortBubbleSize(true)
.render();
});
it('creates correct label for each bubble', function () {
chart.selectAll('g.node title').each(function (d, i) {
if (i === 0) {
expect(d3.select(this).text()).toBe('T: {count:2,value:77}');
}
if (i === 1) {
expect(d3.select(this).text()).toBe('F: {count:0,value:0}');
}
});
});
it('fills bubbles with correct colors', function () {
chart.selectAll('circle.bubble').each(function (d, i) {
if (i === 0) {
expect(d3.select(this).attr('fill')).toMatch(/#ff4040/i);
}
if (i === 1) {
expect(d3.select(this).attr('fill')).toMatch(/#a60000/i);
}
});
});
});
describe('with empty bins removed', function () {
beforeEach(function () {
chart.group(removeEmptyBins(group))
.redraw();
});
it('creates the right number of bubbles', function () {
expect(chart.selectAll('g.node').size()).toBe(1);
});
it('creates correct label for each bubble', function () {
expect(chart.selectAll('g.node title').text()).toBe('T: {count:2,value:77}');
});
it('fills bubbles with correct colors', function () {
expect(chart.selectAll('circle.bubble').attr('fill')).toMatch(/#ff4040/i);
});
function removeEmptyBins (sourceGroup) {
return {
all: function () {
return sourceGroup.all().filter(function (d) {
return d.value.count !== 0;
});
}
};
}
});
});
describe('with no filter', function () {
beforeEach(function () {
countryDimension.filter('ZZ');
chart.render();
});
it('sets invisible if bubble has 0 r', function () {
chart.selectAll('g.node text').each(function (d, i) {
expect(Number(d3.select(this).attr('opacity'))).toBe(0);
});
});
});
describe('with elastic axises', function () {
beforeEach(function () {
chart.elasticY(true)
.yAxisPadding(3)
.elasticX(true)
.xAxisPadding(20)
.render();
});
it('auto calculates x range based on width', function () {
expect(chart.x().range()[0]).toBe(0);
expect(chart.x().range()[1]).toBe(820);
});
it('sets the x domain', function () {
expect(chart.x().domain()[0]).toBe(178);
expect(chart.x().domain()[1]).toBe(240);
});
it('auto calculates y range based on height', function () {
expect(chart.y().range()[0]).toBe(310);
expect(chart.y().range()[1]).toBe(0);
});
it('sets the y domain', function () {
expect(chart.y().domain()[0]).toBe(2);
expect(chart.y().domain()[1]).toBe(8);
});
});
describe('renderlet', function () {
var renderlet;
beforeEach(function () {
// spyOn doesn't seem to work with plain functions
renderlet = jasmine.createSpy('renderlet', function (chart) {
chart.selectAll('circle').attr('fill', 'red');
});
renderlet.and.callThrough();
chart.on('renderlet', renderlet);
});
it('is invoked with render', function () {
chart.render();
expect(chart.selectAll('circle').attr('fill')).toBe('red');
expect(renderlet).toHaveBeenCalled();
});
it('is invoked with redraw', function () {
chart.render().redraw();
expect(chart.selectAll('circle').attr('fill')).toBe('red');
expect(renderlet.calls.count()).toEqual(2);
});
});
describe('non-unique keys', function () {
// plot all rows as (value, nvalue) - a common scatterplot scenario
beforeEach(function () {
var rowDimension = data.dimension(function (d, i) {
return i;
});
var rowGroup = rowDimension.group();
chart.dimension(rowDimension).group(rowGroup)
.keyAccessor(function (kv) {
return +dateFixture[kv.key].value;
})
.valueAccessor(function (kv) {
return +dateFixture[kv.key].nvalue;
})
.elasticY(true)
.yAxisPadding(2)
.elasticX(true)
.xAxisPadding(2);
chart.render();
});
it('generates right number of bubbles', function () {
expect(chart.selectAll('circle.bubble').nodes().length).toBe(10);
});
it('auto calculates x range based on width', function () {
expect(chart.x().range()[0]).toBe(0);
expect(chart.x().range()[1]).toBe(820);
});
it('sets the x domain', function () {
expect(chart.x().domain()[0]).toBe(20);
expect(chart.x().domain()[1]).toBe(68);
});
it('auto calculates y range based on height', function () {
expect(chart.y().range()[0]).toBe(310);
expect(chart.y().range()[1]).toBe(0);
});
it('sets the y domain', function () {
expect(chart.y().domain()[0]).toBe(-7);
expect(chart.y().domain()[1]).toBe(12);
});
});
describe('with logarithmic scales', function () {
beforeEach(function () {
var rowDimension = data.dimension(function (d, i) {
return i;
});
var rowGroup = rowDimension.group();
chart
.dimension(rowDimension)
.group(rowGroup)
.keyAccessor(function (kv) {
return 0;
})
.valueAccessor(function (kv) {
return 0;
})
.x(d3.scaleLog().domain([1, 300]))
.y(d3.scaleLog().domain([1, 10]))
.elasticX(false)
.elasticY(false);
chart.render();
});
it('renders without errors', function () {
chart.selectAll('g.node').each(function (d, i) {
expect(d3.select(this).attr('transform')).toMatchTranslate(0, 0);
});
});
});
describe('with a date-based scale', function () {
beforeEach(function () {
dimension = data.dimension(function (d) { return d3.utcDay(d.dd); });
group = dimension.group();
chart
.dimension(dimension)
.group(group)
.x(d3.scaleUtc().domain([makeDate(2012, 0, 1), makeDate(2012, 11, 31)]))
.elasticX(true)
.elasticY(true)
.keyAccessor(function (kv) {
return kv.key;
})
.valueAccessor(function (kv) {
return kv.value;
})
.radiusValueAccessor(function (kv) {
return kv.value;
})
.colors(d3.scaleOrdinal().range(['#a60000', '#ff0000', '#ff4040', '#ff7373', '#67e667', '#39e639', '#00cc00']))
.colorAccessor(function (kv) {
return kv.key;
})
.render();
});
it('draws bubbles in appropriate locations', function () {
var coords = [
[0,310], [149.1,310], [170.4,0], [394,310], [489.9,155], [820,155],
];
chart.selectAll('g.node').each(function (d, i) {
expect(d3.select(this).attr('transform'))
.toMatchTranslate(coords[i][0], coords[i][1], 1);
});
});
it('calculates elastic x axis exactly', function () {
expect(chart.x().domain()).toEqual([makeDate(2012, 4, 25), makeDate(2012, 7, 10)]);
});
describe('with 10 day padding', function () {
beforeEach(function () {
chart.xAxisPaddingUnit(d3.utcDay)
.xAxisPadding(10)
.render();
});
it('should stretch the domain appropriately', function () {
expect(chart.x().domain()).toEqual([makeDate(2012, 4, 15), makeDate(2012, 7, 20)]);
});
});
describe('with 2 month padding', function () {
beforeEach(function () {
chart.xAxisPaddingUnit(d3.utcMonth)
.xAxisPadding(2)
.render();
});
it('should stretch the domain appropriately', function () {
expect(chart.x().domain()).toEqual([makeDate(2012, 2, 25), makeDate(2012, 9, 10)]);
});
});
});
describe('with minimum radius', function () {
beforeEach(function () {
chart
.minRadius(1)
.render();
});
it('shows smaller bubbles', function () {
chart.selectAll('circle.bubble').each(function (d, i) {
if (i === 0) {
expect(Number(d3.select(this).attr('r'))).toBeCloseTo(41.83333333333333, 3);
}
if (i === 1) {
expect(Number(d3.select(this).attr('r'))).toBeCloseTo(41.83333333333333, 3);
}
});
});
});
describe('iris filtering', function () {
/* eslint camelcase: 0 */
// 2-chart version of from http://bl.ocks.org/gordonwoodhull/14c623b95993808d69620563508edba6
var irisData, heatMap, sepalDim, sepalGroup;
beforeEach(function () {
irisData = loadIrisFixture();
var fields = {
sl: 'sepal_length',
sw: 'sepal_width',
pl: 'petal_length',
pw: 'petal_width'
};
var species = ['setosa', 'versicolor', 'virginica'];
irisData.forEach(function (d) {
Object.keys(fields).forEach(function (ab) {
d[fields[ab]] = +d[fields[ab]];
});
});
// autogenerate a key function for an extent
function key_function (extent) {
var div = extent[1] - extent[0] < 5 ? 2 : 1;
return function (k) {
return Math.floor(k * div) / div;
};
}
var extents = {};
var keyfuncs = {};
Object.keys(fields).forEach(function (ab) {
extents[ab] = d3.extent(irisData, function (d) { return d[fields[ab]]; });
keyfuncs[ab] = key_function(extents[ab]);
});
data = crossfilter(irisData);
function duo_key (ab1, ab2) {
return function (d) {
return [keyfuncs[ab1](d[fields[ab1]]), keyfuncs[ab2](d[fields[ab2]])];
};
}
function key_part (i) {
return function (kv) {
return kv.key[i];
};
}
function reduce_species (group) {
group.reduce(
function (p, v) {
p[v.species]++;
p.total++;
return p;
}, function (p, v) {
p[v.species]--;
p.total--;
return p;
}, function () {
var init = {total: 0};
species.forEach(function (s) { init[s] = 0; });
return init;
}
);
}
function max_species (d) {
var max = 0, i = -1;
species.forEach(function (s, j) {
if (d.value[s] > max) {
max = d.value[s];
i = j;
}
});
return i >= 0 ? species[i] : null;
}
function initialize_bubble (bubbleChart) {
bubbleChart
.transitionDuration(0)
.width(400)
.height(400)
.x(d3.scaleLinear()).xAxisPadding(0.5)
.y(d3.scaleLinear()).yAxisPadding(0.5)
.elasticX(true)
.elasticY(true)
.keyAccessor(key_part(0))
.valueAccessor(key_part(1))
.radiusValueAccessor(function (kv) { return kv.value.total; })
.elasticRadius(true)
.colors(d3.scaleOrdinal()
.domain(species.concat('none'))
.range(['#e41a1c','#377eb8','#4daf4a', '#f8f8f8']))
.colorAccessor(function (d) {
return max_species(d) || 'none';
})
.label(function (d) {
return d.value.total;
})
.title(function (d) {
return JSON.stringify(d.value, null, 2);
});
}
function initialize_heatmap (heatMap) {
heatMap
.transitionDuration(0)
.width(400)
.height(400)
.xBorderRadius(15).yBorderRadius(15)
.keyAccessor(key_part(0))
.valueAccessor(key_part(1))
.colors(d3.scaleOrdinal()
.domain(species.concat('none'))
.range(['#e41a1c','#377eb8','#4daf4a', '#f8f8f8']))
.colorAccessor(function (d) {
return max_species(d) || 'none';
});
}
var heatId = 'heat-map';
appendChartID(heatId);
heatMap = dc.heatMap('#' + heatId);
sepalDim = data.dimension(duo_key('sl', 'sw')); sepalGroup = sepalDim.group();
var petalDim = data.dimension(duo_key('pl', 'pw')), petalGroup = petalDim.group();
reduce_species(sepalGroup);
reduce_species(petalGroup);
initialize_bubble(chart.dimension(sepalDim).group(sepalGroup));
initialize_heatmap(heatMap.dimension(petalDim).group(petalGroup));
chart.render();
heatMap.render();
});
// return brand-new objects and keys every time
function clone_group (group) {
function clone_kvs (all) {
return all.map(function (kv) {
return {
key: kv.key.slice(0),
value: Object.assign({}, kv.value)
};
});
}
return {
all: function () {
return clone_kvs(group.all());
},
top: function (N) {
return clone_kvs(group.top(N));
}
};
}
function testBubbleRadiiCol3 (chart) {
var bubbles = chart.selectAll('circle.bubble').nodes();
var expected = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34.5, 16.1, 0, 0, 16.1, 59.1, 34.5, 16.1, 96, 0, 22.2, 0, 0, 0, 0];
bubbles.forEach(function (b, i) {
expect(+d3.select(b).attr('r')).toBeWithinDelta(expected[i], 0.1);
});
}
function testBubbleTitlesCol3 (chart) {
var titles = chart.selectAll('g.node title').nodes();
var expected = [
{'total': 0,'setosa': 0,'versicolor': 0,'virginica': 0},{'total': 0,'setosa': 0,'versicolor': 0,'virginica': 0},
{'total': 0,'setosa': 0,'versicolor': 0,'virginica': 0},{'total': 0,'setosa': 0,'versicolor': 0,'virginica': 0},
{'total': 0,'setosa': 0,'versicolor': 0,'virginica': 0},{'total': 0,'setosa': 0,'versicolor': 0,'virginica': 0},
{'total': 0,'setosa': 0,'versicolor': 0,'virginica': 0},{'total': 0,'setosa': 0,'versicolor': 0,'virginica': 0},
{'total': 0,'setosa': 0,'versicolor': 0,'virginica': 0},{'total': 0,'setosa': 0,'versicolor': 0,'virginica': 0},
{'total': 0,'setosa': 0,'versicolor': 0,'virginica': 0},{'total': 0,'setosa': 0,'versicolor': 0,'virginica': 0},
{'total': 4,'setosa': 0,'versicolor': 0,'virginica': 4},{'total': 1,'setosa': 0,'versicolor': 0,'virginica': 1},
{'total': 0,'setosa': 0,'versicolor': 0,'virginica': 0},{'total': 0,'setosa': 0,'versicolor': 0,'virginica': 0},
{'total': 1,'setosa': 0,'versicolor': 0,'virginica': 1},{'total': 8,'setosa': 0,'versicolor': 1,'virginica': 7},
{'total': 4,'setosa': 0,'versicolor': 0,'virginica': 4},{'total': 1,'setosa': 0,'versicolor': 0,'virginica': 1},
{'total': 14,'setosa': 0,'versicolor': 1,'virginica': 13},{'total': 0,'setosa': 0,'versicolor': 0,'virginica': 0},
{'total': 2,'setosa': 0,'versicolor': 0,'virginica': 2},{'total': 0,'setosa': 0,'versicolor': 0,'virginica': 0},
{'total': 0,'setosa': 0,'versicolor': 0,'virginica': 0},{'total': 0,'setosa': 0,'versicolor': 0,'virginica': 0},
{'total': 0,'setosa': 0,'versicolor': 0,'virginica': 0}];
titles.forEach(function (t, i) {
expect(JSON.parse(d3.select(t).text())).toEqual(expected[i]);
});
}
function testBubbleLabelsCol3 (chart) {
var labels = chart.selectAll('g.node text').nodes();
var expected = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 0, 0, 1, 8, 4, 1, 14, 0, 2, 0, 0, 0, 0];
labels.forEach(function (l, i) {
expect(+d3.select(l).text()).toBe(expected[i]);
});
}
describe('column filtering with straight crossfilter', function () {
beforeEach(function () {
var axisLabel = d3.select(heatMap.selectAll('.cols.axis text').nodes()[3]);
axisLabel.on('click')(axisLabel.datum());
d3.timerFlush();
});
it('updates bubble radii correctly', function () {
testBubbleRadiiCol3(chart);
});
it('updates bubble titles correctly', function () {
testBubbleTitlesCol3(chart);
});
it('updates bubble labels correctly', function () {
testBubbleLabelsCol3(chart);
});
});
describe('column filtering with cloned results', function () {
beforeEach(function () {
chart.group(clone_group(sepalGroup));
chart.render();
var axisLabel = d3.select(heatMap.selectAll('.cols.axis text').nodes()[3]);
axisLabel.on('click')(axisLabel.datum());
d3.timerFlush();
});
it('updates bubble radii correctly', function () {
testBubbleRadiiCol3(chart);
});
it('updates bubble titles correctly', function () {
testBubbleTitlesCol3(chart);
});
it('updates bubble labels correctly', function () {
testBubbleLabelsCol3(chart);
});
});
});
});