dc
Version:
A multi-dimensional charting library built to work natively with crossfilter and rendered using d3.js
621 lines (545 loc) • 26 kB
JavaScript
/* global appendChartID, loadColorFixture, loadColorFixture2, loadIrisFixture */
describe('dc.heatmap', function () {
var id, data, dimension, group, chart, chartHeight, chartWidth;
beforeEach(function () {
data = crossfilter(loadColorFixture());
dimension = data.dimension(function (d) { return [+d.colData, +d.rowData]; });
group = dimension.group().reduceSum(function (d) { return +d.colorData; });
chartHeight = 210;
chartWidth = 210;
id = 'heatmap-chart';
appendChartID(id);
chart = dc.heatMap('#' + id);
chart
.dimension(dimension)
.group(group)
.keyAccessor(function (d) { return d.key[0]; })
.valueAccessor(function (d) { return d.key[1]; })
.colorAccessor(function (d) { return d.value; })
.colors(['#000001', '#000002', '#000003', '#000004'])
.title(function (d) {return d.key + ': ' + d.value; })
.height(chartHeight)
.width(chartWidth)
.transitionDuration(0)
.margins({top: 5, right: 5, bottom: 5, left: 5})
.calculateColorDomain();
});
describe('rendering the heatmap', function () {
beforeEach(function () {
chart.render();
});
it('should create svg', function () {
expect(chart.svg()).not.toBeNull();
});
it('should transform the graph position using the graph margins', function () {
expect(chart.select('g.heatmap').attr('transform')).toMatchTranslate(5, 5);
});
it('should position the heatboxes in a matrix', function () {
var heatBoxes = chart.selectAll('rect.heat-box');
expect(+heatBoxes.nodes()[0].getAttribute('x')).toEqual(0);
expect(+heatBoxes.nodes()[0].getAttribute('y')).toEqual(100);
expect(+heatBoxes.nodes()[1].getAttribute('x')).toEqual(0);
expect(+heatBoxes.nodes()[1].getAttribute('y')).toEqual(0);
expect(+heatBoxes.nodes()[2].getAttribute('x')).toEqual(100);
expect(+heatBoxes.nodes()[2].getAttribute('y')).toEqual(100);
expect(+heatBoxes.nodes()[3].getAttribute('x')).toEqual(100);
expect(+heatBoxes.nodes()[3].getAttribute('y')).toEqual(0);
});
it('should color heatboxes using the provided color option', function () {
var heatBoxes = chart.selectAll('rect.heat-box');
expect(heatBoxes.nodes()[0].getAttribute('fill')).toMatch(/#000001/i);
expect(heatBoxes.nodes()[1].getAttribute('fill')).toMatch(/#000002/i);
expect(heatBoxes.nodes()[2].getAttribute('fill')).toMatch(/#000003/i);
expect(heatBoxes.nodes()[3].getAttribute('fill')).toMatch(/#000004/i);
});
it('should size heatboxes based on the size of the matrix', function () {
chart.selectAll('rect.heat-box').each(function () {
expect(+this.getAttribute('height')).toEqual(100);
expect(+this.getAttribute('width')).toEqual(100);
});
});
it('should position the y-axis labels with their associated rows', function () {
var yaxisTexts = chart.selectAll('.rows.axis text');
expect(+yaxisTexts.nodes()[0].getAttribute('y')).toEqual(150);
expect(+yaxisTexts.nodes()[0].getAttribute('x')).toEqual(0);
expect(+yaxisTexts.nodes()[1].getAttribute('y')).toEqual(50);
expect(+yaxisTexts.nodes()[1].getAttribute('x')).toEqual(0);
});
it('should have labels on the y-axis corresponding to the row values', function () {
var yaxisTexts = chart.selectAll('.rows.axis text');
expect(yaxisTexts.nodes()[0].textContent).toEqual('1');
expect(yaxisTexts.nodes()[1].textContent).toEqual('2');
});
it('should position the x-axis labels with their associated columns', function () {
var xaxisTexts = chart.selectAll('.cols.axis text');
expect(+xaxisTexts.nodes()[0].getAttribute('y')).toEqual(200);
expect(+xaxisTexts.nodes()[0].getAttribute('x')).toEqual(50);
expect(+xaxisTexts.nodes()[1].getAttribute('y')).toEqual(200);
expect(+xaxisTexts.nodes()[1].getAttribute('x')).toEqual(150);
});
it('should have labels on the x-axis corresponding to the row values', function () {
var xaxisTexts = chart.selectAll('.cols.axis text');
expect(xaxisTexts.nodes()[0].textContent).toEqual('1');
expect(xaxisTexts.nodes()[1].textContent).toEqual('2');
});
it('should have tooltips (titles)', function () {
var titles = chart.selectAll('title');
expect(titles.nodes().length).toEqual(4);
expect(titles.nodes()[0].textContent).toEqual('1,1: 2');
expect(titles.nodes()[2].textContent).toEqual('2,1: 6');
});
describe('with custom labels', function () {
beforeEach(function () {
chart.colsLabel(function (x) { return 'col ' + x;})
.rowsLabel(function (x) { return 'row ' + x;})
.redraw();
});
it('should display the custom labels on the x axis', function () {
var xaxisTexts = chart.selectAll('.cols.axis text');
expect(xaxisTexts.nodes()[0].textContent).toEqual('col 1');
expect(xaxisTexts.nodes()[1].textContent).toEqual('col 2');
});
it('should display the custom labels on the y axis', function () {
var yaxisTexts = chart.selectAll('.rows.axis text');
expect(yaxisTexts.nodes()[0].textContent).toEqual('row 1');
expect(yaxisTexts.nodes()[1].textContent).toEqual('row 2');
});
});
describe('box radius', function () {
it('should default the x', function () {
chart.select('rect.heat-box').each(function () {
expect(this.getAttribute('rx')).toBe('6.75');
});
});
it('should default the y', function () {
chart.select('rect.heat-box').each(function () {
expect(this.getAttribute('ry')).toBe('6.75');
});
});
it('should set the radius to an overridden x', function () {
chart.xBorderRadius(7);
chart.render();
chart.select('rect.heat-box').each(function () {
expect(this.getAttribute('rx')).toBe('7');
});
});
it('should set the radius to an overridden y', function () {
chart.yBorderRadius(7);
chart.render();
chart.select('rect.heat-box').each(function () {
expect(this.getAttribute('ry')).toBe('7');
});
});
});
});
describe('override scale domains', function () {
beforeEach(function () {
chart.rows([1]);
chart.cols([1]);
chart.render();
});
it('should only have 1 row on the y axis', function () {
var yaxisTexts = chart.selectAll('.rows.axis text');
expect(yaxisTexts.nodes().length).toEqual(1);
expect(yaxisTexts.nodes()[0].textContent).toEqual('1');
});
it('should only have 1 col on the x axis', function () {
var xaxisTexts = chart.selectAll('.cols.axis text');
expect(xaxisTexts.nodes().length).toEqual(1);
expect(xaxisTexts.nodes()[0].textContent).toEqual('1');
});
it('should reset the rows to using the chart data on the y axis', function () {
chart.rows(null);
chart.redraw();
var yaxisTexts = chart.selectAll('.rows.axis text');
expect(yaxisTexts.nodes().length).toEqual(2);
expect(yaxisTexts.nodes()[0].textContent).toEqual('1');
expect(yaxisTexts.nodes()[1].textContent).toEqual('2');
});
it('should reset the cols to using the chart data on the y axis', function () {
chart.cols(null);
chart.redraw();
var xaxisTexts = chart.selectAll('.cols.axis text');
expect(xaxisTexts.nodes().length).toEqual(2);
expect(xaxisTexts.nodes()[0].textContent).toEqual('1');
expect(xaxisTexts.nodes()[1].textContent).toEqual('2');
});
});
describe('use a custom ordering on x and y axes', function () {
beforeEach(function () {
chart.rowOrdering(d3.descending);
chart.colOrdering(d3.descending);
chart.render();
});
it('should have descending rows', function () {
var yaxisTexts = chart.selectAll('.rows.axis text');
expect(yaxisTexts.nodes()[0].textContent).toEqual('2');
expect(yaxisTexts.nodes()[1].textContent).toEqual('1');
});
it('should have descending cols', function () {
var yaxisTexts = chart.selectAll('.rows.axis text');
expect(yaxisTexts.nodes()[0].textContent).toEqual('2');
expect(yaxisTexts.nodes()[1].textContent).toEqual('1');
});
});
describe('change crossfilter', function () {
var data2, dimension2, group2, originalDomain;
var reduceDimensionValues = function (dimension) {
return dimension.top(Infinity).reduce(function (p, d) {
p.cols.add(d.colData);
p.rows.add(d.rowData);
return p;
}, {cols: d3.set(), rows: d3.set()});
};
beforeEach(function () {
data2 = crossfilter(loadColorFixture2());
dimension2 = data2.dimension(function (d) { return [+d.colData, +d.rowData]; });
group2 = dimension2.group().reduceSum(function (d) { return +d.colorData; });
originalDomain = reduceDimensionValues(dimension);
chart.dimension(dimension2).group(group2);
chart.render();
chart.dimension(dimension).group(group);
chart.redraw();
});
it('should have the correct number of columns', function () {
chart.selectAll('.box-group').each(function (d) {
expect(originalDomain.cols.has(d.key[0])).toBeTruthy();
});
chart.selectAll('.cols.axis text').each(function (d) {
expect(originalDomain.cols.has(d)).toBeTruthy();
});
});
it('should have the correct number of rows', function () {
chart.selectAll('.box-group').each(function (d) {
expect(originalDomain.rows.has(d.key[1])).toBeTruthy();
});
chart.selectAll('.rows.axis text').each(function (d) {
expect(originalDomain.rows.has(d)).toBeTruthy();
});
});
});
describe('indirect filtering', function () {
var dimension2;
beforeEach(function () {
dimension2 = data.dimension(function (d) { return +d.colorData; });
chart.dimension(dimension).group(group);
chart.render();
dimension2.filter('3');
chart.redraw();
});
it('should update the title of the boxes', function () {
var titles = chart.selectAll('.box-group title');
var expected = ['1,1: 0', '1,2: 0', '2,1: 6', '2,2: 0'];
titles.each(function (d) {
expect(this.textContent).toBe(expected.shift());
});
});
});
describe('filtering', function () {
var filterX, filterY;
var otherDimension;
beforeEach(function () {
filterX = Math.ceil(Math.random() * 2);
filterY = Math.ceil(Math.random() * 2);
otherDimension = data.dimension(function (d) { return +d.colData; });
chart.render();
});
function clickCellOnChart (chart, x, y) {
var oneCell = chart.selectAll('.box-group').filter(function (d) {
return d.key[0] === x && d.key[1] === y;
});
oneCell.select('rect').on('click')(oneCell.datum());
return oneCell;
}
it('cells should have the appropriate class', function () {
clickCellOnChart(chart, filterX, filterY);
chart.selectAll('.box-group').each(function (d) {
var cell = d3.select(this);
if (d.key[0] === filterX && d.key[1] === filterY) {
expect(cell.classed('selected')).toBeTruthy();
expect(chart.hasFilter(d.key)).toBeTruthy();
} else {
expect(cell.classed('deselected')).toBeTruthy();
expect(chart.hasFilter(d.key)).toBeFalsy();
}
});
});
it('should keep all data points for that cell', function () {
var otherGroup = otherDimension.group().reduceSum(function (d) { return +d.colorData; });
var otherChart = dc.baseChart({}).dimension(otherDimension).group(otherGroup);
otherChart.render();
var clickedCell = clickCellOnChart(chart, filterX, filterY);
expect(otherChart.data()[filterX - 1].value).toEqual(clickedCell.datum().value);
});
it('should be able to clear filters by filtering with null', function () {
clickCellOnChart(chart, filterX, filterY);
expect(otherDimension.top(Infinity).length).toBe(2);
chart.filter(null);
expect(otherDimension.top(Infinity).length).toBe(8);
});
});
describe('click events', function () {
beforeEach(function () {
chart.render();
});
it('should toggle a filter for the clicked box', function () {
chart.selectAll('.box-group').each(function (d) {
var cell = d3.select(this).select('rect');
cell.on('click')(d);
expect(chart.hasFilter(d.key)).toBeTruthy();
cell.on('click')(d);
expect(chart.hasFilter(d.key)).toBeFalsy();
});
});
describe('on axis labels', function () {
function assertOnlyThisAxisIsFiltered (chart, axis, value) {
chart.selectAll('.box-group').each(function (d) {
if (d.key[axis] === value) {
expect(chart.hasFilter(d.key)).toBeTruthy();
} else {
expect(chart.hasFilter(d.key)).toBeFalsy();
}
});
}
describe('with nothing previously filtered', function () {
it('should filter all cells on that axis', function () {
chart.selectAll('.cols.axis text').each(function (d) {
var axisLabel = d3.select(this);
axisLabel.on('click')(d);
assertOnlyThisAxisIsFiltered(chart, 0, d);
axisLabel.on('click')(d);
});
chart.selectAll('.rows.axis text').each(function (d) {
var axisLabel = d3.select(this);
axisLabel.on('click')(d);
assertOnlyThisAxisIsFiltered(chart, 1, d);
axisLabel.on('click')(d);
});
});
});
describe('with one cell on that axis already filtered', function () {
it('should filter all cells on that axis (and the original cell should remain filtered)', function () {
var boxNodes = chart.selectAll('.box-group').nodes();
var box = d3.select(boxNodes[Math.floor(Math.random() * boxNodes.length)]);
box.select('rect').on('click')(box.datum());
expect(chart.hasFilter(box.datum().key)).toBeTruthy();
var xVal = box.datum().key[0];
var columns = chart.selectAll('.cols.axis text');
var column = columns.filter(function (columnData) {
return columnData === xVal;
});
column.on('click')(column.datum());
assertOnlyThisAxisIsFiltered(chart, 0, xVal);
column.on('click')(column.datum());
});
});
describe('with all cells on that axis already filtered', function () {
it('should remove all filters on that axis', function () {
var xVal = 1;
chart.selectAll('.box-group').each(function (d) {
var box = d3.select(this);
if (d.key[0] === xVal) {
box.select('rect').on('click')(box.datum());
}
});
assertOnlyThisAxisIsFiltered(chart, 0, xVal);
var columns = chart.selectAll('.cols.axis text');
var column = columns.filter(function (columnData) {
return columnData === xVal;
});
column.on('click')(column.datum());
chart.select('.box-group').each(function (d) {
expect(chart.hasFilter(d.key)).toBeFalsy();
});
});
});
});
});
describe('iris filtering', function () {
/* eslint camelcase: 0 */
// 2-chart version of from http://bl.ocks.org/gordonwoodhull/14c623b95993808d69620563508edba6
var irisData, bubbleChart, petalDim, petalGroup;
var fields = {
sl: 'sepal_length',
sw: 'sepal_width',
pl: 'petal_length',
pw: 'petal_width'
};
var keyfuncs = {};
function duo_key (ab1, ab2) {
return function (d) {
return [keyfuncs[ab1](d[fields[ab1]]), keyfuncs[ab2](d[fields[ab2]])];
};
}
beforeEach(function () {
irisData = loadIrisFixture();
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 = {};
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 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
.width(400)
.height(400)
.transitionDuration(0)
.x(d3.scaleLinear()).xAxisPadding(0.5)
.y(d3.scaleLinear()).yAxisPadding(0.5)
.elasticX(true)
.elasticY(true)
.label(dc.utils.constant(''))
.keyAccessor(key_part(0))
.valueAccessor(key_part(1))
.r(d3.scaleLinear().domain([0,20]).range([4,25]))
.radiusValueAccessor(function (kv) { return kv.value.total; })
.colors(d3.scaleOrdinal()
.domain(species.concat('none'))
.range(['#e41a1c','#377eb8','#4daf4a', '#f8f8f8']))
.colorAccessor(function (d) {
return max_species(d) || 'none';
});
}
function initialize_heatmap (heatMap) {
heatMap
.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';
})
.renderTitle(true)
.title(function (d) {
return JSON.stringify(d.value, null, 2);
});
}
var bubbleId = 'bubble-chart';
appendChartID(bubbleId);
bubbleChart = dc.bubbleChart('#' + bubbleId);
var sepalDim = data.dimension(duo_key('sl', 'sw')), sepalGroup = sepalDim.group();
petalDim = data.dimension(duo_key('pl', 'pw')); petalGroup = petalDim.group();
reduce_species(sepalGroup);
reduce_species(petalGroup);
initialize_bubble(bubbleChart.dimension(sepalDim).group(sepalGroup));
initialize_heatmap(chart.dimension(petalDim).group(petalGroup));
bubbleChart.render();
chart.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 testRectFillsBubble12 (chart) {
var rects = chart.selectAll('rect').nodes();
expect(d3.select(rects[0]).attr('fill')).toMatch(/#f8f8f8/i);
expect(d3.select(rects[3]).attr('fill')).toMatch(/#377eb8/i);
expect(d3.select(rects[4]).attr('fill')).toMatch(/#377eb8/i);
expect(d3.select(rects[7]).attr('fill')).toMatch(/#4daf4a/i);
expect(d3.select(rects[8]).attr('fill')).toMatch(/#f8f8f8/i);
expect(d3.select(rects[10]).attr('fill')).toMatch(/#f8f8f8/i);
expect(d3.select(rects[11]).attr('fill')).toMatch(/#f8f8f8/i);
expect(d3.select(rects[12]).attr('fill')).toMatch(/#f8f8f8/i);
}
function testRectTitlesBubble12 (chart) {
var titles = chart.selectAll('g.box-group title').nodes();
expect(JSON.parse(d3.select(titles[0]).text()).total).toBe(0);
expect(JSON.parse(d3.select(titles[2]).text()).total).toBe(0);
expect(JSON.parse(d3.select(titles[3]).text()).total).toBe(2);
expect(JSON.parse(d3.select(titles[4]).text()).total).toBe(3);
expect(JSON.parse(d3.select(titles[5]).text()).total).toBe(0);
expect(JSON.parse(d3.select(titles[7]).text()).total).toBe(1);
expect(JSON.parse(d3.select(titles[9]).text()).total).toBe(0);
expect(JSON.parse(d3.select(titles[10]).text()).total).toBe(0);
expect(JSON.parse(d3.select(titles[12]).text()).total).toBe(0);
}
describe('bubble filtering with straight crossfilter', function () {
beforeEach(function () {
bubbleChart.filter(duo_key('sl', 'sw')({sepal_length: 5.5, sepal_width: 3})).redrawGroup();
});
it('updates rect fills correctly', function () {
testRectFillsBubble12(chart);
});
it('updates rect titles correctly', function () {
testRectTitlesBubble12(chart);
});
});
describe('column filtering with cloned results', function () {
beforeEach(function () {
chart.group(clone_group(petalGroup));
chart.render();
bubbleChart.filter(duo_key('sl', 'sw')({sepal_length: 5.5, sepal_width: 3})).redrawGroup();
});
it('updates rect fills correctly', function () {
testRectFillsBubble12(chart);
});
it('updates rect titles correctly', function () {
testRectTitlesBubble12(chart);
});
});
});
});