UNPKG

dc

Version:

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

538 lines (464 loc) 18 kB
/** * Composite charts are a special kind of chart that render multiple charts on the same Coordinate * Grid. You can overlay (compose) different bar/line/area charts in a single composite chart to * achieve some quite flexible charting effects. * @class compositeChart * @memberof dc * @mixes dc.coordinateGridMixin * @example * // create a composite chart under #chart-container1 element using the default global chart group * var compositeChart1 = dc.compositeChart('#chart-container1'); * // create a composite chart under #chart-container2 element using chart group A * var compositeChart2 = dc.compositeChart('#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.compositeChart} */ dc.compositeChart = function (parent, chartGroup) { var SUB_CHART_CLASS = 'sub'; var DEFAULT_RIGHT_Y_AXIS_LABEL_PADDING = 12; var _chart = dc.coordinateGridMixin({}); var _children = []; var _childOptions = {}; var _shareColors = false, _shareTitle = true, _alignYAxes = false; var _rightYAxis = d3.svg.axis(), _rightYAxisLabel = 0, _rightYAxisLabelPadding = DEFAULT_RIGHT_Y_AXIS_LABEL_PADDING, _rightY, _rightAxisGridLines = false; _chart._mandatoryAttributes([]); _chart.transitionDuration(500); _chart.transitionDelay(0); dc.override(_chart, '_generateG', function () { var g = this.__generateG(); for (var i = 0; i < _children.length; ++i) { var child = _children[i]; generateChildG(child, i); if (!child.dimension()) { child.dimension(_chart.dimension()); } if (!child.group()) { child.group(_chart.group()); } child.chartGroup(_chart.chartGroup()); child.svg(_chart.svg()); child.xUnits(_chart.xUnits()); child.transitionDuration(_chart.transitionDuration(), _chart.transitionDelay()); child.brushOn(_chart.brushOn()); child.renderTitle(_chart.renderTitle()); child.elasticX(_chart.elasticX()); } return g; }); _chart._brushing = function () { var extent = _chart.extendBrush(); var brushIsEmpty = _chart.brushIsEmpty(extent); for (var i = 0; i < _children.length; ++i) { _children[i].replaceFilter(brushIsEmpty ? null : extent); } }; _chart._prepareYAxis = function () { var left = (leftYAxisChildren().length !== 0); var right = (rightYAxisChildren().length !== 0); var ranges = calculateYAxisRanges(left, right); if (left) { prepareLeftYAxis(ranges); } if (right) { prepareRightYAxis(ranges); } if (leftYAxisChildren().length > 0 && !_rightAxisGridLines) { _chart._renderHorizontalGridLinesForAxis(_chart.g(), _chart.y(), _chart.yAxis()); } else if (rightYAxisChildren().length > 0) { _chart._renderHorizontalGridLinesForAxis(_chart.g(), _rightY, _rightYAxis); } }; _chart.renderYAxis = function () { if (leftYAxisChildren().length !== 0) { _chart.renderYAxisAt('y', _chart.yAxis(), _chart.margins().left); _chart.renderYAxisLabel('y', _chart.yAxisLabel(), -90); } if (rightYAxisChildren().length !== 0) { _chart.renderYAxisAt('yr', _chart.rightYAxis(), _chart.width() - _chart.margins().right); _chart.renderYAxisLabel('yr', _chart.rightYAxisLabel(), 90, _chart.width() - _rightYAxisLabelPadding); } }; function calculateYAxisRanges (left, right) { var lyAxisMin, lyAxisMax, ryAxisMin, ryAxisMax; var ranges; if (left) { lyAxisMin = yAxisMin(); lyAxisMax = yAxisMax(); } if (right) { ryAxisMin = rightYAxisMin(); ryAxisMax = rightYAxisMax(); } if (_chart.alignYAxes() && left && right) { ranges = alignYAxisRanges(lyAxisMin, lyAxisMax, ryAxisMin, ryAxisMax); } return ranges || { lyAxisMin: lyAxisMin, lyAxisMax: lyAxisMax, ryAxisMin: ryAxisMin, ryAxisMax: ryAxisMax }; } function alignYAxisRanges (lyAxisMin, lyAxisMax, ryAxisMin, ryAxisMax) { // since the two series will share a zero, each Y is just a multiple // of the other. and the ratio should be the ratio of the ranges of the // input data, so that they come out the same height. so we just min/max // note: both ranges already include zero due to the stack mixin (#667) // if #667 changes, we can reconsider whether we want data height or // height from zero to be equal. and it will be possible for the axes // to be aligned but not visible. var extentRatio = (ryAxisMax - ryAxisMin) / (lyAxisMax - lyAxisMin); return { lyAxisMin: Math.min(lyAxisMin, ryAxisMin / extentRatio), lyAxisMax: Math.max(lyAxisMax, ryAxisMax / extentRatio), ryAxisMin: Math.min(ryAxisMin, lyAxisMin * extentRatio), ryAxisMax: Math.max(ryAxisMax, lyAxisMax * extentRatio) }; } function prepareRightYAxis (ranges) { var needDomain = _chart.rightY() === undefined || _chart.elasticY(), needRange = needDomain || _chart.resizing(); if (_chart.rightY() === undefined) { _chart.rightY(d3.scale.linear()); } if (needDomain) { _chart.rightY().domain([ranges.ryAxisMin, ranges.ryAxisMax]); } if (needRange) { _chart.rightY().rangeRound([_chart.yAxisHeight(), 0]); } _chart.rightY().range([_chart.yAxisHeight(), 0]); _chart.rightYAxis(_chart.rightYAxis().scale(_chart.rightY())); _chart.rightYAxis().orient('right'); } function prepareLeftYAxis (ranges) { var needDomain = _chart.y() === undefined || _chart.elasticY(), needRange = needDomain || _chart.resizing(); if (_chart.y() === undefined) { _chart.y(d3.scale.linear()); } if (needDomain) { _chart.y().domain([ranges.lyAxisMin, ranges.lyAxisMax]); } if (needRange) { _chart.y().rangeRound([_chart.yAxisHeight(), 0]); } _chart.y().range([_chart.yAxisHeight(), 0]); _chart.yAxis(_chart.yAxis().scale(_chart.y())); _chart.yAxis().orient('left'); } function generateChildG (child, i) { child._generateG(_chart.g()); child.g().attr('class', SUB_CHART_CLASS + ' _' + i); } _chart.plotData = function () { for (var i = 0; i < _children.length; ++i) { var child = _children[i]; if (!child.g()) { generateChildG(child, i); } if (_shareColors) { child.colors(_chart.colors()); } child.x(_chart.x()); child.xAxis(_chart.xAxis()); if (child.useRightYAxis()) { child.y(_chart.rightY()); child.yAxis(_chart.rightYAxis()); } else { child.y(_chart.y()); child.yAxis(_chart.yAxis()); } child.plotData(); child._activateRenderlets(); } }; /** * Get or set whether to draw gridlines from the right y axis. Drawing from the left y axis is the * default behavior. This option is only respected when subcharts with both left and right y-axes * are present. * @method useRightAxisGridLines * @memberof dc.compositeChart * @instance * @param {Boolean} [useRightAxisGridLines=false] * @returns {Boolean|dc.compositeChart} */ _chart.useRightAxisGridLines = function (useRightAxisGridLines) { if (!arguments) { return _rightAxisGridLines; } _rightAxisGridLines = useRightAxisGridLines; return _chart; }; /** * Get or set chart-specific options for all child charts. This is equivalent to calling * {@link dc.baseMixin#options .options} on each child chart. * @method childOptions * @memberof dc.compositeChart * @instance * @param {Object} [childOptions] * @returns {Object|dc.compositeChart} */ _chart.childOptions = function (childOptions) { if (!arguments.length) { return _childOptions; } _childOptions = childOptions; _children.forEach(function (child) { child.options(_childOptions); }); return _chart; }; _chart.fadeDeselectedArea = function () { for (var i = 0; i < _children.length; ++i) { var child = _children[i]; child.brush(_chart.brush()); child.fadeDeselectedArea(); } }; /** * Set or get the right y axis label. * @method rightYAxisLabel * @memberof dc.compositeChart * @instance * @param {String} [rightYAxisLabel] * @param {Number} [padding] * @returns {String|dc.compositeChart} */ _chart.rightYAxisLabel = function (rightYAxisLabel, padding) { if (!arguments.length) { return _rightYAxisLabel; } _rightYAxisLabel = rightYAxisLabel; _chart.margins().right -= _rightYAxisLabelPadding; _rightYAxisLabelPadding = (padding === undefined) ? DEFAULT_RIGHT_Y_AXIS_LABEL_PADDING : padding; _chart.margins().right += _rightYAxisLabelPadding; return _chart; }; /** * Combine the given charts into one single composite coordinate grid chart. * @method compose * @memberof dc.compositeChart * @instance * @example * moveChart.compose([ * // when creating sub-chart you need to pass in the parent chart * dc.lineChart(moveChart) * .group(indexAvgByMonthGroup) // if group is missing then parent's group will be used * .valueAccessor(function (d){return d.value.avg;}) * // most of the normal functions will continue to work in a composed chart * .renderArea(true) * .stack(monthlyMoveGroup, function (d){return d.value;}) * .title(function (d){ * var value = d.value.avg?d.value.avg:d.value; * if(isNaN(value)) value = 0; * return dateFormat(d.key) + '\n' + numberFormat(value); * }), * dc.barChart(moveChart) * .group(volumeByMonthGroup) * .centerBar(true) * ]); * @param {Array<Chart>} [subChartArray] * @returns {dc.compositeChart} */ _chart.compose = function (subChartArray) { _children = subChartArray; _children.forEach(function (child) { child.height(_chart.height()); child.width(_chart.width()); child.margins(_chart.margins()); if (_shareTitle) { child.title(_chart.title()); } child.options(_childOptions); }); return _chart; }; /** * Returns the child charts which are composed into the composite chart. * @method children * @memberof dc.compositeChart * @instance * @returns {Array<dc.baseMixin>} */ _chart.children = function () { return _children; }; /** * Get or set color sharing for the chart. If set, the {@link dc.colorMixin#colors .colors()} value from this chart * will be shared with composed children. Additionally if the child chart implements * Stackable and has not set a custom .colorAccessor, then it will generate a color * specific to its order in the composition. * @method shareColors * @memberof dc.compositeChart * @instance * @param {Boolean} [shareColors=false] * @returns {Boolean|dc.compositeChart} */ _chart.shareColors = function (shareColors) { if (!arguments.length) { return _shareColors; } _shareColors = shareColors; return _chart; }; /** * Get or set title sharing for the chart. If set, the {@link dc.baseMixin#title .title()} value from * this chart will be shared with composed children. * @method shareTitle * @memberof dc.compositeChart * @instance * @param {Boolean} [shareTitle=true] * @returns {Boolean|dc.compositeChart} */ _chart.shareTitle = function (shareTitle) { if (!arguments.length) { return _shareTitle; } _shareTitle = shareTitle; return _chart; }; /** * Get or set the y scale for the right axis. The right y scale is typically automatically * generated by the chart implementation. * @method rightY * @memberof dc.compositeChart * @instance * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/Scales.md d3.scale} * @param {d3.scale} [yScale] * @returns {d3.scale|dc.compositeChart} */ _chart.rightY = function (yScale) { if (!arguments.length) { return _rightY; } _rightY = yScale; _chart.rescale(); return _chart; }; /** * Get or set alignment between left and right y axes. A line connecting '0' on both y axis * will be parallel to x axis. This only has effect when {@link #dc.coordinateGridMixin+elasticY elasticY} is true. * @method alignYAxes * @memberof dc.compositeChart * @instance * @param {Boolean} [alignYAxes=false] * @returns {Chart} */ _chart.alignYAxes = function (alignYAxes) { if (!arguments.length) { return _alignYAxes; } _alignYAxes = alignYAxes; _chart.rescale(); return _chart; }; function leftYAxisChildren () { return _children.filter(function (child) { return !child.useRightYAxis(); }); } function rightYAxisChildren () { return _children.filter(function (child) { return child.useRightYAxis(); }); } function getYAxisMin (charts) { return charts.map(function (c) { return c.yAxisMin(); }); } delete _chart.yAxisMin; function yAxisMin () { return d3.min(getYAxisMin(leftYAxisChildren())); } function rightYAxisMin () { return d3.min(getYAxisMin(rightYAxisChildren())); } function getYAxisMax (charts) { return charts.map(function (c) { return c.yAxisMax(); }); } delete _chart.yAxisMax; function yAxisMax () { return dc.utils.add(d3.max(getYAxisMax(leftYAxisChildren())), _chart.yAxisPadding()); } function rightYAxisMax () { return dc.utils.add(d3.max(getYAxisMax(rightYAxisChildren())), _chart.yAxisPadding()); } function getAllXAxisMinFromChildCharts () { return _children.map(function (c) { return c.xAxisMin(); }); } dc.override(_chart, 'xAxisMin', function () { return dc.utils.subtract(d3.min(getAllXAxisMinFromChildCharts()), _chart.xAxisPadding()); }); function getAllXAxisMaxFromChildCharts () { return _children.map(function (c) { return c.xAxisMax(); }); } dc.override(_chart, 'xAxisMax', function () { return dc.utils.add(d3.max(getAllXAxisMaxFromChildCharts()), _chart.xAxisPadding()); }); _chart.legendables = function () { return _children.reduce(function (items, child) { if (_shareColors) { child.colors(_chart.colors()); } items.push.apply(items, child.legendables()); return items; }, []); }; _chart.legendHighlight = function (d) { for (var j = 0; j < _children.length; ++j) { var child = _children[j]; child.legendHighlight(d); } }; _chart.legendReset = function (d) { for (var j = 0; j < _children.length; ++j) { var child = _children[j]; child.legendReset(d); } }; _chart.legendToggle = function () { console.log('composite should not be getting legendToggle itself'); }; /** * Set or get the right y axis used by the composite chart. This function is most useful when y * axis customization is required. The y axis in dc.js is an instance of a [d3 axis * object](https://github.com/d3/d3-3.x-api-reference/blob/master/SVG-Axes.md#axis) therefore it supports any valid * d3 axis manipulation. * * **Caution**: The y axis is usually generated internally by dc; resetting it may cause * unexpected results. * @method rightYAxis * @memberof dc.compositeChart * @instance * @see {@link https://github.com/d3/d3-3.x-api-reference/blob/master/SVG-Axes.md#axis d3.svg.axis} * @example * // customize y axis tick format * chart.rightYAxis().tickFormat(function (v) {return v + '%';}); * // customize y axis tick values * chart.rightYAxis().tickValues([0, 100, 200, 300]); * @param {d3.svg.axis} [rightYAxis] * @returns {d3.svg.axis|dc.compositeChart} */ _chart.rightYAxis = function (rightYAxis) { if (!arguments.length) { return _rightYAxis; } _rightYAxis = rightYAxis; return _chart; }; return _chart.anchor(parent, chartGroup); };