UNPKG

highcharts

Version:
1,614 lines (1,480 loc) 176 kB
/* * * * (c) 2010-2019 Torstein Honsi * * License: www.highcharts.com/license * * */ /** * The horizontal alignment of an element. * * @typedef {"center"|"left"|"right"} Highcharts.AlignValue */ /** * Options to align the element relative to the chart or another box. * * @interface Highcharts.AlignObject *//** * Horizontal alignment. Can be one of `left`, `center` and `right`. * * @name Highcharts.AlignObject#align * @type {Highcharts.AlignValue|undefined} * * @default left *//** * Vertical alignment. Can be one of `top`, `middle` and `bottom`. * * @name Highcharts.AlignObject#verticalAlign * @type {Highcharts.VerticalAlignValue|undefined} * * @default top *//** * Horizontal pixel offset from alignment. * * @name Highcharts.AlignObject#x * @type {number|undefined} * * @default 0 *//** * Vertical pixel offset from alignment. * * @name Highcharts.AlignObject#y * @type {number|undefined} * * @default 0 *//** * Use the `transform` attribute with translateX and translateY custom * attributes to align this elements rather than `x` and `y` attributes. * * @name Highcharts.AlignObject#alignByTranslate * @type {boolean|undefined} * * @default false */ /** * Bounding box of an element. * * @interface Highcharts.BBoxObject *//** * Height of the bounding box. * * @name Highcharts.BBoxObject#height * @type {number} *//** * Width of the bounding box. * * @name Highcharts.BBoxObject#width * @type {number} *//** * Horizontal position of the bounding box. * * @name Highcharts.BBoxObject#x * @type {number} *//** * Vertical position of the bounding box. * * @name Highcharts.BBoxObject#y * @type {number} */ /** * A clipping rectangle that can be applied to one or more {@link SVGElement} * instances. It is instanciated with the {@link SVGRenderer#clipRect} function * and applied with the {@link SVGElement#clip} function. * * @example * var circle = renderer.circle(100, 100, 100) * .attr({ fill: 'red' }) * .add(); * var clipRect = renderer.clipRect(100, 100, 100, 100); * * // Leave only the lower right quarter visible * circle.clip(clipRect); * * @typedef {Highcharts.SVGElement} Highcharts.ClipRectElement */ /** * The font metrics. * * @interface Highcharts.FontMetricsObject *//** * The baseline relative to the top of the box. * * @name Highcharts.FontMetricsObject#b * @type {number} *//** * The line height. * * @name Highcharts.FontMetricsObject#h * @type {number} *//** * The font size. * * @name Highcharts.FontMetricsObject#f * @type {number} */ /** * Gradient options instead of a solid color. * * @example * // Linear gradient used as a color option * color: { * linearGradient: { x1: 0, x2: 0, y1: 0, y2: 1 }, * stops: [ * [0, '#003399'], // start * [0.5, '#ffffff'], // middle * [1, '#3366AA'] // end * ] * } * } * * @interface Highcharts.GradientColorObject *//** * Holds an object that defines the start position and the end position relative * to the shape. * @name Highcharts.GradientColorObject#linearGradient * @type {Highcharts.LinearGradientColorObject|undefined} *//** * Holds an object that defines the center position and the radius. * @name Highcharts.GradientColorObject#radialGradient * @type {Highcharts.RadialGradientColorObject|undefined} *//** * The first item in each tuple is the position in the gradient, where 0 is the * start of the gradient and 1 is the end of the gradient. Multiple stops can be * applied. The second item is the color for each stop. This color can also be * given in the rgba format. * @name Highcharts.GradientColorObject#stops * @type {Array<Array<number,Highcharts.ColorString>>|undefined} */ /** * Defines the start position and the end position for a gradient relative * to the shape. Start position (x1, y1) and end position (x2, y2) are relative * to the shape, where 0 means top/left and 1 is bottom/right. * * @interface Highcharts.LinearGradientColorObject *//** * Start horizontal position of the gradient. Float ranges 0-1. * @name Highcharts.LinearGradientColorObject#x1 * @type {number} *//** * End horizontal position of the gradient. Float ranges 0-1. * @name Highcharts.LinearGradientColorObject#x2 * @type {number} *//** * Start vertical position of the gradient. Float ranges 0-1. * @name Highcharts.LinearGradientColorObject#y1 * @type {number} *//** * End vertical position of the gradient. Float ranges 0-1. * @name Highcharts.LinearGradientColorObject#y2 * @type {number} */ /** * An object containing `x` and `y` properties for the position of an element. * * @interface Highcharts.PositionObject *//** * X position of the element. * @name Highcharts.PositionObject#x * @type {number} *//** * Y position of the element. * @name Highcharts.PositionObject#y * @type {number} */ /** * Defines the center position and the radius for a gradient. * * @interface Highcharts.RadialGradientColorObject *//** * Center horizontal position relative to the shape. Float ranges 0-1. * @name Highcharts.RadialGradientColorObject#cx * @type {number} *//** * Center vertical position relative to the shape. Float ranges 0-1. * @name Highcharts.RadialGradientColorObject#cy * @type {number} *//** * Radius relative to the shape. Float ranges 0-1. * @name Highcharts.RadialGradientColorObject#r * @type {number} */ /** * A rectangle. * * @interface Highcharts.RectangleObject *//** * Height of the rectangle. * @name Highcharts.RectangleObject#height * @type {number} *//** * Width of the rectangle. * @name Highcharts.RectangleObject#width * @type {number} *//** * Horizontal position of the rectangle. * @name Highcharts.RectangleObject#x * @type {number} *//** * Vertical position of the rectangle. * @name Highcharts.RectangleObject#y * @type {number} */ /** * The shadow options. * * @interface Highcharts.ShadowOptionsObject *//** * The shadow color. * @name Highcharts.ShadowOptionsObject#color * @type {string|undefined} * @default #000000 *//** * The horizontal offset from the element. * * @name Highcharts.ShadowOptionsObject#offsetX * @type {number|undefined} * @default 1 *//** * The vertical offset from the element. * @name Highcharts.ShadowOptionsObject#offsetY * @type {number|undefined} * @default 1 *//** * The shadow opacity. * * @name Highcharts.ShadowOptionsObject#opacity * @type {number|undefined} * @default 0.15 *//** * The shadow width or distance from the element. * @name Highcharts.ShadowOptionsObject#width * @type {number|undefined} * @default 3 */ /** * An object of key-value pairs for SVG attributes. Attributes in Highcharts * elements for the most parts correspond to SVG, but some are specific to * Highcharts, like `zIndex`, `rotation`, `rotationOriginX`, * `rotationOriginY`, `translateX`, `translateY`, `scaleX` and `scaleY`. SVG * attributes containing a hyphen are _not_ camel-cased, they should be * quoted to preserve the hyphen. * * @example * { * 'stroke': '#ff0000', // basic * 'stroke-width': 2, // hyphenated * 'rotation': 45 // custom * 'd': ['M', 10, 10, 'L', 30, 30, 'z'] // path definition, note format * } * * @interface Highcharts.SVGAttributes *//** * @name Highcharts.SVGAttributes#[key:string] * @type {*} *//** * @name Highcharts.SVGAttributes#d * @type {string|Highcharts.SVGPathArray|undefined} *//** * @name Highcharts.SVGAttributes#fill * @type {Highcharts.ColorString|undefined} *//** * @name Highcharts.SVGAttributes#inverted * @type {boolean|undefined} *//** * @name Highcharts.SVGAttributes#matrix * @type {Array<number>|undefined} *//** * @name Highcharts.SVGAttributes#rotation * @type {string|undefined} *//** * @name Highcharts.SVGAttributes#rotationOriginX * @type {number|undefined} *//** * @name Highcharts.SVGAttributes#rotationOriginY * @type {number|undefined} *//** * @name Highcharts.SVGAttributes#scaleX * @type {number|undefined} *//** * @name Highcharts.SVGAttributes#scaleY * @type {number|undefined} *//** * @name Highcharts.SVGAttributes#stroke * @type {Highcharts.ColorString|undefined} *//** * @name Highcharts.SVGAttributes#style * @type {string|Highcharts.CSSObject|undefined} *//** * @name Highcharts.SVGAttributes#translateX * @type {number|undefined} *//** * @name Highcharts.SVGAttributes#translateY * @type {number|undefined} *//** * @name Highcharts.SVGAttributes#zIndex * @type {number|undefined} */ /** * Serialized form of an SVG definition, including children. Some key * property names are reserved: tagName, textContent, and children. * * @interface Highcharts.SVGDefinitionObject *//** * @name Highcharts.SVGDefinitionObject#[key:string] * @type {number|string|Array<Highcharts.SVGDefinitionObject>|undefined} *//** * @name Highcharts.SVGDefinitionObject#children * @type {Array<Highcharts.SVGDefinitionObject>|undefined} *//** * @name Highcharts.SVGDefinitionObject#tagName * @type {string|undefined} *//** * @name Highcharts.SVGDefinitionObject#textContent * @type {string|undefined} */ /** * An SVG DOM element. The type is a reference to the regular SVGElement in the * global scope. * * @typedef {globals.GlobalSVGElement} Highcharts.SVGDOMElement * * @see https://developer.mozilla.org/en-US/docs/Web/API/SVGElement */ /** * Array of path commands, that will go into the `d` attribute of an SVG * element. * * @typedef {Array<number|Highcharts.SVGPathCommand>} Highcharts.SVGPathArray */ /** * Possible path commands in a SVG path array. * * @typedef {string} Highcharts.SVGPathCommand * @validvalue ["a","c","h","l","m","q","s","t","v","z","A","C","H","L","M","Q","S","T","V","Z"] */ /** * An extendable collection of functions for defining symbol paths. Symbols are * used internally for point markers, button and label borders and backgrounds, * or custom shapes. Extendable by adding to {@link SVGRenderer#symbols}. * * @interface Highcharts.SymbolDictionary *//** * @name Highcharts.SymbolDictionary#[key:string] * @type {Function|undefined} *//** * @name Highcharts.SymbolDictionary#arc * @type {Function|undefined} *//** * @name Highcharts.SymbolDictionary#callout * @type {Function|undefined} *//** * @name Highcharts.SymbolDictionary#circle * @type {Function|undefined} *//** * @name Highcharts.SymbolDictionary#diamond * @type {Function|undefined} *//** * @name Highcharts.SymbolDictionary#square * @type {Function|undefined} *//** * @name Highcharts.SymbolDictionary#triangle * @type {Function|undefined} */ /** * Can be one of `arc`, `callout`, `circle`, `diamond`, `square`, `triangle`, * and `triangle-down`. Symbols are used internally for point markers, button * and label borders and backgrounds, or custom shapes. Extendable by adding to * {@link SVGRenderer#symbols}. * * @typedef {"arc"|"callout"|"circle"|"diamond"|"square"|"triangle"|"triangle-down"} Highcharts.SymbolKeyValue */ /** * Additional options, depending on the actual symbol drawn. * * @interface Highcharts.SymbolOptionsObject *//** * The anchor X position for the `callout` symbol. This is where the chevron * points to. * * @name Highcharts.SymbolOptionsObject#anchorX * @type {number|undefined} *//** * The anchor Y position for the `callout` symbol. This is where the chevron * points to. * * @name Highcharts.SymbolOptionsObject#anchorY * @type {number|undefined} *//** * The end angle of an `arc` symbol. * * @name Highcharts.SymbolOptionsObject#end * @type {number|undefined} *//** * Whether to draw `arc` symbol open or closed. * * @name Highcharts.SymbolOptionsObject#open * @type {boolean|undefined} *//** * The radius of an `arc` symbol, or the border radius for the `callout` symbol. * * @name Highcharts.SymbolOptionsObject#r * @type {number|undefined} *//** * The start angle of an `arc` symbol. * * @name Highcharts.SymbolOptionsObject#start * @type {number|undefined} */ /** * The vertical alignment of an element. * * @typedef {"bottom"|"middle"|"top"} Highcharts.VerticalAlignValue */ 'use strict'; import H from './Globals.js'; import './Utilities.js'; import './Color.js'; var SVGElement, SVGRenderer, addEvent = H.addEvent, animate = H.animate, attr = H.attr, charts = H.charts, color = H.color, css = H.css, createElement = H.createElement, defined = H.defined, deg2rad = H.deg2rad, destroyObjectProperties = H.destroyObjectProperties, doc = H.doc, extend = H.extend, erase = H.erase, hasTouch = H.hasTouch, isArray = H.isArray, isFirefox = H.isFirefox, isMS = H.isMS, isObject = H.isObject, isString = H.isString, isWebKit = H.isWebKit, merge = H.merge, noop = H.noop, objectEach = H.objectEach, pick = H.pick, pInt = H.pInt, removeEvent = H.removeEvent, splat = H.splat, stop = H.stop, svg = H.svg, SVG_NS = H.SVG_NS, symbolSizes = H.symbolSizes, win = H.win; /** * The SVGElement prototype is a JavaScript wrapper for SVG elements used in the * rendering layer of Highcharts. Combined with the {@link * Highcharts.SVGRenderer} object, these prototypes allow freeform annotation * in the charts or even in HTML pages without instanciating a chart. The * SVGElement can also wrap HTML labels, when `text` or `label` elements are * created with the `useHTML` parameter. * * The SVGElement instances are created through factory functions on the {@link * Highcharts.SVGRenderer} object, like {@link Highcharts.SVGRenderer#rect| * rect}, {@link Highcharts.SVGRenderer#path|path}, {@link * Highcharts.SVGRenderer#text|text}, {@link Highcharts.SVGRenderer#label| * label}, {@link Highcharts.SVGRenderer#g|g} and more. * * @class * @name Highcharts.SVGElement */ SVGElement = H.SVGElement = function () { return this; }; extend(SVGElement.prototype, /** @lends Highcharts.SVGElement.prototype */ { // Default base for animation opacity: 1, SVG_NS: SVG_NS, /** * For labels, these CSS properties are applied to the `text` node directly. * * @private * @name Highcharts.SVGElement#textProps * @type {Array<string>} */ textProps: ['direction', 'fontSize', 'fontWeight', 'fontFamily', 'fontStyle', 'color', 'lineHeight', 'width', 'textAlign', 'textDecoration', 'textOverflow', 'textOutline', 'cursor'], /** * Initialize the SVG element. This function only exists to make the * initialization process overridable. It should not be called directly. * * @function Highcharts.SVGElement#init * * @param {Highcharts.SVGRenderer} renderer * The SVGRenderer instance to initialize to. * * @param {string} nodeName * The SVG node name. */ init: function (renderer, nodeName) { /** * The primary DOM node. Each `SVGElement` instance wraps a main DOM * node, but may also represent more nodes. * * @name Highcharts.SVGElement#element * @type {Highcharts.SVGDOMElement|Highcharts.HTMLDOMElement} */ this.element = nodeName === 'span' ? createElement(nodeName) : doc.createElementNS(this.SVG_NS, nodeName); /** * The renderer that the SVGElement belongs to. * * @name Highcharts.SVGElement#renderer * @type {Highcharts.SVGRenderer} */ this.renderer = renderer; H.fireEvent(this, 'afterInit'); }, /** * Animate to given attributes or CSS properties. * * @sample highcharts/members/element-on/ * Setting some attributes by animation * * @function Highcharts.SVGElement#animate * * @param {Highcharts.SVGAttributes} params * SVG attributes or CSS to animate. * * @param {Highcharts.AnimationOptionsObject} [options] * Animation options. * * @param {Function} [complete] * Function to perform at the end of animation. * * @return {Highcharts.SVGElement} * Returns the SVGElement for chaining. */ animate: function (params, options, complete) { var animOptions = H.animObject( pick(options, this.renderer.globalAnimation, true) ); // When the page is hidden save resources in the background by not // running animation at all (#9749). if (pick(doc.hidden, doc.msHidden, doc.webkitHidden, false)) { animOptions.duration = 0; } if (animOptions.duration !== 0) { // allows using a callback with the global animation without // overwriting it if (complete) { animOptions.complete = complete; } animate(this, params, animOptions); } else { this.attr(params, null, complete); // Call the end step synchronously H.objectEach(params, function (val, prop) { if (animOptions.step) { animOptions.step.call(this, val, { prop: prop, pos: 1 }); } }, this); } return this; }, /** * Build and apply an SVG gradient out of a common JavaScript configuration * object. This function is called from the attribute setters. An event * hook is added for supporting other complex color types. * * @private * @function Highcharts.SVGElement#complexColor * * @param {Highcharts.GradientColorObject} color * The gradient options structure. * * @param {string} prop * The property to apply, can either be `fill` or `stroke`. * * @param {Highcharts.SVGDOMElement} elem * SVG DOM element to apply the gradient on. */ complexColor: function (color, prop, elem) { var renderer = this.renderer, colorObject, gradName, gradAttr, radAttr, gradients, gradientObject, stops, stopColor, stopOpacity, radialReference, id, key = [], value; H.fireEvent(this.renderer, 'complexColor', { args: arguments }, function () { // Apply linear or radial gradients if (color.radialGradient) { gradName = 'radialGradient'; } else if (color.linearGradient) { gradName = 'linearGradient'; } if (gradName) { gradAttr = color[gradName]; gradients = renderer.gradients; stops = color.stops; radialReference = elem.radialReference; // Keep < 2.2 kompatibility if (isArray(gradAttr)) { color[gradName] = gradAttr = { x1: gradAttr[0], y1: gradAttr[1], x2: gradAttr[2], y2: gradAttr[3], gradientUnits: 'userSpaceOnUse' }; } // Correct the radial gradient for the radial reference system if ( gradName === 'radialGradient' && radialReference && !defined(gradAttr.gradientUnits) ) { // Save the radial attributes for updating radAttr = gradAttr; gradAttr = merge( gradAttr, renderer.getRadialAttr(radialReference, radAttr), { gradientUnits: 'userSpaceOnUse' } ); } // Build the unique key to detect whether we need to create a // new element (#1282) objectEach(gradAttr, function (val, n) { if (n !== 'id') { key.push(n, val); } }); objectEach(stops, function (val) { key.push(val); }); key = key.join(','); // Check if a gradient object with the same config object is // created within this renderer if (gradients[key]) { id = gradients[key].attr('id'); } else { // Set the id and create the element gradAttr.id = id = H.uniqueKey(); gradients[key] = gradientObject = renderer.createElement(gradName) .attr(gradAttr) .add(renderer.defs); gradientObject.radAttr = radAttr; // The gradient needs to keep a list of stops to be able to // destroy them gradientObject.stops = []; stops.forEach(function (stop) { var stopObject; if (stop[1].indexOf('rgba') === 0) { colorObject = H.color(stop[1]); stopColor = colorObject.get('rgb'); stopOpacity = colorObject.get('a'); } else { stopColor = stop[1]; stopOpacity = 1; } stopObject = renderer.createElement('stop').attr({ offset: stop[0], 'stop-color': stopColor, 'stop-opacity': stopOpacity }).add(gradientObject); // Add the stop element to the gradient gradientObject.stops.push(stopObject); }); } // Set the reference to the gradient object value = 'url(' + renderer.url + '#' + id + ')'; elem.setAttribute(prop, value); elem.gradient = key; // Allow the color to be concatenated into tooltips formatters // etc. (#2995) color.toString = function () { return value; }; } }); }, /** * Apply a text outline through a custom CSS property, by copying the text * element and apply stroke to the copy. Used internally. Contrast checks at * [example](https://jsfiddle.net/highcharts/43soe9m1/2/). * * @example * // Specific color * text.css({ * textOutline: '1px black' * }); * // Automatic contrast * text.css({ * color: '#000000', // black text * textOutline: '1px contrast' // => white outline * }); * * @private * @function Highcharts.SVGElement#applyTextOutline * * @param {string} textOutline * A custom CSS `text-outline` setting, defined by `width color`. */ applyTextOutline: function (textOutline) { var elem = this.element, tspans, hasContrast = textOutline.indexOf('contrast') !== -1, styles = {}, color, strokeWidth, firstRealChild; // When the text shadow is set to contrast, use dark stroke for light // text and vice versa. if (hasContrast) { styles.textOutline = textOutline = textOutline.replace( /contrast/g, this.renderer.getContrast(elem.style.fill) ); } // Extract the stroke width and color textOutline = textOutline.split(' '); color = textOutline[textOutline.length - 1]; strokeWidth = textOutline[0]; if (strokeWidth && strokeWidth !== 'none' && H.svg) { this.fakeTS = true; // Fake text shadow tspans = [].slice.call(elem.getElementsByTagName('tspan')); // In order to get the right y position of the clone, // copy over the y setter this.ySetter = this.xSetter; // Since the stroke is applied on center of the actual outline, we // need to double it to get the correct stroke-width outside the // glyphs. strokeWidth = strokeWidth.replace( /(^[\d\.]+)(.*?)$/g, function (match, digit, unit) { return (2 * digit) + unit; } ); // Remove shadows from previous runs. this.removeTextOutline(tspans); // For each of the tspans, create a stroked copy behind it. firstRealChild = elem.firstChild; tspans.forEach(function (tspan, y) { var clone; // Let the first line start at the correct X position if (y === 0) { tspan.setAttribute('x', elem.getAttribute('x')); y = elem.getAttribute('y'); tspan.setAttribute('y', y || 0); if (y === null) { elem.setAttribute('y', 0); } } // Create the clone and apply outline properties clone = tspan.cloneNode(1); attr(clone, { 'class': 'highcharts-text-outline', 'fill': color, 'stroke': color, 'stroke-width': strokeWidth, 'stroke-linejoin': 'round' }); elem.insertBefore(clone, firstRealChild); }); } }, removeTextOutline: function (tspans) { // Iterate from the end to // support removing items inside the cycle (#6472). var i = tspans.length, tspan; while (i--) { tspan = tspans[i]; if (tspan.getAttribute('class') === 'highcharts-text-outline') { // Remove then erase erase(tspans, this.element.removeChild(tspan)); } } }, // Custom attributes used for symbols, these should be filtered out when // setting SVGElement attributes (#9375). symbolCustomAttribs: [ 'x', 'y', 'width', 'height', 'r', 'start', 'end', 'innerR', 'anchorX', 'anchorY', 'rounded' ], /** * Apply native and custom attributes to the SVG elements. * * In order to set the rotation center for rotation, set x and y to 0 and * use `translateX` and `translateY` attributes to position the element * instead. * * Attributes frequently used in Highcharts are `fill`, `stroke`, * `stroke-width`. * * @sample highcharts/members/renderer-rect/ * Setting some attributes * * @example * // Set multiple attributes * element.attr({ * stroke: 'red', * fill: 'blue', * x: 10, * y: 10 * }); * * // Set a single attribute * element.attr('stroke', 'red'); * * // Get an attribute * element.attr('stroke'); // => 'red' * * @function Highcharts.SVGElement#attr * * @param {string|Highcharts.SVGAttributes} [hash] * The native and custom SVG attributes. * * @param {string} [val] * If the type of the first argument is `string`, the second can be a * value, which will serve as a single attribute setter. If the first * argument is a string and the second is undefined, the function * serves as a getter and the current value of the property is * returned. * * @param {Function} [complete] * A callback function to execute after setting the attributes. This * makes the function compliant and interchangeable with the * {@link SVGElement#animate} function. * * @param {boolean} [continueAnimation=true] * Used internally when `.attr` is called as part of an animation * step. Otherwise, calling `.attr` for an attribute will stop * animation for that attribute. * * @return {number|string|Highcharts.SVGElement} * If used as a setter, it returns the current * {@link Highcharts.SVGElement} so the calls can be chained. If * used as a getter, the current value of the attribute is returned. */ attr: function (hash, val, complete, continueAnimation) { var key, element = this.element, hasSetSymbolSize, ret = this, skipAttr, setter, symbolCustomAttribs = this.symbolCustomAttribs; // single key-value pair if (typeof hash === 'string' && val !== undefined) { key = hash; hash = {}; hash[key] = val; } // used as a getter: first argument is a string, second is undefined if (typeof hash === 'string') { ret = (this[hash + 'Getter'] || this._defaultGetter).call( this, hash, element ); // setter } else { objectEach(hash, function eachAttribute(val, key) { skipAttr = false; // Unless .attr is from the animator update, stop current // running animation of this property if (!continueAnimation) { stop(this, key); } // Special handling of symbol attributes if ( this.symbolName && H.inArray(key, symbolCustomAttribs) !== -1 ) { if (!hasSetSymbolSize) { this.symbolAttr(hash); hasSetSymbolSize = true; } skipAttr = true; } if (this.rotation && (key === 'x' || key === 'y')) { this.doTransform = true; } if (!skipAttr) { setter = this[key + 'Setter'] || this._defaultSetter; setter.call(this, val, key, element); // Let the shadow follow the main element if ( !this.styledMode && this.shadows && /^(width|height|visibility|x|y|d|transform|cx|cy|r)$/ .test(key) ) { this.updateShadows(key, val, setter); } } }, this); this.afterSetters(); } // In accordance with animate, run a complete callback if (complete) { complete.call(this); } return ret; }, /** * This method is executed in the end of `attr()`, after setting all * attributes in the hash. In can be used to efficiently consolidate * multiple attributes in one SVG property -- e.g., translate, rotate and * scale are merged in one "transform" attribute in the SVG node. * * @private * @function Highcharts.SVGElement#afterSetters */ afterSetters: function () { // Update transform. Do this outside the loop to prevent redundant // updating for batch setting of attributes. if (this.doTransform) { this.updateTransform(); this.doTransform = false; } }, /** * Update the shadow elements with new attributes. * * @private * @function Highcharts.SVGElement#updateShadows * * @param {string} key * The attribute name. * * @param {string|number} value * The value of the attribute. * * @param {Function} setter * The setter function, inherited from the parent wrapper. */ updateShadows: function (key, value, setter) { var shadows = this.shadows, i = shadows.length; while (i--) { setter.call( shadows[i], key === 'height' ? Math.max(value - (shadows[i].cutHeight || 0), 0) : key === 'd' ? this.d : value, key, shadows[i] ); } }, /** * Add a class name to an element. * * @function Highcharts.SVGElement#addClass * * @param {string} className * The new class name to add. * * @param {boolean} [replace=false] * When true, the existing class name(s) will be overwritten with * the new one. When false, the new one is added. * * @return {Highcharts.SVGElement} * Return the SVG element for chainability. */ addClass: function (className, replace) { var currentClassName = this.attr('class') || ''; if (!replace) { // Filter out existing className = (className || '') .split(/ /g) .reduce(function (newClassName, name) { if (currentClassName.indexOf(name) === -1) { newClassName.push(name); } return newClassName; }, currentClassName ? [currentClassName] : []) .join(' '); } if (className !== currentClassName) { this.attr('class', className); } return this; }, /** * Check if an element has the given class name. * * @function Highcharts.SVGElement#hasClass * * @param {string} className * The class name to check for. * * @return {boolean} * Whether the class name is found. */ hasClass: function (className) { return (this.attr('class') || '').split(' ').indexOf(className) !== -1; }, /** * Remove a class name from the element. * * @function Highcharts.SVGElement#removeClass * * @param {string|RegExp} className * The class name to remove. * * @return {Highcharts.SVGElement} Returns the SVG element for chainability. */ removeClass: function (className) { return this.attr( 'class', (this.attr('class') || '').replace(className, '') ); }, /** * If one of the symbol size affecting parameters are changed, * check all the others only once for each call to an element's * .attr() method * * @private * @function Highcharts.SVGElement#symbolAttr * * @param {Highcharts.Dictionary<number|string>} hash * The attributes to set. */ symbolAttr: function (hash) { var wrapper = this; [ 'x', 'y', 'r', 'start', 'end', 'width', 'height', 'innerR', 'anchorX', 'anchorY', 'clockwise' ].forEach(function (key) { wrapper[key] = pick(hash[key], wrapper[key]); }); wrapper.attr({ d: wrapper.renderer.symbols[wrapper.symbolName]( wrapper.x, wrapper.y, wrapper.width, wrapper.height, wrapper ) }); }, /** * Apply a clipping rectangle to this element. * * @function Highcharts.SVGElement#clip * * @param {Highcharts.ClipRectElement} [clipRect] * The clipping rectangle. If skipped, the current clip is removed. * * @return {Highcharts.SVGElement} * Returns the SVG element to allow chaining. */ clip: function (clipRect) { return this.attr( 'clip-path', clipRect ? 'url(' + this.renderer.url + '#' + clipRect.id + ')' : 'none' ); }, /** * Calculate the coordinates needed for drawing a rectangle crisply and * return the calculated attributes. * * @function Highcharts.SVGElement#crisp * * @param {Highcharts.RectangleObject} rect * Rectangle to crisp. * * @param {number} [strokeWidth] * The stroke width to consider when computing crisp positioning. It * can also be set directly on the rect parameter. * * @return {Highcharts.RectangleObject} * The modified rectangle arguments. */ crisp: function (rect, strokeWidth) { var wrapper = this, normalizer; strokeWidth = strokeWidth || rect.strokeWidth || 0; // Math.round because strokeWidth can sometimes have roundoff errors normalizer = Math.round(strokeWidth) % 2 / 2; // normalize for crisp edges rect.x = Math.floor(rect.x || wrapper.x || 0) + normalizer; rect.y = Math.floor(rect.y || wrapper.y || 0) + normalizer; rect.width = Math.floor( (rect.width || wrapper.width || 0) - 2 * normalizer ); rect.height = Math.floor( (rect.height || wrapper.height || 0) - 2 * normalizer ); if (defined(rect.strokeWidth)) { rect.strokeWidth = strokeWidth; } return rect; }, /** * Set styles for the element. In addition to CSS styles supported by * native SVG and HTML elements, there are also some custom made for * Highcharts, like `width`, `ellipsis` and `textOverflow` for SVG text * elements. * * @sample highcharts/members/renderer-text-on-chart/ * Styled text * * @function Highcharts.SVGElement#css * * @param {Highcharts.CSSObject} styles * The new CSS styles. * * @return {Highcharts.SVGElement} * Return the SVG element for chaining. */ css: function (styles) { var oldStyles = this.styles, newStyles = {}, elem = this.element, textWidth, serializedCss = '', hyphenate, hasNew = !oldStyles, // These CSS properties are interpreted internally by the SVG // renderer, but are not supported by SVG and should not be added to // the DOM. In styled mode, no CSS should find its way to the DOM // whatsoever (#6173, #6474). svgPseudoProps = ['textOutline', 'textOverflow', 'width']; // convert legacy if (styles && styles.color) { styles.fill = styles.color; } // Filter out existing styles to increase performance (#2640) if (oldStyles) { objectEach(styles, function (style, n) { if (style !== oldStyles[n]) { newStyles[n] = style; hasNew = true; } }); } if (hasNew) { // Merge the new styles with the old ones if (oldStyles) { styles = extend( oldStyles, newStyles ); } // Get the text width from style if (styles) { // Previously set, unset it (#8234) if (styles.width === null || styles.width === 'auto') { delete this.textWidth; // Apply new } else if ( elem.nodeName.toLowerCase() === 'text' && styles.width ) { textWidth = this.textWidth = pInt(styles.width); } } // store object this.styles = styles; if (textWidth && (!svg && this.renderer.forExport)) { delete styles.width; } // Serialize and set style attribute if (elem.namespaceURI === this.SVG_NS) { // #7633 hyphenate = function (a, b) { return '-' + b.toLowerCase(); }; objectEach(styles, function (style, n) { if (svgPseudoProps.indexOf(n) === -1) { serializedCss += n.replace(/([A-Z])/g, hyphenate) + ':' + style + ';'; } }); if (serializedCss) { attr(elem, 'style', serializedCss); // #1881 } } else { css(elem, styles); } if (this.added) { // Rebuild text after added. Cache mechanisms in the buildText // will prevent building if there are no significant changes. if (this.element.nodeName === 'text') { this.renderer.buildText(this); } // Apply text outline after added if (styles && styles.textOutline) { this.applyTextOutline(styles.textOutline); } } } return this; }, /** * Get the computed style. Only in styled mode. * * @example * chart.series[0].points[0].graphic.getStyle('stroke-width'); // => '1px' * * @function Highcharts.SVGElement#getStyle * * @param {string} prop * The property name to check for. * * @return {string} * The current computed value. */ getStyle: function (prop) { return win.getComputedStyle(this.element || this, '') .getPropertyValue(prop); }, /** * Get the computed stroke width in pixel values. This is used extensively * when drawing shapes to ensure the shapes are rendered crisp and * positioned correctly relative to each other. Using * `shape-rendering: crispEdges` leaves us less control over positioning, * for example when we want to stack columns next to each other, or position * things pixel-perfectly within the plot box. * * The common pattern when placing a shape is: * - Create the SVGElement and add it to the DOM. In styled mode, it will * now receive a stroke width from the style sheet. In classic mode we * will add the `stroke-width` attribute. * - Read the computed `elem.strokeWidth()`. * - Place it based on the stroke width. * * @function Highcharts.SVGElement#strokeWidth * * @return {number} * The stroke width in pixels. Even if the given stroke widtch (in * CSS or by attributes) is based on `em` or other units, the pixel * size is returned. */ strokeWidth: function () { // In non-styled mode, read the stroke width as set by .attr if (!this.renderer.styledMode) { return this['stroke-width'] || 0; } // In styled mode, read computed stroke width var val = this.getStyle('stroke-width'), ret, dummy; // Read pixel values directly if (val.indexOf('px') === val.length - 2) { ret = pInt(val); // Other values like em, pt etc need to be measured } else { dummy = doc.createElementNS(SVG_NS, 'rect'); attr(dummy, { 'width': val, 'stroke-width': 0 }); this.element.parentNode.appendChild(dummy); ret = dummy.getBBox().width; dummy.parentNode.removeChild(dummy); } return ret; }, /** * Add an event listener. This is a simple setter that replaces all other * events of the same type, opposed to the {@link Highcharts#addEvent} * function. * * @sample highcharts/members/element-on/ * A clickable rectangle * * @function Highcharts.SVGElement#on * * @param {string} eventType * The event type. If the type is `click`, Highcharts will internally * translate it to a `touchstart` event on touch devices, to prevent * the browser from waiting for a click event from firing. * * @param {Function} handler * The handler callback. * * @return {Highcharts.SVGElement} * The SVGElement for chaining. */ on: function (eventType, handler) { var svgElement = this, element = svgElement.element; // touch if (hasTouch && eventType === 'click') { element.ontouchstart = function (e) { svgElement.touchEventFired = Date.now(); // #2269 e.preventDefault(); handler.call(element, e); }; element.onclick = function (e) { if (win.navigator.userAgent.indexOf('Android') === -1 || Date.now() - (svgElement.touchEventFired || 0) > 1100) { handler.call(element, e); } }; } else { // simplest possible event model for internal use element['on' + eventType] = handler; } return this; }, /** * Set the coordinates needed to draw a consistent radial gradient across * a shape regardless of positioning inside the chart. Used on pie slices * to make all the slices have the same radial reference point. * * @function Highcharts.SVGElement#setRadialReference * * @param {Array<number>} coordinates * The center reference. The format is `[centerX, centerY, diameter]` * in pixels. * * @return {Highcharts.SVGElement} * Returns the SVGElement for chaining. */ setRadialReference: function (coordinates) { var existingGradient = this.renderer.gradients[this.element.gradient]; this.element.radialReference = coordinates; // On redrawing objects with an existing gradient, the gradient needs // to be repositioned (#3801) if (existingGradient && existingGradient.radAttr) { existingGradient.animate( this.renderer.getRadialAttr( coordinates, existingGradient.radAttr ) ); } return this; }, /** * Move an object and its children by x and y values. * * @function Highcharts.SVGElement#translate * * @param {number} x * The x value. * * @param {number} y * The y value. */ translate: function (x, y) { return this.attr({ translateX: x, translateY: y }); }, /** * Invert a group, rotate and flip. This is used internally on inverted * charts, where the points and graphs are drawn as if not inverted, then * the series group elements are inverted. * * @function Highcharts.SVGElement#invert * * @param {boolean} inverted * Whether to invert or not. An inverted shape can be un-inverted by * setting it to false. * * @return {Highcharts.SVGElement} * Return the SVGElement for chaining. */ invert: function (inverted) { var wrapper = this; wrapper.inverted = inverted; wrapper.updateTransform(); return wrapper; }, /** * Update the transform attribute based on internal properties. Deals with * the custom `translateX`, `translateY`, `rotation`, `scaleX` and `scaleY` * attributes and updates the SVG `transform` attribute. * * @private * @function Highcharts.SVGElement#updateTransform */ updateTransform: function () { var wrapper = this, translateX = wrapper.translateX || 0, translateY = wrapper.translateY || 0, scaleX = wrapper.scaleX, scaleY = wrapper.scaleY, inverted = wrapper.inverted, rotation = wrapper.rotation, matrix = wrapper.matrix, element = wrapper.element, transform; // Flipping affects translate as adjustment for flipping around the // group's axis if (inverted) { translateX += wrapper.width; translateY += wrapper.height; } // Apply translate. Nearly all transformed elements have translation, // so instead of checking for translate = 0, do it always (#1767, // #1846). transform = ['translate(' + translateX + ',' + translateY + ')']; // apply matrix if (defined(matrix)) { transform.push( 'matrix(' + matrix.join(',') + ')' ); } // apply rotation if (inverted) { transform.push('rotate(90) scale(-1,1)'); } else if (rotation) { // text rotation transform.push( 'rotate(' + rotation + ' ' + pick(this.rotationOriginX, element.getAttribute('x'), 0) + ' ' + pick(this.rotationOriginY, element.getAttribute('y') || 0) + ')' ); } // apply scale if (defined(scaleX) || defined(scaleY)) { transform.push( 'scale(' + pick(scaleX, 1) + ' ' + pick(scaleY, 1) + ')' ); } if (transform.length) { element.setAttribute('transform', transform.join(' ')); } }, /** * Bring th