extjs-gpl
Version:
GPL licensed version of Sencha Ext JS
673 lines (609 loc) • 24.5 kB
JavaScript
/**
* @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 {Boolean} [smooth=false]
* `true` if the sprite uses line smoothing.
* Line smoothing only works with gapless data.
*/
smooth: 'bool',
/**
* @cfg {Boolean} [fillArea=false]
* `true` if the sprite paints the area underneath the line.
*/
fillArea: 'bool',
/**
* @cfg {Boolean} [step=false]
* `true` if the line uses steps instead of straight lines to connect the dots.
* It is ignored if `smooth` is `true`.
*/
step: '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.
*/
yCap: 'default' // The 'default' processor is used here as we don't want this attribute to animate.
},
defaults: {
smooth: false,
nullStyle: 'connect',
fillArea: false,
step: false,
preciseStroke: true,
xAxis: null,
yCap: Math.pow(2, 20),
yJump: 50
},
triggers: {
dataX: 'dataX,bbox,smooth',
dataY: 'dataY,bbox,smooth',
smooth: 'smooth'
},
updaters: {
smooth: function (attr) {
var dataX = attr.dataX,
dataY = attr.dataY;
if (attr.smooth && dataX && dataY && dataX.length > 2 && dataY.length > 2) {
this.smoothX = Ext.draw.Draw.spline(dataX);
this.smoothY = Ext.draw.Draw.spline(dataY);
} else {
delete this.smoothX;
delete this.smoothY;
}
}
}
}
},
list: null,
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) {
ctx.moveTo(strip[0], strip[1]);
for (var 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,
step = attr.step,
needMoveTo = true,
ln = list.length,
lineConfig = {
type: 'line',
smooth: false,
step: step
};
var 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.
var 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.attr.smooth && 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;
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++) {
var 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) {
var 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);
}
}
}
}
});