/**
* Coordinate Grid is an abstract base chart designed to support a number of coordinate grid based
* concrete chart types, e.g. bar chart, line chart, and bubble chart.
* @name coordinateGridMixin
* @memberof dc
* @mixin
* @mixes dc.colorMixin
* @mixes dc.marginMixin
* @mixes dc.baseMixin
* @param {Object} _chart
* @returns {dc.coordinateGridMixin}
*/
dc.coordinateGridMixin = function (_chart) {
var GRID_LINE_CLASS = 'grid-line';
var HORIZONTAL_CLASS = 'horizontal';
var VERTICAL_CLASS = 'vertical';
var Y_AXIS_LABEL_CLASS = 'y-axis-label';
var X_AXIS_LABEL_CLASS = 'x-axis-label';
var CUSTOM_BRUSH_HANDLE_CLASS = 'custom-brush-handle';
var DEFAULT_AXIS_LABEL_PADDING = 12;
_chart = dc.colorMixin(dc.marginMixin(dc.baseMixin(_chart)));
_chart.colors(d3.scaleOrdinal(d3.schemeCategory10));
_chart._mandatoryAttributes().push('x');
var _parent;
var _g;
var _chartBodyG;
var _x;
var _origX; // Will hold orginial scale in case of zoom
var _xOriginalDomain;
var _xAxis = d3.axisBottom();
var _xUnits = dc.units.integers;
var _xAxisPadding = 0;
var _xAxisPaddingUnit = d3.timeDay;
var _xElasticity = false;
var _xAxisLabel;
var _xAxisLabelPadding = 0;
var _lastXDomain;
var _y;
var _yAxis = null;
var _yAxisPadding = 0;
var _yElasticity = false;
var _yAxisLabel;
var _yAxisLabelPadding = 0;
var _brush = d3.brushX();
var _gBrush;
var _brushOn = true;
var _parentBrushOn = false;
var _round;
var _renderHorizontalGridLine = false;
var _renderVerticalGridLine = false;
var _resizing = false;
var _unitCount;
var _zoomScale = [1, Infinity];
var _zoomOutRestrict = true;
var _zoom = d3.zoom().on('zoom', onZoom);
var _nullZoom = d3.zoom().on('zoom', null);
var _hasBeenMouseZoomable = false;
var _rangeChart;
var _focusChart;
var _mouseZoomable = false;
var _clipPadding = 0;
var _outerRangeBandPadding = 0.5;
var _rangeBandPadding = 0;
var _useRightYAxis = false;
/**
* When changing the domain of the x or y scale, it is necessary to tell the chart to recalculate
* and redraw the axes. (`.rescale()` is called automatically when the x or y scale is replaced
* with {@link dc.coordinateGridMixin+x .x()} or {@link dc.coordinateGridMixin#y .y()}, and has
* no effect on elastic scales.)
* @method rescale
* @memberof dc.coordinateGridMixin
* @instance
* @returns {dc.coordinateGridMixin}
*/
_chart.rescale = function () {
_unitCount = undefined;
_resizing = true;
return _chart;
};
_chart.resizing = function (resizing) {
if (!arguments.length) {
return _resizing;
}
_resizing = resizing;
return _chart;
};
/**
* Get or set the range selection chart associated with this instance. Setting the range selection
* chart using this function will automatically update its selection brush when the current chart
* zooms in. In return the given range chart will also automatically attach this chart as its focus
* chart hence zoom in when range brush updates.
*
* Usually the range and focus charts will share a dimension. The range chart will set the zoom
* boundaries for the focus chart, so its dimension values must be compatible with the domain of
* the focus chart.
*
* See the [Nasdaq 100 Index](http://dc-js.github.com/dc.js/) example for this effect in action.
* @method rangeChart
* @memberof dc.coordinateGridMixin
* @instance
* @param {dc.coordinateGridMixin} [rangeChart]
* @returns {dc.coordinateGridMixin}
*/
_chart.rangeChart = function (rangeChart) {
if (!arguments.length) {
return _rangeChart;
}
_rangeChart = rangeChart;
_rangeChart.focusChart(_chart);
return _chart;
};
/**
* Get or set the scale extent for mouse zooms.
* @method zoomScale
* @memberof dc.coordinateGridMixin
* @instance
* @param {Array<Number|Date>} [extent=[1, Infinity]]
* @returns {Array<Number|Date>|dc.coordinateGridMixin}
*/
_chart.zoomScale = function (extent) {
if (!arguments.length) {
return _zoomScale;
}
_zoomScale = extent;
return _chart;
};
/**
* Get or set the zoom restriction for the chart. If true limits the zoom to origional domain of the chart.
* @method zoomOutRestrict
* @memberof dc.coordinateGridMixin
* @instance
* @param {Boolean} [zoomOutRestrict=true]
* @returns {Boolean|dc.coordinateGridMixin}
*/
_chart.zoomOutRestrict = function (zoomOutRestrict) {
if (!arguments.length) {
return _zoomOutRestrict;
}
_zoomOutRestrict = zoomOutRestrict;
return _chart;
};
_chart._generateG = function (parent) {
if (parent === undefined) {
_parent = _chart.svg();
} else {
_parent = parent;
}
var href = window.location.href.split('#')[0];
_g = _parent.append('g');
_chartBodyG = _g.append('g').attr('class', 'chart-body')
.attr('transform', 'translate(' + _chart.margins().left + ', ' + _chart.margins().top + ')')
.attr('clip-path', 'url(' + href + '#' + getClipPathId() + ')');
return _g;
};
/**
* Get or set the root g element. This method is usually used to retrieve the g element in order to
* overlay custom svg drawing programatically. **Caution**: The root g element is usually generated
* by dc.js internals, and resetting it might produce unpredictable result.
* @method g
* @memberof dc.coordinateGridMixin
* @instance
* @param {SVGElement} [gElement]
* @returns {SVGElement|dc.coordinateGridMixin}
*/
_chart.g = function (gElement) {
if (!arguments.length) {
return _g;
}
_g = gElement;
return _chart;
};
/**
* Set or get mouse zoom capability flag (default: false). When turned on the chart will be
* zoomable using the mouse wheel. If the range selector chart is attached zooming will also update
* the range selection brush on the associated range selector chart.
* @method mouseZoomable
* @memberof dc.coordinateGridMixin
* @instance
* @param {Boolean} [mouseZoomable=false]
* @returns {Boolean|dc.coordinateGridMixin}
*/
_chart.mouseZoomable = function (mouseZoomable) {
if (!arguments.length) {
return _mouseZoomable;
}
_mouseZoomable = mouseZoomable;
return _chart;
};
/**
* Retrieve the svg group for the chart body.
* @method chartBodyG
* @memberof dc.coordinateGridMixin
* @instance
* @param {SVGElement} [chartBodyG]
* @returns {SVGElement}
*/
_chart.chartBodyG = function (chartBodyG) {
if (!arguments.length) {
return _chartBodyG;
}
_chartBodyG = chartBodyG;
return _chart;
};
/**
* **mandatory**
*
* Get or set the x scale. The x scale can be any d3
* {@link https://github.com/d3/d3-scale/blob/master/README.md d3.scale} or
* {@link https://github.com/d3/d3-scale/blob/master/README.md#ordinal-scales ordinal scale}
* @method x
* @memberof dc.coordinateGridMixin
* @instance
* @see {@link https://github.com/d3/d3-scale/blob/master/README.md d3.scale}
* @example
* // set x to a linear scale
* chart.x(d3.scaleLinear().domain([-2500, 2500]))
* // set x to a time scale to generate histogram
* chart.x(d3.scaleTime().domain([new Date(1985, 0, 1), new Date(2012, 11, 31)]))
* @param {d3.scale} [xScale]
* @returns {d3.scale|dc.coordinateGridMixin}
*/
_chart.x = function (xScale) {
if (!arguments.length) {
return _x;
}
_x = xScale;
_xOriginalDomain = _x.domain();
_chart.rescale();
return _chart;
};
_chart.xOriginalDomain = function () {
return _xOriginalDomain;
};
/**
* Set or get the xUnits function. The coordinate grid chart uses the xUnits function to calculate
* the number of data projections on the x axis such as the number of bars for a bar chart or the
* number of dots for a line chart.
*
* This function is expected to return a Javascript array of all data points on the x axis, or
* the number of points on the axis. d3 time range functions [d3.timeDays, d3.timeMonths, and
* d3.timeYears](https://github.com/d3/d3-time/blob/master/README.md#intervals) are all valid
* xUnits functions.
*
* dc.js also provides a few units function, see the {@link dc.units Units Namespace} for
* a list of built-in units functions.
*
* Note that as of dc.js 3.0, `dc.units.ordinal` is not a real function, because it is not
* possible to define this function compliant with the d3 range functions. It was already a
* magic value which caused charts to behave differently, and now it is completely so.
* @method xUnits
* @memberof dc.coordinateGridMixin
* @instance
* @example
* // set x units to count days
* chart.xUnits(d3.timeDays);
* // set x units to count months
* chart.xUnits(d3.timeMonths);
*
* // A custom xUnits function can be used as long as it follows the following interface:
* // units in integer
* function(start, end) {
* // simply calculates how many integers in the domain
* return Math.abs(end - start);
* }
*
* // fixed units
* function(start, end) {
* // be aware using fixed units will disable the focus/zoom ability on the chart
* return 1000;
* }
* @param {Function} [xUnits=dc.units.integers]
* @returns {Function|dc.coordinateGridMixin}
*/
_chart.xUnits = function (xUnits) {
if (!arguments.length) {
return _xUnits;
}
_xUnits = xUnits;
return _chart;
};
/**
* Set or get the x axis used by a particular coordinate grid chart instance. This function is most
* useful when x axis customization is required. The x axis in dc.js is an instance of a
* {@link https://github.com/d3/d3-axis/blob/master/README.md#axisBottom d3 bottom axis object};
* therefore it supports any valid d3 axisBottom manipulation.
*
* **Caution**: The x axis is usually generated internally by dc; resetting it may cause
* unexpected results. Note also that when used as a getter, this function is not chainable:
* it returns the axis, not the chart,
* {@link https://github.com/dc-js/dc.js/wiki/FAQ#why-does-everything-break-after-a-call-to-xaxis-or-yaxis
* so attempting to call chart functions after calling `.xAxis()` will fail}.
* @method xAxis
* @memberof dc.coordinateGridMixin
* @instance
* @see {@link https://github.com/d3/d3-axis/blob/master/README.md#axisBottom d3.axisBottom}
* @example
* // customize x axis tick format
* chart.xAxis().tickFormat(function(v) {return v + '%';});
* // customize x axis tick values
* chart.xAxis().tickValues([0, 100, 200, 300]);
* @param {d3.axis} [xAxis=d3.axisBottom()]
* @returns {d3.axis|dc.coordinateGridMixin}
*/
_chart.xAxis = function (xAxis) {
if (!arguments.length) {
return _xAxis;
}
_xAxis = xAxis;
return _chart;
};
/**
* Turn on/off elastic x axis behavior. If x axis elasticity is turned on, then the grid chart will
* attempt to recalculate the x axis range whenever a redraw event is triggered.
* @method elasticX
* @memberof dc.coordinateGridMixin
* @instance
* @param {Boolean} [elasticX=false]
* @returns {Boolean|dc.coordinateGridMixin}
*/
_chart.elasticX = function (elasticX) {
if (!arguments.length) {
return _xElasticity;
}
_xElasticity = elasticX;
return _chart;
};
/**
* Set or get x axis padding for the elastic x axis. The padding will be added to both end of the x
* axis if elasticX is turned on; otherwise it is ignored.
*
* Padding can be an integer or percentage in string (e.g. '10%'). Padding can be applied to
* number or date x axes. When padding a date axis, an integer represents number of units being padded
* and a percentage string will be treated the same as an integer. The unit will be determined by the
* xAxisPaddingUnit variable.
* @method xAxisPadding
* @memberof dc.coordinateGridMixin
* @instance
* @param {Number|String} [padding=0]
* @returns {Number|String|dc.coordinateGridMixin}
*/
_chart.xAxisPadding = function (padding) {
if (!arguments.length) {
return _xAxisPadding;
}
_xAxisPadding = padding;
return _chart;
};
/**
* Set or get x axis padding unit for the elastic x axis. The padding unit will determine which unit to
* use when applying xAxis padding if elasticX is turned on and if x-axis uses a time dimension;
* otherwise it is ignored.
*
* The padding unit should be a
* [d3 time interval](https://github.com/d3/d3-time/blob/master/README.md#_interval).
* For backward compatibility with dc.js 2.0, it can also be the name of a d3 time interval
* ('day', 'hour', etc). Available arguments are the
* [d3 time intervals](https://github.com/d3/d3-time/blob/master/README.md#intervals d3.timeInterval).
* @method xAxisPaddingUnit
* @memberof dc.coordinateGridMixin
* @instance
* @param {String} [unit=d3.timeDay]
* @returns {String|dc.coordinateGridMixin}
*/
_chart.xAxisPaddingUnit = function (unit) {
if (!arguments.length) {
return _xAxisPaddingUnit;
}
_xAxisPaddingUnit = unit;
return _chart;
};
/**
* Returns the number of units displayed on the x axis. If the x axis is ordinal (`xUnits` is
* `dc.units.ordinal`), this is the number of items in the domain of the x scale. Otherwise, the
* x unit count is calculated using the {@link dc.coordinateGridMixin#xUnits xUnits} function.
* @method xUnitCount
* @memberof dc.coordinateGridMixin
* @instance
* @returns {Number}
*/
_chart.xUnitCount = function () {
if (_unitCount === undefined) {
if (_chart.isOrdinal()) {
// In this case it number of items in domain
_unitCount = _chart.x().domain().length;
} else {
_unitCount = _chart.xUnits()(_chart.x().domain()[0], _chart.x().domain()[1]);
// Sometimes xUnits() may return an array while sometimes directly the count
if (_unitCount instanceof Array) {
_unitCount = _unitCount.length;
}
}
}
return _unitCount;
};
/**
* Gets or sets whether the chart should be drawn with a right axis instead of a left axis. When
* used with a chart in a composite chart, allows both left and right Y axes to be shown on a
* chart.
* @method useRightYAxis
* @memberof dc.coordinateGridMixin
* @instance
* @param {Boolean} [useRightYAxis=false]
* @returns {Boolean|dc.coordinateGridMixin}
*/
_chart.useRightYAxis = function (useRightYAxis) {
if (!arguments.length) {
return _useRightYAxis;
}
// We need to warn if value is changing after _yAxis was created
if (_useRightYAxis !== useRightYAxis && _yAxis) {
dc.logger.warn('Value of useRightYAxis has been altered, after yAxis was created. ' +
'You might get unexpected yAxis behavior. ' +
'Make calls to useRightYAxis sooner in your chart creation process.');
}
_useRightYAxis = useRightYAxis;
return _chart;
};
/**
* Returns true if the chart is using ordinal xUnits ({@link dc.units.ordinal dc.units.ordinal}, or false
* otherwise. Most charts behave differently with ordinal data and use the result of this method to
* trigger the appropriate logic.
* @method isOrdinal
* @memberof dc.coordinateGridMixin
* @instance
* @returns {Boolean}
*/
_chart.isOrdinal = function () {
return _chart.xUnits() === dc.units.ordinal;
};
_chart._useOuterPadding = function () {
return true;
};
_chart._ordinalXDomain = function () {
var groups = _chart._computeOrderedGroups(_chart.data());
return groups.map(_chart.keyAccessor());
};
function prepareXAxis (g, render) {
if (!_chart.isOrdinal()) {
if (_chart.elasticX()) {
_x.domain([_chart.xAxisMin(), _chart.xAxisMax()]);
}
} else { // _chart.isOrdinal()
// D3v4 - Ordinal charts would need scaleBand
// bandwidth is a method in scaleBand
// (https://github.com/d3/d3-scale/blob/master/README.md#scaleBand)
if (!_x.bandwidth) {
// If _x is not a scaleBand create a new scale and
// copy the original domain to the new scale
dc.logger.warn('For compatibility with d3v4+, dc.js d3.0 ordinal bar/line/bubble charts need ' +
'd3.scaleBand() for the x scale, instead of d3.scaleOrdinal(). ' +
'Replacing .x() with a d3.scaleBand with the same domain - ' +
'make the same change in your code to avoid this warning!');
_x = d3.scaleBand().domain(_x.domain());
}
if (_chart.elasticX() || _x.domain().length === 0) {
_x.domain(_chart._ordinalXDomain());
}
}
// has the domain changed?
var xdom = _x.domain();
if (render || !dc.utils.arraysEqual(_lastXDomain, xdom)) {
_chart.rescale();
}
_lastXDomain = xdom;
// please can't we always use rangeBands for bar charts?
if (_chart.isOrdinal()) {
_x.range([0, _chart.xAxisLength()])
.paddingInner(_rangeBandPadding)
.paddingOuter(_chart._useOuterPadding() ? _outerRangeBandPadding : 0);
} else {
_x.range([0, _chart.xAxisLength()]);
}
_xAxis = _xAxis.scale(_chart.x());
renderVerticalGridLines(g);
}
_chart.renderXAxis = function (g) {
var axisXG = g.select('g.x');
if (axisXG.empty()) {
axisXG = g.append('g')
.attr('class', 'axis x')
.attr('transform', 'translate(' + _chart.margins().left + ',' + _chart._xAxisY() + ')');
}
var axisXLab = g.select('text.' + X_AXIS_LABEL_CLASS);
if (axisXLab.empty() && _chart.xAxisLabel()) {
axisXLab = g.append('text')
.attr('class', X_AXIS_LABEL_CLASS)
.attr('transform', 'translate(' + (_chart.margins().left + _chart.xAxisLength() / 2) + ',' +
(_chart.height() - _xAxisLabelPadding) + ')')
.attr('text-anchor', 'middle');
}
if (_chart.xAxisLabel() && axisXLab.text() !== _chart.xAxisLabel()) {
axisXLab.text(_chart.xAxisLabel());
}
dc.transition(axisXG, _chart.transitionDuration(), _chart.transitionDelay())
.attr('transform', 'translate(' + _chart.margins().left + ',' + _chart._xAxisY() + ')')
.call(_xAxis);
dc.transition(axisXLab, _chart.transitionDuration(), _chart.transitionDelay())
.attr('transform', 'translate(' + (_chart.margins().left + _chart.xAxisLength() / 2) + ',' +
(_chart.height() - _xAxisLabelPadding) + ')');
};
function renderVerticalGridLines (g) {
var gridLineG = g.select('g.' + VERTICAL_CLASS);
if (_renderVerticalGridLine) {
if (gridLineG.empty()) {
gridLineG = g.insert('g', ':first-child')
.attr('class', GRID_LINE_CLASS + ' ' + VERTICAL_CLASS)
.attr('transform', 'translate(' + _chart.margins().left + ',' + _chart.margins().top + ')');
}
var ticks = _xAxis.tickValues() ? _xAxis.tickValues() :
(typeof _x.ticks === 'function' ? _x.ticks.apply(_x, _xAxis.tickArguments()) : _x.domain());
var lines = gridLineG.selectAll('line')
.data(ticks);
// enter
var linesGEnter = lines.enter()
.append('line')
.attr('x1', function (d) {
return _x(d);
})
.attr('y1', _chart._xAxisY() - _chart.margins().top)
.attr('x2', function (d) {
return _x(d);
})
.attr('y2', 0)
.attr('opacity', 0);
dc.transition(linesGEnter, _chart.transitionDuration(), _chart.transitionDelay())
.attr('opacity', 0.5);
// update
dc.transition(lines, _chart.transitionDuration(), _chart.transitionDelay())
.attr('x1', function (d) {
return _x(d);
})
.attr('y1', _chart._xAxisY() - _chart.margins().top)
.attr('x2', function (d) {
return _x(d);
})
.attr('y2', 0);
// exit
lines.exit().remove();
} else {
gridLineG.selectAll('line').remove();
}
}
_chart._xAxisY = function () {
return (_chart.height() - _chart.margins().bottom);
};
_chart.xAxisLength = function () {
return _chart.effectiveWidth();
};
/**
* Set or get the x axis label. If setting the label, you may optionally include additional padding to
* the margin to make room for the label. By default the padded is set to 12 to accomodate the text height.
* @method xAxisLabel
* @memberof dc.coordinateGridMixin
* @instance
* @param {String} [labelText]
* @param {Number} [padding=12]
* @returns {String}
*/
_chart.xAxisLabel = function (labelText, padding) {
if (!arguments.length) {
return _xAxisLabel;
}
_xAxisLabel = labelText;
_chart.margins().bottom -= _xAxisLabelPadding;
_xAxisLabelPadding = (padding === undefined) ? DEFAULT_AXIS_LABEL_PADDING : padding;
_chart.margins().bottom += _xAxisLabelPadding;
return _chart;
};
function createYAxis () {
return _useRightYAxis ? d3.axisRight() : d3.axisLeft();
}
_chart._prepareYAxis = function (g) {
if (_y === undefined || _chart.elasticY()) {
if (_y === undefined) {
_y = d3.scaleLinear();
}
var min = _chart.yAxisMin() || 0,
max = _chart.yAxisMax() || 0;
_y.domain([min, max]).rangeRound([_chart.yAxisHeight(), 0]);
}
_y.range([_chart.yAxisHeight(), 0]);
if (!_yAxis) {
_yAxis = createYAxis();
}
_yAxis.scale(_y);
_chart._renderHorizontalGridLinesForAxis(g, _y, _yAxis);
};
_chart.renderYAxisLabel = function (axisClass, text, rotation, labelXPosition) {
labelXPosition = labelXPosition || _yAxisLabelPadding;
var axisYLab = _chart.g().select('text.' + Y_AXIS_LABEL_CLASS + '.' + axisClass + '-label');
var labelYPosition = (_chart.margins().top + _chart.yAxisHeight() / 2);
if (axisYLab.empty() && text) {
axisYLab = _chart.g().append('text')
.attr('transform', 'translate(' + labelXPosition + ',' + labelYPosition + '),rotate(' + rotation + ')')
.attr('class', Y_AXIS_LABEL_CLASS + ' ' + axisClass + '-label')
.attr('text-anchor', 'middle')
.text(text);
}
if (text && axisYLab.text() !== text) {
axisYLab.text(text);
}
dc.transition(axisYLab, _chart.transitionDuration(), _chart.transitionDelay())
.attr('transform', 'translate(' + labelXPosition + ',' + labelYPosition + '),rotate(' + rotation + ')');
};
_chart.renderYAxisAt = function (axisClass, axis, position) {
var axisYG = _chart.g().select('g.' + axisClass);
if (axisYG.empty()) {
axisYG = _chart.g().append('g')
.attr('class', 'axis ' + axisClass)
.attr('transform', 'translate(' + position + ',' + _chart.margins().top + ')');
}
dc.transition(axisYG, _chart.transitionDuration(), _chart.transitionDelay())
.attr('transform', 'translate(' + position + ',' + _chart.margins().top + ')')
.call(axis);
};
_chart.renderYAxis = function () {
var axisPosition = _useRightYAxis ? (_chart.width() - _chart.margins().right) : _chart._yAxisX();
_chart.renderYAxisAt('y', _yAxis, axisPosition);
var labelPosition = _useRightYAxis ? (_chart.width() - _yAxisLabelPadding) : _yAxisLabelPadding;
var rotation = _useRightYAxis ? 90 : -90;
_chart.renderYAxisLabel('y', _chart.yAxisLabel(), rotation, labelPosition);
};
_chart._renderHorizontalGridLinesForAxis = function (g, scale, axis) {
var gridLineG = g.select('g.' + HORIZONTAL_CLASS);
if (_renderHorizontalGridLine) {
// see https://github.com/d3/d3-axis/blob/master/src/axis.js#L48
var ticks = axis.tickValues() ? axis.tickValues() :
(scale.ticks ? scale.ticks.apply(scale, axis.tickArguments()) : scale.domain());
if (gridLineG.empty()) {
gridLineG = g.insert('g', ':first-child')
.attr('class', GRID_LINE_CLASS + ' ' + HORIZONTAL_CLASS)
.attr('transform', 'translate(' + _chart.margins().left + ',' + _chart.margins().top + ')');
}
var lines = gridLineG.selectAll('line')
.data(ticks);
// enter
var linesGEnter = lines.enter()
.append('line')
.attr('x1', 1)
.attr('y1', function (d) {
return scale(d);
})
.attr('x2', _chart.xAxisLength())
.attr('y2', function (d) {
return scale(d);
})
.attr('opacity', 0);
dc.transition(linesGEnter, _chart.transitionDuration(), _chart.transitionDelay())
.attr('opacity', 0.5);
// update
dc.transition(lines, _chart.transitionDuration(), _chart.transitionDelay())
.attr('x1', 1)
.attr('y1', function (d) {
return scale(d);
})
.attr('x2', _chart.xAxisLength())
.attr('y2', function (d) {
return scale(d);
});
// exit
lines.exit().remove();
} else {
gridLineG.selectAll('line').remove();
}
};
_chart._yAxisX = function () {
return _chart.useRightYAxis() ? _chart.width() - _chart.margins().right : _chart.margins().left;
};
/**
* Set or get the y axis label. If setting the label, you may optionally include additional padding
* to the margin to make room for the label. By default the padding is set to 12 to accommodate the
* text height.
* @method yAxisLabel
* @memberof dc.coordinateGridMixin
* @instance
* @param {String} [labelText]
* @param {Number} [padding=12]
* @returns {String|dc.coordinateGridMixin}
*/
_chart.yAxisLabel = function (labelText, padding) {
if (!arguments.length) {
return _yAxisLabel;
}
_yAxisLabel = labelText;
_chart.margins().left -= _yAxisLabelPadding;
_yAxisLabelPadding = (padding === undefined) ? DEFAULT_AXIS_LABEL_PADDING : padding;
_chart.margins().left += _yAxisLabelPadding;
return _chart;
};
/**
* Get or set the y scale. The y scale is typically automatically determined by the chart implementation.
* @method y
* @memberof dc.coordinateGridMixin
* @instance
* @see {@link https://github.com/d3/d3-scale/blob/master/README.md d3.scale}
* @param {d3.scale} [yScale]
* @returns {d3.scale|dc.coordinateGridMixin}
*/
_chart.y = function (yScale) {
if (!arguments.length) {
return _y;
}
_y = yScale;
_chart.rescale();
return _chart;
};
/**
* Set or get the y axis used by the coordinate grid chart instance. This function is most useful
* when y axis customization is required. Depending on `useRightYAxis` the y axis in dc.js is an instance of
* either [d3.axisLeft](https://github.com/d3/d3-axis/blob/master/README.md#axisLeft) or
* [d3.axisRight](https://github.com/d3/d3-axis/blob/master/README.md#axisRight); therefore it supports any
* valid d3 axis manipulation.
*
* **Caution**: The y axis is usually generated internally by dc; resetting it may cause
* unexpected results. Note also that when used as a getter, this function is not chainable: it
* returns the axis, not the chart,
* {@link https://github.com/dc-js/dc.js/wiki/FAQ#why-does-everything-break-after-a-call-to-xaxis-or-yaxis
* so attempting to call chart functions after calling `.yAxis()` will fail}.
* In addition, depending on whether you are going to use the axis on left or right
* you need to appropriately pass [d3.axisLeft](https://github.com/d3/d3-axis/blob/master/README.md#axisLeft)
* or [d3.axisRight](https://github.com/d3/d3-axis/blob/master/README.md#axisRight)
* @method yAxis
* @memberof dc.coordinateGridMixin
* @instance
* @see {@link https://github.com/d3/d3-axis/blob/master/README.md d3.axis}
* @example
* // customize y axis tick format
* chart.yAxis().tickFormat(function(v) {return v + '%';});
* // customize y axis tick values
* chart.yAxis().tickValues([0, 100, 200, 300]);
* @param {d3.axisLeft|d3.axisRight} [yAxis]
* @returns {d3.axisLeft|d3.axisRight|dc.coordinateGridMixin}
*/
_chart.yAxis = function (yAxis) {
if (!arguments.length) {
if (!_yAxis) {
_yAxis = createYAxis();
}
return _yAxis;
}
_yAxis = yAxis;
return _chart;
};
/**
* Turn on/off elastic y axis behavior. If y axis elasticity is turned on, then the grid chart will
* attempt to recalculate the y axis range whenever a redraw event is triggered.
* @method elasticY
* @memberof dc.coordinateGridMixin
* @instance
* @param {Boolean} [elasticY=false]
* @returns {Boolean|dc.coordinateGridMixin}
*/
_chart.elasticY = function (elasticY) {
if (!arguments.length) {
return _yElasticity;
}
_yElasticity = elasticY;
return _chart;
};
/**
* Turn on/off horizontal grid lines.
* @method renderHorizontalGridLines
* @memberof dc.coordinateGridMixin
* @instance
* @param {Boolean} [renderHorizontalGridLines=false]
* @returns {Boolean|dc.coordinateGridMixin}
*/
_chart.renderHorizontalGridLines = function (renderHorizontalGridLines) {
if (!arguments.length) {
return _renderHorizontalGridLine;
}
_renderHorizontalGridLine = renderHorizontalGridLines;
return _chart;
};
/**
* Turn on/off vertical grid lines.
* @method renderVerticalGridLines
* @memberof dc.coordinateGridMixin
* @instance
* @param {Boolean} [renderVerticalGridLines=false]
* @returns {Boolean|dc.coordinateGridMixin}
*/
_chart.renderVerticalGridLines = function (renderVerticalGridLines) {
if (!arguments.length) {
return _renderVerticalGridLine;
}
_renderVerticalGridLine = renderVerticalGridLines;
return _chart;
};
/**
* Calculates the minimum x value to display in the chart. Includes xAxisPadding if set.
* @method xAxisMin
* @memberof dc.coordinateGridMixin
* @instance
* @returns {*}
*/
_chart.xAxisMin = function () {
var min = d3.min(_chart.data(), function (e) {
return _chart.keyAccessor()(e);
});
return dc.utils.subtract(min, _xAxisPadding, _xAxisPaddingUnit);
};
/**
* Calculates the maximum x value to display in the chart. Includes xAxisPadding if set.
* @method xAxisMax
* @memberof dc.coordinateGridMixin
* @instance
* @returns {*}
*/
_chart.xAxisMax = function () {
var max = d3.max(_chart.data(), function (e) {
return _chart.keyAccessor()(e);
});
return dc.utils.add(max, _xAxisPadding, _xAxisPaddingUnit);
};
/**
* Calculates the minimum y value to display in the chart. Includes yAxisPadding if set.
* @method yAxisMin
* @memberof dc.coordinateGridMixin
* @instance
* @returns {*}
*/
_chart.yAxisMin = function () {
var min = d3.min(_chart.data(), function (e) {
return _chart.valueAccessor()(e);
});
return dc.utils.subtract(min, _yAxisPadding);
};
/**
* Calculates the maximum y value to display in the chart. Includes yAxisPadding if set.
* @method yAxisMax
* @memberof dc.coordinateGridMixin
* @instance
* @returns {*}
*/
_chart.yAxisMax = function () {
var max = d3.max(_chart.data(), function (e) {
return _chart.valueAccessor()(e);
});
return dc.utils.add(max, _yAxisPadding);
};
/**
* Set or get y axis padding for the elastic y axis. The padding will be added to the top and
* bottom of the y axis if elasticY is turned on; otherwise it is ignored.
*
* Padding can be an integer or percentage in string (e.g. '10%'). Padding can be applied to
* number or date axes. When padding a date axis, an integer represents number of days being padded
* and a percentage string will be treated the same as an integer.
* @method yAxisPadding
* @memberof dc.coordinateGridMixin
* @instance
* @param {Number|String} [padding=0]
* @returns {Number|dc.coordinateGridMixin}
*/
_chart.yAxisPadding = function (padding) {
if (!arguments.length) {
return _yAxisPadding;
}
_yAxisPadding = padding;
return _chart;
};
_chart.yAxisHeight = function () {
return _chart.effectiveHeight();
};
/**
* Set or get the rounding function used to quantize the selection when brushing is enabled.
* @method round
* @memberof dc.coordinateGridMixin
* @instance
* @example
* // set x unit round to by month, this will make sure range selection brush will
* // select whole months
* chart.round(d3.timeMonth.round);
* @param {Function} [round]
* @returns {Function|dc.coordinateGridMixin}
*/
_chart.round = function (round) {
if (!arguments.length) {
return _round;
}
_round = round;
return _chart;
};
_chart._rangeBandPadding = function (_) {
if (!arguments.length) {
return _rangeBandPadding;
}
_rangeBandPadding = _;
return _chart;
};
_chart._outerRangeBandPadding = function (_) {
if (!arguments.length) {
return _outerRangeBandPadding;
}
_outerRangeBandPadding = _;
return _chart;
};
dc.override(_chart, 'filter', function (_) {
if (!arguments.length) {
return _chart._filter();
}
_chart._filter(_);
_chart.redrawBrush(_, false);
return _chart;
});
/**
* Get or set the brush. Brush must be an instance of d3 brushes
* https://github.com/d3/d3-brush/blob/master/README.md
* You will use this only if you are writing a new chart type that supports brushing.
*
* **Caution**: dc creates and manages brushes internally. Go through and understand the source code
* if you want to pass a new brush object. Even if you are only using the getter,
* the brush object may not behave the way you expect.
*
* @method brush
* @memberof dc.coordinateGridMixin
* @instance
* @param {d3.brush} [_]
* @returns {d3.brush|dc.coordinateGridMixin}
*/
_chart.brush = function (_) {
if (!arguments.length) {
return _brush;
}
_brush = _;
return _chart;
};
_chart.renderBrush = function (g, doTransition) {
if (_brushOn) {
_brush.on('start brush end', _chart._brushing);
// To retrieve selection we need _gBrush
_gBrush = g.append('g')
.attr('class', 'brush')
.attr('transform', 'translate(' + _chart.margins().left + ',' + _chart.margins().top + ')');
_chart.setBrushExtents();
_chart.createBrushHandlePaths(_gBrush, doTransition);
_chart.redrawBrush(_chart.filter(), doTransition);
}
};
_chart.createBrushHandlePaths = function (gBrush) {
var brushHandles = gBrush.selectAll('path.' + CUSTOM_BRUSH_HANDLE_CLASS).data([{type: 'w'}, {type: 'e'}]);
brushHandles = brushHandles
.enter()
.append('path')
.attr('class', CUSTOM_BRUSH_HANDLE_CLASS)
.merge(brushHandles);
brushHandles
.attr('d', _chart.resizeHandlePath);
};
_chart.extendBrush = function (brushSelection) {
if (brushSelection && _chart.round()) {
brushSelection[0] = _chart.round()(brushSelection[0]);
brushSelection[1] = _chart.round()(brushSelection[1]);
}
return brushSelection;
};
_chart.brushIsEmpty = function (brushSelection) {
return !brushSelection || brushSelection[1] <= brushSelection[0];
};
_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 rangedFilter = _chart.brushIsEmpty(brushSelection) ? null : dc.filters.RangedFilter(brushSelection[0], brushSelection[1]);
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) {
// Set boundaries of the brush, must set it before applying to _gBrush
_brush.extent([[0, 0], [_chart.effectiveWidth(), _chart.effectiveHeight()]]);
_gBrush
.call(_brush);
};
_chart.redrawBrush = function (brushSelection, doTransition) {
if (_brushOn && _gBrush) {
if (_resizing) {
_chart.setBrushExtents(doTransition);
}
if (!brushSelection) {
_gBrush
.call(_brush.move, null);
_gBrush.selectAll('path.' + CUSTOM_BRUSH_HANDLE_CLASS)
.attr('display', 'none');
} else {
var scaledSelection = [_x(brushSelection[0]), _x(brushSelection[1])];
var gBrush =
dc.optionalTransition(doTransition, _chart.transitionDuration(), _chart.transitionDelay())(_gBrush);
gBrush
.call(_brush.move, scaledSelection);
gBrush.selectAll('path.' + CUSTOM_BRUSH_HANDLE_CLASS)
.attr('display', null)
.attr('transform', function (d, i) {
return 'translate(' + _x(brushSelection[i]) + ', 0)';
})
.attr('d', _chart.resizeHandlePath);
}
}
_chart.fadeDeselectedArea(brushSelection);
};
_chart.fadeDeselectedArea = function (brushSelection) {
// do nothing, sub-chart should override this function
};
// borrowed from Crossfilter example
_chart.resizeHandlePath = function (d) {
d = d.type;
var e = +(d === 'e'), x = e ? 1 : -1, y = _chart.effectiveHeight() / 3;
return 'M' + (0.5 * x) + ',' + y +
'A6,6 0 0 ' + e + ' ' + (6.5 * x) + ',' + (y + 6) +
'V' + (2 * y - 6) +
'A6,6 0 0 ' + e + ' ' + (0.5 * x) + ',' + (2 * y) +
'Z' +
'M' + (2.5 * x) + ',' + (y + 8) +
'V' + (2 * y - 8) +
'M' + (4.5 * x) + ',' + (y + 8) +
'V' + (2 * y - 8);
};
function getClipPathId () {
return _chart.anchorName().replace(/[ .#=\[\]"]/g, '-') + '-clip';
}
/**
* Get or set the padding in pixels for the clip path. Once set padding will be applied evenly to
* the top, left, right, and bottom when the clip path is generated. If set to zero, the clip area
* will be exactly the chart body area minus the margins.
* @method clipPadding
* @memberof dc.coordinateGridMixin
* @instance
* @param {Number} [padding=5]
* @returns {Number|dc.coordinateGridMixin}
*/
_chart.clipPadding = function (padding) {
if (!arguments.length) {
return _clipPadding;
}
_clipPadding = padding;
return _chart;
};
function generateClipPath () {
var defs = dc.utils.appendOrSelect(_parent, 'defs');
// cannot select <clippath> elements; bug in WebKit, must select by id
// https://groups.google.com/forum/#!topic/d3-js/6EpAzQ2gU9I
var id = getClipPathId();
var chartBodyClip = dc.utils.appendOrSelect(defs, '#' + id, 'clipPath').attr('id', id);
var padding = _clipPadding * 2;
dc.utils.appendOrSelect(chartBodyClip, 'rect')
.attr('width', _chart.xAxisLength() + padding)
.attr('height', _chart.yAxisHeight() + padding)
.attr('transform', 'translate(-' + _clipPadding + ', -' + _clipPadding + ')');
}
_chart._preprocessData = function () {};
_chart._doRender = function () {
_chart.resetSvg();
_chart._preprocessData();
_chart._generateG();
generateClipPath();
drawChart(true);
configureMouseZoom();
return _chart;
};
_chart._doRedraw = function () {
_chart._preprocessData();
drawChart(false);
generateClipPath();
return _chart;
};
function drawChart (render) {
if (_chart.isOrdinal()) {
_brushOn = false;
}
prepareXAxis(_chart.g(), render);
_chart._prepareYAxis(_chart.g());
_chart.plotData();
if (_chart.elasticX() || _resizing || render) {
_chart.renderXAxis(_chart.g());
}
if (_chart.elasticY() || _resizing || render) {
_chart.renderYAxis(_chart.g());
}
if (render) {
_chart.renderBrush(_chart.g(), false);
} else {
// Animate the brush only while resizing
_chart.redrawBrush(_chart.filter(), _resizing);
}
_chart.fadeDeselectedArea(_chart.filter());
_chart.resizing(false);
}
function configureMouseZoom () {
// Save a copy of original x scale
_origX = _x.copy();
if (_mouseZoomable) {
_chart._enableMouseZoom();
} else if (_hasBeenMouseZoomable) {
_chart._disableMouseZoom();
}
}
_chart._enableMouseZoom = function () {
_hasBeenMouseZoomable = true;
var extent = [[0, 0],[_chart.effectiveWidth(), _chart.effectiveHeight()]];
_zoom
.scaleExtent(_zoomScale)
.extent(extent)
.duration(_chart.transitionDuration());
if (_zoomOutRestrict) {
// Ensure minimum zoomScale is at least 1
var zoomScaleMin = Math.max(_zoomScale[0], 1);
_zoom
.translateExtent(extent)
.scaleExtent([zoomScaleMin, _zoomScale[1]]);
}
_chart.root().call(_zoom);
// Tell D3 zoom our current zoom/pan status
updateD3zoomTransform();
};
_chart._disableMouseZoom = function () {
_chart.root().call(_nullZoom);
};
function zoomHandler (newDomain, noRaiseEvents) {
var domFilter;
if (hasRangeSelected(newDomain)) {
_chart.x().domain(newDomain);
domFilter = dc.filters.RangedFilter(newDomain[0], newDomain[1]);
} else {
_chart.x().domain(_xOriginalDomain);
domFilter = null;
}
_chart.replaceFilter(domFilter);
_chart.rescale();
_chart.redraw();
if (!noRaiseEvents) {
if (_rangeChart && !dc.utils.arraysEqual(_chart.filter(), _rangeChart.filter())) {
dc.events.trigger(function () {
_rangeChart.replaceFilter(domFilter);
_rangeChart.redraw();
});
}
_chart._invokeZoomedListener();
dc.events.trigger(function () {
_chart.redrawGroup();
}, dc.constants.EVENT_DELAY);
}
}
// event.transform.rescaleX(_origX).domain() should give back newDomain
function domainToZoomTransform (newDomain, origDomain, xScale) {
var k = (origDomain[1] - origDomain[0]) / (newDomain[1] - newDomain[0]);
var xt = -1 * xScale(newDomain[0]);
return d3.zoomIdentity.scale(k).translate(xt, 0);
}
// If we changing zoom status (for example by calling focus), tell D3 zoom about it
function updateD3zoomTransform () {
if (_zoom) {
_zoom.transform(_chart.root(), domainToZoomTransform(_chart.x().domain(), _xOriginalDomain, _origX));
}
}
function onZoom () {
// Avoids infinite recursion (mutual recursion between range and focus operations)
// Source Event will be null when zoom 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 zoom 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', 'zoom', 'end'].indexOf(d3.event.sourceEvent.type) !== -1) {
return;
}
var newDomain = d3.event.transform.rescaleX(_origX).domain();
_chart.focus(newDomain, false);
}
function checkExtents (ext, outerLimits) {
if (!ext || ext.length !== 2 || !outerLimits || outerLimits.length !== 2) {
return ext;
}
if (ext[0] > outerLimits[1] || ext[1] < outerLimits[0]) {
console.warn('Could not intersect extents, will reset');
}
// Math.max does not work (as the values may be dates as well)
return [ext[0] > outerLimits[0] ? ext[0] : outerLimits[0], ext[1] < outerLimits[1] ? ext[1] : outerLimits[1]];
}
/**
* Zoom this chart to focus on the given range. The given range should be an array containing only
* 2 elements (`[start, end]`) defining a range in the x domain. If the range is not given or set
* to null, then the zoom will be reset. _For focus to work elasticX has to be turned off;
* otherwise focus will be ignored.
*
* To avoid ping-pong volley of events between a pair of range and focus charts please set
* `noRaiseEvents` to `true`. In that case it will update this chart but will not fire `zoom` event
* and not try to update back the associated range chart.
* If you are calling it manually - typically you will leave it to `false` (the default).
*
* @method focus
* @memberof dc.coordinateGridMixin
* @instance
* @example
* chart.on('renderlet', function(chart) {
* // smooth the rendering through event throttling
* dc.events.trigger(function(){
* // focus some other chart to the range selected by user on this chart
* someOtherChart.focus(chart.filter());
* });
* })
* @param {Array<Number>} [range]
* @param {Boolean} [noRaiseEvents = false]
* @return {undefined}
*/
_chart.focus = function (range, noRaiseEvents) {
if (_zoomOutRestrict) {
// ensure range is within _xOriginalDomain
range = checkExtents(range, _xOriginalDomain);
// If it has an associated range chart ensure range is within domain of that rangeChart
if (_rangeChart) {
range = checkExtents(range, _rangeChart.x().domain());
}
}
zoomHandler(range, noRaiseEvents);
updateD3zoomTransform();
};
_chart.refocused = function () {
return !dc.utils.arraysEqual(_chart.x().domain(), _xOriginalDomain);
};
_chart.focusChart = function (c) {
if (!arguments.length) {
return _focusChart;
}
_focusChart = c;
_chart.on('filtered.dcjs-range-chart', function (chart) {
if (!chart.filter()) {
dc.events.trigger(function () {
_focusChart.x().domain(_focusChart.xOriginalDomain(), true);
});
} else if (!dc.utils.arraysEqual(chart.filter(), _focusChart.filter())) {
dc.events.trigger(function () {
_focusChart.focus(chart.filter(), true);
});
}
});
return _chart;
};
/**
* Turn on/off the brush-based range filter. When brushing is on then user can drag the mouse
* across a chart with a quantitative scale to perform range filtering based on the extent of the
* brush, or click on the bars of an ordinal bar chart or slices of a pie chart to filter and
* un-filter them. However turning on the brush filter will disable other interactive elements on
* the chart such as highlighting, tool tips, and reference lines. Zooming will still be possible
* if enabled, but only via scrolling (panning will be disabled.)
* @method brushOn
* @memberof dc.coordinateGridMixin
* @instance
* @param {Boolean} [brushOn=true]
* @returns {Boolean|dc.coordinateGridMixin}
*/
_chart.brushOn = function (brushOn) {
if (!arguments.length) {
return _brushOn;
}
_brushOn = brushOn;
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;
};
function hasRangeSelected (range) {
return range instanceof Array && range.length > 1;
}
return _chart;
};