/**
* @class Ext.chart.series.sprite.Line
* @extends Ext.chart.series.sprite.Aggregative
*
* Line series sprite.
*/
Ext.define('Ext.chart.series.sprite.Line', {
alias: 'sprite.lineSeries',
extend: 'Ext.chart.series.sprite.Aggregative',
inheritableStatics: {
def: {
processors: {
/**
* @cfg {Object} [curve={type: 'linear'}]
* The type of curve that connects the data points.
*
* For example:
*
* // The data points are connected by line segments.
* // This is the default setting.
* curve: {
* type: 'linear'
* }
*
* // Cardinal spline interpolation is used to produce the curve
* // that connects the data points. The `tension` parameter can
* // be used to control the smoothness of the curve. A tension
* // of 0 corresponds to infinite tension, which results in straight
* // lines between data points. A tension of 1 corresponds to
* // no tension, allowing the spline to take the path of least
* // total bend. With tension values greater than 1, the curve
* // behaves like a compressed spring, pushed to take a longer path.
* // A cardinal spline with a tension of 0.5 is a special case.
* // It is then called a Catmull-Rom spline. Catmull-Rom splines are
* // thought to be esthetically pleasing and are quite common.
* // Note: spline interpolation only works on gapless data.
* curve: {
* type: 'cardinal,
* tension: 0.5
* }
*
* // Produces a natural cubic spline with the second derivative
* // of the spline set to zero at the endpoints.
* curve: {
* type: 'natural'
* }
*
* // The data points are connected by alternating horizontal and
* // vertical lines. The y-value changes after the x-value.
* curve: {
* type: 'step-after'
* }
*
*/
curve: 'default',
/**
* @cfg {Boolean} [fillArea=false]
* `true` if the sprite paints the area underneath the line.
*/
fillArea: 'bool',
/**
* @cfg {"gap"/"connect"/"origin"} [nullStyle="gap"]
* Possible values:
* 'gap' - null points are rendered as gaps.
* 'connect' - non-null points are connected across null points, so that
* there is no gap, unless null points are at the beginning/end of the line.
* Only the visible data points are connected - if a visible data point
* is followed by a series of null points that go off screen and eventually
* terminate with a non-null point, the connection won't be made.
* 'origin' - null data points are rendered at the origin,
* which is the y-coordinate of a point where the x and y axes meet.
* This requires that at least the x-coordinate of a point is a valid value.
*/
nullStyle: 'enums(gap,connect,origin)',
/**
* @cfg {Boolean} [preciseStroke=true]
* `true` if the line uses precise stroke.
*/
preciseStroke: 'bool',
/**
* @private
* The x-axis associated with the Line series.
* We need to know the position of the x-axis to fill the area underneath
* the stroke properly.
*/
xAxis: 'default',
/**
* @cfg {Number} [yCap=Math.pow(2, 20)]
* Absolute maximum y-value.
* Larger values will be capped to avoid rendering issues.
*/
// The 'default' processor is used here as we don't want this attribute to animate.
yCap: 'default'
},
defaults: {
curve: {
type: 'linear'
},
nullStyle: 'connect',
fillArea: false,
preciseStroke: true,
xAxis: null,
yCap: Math.pow(2, 20),
yJump: 50
},
triggers: {
dataX: 'dataX,bbox,curve',
dataY: 'dataY,bbox,curve',
curve: 'curve'
},
updaters: {
curve: 'curveUpdater'
}
}
},
list: null,
curveUpdater: function(attr) {
var me = this,
dataX = attr.dataX,
dataY = attr.dataY,
curve = attr.curve,
smoothable = dataX && dataY && dataX.length > 2 && dataY.length > 2,
type = curve.type;
if (smoothable) {
if (type === 'natural') {
me.smoothX = Ext.draw.Draw.naturalSpline(dataX);
me.smoothY = Ext.draw.Draw.naturalSpline(dataY);
}
else if (type === 'cardinal') {
me.smoothX = Ext.draw.Draw.cardinalSpline(dataX, curve.tension);
me.smoothY = Ext.draw.Draw.cardinalSpline(dataY, curve.tension);
}
else {
smoothable = false;
}
}
if (!smoothable) {
delete me.smoothX;
delete me.smoothY;
}
},
updatePlainBBox: function(plain) {
var attr = this.attr,
ymin = Math.min(0, attr.dataMinY),
ymax = Math.max(0, attr.dataMaxY);
plain.x = attr.dataMinX;
plain.y = ymin;
plain.width = attr.dataMaxX - attr.dataMinX;
plain.height = ymax - ymin;
},
drawStrip: function(ctx, strip) {
var i, ln;
ctx.moveTo(strip[0], strip[1]);
for (i = 2, ln = strip.length; i < ln; i += 2) {
ctx.lineTo(strip[i], strip[i + 1]);
}
},
drawStraightStroke: function(surface, ctx, start, end, list, xAxis) {
var me = this,
attr = me.attr,
nullStyle = attr.nullStyle,
isConnect = nullStyle === 'connect',
isOrigin = nullStyle === 'origin',
renderer = attr.renderer,
curve = attr.curve,
step = curve.type === 'step-after',
needMoveTo = true,
ln = list.length,
lineConfig = {
type: 'line',
smooth: false,
step: step
},
rendererChanges, params, stripStartX,
isValidX0, isValidX, isValidX1,
isValidPoint0, isValidPoint, isValidPoint1,
isGap, lastValidPoint, px, py,
x, y, x0, y0, x1, y1, i,
// 'strip' stores last continuous segment of the stroke,
// which we may need to re-build, if there's a fill as well.
// For example, if the renderer returned a style that needs
// to be applied to the current step, or we reached a null
// point in the data, where we have to fill the current continuous
// segment, we build and close a path that will be filled, then
// re-build the stroke path, using coordinates saved in the 'strip',
// and render the stroke on top of the fill.
strip = [];
ctx.beginPath();
for (i = 3; i < ln; i += 3) {
x0 = list[i - 3];
y0 = list[i - 2];
x = list[i];
y = list[i + 1];
x1 = list[i + 3];
y1 = list[i + 4];
isValidX0 = Ext.isNumber(x0);
isValidX = Ext.isNumber(x);
isValidX1 = Ext.isNumber(x1);
isValidPoint0 = isValidX0 && Ext.isNumber(y0);
isValidPoint = isValidX && Ext.isNumber(y);
isValidPoint1 = isValidX1 && Ext.isNumber(y1);
if (isOrigin) {
// If only the y-component isn't a valid number,
// we can 'fix' it by setting it to value of y-origin.
if (!isValidPoint0 && isValidX0) {
y0 = xAxis;
isValidPoint0 = true;
}
if (!isValidPoint && isValidX) {
y = xAxis;
isValidPoint = true;
}
if (!isValidPoint1 && isValidX1) {
y1 = xAxis;
isValidPoint1 = true;
}
}
if (renderer) {
lineConfig.x = x;
lineConfig.y = y;
lineConfig.x0 = x0;
lineConfig.y0 = y0;
params = [me, lineConfig, me.rendererData, start + i / 3];
// callback(fn, scope, args, delay, caller)
rendererChanges = Ext.callback(renderer, null, params, 0, me.getSeries());
}
if (isGap && isConnect && isValidPoint0 && lastValidPoint) {
px = lastValidPoint[0];
py = lastValidPoint[1];
if (needMoveTo) {
ctx.beginPath();
ctx.moveTo(px, py);
strip.push(px, py);
stripStartX = px;
needMoveTo = false;
}
if (step) {
ctx.lineTo(x0, py);
strip.push(x0, py);
}
ctx.lineTo(x0, y0);
strip.push(x0, y0);
lastValidPoint = [x0, y0];
isGap = false;
}
// Special case where we have an uninterrupted segment, followed
// by a gap, then a valid point, then another gap. The uninterrupted
// segment should be connenected with the dot situated between the gaps.
if (isConnect && lastValidPoint && isValidPoint && !isValidPoint0) {
x0 = lastValidPoint[0];
y0 = lastValidPoint[1];
isValidPoint0 = true;
}
// Remember last valid point to connect the gap
// when the next valid point is encountered.
if (isValidPoint) {
lastValidPoint = [x, y];
}
if (isValidPoint0 && isValidPoint) {
if (needMoveTo) {
ctx.beginPath();
ctx.moveTo(x0, y0);
strip.push(x0, y0);
stripStartX = x0;
needMoveTo = false;
}
}
else {
isGap = true;
continue;
}
if (step) {
ctx.lineTo(x, y0);
strip.push(x, y0);
}
ctx.lineTo(x, y);
strip.push(x, y);
// If the next point is a gap, then we need to fill what
// has been already rendered so far. The same applies
// if the renderer returned some changes to apply to
// the current step.
if (rendererChanges || !isValidPoint1) {
ctx.save();
Ext.apply(ctx, rendererChanges);
rendererChanges = null;
if (attr.fillArea) {
ctx.lineTo(x, xAxis);
ctx.lineTo(stripStartX, xAxis);
ctx.closePath();
ctx.fill();
}
// Draw the line on top of the filled area.
ctx.beginPath();
me.drawStrip(ctx, strip);
strip = [];
ctx.stroke();
ctx.restore();
ctx.beginPath();
// Take note that the starting point of a path has been reset
// (as a result of filling a sub-path) and needs to be set again
// for the line to continue in a proper manner.
needMoveTo = true;
}
}
},
calculateScale: function(count, end) {
var power = 0,
n = count;
while (n < end && count > 0) {
power++;
n += count >> power;
}
return Math.pow(2, power > 0 ? power - 1 : power);
},
drawSmoothStroke: function(surface, ctx, start, end, list, xAxis) {
var me = this,
attr = me.attr,
step = attr.step,
matrix = attr.matrix,
renderer = attr.renderer,
xx = matrix.getXX(),
yy = matrix.getYY(),
dx = matrix.getDX(),
dy = matrix.getDY(),
smoothX = me.smoothX,
smoothY = me.smoothY,
scale = me.calculateScale(attr.dataX.length, end),
cx1, cy1, cx2, cy2, x, y, x0, y0,
i, j, changes, params,
lineConfig = {
type: 'line',
smooth: true,
step: step
};
ctx.beginPath();
ctx.moveTo(smoothX[start * 3] * xx + dx, smoothY[start * 3] * yy + dy);
for (i = 0, j = start * 3 + 1; i < list.length - 3; i += 3, j += 3 * scale) {
cx1 = smoothX[j] * xx + dx;
cy1 = smoothY[j] * yy + dy;
cx2 = smoothX[j + 1] * xx + dx;
cy2 = smoothY[j + 1] * yy + dy;
x = surface.roundPixel(list[i + 3]);
y = list[i + 4];
x0 = surface.roundPixel(list[i]);
y0 = list[i + 1];
if (renderer) {
lineConfig.x0 = x0;
lineConfig.y0 = y0;
lineConfig.cx1 = cx1;
lineConfig.cy1 = cy1;
lineConfig.cx2 = cx2;
lineConfig.cy2 = cy2;
lineConfig.x = x;
lineConfig.y = y;
params = [me, lineConfig, me.rendererData, start + i / 3 + 1];
changes = Ext.callback(renderer, null, params, 0, me.getSeries());
ctx.save();
Ext.apply(ctx, changes);
}
if (attr.fillArea) {
ctx.moveTo(x0, y0);
ctx.bezierCurveTo(cx1, cy1, cx2, cy2, x, y);
ctx.lineTo(x, xAxis);
ctx.lineTo(x0, xAxis);
ctx.lineTo(x0, y0);
ctx.closePath();
ctx.fill();
ctx.beginPath();
}
// Draw the line on top of the filled area.
ctx.moveTo(x0, y0);
ctx.bezierCurveTo(cx1, cy1, cx2, cy2, x, y);
ctx.stroke();
ctx.moveTo(x0, y0);
ctx.closePath();
if (renderer) {
ctx.restore();
}
ctx.beginPath();
ctx.moveTo(x, y);
}
// Prevent the last visible segment from being stroked twice
// (second time by the ctx.fillStroke inside Path sprite 'render' method)
ctx.beginPath();
},
drawLabel: function(text, dataX, dataY, labelId, rect) {
var me = this,
attr = me.attr,
label = me.getMarker('labels'),
labelTpl = label.getTemplate(),
labelCfg = me.labelCfg || (me.labelCfg = {}),
surfaceMatrix = me.surfaceMatrix,
labelX, labelY,
labelOverflowPadding = attr.labelOverflowPadding,
halfHeight, labelBBox,
changes, params, hasPendingChanges;
// The coordinates below (data point converted to surface coordinates)
// are just for the renderer to give it a notion of where the label will be positioned.
// The actual position of the label will be different
// (unless the renderer returns x/y coordinates in the changes object)
// and depend on several things including the size of the text,
// which has to be measured after the renderer call,
// since text can be modified by the renderer.
labelCfg.x = surfaceMatrix.x(dataX, dataY);
labelCfg.y = surfaceMatrix.y(dataX, dataY);
if (attr.flipXY) {
labelCfg.rotationRads = Math.PI * 0.5;
}
else {
labelCfg.rotationRads = 0;
}
labelCfg.text = text;
if (labelTpl.attr.renderer) {
params = [text, label, labelCfg, me.rendererData, labelId];
changes = Ext.callback(labelTpl.attr.renderer, null, params, 0, me.getSeries());
if (typeof changes === 'string') {
labelCfg.text = changes;
}
else if (typeof changes === 'object') {
if ('text' in changes) {
labelCfg.text = changes.text;
}
hasPendingChanges = true;
}
}
labelBBox = me.getMarkerBBox('labels', labelId, true);
if (!labelBBox) {
me.putMarker('labels', labelCfg, labelId);
labelBBox = me.getMarkerBBox('labels', labelId, true);
}
halfHeight = labelBBox.height / 2;
labelX = dataX;
switch (labelTpl.attr.display) {
case 'under':
labelY = dataY - halfHeight - labelOverflowPadding;
break;
case 'rotate':
labelX += labelOverflowPadding;
labelY = dataY - labelOverflowPadding;
labelCfg.rotationRads = -Math.PI / 4;
break;
default: // 'over'
labelY = dataY + halfHeight + labelOverflowPadding;
}
labelCfg.x = surfaceMatrix.x(labelX, labelY);
labelCfg.y = surfaceMatrix.y(labelX, labelY);
if (hasPendingChanges) {
Ext.apply(labelCfg, changes);
}
me.putMarker('labels', labelCfg, labelId);
},
drawMarker: function(x, y, index) {
var me = this,
attr = me.attr,
renderer = attr.renderer,
surfaceMatrix = me.surfaceMatrix,
markerCfg = {},
changes, params;
if (renderer && me.getMarker('markers')) {
markerCfg.type = 'marker';
markerCfg.x = x;
markerCfg.y = y;
params = [me, markerCfg, me.rendererData, index];
changes = Ext.callback(renderer, null, params, 0, me.getSeries());
if (changes) {
Ext.apply(markerCfg, changes);
}
}
markerCfg.translationX = surfaceMatrix.x(x, y);
markerCfg.translationY = surfaceMatrix.y(x, y);
delete markerCfg.x;
delete markerCfg.y;
me.putMarker('markers', markerCfg, index, !renderer);
},
drawStroke: function(surface, ctx, start, end, list, xAxis) {
var me = this,
isSmooth = me.smoothX && me.smoothY;
if (isSmooth) {
me.drawSmoothStroke(surface, ctx, start, end, list, xAxis);
}
else {
me.drawStraightStroke(surface, ctx, start, end, list, xAxis);
}
},
renderAggregates: function(aggregates, start, end, surface, ctx, clip, rect) {
var me = this,
attr = me.attr,
dataX = attr.dataX,
dataY = attr.dataY,
labels = attr.labels,
xAxis = attr.xAxis,
yCap = attr.yCap,
isSmooth = attr.smooth && me.smoothX && me.smoothY,
isDrawLabels = labels && me.getMarker('labels'),
isDrawMarkers = me.getMarker('markers'),
matrix = attr.matrix,
pixel = surface.devicePixelRatio,
xx = matrix.getXX(),
yy = matrix.getYY(),
dx = matrix.getDX(),
dy = matrix.getDY(),
list = me.list || (me.list = []),
minXs = aggregates.minX,
maxXs = aggregates.maxX,
minYs = aggregates.minY,
maxYs = aggregates.maxY,
idx = aggregates.startIdx,
isContinuousLine = true,
isValidMinX, isValidMaxX,
isValidMinY, isValidMaxY,
xAxisOrigin, isVerticalX,
x, y, i, index, minX, maxX, minY, maxY,
lastPointX, lastPointY, firstPointX, firstPointY;
me.rendererData = { store: me.getStore() };
list.length = 0;
// Say we have 7 y-items (attr.dataY): [20, 19, 17, 15, 11, 10, 14]
// and 7 x-items (attr.dataX): [0, 1, 2, 3, 4, 5, 6].
// Then aggregates.startIdx is an aggregated index,
// where every other item is skipped on each aggregation level:
// [0, 1, 2, 3, 4, 5, 6,
// 0, 2, 4, 6,
// 0, 4,
// 0]
// aggregates.minY
// [20, 19, 17, 15, 11, 10, 14,
// 19, 15, 10, 14,
// 15, 10,
// 10]
// aggregates.maxY
// [20, 19, 17, 15, 11, 10, 14,
// 20, 17, 11, 14,
// 20, 14,
// 20]
// aggregates.minX is
// [0, 1, 2, 3, 4, 5, 6,
// 1, 3, 5, 6, // TODO: why this order for min?
// 3, 5, // TODO: why this inconsistency?
// 5]
// aggregates.maxX is
// [0, 1, 2, 3, 4, 5, 6,
// 0, 2, 4, 6,
// 0, 6,
// 0]
// Create a list of the form [x0, y0, idx0, x1, y1, idx1, ...],
// where each x,y pair is a coordinate representing original data point
// at the idx position.
for (i = start; i < end; i++) {
minX = minXs[i];
maxX = maxXs[i];
minY = minYs[i];
maxY = maxYs[i];
isValidMinX = Ext.isNumber(minX);
isValidMinY = Ext.isNumber(minY);
isValidMaxX = Ext.isNumber(maxX);
isValidMaxY = Ext.isNumber(maxY);
if (minX < maxX) {
list.push(
isValidMinX ? (minX * xx + dx) : null,
isValidMinY ? (minY * yy + dy) : null,
idx[i]
);
list.push(
isValidMaxX ? (maxX * xx + dx) : null,
isValidMaxY ? (maxY * yy + dy) : null,
idx[i]
);
}
else if (minX > maxX) {
list.push(
isValidMaxX ? (maxX * xx + dx) : null,
isValidMaxY ? (maxY * yy + dy) : null,
idx[i]
);
list.push(
isValidMinX ? (minX * xx + dx) : null,
isValidMinY ? (minY * yy + dy) : null,
idx[i]
);
}
else {
list.push(
isValidMaxX ? (maxX * xx + dx) : null,
isValidMaxY ? (maxY * yy + dy) : null,
idx[i]
);
}
}
if (list.length) {
for (i = 0; i < list.length; i += 3) {
x = list[i];
y = list[i + 1];
if (Ext.isNumber(x) && Ext.isNumber(y)) {
if (y > yCap) {
y = yCap;
}
else if (y < -yCap) {
y = -yCap;
}
list[i + 1] = y;
}
else {
isContinuousLine = false;
continue;
}
index = list[i + 2];
if (isDrawMarkers) {
me.drawMarker(x, y, index);
}
if (isDrawLabels && labels[index]) {
me.drawLabel(labels[index], x, y, index, rect);
}
}
me.isContinuousLine = isContinuousLine;
if (isSmooth && !isContinuousLine) {
Ext.raise("Line smoothing in only supported for gapless data, " +
"where all data points are finite numbers.");
}
if (xAxis) {
isVerticalX = xAxis.getAlignment() === 'vertical';
if (Ext.isNumber(xAxis.floatingAtCoord)) {
xAxisOrigin = (isVerticalX ? rect[2] : rect[3]) - xAxis.floatingAtCoord;
}
else {
xAxisOrigin = isVerticalX ? rect[0] : rect[1];
}
}
else {
xAxisOrigin = attr.flipXY ? rect[0] : rect[1];
}
if (attr.preciseStroke) {
if (attr.fillArea) {
ctx.fill();
}
if (attr.transformFillStroke) {
attr.inverseMatrix.toContext(ctx);
}
me.drawStroke(surface, ctx, start, end, list, xAxisOrigin);
if (attr.transformFillStroke) {
attr.matrix.toContext(ctx);
}
ctx.stroke();
}
else {
me.drawStroke(surface, ctx, start, end, list, xAxisOrigin);
if (isContinuousLine && isSmooth && attr.fillArea && !attr.renderer) {
lastPointX = dataX[dataX.length - 1] * xx + dx + pixel;
lastPointY = dataY[dataY.length - 1] * yy + dy;
firstPointX = dataX[0] * xx + dx - pixel;
firstPointY = dataY[0] * yy + dy;
// Fill the area from the series to the xAxis in case there
// are no gaps and no renderer is used, in which case the
// area would be filled per uninterrupted segment or per
// step, instead of being filled a single pass.
ctx.lineTo(lastPointX, lastPointY);
ctx.lineTo(lastPointX, xAxisOrigin - attr.lineWidth);
ctx.lineTo(firstPointX, xAxisOrigin - attr.lineWidth);
ctx.lineTo(firstPointX, firstPointY);
}
if (attr.transformFillStroke) {
attr.matrix.toContext(ctx);
}
// Prevent the reverse transform to fix floating point error.
if (attr.fillArea) {
ctx.fillStroke(attr, true);
}
else {
ctx.stroke(true);
}
}
}
}
});