UNPKG

highcharts

Version:
1,400 lines (1,399 loc) 67.4 kB
/* * * * (c) 2010-2025 Torstein Honsi * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ 'use strict'; import AST from '../HTML/AST.js'; import D from '../../Defaults.js'; const { defaultOptions } = D; import Color from '../../Color/Color.js'; import H from '../../Globals.js'; const { charts, deg2rad, doc, isFirefox, isMS, isWebKit, noop, SVG_NS, symbolSizes, win } = H; import RendererRegistry from '../RendererRegistry.js'; import SVGElement from './SVGElement.js'; import SVGLabel from './SVGLabel.js'; import Symbols from './Symbols.js'; import TextBuilder from './TextBuilder.js'; import U from '../../Utilities.js'; const { addEvent, attr, createElement, crisp, css, defined, destroyObjectProperties, extend, isArray, isNumber, isObject, isString, merge, pick, pInt, replaceNested, uniqueKey } = U; /* * * * Variables * * */ let hasInternalReferenceBug; /* * * * Class * * */ /* eslint-disable no-invalid-this, valid-jsdoc */ /** * 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. * * An existing chart's renderer can be accessed through {@link Chart.renderer}. * The renderer can also be used completely decoupled from a chart. * * See [How to use the SVG Renderer]( * https://www.highcharts.com/docs/advanced-chart-features/renderer) for a * comprehensive tutorial. * * @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. * let 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. */ class SVGRenderer { /** * The root `svg` node of the renderer. * * @name Highcharts.SVGRenderer#box * @type {Highcharts.SVGDOMElement} */ /** * The wrapper for the root `svg` node of the renderer. * * @name Highcharts.SVGRenderer#boxWrapper * @type {Highcharts.SVGElement} */ /** * A pointer to the `defs` node of the root SVG. * * @name Highcharts.SVGRenderer#defs * @type {Highcharts.SVGElement} */ /** * Whether the rendered content is intended for export. * * @name Highcharts.SVGRenderer#forExport * @type {boolean | undefined} */ /** * Page url used for internal references. * * @private * @name Highcharts.SVGRenderer#url * @type {string} */ /** * 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. */ constructor(container, width, height, style, forExport, allowHTML, styledMode) { this.x = 0; this.y = 0; const renderer = this, boxWrapper = renderer .createElement('svg') .attr({ version: '1.1', 'class': 'highcharts-root' }), element = boxWrapper.element; if (!styledMode) { boxWrapper.css(this.getStyle(style || {})); } 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); } this.box = element; this.boxWrapper = boxWrapper; this.alignedObjects = []; this.url = this.getReferenceURL(); // Add description const desc = this.createElement('desc').add(); desc.element.appendChild(doc.createTextNode('Created with Highcharts 12.2.0')); this.defs = this.createElement('defs').add(); this.allowHTML = allowHTML; this.forExport = forExport; this.styledMode = styledMode; this.gradients = {}; // Object where gradient SvgElements are stored this.cache = {}; // Cache for numerical bounding boxes this.cacheKeys = []; this.imgCount = 0; this.rootFontSize = boxWrapper.getStyle('font-size'); 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). let 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); } } /* * * * Functions * * */ /** * 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.ASTNode} def * A serialized form of an SVG definition, including children. * * @return {Highcharts.SVGElement} * The inserted node. */ definition(def) { const ast = new AST([def]); return ast.addToDOM(this.defs.element); } /** * Get the prefix needed for internal URL references to work in certain * cases. Some older browser versions had a bug where internal url * references in SVG attributes, on the form `url(#some-id)`, would fail if * a base tag was present in the page. There were also issues with * `history.pushState` related to this prefix. * * Related issues: #24, #672, #1070, #5244. * * The affected browsers are: * - Chrome <= 53 (May 2018) * - Firefox <= 51 (January 2017) * - Safari/Mac <= 12.1 (2018 or 2019) * - Safari/iOS <= 13 * * @todo Remove this hack when time has passed. All the affected browsers * are evergreens, so it is increasingly unlikely that users are affected by * the bug. * * @return {string} * The prefix to use. An empty string for modern browsers. */ getReferenceURL() { if ((isFirefox || isWebKit) && doc.getElementsByTagName('base').length) { // Detect if a clip path is taking effect by performing a hit test // outside the clipped area. If the hit element is the rectangle // that was supposed to be clipped, the bug is present. This only // has to be performed once per page load, so we store the result // locally in the module. if (!defined(hasInternalReferenceBug)) { const id = uniqueKey(); const ast = new AST([{ tagName: 'svg', attributes: { width: 8, height: 8 }, children: [{ tagName: 'defs', children: [{ tagName: 'clipPath', attributes: { id }, children: [{ tagName: 'rect', attributes: { width: 4, height: 4 } }] }] }, { tagName: 'rect', attributes: { id: 'hitme', width: 8, height: 8, 'clip-path': `url(#${id})`, fill: 'rgba(0,0,0,0.001)' } }] }]); const svg = ast.addToDOM(doc.body); css(svg, { position: 'fixed', top: 0, left: 0, zIndex: 9e5 }); const hitElement = doc.elementFromPoint(6, 6); hasInternalReferenceBug = hitElement?.id === 'hitme'; doc.body.removeChild(svg); } if (hasInternalReferenceBug) { // Scan alert #[72]: Loop for nested patterns return replaceNested(win.location.href.split('#')[0], // Remove hash [/<[^>]*>/g, ''], // Wing cut HTML [/([\('\)])/g, '\\$1'], // Escape parantheses and quotes [/ /g, '%20'] // Replace spaces (needed for Safari only) ); } } return ''; } /** * 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. */ getStyle(style) { this.style = extend({ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", ' + 'Roboto, Helvetica, Arial, "Apple Color Emoji", ' + '"Segoe UI Emoji", "Segoe UI Symbol", sans-serif', fontSize: '1rem' }, 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. */ setStyle(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 * need to render preliminarily in another div to get the text bounding * boxes right. * * @function Highcharts.SVGRenderer#isHidden * * @return {boolean} * True if it is hidden. */ isHidden() { return !this.boxWrapper.getBBox().width; } /** * Destroys the renderer and its allocated members. * * @function Highcharts.SVGRenderer#destroy * * @return {null} * Pass through value. */ destroy() { const 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; 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. */ createElement(nodeName) { return new this.Element(this, nodeName); } /** * Get converted radial gradient attributes according to the radial * reference. Used internally from the {@link SVGElement#colorGradient} * function. * * @private * @function Highcharts.SVGRenderer#getRadialAttr */ getRadialAttr(radialReference, gradAttr) { return { cx: (radialReference[0] - radialReference[2] / 2) + (gradAttr.cx || 0) * radialReference[2], cy: (radialReference[1] - radialReference[2] / 2) + (gradAttr.cy || 0) * radialReference[2], r: (gradAttr.r || 0) * radialReference[2] }; } /** * Create a drop shadow definition and return its id * * @private * @function Highcharts.SVGRenderer#shadowDefinition * * @param {boolean|Highcharts.ShadowOptionsObject} [shadowOptions] The * shadow options. If `true`, the default options are applied */ shadowDefinition(shadowOptions) { const id = [ `highcharts-drop-shadow-${this.chartIndex}`, ...Object.keys(shadowOptions) .map((key) => `${key}-${shadowOptions[key]}`) ].join('-').toLowerCase().replace(/[^a-z\d\-]/g, ''), options = merge({ color: '#000000', offsetX: 1, offsetY: 1, opacity: 0.15, width: 5 }, shadowOptions); if (!this.defs.element.querySelector(`#${id}`)) { this.definition({ tagName: 'filter', attributes: { id, filterUnits: options.filterUnits }, children: this.getShadowFilterContent(options) }); } return id; } /** * Get shadow filter content. * NOTE! Overridden in es5 module for IE11 compatibility. * * @private * @function Highcharts.SVGRenderer#getShadowFilterContent * * @param {ShadowOptionsObject} options * The shadow options. * @return {Array<AST.Node>} * The shadow filter content. */ getShadowFilterContent(options) { return [{ tagName: 'feDropShadow', attributes: { dx: options.offsetX, dy: options.offsetY, 'flood-color': options.color, // Tuned and modified to keep a preserve compatibility // with the old settings 'flood-opacity': Math.min(options.opacity * 5, 1), stdDeviation: options.width / 2 } }]; } /** * 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. */ buildText(wrapper) { new TextBuilder(wrapper).buildSVG(); } /** * Returns white for dark colors and black for bright colors, based on W3C's * definition of [Relative luminance]( * https://www.w3.org/WAI/GL/wiki/Relative_luminance). * * @function Highcharts.SVGRenderer#getContrast * * @param {Highcharts.ColorString} color * The color to get the contrast for. * * @return {Highcharts.ColorString} * The contrast color, either `#000000` or `#FFFFFF`. */ getContrast(color) { // #6216, #17273 const rgba256 = Color.parse(color).rgba, // For each rgb channel, compute the luminosity based on all // channels. Subtract this from 0.5 and multiply by a huge number, // so that all colors with luminosity < 0.5 result in a negative // number, and all colors > 0.5 end up very high. This is then // clamped into the range 0-1, to result in either black or white. // The subtraction of 0.5, multiplication by 9e9, and clamping are // workarounds for lack of support for the round() function. As of // 2025, it is too fresh in Chrome, and doesn't work in Safari. channelFunc = ' clamp(0,calc(9e9*(0.5 - (0.2126*r + 0.7152*g + 0.0722*b))),1)'; // The color is parsable by the Color class parsers if (isNumber(rgba256[0]) || !Color.useColorMix) { const rgba = rgba256.map((b8) => { const c = b8 / 255; return c <= 0.04 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); }), // Relative luminance l = 0.2126 * rgba[0] + 0.7152 * rgba[1] + 0.0722 * rgba[2]; // Use white or black based on which provides more contrast return 1.05 / (l + 0.05) > (l + 0.05) / 0.05 ? '#FFFFFF' : '#000000'; } // Not parsable, use CSS functions instead return 'color(' + 'from ' + color + ' srgb' + channelFunc + channelFunc + channelFunc + ')'; } /** * Create a button with preset states. Styles for the button can either be * set as arguments, or a general theme for all buttons can be set by the * `global.buttonTheme` option. * * @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} [theme] * SVG attributes for the normal state. * * @param {Highcharts.SVGAttributes} [hoverState] * SVG attributes for the hover state. * * @param {Highcharts.SVGAttributes} [selectState] * 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] * Whether to use HTML to render the label. * * @return {Highcharts.SVGElement} * The button element. */ button(text, x, y, callback, theme = {}, hoverState, selectState, disabledState, shape, useHTML) { const label = this.label(text, x, y, shape, void 0, void 0, useHTML, void 0, 'button'), styledMode = this.styledMode, args = arguments; let curState = 0; theme = merge(defaultOptions.global.buttonTheme, theme); // @todo Consider moving this to a lower level, like .attr if (styledMode) { delete theme.fill; delete theme.stroke; delete theme['stroke-width']; } const states = theme.states || {}, normalStyle = theme.style || {}; delete theme.states; delete theme.style; // Presentational const stateAttribs = [ AST.filterUserAttributes(theme) ], // The string type is a mistake, it is just for compliance with // SVGAttribute and is not used in button theme. stateStyles = [normalStyle]; if (!styledMode) { ['hover', 'select', 'disabled'].forEach((stateName, i) => { stateAttribs.push(merge(stateAttribs[0], AST.filterUserAttributes(args[i + 5] || states[stateName] || {}))); stateStyles.push(stateAttribs[i + 1].style); delete stateAttribs[i + 1].style; }); } // Add the events. IE9 and IE10 need mouseover and mouseout to function // (#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 = (state = 0) => { // 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]); if (!styledMode) { label.attr(stateAttribs[state]); const css = stateStyles[state]; if (isObject(css)) { label.css(css); } } }; label.attr(stateAttribs[0]); // Presentational attributes if (!styledMode) { label.css(extend({ cursor: 'default' }, normalStyle)); // HTML labels don't need to handle pointer events because click and // mouseenter/mouseleave is bound to the underlying <g> element. // Should this be reconsidered, we need more complex logic to share // events between the <g> and its <div> counterpart, and avoid // triggering mouseenter/mouseleave when hovering from one to the // other (#17440). if (useHTML) { label.text.css({ pointerEvents: 'none' }); } } return label .on('touchstart', (e) => e.stopPropagation()) .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. * * @return {Highcharts.SVGPathArray} * The original points array, but modified to render crisply. */ crispLine(points, width) { const [start, end] = points; // Normalize to a crisp line if (defined(start[1]) && start[1] === end[1]) { start[1] = end[1] = crisp(start[1], width); } if (defined(start[2]) && start[2] === end[2]) { start[2] = end[2] = crisp(start[2], width); } 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 * let 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. */ path(path) { const 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. */ circle(x, y, r) { const 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. */ arc(x, y, r, innerR, start, end) { let 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, start, end }; } // Arcs are defined as symbols for the ability to set // attributes in attr and animate const arc = this.symbol('arc', x, y, r, r, options); arc.r = r; // #959 return arc; } /** * Draw and return a rectangle. * * @function Highcharts.SVGRenderer#rect * * @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. * * @return {Highcharts.SVGElement} * The generated wrapper element. */ /** * Draw and return a rectangle. * * @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 * * @function Highcharts.SVGRenderer#rect * * @param {Highcharts.SVGAttributes} [attributes] * General SVG attributes for the rectangle. * * @return {Highcharts.SVGElement} * The generated wrapper element. */ rect(x, y, width, height, r, strokeWidth) { const attribs = (isObject(x) ? x : typeof x === 'undefined' ? {} : { x, y, r, width: Math.max(width || 0, 0), height: Math.max(height || 0, 0) }), wrapper = this.createElement('rect'); if (!this.styledMode) { if (typeof strokeWidth !== 'undefined') { attribs['stroke-width'] = strokeWidth; extend(attribs, wrapper.crisp(attribs)); } attribs.fill = 'none'; } wrapper.rSetter = function (value, _key, element) { wrapper.r = value; attr(element, { rx: value, ry: value }); }; wrapper.rGetter = function () { return wrapper.r || 0; }; return wrapper.attr(attribs); } /** * Draw and return a rectangle with advanced corner rounding options. * * @function Highcharts.SVGRenderer#roundedRect * * @param {Highcharts.SVGAttributes} attribs * Attributes * @return {Highcharts.SVGElement} * The generated wrapper element. */ roundedRect(attribs) { return this.symbol('roundedRect').attr(attribs); } /** * Resize the {@link SVGRenderer#box} and re-align all aligned child * elements. * * @sample highcharts/members/renderer-g/ * Show and hide grouped objects * * @function Highcharts.SVGRenderer#setSize * * @param {number} width * The new pixel width. * * @param {number} height * The new pixel height. * * @param {boolean|Partial<Highcharts.AnimationOptionsObject>} [animate=true] * Whether and how to animate. */ setSize(width, height, animate) { const renderer = this; 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) ? void 0 : 0 }); renderer.alignElements(); } /** * 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()}. * * @function Highcharts.SVGRenderer#g * * @param {string} [name] * The group will be given a class name of `highcharts-{name}`. This * can be used for styling and scripting. * * @return {Highcharts.SVGElement} * The generated wrapper element. */ g(name) { const elem = this.createElement('g'); return name ? elem.attr({ 'class': 'highcharts-' + name }) : elem; } /** * Display an image. * * @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 * * @function Highcharts.SVGRenderer#image * * @param {string} href * 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. * * @return {Highcharts.SVGElement} * The generated wrapper element. */ image(href, x, y, width, height, onload) { const attribs = { preserveAspectRatio: 'none' }; // Optional properties (#11756) if (isNumber(x)) { attribs.x = x; } if (isNumber(y)) { attribs.y = y; } if (isNumber(width)) { attribs.width = width; } if (isNumber(height)) { attribs.height = height; } const elemWrapper = this.createElement('image').attr(attribs), onDummyLoad = function (e) { elemWrapper.attr({ href }); onload.call(elemWrapper, e); }; // 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. elemWrapper.attr({ /* eslint-disable-next-line max-len */ href: 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' }); const dummy = new win.Image(); addEvent(dummy, 'load', onDummyLoad); dummy.src = href; if (dummy.complete) { onDummyLoad({}); } } else { elemWrapper.attr({ href }); } 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. * * @function Highcharts.SVGRenderer#symbol * * @param {string} 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 {Highcharts.SymbolOptionsObject} [options] * Additional options, depending on the actual symbol drawn. * * @return {Highcharts.SVGElement} * SVG symbol. */ symbol(symbol, x, y, width, height, options) { const ren = this, imageRegex = /^url\((.*?)\)$/, isImage = imageRegex.test(symbol), sym = (!isImage && (this.symbols[symbol] ? symbol : 'circle')), // Get the symbol definition function symbolFn = (sym && this.symbols[sym]); let obj, path, imageSrc, centerImage; if (symbolFn) { // Check if there's a path defined for this symbol if (typeof x === 'number') { path = symbolFn.call(this.symbols, x || 0, y || 0, width || 0, height || 0, options); } obj = this.path(path); if (!ren.styledMode) { obj.attr('fill', 'none'); } // Expando properties for use in animate and attr extend(obj, { symbolName: (sym || void 0), 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 const img = 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. img.imgwidth = pick(options?.width, symbolSizes[imageSrc]?.width); img.imgheight = pick(options?.height, symbolSizes[imageSrc]?.height); /** * Set the size and position */ centerImage = (obj) => 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. */ ['width', 'height'].forEach((key) => { img[`${key}Setter`] = function (value, key) { this[key] = value; const { alignByTranslate, element, width, height, imgwidth, imgheight } = this, imgSize = key === 'width' ? imgwidth : imgheight; let scale = 1; // Scale and center the image within its container. The name // `backgroundSize` is taken from the CSS spec, but the // value `within` is made up. Other possible values in the // spec, `cover` and `contain`, can be implemented if // needed. if (options && options.backgroundSize === 'within' && width && height && imgwidth && imgheight) { scale = Math.min(width / imgwidth, height / imgheight); // Update both width and height to keep the ratio // correct (#17315) attr(element, { width: Math.round(imgwidth * scale), height: Math.round(imgheight * scale) }); } else if (element && imgSize) { element.setAttribute(key, imgSize); } if (!alignByTranslate && imgwidth && imgheight) { this.translate(((width || 0) - (imgwidth * scale)) / 2, ((height || 0) - (imgheight * scale)) / 2); } }; }); if (defined(x)) { img.attr({ x: x, y: y }); } img.isImg = true; img.symbolUrl = symbol; if (defined(img.imgwidth) && defined(img.imgheight)) { centerImage(img); } else { // Initialize image to be 0 size so export will still function // if there's no cached sizes. img.attr({ width: 0, height: 0 }); // Create a dummy JavaScript image to get the width and height. createElement('img', { onload: function () { const 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] = { width: this.width, height: this.height }; img.imgwidth = this.width; img.imgheight = this.height; if (img.element) { centerImage(img); } // 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.hasLoaded) { chart.onload(); } }, src: imageSrc }); this.imgCount++; } } return obj; } /** * Define a clipping rectangle. The clipping rectangle is later applied * to {@link SVGElement} objects through the {@link SVGElement#clip} * function. * * This function is deprecated as of v11.2. Instead, use a regular shape * (`rect`, `path` etc), and the `SVGElement.clipTo` function. * * @example * let circle = renderer.circle(100, 100, 100) * .attr({ fill: 'red' }) * .add(); * let clipRect = renderer.clipRect(100, 100, 100, 100); * * // Leave only the lower right quarter visible * circle.clip(clipRect); * * @deprecated * * @function Highcharts.SVGRenderer#clipRect * * @param {number} [x] * * @param {number} [y] * * @param {number} [width] * * @param {number} [height] * * @return {Highcharts.ClipRectElement} * A clipping rectangle. */ clipRect(x, y, width, height) { return this.rect(x, y, width, height, 0); } /** * 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' })`. * * @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 * * @function Highcharts.SVGRenderer#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 {Highcharts.SVGElement} * The text object. */ text(str, x, y, useHTML) { const renderer = this, 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 (defined(str)) { attribs.text = str; } const wrapper = renderer.createElement('text').attr(attribs); if (!useHTML || (renderer.forExport && !renderer.allowHTML)) { wrapper.xSetter = function (value, key, element) { const tspans = element.getElementsByTagName('tspan'), parentVal = element.getAttribute(key); for (let i = 0, tspan; i < tspans.length; i++) { tspan = tspans[i]; // If the x values are equal, the tspan represents a line // break 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. * * @function Highcharts.SVGRenderer#fontMetrics * * @param {Highcharts.SVGElement|Highcharts.SVGDOMElement|number} [element] * The element to inspect for a current font size. If a number is * given, it's used as a fall back for direct font size in pixels. * * @return {Highcharts.FontMetricsObject} * The font metrics. */ fontMetrics(element) { const f = pInt(SVGElement.prototype.getStyle.call(element, 'font-size') || 0); // Empirical values found by comparing font size and bounding box // height. Applies to the default font family. // https://jsfiddle.net/highcharts/7xvn7/ const h = f < 24 ? f + 3 : Math.round(f * 1.2), b = Math.round(h * 0.8); return { // Line height h, // Baseline b, // Font size f }; } /** * Correct X and Y positioning of a label for rotation (#1764). * * @private * @function Highcharts.SVGRenderer#rotCorr */ rotCorr(baseline, rotation, alterY) { let y = baseline; if (rotation && alterY) { y = Math.max(y * Math.cos(rotation * deg2rad), 4); } return { x: (-baseline / 3) * Math.sin(rotation * deg2rad), y: y }; } /** * Compatibility function to convert the legacy one-dimensional path array * into an array of segments. * * It is used in maps to parse the `path` option, and in SVGRenderer.dSetter * to support legacy paths from demos. * * @private * @function Highcharts.SVGRenderer#pathToSegments */ pathToSegments(path) { const ret = []; const segment = []; const commandLength = { A: 8, C: 7, H: 2, L: 3, M: 3, Q: 5, S: 5, T: 3, V: 2 }; // Short, non-typesafe parsing of the one-dimensional array. It splits // the path on any string. This is not type checked against the tuple // types, but is shorter, and doesn't require specific checks for any // command type in SVG. for (let i = 0; i < path.length; i++) { // Command skipped, repeat previous or insert L/l for M/m if (isString(segment[0]) && isNumber(path[i]) && segment.length === commandLength[(segment[0].toUpperCase())]) { path.splice(i, 0, segment[0].replace('M', 'L').replace('m', 'l')); } // Split on string if (typeof path[i] === 'string') { if (segment.length) { ret.push(segment.slice(0)); } segment.length = 0; } segment.push(path[i]); } ret.push(segment.slice(0)); return ret; /* // Fully type-safe version where each tuple type is checked. The // downside is filesize and a lack of flexibility for unsupported // commands const ret: SVGPath = [], commands = {