diff --git a/spec/scatter-plot-spec.js b/spec/scatter-plot-spec.js index 8a4b9f366..869b680c8 100644 --- a/spec/scatter-plot-spec.js +++ b/spec/scatter-plot-spec.js @@ -16,6 +16,8 @@ describe('dc.scatterPlot', function () { .group(group) .width(500).height(180) .x(d3.scale.linear().domain([0, 70])) + .excludedColor('#ccc') + .excludedOpacity(0.25) .transitionDuration(0); }); @@ -132,45 +134,115 @@ describe('dc.scatterPlot', function () { expect(chart.select('.resize path').empty()).toBeTruthy(); }); - describe('highlighting', function () { + describe('excluded points', function () { var selectedPoints; beforeEach(function () { - selectedPoints = symbolsOfRadius(chart.highlightedSize()); + jasmine.clock().tick(100); }); - it('should highlight the selected points', function () { + var isOpaque = function () { + return +d3.select(this).attr('opacity') === 1; + }, isTranslucent = function () { + return +d3.select(this).attr('opacity') === 0.25; + }, isBlue = function () { + return d3.select(this).attr('fill') === '#1f77b4'; + }, isGrey = function () { + return d3.select(this).attr('fill') === '#ccc'; + }; + + 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 remove highlighting when the brush is removed from the selected points', function () { + it('should shrink the excluded points', function () { + selectedPoints = symbolsOfRadius(chart.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')(); - selectedPoints = symbolsOfRadius(chart.highlightedSize()); - expect(selectedPoints.length).toBe(0); + 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.highlightedSize()); - expect(selectedPoints.length).toBe(0); + + 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 symbolsOfRadius (r) { - function getData (symbols) { - return symbols[0].map(function (symbol) { - return d3.select(symbol).datum(); - }); - } - return getData(chart.selectAll('path.symbol').filter(function () { + 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 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 () { diff --git a/src/scatter-plot.js b/src/scatter-plot.js index 95ca7625a..ec26d5bf1 100644 --- a/src/scatter-plot.js +++ b/src/scatter-plot.js @@ -34,20 +34,24 @@ dc.scatterPlot = function (parent, chartGroup) { var _locator = function (d) { return 'translate(' + _chart.x()(_chart.keyAccessor()(d)) + ',' + - _chart.y()(_chart.valueAccessor()(d)) + ')'; + _chart.y()(_chart.valueAccessor()(d)) + ')'; }; - var _symbolSize = 3; - var _highlightedSize = 5; - var _hiddenSize = 0; + var _highlightedSize = 7; + var _symbolSize = 5; + var _excludedSize = 3; + var _excludedColor = null; + var _excludedOpacity = 1.0; + var _emptySize = 0; + var _filtered = []; - _symbol.size(function (d) { + _symbol.size(function (d, i) { if (!_existenceAccessor(d)) { - return _hiddenSize; - } else if (this.filtered) { - return Math.pow(_highlightedSize, 2); - } else { + return _emptySize; + } else if (_filtered[i]) { return Math.pow(_symbolSize, 2); + } else { + return Math.pow(_excludedSize, 2); } }); @@ -61,19 +65,30 @@ dc.scatterPlot = function (parent, chartGroup) { _chart.plotData = function () { var symbols = _chart.chartBodyG().selectAll('path.symbol') - .data(_chart.data()); + .data(_chart.data()); symbols .enter() - .append('path') + .append('path') .attr('class', 'symbol') .attr('opacity', 0) .attr('fill', _chart.getColor) .attr('transform', _locator); + symbols.each(function (d, i) { + _filtered[i] = !_chart.filter() || _chart.filter().isFiltered(d.key); + }); + dc.transition(symbols, _chart.transitionDuration()) - .attr('opacity', function (d) { return _existenceAccessor(d) ? 1 : 0; }) - .attr('fill', _chart.getColor) + .attr('opacity', function (d, i) { + return !_existenceAccessor(d) ? 0 : + _filtered[i] ? 1 : _chart.excludedOpacity(); + }) + .attr('fill', function (d, i) { + return _chart.excludedColor() && !_filtered[i] ? + _chart.excludedColor() : + _chart.getColor(d); + }) .attr('transform', _locator) .attr('d', _symbol); @@ -85,13 +100,13 @@ dc.scatterPlot = function (parent, chartGroup) { * Get or set the existence accessor. If a point exists, it is drawn with * {@link #dc.scatterPlot+symbolSize symbolSize} radius and * opacity 1; if it does not exist, it is drawn with - * {@link #dc.scatterPlot+hiddenSize hiddenSize} radius and opacity 0. By default, + * {@link #dc.scatterPlot+emptySize emptySize} radius and opacity 0. By default, * the existence accessor checks if the reduced value is truthy. * @method existenceAccessor * @memberof dc.scatterPlot * @instance * @see {@link #dc.scatterPlot+symbolSize symbolSize} - * @see {@link #dc.scatterPlot+hiddenSize hiddenSize} + * @see {@link #dc.scatterPlot+emptySize emptySize} * @example * // default accessor * chart.existenceAccessor(function (d) { return d.value; }); @@ -167,21 +182,77 @@ dc.scatterPlot = function (parent, chartGroup) { return _chart; }; + /** + * Set or get size for symbols excluded from this chart's filter. If null, no + * special size is applied for symbols based on their filter status + * @method excludedSize + * @memberof dc.scatterPlot + * @instance + * @see {@link https://github.com/mbostock/d3/wiki/SVG-Shapes#symbol_size d3.svg.symbol().size()} + * @param {Number} [excludedSize=null] + * @return {Number} + * @return {dc.scatterPlot} + */ + _chart.excludedSize = function (excludedSize) { + if (!arguments.length) { + return _excludedSize; + } + _excludedSize = excludedSize; + return _chart; + }; + + /** + * Set or get color for symbols excluded from this chart's filter. If null, no + * special color is applied for symbols based on their filter status + * @method excludedColor + * @memberof dc.scatterPlot + * @instance + * @see {@link https://github.com/mbostock/d3/wiki/SVG-Shapes#symbol_size d3.svg.symbol().size()} + * @param {Number} [excludedColor=null] + * @return {Number} + * @return {dc.scatterPlot} + */ + _chart.excludedColor = function (excludedColor) { + if (!arguments.length) { + return _excludedColor; + } + _excludedColor = excludedColor; + return _chart; + }; + + /** + * Set or get opacity for symbols excluded from this chart's filter. + * @method excludedOpacity + * @memberof dc.scatterPlot + * @instance + * @see {@link https://github.com/mbostock/d3/wiki/SVG-Shapes#symbol_size d3.svg.symbol().size()} + * @param {Number} [excludedOpacity=1.0] + * @return {Number} + * @return {dc.scatterPlot} + */ + _chart.excludedOpacity = function (excludedOpacity) { + if (!arguments.length) { + return _excludedOpacity; + } + _excludedOpacity = excludedOpacity; + return _chart; + }; + /** * Set or get radius for symbols when the group is empty. - * @method hiddenSize + * @method emptySize * @memberof dc.scatterPlot * @instance * @see {@link https://github.com/mbostock/d3/wiki/SVG-Shapes#symbol_size d3.svg.symbol().size()} - * @param {Number} [hiddenSize=0] + * @param {Number} [emptySize=0] * @return {Number} * @return {dc.scatterPlot} */ - _chart.hiddenSize = function (hiddenSize) { + _chart.hiddenSize = _chart.emptySize = function (emptySize) { if (!arguments.length) { - return _hiddenSize; + return _emptySize; } - _hiddenSize = hiddenSize; + _emptySize = emptySize; return _chart; }; @@ -237,14 +308,6 @@ dc.scatterPlot = function (parent, chartGroup) { return _chart.brush().empty() || !extent || extent[0][0] >= extent[1][0] || extent[0][1] >= extent[1][1]; }; - function resizeFiltered (filter) { - var symbols = _chart.selectAll('.chart-body path.symbol').each(function (d) { - this.filtered = filter && filter.isFiltered(d.key); - }); - - dc.transition(symbols, _chart.transitionDuration()).attr('d', _symbol); - } - _chart._brushing = function () { var extent = _chart.extendBrush(); @@ -256,8 +319,6 @@ dc.scatterPlot = function (parent, chartGroup) { _chart.redrawGroup(); }); - resizeFiltered(false); - } else { var ranged2DFilter = dc.filters.RangedTwoDimensionalFilter(extent); dc.events.trigger(function () { @@ -266,7 +327,6 @@ dc.scatterPlot = function (parent, chartGroup) { _chart.redrawGroup(); }, dc.constants.EVENT_DELAY); - resizeFiltered(ranged2DFilter); } }; diff --git a/web/examples/scatter-brushing.html b/web/examples/scatter-brushing.html index b2531ca16..9780096c3 100644 --- a/web/examples/scatter-brushing.html +++ b/web/examples/scatter-brushing.html @@ -52,9 +52,9 @@ .x(d3.scale.linear().domain([0, 20])) .yAxisLabel("y") .xAxisLabel("x") - .symbolSize(8) .clipPadding(10) .dimension(dim1) + .excludedOpacity(0.5) .group(group1); chart2.width(300) @@ -62,9 +62,9 @@ .x(d3.scale.linear().domain([0, 20])) .yAxisLabel("z") .xAxisLabel("y") - .symbolSize(8) .clipPadding(10) .dimension(dim2) + .excludedColor('#ddd') .group(group2); dc.renderAll();