| Server IP : 182.53.201.61 / Your IP : 216.73.217.175 Web Server : Apache/2.2.15 (Fedora) System : Linux km10.dyndns.org 2.6.31.5-127.fc12.i686.PAE #1 SMP Sat Nov 7 21:25:57 EST 2009 i686 User : apache ( 48) PHP Version : 5.3.3 Disable Function : NONE MySQL : ON | cURL : ON | WGET : ON | Perl : ON | Python : ON | Sudo : ON | Pkexec : ON Directory : /var/www/html/temp/code/es-modules/parts/ |
Upload File : |
/**
* (c) 2010-2017 Torstein Honsi
*
* License: www.highcharts.com/license
*/
'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,
each = H.each,
extend = H.extend,
erase = H.erase,
grep = H.grep,
hasTouch = H.hasTouch,
inArray = H.inArray,
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;
/**
* @typedef {Object} SVGDOMElement - An SVG DOM element.
*/
/**
* 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
* [rect]{@link Highcharts.SVGRenderer#rect}, [path]{@link
* Highcharts.SVGRenderer#path}, [text]{@link Highcharts.SVGRenderer#text},
* [label]{@link Highcharts.SVGRenderer#label}, [g]{@link
* Highcharts.SVGRenderer#g} and more.
*
* @class 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
* @type {Array.<string>}
*/
textProps: ['direction', 'fontSize', 'fontWeight', 'fontFamily',
'fontStyle', 'color', 'lineHeight', 'width', 'textAlign',
'textDecoration', 'textOverflow', 'textOutline'],
/**
* Initialize the SVG element. This function only exists to make the
* initiation process overridable. It should not be called directly.
*
* @param {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 element
* @memberOf SVGElement
* @type {SVGDOMNode|HTMLDOMNode}
*/
this.element = nodeName === 'span' ?
createElement(nodeName) :
doc.createElementNS(this.SVG_NS, nodeName);
/**
* The renderer that the SVGElement belongs to.
*
* @name renderer
* @memberOf SVGElement
* @type {SVGRenderer}
*/
this.renderer = renderer;
},
/**
* Animate to given attributes or CSS properties.
*
* @param {SVGAttributes} params SVG attributes or CSS to animate.
* @param {AnimationOptions} [options] Animation options.
* @param {Function} [complete] Function to perform at the end of animation.
*
* @sample highcharts/members/element-on/
* Setting some attributes by animation
*
* @returns {SVGElement} Returns the SVGElement for chaining.
*/
animate: function (params, options, complete) {
var animOptions = H.animObject(
pick(options, this.renderer.globalAnimation, true)
);
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);
if (animOptions.step) {
animOptions.step.call(this);
}
}
return this;
},
/**
* @typedef {Object} GradientOptions
* @property {Object} linearGradient Holds an object that defines the start
* position and the end position relative to the shape.
* @property {Number} linearGradient.x1 Start horizontal position of the
* gradient. Ranges 0-1.
* @property {Number} linearGradient.x2 End horizontal position of the
* gradient. Ranges 0-1.
* @property {Number} linearGradient.y1 Start vertical position of the
* gradient. Ranges 0-1.
* @property {Number} linearGradient.y2 End vertical position of the
* gradient. Ranges 0-1.
* @property {Object} radialGradient Holds an object that defines the center
* position and the radius.
* @property {Number} radialGradient.cx Center horizontal position relative
* to the shape. Ranges 0-1.
* @property {Number} radialGradient.cy Center vertical position relative
* to the shape. Ranges 0-1.
* @property {Number} radialGradient.r Radius relative to the shape. Ranges
* 0-1.
* @property {Array.<Array>} stops 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.
*
* @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
* ]
* }
* }
*/
/**
* 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
* @param {GradientOptions} color The gradient options structure.
* @param {string} prop The property to apply, can either be `fill` or
* `stroke`.
* @param {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 = [];
each(stops, 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 http://jsfiddle.net/highcharts/43soe9m1/2/ .
*
* @private
* @param {String} textOutline A custom CSS `text-outline` setting, defined
* by `width color`.
* @example
* // Specific color
* text.css({
* textOutline: '1px black'
* });
* // Automatic contrast
* text.css({
* color: '#000000', // black text
* textOutline: '1px contrast' // => white outline
* });
*/
applyTextOutline: function (textOutline) {
var elem = this.element,
tspans,
tspan,
hasContrast = textOutline.indexOf('contrast') !== -1,
styles = {},
color,
strokeWidth,
firstRealChild,
i;
// 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. Iterate from the end to
// support removing items inside the cycle (#6472).
i = tspans.length;
while (i--) {
tspan = tspans[i];
if (tspan.getAttribute('class') === 'highcharts-text-outline') {
// Remove then erase
erase(tspans, elem.removeChild(tspan));
}
}
// For each of the tspans, create a stroked copy behind it.
firstRealChild = elem.firstChild;
each(tspans, 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);
});
}
},
/**
*
* @typedef {Object} SVGAttributes 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
* }
*/
/**
* 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`.
*
* @param {SVGAttributes|String} 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.
*
* @returns {SVGElement|string|number} If used as a setter, it returns the
* current {@link SVGElement} so the calls can be chained. If used as a
* getter, the current value of the attribute is returned.
*
* @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'
*
*/
attr: function (hash, val, complete, continueAnimation) {
var key,
element = this.element,
hasSetSymbolSize,
ret = this,
skipAttr,
setter;
// 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 &&
/^(x|y|width|height|r|start|end|innerR|anchorX|anchorY)$/
.test(key)
) {
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.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
*/
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
* @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.
*
* @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.
* @returns {SVGElement} Return the SVG element for chainability.
*/
addClass: function (className, replace) {
var currentClassName = this.attr('class') || '';
if (currentClassName.indexOf(className) === -1) {
if (!replace) {
className =
(currentClassName + (currentClassName ? ' ' : '') +
className).replace(' ', ' ');
}
this.attr('class', className);
}
return this;
},
/**
* Check if an element has the given class name.
* @param {string} className
* The class name to check for.
* @return {Boolean}
* Whether the class name is found.
*/
hasClass: function (className) {
return inArray(
className,
(this.attr('class') || '').split(' ')
) !== -1;
},
/**
* Remove a class name from the element.
* @param {String|RegExp} className The class name to remove.
* @return {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
* @param {Object} hash - The attributes to set.
* @private
*/
symbolAttr: function (hash) {
var wrapper = this;
each([
'x',
'y',
'r',
'start',
'end',
'width',
'height',
'innerR',
'anchorX',
'anchorY'
], 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.
*
* @param {ClipRect} [clipRect] - The clipping rectangle. If skipped, the
* current clip is removed.
* @returns {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.
*
* @param {Object} rect - A rectangle.
* @param {number} rect.x - The x position.
* @param {number} rect.y - The y position.
* @param {number} rect.width - The width.
* @param {number} rect.height - The height.
* @param {number} [strokeWidth] - The stroke width to consider when
* computing crisp positioning. It can also be set directly on the rect
* parameter.
*
* @returns {{x: Number, y: Number, width: Number, height: Number}} 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.
* @param {CSSObject} styles The new CSS styles.
* @returns {SVGElement} Return the SVG element for chaining.
*
* @sample highcharts/members/renderer-text-on-chart/
* Styled text
*/
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
textWidth = this.textWidth = (
styles &&
styles.width &&
styles.width !== 'auto' &&
elem.nodeName.toLowerCase() === 'text' &&
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 (inArray(n, svgPseudoProps) === -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 current stroke width. In classic mode, the setter registers it
* directly on the element.
* @returns {number} The stroke width in pixels.
* @ignore
*/
strokeWidth: function () {
return this['stroke-width'] || 0;
},
/**
* 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.
* @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.
* @returns {SVGElement} The SVGElement for chaining.
*
* @sample highcharts/members/element-on/
* A clickable rectangle
*/
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.
*
* @param {Array} coordinates The center reference. The format is
* `[centerX, centerY, diameter]` in pixels.
* @returns {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.
*
* @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.
*
* @param {boolean} inverted
* Whether to invert or not. An inverted shape can be un-inverted by
* setting it to false.
* @return {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
*
*/
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 the element to the front. Alternatively, a new zIndex can be set.
*
* @returns {SVGElement} Returns the SVGElement for chaining.
*
* @sample highcharts/members/element-tofront/
* Click an element to bring it to front
*/
toFront: function () {
var element = this.element;
element.parentNode.appendChild(element);
return this;
},
/**
* Align the element relative to the chart or another box.
*
* @param {Object} [alignOptions] The alignment options. The function can be
* called without this parameter in order to re-align an element after the
* box has been updated.
* @param {string} [alignOptions.align=left] Horizontal alignment. Can be
* one of `left`, `center` and `right`.
* @param {string} [alignOptions.verticalAlign=top] Vertical alignment. Can
* be one of `top`, `middle` and `bottom`.
* @param {number} [alignOptions.x=0] Horizontal pixel offset from
* alignment.
* @param {number} [alignOptions.y=0] Vertical pixel offset from alignment.
* @param {Boolean} [alignByTranslate=false] Use the `transform` attribute
* with translateX and translateY custom attributes to align this elements
* rather than `x` and `y` attributes.
* @param {String|Object} box The box to align to, needs a width and height.
* When the box is a string, it refers to an object in the Renderer. For
* example, when box is `spacingBox`, it refers to `Renderer.spacingBox`
* which holds `width`, `height`, `x` and `y` properties.
* @returns {SVGElement} Returns the SVGElement for chaining.
*/
align: function (alignOptions, alignByTranslate, box) {
var align,
vAlign,
x,
y,
attribs = {},
alignTo,
renderer = this.renderer,
alignedObjects = renderer.alignedObjects,
alignFactor,
vAlignFactor;
// First call on instanciate
if (alignOptions) {
this.alignOptions = alignOptions;
this.alignByTranslate = alignByTranslate;
if (!box || isString(box)) {
this.alignTo = alignTo = box || 'renderer';
// prevent duplicates, like legendGroup after resize
erase(alignedObjects, this);
alignedObjects.push(this);
box = null; // reassign it below
}
// When called on resize, no arguments are supplied
} else {
alignOptions = this.alignOptions;
alignByTranslate = this.alignByTranslate;
alignTo = this.alignTo;
}
box = pick(box, renderer[alignTo], renderer);
// Assign variables
align = alignOptions.align;
vAlign = alignOptions.verticalAlign;
x = (box.x || 0) + (alignOptions.x || 0); // default: left align
y = (box.y || 0) + (alignOptions.y || 0); // default: top align
// Align
if (align === 'right') {
alignFactor = 1;
} else if (align === 'center') {
alignFactor = 2;
}
if (alignFactor) {
x += (box.width - (alignOptions.width || 0)) / alignFactor;
}
attribs[alignByTranslate ? 'translateX' : 'x'] = Math.round(x);
// Vertical align
if (vAlign === 'bottom') {
vAlignFactor = 1;
} else if (vAlign === 'middle') {
vAlignFactor = 2;
}
if (vAlignFactor) {
y += (box.height - (alignOptions.height || 0)) / vAlignFactor;
}
attribs[alignByTranslate ? 'translateY' : 'y'] = Math.round(y);
// Animate only if already placed
this[this.placed ? 'animate' : 'attr'](attribs);
this.placed = true;
this.alignAttr = attribs;
return this;
},
/**
* Get the bounding box (width, height, x and y) for the element. Generally
* used to get rendered text size. Since this is called a lot in charts,
* the results are cached based on text properties, in order to save DOM
* traffic. The returned bounding box includes the rotation, so for example
* a single text line of rotation 90 will report a greater height, and a
* width corresponding to the line-height.
*
* @param {boolean} [reload] Skip the cache and get the updated DOM bouding
* box.
* @param {number} [rot] Override the element's rotation. This is internally
* used on axis labels with a value of 0 to find out what the bounding box
* would be have been if it were not rotated.
* @returns {Object} The bounding box with `x`, `y`, `width` and `height`
* properties.
*
* @sample highcharts/members/renderer-on-chart/
* Draw a rectangle based on a text's bounding box
*/
getBBox: function (reload, rot) {
var wrapper = this,
bBox, // = wrapper.bBox,
renderer = wrapper.renderer,
width,
height,
rotation,
rad,
element = wrapper.element,
styles = wrapper.styles,
fontSize,
textStr = wrapper.textStr,
toggleTextShadowShim,
cache = renderer.cache,
cacheKeys = renderer.cacheKeys,
cacheKey;
rotation = pick(rot, wrapper.rotation);
rad = rotation * deg2rad;
fontSize = styles && styles.fontSize;
// Avoid undefined and null (#7316)
if (defined(textStr)) {
cacheKey = textStr.toString();
// Since numbers are monospaced, and numerical labels appear a lot
// in a chart, we assume that a label of n characters has the same
// bounding box as others of the same length. Unless there is inner
// HTML in the label. In that case, leave the numbers as is (#5899).
if (cacheKey.indexOf('<') === -1) {
cacheKey = cacheKey.replace(/[0-9]/g, '0');
}
// Properties that affect bounding box
cacheKey += [
'',
rotation || 0,
fontSize,
wrapper.textWidth, // #7874, also useHTML
styles && styles.textOverflow // #5968
]
.join(',');
}
if (cacheKey && !reload) {
bBox = cache[cacheKey];
}
// No cache found
if (!bBox) {
// SVG elements
if (element.namespaceURI === wrapper.SVG_NS || renderer.forExport) {
try { // Fails in Firefox if the container has display: none.
// When the text shadow shim is used, we need to hide the
// fake shadows to get the correct bounding box (#3872)
toggleTextShadowShim = this.fakeTS && function (display) {
each(
element.querySelectorAll(
'.highcharts-text-outline'
),
function (tspan) {
tspan.style.display = display;
}
);
};
// Workaround for #3842, Firefox reporting wrong bounding
// box for shadows
if (toggleTextShadowShim) {
toggleTextShadowShim('none');
}
bBox = element.getBBox ?
// SVG: use extend because IE9 is not allowed to change
// width and height in case of rotation (below)
extend({}, element.getBBox()) : {
// Legacy IE in export mode
width: element.offsetWidth,
height: element.offsetHeight
};
// #3842
if (toggleTextShadowShim) {
toggleTextShadowShim('');
}
} catch (e) {}
// If the bBox is not set, the try-catch block above failed. The
// other condition is for Opera that returns a width of
// -Infinity on hidden elements.
if (!bBox || bBox.width < 0) {
bBox = { width: 0, height: 0 };
}
// VML Renderer or useHTML within SVG
} else {
bBox = wrapper.htmlGetBBox();
}
// True SVG elements as well as HTML elements in modern browsers
// using the .useHTML option need to compensated for rotation
if (renderer.isSVG) {
width = bBox.width;
height = bBox.height;
// Workaround for wrong bounding box in IE, Edge and Chrome on
// Windows. With Highcharts' default font, IE and Edge report
// a box height of 16.899 and Chrome rounds it to 17. If this
// stands uncorrected, it results in more padding added below
// the text than above when adding a label border or background.
// Also vertical positioning is affected.
// http://jsfiddle.net/highcharts/em37nvuj/
// (#1101, #1505, #1669, #2568, #6213).
if (
styles &&
styles.fontSize === '11px' &&
Math.round(height) === 17
) {
bBox.height = height = 14;
}
// Adjust for rotated text
if (rotation) {
bBox.width = Math.abs(height * Math.sin(rad)) +
Math.abs(width * Math.cos(rad));
bBox.height = Math.abs(height * Math.cos(rad)) +
Math.abs(width * Math.sin(rad));
}
}
// Cache it. When loading a chart in a hidden iframe in Firefox and
// IE/Edge, the bounding box height is 0, so don't cache it (#5620).
if (cacheKey && bBox.height > 0) {
// Rotate (#4681)
while (cacheKeys.length > 250) {
delete cache[cacheKeys.shift()];
}
if (!cache[cacheKey]) {
cacheKeys.push(cacheKey);
}
cache[cacheKey] = bBox;
}
}
return bBox;
},
/**
* Show the element after it has been hidden.
*
* @param {boolean} [inherit=false] Set the visibility attribute to
* `inherit` rather than `visible`. The difference is that an element with
* `visibility="visible"` will be visible even if the parent is hidden.
*
* @returns {SVGElement} Returns the SVGElement for chaining.
*/
show: function (inherit) {
return this.attr({ visibility: inherit ? 'inherit' : 'visible' });
},
/**
* Hide the element, equivalent to setting the `visibility` attribute to
* `hidden`.
*
* @returns {SVGElement} Returns the SVGElement for chaining.
*/
hide: function () {
return this.attr({ visibility: 'hidden' });
},
/**
* Fade out an element by animating its opacity down to 0, and hide it on
* complete. Used internally for the tooltip.
*
* @param {number} [duration=150] The fade duration in milliseconds.
*/
fadeOut: function (duration) {
var elemWrapper = this;
elemWrapper.animate({
opacity: 0
}, {
duration: duration || 150,
complete: function () {
// #3088, assuming we're only using this for tooltips
elemWrapper.attr({ y: -9999 });
}
});
},
/**
* Add the element to the DOM. All elements must be added this way.
*
* @param {SVGElement|SVGDOMElement} [parent] The parent item to add it to.
* If undefined, the element is added to the {@link
* Highcharts.SVGRenderer.box}.
*
* @returns {SVGElement} Returns the SVGElement for chaining.
*
* @sample highcharts/members/renderer-g - Elements added to a group
*/
add: function (parent) {
var renderer = this.renderer,
element = this.element,
inserted;
if (parent) {
this.parentGroup = parent;
}
// mark as inverted
this.parentInverted = parent && parent.inverted;
// build formatted text
if (this.textStr !== undefined) {
renderer.buildText(this);
}
// Mark as added
this.added = true;
// If we're adding to renderer root, or other elements in the group
// have a z index, we need to handle it
if (!parent || parent.handleZ || this.zIndex) {
inserted = this.zIndexSetter();
}
// If zIndex is not handled, append at the end
if (!inserted) {
(parent ? parent.element : renderer.box).appendChild(element);
}
// fire an event for internal hooks
if (this.onAdd) {
this.onAdd();
}
return this;
},
/**
* Removes an element from the DOM.
*
* @private
* @param {SVGDOMElement|HTMLDOMElement} element The DOM node to remove.
*/
safeRemoveChild: function (element) {
var parentNode = element.parentNode;
if (parentNode) {
parentNode.removeChild(element);
}
},
/**
* Destroy the element and element wrapper and clear up the DOM and event
* hooks.
*
*
*/
destroy: function () {
var wrapper = this,
element = wrapper.element || {},
parentToClean =
wrapper.renderer.isSVG &&
element.nodeName === 'SPAN' &&
wrapper.parentGroup,
grandParent,
ownerSVGElement = element.ownerSVGElement,
i,
clipPath = wrapper.clipPath;
// remove events
element.onclick = element.onmouseout = element.onmouseover =
element.onmousemove = element.point = null;
stop(wrapper); // stop running animations
if (clipPath && ownerSVGElement) {
// Look for existing references to this clipPath and remove them
// before destroying the element (#6196).
each(
// The upper case version is for Edge
ownerSVGElement.querySelectorAll('[clip-path],[CLIP-PATH]'),
function (el) {
var clipPathAttr = el.getAttribute('clip-path'),
clipPathId = clipPath.element.id;
// Include the closing paranthesis in the test to rule out
// id's from 10 and above (#6550). Edge puts quotes inside
// the url, others not.
if (
clipPathAttr.indexOf('(#' + clipPathId + ')') > -1 ||
clipPathAttr.indexOf('("#' + clipPathId + '")') > -1
) {
el.removeAttribute('clip-path');
}
}
);
wrapper.clipPath = clipPath.destroy();
}
// Destroy stops in case this is a gradient object
if (wrapper.stops) {
for (i = 0; i < wrapper.stops.length; i++) {
wrapper.stops[i] = wrapper.stops[i].destroy();
}
wrapper.stops = null;
}
// remove element
wrapper.safeRemoveChild(element);
wrapper.destroyShadows();
// In case of useHTML, clean up empty containers emulating SVG groups
// (#1960, #2393, #2697).
while (
parentToClean &&
parentToClean.div &&
parentToClean.div.childNodes.length === 0
) {
grandParent = parentToClean.parentGroup;
wrapper.safeRemoveChild(parentToClean.div);
delete parentToClean.div;
parentToClean = grandParent;
}
// remove from alignObjects
if (wrapper.alignTo) {
erase(wrapper.renderer.alignedObjects, wrapper);
}
objectEach(wrapper, function (val, key) {
delete wrapper[key];
});
return null;
},
/**
* @typedef {Object} ShadowOptions
* @property {string} [color=#000000] The shadow color.
* @property {number} [offsetX=1] The horizontal offset from the element.
* @property {number} [offsetY=1] The vertical offset from the element.
* @property {number} [opacity=0.15] The shadow opacity.
* @property {number} [width=3] The shadow width or distance from the
* element.
*/
/**
* Add a shadow to the element. Must be called after the element is added to
* the DOM. In styled mode, this method is not used, instead use `defs` and
* filters.
*
* @param {boolean|ShadowOptions} shadowOptions The shadow options. If
* `true`, the default options are applied. If `false`, the current
* shadow will be removed.
* @param {SVGElement} [group] The SVG group element where the shadows will
* be applied. The default is to add it to the same parent as the current
* element. Internally, this is ised for pie slices, where all the
* shadows are added to an element behind all the slices.
* @param {boolean} [cutOff] Used internally for column shadows.
*
* @returns {SVGElement} Returns the SVGElement for chaining.
*
* @example
* renderer.rect(10, 100, 100, 100)
* .attr({ fill: 'red' })
* .shadow(true);
*/
shadow: function (shadowOptions, group, cutOff) {
var shadows = [],
i,
shadow,
element = this.element,
strokeWidth,
shadowWidth,
shadowElementOpacity,
// compensate for inverted plot area
transform;
if (!shadowOptions) {
this.destroyShadows();
} else if (!this.shadows) {
shadowWidth = pick(shadowOptions.width, 3);
shadowElementOpacity = (shadowOptions.opacity || 0.15) /
shadowWidth;
transform = this.parentInverted ?
'(-1,-1)' :
'(' + pick(shadowOptions.offsetX, 1) + ', ' +
pick(shadowOptions.offsetY, 1) + ')';
for (i = 1; i <= shadowWidth; i++) {
shadow = element.cloneNode(0);
strokeWidth = (shadowWidth * 2) + 1 - (2 * i);
attr(shadow, {
'isShadow': 'true',
'stroke':
shadowOptions.color || '#000000',
'stroke-opacity': shadowElementOpacity * i,
'stroke-width': strokeWidth,
'transform': 'translate' + transform,
'fill': 'none'
});
if (cutOff) {
attr(
shadow,
'height',
Math.max(attr(shadow, 'height') - strokeWidth, 0)
);
shadow.cutHeight = strokeWidth;
}
if (group) {
group.element.appendChild(shadow);
} else if (element.parentNode) {
element.parentNode.insertBefore(shadow, element);
}
shadows.push(shadow);
}
this.shadows = shadows;
}
return this;
},
/**
* Destroy shadows on the element.
* @private
*/
destroyShadows: function () {
each(this.shadows || [], function (shadow) {
this.safeRemoveChild(shadow);
}, this);
this.shadows = undefined;
},
xGetter: function (key) {
if (this.element.nodeName === 'circle') {
if (key === 'x') {
key = 'cx';
} else if (key === 'y') {
key = 'cy';
}
}
return this._defaultGetter(key);
},
/**
* Get the current value of an attribute or pseudo attribute, used mainly
* for animation. Called internally from the {@link
* Highcharts.SVGRenderer#attr}
* function.
*
* @private
*/
_defaultGetter: function (key) {
var ret = pick(
this[key + 'Value'], // align getter
this[key],
this.element ? this.element.getAttribute(key) : null,
0
);
if (/^[\-0-9\.]+$/.test(ret)) { // is numerical
ret = parseFloat(ret);
}
return ret;
},
dSetter: function (value, key, element) {
if (value && value.join) { // join path
value = value.join(' ');
}
if (/(NaN| {2}|^$)/.test(value)) {
value = 'M 0 0';
}
// Check for cache before resetting. Resetting causes disturbance in the
// DOM, causing flickering in some cases in Edge/IE (#6747). Also
// possible performance gain.
if (this[key] !== value) {
element.setAttribute(key, value);
this[key] = value;
}
},
dashstyleSetter: function (value) {
var i,
strokeWidth = this['stroke-width'];
// If "inherit", like maps in IE, assume 1 (#4981). With HC5 and the new
// strokeWidth function, we should be able to use that instead.
if (strokeWidth === 'inherit') {
strokeWidth = 1;
}
value = value && value.toLowerCase();
if (value) {
value = value
.replace('shortdashdotdot', '3,1,1,1,1,1,')
.replace('shortdashdot', '3,1,1,1')
.replace('shortdot', '1,1,')
.replace('shortdash', '3,1,')
.replace('longdash', '8,3,')
.replace(/dot/g, '1,3,')
.replace('dash', '4,3,')
.replace(/,$/, '')
.split(','); // ending comma
i = value.length;
while (i--) {
value[i] = pInt(value[i]) * strokeWidth;
}
value = value.join(',')
.replace(/NaN/g, 'none'); // #3226
this.element.setAttribute('stroke-dasharray', value);
}
},
alignSetter: function (value) {
var convert = { left: 'start', center: 'middle', right: 'end' };
this.alignValue = value;
this.element.setAttribute('text-anchor', convert[value]);
},
opacitySetter: function (value, key, element) {
this[key] = value;
element.setAttribute(key, value);
},
titleSetter: function (value) {
var titleNode = this.element.getElementsByTagName('title')[0];
if (!titleNode) {
titleNode = doc.createElementNS(this.SVG_NS, 'title');
this.element.appendChild(titleNode);
}
// Remove text content if it exists
if (titleNode.firstChild) {
titleNode.removeChild(titleNode.firstChild);
}
titleNode.appendChild(
doc.createTextNode(
// #3276, #3895
(String(pick(value), ''))
.replace(/<[^>]*>/g, '')
.replace(/</g, '<')
.replace(/>/g, '>')
)
);
},
textSetter: function (value) {
if (value !== this.textStr) {
// Delete bBox memo when the text changes
delete this.bBox;
this.textStr = value;
if (this.added) {
this.renderer.buildText(this);
}
}
},
fillSetter: function (value, key, element) {
if (typeof value === 'string') {
element.setAttribute(key, value);
} else if (value) {
this.complexColor(value, key, element);
}
},
visibilitySetter: function (value, key, element) {
// IE9-11 doesn't handle visibilty:inherit well, so we remove the
// attribute instead (#2881, #3909)
if (value === 'inherit') {
element.removeAttribute(key);
} else if (this[key] !== value) { // #6747
element.setAttribute(key, value);
}
this[key] = value;
},
zIndexSetter: function (value, key) {
var renderer = this.renderer,
parentGroup = this.parentGroup,
parentWrapper = parentGroup || renderer,
parentNode = parentWrapper.element || renderer.box,
childNodes,
otherElement,
otherZIndex,
element = this.element,
inserted,
undefinedOtherZIndex,
svgParent = parentNode === renderer.box,
run = this.added,
i;
if (defined(value)) {
// So we can read it for other elements in the group
element.zIndex = value;
value = +value;
if (this[key] === value) { // Only update when needed (#3865)
run = false;
}
this[key] = value;
}
// Insert according to this and other elements' zIndex. Before .add() is
// called, nothing is done. Then on add, or by later calls to
// zIndexSetter, the node is placed on the right place in the DOM.
if (run) {
value = this.zIndex;
if (value && parentGroup) {
parentGroup.handleZ = true;
}
childNodes = parentNode.childNodes;
for (i = childNodes.length - 1; i >= 0 && !inserted; i--) {
otherElement = childNodes[i];
otherZIndex = otherElement.zIndex;
undefinedOtherZIndex = !defined(otherZIndex);
if (otherElement !== element) {
if (
// Negative zIndex versus no zIndex:
// On all levels except the highest. If the parent is
// <svg>, then we don't want to put items before <desc>
// or <defs>
(value < 0 && undefinedOtherZIndex && !svgParent && !i)
) {
parentNode.insertBefore(element, childNodes[i]);
inserted = true;
} else if (
// Insert after the first element with a lower zIndex
pInt(otherZIndex) <= value ||
// If negative zIndex, add this before first undefined
// zIndex element
(
undefinedOtherZIndex &&
(!defined(value) || value >= 0)
)
) {
parentNode.insertBefore(
element,
childNodes[i + 1] || null // null for oldIE export
);
inserted = true;
}
}
}
if (!inserted) {
parentNode.insertBefore(
element,
childNodes[svgParent ? 3 : 0] || null // null for oldIE
);
inserted = true;
}
}
return inserted;
},
_defaultSetter: function (value, key, element) {
element.setAttribute(key, value);
}
});
// Some shared setters and getters
SVGElement.prototype.yGetter =
SVGElement.prototype.xGetter;
SVGElement.prototype.translateXSetter =
SVGElement.prototype.translateYSetter =
SVGElement.prototype.rotationSetter =
SVGElement.prototype.verticalAlignSetter =
SVGElement.prototype.rotationOriginXSetter =
SVGElement.prototype.rotationOriginYSetter =
SVGElement.prototype.scaleXSetter =
SVGElement.prototype.scaleYSetter =
SVGElement.prototype.matrixSetter = function (value, key) {
this[key] = value;
this.doTransform = true;
};
// WebKit and Batik have problems with a stroke-width of zero, so in this case
// we remove the stroke attribute altogether. #1270, #1369, #3065, #3072.
SVGElement.prototype['stroke-widthSetter'] =
SVGElement.prototype.strokeSetter = function (value, key, element) {
this[key] = value;
// Only apply the stroke attribute if the stroke width is defined and larger
// than 0
if (this.stroke && this['stroke-width']) {
// Use prototype as instance may be overridden
SVGElement.prototype.fillSetter.call(
this,
this.stroke,
'stroke',
element
);
element.setAttribute('stroke-width', this['stroke-width']);
this.hasStroke = true;
} else if (key === 'stroke-width' && value === 0 && this.hasStroke) {
element.removeAttribute('stroke');
this.hasStroke = false;
}
};
/**
* Allows direct access to the Highcharts rendering layer in order to draw
* primitive shapes like circles, rectangles, paths or text directly on a chart,
* or independent from any chart. The SVGRenderer represents a wrapper object
* for SVG in modern browsers. Through the VMLRenderer, part of the `oldie.js`
* module, it also brings vector graphics to IE <= 8.
*
* An existing chart's renderer can be accessed through {@link Chart.renderer}.
* The renderer can also be used completely decoupled from a chart.
*
* @param {HTMLDOMElement} container - Where to put the SVG in the web page.
* @param {number} width - The width of the SVG.
* @param {number} height - The height of the SVG.
* @param {boolean} [forExport=false] - Whether the rendered content is intended
* for export.
* @param {boolean} [allowHTML=true] - Whether the renderer is allowed to
* include HTML text, which will be projected on top of the SVG.
*
* @example
* // Use directly without a chart object.
* var renderer = new Highcharts.Renderer(parentNode, 600, 400);
*
* @sample highcharts/members/renderer-on-chart
* Annotating a chart programmatically.
* @sample highcharts/members/renderer-basic
* Independent SVG drawing.
*
* @class Highcharts.SVGRenderer
*/
SVGRenderer = H.SVGRenderer = function () {
this.init.apply(this, arguments);
};
extend(SVGRenderer.prototype, /** @lends Highcharts.SVGRenderer.prototype */ {
/**
* A pointer to the renderer's associated Element class. The VMLRenderer
* will have a pointer to VMLElement here.
* @type {SVGElement}
*/
Element: SVGElement,
SVG_NS: SVG_NS,
/**
* Initialize the SVGRenderer. Overridable initiator function that takes
* the same parameters as the constructor.
*/
init: function (container, width, height, style, forExport, allowHTML) {
var renderer = this,
boxWrapper,
element,
desc;
boxWrapper = renderer.createElement('svg')
.attr({
'version': '1.1',
'class': 'highcharts-root'
})
.css(this.getStyle(style))
;
element = boxWrapper.element;
container.appendChild(element);
// Always use ltr on the container, otherwise text-anchor will be
// flipped and text appear outside labels, buttons, tooltip etc (#3482)
attr(container, 'dir', 'ltr');
// For browsers other than IE, add the namespace attribute (#1978)
if (container.innerHTML.indexOf('xmlns') === -1) {
attr(element, 'xmlns', this.SVG_NS);
}
// object properties
renderer.isSVG = true;
/**
* The root `svg` node of the renderer.
* @name box
* @memberOf SVGRenderer
* @type {SVGDOMElement}
*/
this.box = element;
/**
* The wrapper for the root `svg` node of the renderer.
*
* @name boxWrapper
* @memberOf SVGRenderer
* @type {SVGElement}
*/
this.boxWrapper = boxWrapper;
renderer.alignedObjects = [];
/**
* Page url used for internal references.
* @type {string}
*/
// #24, #672, #1070
this.url = (
(isFirefox || isWebKit) &&
doc.getElementsByTagName('base').length
) ?
win.location.href
.replace(/#.*?$/, '') // remove the hash
.replace(/<[^>]*>/g, '') // wing cut HTML
// escape parantheses and quotes
.replace(/([\('\)])/g, '\\$1')
// replace spaces (needed for Safari only)
.replace(/ /g, '%20') :
'';
// Add description
desc = this.createElement('desc').add();
desc.element.appendChild(
doc.createTextNode('Created with @product.name@ @product.version@')
);
/**
* A pointer to the `defs` node of the root SVG.
* @type {SVGElement}
* @name defs
* @memberOf SVGRenderer
*/
renderer.defs = this.createElement('defs').add();
renderer.allowHTML = allowHTML;
renderer.forExport = forExport;
renderer.gradients = {}; // Object where gradient SvgElements are stored
renderer.cache = {}; // Cache for numerical bounding boxes
renderer.cacheKeys = [];
renderer.imgCount = 0;
renderer.setSize(width, height, false);
// Issue 110 workaround:
// In Firefox, if a div is positioned by percentage, its pixel position
// may land between pixels. The container itself doesn't display this,
// but an SVG element inside this container will be drawn at subpixel
// precision. In order to draw sharp lines, this must be compensated
// for. This doesn't seem to work inside iframes though (like in
// jsFiddle).
var subPixelFix, rect;
if (isFirefox && container.getBoundingClientRect) {
subPixelFix = function () {
css(container, { left: 0, top: 0 });
rect = container.getBoundingClientRect();
css(container, {
left: (Math.ceil(rect.left) - rect.left) + 'px',
top: (Math.ceil(rect.top) - rect.top) + 'px'
});
};
// run the fix now
subPixelFix();
// run it on resize
renderer.unSubPixelFix = addEvent(win, 'resize', subPixelFix);
}
},
/**
* Get the global style setting for the renderer.
* @private
* @param {CSSObject} style - Style settings.
* @return {CSSObject} The style settings mixed with defaults.
*/
getStyle: function (style) {
this.style = extend({
fontFamily: '"Lucida Grande", "Lucida Sans Unicode", ' +
'Arial, Helvetica, sans-serif',
fontSize: '12px'
}, style);
return this.style;
},
/**
* Apply the global style on the renderer, mixed with the default styles.
*
* @param {CSSObject} style - CSS to apply.
*/
setStyle: function (style) {
this.boxWrapper.css(this.getStyle(style));
},
/**
* Detect whether the renderer is hidden. This happens when one of the
* parent elements has `display: none`. Used internally to detect when we
* needto render preliminarily in another div to get the text bounding boxes
* right.
*
* @returns {boolean} True if it is hidden.
*/
isHidden: function () { // #608
return !this.boxWrapper.getBBox().width;
},
/**
* Destroys the renderer and its allocated members.
*/
destroy: function () {
var renderer = this,
rendererDefs = renderer.defs;
renderer.box = null;
renderer.boxWrapper = renderer.boxWrapper.destroy();
// Call destroy on all gradient elements
destroyObjectProperties(renderer.gradients || {});
renderer.gradients = null;
// Defs are null in VMLRenderer
// Otherwise, destroy them here.
if (rendererDefs) {
renderer.defs = rendererDefs.destroy();
}
// Remove sub pixel fix handler (#982)
if (renderer.unSubPixelFix) {
renderer.unSubPixelFix();
}
renderer.alignedObjects = null;
return null;
},
/**
* Create a wrapper for an SVG element. Serves as a factory for
* {@link SVGElement}, but this function is itself mostly called from
* primitive factories like {@link SVGRenderer#path}, {@link
* SVGRenderer#rect} or {@link SVGRenderer#text}.
*
* @param {string} nodeName - The node name, for example `rect`, `g` etc.
* @returns {SVGElement} The generated SVGElement.
*/
createElement: function (nodeName) {
var wrapper = new this.Element();
wrapper.init(this, nodeName);
return wrapper;
},
/**
* Dummy function for plugins, called every time the renderer is updated.
* Prior to Highcharts 5, this was used for the canvg renderer.
* @function
*/
draw: noop,
/**
* Get converted radial gradient attributes according to the radial
* reference. Used internally from the {@link SVGElement#colorGradient}
* function.
*
* @private
*/
getRadialAttr: function (radialReference, gradAttr) {
return {
cx: (radialReference[0] - radialReference[2] / 2) +
gradAttr.cx * radialReference[2],
cy: (radialReference[1] - radialReference[2] / 2) +
gradAttr.cy * radialReference[2],
r: gradAttr.r * radialReference[2]
};
},
/**
* Extendable function to measure the tspan width.
*
* @private
*/
getSpanWidth: function (wrapper) {
return wrapper.getBBox(true).width;
},
applyEllipsis: function (wrapper, tspan, text, width) {
var renderer = this,
rotation = wrapper.rotation,
str = text,
currentIndex,
minIndex = 0,
maxIndex = text.length,
updateTSpan = function (s) {
tspan.removeChild(tspan.firstChild);
if (s) {
tspan.appendChild(doc.createTextNode(s));
}
},
actualWidth,
wasTooLong;
wrapper.rotation = 0; // discard rotation when computing box
actualWidth = renderer.getSpanWidth(wrapper, tspan);
wasTooLong = actualWidth > width;
if (wasTooLong) {
while (minIndex <= maxIndex) {
currentIndex = Math.ceil((minIndex + maxIndex) / 2);
str = text.substring(0, currentIndex) + '\u2026';
updateTSpan(str);
actualWidth = renderer.getSpanWidth(wrapper, tspan);
if (minIndex === maxIndex) {
// Complete
minIndex = maxIndex + 1;
} else if (actualWidth > width) {
// Too large. Set max index to current.
maxIndex = currentIndex - 1;
} else {
// Within width. Set min index to current.
minIndex = currentIndex;
}
}
// If max index was 0 it means just ellipsis was also to large.
if (maxIndex === 0) {
// Remove ellipses.
updateTSpan('');
}
}
wrapper.rotation = rotation; // Apply rotation again.
return wasTooLong;
},
/**
* A collection of characters mapped to HTML entities. When `useHTML` on an
* element is true, these entities will be rendered correctly by HTML. In
* the SVG pseudo-HTML, they need to be unescaped back to simple characters,
* so for example `<` will render as `<`.
*
* @example
* // Add support for unescaping quotes
* Highcharts.SVGRenderer.prototype.escapes['"'] = '"';
*
* @type {Object}
*/
escapes: {
'&': '&',
'<': '<',
'>': '>',
"'": ''', // eslint-disable-line quotes
'"': '"'
},
/**
* Parse a simple HTML string into SVG tspans. Called internally when text
* is set on an SVGElement. The function supports a subset of HTML tags,
* CSS text features like `width`, `text-overflow`, `white-space`, and
* also attributes like `href` and `style`.
* @private
* @param {SVGElement} wrapper The parent SVGElement.
*/
buildText: function (wrapper) {
var textNode = wrapper.element,
renderer = this,
forExport = renderer.forExport,
textStr = pick(wrapper.textStr, '').toString(),
hasMarkup = textStr.indexOf('<') !== -1,
lines,
childNodes = textNode.childNodes,
wasTooLong,
parentX = attr(textNode, 'x'),
textStyles = wrapper.styles,
width = wrapper.textWidth,
textLineHeight = textStyles && textStyles.lineHeight,
textOutline = textStyles && textStyles.textOutline,
ellipsis = textStyles && textStyles.textOverflow === 'ellipsis',
noWrap = textStyles && textStyles.whiteSpace === 'nowrap',
fontSize = textStyles && textStyles.fontSize,
textCache,
isSubsequentLine,
i = childNodes.length,
tempParent = width && !wrapper.added && this.box,
getLineHeight = function (tspan) {
var fontSizeStyle;
fontSizeStyle = /(px|em)$/.test(tspan && tspan.style.fontSize) ?
tspan.style.fontSize :
(fontSize || renderer.style.fontSize || 12);
return textLineHeight ?
pInt(textLineHeight) :
renderer.fontMetrics(
fontSizeStyle,
// Get the computed size from parent if not explicit
tspan.getAttribute('style') ? tspan : textNode
).h;
},
unescapeEntities = function (inputStr, except) {
objectEach(renderer.escapes, function (value, key) {
if (!except || inArray(value, except) === -1) {
inputStr = inputStr.toString().replace(
new RegExp(value, 'g'), // eslint-disable-line security/detect-non-literal-regexp
key
);
}
});
return inputStr;
},
parseAttribute = function (s, attr) {
var start,
delimiter;
start = s.indexOf('<');
s = s.substring(start, s.indexOf('>') - start);
start = s.indexOf(attr + '=');
if (start !== -1) {
start = start + attr.length + 1;
delimiter = s.charAt(start);
if (delimiter === '"' || delimiter === "'") { // eslint-disable-line quotes
s = s.substring(start + 1);
return s.substring(0, s.indexOf(delimiter));
}
}
};
// The buildText code is quite heavy, so if we're not changing something
// that affects the text, skip it (#6113).
textCache = [
textStr,
ellipsis,
noWrap,
textLineHeight,
textOutline,
fontSize,
width
].join(',');
if (textCache === wrapper.textCache) {
return;
}
wrapper.textCache = textCache;
// Remove old text
while (i--) {
textNode.removeChild(childNodes[i]);
}
// Skip tspans, add text directly to text node. The forceTSpan is a hook
// used in text outline hack.
if (
!hasMarkup &&
!textOutline &&
!ellipsis &&
!width &&
textStr.indexOf(' ') === -1
) {
textNode.appendChild(doc.createTextNode(unescapeEntities(textStr)));
// Complex strings, add more logic
} else {
if (tempParent) {
// attach it to the DOM to read offset width
tempParent.appendChild(textNode);
}
if (hasMarkup) {
lines = textStr
.replace(/<(b|strong)>/g, '<span style="font-weight:bold">')
.replace(/<(i|em)>/g, '<span style="font-style:italic">')
.replace(/<a/g, '<span')
.replace(/<\/(b|strong|i|em|a)>/g, '</span>')
.split(/<br.*?>/g);
} else {
lines = [textStr];
}
// Trim empty lines (#5261)
lines = grep(lines, function (line) {
return line !== '';
});
// build the lines
each(lines, function buildTextLines(line, lineNo) {
var spans,
spanNo = 0;
line = line
// Trim to prevent useless/costly process on the spaces
// (#5258)
.replace(/^\s+|\s+$/g, '')
.replace(/<span/g, '|||<span')
.replace(/<\/span>/g, '</span>|||');
spans = line.split('|||');
each(spans, function buildTextSpans(span) {
if (span !== '' || spans.length === 1) {
var attributes = {},
tspan = doc.createElementNS(
renderer.SVG_NS,
'tspan'
),
classAttribute,
styleAttribute, // #390
hrefAttribute;
classAttribute = parseAttribute(span, 'class');
if (classAttribute) {
attr(tspan, 'class', classAttribute);
}
styleAttribute = parseAttribute(span, 'style');
if (styleAttribute) {
styleAttribute = styleAttribute.replace(
/(;| |^)color([ :])/,
'$1fill$2'
);
attr(tspan, 'style', styleAttribute);
}
// Not for export - #1529
hrefAttribute = parseAttribute(span, 'href');
if (hrefAttribute && !forExport) {
attr(
tspan,
'onclick',
'location.href=\"' + hrefAttribute + '\"'
);
attr(tspan, 'class', 'highcharts-anchor');
css(tspan, { cursor: 'pointer' });
}
// Strip away unsupported HTML tags (#7126)
span = unescapeEntities(
span.replace(/<[a-zA-Z\/](.|\n)*?>/g, '') || ' '
);
// Nested tags aren't supported, and cause crash in
// Safari (#1596)
if (span !== ' ') {
// add the text node
tspan.appendChild(doc.createTextNode(span));
// First span in a line, align it to the left
if (!spanNo) {
if (lineNo && parentX !== null) {
attributes.x = parentX;
}
} else {
attributes.dx = 0; // #16
}
// add attributes
attr(tspan, attributes);
// Append it
textNode.appendChild(tspan);
// first span on subsequent line, add the line
// height
if (!spanNo && isSubsequentLine) {
// allow getting the right offset height in
// exporting in IE
if (!svg && forExport) {
css(tspan, { display: 'block' });
}
// Set the line height based on the font size of
// either the text element or the tspan element
attr(
tspan,
'dy',
getLineHeight(tspan)
);
}
/*
// Experimental text wrapping based on
// getSubstringLength
if (width) {
var spans = renderer.breakText(wrapper, width);
each(spans, function (span) {
var dy = getLineHeight(tspan);
tspan = doc.createElementNS(
SVG_NS,
'tspan'
);
tspan.appendChild(
doc.createTextNode(span)
);
attr(tspan, {
dy: dy,
x: parentX
});
if (spanStyle) { // #390
attr(tspan, 'style', spanStyle);
}
textNode.appendChild(tspan);
});
}
// */
// Check width and apply soft breaks or ellipsis
if (width) {
var words = span.replace(
/([^\^])-/g,
'$1- '
).split(' '), // #1273
hasWhiteSpace = (
spans.length > 1 ||
lineNo ||
(words.length > 1 && !noWrap)
),
tooLong,
rest = [],
actualWidth,
dy = getLineHeight(tspan),
rotation = wrapper.rotation;
if (ellipsis) {
wasTooLong = renderer.applyEllipsis(
wrapper,
tspan,
span,
width
);
}
while (
!ellipsis &&
hasWhiteSpace &&
(words.length || rest.length)
) {
// discard rotation when computing box
wrapper.rotation = 0;
actualWidth = renderer.getSpanWidth(
wrapper,
tspan
);
tooLong = actualWidth > width;
// For ellipsis, do a binary search for the
// correct string length
if (wasTooLong === undefined) {
wasTooLong = tooLong; // First time
}
// Looping down, this is the first word
// sequence that is not too long, so we can
// move on to build the next line.
if (!tooLong || words.length === 1) {
words = rest;
rest = [];
if (words.length && !noWrap) {
tspan = doc.createElementNS(
SVG_NS,
'tspan'
);
attr(tspan, {
dy: dy,
x: parentX
});
if (styleAttribute) { // #390
attr(
tspan,
'style',
styleAttribute
);
}
textNode.appendChild(tspan);
}
// a single word is pressing it out
if (actualWidth > width) {
width = actualWidth;
}
} else { // append to existing line tspan
tspan.removeChild(tspan.firstChild);
rest.unshift(words.pop());
}
if (words.length) {
tspan.appendChild(
doc.createTextNode(
words.join(' ')
.replace(/- /g, '-')
)
);
}
}
wrapper.rotation = rotation;
}
spanNo++;
}
}
});
// To avoid beginning lines that doesn't add to the textNode
// (#6144)
isSubsequentLine = (
isSubsequentLine ||
textNode.childNodes.length
);
});
if (wasTooLong) {
wrapper.attr(
'title',
unescapeEntities(wrapper.textStr, ['<', '>']) // #7179
);
}
if (tempParent) {
tempParent.removeChild(textNode);
}
// Apply the text outline
if (textOutline && wrapper.applyTextOutline) {
wrapper.applyTextOutline(textOutline);
}
}
},
/*
breakText: function (wrapper, width) {
var bBox = wrapper.getBBox(),
node = wrapper.element,
charnum = node.textContent.length,
stringWidth,
// try this position first, based on average character width
guessedLineCharLength = Math.round(width * charnum / bBox.width),
pos = guessedLineCharLength,
spans = [],
increment = 0,
startPos = 0,
endPos,
safe = 0;
if (bBox.width > width) {
while (startPos < charnum && safe < 100) {
while (endPos === undefined && safe < 100) {
stringWidth = node.getSubStringLength(
startPos,
pos - startPos
);
if (stringWidth <= width) {
if (increment === -1) {
endPos = pos;
} else {
increment = 1;
}
} else {
if (increment === 1) {
endPos = pos - 1;
} else {
increment = -1;
}
}
pos += increment;
safe++;
}
spans.push(
node.textContent.substr(startPos, endPos - startPos)
);
startPos = endPos;
pos = startPos + guessedLineCharLength;
endPos = undefined;
}
}
return spans;
},
// */
/**
* Returns white for dark colors and black for bright colors.
*
* @param {ColorString} rgba - The color to get the contrast for.
* @returns {string} The contrast color, either `#000000` or `#FFFFFF`.
*/
getContrast: function (rgba) {
rgba = color(rgba).rgba;
// The threshold may be discussed. Here's a proposal for adding
// different weight to the color channels (#6216)
/*
rgba[0] *= 1; // red
rgba[1] *= 1.2; // green
rgba[2] *= 0.7; // blue
*/
return rgba[0] + rgba[1] + rgba[2] > 2 * 255 ? '#000000' : '#FFFFFF';
},
/**
* Create a button with preset states.
* @param {string} text - The text or HTML to draw.
* @param {number} x - The x position of the button's left side.
* @param {number} y - The y position of the button's top side.
* @param {Function} callback - The function to execute on button click or
* touch.
* @param {SVGAttributes} [normalState] - SVG attributes for the normal
* state.
* @param {SVGAttributes} [hoverState] - SVG attributes for the hover state.
* @param {SVGAttributes} [pressedState] - SVG attributes for the pressed
* state.
* @param {SVGAttributes} [disabledState] - SVG attributes for the disabled
* state.
* @param {Symbol} [shape=rect] - The shape type.
* @returns {SVGRenderer} The button element.
*/
button: function (
text,
x,
y,
callback,
normalState,
hoverState,
pressedState,
disabledState,
shape
) {
var label = this.label(
text,
x,
y,
shape,
null,
null,
null,
null,
'button'
),
curState = 0;
// Default, non-stylable attributes
label.attr(merge({
'padding': 8,
'r': 2
}, normalState));
// Presentational
var normalStyle,
hoverStyle,
pressedStyle,
disabledStyle;
// Normal state - prepare the attributes
normalState = merge({
fill: '#f7f7f7',
stroke: '#cccccc',
'stroke-width': 1,
style: {
color: '#333333',
cursor: 'pointer',
fontWeight: 'normal'
}
}, normalState);
normalStyle = normalState.style;
delete normalState.style;
// Hover state
hoverState = merge(normalState, {
fill: '#e6e6e6'
}, hoverState);
hoverStyle = hoverState.style;
delete hoverState.style;
// Pressed state
pressedState = merge(normalState, {
fill: '#e6ebf5',
style: {
color: '#000000',
fontWeight: 'bold'
}
}, pressedState);
pressedStyle = pressedState.style;
delete pressedState.style;
// Disabled state
disabledState = merge(normalState, {
style: {
color: '#cccccc'
}
}, disabledState);
disabledStyle = disabledState.style;
delete disabledState.style;
// Add the events. IE9 and IE10 need mouseover and mouseout to funciton
// (#667).
addEvent(label.element, isMS ? 'mouseover' : 'mouseenter', function () {
if (curState !== 3) {
label.setState(1);
}
});
addEvent(label.element, isMS ? 'mouseout' : 'mouseleave', function () {
if (curState !== 3) {
label.setState(curState);
}
});
label.setState = function (state) {
// Hover state is temporary, don't record it
if (state !== 1) {
label.state = curState = state;
}
// Update visuals
label.removeClass(
/highcharts-button-(normal|hover|pressed|disabled)/
)
.addClass(
'highcharts-button-' +
['normal', 'hover', 'pressed', 'disabled'][state || 0]
);
label.attr([
normalState,
hoverState,
pressedState,
disabledState
][state || 0])
.css([
normalStyle,
hoverStyle,
pressedStyle,
disabledStyle
][state || 0]);
};
// Presentational attributes
label
.attr(normalState)
.css(extend({ cursor: 'default' }, normalStyle));
return label
.on('click', function (e) {
if (curState !== 3) {
callback.call(label, e);
}
});
},
/**
* Make a straight line crisper by not spilling out to neighbour pixels.
*
* @param {Array} points - The original points on the format
* `['M', 0, 0, 'L', 100, 0]`.
* @param {number} width - The width of the line.
* @returns {Array} The original points array, but modified to render
* crisply.
*/
crispLine: function (points, width) {
// normalize to a crisp line
if (points[1] === points[4]) {
// Substract due to #1129. Now bottom and left axis gridlines behave
// the same.
points[1] = points[4] = Math.round(points[1]) - (width % 2 / 2);
}
if (points[2] === points[5]) {
points[2] = points[5] = Math.round(points[2]) + (width % 2 / 2);
}
return points;
},
/**
* Draw a path, wraps the SVG `path` element.
*
* @param {Array} [path] An SVG path definition in array form.
*
* @example
* var path = renderer.path(['M', 10, 10, 'L', 30, 30, 'z'])
* .attr({ stroke: '#ff00ff' })
* .add();
* @returns {SVGElement} The generated wrapper element.
*
* @sample highcharts/members/renderer-path-on-chart/
* Draw a path in a chart
* @sample highcharts/members/renderer-path/
* Draw a path independent from a chart
*
*//**
* Draw a path, wraps the SVG `path` element.
*
* @param {SVGAttributes} [attribs] The initial attributes.
* @returns {SVGElement} The generated wrapper element.
*/
path: function (path) {
var attribs = {
fill: 'none'
};
if (isArray(path)) {
attribs.d = path;
} else if (isObject(path)) { // attributes
extend(attribs, path);
}
return this.createElement('path').attr(attribs);
},
/**
* Draw a circle, wraps the SVG `circle` element.
*
* @param {number} [x] The center x position.
* @param {number} [y] The center y position.
* @param {number} [r] The radius.
* @returns {SVGElement} The generated wrapper element.
*
* @sample highcharts/members/renderer-circle/ Drawing a circle
*//**
* Draw a circle, wraps the SVG `circle` element.
*
* @param {SVGAttributes} [attribs] The initial attributes.
* @returns {SVGElement} The generated wrapper element.
*/
circle: function (x, y, r) {
var attribs = isObject(x) ? x : { x: x, y: y, r: r },
wrapper = this.createElement('circle');
// Setting x or y translates to cx and cy
wrapper.xSetter = wrapper.ySetter = function (value, key, element) {
element.setAttribute('c' + key, value);
};
return wrapper.attr(attribs);
},
/**
* Draw and return an arc.
* @param {number} [x=0] Center X position.
* @param {number} [y=0] Center Y position.
* @param {number} [r=0] The outer radius of the arc.
* @param {number} [innerR=0] Inner radius like used in donut charts.
* @param {number} [start=0] The starting angle of the arc in radians, where
* 0 is to the right and `-Math.PI/2` is up.
* @param {number} [end=0] The ending angle of the arc in radians, where 0
* is to the right and `-Math.PI/2` is up.
* @returns {SVGElement} The generated wrapper element.
*
* @sample highcharts/members/renderer-arc/
* Drawing an arc
*//**
* Draw and return an arc. Overloaded function that takes arguments object.
* @param {SVGAttributes} attribs Initial SVG attributes.
* @returns {SVGElement} The generated wrapper element.
*/
arc: function (x, y, r, innerR, start, end) {
var arc,
options;
if (isObject(x)) {
options = x;
y = options.y;
r = options.r;
innerR = options.innerR;
start = options.start;
end = options.end;
x = options.x;
} else {
options = {
innerR: innerR,
start: start,
end: end
};
}
// Arcs are defined as symbols for the ability to set
// attributes in attr and animate
arc = this.symbol('arc', x, y, r, r, options);
arc.r = r; // #959
return arc;
},
/**
* Draw and return a rectangle.
* @param {number} [x] Left position.
* @param {number} [y] Top position.
* @param {number} [width] Width of the rectangle.
* @param {number} [height] Height of the rectangle.
* @param {number} [r] Border corner radius.
* @param {number} [strokeWidth] A stroke width can be supplied to allow
* crisp drawing.
* @returns {SVGElement} The generated wrapper element.
*//**
* Draw and return a rectangle.
* @param {SVGAttributes} [attributes]
* General SVG attributes for the rectangle.
* @return {SVGElement}
* The generated wrapper element.
*
* @sample highcharts/members/renderer-rect-on-chart/
* Draw a rectangle in a chart
* @sample highcharts/members/renderer-rect/
* Draw a rectangle independent from a chart
*/
rect: function (x, y, width, height, r, strokeWidth) {
r = isObject(x) ? x.r : r;
var wrapper = this.createElement('rect'),
attribs = isObject(x) ? x : x === undefined ? {} : {
x: x,
y: y,
width: Math.max(width, 0),
height: Math.max(height, 0)
};
if (strokeWidth !== undefined) {
attribs.strokeWidth = strokeWidth;
attribs = wrapper.crisp(attribs);
}
attribs.fill = 'none';
if (r) {
attribs.r = r;
}
wrapper.rSetter = function (value, key, element) {
attr(element, {
rx: value,
ry: value
});
};
return wrapper.attr(attribs);
},
/**
* Resize the {@link SVGRenderer#box} and re-align all aligned child
* elements.
* @param {number} width
* The new pixel width.
* @param {number} height
* The new pixel height.
* @param {Boolean|AnimationOptions} [animate=true]
* Whether and how to animate.
*/
setSize: function (width, height, animate) {
var renderer = this,
alignedObjects = renderer.alignedObjects,
i = alignedObjects.length;
renderer.width = width;
renderer.height = height;
renderer.boxWrapper.animate({
width: width,
height: height
}, {
step: function () {
this.attr({
viewBox: '0 0 ' + this.attr('width') + ' ' +
this.attr('height')
});
},
duration: pick(animate, true) ? undefined : 0
});
while (i--) {
alignedObjects[i].align();
}
},
/**
* Create and return an svg group element. Child
* {@link Highcharts.SVGElement} objects are added to the group by using the
* group as the first parameter
* in {@link Highcharts.SVGElement#add|add()}.
*
* @param {string} [name] The group will be given a class name of
* `highcharts-{name}`. This can be used for styling and scripting.
* @returns {SVGElement} The generated wrapper element.
*
* @sample highcharts/members/renderer-g/
* Show and hide grouped objects
*/
g: function (name) {
var elem = this.createElement('g');
return name ? elem.attr({ 'class': 'highcharts-' + name }) : elem;
},
/**
* Display an image.
* @param {string} src The image source.
* @param {number} [x] The X position.
* @param {number} [y] The Y position.
* @param {number} [width] The image width. If omitted, it defaults to the
* image file width.
* @param {number} [height] The image height. If omitted it defaults to the
* image file height.
* @param {function} [onload] Event handler for image load.
* @returns {SVGElement} The generated wrapper element.
*
* @sample highcharts/members/renderer-image-on-chart/
* Add an image in a chart
* @sample highcharts/members/renderer-image/
* Add an image independent of a chart
*/
image: function (src, x, y, width, height, onload) {
var attribs = {
preserveAspectRatio: 'none'
},
elemWrapper,
dummy,
setSVGImageSource = function (el, src) {
// Set the href in the xlink namespace
if (el.setAttributeNS) {
el.setAttributeNS(
'http://www.w3.org/1999/xlink', 'href', src
);
} else {
// could be exporting in IE
// using href throws "not supported" in ie7 and under,
// requries regex shim to fix later
el.setAttribute('hc-svg-href', src);
}
};
// optional properties
if (arguments.length > 1) {
extend(attribs, {
x: x,
y: y,
width: width,
height: height
});
}
elemWrapper = this.createElement('image').attr(attribs);
// Add load event if supplied
if (onload) {
// We have to use a dummy HTML image since IE support for SVG image
// load events is very buggy. First set a transparent src, wait for
// dummy to load, and then add the real src to the SVG image.
setSVGImageSource(
elemWrapper.element,
'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' /* eslint-disable-line */
);
dummy = new win.Image();
addEvent(dummy, 'load', function (e) {
setSVGImageSource(elemWrapper.element, src);
onload.call(elemWrapper, e);
});
dummy.src = src;
} else {
setSVGImageSource(elemWrapper.element, src);
}
return elemWrapper;
},
/**
* Draw a symbol out of pre-defined shape paths from
* {@link SVGRenderer#symbols}.
* It is used in Highcharts for point makers, which cake a `symbol` option,
* and label and button backgrounds like in the tooltip and stock flags.
*
* @param {Symbol} symbol - The symbol name.
* @param {number} x - The X coordinate for the top left position.
* @param {number} y - The Y coordinate for the top left position.
* @param {number} width - The pixel width.
* @param {number} height - The pixel height.
* @param {Object} [options] - Additional options, depending on the actual
* symbol drawn.
* @param {number} [options.anchorX] - The anchor X position for the
* `callout` symbol. This is where the chevron points to.
* @param {number} [options.anchorY] - The anchor Y position for the
* `callout` symbol. This is where the chevron points to.
* @param {number} [options.end] - The end angle of an `arc` symbol.
* @param {boolean} [options.open] - Whether to draw `arc` symbol open or
* closed.
* @param {number} [options.r] - The radius of an `arc` symbol, or the
* border radius for the `callout` symbol.
* @param {number} [options.start] - The start angle of an `arc` symbol.
*/
symbol: function (symbol, x, y, width, height, options) {
var ren = this,
obj,
imageRegex = /^url\((.*?)\)$/,
isImage = imageRegex.test(symbol),
sym = !isImage && (this.symbols[symbol] ? symbol : 'circle'),
// get the symbol definition function
symbolFn = sym && this.symbols[sym],
// check if there's a path defined for this symbol
path = defined(x) && symbolFn && symbolFn.call(
this.symbols,
Math.round(x),
Math.round(y),
width,
height,
options
),
imageSrc,
centerImage;
if (symbolFn) {
obj = this.path(path);
obj.attr('fill', 'none');
// expando properties for use in animate and attr
extend(obj, {
symbolName: sym,
x: x,
y: y,
width: width,
height: height
});
if (options) {
extend(obj, options);
}
// Image symbols
} else if (isImage) {
imageSrc = symbol.match(imageRegex)[1];
// Create the image synchronously, add attribs async
obj = this.image(imageSrc);
// The image width is not always the same as the symbol width. The
// image may be centered within the symbol, as is the case when
// image shapes are used as label backgrounds, for example in flags.
obj.imgwidth = pick(
symbolSizes[imageSrc] && symbolSizes[imageSrc].width,
options && options.width
);
obj.imgheight = pick(
symbolSizes[imageSrc] && symbolSizes[imageSrc].height,
options && options.height
);
/**
* Set the size and position
*/
centerImage = function () {
obj.attr({
width: obj.width,
height: obj.height
});
};
/**
* Width and height setters that take both the image's physical size
* and the label size into consideration, and translates the image
* to center within the label.
*/
each(['width', 'height'], function (key) {
obj[key + 'Setter'] = function (value, key) {
var attribs = {},
imgSize = this['img' + key],
trans = key === 'width' ? 'translateX' : 'translateY';
this[key] = value;
if (defined(imgSize)) {
if (this.element) {
this.element.setAttribute(key, imgSize);
}
if (!this.alignByTranslate) {
attribs[trans] = ((this[key] || 0) - imgSize) / 2;
this.attr(attribs);
}
}
};
});
if (defined(x)) {
obj.attr({
x: x,
y: y
});
}
obj.isImg = true;
if (defined(obj.imgwidth) && defined(obj.imgheight)) {
centerImage();
} else {
// Initialize image to be 0 size so export will still function
// if there's no cached sizes.
obj.attr({ width: 0, height: 0 });
// Create a dummy JavaScript image to get the width and height.
createElement('img', {
onload: function () {
var chart = charts[ren.chartIndex];
// Special case for SVGs on IE11, the width is not
// accessible until the image is part of the DOM
// (#2854).
if (this.width === 0) {
css(this, {
position: 'absolute',
top: '-999em'
});
doc.body.appendChild(this);
}
// Center the image
symbolSizes[imageSrc] = { // Cache for next
width: this.width,
height: this.height
};
obj.imgwidth = this.width;
obj.imgheight = this.height;
if (obj.element) {
centerImage();
}
// Clean up after #2854 workaround.
if (this.parentNode) {
this.parentNode.removeChild(this);
}
// Fire the load event when all external images are
// loaded
ren.imgCount--;
if (!ren.imgCount && chart && chart.onload) {
chart.onload();
}
},
src: imageSrc
});
this.imgCount++;
}
}
return obj;
},
/**
* @typedef {string} Symbol
*
* Can be one of `arc`, `callout`, `circle`, `diamond`, `square`,
* `triangle`, `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}.
*/
/**
* An extendable collection of functions for defining symbol paths.
*/
symbols: {
'circle': function (x, y, w, h) {
// Return a full arc
return this.arc(x + w / 2, y + h / 2, w / 2, h / 2, {
start: 0,
end: Math.PI * 2,
open: false
});
},
'square': function (x, y, w, h) {
return [
'M', x, y,
'L', x + w, y,
x + w, y + h,
x, y + h,
'Z'
];
},
'triangle': function (x, y, w, h) {
return [
'M', x + w / 2, y,
'L', x + w, y + h,
x, y + h,
'Z'
];
},
'triangle-down': function (x, y, w, h) {
return [
'M', x, y,
'L', x + w, y,
x + w / 2, y + h,
'Z'
];
},
'diamond': function (x, y, w, h) {
return [
'M', x + w / 2, y,
'L', x + w, y + h / 2,
x + w / 2, y + h,
x, y + h / 2,
'Z'
];
},
'arc': function (x, y, w, h, options) {
var start = options.start,
rx = options.r || w,
ry = options.r || h || w,
proximity = 0.001,
fullCircle =
Math.abs(options.end - options.start - 2 * Math.PI) <
proximity,
// Substract a small number to prevent cos and sin of start and
// end from becoming equal on 360 arcs (related: #1561)
end = options.end - proximity,
innerRadius = options.innerR,
open = pick(options.open, fullCircle),
cosStart = Math.cos(start),
sinStart = Math.sin(start),
cosEnd = Math.cos(end),
sinEnd = Math.sin(end),
// Proximity takes care of rounding errors around PI (#6971)
longArc = options.end - start - Math.PI < proximity ? 0 : 1,
arc;
arc = [
'M',
x + rx * cosStart,
y + ry * sinStart,
'A', // arcTo
rx, // x radius
ry, // y radius
0, // slanting
longArc, // long or short arc
1, // clockwise
x + rx * cosEnd,
y + ry * sinEnd
];
if (defined(innerRadius)) {
arc.push(
open ? 'M' : 'L',
x + innerRadius * cosEnd,
y + innerRadius * sinEnd,
'A', // arcTo
innerRadius, // x radius
innerRadius, // y radius
0, // slanting
longArc, // long or short arc
0, // clockwise
x + innerRadius * cosStart,
y + innerRadius * sinStart
);
}
arc.push(open ? '' : 'Z'); // close
return arc;
},
/**
* Callout shape used for default tooltips, also used for rounded
* rectangles in VML
*/
callout: function (x, y, w, h, options) {
var arrowLength = 6,
halfDistance = 6,
r = Math.min((options && options.r) || 0, w, h),
safeDistance = r + halfDistance,
anchorX = options && options.anchorX,
anchorY = options && options.anchorY,
path;
path = [
'M', x + r, y,
'L', x + w - r, y, // top side
'C', x + w, y, x + w, y, x + w, y + r, // top-right corner
'L', x + w, y + h - r, // right side
'C', x + w, y + h, x + w, y + h, x + w - r, y + h, // bottom-rgt
'L', x + r, y + h, // bottom side
'C', x, y + h, x, y + h, x, y + h - r, // bottom-left corner
'L', x, y + r, // left side
'C', x, y, x, y, x + r, y // top-left corner
];
// Anchor on right side
if (anchorX && anchorX > w) {
// Chevron
if (
anchorY > y + safeDistance &&
anchorY < y + h - safeDistance
) {
path.splice(13, 3,
'L', x + w, anchorY - halfDistance,
x + w + arrowLength, anchorY,
x + w, anchorY + halfDistance,
x + w, y + h - r
);
// Simple connector
} else {
path.splice(13, 3,
'L', x + w, h / 2,
anchorX, anchorY,
x + w, h / 2,
x + w, y + h - r
);
}
// Anchor on left side
} else if (anchorX && anchorX < 0) {
// Chevron
if (
anchorY > y + safeDistance &&
anchorY < y + h - safeDistance
) {
path.splice(33, 3,
'L', x, anchorY + halfDistance,
x - arrowLength, anchorY,
x, anchorY - halfDistance,
x, y + r
);
// Simple connector
} else {
path.splice(33, 3,
'L', x, h / 2,
anchorX, anchorY,
x, h / 2,
x, y + r
);
}
} else if ( // replace bottom
anchorY &&
anchorY > h &&
anchorX > x + safeDistance &&
anchorX < x + w - safeDistance
) {
path.splice(23, 3,
'L', anchorX + halfDistance, y + h,
anchorX, y + h + arrowLength,
anchorX - halfDistance, y + h,
x + r, y + h
);
} else if ( // replace top
anchorY &&
anchorY < 0 &&
anchorX > x + safeDistance &&
anchorX < x + w - safeDistance
) {
path.splice(3, 3,
'L', anchorX - halfDistance, y,
anchorX, y - arrowLength,
anchorX + halfDistance, y,
w - r, y
);
}
return path;
}
},
/**
* @typedef {SVGElement} ClipRect - 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);
*/
/**
* Define a clipping rectangle. The clipping rectangle is later applied
* to {@link SVGElement} objects through the {@link SVGElement#clip}
* function.
*
* @param {String} id
* @param {number} x
* @param {number} y
* @param {number} width
* @param {number} height
* @returns {ClipRect} A clipping rectangle.
*
* @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);
*/
clipRect: function (x, y, width, height) {
var wrapper,
id = H.uniqueKey(),
clipPath = this.createElement('clipPath').attr({
id: id
}).add(this.defs);
wrapper = this.rect(x, y, width, height, 0).add(clipPath);
wrapper.id = id;
wrapper.clipPath = clipPath;
wrapper.count = 0;
return wrapper;
},
/**
* Draw text. The text can contain a subset of HTML, like spans and anchors
* and some basic text styling of these. For more advanced features like
* border and background, use {@link Highcharts.SVGRenderer#label} instead.
* To update the text after render, run `text.attr({ text: 'New text' })`.
* @param {String} str
* The text of (subset) HTML to draw.
* @param {number} x
* The x position of the text's lower left corner.
* @param {number} y
* The y position of the text's lower left corner.
* @param {Boolean} [useHTML=false]
* Use HTML to render the text.
*
* @return {SVGElement} The text object.
*
* @sample highcharts/members/renderer-text-on-chart/
* Annotate the chart freely
* @sample highcharts/members/renderer-on-chart/
* Annotate with a border and in response to the data
* @sample highcharts/members/renderer-text/
* Formatted text
*/
text: function (str, x, y, useHTML) {
// declare variables
var renderer = this,
wrapper,
attribs = {};
if (useHTML && (renderer.allowHTML || !renderer.forExport)) {
return renderer.html(str, x, y);
}
attribs.x = Math.round(x || 0); // X always needed for line-wrap logic
if (y) {
attribs.y = Math.round(y);
}
if (str || str === 0) {
attribs.text = str;
}
wrapper = renderer.createElement('text')
.attr(attribs);
if (!useHTML) {
wrapper.xSetter = function (value, key, element) {
var tspans = element.getElementsByTagName('tspan'),
tspan,
parentVal = element.getAttribute(key),
i;
for (i = 0; i < tspans.length; i++) {
tspan = tspans[i];
// If the x values are equal, the tspan represents a
// linebreak
if (tspan.getAttribute(key) === parentVal) {
tspan.setAttribute(key, value);
}
}
element.setAttribute(key, value);
};
}
return wrapper;
},
/**
* Utility to return the baseline offset and total line height from the font
* size.
*
* @param {?string} fontSize The current font size to inspect. If not given,
* the font size will be found from the DOM element.
* @param {SVGElement|SVGDOMElement} [elem] The element to inspect for a
* current font size.
* @returns {Object} An object containing `h`: the line height, `b`: the
* baseline relative to the top of the box, and `f`: the font size.
*/
fontMetrics: function (fontSize, elem) {
var lineHeight,
baseline;
fontSize = fontSize ||
// When the elem is a DOM element (#5932)
(elem && elem.style && elem.style.fontSize) ||
// Fall back on the renderer style default
(this.style && this.style.fontSize);
// Handle different units
if (/px/.test(fontSize)) {
fontSize = pInt(fontSize);
} else if (/em/.test(fontSize)) {
// The em unit depends on parent items
fontSize = parseFloat(fontSize) *
(elem ? this.fontMetrics(null, elem.parentNode).f : 16);
} else {
fontSize = 12;
}
// Empirical values found by comparing font size and bounding box
// height. Applies to the default font family.
// http://jsfiddle.net/highcharts/7xvn7/
lineHeight = fontSize < 24 ? fontSize + 3 : Math.round(fontSize * 1.2);
baseline = Math.round(lineHeight * 0.8);
return {
h: lineHeight,
b: baseline,
f: fontSize
};
},
/**
* Correct X and Y positioning of a label for rotation (#1764).
*
* @private
*/
rotCorr: function (baseline, rotation, alterY) {
var y = baseline;
if (rotation && alterY) {
y = Math.max(y * Math.cos(rotation * deg2rad), 4);
}
return {
x: (-baseline / 3) * Math.sin(rotation * deg2rad),
y: y
};
},
/**
* Draw a label, which is an extended text element with support for border
* and background. Highcharts creates a `g` element with a text and a `path`
* or `rect` inside, to make it behave somewhat like a HTML div. Border and
* background are set through `stroke`, `stroke-width` and `fill` attributes
* using the {@link Highcharts.SVGElement#attr|attr} method. To update the
* text after render, run `label.attr({ text: 'New text' })`.
*
* @param {string} str
* The initial text string or (subset) HTML to render.
* @param {number} x
* The x position of the label's left side.
* @param {number} y
* The y position of the label's top side or baseline, depending on
* the `baseline` parameter.
* @param {String} shape
* The shape of the label's border/background, if any. Defaults to
* `rect`. Other possible values are `callout` or other shapes
* defined in {@link Highcharts.SVGRenderer#symbols}.
* @param {number} anchorX
* In case the `shape` has a pointer, like a flag, this is the
* coordinates it should be pinned to.
* @param {number} anchorY
* In case the `shape` has a pointer, like a flag, this is the
* coordinates it should be pinned to.
* @param {Boolean} baseline
* Whether to position the label relative to the text baseline,
* like {@link Highcharts.SVGRenderer#text|renderer.text}, or to the
* upper border of the rectangle.
* @param {String} className
* Class name for the group.
*
* @return {SVGElement}
* The generated label.
*
* @sample highcharts/members/renderer-label-on-chart/
* A label on the chart
*/
label: function (
str,
x,
y,
shape,
anchorX,
anchorY,
useHTML,
baseline,
className
) {
var renderer = this,
wrapper = renderer.g(className !== 'button' && 'label'),
text = wrapper.text = renderer.text('', 0, 0, useHTML)
.attr({
zIndex: 1
}),
box,
bBox,
alignFactor = 0,
padding = 3,
paddingLeft = 0,
width,
height,
wrapperX,
wrapperY,
textAlign,
deferredAttr = {},
strokeWidth,
baselineOffset,
hasBGImage = /^url\((.*?)\)$/.test(shape),
needsBox = hasBGImage,
getCrispAdjust,
updateBoxSize,
updateTextPadding,
boxAttr;
if (className) {
wrapper.addClass('highcharts-' + className);
}
needsBox = hasBGImage;
getCrispAdjust = function () {
return (strokeWidth || 0) % 2 / 2;
};
/**
* This function runs after the label is added to the DOM (when the
* bounding box is available), and after the text of the label is
* updated to detect the new bounding box and reflect it in the border
* box.
*/
updateBoxSize = function () {
var style = text.element.style,
crispAdjust,
attribs = {};
bBox = (
(width === undefined || height === undefined || textAlign) &&
defined(text.textStr) &&
text.getBBox()
); // #3295 && 3514 box failure when string equals 0
wrapper.width = (
(width || bBox.width || 0) +
2 * padding +
paddingLeft
);
wrapper.height = (height || bBox.height || 0) + 2 * padding;
// Update the label-scoped y offset
baselineOffset = padding +
renderer.fontMetrics(style && style.fontSize, text).b;
if (needsBox) {
// Create the border box if it is not already present
if (!box) {
// Symbol definition exists (#5324)
wrapper.box = box = renderer.symbols[shape] || hasBGImage ?
renderer.symbol(shape) :
renderer.rect();
box.addClass( // Don't use label className for buttons
(className === 'button' ? '' : 'highcharts-label-box') +
(className ? ' highcharts-' + className + '-box' : '')
);
box.add(wrapper);
crispAdjust = getCrispAdjust();
attribs.x = crispAdjust;
attribs.y = (baseline ? -baselineOffset : 0) + crispAdjust;
}
// Apply the box attributes
attribs.width = Math.round(wrapper.width);
attribs.height = Math.round(wrapper.height);
box.attr(extend(attribs, deferredAttr));
deferredAttr = {};
}
};
/**
* This function runs after setting text or padding, but only if padding
* is changed
*/
updateTextPadding = function () {
var textX = paddingLeft + padding,
textY;
// determin y based on the baseline
textY = baseline ? 0 : baselineOffset;
// compensate for alignment
if (
defined(width) &&
bBox &&
(textAlign === 'center' || textAlign === 'right')
) {
textX += { center: 0.5, right: 1 }[textAlign] *
(width - bBox.width);
}
// update if anything changed
if (textX !== text.x || textY !== text.y) {
text.attr('x', textX);
if (textY !== undefined) {
text.attr('y', textY);
}
}
// record current values
text.x = textX;
text.y = textY;
};
/**
* Set a box attribute, or defer it if the box is not yet created
* @param {Object} key
* @param {Object} value
*/
boxAttr = function (key, value) {
if (box) {
box.attr(key, value);
} else {
deferredAttr[key] = value;
}
};
/**
* After the text element is added, get the desired size of the border
* box and add it before the text in the DOM.
*/
wrapper.onAdd = function () {
text.add(wrapper);
wrapper.attr({
// Alignment is available now (#3295, 0 not rendered if given
// as a value)
text: (str || str === 0) ? str : '',
x: x,
y: y
});
if (box && defined(anchorX)) {
wrapper.attr({
anchorX: anchorX,
anchorY: anchorY
});
}
};
/*
* Add specific attribute setters.
*/
// only change local variables
wrapper.widthSetter = function (value) {
width = H.isNumber(value) ? value : null; // width:auto => null
};
wrapper.heightSetter = function (value) {
height = value;
};
wrapper['text-alignSetter'] = function (value) {
textAlign = value;
};
wrapper.paddingSetter = function (value) {
if (defined(value) && value !== padding) {
padding = wrapper.padding = value;
updateTextPadding();
}
};
wrapper.paddingLeftSetter = function (value) {
if (defined(value) && value !== paddingLeft) {
paddingLeft = value;
updateTextPadding();
}
};
// change local variable and prevent setting attribute on the group
wrapper.alignSetter = function (value) {
value = { left: 0, center: 0.5, right: 1 }[value];
if (value !== alignFactor) {
alignFactor = value;
// Bounding box exists, means we're dynamically changing
if (bBox) {
wrapper.attr({ x: wrapperX }); // #5134
}
}
};
// apply these to the box and the text alike
wrapper.textSetter = function (value) {
if (value !== undefined) {
text.textSetter(value);
}
updateBoxSize();
updateTextPadding();
};
// apply these to the box but not to the text
wrapper['stroke-widthSetter'] = function (value, key) {
if (value) {
needsBox = true;
}
strokeWidth = this['stroke-width'] = value;
boxAttr(key, value);
};
wrapper.strokeSetter =
wrapper.fillSetter =
wrapper.rSetter = function (value, key) {
if (key !== 'r') {
if (key === 'fill' && value) {
needsBox = true;
}
// for animation getter (#6776)
wrapper[key] = value;
}
boxAttr(key, value);
};
wrapper.anchorXSetter = function (value, key) {
anchorX = wrapper.anchorX = value;
boxAttr(key, Math.round(value) - getCrispAdjust() - wrapperX);
};
wrapper.anchorYSetter = function (value, key) {
anchorY = wrapper.anchorY = value;
boxAttr(key, value - wrapperY);
};
// rename attributes
wrapper.xSetter = function (value) {
wrapper.x = value; // for animation getter
if (alignFactor) {
value -= alignFactor * ((width || bBox.width) + 2 * padding);
// Force animation even when setting to the same value (#7898)
wrapper['forceAnimate:x'] = true;
}
wrapperX = Math.round(value);
wrapper.attr('translateX', wrapperX);
};
wrapper.ySetter = function (value) {
wrapperY = wrapper.y = Math.round(value);
wrapper.attr('translateY', wrapperY);
};
// Redirect certain methods to either the box or the text
var baseCss = wrapper.css;
return extend(wrapper, {
/**
* Pick up some properties and apply them to the text instead of the
* wrapper.
* @ignore
*/
css: function (styles) {
if (styles) {
var textStyles = {};
// Create a copy to avoid altering the original object
// (#537)
styles = merge(styles);
each(wrapper.textProps, function (prop) {
if (styles[prop] !== undefined) {
textStyles[prop] = styles[prop];
delete styles[prop];
}
});
text.css(textStyles);
if ('width' in textStyles) {
updateBoxSize();
}
}
return baseCss.call(wrapper, styles);
},
/**
* Return the bounding box of the box, not the group.
* @ignore
*/
getBBox: function () {
return {
width: bBox.width + 2 * padding,
height: bBox.height + 2 * padding,
x: bBox.x - padding,
y: bBox.y - padding
};
},
/**
* Apply the shadow to the box.
* @ignore
*/
shadow: function (b) {
if (b) {
updateBoxSize();
if (box) {
box.shadow(b);
}
}
return wrapper;
},
/**
* Destroy and release memory.
* @ignore
*/
destroy: function () {
// Added by button implementation
removeEvent(wrapper.element, 'mouseenter');
removeEvent(wrapper.element, 'mouseleave');
if (text) {
text = text.destroy();
}
if (box) {
box = box.destroy();
}
// Call base implementation to destroy the rest
SVGElement.prototype.destroy.call(wrapper);
// Release local pointers (#1298)
wrapper =
renderer =
updateBoxSize =
updateTextPadding =
boxAttr = null;
}
});
}
}); // end SVGRenderer
// general renderer
H.Renderer = SVGRenderer;