highcharts
Version:
JavaScript charting framework
1,302 lines • 88.9 kB
JavaScript
/* *
*
* (c) 2010-2020 Torstein Honsi
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import Color from '../../Color.js';
import H from '../../Globals.js';
import SVGElement from './SVGElement.js';
import SVGLabel from './SVGLabel.js';
import U from '../../Utilities.js';
var addEvent = U.addEvent, attr = U.attr, createElement = U.createElement, css = U.css, defined = U.defined, destroyObjectProperties = U.destroyObjectProperties, extend = U.extend, isArray = U.isArray, isNumber = U.isNumber, isObject = U.isObject, isString = U.isString, merge = U.merge, objectEach = U.objectEach, pick = U.pick, pInt = U.pInt, splat = U.splat, uniqueKey = U.uniqueKey;
/**
* A clipping rectangle that can be applied to one or more {@link SVGElement}
* instances. It is instanciated with the {@link SVGRenderer#clipRect} function
* and applied with the {@link SVGElement#clip} function.
*
* @example
* var circle = renderer.circle(100, 100, 100)
* .attr({ fill: 'red' })
* .add();
* var clipRect = renderer.clipRect(100, 100, 100, 100);
*
* // Leave only the lower right quarter visible
* circle.clip(clipRect);
*
* @typedef {Highcharts.SVGElement} Highcharts.ClipRectElement
*/
/**
* The font metrics.
*
* @interface Highcharts.FontMetricsObject
*/ /**
* The baseline relative to the top of the box.
*
* @name Highcharts.FontMetricsObject#b
* @type {number}
*/ /**
* The font size.
*
* @name Highcharts.FontMetricsObject#f
* @type {number}
*/ /**
* The line height.
*
* @name Highcharts.FontMetricsObject#h
* @type {number}
*/
/**
* An object containing `x` and `y` properties for the position of an element.
*
* @interface Highcharts.PositionObject
*/ /**
* X position of the element.
* @name Highcharts.PositionObject#x
* @type {number}
*/ /**
* Y position of the element.
* @name Highcharts.PositionObject#y
* @type {number}
*/
/**
* A rectangle.
*
* @interface Highcharts.RectangleObject
*/ /**
* Height of the rectangle.
* @name Highcharts.RectangleObject#height
* @type {number}
*/ /**
* Width of the rectangle.
* @name Highcharts.RectangleObject#width
* @type {number}
*/ /**
* Horizontal position of the rectangle.
* @name Highcharts.RectangleObject#x
* @type {number}
*/ /**
* Vertical position of the rectangle.
* @name Highcharts.RectangleObject#y
* @type {number}
*/
/**
* The shadow options.
*
* @interface Highcharts.ShadowOptionsObject
*/ /**
* The shadow color.
* @name Highcharts.ShadowOptionsObject#color
* @type {Highcharts.ColorString|undefined}
* @default #000000
*/ /**
* The horizontal offset from the element.
*
* @name Highcharts.ShadowOptionsObject#offsetX
* @type {number|undefined}
* @default 1
*/ /**
* The vertical offset from the element.
* @name Highcharts.ShadowOptionsObject#offsetY
* @type {number|undefined}
* @default 1
*/ /**
* The shadow opacity.
*
* @name Highcharts.ShadowOptionsObject#opacity
* @type {number|undefined}
* @default 0.15
*/ /**
* The shadow width or distance from the element.
* @name Highcharts.ShadowOptionsObject#width
* @type {number|undefined}
* @default 3
*/
/**
* @interface Highcharts.SizeObject
*/ /**
* @name Highcharts.SizeObject#height
* @type {number}
*/ /**
* @name Highcharts.SizeObject#width
* @type {number}
*/
/**
* Serialized form of an SVG definition, including children. Some key
* property names are reserved: tagName, textContent, and children.
*
* @interface Highcharts.SVGDefinitionObject
*/ /**
* @name Highcharts.SVGDefinitionObject#[key:string]
* @type {boolean|number|string|Array<Highcharts.SVGDefinitionObject>|undefined}
*/ /**
* @name Highcharts.SVGDefinitionObject#children
* @type {Array<Highcharts.SVGDefinitionObject>|undefined}
*/ /**
* @name Highcharts.SVGDefinitionObject#tagName
* @type {string|undefined}
*/ /**
* @name Highcharts.SVGDefinitionObject#textContent
* @type {string|undefined}
*/
/**
* Array of path commands, that will go into the `d` attribute of an SVG
* element.
*
* @typedef {Array<(Array<Highcharts.SVGPathCommand>|Array<Highcharts.SVGPathCommand,number>|Array<Highcharts.SVGPathCommand,number,number>|Array<Highcharts.SVGPathCommand,number,number,number,number>|Array<Highcharts.SVGPathCommand,number,number,number,number,number,number>|Array<Highcharts.SVGPathCommand,number,number,number,number,number,number,number>)>} Highcharts.SVGPathArray
*/
/**
* Possible path commands in an SVG path array. Valid values are `A`, `C`, `H`,
* `L`, `M`, `Q`, `S`, `T`, `V`, `Z`.
*
* @typedef {string} Highcharts.SVGPathCommand
* @validvalue ["a","c","h","l","m","q","s","t","v","z","A","C","H","L","M","Q","S","T","V","Z"]
*/
/**
* An extendable collection of functions for defining symbol paths. Symbols are
* used internally for point markers, button and label borders and backgrounds,
* or custom shapes. Extendable by adding to {@link SVGRenderer#symbols}.
*
* @interface Highcharts.SymbolDictionary
*/ /**
* @name Highcharts.SymbolDictionary#[key:string]
* @type {Function|undefined}
*/ /**
* @name Highcharts.SymbolDictionary#arc
* @type {Function|undefined}
*/ /**
* @name Highcharts.SymbolDictionary#callout
* @type {Function|undefined}
*/ /**
* @name Highcharts.SymbolDictionary#circle
* @type {Function|undefined}
*/ /**
* @name Highcharts.SymbolDictionary#diamond
* @type {Function|undefined}
*/ /**
* @name Highcharts.SymbolDictionary#square
* @type {Function|undefined}
*/ /**
* @name Highcharts.SymbolDictionary#triangle
* @type {Function|undefined}
*/
/**
* Can be one of `arc`, `callout`, `circle`, `diamond`, `square`, `triangle`,
* and `triangle-down`. Symbols are used internally for point markers, button
* and label borders and backgrounds, or custom shapes. Extendable by adding to
* {@link SVGRenderer#symbols}.
*
* @typedef {"arc"|"callout"|"circle"|"diamond"|"square"|"triangle"|"triangle-down"} Highcharts.SymbolKeyValue
*/
/**
* Additional options, depending on the actual symbol drawn.
*
* @interface Highcharts.SymbolOptionsObject
*/ /**
* The anchor X position for the `callout` symbol. This is where the chevron
* points to.
*
* @name Highcharts.SymbolOptionsObject#anchorX
* @type {number|undefined}
*/ /**
* The anchor Y position for the `callout` symbol. This is where the chevron
* points to.
*
* @name Highcharts.SymbolOptionsObject#anchorY
* @type {number|undefined}
*/ /**
* The end angle of an `arc` symbol.
*
* @name Highcharts.SymbolOptionsObject#end
* @type {number|undefined}
*/ /**
* Whether to draw `arc` symbol open or closed.
*
* @name Highcharts.SymbolOptionsObject#open
* @type {boolean|undefined}
*/ /**
* The radius of an `arc` symbol, or the border radius for the `callout` symbol.
*
* @name Highcharts.SymbolOptionsObject#r
* @type {number|undefined}
*/ /**
* The start angle of an `arc` symbol.
*
* @name Highcharts.SymbolOptionsObject#start
* @type {number|undefined}
*/
/* eslint-disable no-invalid-this, valid-jsdoc */
var charts = H.charts, deg2rad = H.deg2rad, doc = H.doc, isFirefox = H.isFirefox, isMS = H.isMS, isWebKit = H.isWebKit, noop = H.noop, svg = H.svg, SVG_NS = H.SVG_NS, symbolSizes = H.symbolSizes, win = H.win;
/**
* 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.
*
* @sample highcharts/members/renderer-on-chart
* Annotating a chart programmatically.
* @sample highcharts/members/renderer-basic
* Independent SVG drawing.
*
* @example
* // Use directly without a chart object.
* var renderer = new Highcharts.Renderer(parentNode, 600, 400);
*
* @class
* @name Highcharts.SVGRenderer
*
* @param {Highcharts.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 {Highcharts.CSSObject} [style]
* The box style, if not in styleMode
*
* @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.
*
* @param {boolean} [styledMode=false]
* Whether the renderer belongs to a chart that is in styled mode.
* If it does, it will avoid setting presentational attributes in
* some cases, but not when set explicitly through `.attr` and `.css`
* etc.
*/
var SVGRenderer = /** @class */ (function () {
/* *
*
* Constructors
*
* */
function SVGRenderer(container, width, height, style, forExport, allowHTML, styledMode) {
/* *
*
* Properties
*
* */
this.alignedObjects = void 0;
/**
* The root `svg` node of the renderer.
*
* @name Highcharts.SVGRenderer#box
* @type {Highcharts.SVGDOMElement}
*/
this.box = void 0;
/**
* The wrapper for the root `svg` node of the renderer.
*
* @name Highcharts.SVGRenderer#boxWrapper
* @type {Highcharts.SVGElement}
*/
this.boxWrapper = void 0;
this.cache = void 0;
this.cacheKeys = void 0;
this.chartIndex = void 0;
/**
* A pointer to the `defs` node of the root SVG.
*
* @name Highcharts.SVGRenderer#defs
* @type {Highcharts.SVGElement}
*/
this.defs = void 0;
this.globalAnimation = void 0;
this.gradients = void 0;
this.height = void 0;
this.imgCount = void 0;
this.isSVG = void 0;
this.style = void 0;
/**
* Page url used for internal references.
*
* @private
* @name Highcharts.SVGRenderer#url
* @type {string}
*/
this.url = void 0;
this.width = void 0;
this.init(container, width, height, style, forExport, allowHTML, styledMode);
}
/* *
*
* Functions
*
* */
/**
* Initialize the SVGRenderer. Overridable initializer function that takes
* the same parameters as the constructor.
*
* @function Highcharts.SVGRenderer#init
*
* @param {Highcharts.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 {Highcharts.CSSObject} [style]
* The box style, if not in styleMode
*
* @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.
*
* @param {boolean} [styledMode=false]
* Whether the renderer belongs to a chart that is in styled mode. If it
* does, it will avoid setting presentational attributes in some cases, but
* not when set explicitly through `.attr` and `.css` etc.
*/
SVGRenderer.prototype.init = function (container, width, height, style, forExport, allowHTML, styledMode) {
var renderer = this, boxWrapper, element, desc;
boxWrapper = renderer.createElement('svg')
.attr({
version: '1.1',
'class': 'highcharts-root'
});
if (!styledMode) {
boxWrapper.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;
this.box = element;
this.boxWrapper = boxWrapper;
renderer.alignedObjects = [];
// #24, #672, #1070
this.url = ((isFirefox || isWebKit) &&
doc.getElementsByTagName('base').length) ?
win.location.href
.split('#')[0] // 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 Highcharts 8.2.0'));
renderer.defs = this.createElement('defs').add();
renderer.allowHTML = allowHTML;
renderer.forExport = forExport;
renderer.styledMode = styledMode;
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);
}
};
/**
* General method for adding a definition to the SVG `defs` tag. Can be used
* for gradients, fills, filters etc. Styled mode only. A hook for adding
* general definitions to the SVG's defs tag. Definitions can be referenced
* from the CSS by its `id`. Read more in
* [gradients, shadows and patterns](https://www.highcharts.com/docs/chart-design-and-style/gradients-shadows-and-patterns).
* Styled mode only.
*
* @function Highcharts.SVGRenderer#definition
*
* @param {Highcharts.SVGDefinitionObject} def
* A serialized form of an SVG definition, including children.
*
* @return {Highcharts.SVGElement}
* The inserted node.
*/
SVGRenderer.prototype.definition = function (def) {
var ren = this;
/**
* @private
* @param {Highcharts.SVGDefinitionObject} config - SVG definition
* @param {Highcharts.SVGElement} [parent] - parent node
*/
function recurse(config, parent) {
var ret;
splat(config).forEach(function (item) {
var node = ren.createElement(item.tagName), attr = {};
// Set attributes
objectEach(item, function (val, key) {
if (key !== 'tagName' &&
key !== 'children' &&
key !== 'textContent') {
attr[key] = val;
}
});
node.attr(attr);
// Add to the tree
node.add(parent || ren.defs);
// Add text content
if (item.textContent) {
node.element.appendChild(doc.createTextNode(item.textContent));
}
// Recurse
recurse(item.children || [], node);
ret = node;
});
// Return last node added (on top level it's the only one)
return ret;
}
return recurse(def);
};
/**
* Get the global style setting for the renderer.
*
* @private
* @function Highcharts.SVGRenderer#getStyle
*
* @param {Highcharts.CSSObject} style
* Style settings.
*
* @return {Highcharts.CSSObject}
* The style settings mixed with defaults.
*/
SVGRenderer.prototype.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.
*
* @function Highcharts.SVGRenderer#setStyle
*
* @param {Highcharts.CSSObject} style
* CSS to apply.
*/
SVGRenderer.prototype.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.
*
* @function Highcharts.SVGRenderer#isHidden
*
* @return {boolean}
* True if it is hidden.
*/
SVGRenderer.prototype.isHidden = function () {
return !this.boxWrapper.getBBox().width;
};
/**
* Destroys the renderer and its allocated members.
*
* @function Highcharts.SVGRenderer#destroy
*
* @return {null}
*/
SVGRenderer.prototype.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}.
*
* @function Highcharts.SVGRenderer#createElement
*
* @param {string} nodeName
* The node name, for example `rect`, `g` etc.
*
* @return {Highcharts.SVGElement}
* The generated SVGElement.
*/
SVGRenderer.prototype.createElement = function (nodeName) {
var wrapper = new this.Element();
wrapper.init(this, nodeName);
return wrapper;
};
/**
* Get converted radial gradient attributes according to the radial
* reference. Used internally from the {@link SVGElement#colorGradient}
* function.
*
* @private
* @function Highcharts.SVGRenderer#getRadialAttr
*/
SVGRenderer.prototype.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]
};
};
/**
* Truncate the text node contents to a given length. Used when the css
* width is set. If the `textOverflow` is `ellipsis`, the text is truncated
* character by character to the given length. If not, the text is
* word-wrapped line by line.
*
* @private
* @function Highcharts.SVGRenderer#truncate
*
* @return {boolean}
* True if tspan is too long.
*/
SVGRenderer.prototype.truncate = function (wrapper, tspan, text, words, startAt, width, getString) {
var renderer = this, rotation = wrapper.rotation, str,
// Word wrap can not be truncated to shorter than one word, ellipsis
// text can be completely blank.
minIndex = words ? 1 : 0, maxIndex = (text || words).length, currentIndex = maxIndex,
// Cache the lengths to avoid checking the same twice
lengths = [], updateTSpan = function (s) {
if (tspan.firstChild) {
tspan.removeChild(tspan.firstChild);
}
if (s) {
tspan.appendChild(doc.createTextNode(s));
}
}, getSubStringLength = function (charEnd, concatenatedEnd) {
// charEnd is useed when finding the character-by-character
// break for ellipsis, concatenatedEnd is used for word-by-word
// break for word wrapping.
var end = concatenatedEnd || charEnd;
if (typeof lengths[end] === 'undefined') {
// Modern browsers
if (tspan.getSubStringLength) {
// Fails with DOM exception on unit-tests/legend/members
// of unknown reason. Desired width is 0, text content
// is "5" and end is 1.
try {
lengths[end] = startAt +
tspan.getSubStringLength(0, words ? end + 1 : end);
}
catch (e) {
'';
}
// Legacy
}
else if (renderer.getSpanWidth) { // #9058 jsdom
updateTSpan(getString(text || words, charEnd));
lengths[end] = startAt +
renderer.getSpanWidth(wrapper, tspan);
}
}
return lengths[end];
}, actualWidth, truncated;
wrapper.rotation = 0; // discard rotation when computing box
actualWidth = getSubStringLength(tspan.textContent.length);
truncated = startAt + actualWidth > width;
if (truncated) {
// Do a binary search for the index where to truncate the text
while (minIndex <= maxIndex) {
currentIndex = Math.ceil((minIndex + maxIndex) / 2);
// When checking words for word-wrap, we need to build the
// string and measure the subStringLength at the concatenated
// word length.
if (words) {
str = getString(words, currentIndex);
}
actualWidth = getSubStringLength(currentIndex, str && str.length - 1);
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 the shortest possible text was also
// too large. For ellipsis that means only the ellipsis, while for
// word wrap it means the whole first word.
if (maxIndex === 0) {
// Remove ellipsis
updateTSpan('');
// If the new text length is one less than the original, we don't
// need the ellipsis
}
else if (!(text && maxIndex === text.length - 1)) {
updateTSpan(str || getString(text || words, currentIndex));
}
}
// When doing line wrapping, prepare for the next line by removing the
// items from this line.
if (words) {
words.splice(0, currentIndex);
}
wrapper.actualWidth = actualWidth;
wrapper.rotation = rotation; // Apply rotation again.
return truncated;
};
/**
* 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
* @function Highcharts.SVGRenderer#buildText
*
* @param {Highcharts.SVGElement} wrapper
* The parent SVGElement.
*/
SVGRenderer.prototype.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, truncated, 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;
if (!renderer.styledMode) {
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 || except.indexOf(value) === -1) {
inputStr = inputStr.toString().replace(new RegExp(value, 'g'), 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));
}
}
};
var regexMatchBreaks = /<br.*?>/g;
// 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 ||
(noWrap && !regexMatchBreaks.test(textStr)))) {
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 = renderer.styledMode ? (textStr
.replace(/<(b|strong)>/g, '<span class="highcharts-strong">')
.replace(/<(i|em)>/g, '<span class="highcharts-emphasized">')) : (textStr
.replace(/<(b|strong)>/g, '<span style="font-weight:bold">')
.replace(/<(i|em)>/g, '<span style="font-style:italic">'));
lines = lines
.replace(/<a/g, '<span')
.replace(/<\/(b|strong|i|em|a)>/g, '</span>')
.split(regexMatchBreaks);
}
else {
lines = [textStr];
}
// Trim empty lines (#5261)
lines = lines.filter(function (line) {
return line !== '';
});
// build the lines
lines.forEach(function (line, lineNo) {
var spans, spanNo = 0, lineLength = 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('|||');
spans.forEach(function buildTextSpans(span) {
if (span !== '' || spans.length === 1) {
var attributes = {}, tspan = doc.createElementNS(renderer.SVG_NS, 'tspan'), a, 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);
}
// For anchors, wrap the tspan in an <a> tag and apply
// the href attribute as is (#13559). Not for export
// (#1529)
hrefAttribute = parseAttribute(span, 'href');
if (hrefAttribute && !forExport) {
if (
// Stop JavaScript links, vulnerable to XSS
hrefAttribute.split(':')[0].toLowerCase()
.indexOf('javascript') === -1) {
a = doc.createElementNS(renderer.SVG_NS, 'a');
attr(a, 'href', hrefAttribute);
attr(tspan, 'class', 'highcharts-anchor');
a.appendChild(tspan);
if (!renderer.styledMode) {
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(a || 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));
}
// Check width and apply soft breaks or ellipsis
if (width) {
var words = span.replace(/([^\^])-/g, '$1- ').split(' '), // #1273
hasWhiteSpace = !noWrap && (spans.length > 1 ||
lineNo ||
words.length > 1), wrapLineNo = 0, dy = getLineHeight(tspan);
if (ellipsis) {
truncated = renderer.truncate(wrapper, tspan, span, void 0, 0,
// Target width
Math.max(0,
// Substract the font face to make
// room for the ellipsis itself
width - parseInt(fontSize || 12, 10)),
// Build the text to test for
function (text, currentIndex) {
return text.substring(0, currentIndex) + '\u2026';
});
}
else if (hasWhiteSpace) {
while (words.length) {
// For subsequent lines, create tspans
// with the same style attributes as the
// parent text node.
if (words.length &&
!noWrap &&
wrapLineNo > 0) {
tspan = doc.createElementNS(SVG_NS, 'tspan');
attr(tspan, {
dy: dy,
x: parentX
});
if (styleAttribute) { // #390
attr(tspan, 'style', styleAttribute);
}
// Start by appending the full
// remaining text
tspan.appendChild(doc.createTextNode(words.join(' ')
.replace(/- /g, '-')));
textNode.appendChild(tspan);
}
// For each line, truncate the remaining
// words into the line length.
renderer.truncate(wrapper, tspan, null, words, wrapLineNo === 0 ? lineLength : 0, width,
// Build the text to test for
function (text, currentIndex) {
return words
.slice(0, currentIndex)
.join(' ')
.replace(/- /g, '-');
});
lineLength = wrapper.actualWidth;
wrapLineNo++;
}
}
}
spanNo++;
}
}
});
// To avoid beginning lines that doesn't add to the textNode
// (#6144)
isSubsequentLine = (isSubsequentLine ||
textNode.childNodes.length);
});
if (ellipsis && truncated) {
wrapper.attr('title', unescapeEntities(wrapper.textStr || '', ['<', '>']) // #7179
);
}
if (tempParent) {
tempParent.removeChild(textNode);
}
// Apply the text outline
if (isString(textOutline) && wrapper.applyTextOutline) {
wrapper.applyTextOutline(textOutline);
}
}
};
/**
* Returns white for dark colors and black for bright colors.
*
* @function Highcharts.SVGRenderer#getContrast
*
* @param {Highcharts.ColorString} rgba
* The color to get the contrast for.
*
* @return {Highcharts.ColorString}
* The contrast color, either `#000000` or `#FFFFFF`.
*/
SVGRenderer.prototype.getContrast = function (rgba) {
rgba = Color.parse(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.5; // blue
return rgba[0] + rgba[1] + rgba[2] >
1.8 * 255 ?
'#000000' :
'#FFFFFF';
};
/**
* Create a button with preset states.
*
* @function Highcharts.SVGRenderer#button
*
* @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 {Highcharts.EventCallbackFunction<Highcharts.SVGElement>} callback
* The function to execute on button click or touch.
*
* @param {Highcharts.SVGAttributes} [normalState]
* SVG attributes for the normal state.
*
* @param {Highcharts.SVGAttributes} [hoverState]
* SVG attributes for the hover state.
*
* @param {Highcharts.SVGAttributes} [pressedState]
* SVG attributes for the pressed state.
*
* @param {Highcharts.SVGAttributes} [disabledState]
* SVG attributes for the disabled state.
*
* @param {Highcharts.SymbolKeyValue} [shape=rect]
* The shape type.
*
* @param {boolean} [useHTML=false]
* Wether to use HTML to render the label.
*
* @return {Highcharts.SVGElement}
* The button element.
*/
SVGRenderer.prototype.button = function (text, x, y, callback, normalState, hoverState, pressedState, disabledState, shape, useHTML) {
var label = this.label(text, x, y, shape, void 0, void 0, useHTML, void 0, 'button'), curState = 0, styledMode = this.styledMode,
// Make a copy of normalState (#13798)
// (reference to options.rangeSelector.buttonTheme)
normalState = normalState ? merge(normalState) : normalState, userNormalStyle = normalState && normalState.style || {};
// Remove stylable attributes
if (normalState && normalState.style) {
delete normalState.style;
}
// Default, non-stylable attributes
label.attr(merge({ padding: 8, r: 2 }, normalState));
if (!styledMode) {
// 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'
}
}, {
style: userNormalStyle
}, 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]);
if (!styledMode) {
label
.attr([
normalState,
hoverState,
pressedState,
disabledState
][state || 0])
.css([
normalStyle,
hoverStyle,
pressedStyle,
disabledStyle
][state || 0]);
}
};
// Presentational attributes
if (!styledMode) {
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.
*
* @function Highcharts.SVGRenderer#crispLine
*
* @param {Highcharts.SVGPathArray} points
* The original points on the format `[['M', 0, 0], ['L', 100, 0]]`.
*
* @param {number} width
* The width of the line.
*
* @param {string} roundingFunction
* The rounding function name on the `Math` object, can be one of
* `round`, `floor` or `ceil`.
*
* @return {Highcharts.SVGPathArray}
* The original points array, but modified to render crisply.
*/
SVGRenderer.prototype.crispLine = function (points, width, roundingFunction) {
if (roundingFunction === void 0) { roundingFunction = 'round'; }
var start = points[0];
var end = points[1];
// Normalize to a crisp line
if (start[1] === end[1]) {
// Substract due to #1129. Now bottom and left axis gridlines behave
// the same.
start[1] = end[1] =
Math[roundingFunction](start[1]) - (width % 2 / 2);
}
if (start[2] === end[2]) {
start[2] = end[2] =
Math[roundingFunction](start[2]) + (width % 2 / 2);
}
return points;
};
/**
* Draw a path, wraps the SVG `path` 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
*
* @example
* var path = renderer.path(['M', 10, 10, 'L', 30, 30, 'z'])
* .attr({ stroke: '#ff00ff' })
* .add();
*
* @function Highcharts.SVGRenderer#path
*
* @param {Highcharts.SVGPathArray} [path]
* An SVG path definition in array form.
*
* @return {Highcharts.SVGElement}
* The generated wrapper element.
*
*/ /**
* Draw a path, wraps the SVG `path` element.
*
* @function Highcharts.SVGRenderer#path
*
* @param {Highcharts.SVGAttributes} [attribs]
* The initial attributes.
*
* @return {Highcharts.SVGElement}
* The generated wrapper element.
*/
SVGRenderer.prototype.path = function (path) {
var attribs = (this.styledMode ? {} : {
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.
*
* @sample highcharts/members/renderer-circle/
* Drawing a circle
*
* @function Highcharts.SVGRenderer#circle
*
* @param {number} [x]
* The center x position.
*
* @param {number} [y]
* The center y position.
*
* @param {number} [r]
* The radius.
*
* @return {Highcharts.SVGElement}
* The generated wrapper element.
*/ /**
* Draw a circle, wraps the SVG `circle` element.
*
* @function Highcharts.SVGRenderer#circle
*
* @param {Highcharts.SVGAttributes} [attribs]
* The initial attributes.
*
* @return {Highcharts.SVGElement}
* The generated wrapper element.
*/
SVGRenderer.prototype.circle = function (x, y, r) {
var attribs = (isObject(x) ?
x :
typeof x === 'undefined' ? {} : { 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.
*
* @sample highcharts/members/renderer-arc/
* Drawing an arc
*
* @function Highcharts.SVGRenderer#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.
*
* @return {Highcharts.SVGElement}
* The generated wrapper element.
*/ /**
* Draw and return an arc. Overloaded function that takes arguments object.
*
* @function Highcharts.SVGRenderer#arc
*
* @param {Highcharts.SVGAttributes} attribs
* Initial SVG attributes.
*
* @return {Highcharts.SVGElement}
* The generated wrapper element.
*/
SVGRenderer.prototype.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;
};
/**
* Dra