/**
* Concrete line/area chart implementation.
*
* Examples:
* - {@link http://dc-js.github.com/dc.js/ Nasdaq 100 Index}
* - {@link http://dc-js.github.com/dc.js/crime/index.html Canadian City Crime Stats}
* @class lineChart
* @memberof dc
* @mixes dc.stackMixin
* @mixes dc.coordinateGridMixin
* @example
* // create a line chart under #chart-container1 element using the default global chart group
* var chart1 = dc.lineChart('#chart-container1');
* // create a line chart under #chart-container2 element using chart group A
* var chart2 = dc.lineChart('#chart-container2', 'chartGroupA');
* // create a sub-chart under a composite parent chart
* var chart3 = dc.lineChart(compositeChart);
* @param {String|node|d3.selection|dc.compositeChart} parent - Any valid
* {@link https://github.com/d3/d3-selection/blob/master/README.md#select d3 single selector}
* specifying a dom block element such as a div; or a dom element or d3 selection. If the line
* chart is a sub-chart in a {@link dc.compositeChart Composite Chart} then pass in the parent
* composite chart instance instead.
* @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.lineChart}
*/
dc.lineChart = function (parent, chartGroup) {
var DEFAULT_DOT_RADIUS = 5;
var TOOLTIP_G_CLASS = 'dc-tooltip';
var DOT_CIRCLE_CLASS = 'dot';
var Y_AXIS_REF_LINE_CLASS = 'yRef';
var X_AXIS_REF_LINE_CLASS = 'xRef';
var DEFAULT_DOT_OPACITY = 1e-6;
var LABEL_PADDING = 3;
var _chart = dc.stackMixin(dc.coordinateGridMixin({}));
var _renderArea = false;
var _dotRadius = DEFAULT_DOT_RADIUS;
var _dataPointRadius = null;
var _dataPointFillOpacity = DEFAULT_DOT_OPACITY;
var _dataPointStrokeOpacity = DEFAULT_DOT_OPACITY;
var _curve = null;
var _interpolate = null; // d3.curveLinear; // deprecated in 3.0
var _tension = null; // deprecated in 3.0
var _defined;
var _dashStyle;
var _xyTipsOn = true;
_chart.transitionDuration(500);
_chart.transitionDelay(0);
_chart._rangeBandPadding(1);
_chart.plotData = function () {
var chartBody = _chart.chartBodyG();
var layersList = chartBody.select('g.stack-list');
if (layersList.empty()) {
layersList = chartBody.append('g').attr('class', 'stack-list');
}
var layers = layersList.selectAll('g.stack').data(_chart.data());
var layersEnter = layers
.enter()
.append('g')
.attr('class', function (d, i) {
return 'stack ' + '_' + i;
});
layers = layersEnter.merge(layers);
drawLine(layersEnter, layers);
drawArea(layersEnter, layers);
drawDots(chartBody, layers);
if (_chart.renderLabel()) {
drawLabels(layers);
}
};
/**
* Gets or sets the curve factory to use for lines and areas drawn, allowing e.g. step
* functions, splines, and cubic interpolation. Typically you would use one of the interpolator functions
* provided by {@link https://github.com/d3/d3-shape/blob/master/README.md#curves d3 curves}.
*
* Replaces the use of {@link dc.lineChart#interpolate} and {@link dc.lineChart#tension}
* in dc.js < 3.0
*
* This is passed to
* {@link https://github.com/d3/d3-shape/blob/master/README.md#line_curve line.curve} and
* {@link https://github.com/d3/d3-shape/blob/master/README.md#area_curve area.curve}.
* @example
* // default
* chart
* .curve(d3.curveLinear);
* // Add tension to curves that support it
* chart
* .curve(d3.curveCardinal.tension(0.5));
* // You can use some specialized variation like
* // https://en.wikipedia.org/wiki/Centripetal_Catmull%E2%80%93Rom_spline
* chart
* .curve(d3.curveCatmullRom.alpha(0.5));
* @method curve
* @memberof dc.lineChart
* @instance
* @see {@link https://github.com/d3/d3-shape/blob/master/README.md#line_curve line.curve}
* @see {@link https://github.com/d3/d3-shape/blob/master/README.md#area_curve area.curve}
* @param {d3.curve} [curve=d3.curveLinear]
* @returns {d3.curve|dc.lineChart}
*/
_chart.curve = function (curve) {
if (!arguments.length) {
return _curve;
}
_curve = curve;
return _chart;
};
/**
* Gets or sets the interpolator to use for lines drawn, by string name, allowing e.g. step
* functions, splines, and cubic interpolation.
*
* Possible values are: 'linear', 'linear-closed', 'step', 'step-before', 'step-after', 'basis',
* 'basis-open', 'basis-closed', 'bundle', 'cardinal', 'cardinal-open', 'cardinal-closed', and
* 'monotone'.
*
* This function exists for backward compatibility. Use {@link dc.lineChart#curve}
* which is generic and provides more options.
* Value set through `.curve` takes precedence over `.interpolate` and `.tension`.
* @method interpolate
* @memberof dc.lineChart
* @instance
* @deprecated since version 3.0 use {@link dc.lineChart#curve} instead
* @see {@link dc.lineChart#curve}
* @param {d3.curve} [interpolate=d3.curveLinear]
* @returns {d3.curve|dc.lineChart}
*/
_chart.interpolate = dc.logger.deprecate(function (interpolate) {
if (!arguments.length) {
return _interpolate;
}
_interpolate = interpolate;
return _chart;
}, 'dc.lineChart.interpolate has been deprecated since version 3.0 use dc.lineChart.curve instead');
/**
* Gets or sets the tension to use for lines drawn, in the range 0 to 1.
*
* Passed to the {@link https://github.com/d3/d3-shape/blob/master/README.md#curves d3 curve function}
* if it provides a `.tension` function. Example:
* {@link https://github.com/d3/d3-shape/blob/master/README.md#curveCardinal_tension curveCardinal.tension}.
*
* This function exists for backward compatibility. Use {@link dc.lineChart#curve}
* which is generic and provides more options.
* Value set through `.curve` takes precedence over `.interpolate` and `.tension`.
* @method tension
* @memberof dc.lineChart
* @instance
* @deprecated since version 3.0 use {@link dc.lineChart#curve} instead
* @see {@link dc.lineChart#curve}
* @param {Number} [tension=0]
* @returns {Number|dc.lineChart}
*/
_chart.tension = dc.logger.deprecate(function (tension) {
if (!arguments.length) {
return _tension;
}
_tension = tension;
return _chart;
}, 'dc.lineChart.tension has been deprecated since version 3.0 use dc.lineChart.curve instead');
/**
* Gets or sets a function that will determine discontinuities in the line which should be
* skipped: the path will be broken into separate subpaths if some points are undefined.
* This function is passed to
* {@link https://github.com/d3/d3-shape/blob/master/README.md#line_defined line.defined}
*
* Note: crossfilter will sometimes coerce nulls to 0, so you may need to carefully write
* custom reduce functions to get this to work, depending on your data. See
* {@link https://github.com/dc-js/dc.js/issues/615#issuecomment-49089248 this GitHub comment}
* for more details and an example.
* @method defined
* @memberof dc.lineChart
* @instance
* @see {@link https://github.com/d3/d3-shape/blob/master/README.md#line_defined line.defined}
* @param {Function} [defined]
* @returns {Function|dc.lineChart}
*/
_chart.defined = function (defined) {
if (!arguments.length) {
return _defined;
}
_defined = defined;
return _chart;
};
/**
* Set the line's d3 dashstyle. This value becomes the 'stroke-dasharray' of line. Defaults to empty
* array (solid line).
* @method dashStyle
* @memberof dc.lineChart
* @instance
* @see {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dasharray stroke-dasharray}
* @example
* // create a Dash Dot Dot Dot
* chart.dashStyle([3,1,1,1]);
* @param {Array<Number>} [dashStyle=[]]
* @returns {Array<Number>|dc.lineChart}
*/
_chart.dashStyle = function (dashStyle) {
if (!arguments.length) {
return _dashStyle;
}
_dashStyle = dashStyle;
return _chart;
};
/**
* Get or set render area flag. If the flag is set to true then the chart will render the area
* beneath each line and the line chart effectively becomes an area chart.
* @method renderArea
* @memberof dc.lineChart
* @instance
* @param {Boolean} [renderArea=false]
* @returns {Boolean|dc.lineChart}
*/
_chart.renderArea = function (renderArea) {
if (!arguments.length) {
return _renderArea;
}
_renderArea = renderArea;
return _chart;
};
function colors (d, i) {
return _chart.getColor.call(d, d.values, i);
}
// To keep it backward compatible, this covers multiple cases
// See https://github.com/dc-js/dc.js/issues/1376
// It will be removed when interpolate and tension are removed.
function getCurveFactory () {
var curve = null;
// _curve takes precedence
if (_curve) {
return _curve;
}
// Approximate the D3v3 behavior
if (typeof _interpolate === 'function') {
curve = _interpolate;
} else {
// If _interpolate is string
var mapping = {
'linear': d3.curveLinear,
'linear-closed': d3.curveLinearClosed,
'step': d3.curveStep,
'step-before': d3.curveStepBefore,
'step-after': d3.curveStepAfter,
'basis': d3.curveBasis,
'basis-open': d3.curveBasisOpen,
'basis-closed': d3.curveBasisClosed,
'bundle': d3.curveBundle,
'cardinal': d3.curveCardinal,
'cardinal-open': d3.curveCardinalOpen,
'cardinal-closed': d3.curveCardinalClosed,
'monotone': d3.curveMonotoneX
};
curve = mapping[_interpolate];
}
// Default value
if (!curve) {
curve = d3.curveLinear;
}
if (_tension !== null) {
if (typeof curve.tension !== 'function') {
dc.logger.warn('tension was specified but the curve/interpolate does not support it.');
} else {
curve = curve.tension(_tension);
}
}
return curve;
}
function drawLine (layersEnter, layers) {
var line = d3.line()
.x(function (d) {
return _chart.x()(d.x);
})
.y(function (d) {
return _chart.y()(d.y + d.y0);
})
.curve(getCurveFactory());
if (_defined) {
line.defined(_defined);
}
var path = layersEnter.append('path')
.attr('class', 'line')
.attr('stroke', colors);
if (_dashStyle) {
path.attr('stroke-dasharray', _dashStyle);
}
dc.transition(layers.select('path.line'), _chart.transitionDuration(), _chart.transitionDelay())
//.ease('linear')
.attr('stroke', colors)
.attr('d', function (d) {
return safeD(line(d.values));
});
}
function drawArea (layersEnter, layers) {
if (_renderArea) {
var area = d3.area()
.x(function (d) {
return _chart.x()(d.x);
})
.y1(function (d) {
return _chart.y()(d.y + d.y0);
})
.y0(function (d) {
return _chart.y()(d.y0);
})
.curve(getCurveFactory());
if (_defined) {
area.defined(_defined);
}
layersEnter.append('path')
.attr('class', 'area')
.attr('fill', colors)
.attr('d', function (d) {
return safeD(area(d.values));
});
dc.transition(layers.select('path.area'), _chart.transitionDuration(), _chart.transitionDelay())
//.ease('linear')
.attr('fill', colors)
.attr('d', function (d) {
return safeD(area(d.values));
});
}
}
function safeD (d) {
return (!d || d.indexOf('NaN') >= 0) ? 'M0,0' : d;
}
function drawDots (chartBody, layers) {
if (_chart.xyTipsOn() === 'always' || (!(_chart.brushOn() || _chart.parentBrushOn()) && _chart.xyTipsOn())) {
var tooltipListClass = TOOLTIP_G_CLASS + '-list';
var tooltips = chartBody.select('g.' + tooltipListClass);
if (tooltips.empty()) {
tooltips = chartBody.append('g').attr('class', tooltipListClass);
}
layers.each(function (d, layerIndex) {
var points = d.values;
if (_defined) {
points = points.filter(_defined);
}
var g = tooltips.select('g.' + TOOLTIP_G_CLASS + '._' + layerIndex);
if (g.empty()) {
g = tooltips.append('g').attr('class', TOOLTIP_G_CLASS + ' _' + layerIndex);
}
createRefLines(g);
var dots = g.selectAll('circle.' + DOT_CIRCLE_CLASS)
.data(points, dc.pluck('x'));
var dotsEnterModify = dots
.enter()
.append('circle')
.attr('class', DOT_CIRCLE_CLASS)
.attr('cx', function (d) {
return dc.utils.safeNumber(_chart.x()(d.x));
})
.attr('cy', function (d) {
return dc.utils.safeNumber(_chart.y()(d.y + d.y0));
})
.attr('r', getDotRadius())
.style('fill-opacity', _dataPointFillOpacity)
.style('stroke-opacity', _dataPointStrokeOpacity)
.attr('fill', _chart.getColor)
.attr('stroke', _chart.getColor)
.on('mousemove', function () {
var dot = d3.select(this);
showDot(dot);
showRefLines(dot, g);
})
.on('mouseout', function () {
var dot = d3.select(this);
hideDot(dot);
hideRefLines(g);
})
.merge(dots);
dotsEnterModify.call(renderTitle, d);
dc.transition(dotsEnterModify, _chart.transitionDuration())
.attr('cx', function (d) {
return dc.utils.safeNumber(_chart.x()(d.x));
})
.attr('cy', function (d) {
return dc.utils.safeNumber(_chart.y()(d.y + d.y0));
})
.attr('fill', _chart.getColor);
dots.exit().remove();
});
}
}
_chart.label(function (d) {
return dc.utils.printSingleValue(d.y0 + d.y);
}, false);
function drawLabels (layers) {
layers.each(function (d, layerIndex) {
var layer = d3.select(this);
var labels = layer.selectAll('text.lineLabel')
.data(d.values, dc.pluck('x'));
var labelsEnterModify = labels
.enter()
.append('text')
.attr('class', 'lineLabel')
.attr('text-anchor', 'middle')
.merge(labels);
dc.transition(labelsEnterModify, _chart.transitionDuration())
.attr('x', function (d) {
return dc.utils.safeNumber(_chart.x()(d.x));
})
.attr('y', function (d) {
var y = _chart.y()(d.y + d.y0) - LABEL_PADDING;
return dc.utils.safeNumber(y);
})
.text(function (d) {
return _chart.label()(d);
});
dc.transition(labels.exit(), _chart.transitionDuration())
.attr('height', 0)
.remove();
});
}
function createRefLines (g) {
var yRefLine = g.select('path.' + Y_AXIS_REF_LINE_CLASS).empty() ?
g.append('path').attr('class', Y_AXIS_REF_LINE_CLASS) : g.select('path.' + Y_AXIS_REF_LINE_CLASS);
yRefLine.style('display', 'none').attr('stroke-dasharray', '5,5');
var xRefLine = g.select('path.' + X_AXIS_REF_LINE_CLASS).empty() ?
g.append('path').attr('class', X_AXIS_REF_LINE_CLASS) : g.select('path.' + X_AXIS_REF_LINE_CLASS);
xRefLine.style('display', 'none').attr('stroke-dasharray', '5,5');
}
function showDot (dot) {
dot.style('fill-opacity', 0.8);
dot.style('stroke-opacity', 0.8);
dot.attr('r', _dotRadius);
return dot;
}
function showRefLines (dot, g) {
var x = dot.attr('cx');
var y = dot.attr('cy');
var yAxisX = (_chart._yAxisX() - _chart.margins().left);
var yAxisRefPathD = 'M' + yAxisX + ' ' + y + 'L' + (x) + ' ' + (y);
var xAxisRefPathD = 'M' + x + ' ' + _chart.yAxisHeight() + 'L' + x + ' ' + y;
g.select('path.' + Y_AXIS_REF_LINE_CLASS).style('display', '').attr('d', yAxisRefPathD);
g.select('path.' + X_AXIS_REF_LINE_CLASS).style('display', '').attr('d', xAxisRefPathD);
}
function getDotRadius () {
return _dataPointRadius || _dotRadius;
}
function hideDot (dot) {
dot.style('fill-opacity', _dataPointFillOpacity)
.style('stroke-opacity', _dataPointStrokeOpacity)
.attr('r', getDotRadius());
}
function hideRefLines (g) {
g.select('path.' + Y_AXIS_REF_LINE_CLASS).style('display', 'none');
g.select('path.' + X_AXIS_REF_LINE_CLASS).style('display', 'none');
}
function renderTitle (dot, d) {
if (_chart.renderTitle()) {
dot.select('title').remove();
dot.append('title').text(dc.pluck('data', _chart.title(d.name)));
}
}
/**
* Turn on/off the mouseover behavior of an individual data point which renders a circle and x/y axis
* dashed lines back to each respective axis. This is ignored if the chart
* {@link dc.coordinateGridMixin#brushOn brush} is on
* @method xyTipsOn
* @memberof dc.lineChart
* @instance
* @param {Boolean} [xyTipsOn=false]
* @returns {Boolean|dc.lineChart}
*/
_chart.xyTipsOn = function (xyTipsOn) {
if (!arguments.length) {
return _xyTipsOn;
}
_xyTipsOn = xyTipsOn;
return _chart;
};
/**
* Get or set the radius (in px) for dots displayed on the data points.
* @method dotRadius
* @memberof dc.lineChart
* @instance
* @param {Number} [dotRadius=5]
* @returns {Number|dc.lineChart}
*/
_chart.dotRadius = function (dotRadius) {
if (!arguments.length) {
return _dotRadius;
}
_dotRadius = dotRadius;
return _chart;
};
/**
* Always show individual dots for each datapoint.
*
* If `options` is falsy, it disables data point rendering. If no `options` are provided, the
* current `options` values are instead returned.
* @method renderDataPoints
* @memberof dc.lineChart
* @instance
* @example
* chart.renderDataPoints({radius: 2, fillOpacity: 0.8, strokeOpacity: 0.0})
* @param {{fillOpacity: Number, strokeOpacity: Number, radius: Number}} [options={fillOpacity: 0.8, strokeOpacity: 0.0, radius: 2}]
* @returns {{fillOpacity: Number, strokeOpacity: Number, radius: Number}|dc.lineChart}
*/
_chart.renderDataPoints = function (options) {
if (!arguments.length) {
return {
fillOpacity: _dataPointFillOpacity,
strokeOpacity: _dataPointStrokeOpacity,
radius: _dataPointRadius
};
} else if (!options) {
_dataPointFillOpacity = DEFAULT_DOT_OPACITY;
_dataPointStrokeOpacity = DEFAULT_DOT_OPACITY;
_dataPointRadius = null;
} else {
_dataPointFillOpacity = options.fillOpacity || 0.8;
_dataPointStrokeOpacity = options.strokeOpacity || 0.0;
_dataPointRadius = options.radius || 2;
}
return _chart;
};
function colorFilter (color, dashstyle, inv) {
return function () {
var item = d3.select(this);
var match = (item.attr('stroke') === color &&
item.attr('stroke-dasharray') === ((dashstyle instanceof Array) ?
dashstyle.join(',') : null)) || item.attr('fill') === color;
return inv ? !match : match;
};
}
_chart.legendHighlight = function (d) {
if (!_chart.isLegendableHidden(d)) {
_chart.g().selectAll('path.line, path.area')
.classed('highlight', colorFilter(d.color, d.dashstyle))
.classed('fadeout', colorFilter(d.color, d.dashstyle, true));
}
};
_chart.legendReset = function () {
_chart.g().selectAll('path.line, path.area')
.classed('highlight', false)
.classed('fadeout', false);
};
dc.override(_chart, 'legendables', function () {
var legendables = _chart._legendables();
if (!_dashStyle) {
return legendables;
}
return legendables.map(function (l) {
l.dashstyle = _dashStyle;
return l;
});
});
return _chart.anchor(parent, chartGroup);
};