highcharts
Version:
JavaScript charting framework
1,321 lines (1,320 loc) • 48.6 kB
JavaScript
/* *
*
* (c) 2010-2025 Torstein Honsi
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import AST from '../Renderer/HTML/AST.js';
import A from '../Animation/AnimationUtilities.js';
const { animObject } = A;
import D from '../Defaults.js';
const { defaultOptions } = D;
import F from '../Templating.js';
const { format } = F;
import U from '../Utilities.js';
const { addEvent, crisp, erase, extend, fireEvent, getNestedProperty, isArray, isFunction, isNumber, isObject, merge, pick, syncTimeout, removeEvent, uniqueKey } = U;
/* eslint-disable no-invalid-this, valid-jsdoc */
/* *
*
* Class
*
* */
/**
* The Point object. The point objects are generated from the `series.data`
* configuration objects or raw numbers. They can be accessed from the
* `Series.points` array. Other ways to instantiate points are through {@link
* Highcharts.Series#addPoint} or {@link Highcharts.Series#setData}.
*
* @class
* @name Highcharts.Point
*/
class Point {
/**
* For categorized axes this property holds the category name for the
* point. For other axes it holds the X value.
*
* @name Highcharts.Point#category
* @type {number|string}
*/
/**
* The name of the point. The name can be given as the first position of the
* point configuration array, or as a `name` property in the configuration:
*
* @example
* // Array config
* data: [
* ['John', 1],
* ['Jane', 2]
* ]
*
* // Object config
* data: [{
* name: 'John',
* y: 1
* }, {
* name: 'Jane',
* y: 2
* }]
*
* @name Highcharts.Point#name
* @type {string}
*/
/**
* The point's name if it is defined, or its category in case of a category,
* otherwise the x value. Convenient for tooltip and data label formatting.
*
* @name Highcharts.Point#key
* @type {number|string}
*/
/**
* The point's options as applied in the initial configuration, or
* extended through `Point.update`.
*
* In TypeScript you have to extend `PointOptionsObject` via an
* additional interface to allow custom data options:
*
* ```
* declare interface PointOptionsObject {
* customProperty: string;
* }
* ```
*
* @name Highcharts.Point#options
* @type {Highcharts.PointOptionsObject}
*/
/**
* The percentage for points in a stacked series, pies or gauges.
*
* @name Highcharts.Point#percentage
* @type {number|undefined}
*/
/**
* Array of all hovered points when using shared tooltips.
*
* @name Highcharts.Point#points
* @type {Array<Highcharts.Point>|undefined}
*/
/**
* The series object associated with the point.
*
* @name Highcharts.Point#series
* @type {Highcharts.Series}
*/
/**
* The attributes of the rendered SVG shape like in `column` or `pie`
* series.
*
* @readonly
* @name Highcharts.Point#shapeArgs
* @type {Readonly<Highcharts.SVGAttributes>|undefined}
*/
/**
* The total of values in either a stack for stacked series, or a pie in a
* pie series.
*
* @name Highcharts.Point#total
* @type {number|undefined}
*/
/**
* For certain series types, like pie charts, where individual points can
* be shown or hidden.
*
* @name Highcharts.Point#visible
* @type {boolean}
* @default true
*/
/* *
*
* Functions
*
* */
/**
* Animate SVG elements associated with the point.
*
* @private
* @function Highcharts.Point#animateBeforeDestroy
*/
animateBeforeDestroy() {
const point = this, animateParams = { x: point.startXPos, opacity: 0 }, graphicalProps = point.getGraphicalProps();
graphicalProps.singular.forEach(function (prop) {
const isDataLabel = prop === 'dataLabel';
point[prop] = point[prop].animate(isDataLabel ? {
x: point[prop].startXPos,
y: point[prop].startYPos,
opacity: 0
} : animateParams);
});
graphicalProps.plural.forEach(function (plural) {
point[plural].forEach(function (item) {
if (item.element) {
item.animate(extend({ x: point.startXPos }, (item.startYPos ? {
x: item.startXPos,
y: item.startYPos
} : {})));
}
});
});
}
/**
* Apply the options containing the x and y data and possible some extra
* properties. Called on point init or from point.update.
*
* @private
* @function Highcharts.Point#applyOptions
*
* @param {Highcharts.PointOptionsType} options
* The point options as defined in series.data.
*
* @param {number} [x]
* Optionally, the x value.
*
* @return {Highcharts.Point}
* The Point instance.
*/
applyOptions(options, x) {
const point = this, series = point.series, pointValKey = series.options.pointValKey || series.pointValKey;
options = Point.prototype.optionsToObject.call(this, options);
// Copy options directly to point
extend(point, options);
point.options = point.options ?
extend(point.options, options) :
options;
// Since options are copied into the Point instance, some accidental
// options must be shielded (#5681)
if (options.group) {
delete point.group;
}
if (options.dataLabels) {
delete point.dataLabels;
}
/**
* The y value of the point.
* @name Highcharts.Point#y
* @type {number|undefined}
*/
// For higher dimension series types. For instance, for ranges, point.y
// is mapped to point.low.
if (pointValKey) {
point.y = Point.prototype.getNestedProperty.call(point, pointValKey);
}
// The point is initially selected by options (#5777)
if (point.selected) {
point.state = 'select';
}
/**
* The x value of the point.
* @name Highcharts.Point#x
* @type {number}
*/
// If no x is set by now, get auto incremented value. All points must
// have an x value, however the y value can be null to create a gap in
// the series
if ('name' in point &&
typeof x === 'undefined' &&
series.xAxis &&
series.xAxis.hasNames) {
point.x = series.xAxis.nameToX(point);
}
if (typeof point.x === 'undefined' && series) {
point.x = x ?? series.autoIncrement();
}
else if (isNumber(options.x) && series.options.relativeXValue) {
point.x = series.autoIncrement(options.x);
// If x is a string, try to parse it to a datetime
}
else if (typeof point.x === 'string') {
x ?? (x = series.chart.time.parse(point.x));
if (isNumber(x)) {
point.x = x;
}
}
point.isNull = this.isValid && !this.isValid();
point.formatPrefix = point.isNull ? 'null' : 'point'; // #9233, #10874
return point;
}
/**
* Destroy a point to clear memory. Its reference still stays in
* `series.data`.
*
* @private
* @function Highcharts.Point#destroy
*/
destroy() {
if (!this.destroyed) {
const point = this, series = point.series, chart = series.chart, dataSorting = series.options.dataSorting, hoverPoints = chart.hoverPoints, globalAnimation = point.series.chart.renderer.globalAnimation, animation = animObject(globalAnimation);
/**
* Allow to call after animation.
* @private
*/
const destroyPoint = () => {
// Remove all events and elements
if (point.graphic ||
point.graphics ||
point.dataLabel ||
point.dataLabels) {
removeEvent(point);
point.destroyElements();
}
for (const prop in point) { // eslint-disable-line guard-for-in
delete point[prop];
}
};
if (point.legendItem) {
// Pies have legend items
chart.legend.destroyItem(point);
}
if (hoverPoints) {
point.setState();
erase(hoverPoints, point);
if (!hoverPoints.length) {
chart.hoverPoints = null;
}
}
if (point === chart.hoverPoint) {
point.onMouseOut();
}
// Remove properties after animation
if (!dataSorting?.enabled) {
destroyPoint();
}
else {
this.animateBeforeDestroy();
syncTimeout(destroyPoint, animation.duration);
}
chart.pointCount--;
}
this.destroyed = true;
}
/**
* Destroy SVG elements associated with the point.
*
* @private
* @function Highcharts.Point#destroyElements
* @param {Highcharts.Dictionary<number>} [kinds]
*/
destroyElements(kinds) {
const point = this, props = point.getGraphicalProps(kinds);
props.singular.forEach(function (prop) {
point[prop] = point[prop].destroy();
});
props.plural.forEach(function (plural) {
point[plural].forEach(function (item) {
if (item?.element) {
item.destroy();
}
});
delete point[plural];
});
}
/**
* Fire an event on the Point object.
*
* @private
* @function Highcharts.Point#firePointEvent
*
* @param {string} eventType
* Type of the event.
*
* @param {Highcharts.Dictionary<any>|Event} [eventArgs]
* Additional event arguments.
*
* @param {Highcharts.EventCallbackFunction<Highcharts.Point>|Function} [defaultFunction]
* Default event handler.
*
* @emits Highcharts.Point#event:*
*/
firePointEvent(eventType, eventArgs, defaultFunction) {
const point = this, series = this.series, seriesOptions = series.options;
// Load event handlers on demand to save time on mouseover/out
point.manageEvent(eventType);
// Add default handler if in selection mode
if (eventType === 'click' && seriesOptions.allowPointSelect) {
defaultFunction = function (event) {
// Control key is for Windows, meta (= Cmd key) for Mac, Shift
// for Opera.
if (!point.destroyed && point.select) { // #2911, #19075
point.select(null, event.ctrlKey || event.metaKey || event.shiftKey);
}
};
}
fireEvent(point, eventType, eventArgs, defaultFunction);
}
/**
* Get the CSS class names for individual points. Used internally where the
* returned value is set on every point.
*
* @function Highcharts.Point#getClassName
*
* @return {string}
* The class names.
*/
getClassName() {
const point = this;
return 'highcharts-point' +
(point.selected ? ' highcharts-point-select' : '') +
(point.negative ? ' highcharts-negative' : '') +
(point.isNull ? ' highcharts-null-point' : '') +
(typeof point.colorIndex !== 'undefined' ?
' highcharts-color-' + point.colorIndex : '') +
(point.options.className ? ' ' + point.options.className : '') +
(point.zone?.className ?
' ' + point.zone.className.replace('highcharts-negative', '') :
'');
}
/**
* Get props of all existing graphical point elements.
*
* @private
* @function Highcharts.Point#getGraphicalProps
*/
getGraphicalProps(kinds) {
const point = this, props = [], graphicalProps = { singular: [], plural: [] };
let prop, i;
kinds = kinds || { graphic: 1, dataLabel: 1 };
if (kinds.graphic) {
props.push('graphic', 'connector' // Used by dumbbell
);
}
if (kinds.dataLabel) {
props.push('dataLabel', 'dataLabelPath', 'dataLabelUpper');
}
i = props.length;
while (i--) {
prop = props[i];
if (point[prop]) {
graphicalProps.singular.push(prop);
}
}
[
'graphic',
'dataLabel'
].forEach(function (prop) {
const plural = prop + 's';
if (kinds[prop] && point[plural]) {
graphicalProps.plural.push(plural);
}
});
return graphicalProps;
}
/**
* Returns the value of the point property for a given value.
* @private
*/
getNestedProperty(key) {
if (!key) {
return;
}
if (key.indexOf('custom.') === 0) {
return getNestedProperty(key, this.options);
}
return this[key];
}
/**
* In a series with `zones`, return the zone that the point belongs to.
*
* @function Highcharts.Point#getZone
*
* @return {Highcharts.SeriesZonesOptionsObject}
* The zone item.
*/
getZone() {
const series = this.series, zones = series.zones, zoneAxis = series.zoneAxis || 'y';
let zone, i = 0;
zone = zones[i];
while (this[zoneAxis] >= zone.value) {
zone = zones[++i];
}
// For resetting or reusing the point (#8100)
if (!this.nonZonedColor) {
this.nonZonedColor = this.color;
}
if (zone?.color && !this.options.color) {
this.color = zone.color;
}
else {
this.color = this.nonZonedColor;
}
return zone;
}
/**
* Utility to check if point has new shape type. Used in column series and
* all others that are based on column series.
* @private
*/
hasNewShapeType() {
const point = this;
const oldShapeType = point.graphic &&
(point.graphic.symbolName || point.graphic.element.nodeName);
return oldShapeType !== this.shapeType;
}
/**
* Initialize the point. Called internally based on the `series.data`
* option.
*
* @function Highcharts.Point#init
*
* @param {Highcharts.Series} series
* The series object containing this point.
*
* @param {Highcharts.PointOptionsType} options
* The data in either number, array or object format.
*
* @param {number} [x]
* Optionally, the X value of the point.
*
* @return {Highcharts.Point}
* The Point instance.
*
* @emits Highcharts.Point#event:afterInit
*/
constructor(series, options, x) {
this.formatPrefix = 'point';
this.visible = true;
// For tooltip and data label formatting
this.point = this;
this.series = series;
this.applyOptions(options, x);
// Add a unique ID to the point if none is assigned
this.id ?? (this.id = uniqueKey());
this.resolveColor();
this.dataLabelOnNull ?? (this.dataLabelOnNull = series.options.nullInteraction);
series.chart.pointCount++;
fireEvent(this, 'afterInit');
}
/**
* Determine if point is valid.
* @private
* @function Highcharts.Point#isValid
*/
isValid() {
return ((isNumber(this.x) ||
this.x instanceof Date) &&
isNumber(this.y));
}
/**
* Transform number or array configs into objects. Also called for object
* configs. Used internally to unify the different configuration formats for
* points. For example, a simple number `10` in a line series will be
* transformed to `{ y: 10 }`, and an array config like `[1, 10]` in a
* scatter series will be transformed to `{ x: 1, y: 10 }`.
*
* @function Highcharts.Point#optionsToObject
*
* @param {Highcharts.PointOptionsType} options
* Series data options.
*
* @return {Highcharts.Dictionary<*>}
* Transformed point options.
*/
optionsToObject(options) {
const series = this.series, keys = series.options.keys, pointArrayMap = keys || series.pointArrayMap || ['y'], valueCount = pointArrayMap.length;
let ret = {}, firstItemType, i = 0, j = 0;
if (isNumber(options) || options === null) {
ret[pointArrayMap[0]] = options;
}
else if (isArray(options)) {
// With leading x value
if (!keys && options.length > valueCount) {
firstItemType = typeof options[0];
if (firstItemType === 'string') {
if (series.xAxis?.dateTime) {
ret.x = series.chart.time.parse(options[0]);
}
else {
ret.name = options[0];
}
}
else if (firstItemType === 'number') {
ret.x = options[0];
}
i++;
}
while (j < valueCount) {
// Skip undefined positions for keys
if (!keys || typeof options[i] !== 'undefined') {
if (pointArrayMap[j].indexOf('.') > 0) {
// Handle nested keys, e.g. ['color.pattern.image']
// Avoid function call unless necessary.
Point.prototype.setNestedProperty(ret, options[i], pointArrayMap[j]);
}
else {
ret[pointArrayMap[j]] = options[i];
}
}
i++;
j++;
}
}
else if (typeof options === 'object') {
ret = options;
// This is the fastest way to detect if there are individual point
// dataLabels that need to be considered in drawDataLabels. These
// can only occur in object configs.
if (options.dataLabels) {
// Override the prototype function to always return true,
// regardless of whether data labels are enabled series-wide
series.hasDataLabels = () => true;
}
// Same approach as above for markers
if (options.marker) {
series._hasPointMarkers = true;
}
}
return ret;
}
/**
* Get the pixel position of the point relative to the plot area.
* @function Highcharts.Point#pos
*
* @sample highcharts/point/position
* Get point's position in pixels.
*
* @param {boolean} chartCoordinates
* If true, the returned position is relative to the full chart area.
* If false, it is relative to the plot area determined by the axes.
*
* @param {number|undefined} plotY
* A custom plot y position to be computed. Used internally for some
* series types that have multiple `y` positions, like area range (low
* and high values).
*
* @return {Array<number>|undefined}
* Coordinates of the point if the point exists.
*/
pos(chartCoordinates, plotY = this.plotY) {
if (!this.destroyed) {
const { plotX, series } = this, { chart, xAxis, yAxis } = series;
let posX = 0, posY = 0;
if (isNumber(plotX) && isNumber(plotY)) {
if (chartCoordinates) {
posX = xAxis ? xAxis.pos : chart.plotLeft;
posY = yAxis ? yAxis.pos : chart.plotTop;
}
return chart.inverted && xAxis && yAxis ?
[yAxis.len - plotY + posY, xAxis.len - plotX + posX] :
[plotX + posX, plotY + posY];
}
}
}
/**
* @private
* @function Highcharts.Point#resolveColor
*/
resolveColor() {
const series = this.series, optionsChart = series.chart.options.chart, styledMode = series.chart.styledMode;
let color, colors, colorCount = optionsChart.colorCount, colorIndex;
// Remove points nonZonedColor for later recalculation
delete this.nonZonedColor;
if (series.options.colorByPoint) {
if (!styledMode) {
colors = series.options.colors || series.chart.options.colors;
color = colors[series.colorCounter];
colorCount = colors.length;
}
colorIndex = series.colorCounter;
series.colorCounter++;
// Loop back to zero
if (series.colorCounter === colorCount) {
series.colorCounter = 0;
}
}
else {
if (!styledMode) {
color = series.color;
}
colorIndex = series.colorIndex;
}
/**
* The point's current color index, used in styled mode instead of
* `color`. The color index is inserted in class names used for styling.
*
* @name Highcharts.Point#colorIndex
* @type {number|undefined}
*/
this.colorIndex = pick(this.options.colorIndex, colorIndex);
/**
* The point's current color.
*
* @name Highcharts.Point#color
* @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject|undefined}
*/
this.color = pick(this.options.color, color);
}
/**
* Set a value in an object, on the property defined by key. The key
* supports nested properties using dot notation. The function modifies the
* input object and does not make a copy.
*
* @function Highcharts.Point#setNestedProperty<T>
*
* @param {T} object
* The object to set the value on.
*
* @param {*} value
* The value to set.
*
* @param {string} key
* Key to the property to set.
*
* @return {T}
* The modified object.
*/
setNestedProperty(object, value, key) {
const nestedKeys = key.split('.');
nestedKeys.reduce(function (result, key, i, arr) {
const isLastKey = arr.length - 1 === i;
result[key] = (isLastKey ?
value :
isObject(result[key], true) ?
result[key] :
{});
return result[key];
}, object);
return object;
}
shouldDraw() {
return !this.isNull;
}
/**
* Extendable method for formatting each point's tooltip line.
*
* @function Highcharts.Point#tooltipFormatter
*
* @param {string} pointFormat
* The point format.
*
* @return {string}
* A string to be concatenated in to the common tooltip text.
*/
tooltipFormatter(pointFormat) {
// Insert options for valueDecimals, valuePrefix, and valueSuffix
const { chart, pointArrayMap = ['y'], tooltipOptions } = this.series, { valueDecimals = '', valuePrefix = '', valueSuffix = '' } = tooltipOptions;
// Replace default point style with class name
if (chart.styledMode) {
pointFormat = chart.tooltip?.styledModeFormat(pointFormat) ||
pointFormat;
}
// Loop over the point array map and replace unformatted values with
// sprintf formatting markup
pointArrayMap.forEach((key) => {
key = '{point.' + key; // Without the closing bracket
if (valuePrefix || valueSuffix) {
pointFormat = pointFormat.replace(RegExp(key + '}', 'g'), valuePrefix + key + '}' + valueSuffix);
}
pointFormat = pointFormat.replace(RegExp(key + '}', 'g'), key + ':,.' + valueDecimals + 'f}');
});
return format(pointFormat, this, chart);
}
/**
* Update point with new options (typically x/y data) and optionally redraw
* the series.
*
* @sample highcharts/members/point-update-column/
* Update column value
* @sample highcharts/members/point-update-pie/
* Update pie slice
* @sample maps/members/point-update/
* Update map area value in Highmaps
*
* @function Highcharts.Point#update
*
* @param {Highcharts.PointOptionsType} options
* The point options. Point options are handled as described under
* the `series.type.data` item for each series type. For example
* for a line series, if options is a single number, the point will
* be given that number as the marin y value. If it is an array, it
* will be interpreted as x and y values respectively. If it is an
* object, advanced options are applied.
*
* @param {boolean} [redraw=true]
* Whether to redraw the chart after the point is updated. If doing
* more operations on the chart, it is best practice to set
* `redraw` to false and call `chart.redraw()` after.
*
* @param {boolean|Partial<Highcharts.AnimationOptionsObject>} [animation=true]
* Whether to apply animation, and optionally animation
* configuration.
*
* @emits Highcharts.Point#event:update
*/
update(options, redraw, animation, runEvent) {
const point = this, series = point.series, graphic = point.graphic, chart = series.chart, seriesOptions = series.options;
let i;
redraw = pick(redraw, true);
/**
* @private
*/
function update() {
point.applyOptions(options);
// Update visuals, #4146
// Handle mock graphic elements for a11y, #12718
const hasMockGraphic = graphic && point.hasMockGraphic;
const shouldDestroyGraphic = point.y === null ?
!hasMockGraphic :
hasMockGraphic;
if (graphic && shouldDestroyGraphic) {
point.graphic = graphic.destroy();
delete point.hasMockGraphic;
}
if (isObject(options, true)) {
// Destroy so we can get new elements
if (graphic?.element) {
// "null" is also a valid symbol
if (options &&
options.marker &&
typeof options.marker.symbol !== 'undefined') {
point.graphic = graphic.destroy();
}
}
if (options?.dataLabels && point.dataLabel) {
point.dataLabel = point.dataLabel.destroy(); // #2468
}
}
// Record changes in the data table
i = point.index;
const row = {};
for (const key of series.dataColumnKeys()) {
row[key] = point[key];
}
series.dataTable.setRow(row, i);
// Record the options to options.data. If the old or the new config
// is an object, use point options, otherwise use raw options
// (#4701, #4916).
seriesOptions.data[i] = (isObject(seriesOptions.data[i], true) ||
isObject(options, true)) ?
point.options :
pick(options, seriesOptions.data[i]);
// Redraw
series.isDirty = series.isDirtyData = true;
if (!series.fixedBox && series.hasCartesianSeries) { // #1906, #2320
chart.isDirtyBox = true;
}
if (seriesOptions.legendType === 'point') { // #1831, #1885
chart.isDirtyLegend = true;
}
if (redraw) {
chart.redraw(animation);
}
}
// Fire the event with a default handler of doing the update
if (runEvent === false) { // When called from setData
update();
}
else {
point.firePointEvent('update', { options: options }, update);
}
}
/**
* Remove a point and optionally redraw the series and if necessary the axes
*
* @sample highcharts/plotoptions/series-point-events-remove/
* Remove point and confirm
* @sample highcharts/members/point-remove/
* Remove pie slice
* @sample maps/members/point-remove/
* Remove selected points in Highmaps
*
* @function Highcharts.Point#remove
*
* @param {boolean} [redraw=true]
* Whether to redraw the chart or wait for an explicit call. When
* doing more operations on the chart, for example running
* `point.remove()` in a loop, it is best practice to set `redraw`
* to false and call `chart.redraw()` after.
*
* @param {boolean|Partial<Highcharts.AnimationOptionsObject>} [animation=false]
* Whether to apply animation, and optionally animation
* configuration.
*/
remove(redraw, animation) {
this.series.removePoint(this.series.data.indexOf(this), redraw, animation);
}
/**
* Toggle the selection status of a point.
*
* @see Highcharts.Chart#getSelectedPoints
*
* @sample highcharts/members/point-select/
* Select a point from a button
* @sample highcharts/members/point-select-lasso/
* Lasso selection
* @sample highcharts/chart/events-selection-points/
* Rectangle selection
* @sample maps/series/data-id/
* Select a point in Highmaps
*
* @function Highcharts.Point#select
*
* @param {boolean} [selected]
* When `true`, the point is selected. When `false`, the point is
* unselected. When `null` or `undefined`, the selection state is toggled.
*
* @param {boolean} [accumulate=false]
* When `true`, the selection is added to other selected points.
* When `false`, other selected points are deselected. Internally in
* Highcharts, when
* [allowPointSelect](https://api.highcharts.com/highcharts/plotOptions.series.allowPointSelect)
* is `true`, selected points are accumulated on Control, Shift or Cmd
* clicking the point.
*
* @emits Highcharts.Point#event:select
* @emits Highcharts.Point#event:unselect
*/
select(selected, accumulate) {
const point = this, series = point.series, chart = series.chart;
selected = pick(selected, !point.selected);
this.selectedStaging = selected;
// Fire the event with the default handler
point.firePointEvent(selected ? 'select' : 'unselect', { accumulate: accumulate }, function () {
/**
* Whether the point is selected or not.
*
* @see Point#select
* @see Chart#getSelectedPoints
*
* @name Highcharts.Point#selected
* @type {boolean}
*/
point.selected = point.options.selected = selected;
series.options.data[series.data.indexOf(point)] =
point.options;
point.setState(selected && 'select');
// Unselect all other points unless Ctrl or Cmd + click
if (!accumulate) {
chart.getSelectedPoints().forEach(function (loopPoint) {
const loopSeries = loopPoint.series;
if (loopPoint.selected && loopPoint !== point) {
loopPoint.selected = loopPoint.options.selected =
false;
loopSeries.options.data[loopSeries.data.indexOf(loopPoint)] = loopPoint.options;
// Programmatically selecting a point should restore
// normal state, but when click happened on other
// point, set inactive state to match other points
loopPoint.setState(chart.hoverPoints &&
loopSeries.options.inactiveOtherPoints ?
'inactive' : '');
loopPoint.firePointEvent('unselect');
}
});
}
});
delete this.selectedStaging;
}
/**
* Runs on mouse over the point. Called internally from mouse and touch
* events.
*
* @function Highcharts.Point#onMouseOver
*
* @param {Highcharts.PointerEventObject} [e]
* The event arguments.
*/
onMouseOver(e) {
const point = this, series = point.series, { inverted, pointer } = series.chart;
if (pointer) {
e = e ?
pointer.normalize(e) :
// In cases where onMouseOver is called directly without an
// event
pointer.getChartCoordinatesFromPoint(point, inverted);
pointer.runPointActions(e, point);
}
}
/**
* Runs on mouse out from the point. Called internally from mouse and touch
* events.
*
* @function Highcharts.Point#onMouseOut
* @emits Highcharts.Point#event:mouseOut
*/
onMouseOut() {
const point = this, chart = point.series.chart;
point.firePointEvent('mouseOut');
if (!point.series.options.inactiveOtherPoints) {
(chart.hoverPoints || []).forEach(function (p) {
p.setState();
});
}
chart.hoverPoints = chart.hoverPoint = null;
}
/**
* Manage specific event from the series' and point's options. Only do it on
* demand, to save processing time on hovering.
*
* @private
* @function Highcharts.Point#importEvents
*/
manageEvent(eventType) {
const point = this, options = merge(point.series.options.point, point.options), userEvent = options.events?.[eventType];
if (isFunction(userEvent) &&
(!point.hcEvents?.[eventType] ||
// Some HC modules, like marker-clusters, draggable-poins etc.
// use events in their logic, so we need to be sure, that
// callback function is different
point.hcEvents?.[eventType]?.map((el) => el.fn)
.indexOf(userEvent) === -1)) {
// While updating the existing callback event the old one should be
// removed
point.importedUserEvent?.();
point.importedUserEvent = addEvent(point, eventType, userEvent);
if (point.hcEvents) {
point.hcEvents[eventType].userEvent = true;
}
}
else if (point.importedUserEvent &&
!userEvent &&
point.hcEvents?.[eventType] &&
point.hcEvents?.[eventType].userEvent) {
removeEvent(point, eventType);
delete point.hcEvents[eventType];
if (!Object.keys(point.hcEvents)) {
delete point.importedUserEvent;
}
}
}
/**
* Set the point's state.
*
* @function Highcharts.Point#setState
*
* @param {Highcharts.PointStateValue|""} [state]
* The new state, can be one of `'hover'`, `'select'`, `'inactive'`,
* or `''` (an empty string), `'normal'` or `undefined` to set to
* normal state.
* @param {boolean} [move]
* State for animation.
*
* @emits Highcharts.Point#event:afterSetState
*/
setState(state, move) {
const point = this, series = point.series, previousState = point.state, stateOptions = (series.options.states[state || 'normal'] ||
{}), markerOptions = (defaultOptions.plotOptions[series.type].marker &&
series.options.marker), normalDisabled = (markerOptions && markerOptions.enabled === false), markerStateOptions = markerOptions?.states?.[state || 'normal'] ||
{}, stateDisabled = markerStateOptions.enabled === false, pointMarker = point.marker || {}, chart = series.chart, hasMarkers = (markerOptions && series.markerAttribs);
let halo = series.halo, markerAttribs, pointAttribs, pointAttribsAnimation, stateMarkerGraphic = series.stateMarkerGraphic, newSymbol;
state = state || ''; // Empty string
if (
// Already has this state
(state === point.state && !move) ||
// Selected points don't respond to hover
(point.selected && state !== 'select') ||
// Series' state options is disabled
(stateOptions.enabled === false) ||
// General point marker's state options is disabled
(state && (stateDisabled ||
(normalDisabled &&
markerStateOptions.enabled === false))) ||
// Individual point marker's state options is disabled
(state &&
pointMarker.states &&
pointMarker.states[state] &&
pointMarker.states[state].enabled === false) // #1610
) {
return;
}
point.state = state;
if (hasMarkers) {
markerAttribs = series.markerAttribs(point, state);
}
// Apply hover styles to the existing point
// Prevent from mocked null points (#14966)
if (point.graphic && !point.hasMockGraphic) {
if (previousState) {
point.graphic.removeClass('highcharts-point-' + previousState);
}
if (state) {
point.graphic.addClass('highcharts-point-' + state);
}
if (!chart.styledMode) {
pointAttribs = series.pointAttribs(point, state);
pointAttribsAnimation = pick(chart.options.chart.animation, stateOptions.animation);
const opacity = pointAttribs.opacity;
// Some inactive points (e.g. slices in pie) should apply
// opacity also for their labels
if (series.options.inactiveOtherPoints && isNumber(opacity)) {
(point.dataLabels || []).forEach(function (label) {
if (label &&
!label.hasClass('highcharts-data-label-hidden')) {
label.animate({ opacity }, pointAttribsAnimation);
if (label.connector) {
label.connector.animate({ opacity }, pointAttribsAnimation);
}
}
});
}
point.graphic.animate(pointAttribs, pointAttribsAnimation);
}
if (markerAttribs) {
point.graphic.animate(markerAttribs, pick(
// Turn off globally:
chart.options.chart.animation, markerStateOptions.animation, markerOptions.animation));
}
// Zooming in from a range with no markers to a range with markers
if (stateMarkerGraphic) {
stateMarkerGraphic.hide();
}
}
else {
// If a graphic is not applied to each point in the normal state,
// create a shared graphic for the hover state
if (state && markerStateOptions) {
newSymbol = pointMarker.symbol || series.symbol;
// If the point has another symbol than the previous one, throw
// away the state marker graphic and force a new one (#1459)
if (stateMarkerGraphic &&
stateMarkerGraphic.currentSymbol !== newSymbol) {
stateMarkerGraphic = stateMarkerGraphic.destroy();
}
// Add a new state marker graphic
if (markerAttribs) {
if (!stateMarkerGraphic) {
if (newSymbol) {
series.stateMarkerGraphic = stateMarkerGraphic =
chart.renderer
.symbol(newSymbol, markerAttribs.x, markerAttribs.y, markerAttribs.width, markerAttribs.height, merge(markerOptions, markerStateOptions))
.add(series.markerGroup);
stateMarkerGraphic.currentSymbol = newSymbol;
}
// Move the existing graphic
}
else {
stateMarkerGraphic[move ? 'animate' : 'attr']({
x: markerAttribs.x,
y: markerAttribs.y
});
}
}
if (!chart.styledMode && stateMarkerGraphic &&
point.state !== 'inactive') {
stateMarkerGraphic.attr(series.pointAttribs(point, state));
}
}
if (stateMarkerGraphic) {
stateMarkerGraphic[state && point.isInside ? 'show' : 'hide'](); // #2450
stateMarkerGraphic.element.point = point; // #4310
stateMarkerGraphic.addClass(point.getClassName(), true);
}
}
// Show me your halo
const haloOptions = stateOptions.halo;
const markerGraphic = (point.graphic || stateMarkerGraphic);
const markerVisibility = markerGraphic?.visibility || 'inherit';
if (haloOptions?.size &&
markerGraphic &&
markerVisibility !== 'hidden' &&
!point.isCluster) {
if (!halo) {
series.halo = halo = chart.renderer.path()
// #5818, #5903, #6705
.add(markerGraphic.parentGroup);
}
halo.show()[move ? 'animate' : 'attr']({
d: point.haloPath(haloOptions.size)
});
halo.attr({
'class': 'highcharts-halo highcharts-color-' +
pick(point.colorIndex, series.colorIndex) +
(point.className ? ' ' + point.className : ''),
'visibility': markerVisibility,
'zIndex': -1 // #4929, #8276
});
halo.point = point; // #6055
if (!chart.styledMode) {
halo.attr(extend({
'fill': point.color || series.color,
'fill-opacity': haloOptions.opacity
}, AST.filterUserAttributes(haloOptions.attributes || {})));
}
}
else if (halo?.point?.haloPath &&
!halo.point.destroyed) {
// Animate back to 0 on the current halo point (#6055)
halo.animate({ d: halo.point.haloPath(0) }, null,
// Hide after unhovering. The `complete` callback runs in the
// halo's context (#7681).
halo.hide);
}
fireEvent(point, 'afterSetState', { state });
}
/**
* Get the path definition for the halo, which is usually a shadow-like
* circle around the currently hovered point.
*
* @function Highcharts.Point#haloPath
*
* @param {number} size
* The radius of the circular halo.
*
* @return {Highcharts.SVGPathArray}
* The path definition.
*/
haloPath(size) {
const pos = this.pos();
return pos ? this.series.chart.renderer.symbols.circle(crisp(pos[0], 1) - size, pos[1] - size, size * 2, size * 2) : [];
}
}
/* *
*
* Default Export
*
* */
export default Point;
/* *
*
* API Declarations
*
* */
/**
* Function callback when a series point is clicked. Return false to cancel the
* action.
*
* @callback Highcharts.PointClickCallbackFunction
*
* @param {Highcharts.Point} this
* The point where the event occurred.
*
* @param {Highcharts.PointClickEventObject} event
* Event arguments.
*/
/**
* Common information for a click event on a series point.
*
* @interface Highcharts.PointClickEventObject
* @extends Highcharts.PointerEventObject
*/ /**
* Clicked point.
* @name Highcharts.PointClickEventObject#point
* @type {Highcharts.Point}
*/
/**
* Gets fired when the mouse leaves the area close to the point.
*
* @callback Highcharts.PointMouseOutCallbackFunction
*
* @param {Highcharts.Point} this
* Point where the event occurred.
*
* @param {global.PointerEvent} event
* Event that occurred.
*/
/**
* Gets fired when the mouse enters the area close to the point.
*
* @callback Highcharts.PointMouseOverCallbackFunction
*
* @param {Highcharts.Point} this
* Point where the event occurred.
*
* @param {global.Event} event
* Event that occurred.
*/
/**
* The generic point options for all series.
*
* In TypeScript you have to extend `PointOptionsObject` with an additional
* declaration to allow custom data options:
*
* ```
* declare interface PointOptionsObject {
* customProperty: string;
* }
* ```
*
* @interface Highcharts.PointOptionsObject
*/
/**
* Possible option types for a data point. Use `null` to indicate a gap.
*
* @typedef {number|string|Highcharts.PointOptionsObject|Array<(number|string|null)>|null} Highcharts.PointOptionsType
*/
/**
* Gets fired when the point is removed using the `.remove()` method.
*
* @callback Highcharts.PointRemoveCallbackFunction
*
* @param {Highcharts.Point} this
* Point where the event occurred.
*
* @param {global.Event} event
* Event that occurred.
*/
/**
* Possible key values for the point state options.
*
* @typedef {"hover"|"inactive"|"normal"|"select"} Highcharts.PointStateValue
*/
/**
* Gets fired when the point is updated programmatically through the `.update()`
* method.
*
* @callback Highcharts.PointUpdateCallbackFunction
*
* @param {Highcharts.Point} this
* Point where the event occurred.
*
* @param {Highcharts.PointUpdateEventObject} event
* Event that occurred.
*/
/**
* Information about the update event.
*
* @interface Highcharts.PointUpdateEventObject
* @extends global.Event
*/ /**
* Options data of the update event.
* @name Highcharts.PointUpdateEventObject#options
* @type {Highcharts.PointOptionsType}
*/
/**
* @interface Highcharts.PointEventsOptionsObject
*/ /**
* Fires when the point is selected either programmatically or following a click
* on the point. One parameter, `event`, is passed to the function. Returning
* `false` cancels the operation.
* @name Highcharts.PointEventsOptionsObject#select
* @type {Highcharts.PointSelectCallbackFunction|undefined}
*/ /**
* Fires when the point is unselected either programmatically or following a
* click on the point. One parameter, `event`, is passed to the function.
* Returning `false` cancels the operation.
* @name Highcharts.PointEventsOptionsObject#unselect
* @type {Highcharts.PointUnselectCallbackFunction|undefined}
*/
/**
* Information about the select/unselect event.
*
* @interface Highcharts.PointInteractionEventObject
* @extends global.Event
*/ /**
* @name Highcharts.PointInteractionEventObject#accumulate
* @type {boolean}
*/
/**
* Gets fired when the point is selected either programmatically or following a
* click on the point.
*
* @callback Highcharts.PointSelectCallbackFunction
*
* @param {Highcharts.Point} this
* Point where the event occurred.
*
* @param {Highcharts.PointInteractionEventObject} event
* Event that occurred.
*/
/**
* Fires when the point is unselected either programmatically or following a
* click on the point.
*
* @callback Highcharts.PointUnselectCallbackFunction
*
* @param {Highcharts.Point} this
* Point where the event occurred.
*
* @param {Highcharts.PointInteractionEventObject} event
* Event that occurred.
*/
''; // Keeps doclets above in JS file.