package.es-modules.Extensions.Annotations.Annotation.js Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of highcharts Show documentation
Show all versions of highcharts Show documentation
JavaScript charting framework
The newest version!
/* *
*
* (c) 2009-2024 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];
if (someBaseOptions) {
if (newOptions[name]) {
mergedOptions[name] = splat(newOptions[name]).map(function (basicOptions, i) {
return 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}
*/
this.points = [];
/**
* The array of control points.
* @private
* @name Highcharts.Annotation#controlPoints
* @type {Array}
*/
this.controlPoints = [];
this.coll = 'annotations';
this.index = -1;
/**
* The array of labels which belong to the annotation.
* @private
* @name Highcharts.Annotation#labels
* @type {Array}
*/
this.labels = [];
/**
* The array of shapes which belong to the annotation.
* @private
* @name Highcharts.Annotation#shapes
* @type {Array}
*/
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 module 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} 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}
*/
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