text: /**
   * (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<Number|String>>} 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
              if (styles) {
                  // Previously set, unset it (#8234)
                  if (styles.width === null || styles.width === 'auto') {
                      delete this.textWidth;
  
                  // Apply new
                  } else if (
                      elem.nodeName.toLowerCase() === 'text' &&
                      styles.width
                  ) {
                      textWidth = this.textWidth = pInt(styles.width);
                  }
              }
  
              // store object
              this.styles = styles;
  
              if (textWidth && (!svg && this.renderer.forExport)) {
                  delete styles.width;
              }
  
              // Serialize and set style attribute
              if (elem.namespaceURI === this.SVG_NS) { // #7633
                  hyphenate = function (a, b) {
                      return '-' + b.toLowerCase();
                  };
                  objectEach(styles, function (style, n) {
                      if (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(/&lt;/g, '<')
                      .replace(/&gt;/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.setAttribute('data-z-index', value);
  
              value = +value;
              if (this[key] === value) { // Only update when needed (#3865)
                  run = false;
              }
          } else if (defined(this[key])) {
              element.removeAttribute('data-z-index');
          }
  
          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.getAttribute('data-z-index');
                  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 `&lt;` will render as `<`.
       *
       * @example
       * // Add support for unescaping quotes
       * Highcharts.SVGRenderer.prototype.escapes['"'] = '&quot;';
       *
       * @type {Object}
       */
      escapes: {
          '&': '&amp;',
          '<': '&lt;',
          '>': '&gt;',
          "'": '&#39;', // eslint-disable-line quotes
          '"': '&quot;'
      },
  
      /**
       * 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) {
                                              // one more pixel for Chrome, #3158
                                              width = actualWidth + 1;
                                          }
                                      } 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 (ellipsis && wasTooLong) {
                  wrapper.attr(
                      'title',
                      unescapeEntities(wrapper.textStr, ['&lt;', '&gt;']) // #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);
                  }
              },
              onDummyLoad = function (e) {
                  setSVGImageSource(elemWrapper.element, src);
                  onload.call(elemWrapper, e);
              };
  
          // 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', onDummyLoad);
              dummy.src = src;
              if (dummy.complete) {
                  onDummyLoad({});
              }
          } 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);
                  // #8159 - prevent misplaced data labels in treemap
                  // (useHTML: true)
                  if (text.hasBoxWidthChanged) {
                      bBox = text.getBBox(true);
                      updateBoxSize();
                  }
                  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;
  