/** * (c) 2009-2017 Highsoft, Black Label * * License: www.highcharts.com/license */ 'use strict'; import H from '../parts/Globals.js'; import '../parts/Utilities.js'; import '../parts/Chart.js'; import controllableMixin from './controllable/controllableMixin.js'; import ControllableRect from './controllable/ControllableRect.js'; import ControllableCircle from './controllable/ControllableCircle.js'; import ControllablePath from './controllable/ControllablePath.js'; import ControllableImage from './controllable/ControllableImage.js'; import ControllableLabel from './controllable/ControllableLabel.js'; import eventEmitterMixin from './eventEmitterMixin.js'; import MockPoint from './MockPoint.js'; import ControlPoint from './ControlPoint.js'; var merge = H.merge, addEvent = H.addEvent, defined = H.defined, erase = H.erase, find = H.find, isString = H.isString, pick = H.pick, reduce = H.reduce, splat = H.splat, destroyObjectProperties = H.destroyObjectProperties; /* ********************************************************************* * * ANNOTATION * ******************************************************************** */ /** * @typedef { * Annotation.ControllableCircle| * Annotation.ControllableImage| * Annotation.ControllablePath| * Annotation.ControllableRect * } * Annotation.Shape */ /** * @typedef {Annotation.ControllableLabel} Annotation.Label */ /** * 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 * * @class * @mixes Annotation.controllableMixin * @mixes Annotation.eventEmitterMixin * * @param {Highcharts.Chart} chart a chart instance * @param {AnnotationOptions} options the options object */ var Annotation = H.Annotation = function (chart, options) { var labelsAndShapes; /** * The chart that the annotation belongs to. * * @type {Highcharts.Chart} */ this.chart = chart; /** * The array of points which defines the annotation. * * @type {Array} */ this.points = []; /** * The array of control points. * * @type {Array} */ this.controlPoints = []; this.coll = 'annotations'; /** * The array of labels which belong to the annotation. * * @type {Array} */ this.labels = []; /** * The array of shapes which belong to the annotation. * * @type {Array} */ this.shapes = []; /** * The options for the annotations. * * @type {AnnotationOptions} */ // this.options = merge(this.defaultOptions, userOptions); this.options = options; /** * The user options for the annotations. * * @type {AnnotationOptions} */ this.userOptions = merge(true, {}, options); // Handle labels and shapes - those are arrays // Merging does not work with arrays (stores reference) labelsAndShapes = this.getLabelsAndShapesOptions( this.userOptions, options ); this.userOptions.labels = labelsAndShapes.labels; this.userOptions.shapes = labelsAndShapes.shapes; /** * The callback that reports to the overlapping-labels module which * labels it should account for. * * @name labelCollector * @memberOf Annotation# * @type {Function} */ /** * The group svg element. * * @name group * @memberOf Annotation# * @type {Highcharts.SVGElement} */ /** * The group svg element of the annotation's shapes. * * @name shapesGroup * @memberOf Annotation# * @type {Highcharts.SVGElement} */ /** * The group svg element of the annotation's labels. * * @name labelsGroup * @memberOf Annotation# * @type {Highcharts.SVGElement} */ this.init(chart, options); }; merge( true, Annotation.prototype, controllableMixin, eventEmitterMixin, /** @lends Annotation# */ { /** * A basic type of an annotation. It allows to add custom labels * or shapes. The items can be tied to points, axis coordinates * or chart pixel coordinates. * * @private * @type {Object} * @ignore-options base, annotations.crookedLine * @sample highcharts/annotations/basic/ * Basic annotations * @sample highcharts/demo/annotations/ * Advanced annotations * @sample highcharts/css/annotations * Styled mode * @sample highcharts/annotations-advanced/controllable * Controllable items * @sample {highstock} stock/annotations/fibonacci-retracements * Custom annotation, Fibonacci retracement * @since 6.0.0 * @optionparent annotations.crookedLine */ defaultOptions: { /** * Whether the annotation is visible. * * @sample highcharts/annotations/visible/ * Set annotation visibility */ visible: true, /** * Allow an annotation to be draggable by a user. Possible * values are `"x"`, `"xy"`, `"y"` and `""` (disabled). * * @type {string} * @validvalue ["x", "xy", "y", ""] */ draggable: 'xy', /** * Options for annotation's labels. Each label inherits options * from the labelOptions object. An option from the labelOptions * can be overwritten by config for a specific label. */ labelOptions: { /** * The alignment of the annotation's label. If right, * the right side of the label should be touching the point. * * @validvalue ["left", "center", "right"] * @sample highcharts/annotations/label-position/ * Set labels position */ align: 'center', /** * Whether to allow the annotation's labels to overlap. * To make the labels less sensitive for overlapping, * the can be set to 0. * * @sample highcharts/annotations/tooltip-like/ * Hide overlapping labels */ allowOverlap: false, /** * The background color or gradient for the annotation's label. * * @type {Color} * @sample highcharts/annotations/label-presentation/ * Set labels graphic options */ backgroundColor: 'rgba(0, 0, 0, 0.75)', /** * The border color for the annotation's label. * * @type {Color} * @sample highcharts/annotations/label-presentation/ * Set labels graphic options */ borderColor: 'black', /** * The border radius in pixels for the annotaiton's label. * * @sample highcharts/annotations/label-presentation/ * Set labels graphic options */ borderRadius: 3, /** * The border width in pixels for the annotation's label * * @sample highcharts/annotations/label-presentation/ * Set labels graphic options */ borderWidth: 1, /** * A class name for styling by CSS. * * @sample highcharts/css/annotations * Styled mode annotations * @since 6.0.5 */ className: '', /** * Whether to hide the annotation's label * that is outside the plot area. * * @sample highcharts/annotations/label-crop-overflow/ * Crop or justify labels */ crop: false, /** * The label's pixel distance from the point. * * @type {number} * @sample highcharts/annotations/label-position/ * Set labels position * @default undefined * @apioption annotations.crookedLine.labelOptions.distance */ /** * A [format](https://www.highcharts.com/docs/chart-concepts/labels-and-string-formatting) string for the data label. * * @type {string} * @see [plotOptions.series.dataLabels.format]( * plotOptions.series.dataLabels.format.html) * @sample highcharts/annotations/label-text/ * Set labels text * @default undefined * @apioption annotations.crookedLine.labelOptions.format */ /** * Alias for the format option. * * @type {string} * @see [format](annotations.labelOptions.format.html) * @sample highcharts/annotations/label-text/ * Set labels text * @default undefined * @apioption annotations.crookedLine.labelOptions.text */ /** * Callback JavaScript function to format * the annotation's label. Note that if a `format` or `text` * are defined, the format or text take precedence and * the formatter is ignored. `This` refers to a * point object. * * @type {function} * @sample highcharts/annotations/label-text/ * Set labels text * @default function () { * return defined(this.y) ? this.y : 'Annotation label'; * } */ formatter: function () { return defined(this.y) ? this.y : 'Annotation label'; }, /** * How to handle the annotation's label that flow * outside the plot area. The justify option aligns the label * inside the plot area. * * @validvalue ["none", "justify"] * @sample highcharts/annotations/label-crop-overflow/ * Crop or justify labels **/ overflow: 'justify', /** * When either the borderWidth or the backgroundColor is set, * this is the padding within the box. * * @sample highcharts/annotations/label-presentation/ * Set labels graphic options */ padding: 5, /** * The shadow of the box. The shadow can be * an object configuration containing * `color`, `offsetX`, `offsetY`, `opacity` and `width`. * * @type {Boolean|Object} * @sample highcharts/annotations/label-presentation/ * Set labels graphic options */ shadow: false, /** * The name of a symbol to use for the border around the label. * Symbols are predefined functions on the Renderer object. * * @type {string} * @sample highcharts/annotations/shapes/ * Available shapes for labels */ shape: 'callout', /** * Styles for the annotation's label. * * @type {CSSObject} * @sample highcharts/annotations/label-presentation/ * Set labels graphic options * @see [plotOptions.series.dataLabels.style]( * plotOptions.series.dataLabels.style.html) */ style: { fontSize: '11px', fontWeight: 'normal', color: 'contrast' }, /** * Whether to [use HTML](http://www.highcharts.com/docs/chart-concepts/labels-and-string-formatting#html) * to render the annotation's label. * * @type {boolean} * @default false */ useHTML: false, /** * The vertical alignment of the annotation's label. * * @type {string} * @validvalue ["top", "middle", "bottom"] * @sample highcharts/annotations/label-position/ * Set labels position */ verticalAlign: 'bottom', /** * The x position offset of the label relative to the point. * Note that if a `distance` is defined, the distance takes * precedence over `x` and `y` options. * * @sample highcharts/annotations/label-position/ * Set labels position */ x: 0, /** * The y position offset of the label relative to the point. * Note that if a `distance` is defined, the distance takes * precedence over `x` and `y` options. * * @sample highcharts/annotations/label-position/ * Set labels position */ y: -16 }, /** * An array of labels for the annotation. For options that apply to * multiple labels, they can be added to the * [labelOptions](annotations.labelOptions.html). * * @type {Array} * @extends annotations.crookedLine.labelOptions * @apioption annotations.crookedLine.labels */ /** * This option defines the point to which the label * will be connected. * It can be either the point which exists in the series - it is * referenced by the point's id - or a new point with defined x, y * properies and optionally axes. * * @type {string|MockPointOptions} * @sample highcharts/annotations/mock-point/ * Attach annotation to a mock point * @apioption annotations.crookedLine.labels.point */ /** * The x position of the point. Units can be either in axis * or chart pixel coordinates. * * @type {number} * @apioption annotations.crookedLine.labels.point.x */ /** * The y position of the point. Units can be either in axis * or chart pixel coordinates. * * @type {number} * @apioption annotations.crookedLine.labels.point.y */ /** * This number defines which xAxis the point is connected to. * It refers to either the axis id or the index of the axis * in the xAxis array. If the option is not configured or * the axis is not found the point's * x coordinate refers to the chart pixels. * * @type {number|string} * @apioption annotations.crookedLine.labels.point.xAxis */ /** * This number defines which yAxis the point is connected to. * It refers to either the axis id or the index of the axis * in the yAxis array. If the option is not configured or * the axis is not found the point's * y coordinate refers to the chart pixels. * * @type {number|string} * @apioption annotations.crookedLine.labels.point.yAxis */ /** * An array of shapes for the annotation. For options that apply to * multiple shapes, then can be added to the * [shapeOptions](annotations.shapeOptions.html). * * @type {Array} * @extends annotations.crookedLine.shapeOptions * @apioption annotations.crookedLine.shapes */ /** * This option defines the point to which the shape will be * connected. * It can be either the point which exists in the series - it is * referenced by the point's id - or a new point with defined x, y * properties and optionally axes. * * @type {string|MockPointOptions} * @extends annotations.crookedLine.labels.point * @apioption annotations.crookedLine.shapes.point */ /** * An array of points for the shape. This option is available * for shapes which can use multiple points such as path. * A point can be either a point object or a point's id. * * @type {Array} * @see [annotations.shapes.point](annotations.shapes.point.html) * @apioption annotations.crookedLine.shapes.points */ /** * Id of the marker which will be drawn at the final * vertex of the path. * Custom markers can be defined in defs property. * * @type {string} * @see [defs.markers](defs.markers.html) * @sample highcharts/annotations/custom-markers/ * Define a custom marker for annotations * @apioption annotations.crookedLine.shapes.markerEnd */ /** * Id of the marker which will be drawn at the first * vertex of the path. * Custom markers can be defined in defs property. * * @type {string} * @see [defs.markers](defs.markers.html) * @sample {highcharts} highcharts/annotations/custom-markers/ * Define a custom marker for annotations * @apioption annotations.crookedLine.shapes.markerStart */ /** * Options for annotation's shapes. Each shape inherits options * from the shapeOptions object. An option from the shapeOptions * can be overwritten by config for a specific shape. * * @type {Object} */ shapeOptions: { /** * The width of the shape. * * @type {number} * @sample highcharts/annotations/shape/ * Basic shape annotation * @apioption annotations.crookedLine.shapeOptions.width **/ /** * The height of the shape. * * @type {number} * @sample highcharts/annotations/shape/ * Basic shape annotation * @apioption annotations.crookedLine.shapeOptions.height */ /** * The color of the shape's stroke. * * @type {Color} * @sample highcharts/annotations/shape/ * Basic shape annotation */ stroke: 'rgba(0, 0, 0, 0.75)', /** * The pixel stroke width of the shape. * * @sample highcharts/annotations/shape/ * Basic shape annotation */ strokeWidth: 1, /** * The color of the shape's fill. * * @type {Color} * @sample highcharts/annotations/shape/ * Basic shape annotation */ fill: 'rgba(0, 0, 0, 0.75)', /** * The type of the shape, e.g. circle or rectangle. * * @type {string} * @sample highcharts/annotations/shape/ * Basic shape annotation * @default 'rect' * @apioption annotations.crookedLine.shapeOptions.type */ /** * The radius of the shape. * * @sample highcharts/annotations/shape/ * Basic shape annotation */ r: 0, /** * Defines additional snapping area around an annotation * making this annotation to focus. Defined in pixels. */ snap: 2 }, /** * Options for annotation's control points. Each control point * inherits options from controlPointOptions object. * Options from the controlPointOptions can be overwritten * by options in a specific control point. * * @type {Annotation.ControlPoint.Options} * @apioption annotations.crookedLine.controlPointOptions */ controlPointOptions: { symbol: 'circle', width: 10, height: 10, style: { stroke: 'black', 'stroke-width': 2, fill: 'white' }, visible: false, /** * @function {Annotation.ControlPoint.Positioner} * @apioption annotations.crookedLine.controlPointOptions.positioner */ events: {} }, /** * @type {Object} */ events: {}, /** * The Z index of the annotation. * * @type {number} * @default 6 */ zIndex: 6 }, /** * Initialize the annotation. * * @param {Highcharts.Chart} - the chart * @param {AnnotationOptions} - the user options for the annotation */ init: function () { this.linkPoints(); this.addControlPoints(); this.addShapes(); this.addLabels(); this.addClipPaths(); this.setLabelCollector(); }, getLabelsAndShapesOptions: function (baseOptions, newOptions) { var mergedOptions = {}; ['labels', 'shapes'].forEach(function (name) { if (baseOptions[name]) { mergedOptions[name] = splat(newOptions[name]).map( function (basicOptions, i) { return merge(baseOptions[name][i], basicOptions); } ); } }); return mergedOptions; }, addShapes: function () { (this.options.shapes || []).forEach(function (shapeOptions, i) { var shape = this.initShape(shapeOptions, i); this.options.shapes[i] = shape.options; }, this); }, addLabels: function () { (this.options.labels || []).forEach(function (labelOptions, i) { var label = this.initLabel(labelOptions, i); this.options.labels[i] = label.options; }, this); }, addClipPaths: function () { this.setClipAxes(); if (this.clipXAxis && this.clipYAxis) { this.clipRect = this.chart.renderer.clipRect( this.getClipBox() ); } }, setClipAxes: function () { var xAxes = this.chart.xAxis, yAxes = this.chart.yAxis, linkedAxes = reduce( (this.options.labels || []) .concat(this.options.shapes || []), function (axes, labelOrShape) { return [ xAxes[ labelOrShape && labelOrShape.point && labelOrShape.point.xAxis ] || axes[0], yAxes[ labelOrShape && labelOrShape.point && labelOrShape.point.yAxis ] || axes[1] ]; }, [] ); this.clipXAxis = linkedAxes[0]; this.clipYAxis = linkedAxes[1]; }, getClipBox: function () { return { x: this.clipXAxis.left, y: this.clipYAxis.top, width: this.clipXAxis.width, height: this.clipYAxis.height }; }, setLabelCollector: function () { var 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. * * @param {AnnotationOptions} - user options for an annotation */ setOptions: function (userOptions) { this.options = merge(this.defaultOptions, userOptions); }, redraw: function (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); controllableMixin.redraw.call(this, animation); }, /** * @param {Array<(Annotation.Label|Annotation.Shape)>} items * @param {boolean} [animation] */ redrawItems: function (items, animation) { var 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); } }, render: function () { var renderer = this.chart.renderer; this.graphic = renderer .g('annotation') .attr({ zIndex: this.options.zIndex, visibility: this.options.visible ? 'visible' : 'hidden' }) .add(); this.shapesGroup = renderer .g('annotation-shapes') .add(this.graphic) .clip(this.chart.plotBoxClip); this.labelsGroup = renderer .g('annotation-labels') .attr({ // hideOverlappingLabels requires translation translateX: 0, translateY: 0 }) .add(this.graphic); if (this.clipRect) { this.graphic.clip(this.clipRect); } this.addEvents(); controllableMixin.render.call(this); }, /** * Set the annotation's visibility. * * @param {Boolean} [visible] - Whether to show or hide an annotation. * If the param is omitted, the annotation's visibility is toggled. */ setVisibility: function (visibility) { var options = this.options, visible = pick(visibility, !options.visible); this.graphic.attr( 'visibility', visible ? 'visible' : 'hidden' ); if (!visible) { this.setControlPointsVisibility(false); } options.visible = visible; }, setControlPointsVisibility: function (visible) { var setItemControlPointsVisibility = function (item) { item.setControlPointsVisibility(visible); }; controllableMixin.setControlPointsVisibility.call( this, visible ); this.shapes.forEach(setItemControlPointsVisibility); this.labels.forEach(setItemControlPointsVisibility); }, /** * 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. */ destroy: function () { var 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); eventEmitterMixin.destroy.call(this); controllableMixin.destroy.call(this); destroyObjectProperties(this, chart); }, /** * See {@link Highcharts.Annotation#destroy}. */ remove: function () { return this.destroy(); }, update: function (userOptions) { var chart = this.chart, labelsAndShapes = this.getLabelsAndShapesOptions( this.userOptions, userOptions ), userOptionsIndex = chart.annotations.indexOf(this), options = H.merge(true, this.userOptions, userOptions); options.labels = labelsAndShapes.labels; options.shapes = labelsAndShapes.shapes; this.destroy(); this.constructor(chart, options); // Update options in chart options, used in exporting (#9767): chart.options.annotations[userOptionsIndex] = options; this.redraw(); }, /* ************************************************************* * ITEM SECTION * Contains methods for handling a single item in an annotation **************************************************************** */ /** * Initialisation of a single shape * * @param {Object} shapeOptions - a confg object for a single shape **/ initShape: function (shapeOptions, index) { var 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; }, /** * Initialisation of a single label * * @param {Object} labelOptions **/ initLabel: function (labelOptions, index) { var 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; }, /** * Redraw a single item. * * @param {Annotation.Label|Annotation.Shape} item * @param {boolean} [animation] */ redrawItem: function (item, animation) { item.linkPoints(); if (!item.shouldBeDrawn()) { this.destroyItem(item); } else { if (!item.graphic) { this.renderItem(item); } item.redraw( H.pick(animation, true) && item.graphic.placed ); if (item.points.length) { this.adjustVisibility(item); } } }, /** * Hide or show annotaiton attached to points. * * @param {Annotation.Label|Annotation.Shape} item */ adjustVisibility: function (item) { // #9481 var hasVisiblePoints = false, label = item.graphic; item.points.forEach(function (point) { if ( point.series.visible !== false && point.visible !== false ) { hasVisiblePoints = true; } }); if (!hasVisiblePoints) { label.hide(); } else if (label.visibility === 'hidden') { label.show(); } }, /** * Destroy a single item. * * @param {Annotation.Label|Annotation.Shape} item */ destroyItem: function (item) { // erase from shapes or labels array erase(this[item.itemType + 's'], item); item.destroy(); }, /* * @private */ renderItem: function (item) { item.render( item.itemType === 'label' ? this.labelsGroup : this.shapesGroup ); } } ); /** * 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. **/ Annotation.shapesMap = { 'rect': ControllableRect, 'circle': ControllableCircle, 'path': ControllablePath, 'image': ControllableImage }; Annotation.types = {}; Annotation.MockPoint = MockPoint; Annotation.ControlPoint = ControlPoint; H.extendAnnotation = function ( Constructor, BaseConstructor, prototype, defaultOptions ) { BaseConstructor = BaseConstructor || Annotation; merge( true, Constructor.prototype, BaseConstructor.prototype, prototype ); Constructor.prototype.defaultOptions = merge( Constructor.prototype.defaultOptions, defaultOptions || {} ); }; /* ********************************************************************* * * EXTENDING CHART PROTOTYPE * ******************************************************************** */ // Let chart.update() work with annotations H.Chart.prototype.collectionsWithUpdate.push('annotations'); H.extend(H.Chart.prototype, /** @lends Highcharts.Chart# */ { initAnnotation: function (userOptions) { var Constructor = Annotation.types[userOptions.type] || Annotation, options = H.merge( Constructor.prototype.defaultOptions, userOptions ), annotation = new Constructor(this, options); this.annotations.push(annotation); return annotation; }, /** * Add an annotation to the chart after render time. * * @param {AnnotationOptions} options * The annotation options for the new, detailed annotation. * @param {boolean} [redraw] * * @return {Highcharts.Annotation} - The newly generated annotation. */ addAnnotation: function (userOptions, redraw) { var annotation = this.initAnnotation(userOptions); this.options.annotations.push(annotation.options); if (pick(redraw, true)) { annotation.redraw(); } return annotation; }, /** * Remove an annotation from the chart. * * @param {String|Annotation} idOrAnnotation - The annotation's id or * direct annotation object. */ removeAnnotation: function (idOrAnnotation) { var annotations = this.annotations, annotation = isString(idOrAnnotation) ? find( annotations, function (annotation) { return annotation.options.id === idOrAnnotation; } ) : idOrAnnotation; if (annotation) { erase(this.options.annotations, annotation.options); erase(annotations, annotation); annotation.destroy(); } }, drawAnnotations: function () { this.plotBoxClip.attr(this.plotBox); this.annotations.forEach(function (annotation) { annotation.redraw(); }); } }); H.Chart.prototype.callbacks.push(function (chart) { chart.annotations = []; if (!chart.options.annotations) { chart.options.annotations = []; } chart.plotBoxClip = this.renderer.clipRect(this.plotBox); chart.controlPointsGroup = chart.renderer .g('control-points') .attr({ zIndex: 99 }) .clip(chart.plotBoxClip) .add(); chart.options.annotations.forEach(function (annotationOptions, i) { var annotation = chart.initAnnotation(annotationOptions); chart.options.annotations[i] = annotation.options; }); chart.drawAnnotations(); addEvent(chart, 'redraw', chart.drawAnnotations); addEvent(chart, 'destroy', function () { chart.plotBoxClip.destroy(); chart.controlPointsGroup.destroy(); }); });