UNPKG

dc

Version:

A multi-dimensional charting library built to work natively with crossfilter and rendered using d3.js

459 lines (385 loc) 18.1 kB
/* global appendChartID, comparePaths, loadDateFixture, makeDate */ describe('dc.scatterPlot', function () { var id, chart; var data, group, dimension; beforeEach(function () { data = crossfilter(loadDateFixture()); dimension = data.dimension(function (d) { return [+d.value, +d.nvalue]; }); group = dimension.group(); id = 'scatter-plot'; appendChartID(id); chart = dc.scatterPlot('#' + id); chart.dimension(dimension) .group(group) .width(500).height(180) .x(d3.scale.linear().domain([0, 70])) .symbolSize(10) .nonemptyOpacity(0.9) .excludedSize(2) .excludedColor('#ccc') .excludedOpacity(0.25) .emptySize(4) .emptyOpacity(0.5) .emptyColor('#DFFF00') .transitionDuration(0); }); describe('rendering the scatter plot', function () { beforeEach(function () { chart.render(); }); it('should create an svg', function () { expect(chart.svg().empty()).toBeFalsy(); }); it('should create the correct number of symbols', function () { expect(chart.group().all().length).toBe(chart.selectAll('path.symbol').size()); }); it('should correctly place the symbols', function () { expect(nthSymbol(4).attr('transform')).toMatchTranslate(264, 131); expect(nthSymbol(5).attr('transform')).toMatchTranslate(264, 75); expect(nthSymbol(8).attr('transform')).toMatchTranslate(396, 131); }); it('should generate a default color fill for symbols', function () { expect(nthSymbol(4).attr('fill')).toMatch(/#1f77b4/i); expect(nthSymbol(5).attr('fill')).toMatch(/#1f77b4/i); expect(nthSymbol(8).attr('fill')).toMatch(/#1f77b4/i); }); it('should generate the correct titles', function () { var titles = chart.selectAll('path.symbol title'); var expected = ['22,-2: 1','22,10: 1','33,1: 2','44,-3: 1','44,-4: 1', '44,2: 1','55,-3: 1','55,-5: 1','66,-4: 1']; expect(titles.size()).toBe(expected.length); titles.each(function (d) { expect(this.textContent).toBe(expected.shift()); }); }); describe('with a custom color', function () { beforeEach(function () { chart.colors('red').render(); }); it('should color the symbols to the provided color', function () { expect(nthSymbol(4).attr('fill')).toBe('red'); expect(nthSymbol(5).attr('fill')).toBe('red'); expect(nthSymbol(8).attr('fill')).toBe('red'); }); }); function fishSymbol () { var size; var points = [[2, 0], [1, -1], [-1, 1], [-1, -1], [1, 1]]; function symbol (d, i) { // native size is 3 square pixels, so to get size N, multiply by sqrt(N)/3 var m = size.call(this, d, i); m = Math.sqrt(m) / 3; var path = d3.svg.line() .x(function (d) { return d[0] * m; }) .y(function (d) { return d[1] * m; }); return path(points) + 'Z'; } symbol.type = function () { if (arguments.length) { throw new Error('no, you must have fish'); } return 'fish'; }; symbol.size = function (_) { if (!arguments.length) { return size; } size = d3.functor(_); return symbol; }; return symbol; } describe('with a fish symbol', function () { beforeEach(function () { chart.customSymbol(fishSymbol().size(chart.symbolSize())) .render(); }); it('should draw fishes', function () { expect(symbolsMatching(matchSymbol(fishSymbol(), chart.symbolSize())).length).toBe(9); }); }); describe('with title rendering disabled', function () { beforeEach(function () { chart.renderTitle(false).render(); }); it('should not generate title elements', function () { expect(chart.selectAll('rect.bar title').empty()).toBeTruthy(); }); }); function nthSymbol (i) { return d3.select(chart.selectAll('path.symbol')[0][i]); } describe('filtering the chart', function () { var otherDimension; beforeEach(function () { otherDimension = data.dimension(function (d) { return [+d.value, +d.nvalue]; }); chart.filterAll(); chart.filter([[22, -3], [44, 2]]); }); it('should filter dimensions based on the same data', function () { expect(otherDimension.top(Infinity).length).toBe(3); }); describe('when filtering with null', function () { beforeEach(function () { chart.filter(null); }); it('should remove all filtering from the dimensions based on the same data', function () { expect(otherDimension.top(Infinity).length).toBe(10); }); }); }); function filteringAnotherDimension () { describe('filtering another dimension', function () { var otherDimension; beforeEach(function () { otherDimension = data.dimension(function (d) { return [+d.value, +d.nvalue]; }); var ff = dc.filters.RangedTwoDimensionalFilter([[22, -3], [44, 2]]).isFiltered; otherDimension.filterFunction(ff); chart.redraw(); }); it('should show the included points', function () { var shownPoints = symbolsOfRadius(10); // test symbolSize expect(shownPoints.length).toBe(2); expect(shownPoints[0].key).toEqual([22, -2]); expect(shownPoints[1].key).toEqual([33, 1]); }); it('should hide the excluded points', function () { var emptyPoints = symbolsOfRadius(4); // test emptySize expect(emptyPoints.length).toBe(7); }); it('should use emptyOpacity for excluded points', function () { var translucentPoints = symbolsMatching(function () { return +d3.select(this).attr('opacity') === 0.5; // emptyOpacity }); expect(translucentPoints.length).toBe(7); }); it('should use emptyColor for excluded points', function () { var chartreusePoints = symbolsMatching(function () { // don't try this at home return /#DFFF00/i.test(d3.select(this).attr('fill')); // emptyColor }); expect(chartreusePoints.length).toBe(7); }); it('should update the titles', function () { var titles = chart.selectAll('path.symbol title'); var expected = ['22,-2: 1','22,10: 0','33,1: 2','44,-3: 0','44,-4: 0', '44,2: 0','55,-3: 0','55,-5: 0','66,-4: 0']; expect(titles.size()).toBe(expected.length); titles.each(function (d) { expect(this.textContent).toBe(expected.shift()); }); }); }); } filteringAnotherDimension(); function cloneGroup (group) { return { all: function () { return group.all().map(function (kv) { return { key: kv.key.slice(0), value: kv.value }; }); } }; } describe('with cloned data', function () { beforeEach(function () { chart.group(cloneGroup(group)) .render(); }); filteringAnotherDimension(); }); describe('brushing', function () { var otherDimension; beforeEach(function () { otherDimension = data.dimension(function (d) { return [+d.value, +d.nvalue]; }); chart.brush().extent([[22, -3], [44, 2]]); chart.brush().on('brush')(); chart.redraw(); }); it('should filter dimensions based on the same data', function () { jasmine.clock().tick(100); expect(otherDimension.top(Infinity).length).toBe(3); }); it('should set the height of the brush to the height implied by the extent', function () { expect(chart.select('g.brush rect.extent').attr('height')).toBe('46'); }); it('should not add handles to the brush', function () { expect(chart.select('.resize path').empty()).toBeTruthy(); }); describe('excluded points', function () { var selectedPoints; beforeEach(function () { jasmine.clock().tick(100); }); var isOpaque = function () { return +d3.select(this).attr('opacity') === 0.9; // test nonemptyOpacity }, isTranslucent = function () { return +d3.select(this).attr('opacity') === 0.25; // test excludedOpacity }, isBlue = function () { return d3.select(this).attr('fill') === '#1f77b4'; }, isGrey = function () { return d3.select(this).attr('fill') === '#ccc'; // test excludedColor }; it('should not shrink the included points', function () { selectedPoints = symbolsOfRadius(chart.symbolSize()); expect(selectedPoints.length).toBe(2); expect(selectedPoints[0].key).toEqual([22, -2]); expect(selectedPoints[1].key).toEqual([33, 1]); }); it('should shrink the excluded points', function () { selectedPoints = symbolsOfRadius(2); // test excludedSize expect(selectedPoints.length).toBe(7); expect(selectedPoints[0].key).toEqual([22, 10]); expect(selectedPoints[1].key).toEqual([44, -3]); }); it('should keep the included points opaque', function () { selectedPoints = symbolsMatching(isOpaque); expect(selectedPoints.length).toBe(2); expect(selectedPoints[0].key).toEqual([22, -2]); expect(selectedPoints[1].key).toEqual([33, 1]); }); it('should make the excluded points translucent', function () { selectedPoints = symbolsMatching(isTranslucent); expect(selectedPoints.length).toBe(7); expect(selectedPoints[0].key).toEqual([22, 10]); expect(selectedPoints[1].key).toEqual([44, -3]); }); it('should keep the included points blue', function () { selectedPoints = symbolsMatching(isBlue); expect(selectedPoints.length).toBe(2); expect(selectedPoints[0].key).toEqual([22, -2]); expect(selectedPoints[1].key).toEqual([33, 1]); }); it('should make the excluded points grey', function () { selectedPoints = symbolsMatching(isGrey); expect(selectedPoints.length).toBe(7); expect(selectedPoints[0].key).toEqual([22, 10]); expect(selectedPoints[1].key).toEqual([44, -3]); }); it('should restore sizes, colors, and opacity when the brush is empty', function () { chart.brush().extent([[22, 2], [22, -3]]); chart.brush().on('brush')(); jasmine.clock().tick(100); selectedPoints = symbolsOfRadius(chart.symbolSize()); expect(selectedPoints.length).toBe(9); selectedPoints = symbolsMatching(isBlue); expect(selectedPoints.length).toBe(9); selectedPoints = symbolsMatching(isOpaque); expect(selectedPoints.length).toBe(9); chart.redraw(); selectedPoints = symbolsOfRadius(chart.symbolSize()); expect(selectedPoints.length).toBe(9); selectedPoints = symbolsMatching(isBlue); expect(selectedPoints.length).toBe(9); selectedPoints = symbolsMatching(isOpaque); expect(selectedPoints.length).toBe(9); }); }); }); }); function matchSymbolSize (r) { return function () { var symbol = d3.select(this); var size = Math.pow(r, 2); var path = d3.svg.symbol().size(size)(); var result = comparePaths(symbol.attr('d'), path); return result.pass; }; } function matchSymbol (s, r) { return function () { var symbol = d3.select(this); var size = Math.pow(r, 2); var path = s.size(size)(); var result = comparePaths(symbol.attr('d'), path); return result.pass; }; } function symbolsMatching (pred) { function getData (symbols) { return symbols[0].map(function (symbol) { return d3.select(symbol).datum(); }); } return getData(chart.selectAll('path.symbol').filter(pred)); } function symbolsOfRadius (r) { return symbolsMatching(matchSymbolSize(r)); } describe('legends', function () { var compositeChart, id; var subChart1, subChart2; var firstItem; beforeEach(function () { id = 'scatter-plot-composite'; appendChartID(id); compositeChart = dc.compositeChart('#' + id); compositeChart .dimension(dimension) .x(d3.time.scale.utc().domain([makeDate(2012, 0, 1), makeDate(2012, 11, 31)])) .transitionDuration(0) .legend(dc.legend()) .compose([ subChart1 = dc.scatterPlot(compositeChart).colors('red').group(group, 'Scatter 1'), subChart2 = dc.scatterPlot(compositeChart).colors('blue').group(group, 'Scatter 2') ]).render(); firstItem = compositeChart.select('g.dc-legend g.dc-legend-item'); }); it('should provide a composite chart with corresponding legend data', function () { expect(compositeChart.legendables()).toEqual([ {chart: subChart1, name: 'Scatter 1', color: 'red'}, {chart: subChart2, name: 'Scatter 2', color: 'blue'} ]); }); describe('hovering', function () { beforeEach(function () { firstItem.on('mouseover')(firstItem.datum()); }); describe('when a legend item is hovered over', function () { it('should highlight corresponding plot', function () { nthChart(0).expectPlotSymbolsToHaveSize(subChart1.highlightedSize()); }); it('should fade out non-corresponding lines and areas', function () { nthChart(1).expectPlotSymbolsToHaveClass('fadeout'); }); }); describe('when a legend item is hovered out', function () { beforeEach(function () { firstItem.on('mouseout')(firstItem.datum()); }); it('should remove highlighting from corresponding lines and areas', function () { nthChart(0).expectPlotSymbolsToHaveSize(subChart1.symbolSize()); }); it('should fade in non-corresponding lines and areas', function () { nthChart(1).expectPlotSymbolsNotToHaveClass('fadeout'); }); }); }); function nthChart (n) { var subChart = d3.select(compositeChart.selectAll('g.sub')[0][n]); subChart.expectPlotSymbolsToHaveClass = function (className) { subChart.selectAll('path.symbol').each(function () { expect(d3.select(this).classed(className)).toBeTruthy(); }); }; subChart.expectPlotSymbolsToHaveSize = function (size) { var match = matchSymbolSize(size); subChart.selectAll('path.symbol').each(function () { expect(match.apply(this)).toBeTruthy(); }); }; subChart.expectPlotSymbolsNotToHaveClass = function (className) { subChart.selectAll('path.symbol').each(function () { expect(d3.select(this).classed(className)).toBeFalsy(); }); }; return subChart; } }); });