(function (Highcharts, HighchartsAdapter) { var UNDEFINED, ALIGN_FACTOR, ALLOWED_SHAPES, Chart = Highcharts.Chart, extend = Highcharts.extend, each = Highcharts.each; ALLOWED_SHAPES = ["path", "rect", "circle"]; ALIGN_FACTOR = { top: 0, left: 0, center: 0.5, middle: 0.5, bottom: 1, right: 1 }; // Highcharts helper methods var inArray = HighchartsAdapter.inArray, merge = Highcharts.merge; function defaultOptions(shapeType) { var shapeOptions, options; options = { xAxis: 0, yAxis: 0, title: { style: {}, text: "", x: 0, y: 0 }, shape: { params: { stroke: "#000000", fill: "transparent", strokeWidth: 2 } } }; shapeOptions = { circle: { params: { x: 0, y: 0 } } }; if (shapeOptions[shapeType]) { options.shape = merge(options.shape, shapeOptions[shapeType]); } return options; } function isArray(obj) { return Object.prototype.toString.call(obj) === '[object Array]'; } function isNumber(n) { return typeof n === 'number'; } function defined(obj) { return obj !== UNDEFINED && obj !== null; } function translatePath(d, xAxis, yAxis, xOffset, yOffset) { var len = d.length, i = 0; while (i < len) { if (typeof d[i] === 'number' && typeof d[i + 1] === 'number') { d[i] = xAxis.toPixels(d[i]) - xOffset; d[i + 1] = yAxis.toPixels(d[i + 1]) - yOffset; i += 2; } else { i += 1; } } return d; } // Define annotation prototype var Annotation = function () { this.init.apply(this, arguments); }; Annotation.prototype = { /* * Initialize the annotation */ init: function (chart, options) { var shapeType = options.shape && options.shape.type; this.chart = chart; this.options = merge({}, defaultOptions(shapeType), options); }, /* * Render the annotation */ render: function (redraw) { var annotation = this, chart = this.chart, renderer = annotation.chart.renderer, group = annotation.group, title = annotation.title, shape = annotation.shape, options = annotation.options, titleOptions = options.title, shapeOptions = options.shape; if (!group) { group = annotation.group = renderer.g(); } if (!shape && shapeOptions && inArray(shapeOptions.type, ALLOWED_SHAPES) !== -1) { shape = annotation.shape = renderer[options.shape.type](shapeOptions.params); shape.add(group); } if (!title && titleOptions) { title = annotation.title = renderer.label(titleOptions); title.add(group); } group.add(chart.annotations.group); // link annotations to point or series annotation.linkObjects(); if (redraw !== false) { annotation.redraw(); } }, /* * Redraw the annotation title or shape after options update */ redraw: function () { var options = this.options, chart = this.chart, group = this.group, title = this.title, shape = this.shape, linkedTo = this.linkedObject, xAxis = chart.xAxis[options.xAxis], yAxis = chart.yAxis[options.yAxis], width = options.width, height = options.height, anchorY = ALIGN_FACTOR[options.anchorY], anchorX = ALIGN_FACTOR[options.anchorX], resetBBox = false, shapeParams, linkType, series, param, bbox, x, y; if (linkedTo) { linkType = (linkedTo instanceof Highcharts.Point) ? 'point' : (linkedTo instanceof Highcharts.Series) ? 'series' : null; if (linkType === 'point') { options.xValue = linkedTo.x; options.yValue = linkedTo.y; series = linkedTo.series; } else if (linkType === 'series') { series = linkedTo; } if (group.visibility !== series.group.visibility) { group.attr({ visibility: series.group.visibility }); } } // Based on given options find annotation pixel position x = (defined(options.xValue) ? xAxis.toPixels(options.xValue + xAxis.minPointOffset) - xAxis.minPixelPadding : options.x); y = defined(options.yValue) ? yAxis.toPixels(options.yValue) : options.y; if (isNaN(x) || isNaN(y) || !isNumber(x) || !isNumber(y)) { return; } if (title) { title.attr(options.title); title.css(options.title.style); resetBBox = true; } if (shape) { shapeParams = extend({}, options.shape.params); if (options.units === 'values') { for (param in shapeParams) { if (inArray(param, ['width', 'x']) > -1) { shapeParams[param] = xAxis.translate(shapeParams[param]); } else if (inArray(param, ['height', 'y']) > -1) { shapeParams[param] = yAxis.translate(shapeParams[param]); } } if (shapeParams.width) { shapeParams.width -= xAxis.toPixels(0) - xAxis.left; } if (shapeParams.x) { shapeParams.x += xAxis.minPixelPadding; } if (options.shape.type === 'path') { translatePath(shapeParams.d, xAxis, yAxis, x, y); } } // move the center of the circle to shape x/y if (options.shape.type === 'circle') { shapeParams.x += shapeParams.r; shapeParams.y += shapeParams.r; } resetBBox = true; shape.attr(shapeParams); } group.bBox = null; // If annotation width or height is not defined in options use bounding box size if (!isNumber(width)) { bbox = group.getBBox(); width = bbox.width; } if (!isNumber(height)) { // get bbox only if it wasn't set before if (!bbox) { bbox = group.getBBox(); } height = bbox.height; } // Calculate anchor point if (!isNumber(anchorX)) { anchorX = ALIGN_FACTOR.center; } if (!isNumber(anchorY)) { anchorY = ALIGN_FACTOR.center; } // Translate group according to its dimension and anchor point x = x - width * anchorX; y = y - height * anchorY; if (chart.animation && defined(group.translateX) && defined(group.translateY)) { group.animate({ translateX: x, translateY: y }); } else { group.translate(x, y); } }, /* * Destroy the annotation */ destroy: function () { var annotation = this, chart = this.chart, allItems = chart.annotations.allItems, index = allItems.indexOf(annotation); if (index > -1) { allItems.splice(index, 1); } each(['title', 'shape', 'group'], function (element) { if (annotation[element]) { annotation[element].destroy(); annotation[element] = null; } }); annotation.group = annotation.title = annotation.shape = annotation.chart = annotation.options = null; }, /* * Update the annotation with a given options */ update: function (options, redraw) { extend(this.options, options); // update link to point or series this.linkObjects(); this.render(redraw); }, linkObjects: function () { var annotation = this, chart = annotation.chart, linkedTo = annotation.linkedObject, linkedId = linkedTo && (linkedTo.id || linkedTo.options.id), options = annotation.options, id = options.linkedTo; if (!defined(id)) { annotation.linkedObject = null; } else if (!defined(linkedTo) || id !== linkedId) { annotation.linkedObject = chart.get(id); } } }; // Add annotations methods to chart prototype extend(Chart.prototype, { annotations: { /* * Unified method for adding annotations to the chart */ add: function (options, redraw) { var annotations = this.allItems, chart = this.chart, item, len; if (!isArray(options)) { options = [options]; } len = options.length; while (len--) { item = new Annotation(chart, options[len]); annotations.push(item); item.render(redraw); } }, /** * Redraw all annotations, method used in chart events */ redraw: function () { each(this.allItems, function (annotation) { annotation.redraw(); }); } } }); // Initialize on chart load Chart.prototype.callbacks.push(function (chart) { var options = chart.options.annotations, group; group = chart.renderer.g("annotations"); group.attr({ zIndex: 7 }); group.add(); // initialize empty array for annotations chart.annotations.allItems = []; // link chart object to annotations chart.annotations.chart = chart; // link annotations group element to the chart chart.annotations.group = group; if (isArray(options) && options.length > 0) { chart.annotations.add(chart.options.annotations); } // update annotations after chart redraw Highcharts.addEvent(chart, 'redraw', function () { chart.annotations.redraw(); }); }); }(Highcharts, HighchartsAdapter));