dc.js Source: line-chart.js (original) (raw)
/**
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} [dashStyle=[]]
- @returns {Array|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 nooptions
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);
};