/**
* The Crosshair interaction allows the user to get precise values for a specific point
* on the chart. The values are obtained by single-touch dragging on the chart.
*
* @example
* Ext.create('Ext.Container', {
* renderTo: Ext.getBody(),
* width: 600,
* height: 400,
* layout: 'fit',
* items: {
* xtype: 'cartesian',
* innerPadding: 20,
* interactions: {
* type: 'crosshair',
* axes: {
* left: {
* label: {
* fillStyle: 'white'
* },
* rect: {
* fillStyle: 'brown',
* radius: 6
* }
* },
* bottom: {
* label: {
* fontSize: '14px',
* fontWeight: 'bold'
* }
* }
* },
* lines: {
* horizontal: {
* strokeStyle: 'brown',
* lineWidth: 2,
* lineDash: [20, 2, 2, 2, 2, 2, 2, 2]
* }
* }
* },
* store: {
* fields: ['name', 'data'],
* data: [
* {name: 'apple', data: 300},
* {name: 'orange', data: 900},
* {name: 'banana', data: 800},
* {name: 'pear', data: 400},
* {name: 'grape', data: 500}
* ]
* },
* axes: [{
* type: 'numeric',
* position: 'left',
* fields: ['data'],
* title: {
* text: 'Value',
* fontSize: 15
* },
* grid: true,
* label: {
* rotationRads: -Math.PI / 4
* }
* }, {
* type: 'category',
* position: 'bottom',
* fields: ['name'],
* title: {
* text: 'Category',
* fontSize: 15
* }
* }],
* series: {
* type: 'line',
* style: {
* strokeStyle: 'black'
* },
* xField: 'name',
* yField: 'data',
* marker: {
* type: 'circle',
* radius: 5,
* fillStyle: 'lightblue'
* }
* }
* }
* });
*/
Ext.define('Ext.chart.interactions.Crosshair', {
extend: 'Ext.chart.interactions.Abstract',
requires: [
'Ext.chart.grid.HorizontalGrid',
'Ext.chart.grid.VerticalGrid',
'Ext.chart.CartesianChart',
'Ext.chart.axis.layout.Discrete'
],
type: 'crosshair',
alias: 'interaction.crosshair',
config: {
/**
* @cfg {Object} axes
* Specifies label text and label rect configs on per axis basis or as a single config
* for all axes.
*
* {
* type: 'crosshair',
* axes: {
* label: { fillStyle: 'white' },
* rect: { fillStyle: 'maroon'}
* }
* }
*
* In case per axis configuration is used, an object with keys corresponding
* to the {@link Ext.chart.axis.Axis#position position} must be provided.
*
* {
* type: 'crosshair',
* axes: {
* left: {
* label: { fillStyle: 'white' },
* rect: {
* fillStyle: 'maroon',
* radius: 4
* }
* },
* bottom: {
* label: {
* fontSize: '14px',
* fontWeight: 'bold'
* },
* rect: { fillStyle: 'white' }
* }
* }
*
* If the `axes` config is not specified, the following defaults will be used:
* - `label` will use values from the {@link Ext.chart.axis.Axis#label label} config.
* - `rect` will use the 'white' fillStyle.
*/
axes: {
top: { label: {}, rect: {} },
right: { label: {}, rect: {} },
bottom: { label: {}, rect: {} },
left: { label: {}, rect: {} }
},
/**
* @cfg {Object} lines
* Specifies attributes of horizontal and vertical lines that make up the crosshair.
* If this config is missing, black dashed lines will be used.
*
* {
* horizontal: {
* strokeStyle: 'red',
* lineDash: [] // solid line
* },
* vertical: {
* lineWidth: 2,
* lineDash: [15, 5, 5, 5]
* }
* }
*/
lines: {
horizontal: {
strokeStyle: 'black',
lineDash: [5, 5]
},
vertical: {
strokeStyle: 'black',
lineDash: [5, 5]
}
},
/**
* @cfg {String} gesture
* Specifies which gesture should be used for starting/maintaining/ending the interaction.
*/
gesture: 'drag'
},
applyAxes: function(axesConfig, oldAxesConfig) {
return Ext.merge(oldAxesConfig || {}, axesConfig);
},
applyLines: function(linesConfig, oldLinesConfig) {
return Ext.merge(oldLinesConfig || {}, linesConfig);
},
updateChart: function(chart) {
if (chart && !chart.isCartesian) {
Ext.raise("Crosshair interaction can only be used on cartesian charts.");
}
this.callParent(arguments);
},
getGestures: function() {
var me = this,
gestures = {},
gesture = me.getGesture();
gestures[gesture] = 'onGesture';
gestures[gesture + 'start'] = 'onGestureStart';
gestures[gesture + 'end'] = 'onGestureEnd';
gestures[gesture + 'cancel'] = 'onGestureCancel';
return gestures;
},
onGestureStart: function(e) {
var me = this,
chart = me.getChart(),
axesTheme = chart.getTheme().getAxis(),
axisTheme,
surface = chart.getSurface('overlay'),
rect = chart.getInnerRect(),
chartWidth = rect[2],
chartHeight = rect[3],
xy = chart.getEventXY(e),
x = xy[0],
y = xy[1],
axes = chart.getAxes(),
axesConfig = me.getAxes(),
linesConfig = me.getLines(),
axis, axisSurface, axisRect, axisWidth, axisHeight, axisPosition, axisAlignment,
axisLabel, axisLabelConfig, crosshairLabelConfig, tickPadding,
axisSprite, attr, lineWidth, halfLineWidth, title, titleBBox,
horizontalLineCfg, verticalLineCfg,
i;
e.claimGesture();
if (x > 0 && x < chartWidth && y > 0 && y < chartHeight) {
me.lockEvents(me.getGesture());
horizontalLineCfg = Ext.apply({
xclass: 'Ext.chart.grid.HorizontalGrid',
x: 0,
y: y,
width: chartWidth
}, linesConfig.horizontal);
verticalLineCfg = Ext.apply({
xclass: 'Ext.chart.grid.VerticalGrid',
x: x,
y: 0,
height: chartHeight
}, linesConfig.vertical);
me.axesLabels = me.axesLabels || {};
for (i = 0; i < axes.length; i++) {
axis = axes[i];
axisSurface = axis.getSurface();
axisRect = axisSurface.getRect();
axisSprite = axis.getSprites()[0];
axisWidth = axisRect[2];
axisHeight = axisRect[3];
axisPosition = axis.getPosition();
axisAlignment = axis.getAlignment();
title = axis.getTitle();
titleBBox = title && title.attr.text !== '' && title.getBBox();
attr = axisSprite.attr;
lineWidth = attr.axisLine ? attr.lineWidth : 0;
halfLineWidth = lineWidth / 2;
tickPadding = Math.max(attr.majorTickSize, attr.minorTickSize) + lineWidth;
axisLabel = me.axesLabels[axisPosition] = axisSurface.add({ type: 'composite' });
axisLabel.labelRect = axisLabel.addSprite(Ext.apply({
type: 'rect',
fillStyle: 'white',
x: axisPosition === 'right' ? lineWidth : 0,
y: axisPosition === 'bottom' ? lineWidth : 0,
width: axisWidth - lineWidth -
(axisAlignment === 'vertical' && titleBBox ? titleBBox.width : 0),
height: axisHeight - lineWidth -
(axisAlignment === 'horizontal' && titleBBox ? titleBBox.height : 0),
translationX: axisPosition === 'left' && titleBBox ? titleBBox.width : 0,
translationY: axisPosition === 'top' && titleBBox ? titleBBox.height : 0
}, axesConfig.rect || axesConfig[axisPosition].rect));
if (axisAlignment === 'vertical' && !verticalLineCfg.strokeStyle) {
verticalLineCfg.strokeStyle = attr.strokeStyle;
}
if (axisAlignment === 'horizontal' && !horizontalLineCfg.strokeStyle) {
horizontalLineCfg.strokeStyle = attr.strokeStyle;
}
axisTheme = Ext.merge({}, axesTheme.defaults, axesTheme[axisPosition]);
axisLabelConfig = Ext.apply({}, axis.config.label, axisTheme.label);
crosshairLabelConfig = axesConfig.label || axesConfig[axisPosition].label;
axisLabel.labelText = axisLabel.addSprite(
Ext.apply(axisLabelConfig, crosshairLabelConfig, {
type: 'text',
x: me.calculateLabelTextPoint(false, axisPosition, tickPadding, titleBBox,
axisWidth, halfLineWidth),
y: me.calculateLabelTextPoint(true, axisPosition, tickPadding, titleBBox,
axisHeight, halfLineWidth)
})
);
}
me.horizontalLine = surface.add(horizontalLineCfg);
me.verticalLine = surface.add(verticalLineCfg);
return false;
}
},
onGesture: function(e) {
var me = this;
if (me.getLocks()[me.getGesture()] !== me) {
return;
}
// eslint-disable-next-line vars-on-top, one-var
var chart = me.getChart(),
surface = chart.getSurface('overlay'),
rect = Ext.Array.slice(chart.getInnerRect()),
padding = chart.getInnerPadding(),
px = padding.left,
py = padding.top,
chartWidth = rect[2],
chartHeight = rect[3],
xy = chart.getEventXY(e),
x = xy[0],
y = xy[1],
axes = chart.getAxes(),
axis, axisPosition, axisAlignment, axisSurface, axisSprite, axisMatrix,
axisLayoutContext, axisSegmenter,
axisLabel, labelBBox, textPadding,
xx, yy, dx, dy,
xValue, yValue,
text,
i;
if (x < 0) {
x = 0;
}
else if (x > chartWidth) {
x = chartWidth;
}
if (y < 0) {
y = 0;
}
else if (y > chartHeight) {
y = chartHeight;
}
x += px;
y += py;
for (i = 0; i < axes.length; i++) {
axis = axes[i];
axisPosition = axis.getPosition();
axisAlignment = axis.getAlignment();
axisSurface = axis.getSurface();
axisSprite = axis.getSprites()[0];
axisMatrix = axisSprite.attr.matrix;
textPadding = axisSprite.attr.textPadding * 2;
axisLabel = me.axesLabels[axisPosition];
axisLayoutContext = axisSprite.getLayoutContext();
axisSegmenter = axis.getSegmenter();
if (axisLabel) {
if (axisAlignment === 'vertical') {
yy = axisMatrix.getYY();
dy = axisMatrix.getDY();
yValue = (y - dy - py) / yy;
if (axis.getLayout() instanceof Ext.chart.axis.layout.Discrete) {
y = Math.round(yValue) * yy + dy + py;
yValue = axisSegmenter.from(Math.round(yValue));
yValue = axisSprite.attr.data[yValue];
}
else {
yValue = axisSegmenter.from(yValue);
}
text = axisSegmenter.renderer(yValue, axisLayoutContext);
axisLabel.setAttributes({ translationY: y - py });
axisLabel.labelText.setAttributes({ text: text });
labelBBox = axisLabel.labelText.getBBox();
axisLabel.labelRect.setAttributes({
height: labelBBox.height + textPadding,
y: -(labelBBox.height + textPadding) / 2
});
axisSurface.renderFrame();
}
else {
xx = axisMatrix.getXX();
dx = axisMatrix.getDX();
xValue = (x - dx - px) / xx;
if (axis.getLayout() instanceof Ext.chart.axis.layout.Discrete) {
x = Math.round(xValue) * xx + dx + px;
xValue = axisSegmenter.from(Math.round(xValue));
xValue = axisSprite.attr.data[xValue];
}
else {
xValue = axisSegmenter.from(xValue);
}
text = axisSegmenter.renderer(xValue, axisLayoutContext);
axisLabel.setAttributes({ translationX: x - px });
axisLabel.labelText.setAttributes({ text: text });
labelBBox = axisLabel.labelText.getBBox();
axisLabel.labelRect.setAttributes({
width: labelBBox.width + textPadding,
x: -(labelBBox.width + textPadding) / 2
});
axisSurface.renderFrame();
}
}
}
me.horizontalLine.setAttributes({ y: y, strokeStyle: axisSprite.attr.strokeStyle });
me.verticalLine.setAttributes({ x: x, strokeStyle: axisSprite.attr.strokeStyle });
surface.renderFrame();
return false;
},
onGestureEnd: function(e) {
var me = this,
chart = me.getChart(),
surface = chart.getSurface('overlay'),
axes = chart.getAxes(),
axis, axisPosition, axisSurface, axisLabel,
i;
surface.remove(me.verticalLine);
surface.remove(me.horizontalLine);
for (i = 0; i < axes.length; i++) {
axis = axes[i];
axisPosition = axis.getPosition();
axisSurface = axis.getSurface();
axisLabel = me.axesLabels[axisPosition];
if (axisLabel) {
delete me.axesLabels[axisPosition];
axisSurface.remove(axisLabel);
}
axisSurface.renderFrame();
}
surface.renderFrame();
me.unlockEvents(me.getGesture());
},
onGestureCancel: function(e) {
this.onGestureEnd(e);
},
privates: {
vertMap: {
top: 'start',
bottom: 'end'
},
horzMap: {
left: 'start',
right: 'end'
},
calculateLabelTextPoint: function(
vertical, position, tickPadding, titleBBox, axisSize, halfLineWidth
) {
var titlePadding, sizeProp, pointProp;
if (vertical) {
pointProp = 'y';
sizeProp = 'height';
position = this.vertMap[position];
}
else {
pointProp = 'x';
sizeProp = 'width';
position = this.horzMap[position];
}
switch (position) {
case 'start':
titlePadding = titleBBox ? titleBBox[pointProp] + titleBBox[sizeProp] : 0;
return titlePadding + (axisSize - titlePadding - tickPadding) / 2 -
halfLineWidth;
case 'end':
titlePadding = titleBBox ? axisSize - titleBBox[pointProp] : 0;
return tickPadding + (axisSize - tickPadding - titlePadding) / 2 +
halfLineWidth;
default:
return 0;
}
}
}
});