/**
* Cartesian sprite.
*/
Ext.define('Ext.chart.series.sprite.Cartesian', {
extend: 'Ext.chart.series.sprite.Series',
inheritableStatics: {
def: {
processors: {
/**
* @cfg {Number} [selectionTolerance=20]
* The distance from the event position to the sprite's data points to trigger
* interactions (used for 'iteminfo', etc).
*/
selectionTolerance: 'number',
/**
* @cfg {Boolean} flipXY If flipXY is 'true', the series is flipped.
*/
flipXY: 'bool',
renderer: 'default',
// Visible range of data (pan/zoom) information.
visibleMinX: 'number',
visibleMinY: 'number',
visibleMaxX: 'number',
visibleMaxY: 'number',
innerWidth: 'number',
innerHeight: 'number'
},
defaults: {
selectionTolerance: 20,
flipXY: false,
renderer: null,
transformFillStroke: false,
visibleMinX: 0,
visibleMinY: 0,
visibleMaxX: 1,
visibleMaxY: 1,
innerWidth: 1,
innerHeight: 1
},
triggers: {
dataX: 'dataX,bbox',
dataY: 'dataY,bbox',
visibleMinX: 'panzoom',
visibleMinY: 'panzoom',
visibleMaxX: 'panzoom',
visibleMaxY: 'panzoom',
innerWidth: 'panzoom',
innerHeight: 'panzoom'
},
updaters: {
dataX: function(attr) {
this.processDataX();
this.scheduleUpdater(attr, 'dataY', ['dataY']);
},
dataY: function() {
this.processDataY();
},
panzoom: function(attr) {
// dx, dy are deltas between min & max of coordinated data values.
var dx = attr.visibleMaxX - attr.visibleMinX,
dy = attr.visibleMaxY - attr.visibleMinY,
innerWidth = attr.flipXY ? attr.innerHeight : attr.innerWidth,
innerHeight = !attr.flipXY ? attr.innerHeight : attr.innerWidth,
surface = this.getSurface(),
isRtl = surface ? surface.getInherited().rtl : false;
attr.scalingCenterX = 0;
attr.scalingCenterY = 0;
attr.scalingX = innerWidth / dx;
attr.scalingY = innerHeight / dy;
// (attr.visibleMinY * attr.scalingY) will be the vertical position of
// our minimum data points, which we want to be at zero, so we offset
// by this amount.
attr.translationX = -(attr.visibleMinX * attr.scalingX);
attr.translationY = -(attr.visibleMinY * attr.scalingY);
if (isRtl && !attr.flipXY) {
attr.scalingX *= -1;
attr.translationX *= -1;
attr.translationX += innerWidth;
}
this.applyTransformations(true);
}
}
}
},
processDataY: Ext.emptyFn,
processDataX: Ext.emptyFn,
updatePlainBBox: function(plain) {
var attr = this.attr;
plain.x = attr.dataMinX;
plain.y = attr.dataMinY;
plain.width = attr.dataMaxX - attr.dataMinX;
plain.height = attr.dataMaxY - attr.dataMinY;
},
/**
* Does a binary search of the data on the x-axis using the given key.
* @param {String} key
* @return {*}
*/
binarySearch: function(key) {
var dx = this.attr.dataX,
start = 0,
end = dx.length,
mid, val;
if (key <= dx[0]) {
return start;
}
if (key >= dx[end - 1]) {
return end - 1;
}
while (start + 1 < end) {
mid = (start + end) >> 1;
val = dx[mid];
if (val === key) {
return mid;
}
else if (val < key) {
start = mid;
}
else {
end = mid;
}
}
return start;
},
render: function(surface, ctx, surfaceClipRect) {
var me = this,
attr = me.attr,
margin = 1, // TODO: why do we need it?
inverseMatrix = attr.inverseMatrix.clone(),
dataClipRect;
// The sprite's `attr.matrix` is stretching/shrinking data coordinates
// to surface coordinates.
// This matrix is set (indirectly) by the 'panzoom' updater.
// The sprite's `attr.inverseMatrix` does the opposite.
//
// The `surface.matrix` of the 'series' surface of a cartesian chart flips the
// surface content vertically, so that y=0 is at the bottom (look for
// `surface.matrix.set` call in the CartesianChart.performLayout method).
// This matrix is set in the 'performLayout' of the CartesianChart.
// The `surface.inverseMatrix` flips the content back.
//
// By combining the inverse matrices of the series surface and the series sprite,
// we essentially get a transformation that allows us to go from surface coordinates
// in a final flipped drawing back to data points.
//
// For example
//
// inverseMatrix.transformPoint([ 0, rect[3] ])
// inverseMatrix.transformPoint([ rect[2], 0 ])
//
// will return
//
// [attr.dataMinX, attr.dataMinY]
// [attr.dataMaxX, attr.dataMaxY]
//
// because left/bottom and top/right of the series surface is where the first smallest
// and last largest data points would be (given no pan/zoom), respectively.
//
// So the `dataClipRect` passed to the `renderClipped` call below is effectively
// the visible rect in data (not surface!) coordinates.
// It is important to note, that the all the scaling and translation is defined
// by the sprite's matrix, the 'series' surface matrix does not contain scaling
// or translation components, except for the vertical flipping.
// This is important because there is a common pattern in chart series sprites
// (MarkerHolders) - instead of using transform attributes for their Markers
// (e.g. instances of a 'rect' sprite in case of 'bar' series), the attributes
// that would position a sprite with no transformations are transformed.
// For example, to draw a rect with coordinates TL(10, 10), BR(20, 40),
// we could use the folling 'rect' sprite attributes:
//
// {
// x: 0,
// y: 0
// width: 10,
// height: 30
//
// translationX: 10,
// translationY: 10
//
// But the correct thing to do here is
//
// {
// x: 10,
// y: 10,
// width: 10,
// height: 30
// }
//
// Similarly, if the sprite was scaled, the 'x', 'y', 'width', 'height' attributes
// would have to account for that as well.
//
// This is done, so that the attribute values a marker gets by the time it renders,
// are the final values, and are not affected later by other transforms, such as
// surface matrix scaling, which could ruin the visual result, if the attributes
// values are doctored to make lines align to the pixel grid (which is typically
// the case).
inverseMatrix.appendMatrix(surface.inverseMatrix);
if (attr.dataX === null || attr.dataX === undefined) {
return;
}
if (attr.dataY === null || attr.dataY === undefined) {
return;
}
if (inverseMatrix.getXX() * inverseMatrix.getYX() ||
inverseMatrix.getXY() * inverseMatrix.getYY()) {
Ext.Logger.warn('Cartesian Series sprite does not support rotation/sheering');
return;
}
dataClipRect = inverseMatrix.transformList([
[surfaceClipRect[0] - margin, surfaceClipRect[3] + margin], // (left, height)
[surfaceClipRect[0] + surfaceClipRect[2] + margin, -margin] // (width, top)
]);
dataClipRect = dataClipRect[0].concat(dataClipRect[1]);
// TODO: RTL improvements:
// TODO: produce such a dataClipRect here, so that we don't have to do:
// TODO: min = Math.min(dataClipRect[0], dataClipRect[2])
// TODO: max = Math.max(dataClipRect[0], dataClipRect[2])
// TODO: inside each 'renderClipped' call
me.renderClipped(surface, ctx, dataClipRect, surfaceClipRect);
},
/**
* Render the given visible clip range.
* @param {Ext.draw.Surface} surface A draw container surface.
* @param {CanvasRenderingContext2D} ctx A context object that is API compatible with the native
* [CanvasRenderingContext2D](https://developer.mozilla.org/en/docs/Web/API/CanvasRenderingContext2D).
* @param {Number[]} dataClipRect The clip rect in data coordinates, roughly equivalent to
* [attr.dataMinX, attr.dataMinY, attr.dataMaxX, attr.dataMaxY] for an untranslated/unscaled
* surface/sprite.
* @param {Number[]} surfaceClipRect The clip rect in surface coordinates:
* [left, top, width, height].
* @method
*/
renderClipped: Ext.emptyFn,
/**
* Get the nearest item index from point (x, y). -1 as not found.
* @param {Number} x
* @param {Number} y
* @return {Number} The index
* @deprecated 6.5.2 Use {@link #getNearestDataPoint} instead.
*/
getIndexNearPoint: function(x, y) {
var result = this.getNearestDataPoint(x, y);
return result ? result.index : -1;
},
/**
* Given a point in 'series' surface element coordinates, returns the `index` of the
* sprite's data point that is nearest to that point, along with the `distance`
* between points.
* If the `selectionTolerance` attribute of the sprite is not zero, only the data points
* that are within that pixel distance from the given point will be checked.
* In the event no such data points exist or the data is empty, `null` is returned.
*
* Notes:
* 1) given a mouse/pointer event object, the surface coordinates of the event can be
* obtained with the `getEventXY` method of the chart;
* 2) using `selectionTolerance` of zero is useful for series with no visible markers,
* such as the Area series, where this attribute becomes meaningless.
*
* @param {Number} x
* @param {Number} y
* @return {Object}
*/
getNearestDataPoint: function(x, y) {
var me = this,
attr = me.attr,
series = me.getSeries(),
surface = me.getSurface(),
items = me.boundMarkers.items,
matrix = attr.matrix,
dataX = attr.dataX,
dataY = attr.dataY,
selectionTolerance = attr.selectionTolerance,
minDistance = Infinity,
index = -1,
result = null,
distance, dx, dy, xy, i, ln, end, inc, bbox;
// Notes:
// Instead of converting the given point from surface coordinates to data coordinates
// and then measuring the distances between it and the data points, we have to
// convert all the data points to surface coordinates and measure the distances
// between them and the given point. This is because the data coordinates can use
// different scales, which makes distance measurement impossible.
// For example, if the x-axis is a `category` axis, the categories will be assigned
// indexes starting from 0, that's what the `attr.dataX` array will contain;
// and if the y-axis is a `numeric` axis, the `attr.dataY` array will simply contain
// the original values.
//
// Either 'items' or 'markers' will be highlighted. If a sprite has both (for example,
// 'bar' series with the 'marker' config, where the bars are 'items' and marker instances
// are 'markers'), only the 'items' (bars) will be highlighted.
if (items) {
ln = dataX.length;
if (series.reversedSpriteZOrder) {
i = ln - 1;
end = -1;
inc = -1;
}
else {
i = 0;
end = ln;
inc = 1;
}
for (; i !== end; i += inc) {
bbox = me.getMarkerBBox('items', i);
// Transform the given surface element coordinates to logical coordinates
// of the surface (the ones the bbox uses).
xy = surface.inverseMatrix.transformPoint([x, y]);
if (Ext.draw.Draw.isPointInBBox(xy[0], xy[1], bbox)) {
index = i;
minDistance = 0;
// Return the first item that contains our touch point.
break;
}
}
}
else { // markers
for (i = 0, ln = dataX.length; i < ln; i++) {
// Convert from data coordinates to coordinates within inner size rectangle.
// See `panzoom` method for more details.
xy = matrix.transformPoint([dataX[i], dataY[i]]);
// Flip back vertically and padding adjust (see `render` method comments).
xy = surface.matrix.transformPoint(xy);
// Essentially sprites go through the same two transformations when they render
// data points.
dx = x - xy[0];
dy = y - xy[1];
distance = Math.sqrt(dx * dx + dy * dy);
if (selectionTolerance && distance > selectionTolerance) {
continue;
}
if (distance < minDistance) {
minDistance = distance;
index = i;
// Keep looking for the nearest marker.
}
}
}
if (index > -1) {
result = {
index: index,
distance: minDistance
};
}
return result;
}
});