highcharts
Version:
JavaScript charting framework
642 lines (641 loc) • 19.4 kB
JavaScript
/* *
*
* (c) 2009-2025 Highsoft, Black Label
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import A from '../../Core/Animation/AnimationUtilities.js';
const { getDeferredAnimation } = A;
import AnnotationChart from './AnnotationChart.js';
import AnnotationDefaults from './AnnotationDefaults.js';
import ControllableRect from './Controllables/ControllableRect.js';
import ControllableCircle from './Controllables/ControllableCircle.js';
import ControllableEllipse from './Controllables/ControllableEllipse.js';
import ControllablePath from './Controllables/ControllablePath.js';
import ControllableImage from './Controllables/ControllableImage.js';
import ControllableLabel from './Controllables/ControllableLabel.js';
import ControlPoint from './ControlPoint.js';
import ControlTarget from './ControlTarget.js';
import EventEmitter from './EventEmitter.js';
import MockPoint from './MockPoint.js';
import PopupComposition from './Popup/PopupComposition.js';
import U from '../../Core/Utilities.js';
const { destroyObjectProperties, erase, fireEvent, merge, pick, splat } = U;
/* *
*
* Functions
*
* */
/**
* Hide or show annotation attached to points.
* @private
*/
function adjustVisibility(item) {
const label = item.graphic, hasVisiblePoints = item.points.some((point) => (point.series.visible !== false &&
point.visible !== false));
if (label) {
if (!hasVisiblePoints) {
label.hide();
}
else if (label.visibility === 'hidden') {
label.show();
}
}
}
/**
* @private
*/
function getLabelsAndShapesOptions(baseOptions, newOptions) {
const mergedOptions = {};
['labels', 'shapes'].forEach((name) => {
const someBaseOptions = baseOptions[name], newOptionsValue = newOptions[name];
if (someBaseOptions) {
if (newOptionsValue) {
mergedOptions[name] = splat(newOptionsValue).map((basicOptions, i) => merge(someBaseOptions[i], basicOptions));
}
else {
mergedOptions[name] = baseOptions[name];
}
}
});
return mergedOptions;
}
/* *
*
* Class
*
* */
/**
* An annotation class which serves as a container for items like labels or
* shapes. Created items are positioned on the chart either by linking them to
* existing points or created mock points
*
* @requires modules/annotations
*
* @class
* @name Highcharts.Annotation
*
* @param {Highcharts.Chart} chart
* A chart instance
* @param {Highcharts.AnnotationsOptions} userOptions
* The annotation options
*/
class Annotation extends EventEmitter {
/* *
*
* Static Functions
*
* */
/**
* @private
*/
static compose(ChartClass, NavigationBindingsClass, PointerClass, SVGRendererClass) {
AnnotationChart.compose(Annotation, ChartClass, PointerClass);
ControllableLabel.compose(SVGRendererClass);
ControllablePath.compose(ChartClass, SVGRendererClass);
NavigationBindingsClass.compose(Annotation, ChartClass);
PopupComposition.compose(NavigationBindingsClass, PointerClass);
}
/* *
*
* Constructors
*
* */
constructor(chart, userOptions) {
super();
this.coll = 'annotations';
/**
* The chart that the annotation belongs to.
*
* @name Highcharts.Annotation#chart
* @type {Highcharts.Chart}
*/
this.chart = chart;
/**
* The array of points which defines the annotation.
* @private
* @name Highcharts.Annotation#points
* @type {Array<Highcharts.Point>}
*/
this.points = [];
/**
* The array of control points.
* @private
* @name Highcharts.Annotation#controlPoints
* @type {Array<Annotation.ControlPoint>}
*/
this.controlPoints = [];
this.coll = 'annotations';
this.index = -1;
/**
* The array of labels which belong to the annotation.
* @private
* @name Highcharts.Annotation#labels
* @type {Array<Highcharts.AnnotationLabelType>}
*/
this.labels = [];
/**
* The array of shapes which belong to the annotation.
* @private
* @name Highcharts.Annotation#shapes
* @type {Array<Highcharts.AnnotationShapeType>}
*/
this.shapes = [];
/**
* The options for the annotations.
*
* @name Highcharts.Annotation#options
* @type {Highcharts.AnnotationsOptions}
*/
this.options = merge(this.defaultOptions, userOptions);
/**
* The user options for the annotations.
*
* @name Highcharts.Annotation#userOptions
* @type {Highcharts.AnnotationsOptions}
*/
this.userOptions = userOptions;
// Handle labels and shapes - those are arrays
// Merging does not work with arrays (stores reference)
const labelsAndShapes = getLabelsAndShapesOptions(this.options, userOptions);
this.options.labels = labelsAndShapes.labels;
this.options.shapes = labelsAndShapes.shapes;
/**
* The callback that reports to the overlapping labels logic which
* labels it should account for.
* @private
* @name Highcharts.Annotation#labelCollector
* @type {Function}
*/
/**
* The group svg element.
*
* @name Highcharts.Annotation#group
* @type {Highcharts.SVGElement}
*/
/**
* The group svg element of the annotation's shapes.
*
* @name Highcharts.Annotation#shapesGroup
* @type {Highcharts.SVGElement}
*/
/**
* The group svg element of the annotation's labels.
*
* @name Highcharts.Annotation#labelsGroup
* @type {Highcharts.SVGElement}
*/
this.init(chart, this.options);
}
/* *
*
* Functions
*
* */
/**
* @private
*/
addClipPaths() {
this.setClipAxes();
if (this.clipXAxis &&
this.clipYAxis &&
this.options.crop // #15399
) {
this.clipRect = this.chart.renderer.clipRect(this.getClipBox());
}
}
/**
* @private
*/
addLabels() {
const labelsOptions = (this.options.labels || []);
labelsOptions.forEach((labelOptions, i) => {
const label = this.initLabel(labelOptions, i);
merge(true, labelsOptions[i], label.options);
});
}
/**
* @private
*/
addShapes() {
const shapes = this.options.shapes || [];
shapes.forEach((shapeOptions, i) => {
const shape = this.initShape(shapeOptions, i);
merge(true, shapes[i], shape.options);
});
}
/**
* Destroy the annotation. This function does not touch the chart
* that the annotation belongs to (all annotations are kept in
* the chart.annotations array) - it is recommended to use
* {@link Highcharts.Chart#removeAnnotation} instead.
* @private
*/
destroy() {
const chart = this.chart, destroyItem = function (item) {
item.destroy();
};
this.labels.forEach(destroyItem);
this.shapes.forEach(destroyItem);
this.clipXAxis = null;
this.clipYAxis = null;
erase(chart.labelCollectors, this.labelCollector);
super.destroy();
this.destroyControlTarget();
destroyObjectProperties(this, chart);
}
/**
* Destroy a single item.
* @private
*/
destroyItem(item) {
// Erase from shapes or labels array
erase(this[item.itemType + 's'], item);
item.destroy();
}
/**
* @private
*/
getClipBox() {
if (this.clipXAxis && this.clipYAxis) {
return {
x: this.clipXAxis.left,
y: this.clipYAxis.top,
width: this.clipXAxis.width,
height: this.clipYAxis.height
};
}
}
/**
* Initialize the annotation properties.
* @private
*/
initProperties(chart, userOptions) {
this.setOptions(userOptions);
const labelsAndShapes = getLabelsAndShapesOptions(this.options, userOptions);
this.options.labels = labelsAndShapes.labels;
this.options.shapes = labelsAndShapes.shapes;
this.chart = chart;
this.points = [];
this.controlPoints = [];
this.coll = 'annotations';
this.userOptions = userOptions;
this.labels = [];
this.shapes = [];
}
/**
* Initialize the annotation.
* @private
*/
init(_annotationOrChart, _userOptions, index = this.index) {
const chart = this.chart, animOptions = this.options.animation;
this.index = index;
this.linkPoints();
this.addControlPoints();
this.addShapes();
this.addLabels();
this.setLabelCollector();
this.animationConfig = getDeferredAnimation(chart, animOptions);
}
/**
* Initialisation of a single label
* @private
*/
initLabel(labelOptions, index) {
const options = merge(this.options.labelOptions, {
controlPointOptions: this.options.controlPointOptions
}, labelOptions), label = new ControllableLabel(this, options, index);
label.itemType = 'label';
this.labels.push(label);
return label;
}
/**
* Initialisation of a single shape
* @private
* @param {Object} shapeOptions
* a config object for a single shape
* @param {number} index
* annotation may have many shapes, this is the shape's index saved in
* shapes.index.
*/
initShape(shapeOptions, index) {
const options = merge(this.options.shapeOptions, {
controlPointOptions: this.options.controlPointOptions
}, shapeOptions), shape = new (Annotation.shapesMap[options.type])(this, options, index);
shape.itemType = 'shape';
this.shapes.push(shape);
return shape;
}
/**
* @private
*/
redraw(animation) {
this.linkPoints();
if (!this.graphic) {
this.render();
}
if (this.clipRect) {
this.clipRect.animate(this.getClipBox());
}
this.redrawItems(this.shapes, animation);
this.redrawItems(this.labels, animation);
this.redrawControlPoints(animation);
}
/**
* Redraw a single item.
* @private
*/
redrawItem(item, animation) {
item.linkPoints();
if (!item.shouldBeDrawn()) {
this.destroyItem(item);
}
else {
if (!item.graphic) {
this.renderItem(item);
}
item.redraw(pick(animation, true) && item.graphic.placed);
if (item.points.length) {
adjustVisibility(item);
}
}
}
/**
* @private
*/
redrawItems(items, animation) {
let i = items.length;
// Needs a backward loop. Labels/shapes array might be modified due to
// destruction of the item
while (i--) {
this.redrawItem(items[i], animation);
}
}
/**
* See {@link Highcharts.Chart#removeAnnotation}.
* @private
*/
remove() {
// Let chart.update() remove annotations on demand
return this.chart.removeAnnotation(this);
}
/**
* @private
*/
render() {
const renderer = this.chart.renderer;
this.graphic = renderer
.g('annotation')
.attr({
opacity: 0,
zIndex: this.options.zIndex,
visibility: this.options.visible ?
'inherit' :
'hidden'
})
.add();
this.shapesGroup = renderer
.g('annotation-shapes')
.add(this.graphic);
if (this.options.crop) { // #15399
this.shapesGroup.clip(this.chart.plotBoxClip);
}
this.labelsGroup = renderer
.g('annotation-labels')
.attr({
// `hideOverlappingLabels` requires translation
translateX: 0,
translateY: 0
})
.add(this.graphic);
this.addClipPaths();
if (this.clipRect) {
this.graphic.clip(this.clipRect);
}
// Render shapes and labels before adding events (#13070).
this.renderItems(this.shapes);
this.renderItems(this.labels);
this.addEvents();
this.renderControlPoints();
}
/**
* @private
*/
renderItem(item) {
item.render(item.itemType === 'label' ?
this.labelsGroup :
this.shapesGroup);
}
/**
* @private
*/
renderItems(items) {
let i = items.length;
while (i--) {
this.renderItem(items[i]);
}
}
/**
* @private
*/
setClipAxes() {
const xAxes = this.chart.xAxis, yAxes = this.chart.yAxis, linkedAxes = (this.options.labels || [])
.concat(this.options.shapes || [])
.reduce((axes, labelOrShape) => {
const point = labelOrShape &&
(labelOrShape.point ||
(labelOrShape.points && labelOrShape.points[0]));
return [
xAxes[point && point.xAxis] || axes[0],
yAxes[point && point.yAxis] || axes[1]
];
}, []);
this.clipXAxis = linkedAxes[0];
this.clipYAxis = linkedAxes[1];
}
/**
* @private
*/
setControlPointsVisibility(visible) {
const setItemControlPointsVisibility = function (item) {
item.setControlPointsVisibility(visible);
};
this.controlPoints.forEach((controlPoint) => {
controlPoint.setVisibility(visible);
});
this.shapes.forEach(setItemControlPointsVisibility);
this.labels.forEach(setItemControlPointsVisibility);
}
/**
* @private
*/
setLabelCollector() {
const annotation = this;
annotation.labelCollector = function () {
return annotation.labels.reduce(function (labels, label) {
if (!label.options.allowOverlap) {
labels.push(label.graphic);
}
return labels;
}, []);
};
annotation.chart.labelCollectors.push(annotation.labelCollector);
}
/**
* Set an annotation options.
* @private
* @param {Highcharts.AnnotationsOptions} userOptions
* User options for an annotation
*/
setOptions(userOptions) {
this.options = merge(this.defaultOptions, userOptions);
}
/**
* Set the annotation's visibility.
* @private
* @param {boolean} [visible]
* Whether to show or hide an annotation. If the param is omitted, the
* annotation's visibility is toggled.
*/
setVisibility(visible) {
const options = this.options, navigation = this.chart.navigationBindings, visibility = pick(visible, !options.visible);
this.graphic.attr('visibility', visibility ? 'inherit' : 'hidden');
if (!visibility) {
const setItemControlPointsVisibility = function (item) {
item.setControlPointsVisibility(visibility);
};
this.shapes.forEach(setItemControlPointsVisibility);
this.labels.forEach(setItemControlPointsVisibility);
if (navigation.activeAnnotation === this &&
navigation.popup &&
navigation.popup.type === 'annotation-toolbar') {
fireEvent(navigation, 'closePopup');
}
}
options.visible = visibility;
}
/**
* Updates an annotation.
*
* @function Highcharts.Annotation#update
*
* @param {Partial<Highcharts.AnnotationsOptions>} userOptions
* New user options for the annotation.
*
*/
update(userOptions, redraw) {
const chart = this.chart, labelsAndShapes = getLabelsAndShapesOptions(this.userOptions, userOptions), userOptionsIndex = chart.annotations.indexOf(this), options = merge(true, this.userOptions, userOptions);
options.labels = labelsAndShapes.labels;
options.shapes = labelsAndShapes.shapes;
this.destroy();
this.initProperties(chart, options);
this.init(chart, options);
// Update options in chart options, used in exporting (#9767, #21507):
chart.options.annotations[userOptionsIndex] = this.options;
this.isUpdating = true;
if (pick(redraw, true)) {
chart.drawAnnotations();
}
fireEvent(this, 'afterUpdate');
this.isUpdating = false;
}
}
/* *
*
* Static Properties
*
* */
/**
* @private
*/
Annotation.ControlPoint = ControlPoint;
/**
* @private
*/
Annotation.MockPoint = MockPoint;
/**
* An object uses for mapping between a shape type and a constructor.
* To add a new shape type extend this object with type name as a key
* and a constructor as its value.
*
* @private
*/
Annotation.shapesMap = {
'rect': ControllableRect,
'circle': ControllableCircle,
'ellipse': ControllableEllipse,
'path': ControllablePath,
'image': ControllableImage
};
/**
* @private
*/
Annotation.types = {};
Annotation.prototype.defaultOptions = AnnotationDefaults;
/**
* List of events for `annotation.options.events` that should not be
* added to `annotation.graphic` but to the `annotation`.
*
* @private
* @type {Array<string>}
*/
Annotation.prototype.nonDOMEvents = ['add', 'afterUpdate', 'drag', 'remove'];
ControlTarget.compose(Annotation);
/* *
*
* Default Export
*
* */
export default Annotation;
/* *
*
* API Declarations
*
* */
/**
* Possible directions for draggable annotations. An empty string (`''`)
* makes the annotation undraggable.
*
* @typedef {''|'x'|'xy'|'y'} Highcharts.AnnotationDraggableValue
* @requires modules/annotations
*/
/**
* @private
* @typedef {
* Highcharts.AnnotationControllableCircle|
* Highcharts.AnnotationControllableImage|
* Highcharts.AnnotationControllablePath|
* Highcharts.AnnotationControllableRect
* } Highcharts.AnnotationShapeType
* @requires modules/annotations
*/
/**
* @private
* @typedef {
* Highcharts.AnnotationControllableLabel
* } Highcharts.AnnotationLabelType
* @requires modules/annotations
*/
/**
* A point-like object, a mock point or a point used in series.
* @private
* @typedef {
* Highcharts.AnnotationMockPoint|
* Highcharts.Point
* } Highcharts.AnnotationPointType
* @requires modules/annotations
*/
/**
* Shape point as string, object or function.
*
* @typedef {
* string|
* Highcharts.AnnotationMockPointOptionsObject|
* Highcharts.AnnotationMockPointFunction
* } Highcharts.AnnotationShapePointOptions
*/
(''); // Keeps doclets above in JS file