dc
Version:
A multi-dimensional charting library built to work natively with crossfilter and rendered using d3.js
253 lines (224 loc) • 8.77 kB
JavaScript
/**
* A box plot is a chart that depicts numerical data via their quartile ranges.
*
* Examples:
* - {@link http://dc-js.github.io/dc.js/examples/box-plot-time.html Box plot time example}
* - {@link http://dc-js.github.io/dc.js/examples/box-plot.html Box plot example}
* @class boxPlot
* @memberof dc
* @mixes dc.coordinateGridMixin
* @example
* // create a box plot under #chart-container1 element using the default global chart group
* var boxPlot1 = dc.boxPlot('#chart-container1');
* // create a box plot under #chart-container2 element using chart group A
* var boxPlot2 = dc.boxPlot('#chart-container2', 'chartGroupA');
* @param {String|node|d3.selection} parent - Any valid
* {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Selections.md#selecting-elements d3 single selector} specifying
* a dom block element such as a div; or a dom element or d3 selection.
* @param {String} [chartGroup] - The name of the chart group this chart instance should be placed in.
* Interaction with a chart will only trigger events and redraws within the chart's group.
* @returns {dc.boxPlot}
*/
dc.boxPlot = function (parent, chartGroup) {
var _chart = dc.coordinateGridMixin({});
// Returns a function to compute the interquartile range.
function DEFAULT_WHISKERS_IQR (k) {
return function (d) {
var q1 = d.quartiles[0],
q3 = d.quartiles[2],
iqr = (q3 - q1) * k,
i = -1,
j = d.length;
do { ++i; } while (d[i] < q1 - iqr);
do { --j; } while (d[j] > q3 + iqr);
return [i, j];
};
}
var _whiskerIqrFactor = 1.5;
var _whiskersIqr = DEFAULT_WHISKERS_IQR;
var _whiskers = _whiskersIqr(_whiskerIqrFactor);
var _box = d3.box();
var _tickFormat = null;
var _boxWidth = function (innerChartWidth, xUnits) {
if (_chart.isOrdinal()) {
return _chart.x().rangeBand();
} else {
return innerChartWidth / (1 + _chart.boxPadding()) / xUnits;
}
};
// default padding to handle min/max whisker text
_chart.yAxisPadding(12);
// default to ordinal
_chart.x(d3.scale.ordinal());
_chart.xUnits(dc.units.ordinal);
// valueAccessor should return an array of values that can be coerced into numbers
// or if data is overloaded for a static array of arrays, it should be `Number`.
// Empty arrays are not included.
_chart.data(function (group) {
return group.all().map(function (d) {
d.map = function (accessor) { return accessor.call(d, d); };
return d;
}).filter(function (d) {
var values = _chart.valueAccessor()(d);
return values.length !== 0;
});
});
/**
* Get or set the spacing between boxes as a fraction of box size. Valid values are within 0-1.
* See the {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Ordinal-Scales.md#ordinal_rangeBands d3 docs}
* for a visual description of how the padding is applied.
* @method boxPadding
* @memberof dc.boxPlot
* @instance
* @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Ordinal-Scales.md#ordinal_rangeBands d3.scale.ordinal.rangeBands}
* @param {Number} [padding=0.8]
* @returns {Number|dc.boxPlot}
*/
_chart.boxPadding = _chart._rangeBandPadding;
_chart.boxPadding(0.8);
/**
* Get or set the outer padding on an ordinal box chart. This setting has no effect on non-ordinal charts
* or on charts with a custom {@link dc.boxPlot#boxWidth .boxWidth}. Will pad the width by
* `padding * barWidth` on each side of the chart.
* @method outerPadding
* @memberof dc.boxPlot
* @instance
* @param {Number} [padding=0.5]
* @returns {Number|dc.boxPlot}
*/
_chart.outerPadding = _chart._outerRangeBandPadding;
_chart.outerPadding(0.5);
/**
* Get or set the numerical width of the boxplot box. The width may also be a function taking as
* parameters the chart width excluding the right and left margins, as well as the number of x
* units.
* @example
* // Using numerical parameter
* chart.boxWidth(10);
* // Using function
* chart.boxWidth((innerChartWidth, xUnits) { ... });
* @method boxWidth
* @memberof dc.boxPlot
* @instance
* @param {Number|Function} [boxWidth=0.5]
* @returns {Number|Function|dc.boxPlot}
*/
_chart.boxWidth = function (boxWidth) {
if (!arguments.length) {
return _boxWidth;
}
_boxWidth = d3.functor(boxWidth);
return _chart;
};
var boxTransform = function (d, i) {
var xOffset = _chart.x()(_chart.keyAccessor()(d, i));
return 'translate(' + xOffset + ', 0)';
};
_chart._preprocessData = function () {
if (_chart.elasticX()) {
_chart.x().domain([]);
}
};
_chart.plotData = function () {
var _calculatedBoxWidth = _boxWidth(_chart.effectiveWidth(), _chart.xUnitCount());
_box.whiskers(_whiskers)
.width(_calculatedBoxWidth)
.height(_chart.effectiveHeight())
.value(_chart.valueAccessor())
.domain(_chart.y().domain())
.duration(_chart.transitionDuration())
.tickFormat(_tickFormat);
var boxesG = _chart.chartBodyG().selectAll('g.box').data(_chart.data(), _chart.keyAccessor());
renderBoxes(boxesG);
updateBoxes(boxesG);
removeBoxes(boxesG);
_chart.fadeDeselectedArea();
};
function renderBoxes (boxesG) {
var boxesGEnter = boxesG.enter().append('g');
boxesGEnter
.attr('class', 'box')
.attr('transform', boxTransform)
.call(_box)
.on('click', function (d) {
_chart.filter(_chart.keyAccessor()(d));
_chart.redrawGroup();
});
}
function updateBoxes (boxesG) {
dc.transition(boxesG, _chart.transitionDuration(), _chart.transitionDelay())
.attr('transform', boxTransform)
.call(_box)
.each(function () {
d3.select(this).select('rect.box').attr('fill', _chart.getColor);
});
}
function removeBoxes (boxesG) {
boxesG.exit().remove().call(_box);
}
_chart.fadeDeselectedArea = function () {
if (_chart.hasFilter()) {
if (_chart.isOrdinal()) {
_chart.g().selectAll('g.box').each(function (d) {
if (_chart.isSelectedNode(d)) {
_chart.highlightSelected(this);
} else {
_chart.fadeDeselected(this);
}
});
} else {
var extent = _chart.brush().extent();
var start = extent[0];
var end = extent[1];
var keyAccessor = _chart.keyAccessor();
_chart.g().selectAll('g.box').each(function (d) {
var key = keyAccessor(d);
if (key < start || key >= end) {
_chart.fadeDeselected(this);
} else {
_chart.highlightSelected(this);
}
});
}
} else {
_chart.g().selectAll('g.box').each(function () {
_chart.resetHighlight(this);
});
}
};
_chart.isSelectedNode = function (d) {
return _chart.hasFilter(_chart.keyAccessor()(d));
};
_chart.yAxisMin = function () {
var min = d3.min(_chart.data(), function (e) {
return d3.min(_chart.valueAccessor()(e));
});
return dc.utils.subtract(min, _chart.yAxisPadding());
};
_chart.yAxisMax = function () {
var max = d3.max(_chart.data(), function (e) {
return d3.max(_chart.valueAccessor()(e));
});
return dc.utils.add(max, _chart.yAxisPadding());
};
/**
* Set the numerical format of the boxplot median, whiskers and quartile labels. Defaults to
* integer formatting.
* @example
* // format ticks to 2 decimal places
* chart.tickFormat(d3.format('.2f'));
* @method tickFormat
* @memberof dc.boxPlot
* @instance
* @param {Function} [tickFormat]
* @returns {Number|Function|dc.boxPlot}
*/
_chart.tickFormat = function (tickFormat) {
if (!arguments.length) {
return _tickFormat;
}
_tickFormat = tickFormat;
return _chart;
};
return _chart.anchor(parent, chartGroup);
};