Files
2023-01-13 15:30:20 +08:00

1319 lines
40 KiB
JavaScript

/**
* (c) 2010-2018 Torstein Honsi
*
* License: www.highcharts.com/license
*/
/**
* One position in relation to an axis.
*
* @interface Highcharts.PointerAxisCoordinateObject
*//**
* Related axis.
*
* @name Highcharts.PointerAxisCoordinateObject#axis
* @type {Highcharts.Axis}
*//**
* Axis value.
*
* @name Highcharts.PointerAxisCoordinateObject#value
* @type {number}
*/
/**
* Positions in terms of axis values.
*
* @interface Highcharts.PointerAxisCoordinatesObject
*//**
* Positions on the x-axis.
* @name Highcharts.PointerAxisCoordinatesObject#xAxis
* @type {Array<Highcharts.PointerAxisCoordinateObject>}
*//**
* Positions on the y-axis.
* @name Highcharts.PointerAxisCoordinatesObject#yAxis
* @type {Array<Highcharts.PointerAxisCoordinateObject>}
*/
/**
* Pointer coordinates.
*
* @interface Highcharts.PointerCoordinatesObject
*//**
* @name Highcharts.PointerCoordinatesObject#chartX
* @type {number}
*//**
* @name Highcharts.PointerCoordinatesObject#chartY
* @type {number}
*/
/**
* A native browser mouse or touch event, extended with position information
* relative to the {@link Chart.container}.
*
* @interface Highcharts.PointerEventObject
* @extends global.PointerEvent
*//**
* The X coordinate of the pointer interaction relative to the chart.
*
* @name Highcharts.PointerEventObject#chartX
* @type {number}
*//**
* The Y coordinate of the pointer interaction relative to the chart.
*
* @name Highcharts.PointerEventObject#chartY
* @type {number}
*/
/**
* Axis-specific data of a selection.
*
* @interface Highcharts.SelectDataObject
*//**
* @name Highcharts.SelectDataObject#axis
* @type {Highcharts.Axis}
*//**
* @name Highcharts.SelectDataObject#min
* @type {number}
*//**
* @name Highcharts.SelectDataObject#max
* @type {number}
*/
/**
* Object for select events.
*
* @interface Highcharts.SelectEventObject
*//**
* @name Highcharts.SelectEventObject#originalEvent
* @type {global.Event}
*//**
* @name Highcharts.SelectEventObject#xAxis
* @type {Array<Highcharts.SelectDataObject>}
*//**
* @name Highcharts.SelectEventObject#yAxis
* @type {Array<Highcharts.SelectDataObject>}
*/
'use strict';
import Highcharts from './Globals.js';
import './Utilities.js';
import './Tooltip.js';
import './Color.js';
var H = Highcharts,
addEvent = H.addEvent,
attr = H.attr,
charts = H.charts,
color = H.color,
css = H.css,
defined = H.defined,
extend = H.extend,
find = H.find,
fireEvent = H.fireEvent,
isNumber = H.isNumber,
isObject = H.isObject,
offset = H.offset,
pick = H.pick,
splat = H.splat,
Tooltip = H.Tooltip;
/**
* The mouse and touch tracker object. Each {@link Chart} item has one
* assosiated Pointer item that can be accessed from the {@link Chart.pointer}
* property.
*
* @class
* @name Highcharts.Pointer
*
* @param {Highcharts.Chart} chart
* The Chart instance.
*
* @param {Highcharts.Options} options
* The root options object. The pointer uses options from the chart and
* tooltip structures.
*/
Highcharts.Pointer = function (chart, options) {
this.init(chart, options);
};
Highcharts.Pointer.prototype = {
/**
* Initialize the Pointer.
*
* @private
* @function Highcharts.Pointer#init
*
* @param {Highcharts.Chart} chart
* The Chart instance.
*
* @param {Highcharts.Options} options
* The root options object. The pointer uses options from the chart
* and tooltip structures.
*/
init: function (chart, options) {
// Store references
this.options = options;
this.chart = chart;
// Do we need to handle click on a touch device?
this.runChartClick =
options.chart.events && !!options.chart.events.click;
this.pinchDown = [];
this.lastValidTouch = {};
if (Tooltip) {
/**
* Tooltip object for points of series.
*
* @name Highcharts.Chart#tooltip
* @type {Highcharts.Tooltip}
*/
chart.tooltip = new Tooltip(chart, options.tooltip);
this.followTouchMove = pick(options.tooltip.followTouchMove, true);
}
this.setDOMEvents();
},
/**
* Resolve the zoomType option, this is reset on all touch start and mouse
* down events.
*
* @private
* @function Highcharts.Pointer#zoomOption
*
* @param {global.Event} e
* Event object.
*/
zoomOption: function (e) {
var chart = this.chart,
options = chart.options.chart,
zoomType = options.zoomType || '',
inverted = chart.inverted,
zoomX,
zoomY;
// Look for the pinchType option
if (/touch/.test(e.type)) {
zoomType = pick(options.pinchType, zoomType);
}
this.zoomX = zoomX = /x/.test(zoomType);
this.zoomY = zoomY = /y/.test(zoomType);
this.zoomHor = (zoomX && !inverted) || (zoomY && inverted);
this.zoomVert = (zoomY && !inverted) || (zoomX && inverted);
this.hasZoom = zoomX || zoomY;
},
/**
* Takes a browser event object and extends it with custom Highcharts
* properties `chartX` and `chartY` in order to work on the internal
* coordinate system.
*
* @function Highcharts.Pointer#normalize
*
* @param {global.Event} e
* Event object in standard browsers.
*
* @return {Highcharts.PointerEventObject}
* A browser event with extended properties `chartX` and `chartY`.
*/
normalize: function (e, chartPosition) {
var ePos;
// iOS (#2757)
ePos = e.touches ?
(e.touches.length ? e.touches.item(0) : e.changedTouches[0]) :
e;
// Get mouse position
if (!chartPosition) {
this.chartPosition = chartPosition = offset(this.chart.container);
}
return extend(e, {
chartX: Math.round(ePos.pageX - chartPosition.left),
chartY: Math.round(ePos.pageY - chartPosition.top)
});
},
/**
* Get the click position in terms of axis values.
*
* @function Highcharts.Pointer#getCoordinates
*
* @param {Highcharts.PointerEventObject} e
* Pointer event, extended with `chartX` and `chartY` properties.
*
* @return {Highcharts.PointerAxisCoordinatesObject}
*/
getCoordinates: function (e) {
var coordinates = {
xAxis: [],
yAxis: []
};
this.chart.axes.forEach(function (axis) {
coordinates[axis.isXAxis ? 'xAxis' : 'yAxis'].push({
axis: axis,
value: axis.toValue(e[axis.horiz ? 'chartX' : 'chartY'])
});
});
return coordinates;
},
/**
* Finds the closest point to a set of coordinates, using the k-d-tree
* algorithm.
*
* @function Highcharts.Pointer#findNearestKDPoints
*
* @param {Array<Highcharts.Series>} series
* All the series to search in.
*
* @param {boolean} shared
* Whether it is a shared tooltip or not.
*
* @param {Highcharts.PointerCoordinatesObject} coordinates
* Chart coordinates of the pointer.
*
* @return {Point|undefined}
* The point closest to given coordinates.
*/
findNearestKDPoint: function (series, shared, coordinates) {
var closest,
sort = function (p1, p2) {
var isCloserX = p1.distX - p2.distX,
isCloser = p1.dist - p2.dist,
isAbove =
(p2.series.group && p2.series.group.zIndex) -
(p1.series.group && p1.series.group.zIndex),
result;
// We have two points which are not in the same place on xAxis
// and shared tooltip:
if (isCloserX !== 0 && shared) { // #5721
result = isCloserX;
// Points are not exactly in the same place on x/yAxis:
} else if (isCloser !== 0) {
result = isCloser;
// The same xAxis and yAxis position, sort by z-index:
} else if (isAbove !== 0) {
result = isAbove;
// The same zIndex, sort by array index:
} else {
result = p1.series.index > p2.series.index ? -1 : 1;
}
return result;
};
series.forEach(function (s) {
var noSharedTooltip = s.noSharedTooltip && shared,
compareX = (
!noSharedTooltip &&
s.options.findNearestPointBy.indexOf('y') < 0
),
point = s.searchPoint(
coordinates,
compareX
);
if (
// Check that we actually found a point on the series.
isObject(point, true) &&
// Use the new point if it is closer.
(!isObject(closest, true) || (sort(closest, point) > 0))
) {
closest = point;
}
});
return closest;
},
/**
* @private
* @function Highcharts.Pointer#getPointFromEvent
*
* @param {global.Event} e
*
* @return {Highcharts.Point|undefined}
*/
getPointFromEvent: function (e) {
var target = e.target,
point;
while (target && !point) {
point = target.point;
target = target.parentNode;
}
return point;
},
/**
* @private
* @function Highcharts.Pointer#getChartCoordinatesFromPoint
*
* @param {Highcharts.Point} point
*
* @param {boolean} inverted
*
* @return {Highcharts.PointerCoordinatesObject}
*/
getChartCoordinatesFromPoint: function (point, inverted) {
var series = point.series,
xAxis = series.xAxis,
yAxis = series.yAxis,
plotX = pick(point.clientX, point.plotX),
shapeArgs = point.shapeArgs;
if (xAxis && yAxis) {
return inverted ? {
chartX: xAxis.len + xAxis.pos - plotX,
chartY: yAxis.len + yAxis.pos - point.plotY
} : {
chartX: plotX + xAxis.pos,
chartY: point.plotY + yAxis.pos
};
} else if (shapeArgs && shapeArgs.x && shapeArgs.y) {
// E.g. pies do not have axes
return {
chartX: shapeArgs.x,
chartY: shapeArgs.y
};
}
},
/**
* Calculates what is the current hovered point/points and series.
*
* @private
* @function Highcharts.Pointer#getHoverData
*
* @param {Highcharts.Point|undefined} existingHoverPoint
* The point currrently beeing hovered.
*
* @param {Highcharts.Series|undefined} existingHoverSeries
* The series currently beeing hovered.
*
* @param {Array<Highcharts.Series>} series
* All the series in the chart.
*
* @param {boolean} isDirectTouch
* Is the pointer directly hovering the point.
*
* @param {boolean} shared
* Whether it is a shared tooltip or not.
*
* @param {Highcharts.PointerCoordinatesObject} coordinates
* Chart coordinates of the pointer.
*
* @return {*}
* Object containing resulting hover data: hoverPoint, hoverSeries,
* and hoverPoints.
*/
getHoverData: function (
existingHoverPoint,
existingHoverSeries,
series,
isDirectTouch,
shared,
coordinates,
params
) {
var hoverPoint,
hoverPoints = [],
hoverSeries = existingHoverSeries,
isBoosting = params && params.isBoosting,
useExisting = !!(isDirectTouch && existingHoverPoint),
notSticky = hoverSeries && !hoverSeries.stickyTracking,
filter = function (s) {
return (
s.visible &&
!(!shared && s.directTouch) && // #3821
pick(s.options.enableMouseTracking, true)
);
},
// Which series to look in for the hover point
searchSeries = notSticky ?
// Only search on hovered series if it has stickyTracking false
[hoverSeries] :
// Filter what series to look in.
series.filter(function (s) {
return filter(s) && s.stickyTracking;
});
// Use existing hovered point or find the one closest to coordinates.
hoverPoint = useExisting ?
existingHoverPoint :
this.findNearestKDPoint(searchSeries, shared, coordinates);
// Assign hover series
hoverSeries = hoverPoint && hoverPoint.series;
// If we have a hoverPoint, assign hoverPoints.
if (hoverPoint) {
// When tooltip is shared, it displays more than one point
if (shared && !hoverSeries.noSharedTooltip) {
searchSeries = series.filter(function (s) {
return filter(s) && !s.noSharedTooltip;
});
// Get all points with the same x value as the hoverPoint
searchSeries.forEach(function (s) {
var point = find(s.points, function (p) {
return p.x === hoverPoint.x && !p.isNull;
});
if (isObject(point)) {
/*
* Boost returns a minimal point. Convert it to a usable
* point for tooltip and states.
*/
if (isBoosting) {
point = s.getPoint(point);
}
hoverPoints.push(point);
}
});
} else {
hoverPoints.push(hoverPoint);
}
}
return {
hoverPoint: hoverPoint,
hoverSeries: hoverSeries,
hoverPoints: hoverPoints
};
},
/**
* With line type charts with a single tracker, get the point closest to the
* mouse. Run Point.onMouseOver and display tooltip for the point or points.
*
* @private
* @function Highcharts.Pointer#runPointActions
*
* @param {global.Event} e
*
* @param {Highcharts.Point} p
*
* @fires Highcharts.Point#event:mouseOut
* @fires Highcharts.Point#event:mouseOver
*/
runPointActions: function (e, p) {
var pointer = this,
chart = pointer.chart,
series = chart.series,
tooltip = chart.tooltip && chart.tooltip.options.enabled ?
chart.tooltip :
undefined,
shared = tooltip ? tooltip.shared : false,
hoverPoint = p || chart.hoverPoint,
hoverSeries = hoverPoint && hoverPoint.series || chart.hoverSeries,
// onMouseOver or already hovering a series with directTouch
isDirectTouch = e.type !== 'touchmove' && (
!!p || (
(hoverSeries && hoverSeries.directTouch) &&
pointer.isDirectTouch
)
),
hoverData = this.getHoverData(
hoverPoint,
hoverSeries,
series,
isDirectTouch,
shared,
e,
{ isBoosting: chart.isBoosting }
),
useSharedTooltip,
followPointer,
anchor,
points;
// Update variables from hoverData.
hoverPoint = hoverData.hoverPoint;
points = hoverData.hoverPoints;
hoverSeries = hoverData.hoverSeries;
followPointer = hoverSeries && hoverSeries.tooltipOptions.followPointer;
useSharedTooltip = (
shared &&
hoverSeries &&
!hoverSeries.noSharedTooltip
);
// Refresh tooltip for kdpoint if new hover point or tooltip was hidden
// #3926, #4200
if (
hoverPoint &&
// !(hoverSeries && hoverSeries.directTouch) &&
(hoverPoint !== chart.hoverPoint || (tooltip && tooltip.isHidden))
) {
(chart.hoverPoints || []).forEach(function (p) {
if (points.indexOf(p) === -1) {
p.setState();
}
});
// Do mouseover on all points (#3919, #3985, #4410, #5622)
(points || []).forEach(function (p) {
p.setState('hover');
});
// set normal state to previous series
if (chart.hoverSeries !== hoverSeries) {
hoverSeries.onMouseOver();
}
// If tracking is on series in stead of on each point,
// fire mouseOver on hover point. // #4448
if (chart.hoverPoint) {
chart.hoverPoint.firePointEvent('mouseOut');
}
// Hover point may have been destroyed in the event handlers (#7127)
if (!hoverPoint.series) {
return;
}
hoverPoint.firePointEvent('mouseOver');
chart.hoverPoints = points;
chart.hoverPoint = hoverPoint;
// Draw tooltip if necessary
if (tooltip) {
tooltip.refresh(useSharedTooltip ? points : hoverPoint, e);
}
// Update positions (regardless of kdpoint or hoverPoint)
} else if (followPointer && tooltip && !tooltip.isHidden) {
anchor = tooltip.getAnchor([{}], e);
tooltip.updatePosition({ plotX: anchor[0], plotY: anchor[1] });
}
// Start the event listener to pick up the tooltip and crosshairs
if (!pointer.unDocMouseMove) {
pointer.unDocMouseMove = addEvent(
chart.container.ownerDocument,
'mousemove',
function (e) {
var chart = charts[H.hoverChartIndex];
if (chart) {
chart.pointer.onDocumentMouseMove(e);
}
}
);
}
// Issues related to crosshair #4927, #5269 #5066, #5658
chart.axes.forEach(function drawAxisCrosshair(axis) {
var snap = pick(axis.crosshair.snap, true),
point = !snap ?
undefined :
H.find(points, function (p) {
return p.series[axis.coll] === axis;
});
// Axis has snapping crosshairs, and one of the hover points belongs
// to axis. Always call drawCrosshair when it is not snap.
if (point || !snap) {
axis.drawCrosshair(e, point);
// Axis has snapping crosshairs, but no hover point belongs to axis
} else {
axis.hideCrosshair();
}
});
},
/**
* Reset the tracking by hiding the tooltip, the hover series state and the
* hover point
*
* @function Highcharts.Pointer#reset
*
* @param {boolean} allowMove
* Instead of destroying the tooltip altogether, allow moving it if
* possible.
*
* @param {number} delay
*/
reset: function (allowMove, delay) {
var pointer = this,
chart = pointer.chart,
hoverSeries = chart.hoverSeries,
hoverPoint = chart.hoverPoint,
hoverPoints = chart.hoverPoints,
tooltip = chart.tooltip,
tooltipPoints = tooltip && tooltip.shared ?
hoverPoints :
hoverPoint;
// Check if the points have moved outside the plot area (#1003, #4736,
// #5101)
if (allowMove && tooltipPoints) {
splat(tooltipPoints).forEach(function (point) {
if (point.series.isCartesian && point.plotX === undefined) {
allowMove = false;
}
});
}
// Just move the tooltip, #349
if (allowMove) {
if (tooltip && tooltipPoints) {
tooltip.refresh(tooltipPoints);
if (tooltip.shared && hoverPoints) { // #8284
hoverPoints.forEach(function (point) {
point.setState(point.state, true);
if (point.series.isCartesian) {
if (point.series.xAxis.crosshair) {
point.series.xAxis.drawCrosshair(null, point);
}
if (point.series.yAxis.crosshair) {
point.series.yAxis.drawCrosshair(null, point);
}
}
});
} else if (hoverPoint) { // #2500
hoverPoint.setState(hoverPoint.state, true);
chart.axes.forEach(function (axis) {
if (axis.crosshair) {
axis.drawCrosshair(null, hoverPoint);
}
});
}
}
// Full reset
} else {
if (hoverPoint) {
hoverPoint.onMouseOut();
}
if (hoverPoints) {
hoverPoints.forEach(function (point) {
point.setState();
});
}
if (hoverSeries) {
hoverSeries.onMouseOut();
}
if (tooltip) {
tooltip.hide(delay);
}
if (pointer.unDocMouseMove) {
pointer.unDocMouseMove = pointer.unDocMouseMove();
}
// Remove crosshairs
chart.axes.forEach(function (axis) {
axis.hideCrosshair();
});
pointer.hoverX = chart.hoverPoints = chart.hoverPoint = null;
}
},
/**
* Scale series groups to a certain scale and translation.
*
* @private
* @function Highcharts.Pointer#scaleGroups
*
* @param {Highcharts.SeriesPlotBoxObject} attribs
*
* @param {boolean} clip
*/
scaleGroups: function (attribs, clip) {
var chart = this.chart,
seriesAttribs;
// Scale each series
chart.series.forEach(function (series) {
seriesAttribs = attribs || series.getPlotBox(); // #1701
if (series.xAxis && series.xAxis.zoomEnabled && series.group) {
series.group.attr(seriesAttribs);
if (series.markerGroup) {
series.markerGroup.attr(seriesAttribs);
series.markerGroup.clip(clip ? chart.clipRect : null);
}
if (series.dataLabelsGroup) {
series.dataLabelsGroup.attr(seriesAttribs);
}
}
});
// Clip
chart.clipRect.attr(clip || chart.clipBox);
},
/**
* Start a drag operation.
*
* @private
* @function Highcharts.Pointer#dragStart
*
* @param {Highcharts.PointerEventObject} e
*/
dragStart: function (e) {
var chart = this.chart;
// Record the start position
chart.mouseIsDown = e.type;
chart.cancelClick = false;
chart.mouseDownX = this.mouseDownX = e.chartX;
chart.mouseDownY = this.mouseDownY = e.chartY;
},
/**
* Perform a drag operation in response to a mousemove event while the mouse
* is down.
*
* @private
* @function Highcharts.Pointer#drag
*
* @param {Highcharts.PointerEventObject} e
*/
drag: function (e) {
var chart = this.chart,
chartOptions = chart.options.chart,
chartX = e.chartX,
chartY = e.chartY,
zoomHor = this.zoomHor,
zoomVert = this.zoomVert,
plotLeft = chart.plotLeft,
plotTop = chart.plotTop,
plotWidth = chart.plotWidth,
plotHeight = chart.plotHeight,
clickedInside,
size,
selectionMarker = this.selectionMarker,
mouseDownX = this.mouseDownX,
mouseDownY = this.mouseDownY,
panKey = chartOptions.panKey && e[chartOptions.panKey + 'Key'];
// If the device supports both touch and mouse (like IE11), and we are
// touch-dragging inside the plot area, don't handle the mouse event.
// #4339.
if (selectionMarker && selectionMarker.touch) {
return;
}
// If the mouse is outside the plot area, adjust to cooordinates
// inside to prevent the selection marker from going outside
if (chartX < plotLeft) {
chartX = plotLeft;
} else if (chartX > plotLeft + plotWidth) {
chartX = plotLeft + plotWidth;
}
if (chartY < plotTop) {
chartY = plotTop;
} else if (chartY > plotTop + plotHeight) {
chartY = plotTop + plotHeight;
}
// determine if the mouse has moved more than 10px
this.hasDragged = Math.sqrt(
Math.pow(mouseDownX - chartX, 2) +
Math.pow(mouseDownY - chartY, 2)
);
if (this.hasDragged > 10) {
clickedInside = chart.isInsidePlot(
mouseDownX - plotLeft,
mouseDownY - plotTop
);
// make a selection
if (
chart.hasCartesianSeries &&
(this.zoomX || this.zoomY) &&
clickedInside &&
!panKey
) {
if (!selectionMarker) {
this.selectionMarker = selectionMarker =
chart.renderer.rect(
plotLeft,
plotTop,
zoomHor ? 1 : plotWidth,
zoomVert ? 1 : plotHeight,
0
)
.attr({
'class': 'highcharts-selection-marker',
'zIndex': 7
})
.add();
if (!chart.styledMode) {
selectionMarker.attr({
fill: (
chartOptions.selectionMarkerFill ||
color('#335cad')
.setOpacity(0.25).get()
)
});
}
}
}
// adjust the width of the selection marker
if (selectionMarker && zoomHor) {
size = chartX - mouseDownX;
selectionMarker.attr({
width: Math.abs(size),
x: (size > 0 ? 0 : size) + mouseDownX
});
}
// adjust the height of the selection marker
if (selectionMarker && zoomVert) {
size = chartY - mouseDownY;
selectionMarker.attr({
height: Math.abs(size),
y: (size > 0 ? 0 : size) + mouseDownY
});
}
// panning
if (clickedInside && !selectionMarker && chartOptions.panning) {
chart.pan(e, chartOptions.panning);
}
}
},
/**
* On mouse up or touch end across the entire document, drop the selection.
*
* @private
* @function Highcharts.Pointer#drop
*
* @param {global.Event} e
*/
drop: function (e) {
var pointer = this,
chart = this.chart,
hasPinched = this.hasPinched;
if (this.selectionMarker) {
var selectionData = {
originalEvent: e, // #4890
xAxis: [],
yAxis: []
},
selectionBox = this.selectionMarker,
selectionLeft = selectionBox.attr ?
selectionBox.attr('x') :
selectionBox.x,
selectionTop = selectionBox.attr ?
selectionBox.attr('y') :
selectionBox.y,
selectionWidth = selectionBox.attr ?
selectionBox.attr('width') :
selectionBox.width,
selectionHeight = selectionBox.attr ?
selectionBox.attr('height') :
selectionBox.height,
runZoom;
// a selection has been made
if (this.hasDragged || hasPinched) {
// record each axis' min and max
chart.axes.forEach(function (axis) {
if (
axis.zoomEnabled &&
defined(axis.min) &&
(
hasPinched ||
pointer[{
xAxis: 'zoomX',
yAxis: 'zoomY'
}[axis.coll]]
)
) { // #859, #3569
var horiz = axis.horiz,
minPixelPadding = e.type === 'touchend' ?
axis.minPixelPadding :
0, // #1207, #3075
selectionMin = axis.toValue(
(horiz ? selectionLeft : selectionTop) +
minPixelPadding
),
selectionMax = axis.toValue(
(
horiz ?
selectionLeft + selectionWidth :
selectionTop + selectionHeight
) - minPixelPadding
);
selectionData[axis.coll].push({
axis: axis,
// Min/max for reversed axes
min: Math.min(selectionMin, selectionMax),
max: Math.max(selectionMin, selectionMax)
});
runZoom = true;
}
});
if (runZoom) {
fireEvent(
chart,
'selection',
selectionData,
function (args) {
chart.zoom(
extend(
args,
hasPinched ? { animation: false } : null
)
);
}
);
}
}
if (isNumber(chart.index)) {
this.selectionMarker = this.selectionMarker.destroy();
}
// Reset scaling preview
if (hasPinched) {
this.scaleGroups();
}
}
// Reset all. Check isNumber because it may be destroyed on mouse up
// (#877)
if (chart && isNumber(chart.index)) {
css(chart.container, { cursor: chart._cursor });
chart.cancelClick = this.hasDragged > 10; // #370
chart.mouseIsDown = this.hasDragged = this.hasPinched = false;
this.pinchDown = [];
}
},
/**
* @private
* @function Highcharts.Pointer#onContainerMouseDown
*
* @param {global.Event} e
*/
onContainerMouseDown: function (e) {
// Normalize before the 'if' for the legacy IE (#7850)
e = this.normalize(e);
if (e.button !== 2) {
this.zoomOption(e);
// issue #295, dragging not always working in Firefox
if (e.preventDefault) {
e.preventDefault();
}
this.dragStart(e);
}
},
/**
* @private
* @function Highcharts.Pointer#onDocumentMouseUp
*
* @param {global.Event} e
*/
onDocumentMouseUp: function (e) {
if (charts[H.hoverChartIndex]) {
charts[H.hoverChartIndex].pointer.drop(e);
}
},
/**
* Special handler for mouse move that will hide the tooltip when the mouse
* leaves the plotarea. Issue #149 workaround. The mouseleave event does not
* always fire.
*
* @private
* @function Highcharts.Pointer#onDocumentMouseMove
*
* @param {Highcharts.PointerEventObject} e
*/
onDocumentMouseMove: function (e) {
var chart = this.chart,
chartPosition = this.chartPosition;
e = this.normalize(e, chartPosition);
// If we're outside, hide the tooltip
if (
chartPosition &&
!this.inClass(e.target, 'highcharts-tracker') &&
!chart.isInsidePlot(
e.chartX - chart.plotLeft,
e.chartY - chart.plotTop
)
) {
this.reset();
}
},
/**
* When mouse leaves the container, hide the tooltip.
*
* @private
* @function Highcharts.Pointer#onContainerMouseLeave
*
* @param {global.Event} e
*/
onContainerMouseLeave: function (e) {
var chart = charts[H.hoverChartIndex];
// #4886, MS Touch end fires mouseleave but with no related target
if (chart && (e.relatedTarget || e.toElement)) {
chart.pointer.reset();
// Also reset the chart position, used in #149 fix
chart.pointer.chartPosition = null;
}
},
/**
* The mousemove, touchmove and touchstart event handler
*
* @private
* @function Highcharts.Pointer#onContainerMouseMove
*
* @param {Highcharts.PointerEventObject} e
*/
onContainerMouseMove: function (e) {
var chart = this.chart;
if (
!defined(H.hoverChartIndex) ||
!charts[H.hoverChartIndex] ||
!charts[H.hoverChartIndex].mouseIsDown
) {
H.hoverChartIndex = chart.index;
}
e = this.normalize(e);
e.returnValue = false; // #2251, #3224
if (chart.mouseIsDown === 'mousedown') {
this.drag(e);
}
// Show the tooltip and run mouse over events (#977)
if (
(
this.inClass(e.target, 'highcharts-tracker') ||
chart.isInsidePlot(
e.chartX - chart.plotLeft,
e.chartY - chart.plotTop
)
) &&
!chart.openMenu
) {
this.runPointActions(e);
}
},
/**
* Utility to detect whether an element has, or has a parent with, a
* specificclass name. Used on detection of tracker objects and on deciding
* whether hovering the tooltip should cause the active series to mouse out.
*
* @function Highcharts.Pointer#inClass
*
* @param {Highcharts.SVGDOMElement|Highcharts.HTMLDOMElement} element
* The element to investigate.
*
* @param {string} className
* The class name to look for.
*
* @return {boolean}
* True if either the element or one of its parents has the given
* class name.
*/
inClass: function (element, className) {
var elemClassName;
while (element) {
elemClassName = attr(element, 'class');
if (elemClassName) {
if (elemClassName.indexOf(className) !== -1) {
return true;
}
if (elemClassName.indexOf('highcharts-container') !== -1) {
return false;
}
}
element = element.parentNode;
}
},
/**
* @private
* @function Highcharts.Pointer#onTrackerMouseOut
*
* @param {global.Event} e
*/
onTrackerMouseOut: function (e) {
var series = this.chart.hoverSeries,
relatedTarget = e.relatedTarget || e.toElement;
this.isDirectTouch = false;
if (
series &&
relatedTarget &&
!series.stickyTracking &&
!this.inClass(relatedTarget, 'highcharts-tooltip') &&
(
!this.inClass(
relatedTarget,
'highcharts-series-' + series.index
) || // #2499, #4465
!this.inClass(relatedTarget, 'highcharts-tracker') // #5553
)
) {
series.onMouseOut();
}
},
/**
* @private
* @function Highcharts.Pointer#onContainerClick
*
* @param {Highcharts.PointerEventObject} e
*/
onContainerClick: function (e) {
var chart = this.chart,
hoverPoint = chart.hoverPoint,
plotLeft = chart.plotLeft,
plotTop = chart.plotTop;
e = this.normalize(e);
if (!chart.cancelClick) {
// On tracker click, fire the series and point events. #783, #1583
if (hoverPoint && this.inClass(e.target, 'highcharts-tracker')) {
// the series click event
fireEvent(hoverPoint.series, 'click', extend(e, {
point: hoverPoint
}));
// the point click event
if (chart.hoverPoint) { // it may be destroyed (#1844)
hoverPoint.firePointEvent('click', e);
}
// When clicking outside a tracker, fire a chart event
} else {
extend(e, this.getCoordinates(e));
// fire a click event in the chart
if (
chart.isInsidePlot(e.chartX - plotLeft, e.chartY - plotTop)
) {
fireEvent(chart, 'click', e);
}
}
}
},
/**
* Set the JS DOM events on the container and document. This method should
* contain a one-to-one assignment between methods and their handlers. Any
* advanced logic should be moved to the handler reflecting the event's
* name.
*
* @private
* @function Highcharts.Pointer#setDOMEvents
*/
setDOMEvents: function () {
var pointer = this,
container = pointer.chart.container,
ownerDoc = container.ownerDocument;
container.onmousedown = function (e) {
pointer.onContainerMouseDown(e);
};
container.onmousemove = function (e) {
pointer.onContainerMouseMove(e);
};
container.onclick = function (e) {
pointer.onContainerClick(e);
};
this.unbindContainerMouseLeave = addEvent(
container,
'mouseleave',
pointer.onContainerMouseLeave
);
if (!H.unbindDocumentMouseUp) {
H.unbindDocumentMouseUp = addEvent(
ownerDoc,
'mouseup',
pointer.onDocumentMouseUp
);
}
if (H.hasTouch) {
container.ontouchstart = function (e) {
pointer.onContainerTouchStart(e);
};
container.ontouchmove = function (e) {
pointer.onContainerTouchMove(e);
};
if (!H.unbindDocumentTouchEnd) {
H.unbindDocumentTouchEnd = addEvent(
ownerDoc,
'touchend',
pointer.onDocumentTouchEnd
);
}
}
},
/**
* Destroys the Pointer object and disconnects DOM events.
*
* @function Highcharts.Pointer#destroy
*/
destroy: function () {
var pointer = this;
if (pointer.unDocMouseMove) {
pointer.unDocMouseMove();
}
this.unbindContainerMouseLeave();
if (!H.chartCount) {
if (H.unbindDocumentMouseUp) {
H.unbindDocumentMouseUp = H.unbindDocumentMouseUp();
}
if (H.unbindDocumentTouchEnd) {
H.unbindDocumentTouchEnd = H.unbindDocumentTouchEnd();
}
}
// memory and CPU leak
clearInterval(pointer.tooltipTimeout);
H.objectEach(pointer, function (val, prop) {
pointer[prop] = null;
});
}
};