Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Alternate strategy for brushing in composite charts #1408

Closed
wants to merge 8 commits into from
2 changes: 2 additions & 0 deletions spec/composite-chart-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ describe('dc.compositeChart', function () {
dateIdSumGroup, dateIdNegativeSumGroup, dateGroup;

beforeEach(function () {
dc.constants.EVENT_DELAY = 0; // so that dc.events.trigger executes immediately

data = crossfilter(loadDateFixture());
dateDimension = data.dimension(function (d) { return d3.utcDay(d.dd); });
dateValueSumGroup = dateDimension.group().reduceSum(function (d) { return d.value; });
Expand Down
2 changes: 1 addition & 1 deletion spec/coordinate-grid-chart-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -741,7 +741,7 @@ describe('dc.coordinateGridChart', function () {
});

it('should update its range chart\'s filter', function () {
expect(chart.rangeChart().filter()).toEqual(chart.filter());
expect(dc.utils.arraysEqual(chart.rangeChart().filter(), chart.filter())).toEqual(true);
});

it('should trigger redraw on its range chart', function () {
Expand Down
2 changes: 1 addition & 1 deletion src/bar-chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ dc.barChart = function (parent, chartGroup) {
bars.classed(dc.constants.SELECTED_CLASS, false);
bars.classed(dc.constants.DESELECTED_CLASS, false);
}
} else if (_chart.brushOn()) {
} else if (_chart.brushOn() || _chart.parentBrushOn()) {
if (!_chart.brushIsEmpty(brushSelection)) {
var start = brushSelection[0];
var end = brushSelection[1];
Expand Down
2 changes: 1 addition & 1 deletion src/box-plot.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ dc.boxPlot = function (parent, chartGroup) {
}
});
} else {
if (!_chart.brushOn()) {
if (!(_chart.brushOn() || _chart.parentBrushOn())) {
return;
}
var start = brushSelection[0];
Expand Down
42 changes: 11 additions & 31 deletions src/composite-chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,42 +60,21 @@ dc.compositeChart = function (parent, chartGroup) {
child.svg(_chart.svg());
child.xUnits(_chart.xUnits());
child.transitionDuration(_chart.transitionDuration(), _chart.transitionDelay());
child.brushOn(_chart.brushOn());
child.parentBrushOn(_chart.brushOn());
child.brushOn(false);
child.renderTitle(_chart.renderTitle());
child.elasticX(_chart.elasticX());
}

return g;
});

_chart._brushing = function () {
// Avoids infinite recursion (mutual recursion between range and focus operations)
// Source Event will be null when brush.move is called programmatically (see below as well).
if (!d3.event.sourceEvent) { return; }

// Ignore event if recursive event - i.e. not directly generated by user action (like mouse/touch etc.)
// In this case we are more worried about this handler causing brush move programmatically which will
// cause this handler to be invoked again with a new d3.event (and current event set as sourceEvent)
// This check avoids recursive calls
if (d3.event.sourceEvent.type && ['start', 'brush', 'end'].indexOf(d3.event.sourceEvent.type) !== -1) {
return;
}

var brushSelection = d3.event.selection;
if (brushSelection) {
brushSelection = brushSelection.map(_chart.x().invert);
}
brushSelection = _chart.extendBrush(brushSelection);

_chart.redrawBrush(brushSelection, false);

var brushIsEmpty = _chart.brushIsEmpty(brushSelection);

_chart.replaceFilter(brushIsEmpty ? null : brushSelection);

_chart.applyBrushSelection = function (rangedFilter) {
_chart.replaceFilter(rangedFilter);
for (var i = 0; i < _children.length; ++i) {
_children[i].replaceFilter(brushIsEmpty ? null : brushSelection);
_children[i].replaceFilter(rangedFilter);
}
_chart.redrawGroup();
};

_chart._prepareYAxis = function () {
Expand Down Expand Up @@ -285,10 +264,11 @@ dc.compositeChart = function (parent, chartGroup) {
};

_chart.fadeDeselectedArea = function (brushSelection) {
for (var i = 0; i < _children.length; ++i) {
var child = _children[i];
child.brush(_chart.brush());
child.fadeDeselectedArea(brushSelection);
if (_chart.brushOn()) {
for (var i = 0; i < _children.length; ++i) {
var child = _children[i];
child.fadeDeselectedArea(brushSelection);
}
}
};

Expand Down
41 changes: 29 additions & 12 deletions src/coordinate-grid-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ dc.coordinateGridMixin = function (_chart) {
var _brush = d3.brushX();
var _gBrush;
var _brushOn = true;
var _parentBrushOn = false;
var _round;

var _renderHorizontalGridLine = false;
Expand Down Expand Up @@ -1087,19 +1088,17 @@ dc.coordinateGridMixin = function (_chart) {

_chart.redrawBrush(brushSelection, false);

if (_chart.brushIsEmpty(brushSelection)) {
dc.events.trigger(function () {
_chart.filter(null);
_chart.redrawGroup();
}, dc.constants.EVENT_DELAY);
} else {
var rangedFilter = dc.filters.RangedFilter(brushSelection[0], brushSelection[1]);
var rangedFilter = _chart.brushIsEmpty(brushSelection) ? null : dc.filters.RangedFilter(brushSelection[0], brushSelection[1]);

dc.events.trigger(function () {
_chart.replaceFilter(rangedFilter);
_chart.redrawGroup();
}, dc.constants.EVENT_DELAY);
}
dc.events.trigger(function () {
_chart.applyBrushSelection(rangedFilter);
}, dc.constants.EVENT_DELAY);
};

// This can be overridden in a derived chart. For example Composite chart overrides it
_chart.applyBrushSelection = function (rangedFilter) {
_chart.replaceFilter(rangedFilter);
_chart.redrawGroup();
};

_chart.setBrushExtents = function (doTransition) {
Expand Down Expand Up @@ -1463,6 +1462,24 @@ dc.coordinateGridMixin = function (_chart) {
return _chart;
};

/**
* This will be internally used by composite chart onto children. Please go not invoke directly.
*
* @method parentBrushOn
* @memberof dc.coordinateGridMixin
* @protected
* @instance
* @param {Boolean} [brushOn=false]
* @returns {Boolean|dc.coordinateGridMixin}
*/
_chart.parentBrushOn = function (brushOn) {
if (!arguments.length) {
return _parentBrushOn;
}
_parentBrushOn = brushOn;
return _chart;
};

// Get the SVG rendered brush
_chart.gBrush = function () {
return _gBrush;
Expand Down
2 changes: 1 addition & 1 deletion src/line-chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ dc.lineChart = function (parent, chartGroup) {
}

function drawDots (chartBody, layers) {
if (_chart.xyTipsOn() === 'always' || (!_chart.brushOn() && _chart.xyTipsOn())) {
if (_chart.xyTipsOn() === 'always' || (!(_chart.brushOn() || _chart.parentBrushOn()) && _chart.xyTipsOn())) {
var tooltipListClass = TOOLTIP_G_CLASS + '-list';
var tooltips = chartBody.select('g.' + tooltipListClass);

Expand Down
2 changes: 1 addition & 1 deletion src/scatter-plot.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ dc.scatterPlot = function (parent, chartGroup) {
symbols.call(renderTitles, _chart.data());

symbols.each(function (d, i) {
_filtered[i] = !_chart.filter() || _chart.filter().isFiltered([d.key[0], d.key[1]]);
_filtered[i] = !_chart.filter() || _chart.filter().isFiltered([_chart.keyAccessor()(d), _chart.valueAccessor()(d)]);
});

dc.transition(symbols, _chart.transitionDuration(), _chart.transitionDelay())
Expand Down
2 changes: 1 addition & 1 deletion src/series-chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ dc.seriesChart = function (parent, chartGroup) {
}, sub.key)
.keyAccessor(_chart.keyAccessor())
.valueAccessor(_chart.valueAccessor())
.brushOn(_chart.brushOn());
.brushOn(false);
});
// this works around the fact compositeChart doesn't really
// have a removal interface
Expand Down
102 changes: 102 additions & 0 deletions web/examples/composite-brushing.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>dc.js - Composite Chart Brushing Example</title>
<meta charset="UTF-8">
<link rel="stylesheet" type="text/css" href="../css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="../css/dc.css"/>
</head>
<body>
<div class="container">
<script type="text/javascript" src="header.js"></script>

<p>Usually sub charts of a composite chart share the dimension of the parent.
However sometimes, especially when scatter plots are composed, the sub charts may
used different dimensions. This example uses two scatter plots both using array dimensions.
Typically scatter plots use two dimensional brushing (see <a href="scatter-brushing.html">scatter brushing</a>),
however, composite charts only support one dimensional (along x axis) brushing.</p>
<p>Try brushing on the chart and see data getting filtered on the right.</p>
<p>For the curious, you will notice that unlike other charts brushing removes points outside range of the brush
instead of just fading them.
The three dimensions used by different charts are actually related.
When brush applies filters on all these three dimensions, all data outside the brush range actually gets removed
by crossfilter. There is no easy way to currently avoid that.</p>
<div id="test1"></div>
<div id="test2"></div>

<script type="text/javascript" src="../js/d3.js"></script>
<script type="text/javascript" src="../js/crossfilter.js"></script>
<script type="text/javascript" src="../js/dc.js"></script>
<script type="text/javascript">

var chart = dc.compositeChart("#test1");
var dataTable = dc.dataTable("#test2");

var data = [
{x: 1, y: 1, z: 3},
{x: 5, y: 2, z: 11},
{x: 13, y: 13, z: 14},
{x: 5, y: 3, z: 20},
{x: 12, y: 12, z: 10},
{x: 3, y: 6, z: 8},
{x: 15, y: 2, z: 9},
{x: 8, y: 6, z: 14},
{x: 1, y: 4, z: 9},
{x: 8, y: 8, z: 12}
];

var ndx = crossfilter(data),
dimXY = ndx.dimension(function (d) {
return [d.x, d.y];
}),
groupXY = dimXY.group(),
dimXZ = ndx.dimension(function (d) {
return [d.x, d.z];
}),
groupXZ = dimXZ.group(),
dimX = ndx.dimension(function (d) {
return d.x;
}),
groupX = dimX.group();

chart.width(768)
.height(480)
.x(d3.scaleLinear().domain([0, 16]))
.yAxisLabel("y")
.xAxisLabel("x")
.clipPadding(10)
.dimension(dimXY)
.group(groupXY)
.compose([
dc.scatterPlot(chart)
.symbol(d3.symbolStar)
.ordinalColors(['red'])
.symbolSize(8)
.excludedOpacity(0.5),
dc.scatterPlot(chart)
.dimension(dimXZ)
.group(groupXZ)
.symbol(d3.symbolSquare)
.ordinalColors(['green'])
.symbolSize(8)
.excludedOpacity(0.5),
dc.barChart(chart)
.dimension(dimX)
.group(groupX)
.valueAccessor(function (d) { return d.value * 3; })
.ordinalColors(['#e7e4ff'])
.barPadding(8)
]);

dataTable
.dimension(dimX)
.group(function (d) { return d.x; })
.columns(['x', 'y', 'z']);

dc.renderAll();

</script>

</div>
</body>
</html>
Loading