text: /* *
   *
   *  (c) 2010-2019 Torstein Honsi
   *
   *  License: www.highcharts.com/license
   *
   *  !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
   *
   * */
  'use strict';
  import H from './Globals.js';
  /**
   * Callback JavaScript function to format the data label as a string. Note that
   * if a `format` is defined, the format takes precedence and the formatter is
   * ignored.
   *
   * @callback Highcharts.DataLabelsFormatterCallbackFunction
   *
   * @param {Highcharts.DataLabelsFormatterContextObject} this
   *        Data label context to format
   *
   * @return {number|string|null|undefined}
   *         Formatted data label text
   */
  /**
   * Context for the callback function to format the data label.
   *
   * @interface Highcharts.DataLabelsFormatterContextObject
   */ /**
   * Stacked series and pies only. The point's percentage of the total.
   * @name Highcharts.DataLabelsFormatterContextObject#percentage
   * @type {number|undefined}
   */ /**
   * The point object. The point name, if defined, is available through
   * `this.point.name`.
   * @name Highcharts.DataLabelsFormatterContextObject#point
   * @type {Highcharts.Point}
   */ /**
   * The series object. The series name is available through `this.series.name`.
   * @name Highcharts.DataLabelsFormatterContextObject#series
   * @type {Highcharts.Series}
   */ /**
   * Stacked series only. The total value at this point's x value.
   * @name Highcharts.DataLabelsFormatterContextObject#total
   * @type {number|undefined}
   */ /**
   * The x value.
   * @name Highcharts.DataLabelsFormatterContextObject#x
   * @type {number}
   */ /**
   * The y value.
   * @name Highcharts.DataLabelsFormatterContextObject#y
   * @type {number|null}
   */
  /**
   * Options for the series data labels, appearing next to each data point.
   *
   * Since v6.2.0, multiple data labels can be applied to each single point by
   * defining them as an array of configs.
   *
   * In styled mode, the data labels can be styled with the
   * `.highcharts-data-label-box` and `.highcharts-data-label` class names.
   *
   * @see {@link https://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/plotoptions/series-datalabels-enabled|Highcharts-Demo:}
   *      Data labels enabled
   * @see {@link https://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/plotoptions/series-datalabels-multiple|Highcharts-Demo:}
   *      Multiple data labels on a bar series
   * @see {@link https://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/css/series-datalabels|Highcharts-Demo:}
   *      Style mode example
   *
   * @interface Highcharts.DataLabelsOptionsObject
   */
  /**
   * Values for handling data labels that flow outside the plot area.
   *
   * @typedef {"allow"|"justify"} Highcharts.DataLabelsOverflowValue
   */
  import U from './Utilities.js';
  
  var animObject = U.animObject, arrayMax = U.arrayMax, clamp = U.clamp, defined = U.defined, extend = U.extend,
      isArray = U.isArray, objectEach = U.objectEach, pick = U.pick, relativeLength = U.relativeLength, splat = U.splat;
  import './Series.js';
  
  var format = H.format, merge = H.merge, noop = H.noop, Series = H.Series, seriesTypes = H.seriesTypes,
      stableSort = H.stableSort;
  /* eslint-disable valid-jsdoc */
  /**
   * General distribution algorithm for distributing labels of differing size
   * along a confined length in two dimensions. The algorithm takes an array of
   * objects containing a size, a target and a rank. It will place the labels as
   * close as possible to their targets, skipping the lowest ranked labels if
   * necessary.
   *
   * @private
   * @function Highcharts.distribute
   * @param {Highcharts.DataLabelsBoxArray} boxes
   * @param {number} len
   * @param {number} maxDistance
   * @return {void}
   */
  H.distribute = function (boxes, len, maxDistance) {
      var i, overlapping = true, origBoxes = boxes, // Original array will be altered with added .pos
          restBoxes = [], // The outranked overshoot
          box, target, total = 0, reducedLen = origBoxes.reducedLen || len;
  
      /**
       * @private
       */
      function sortByTarget(a, b) {
          return a.target - b.target;
      }
  
      // If the total size exceeds the len, remove those boxes with the lowest
      // rank
      i = boxes.length;
      while (i--) {
          total += boxes[i].size;
      }
      // Sort by rank, then slice away overshoot
      if (total > reducedLen) {
          stableSort(boxes, function (a, b) {
              return (b.rank || 0) - (a.rank || 0);
          });
          i = 0;
          total = 0;
          while (total <= reducedLen) {
              total += boxes[i].size;
              i++;
          }
          restBoxes = boxes.splice(i - 1, boxes.length);
      }
      // Order by target
      stableSort(boxes, sortByTarget);
      // So far we have been mutating the original array. Now
      // create a copy with target arrays
      boxes = boxes.map(function (box) {
          return {
              size: box.size,
              targets: [box.target],
              align: pick(box.align, 0.5)
          };
      });
      while (overlapping) {
          // Initial positions: target centered in box
          i = boxes.length;
          while (i--) {
              box = boxes[i];
              // Composite box, average of targets
              target = (Math.min.apply(0, box.targets) +
                  Math.max.apply(0, box.targets)) / 2;
              box.pos = clamp(target - box.size * box.align, 0, len - box.size);
          }
          // Detect overlap and join boxes
          i = boxes.length;
          overlapping = false;
          while (i--) {
              // Overlap
              if (i > 0 &&
                  boxes[i - 1].pos + boxes[i - 1].size >
                  boxes[i].pos) {
                  // Add this size to the previous box
                  boxes[i - 1].size += boxes[i].size;
                  boxes[i - 1].targets = boxes[i - 1]
                      .targets
                      .concat(boxes[i].targets);
                  boxes[i - 1].align = 0.5;
                  // Overlapping right, push left
                  if (boxes[i - 1].pos + boxes[i - 1].size > len) {
                      boxes[i - 1].pos = len - boxes[i - 1].size;
                  }
                  boxes.splice(i, 1); // Remove this item
                  overlapping = true;
              }
          }
      }
      // Add the rest (hidden boxes)
      origBoxes.push.apply(origBoxes, restBoxes);
      // Now the composite boxes are placed, we need to put the original boxes
      // within them
      i = 0;
      boxes.some(function (box) {
          var posInCompositeBox = 0;
          if (box.targets.some(function () {
              origBoxes[i].pos = box.pos + posInCompositeBox;
              // If the distance between the position and the target exceeds
              // maxDistance, abort the loop and decrease the length in increments
              // of 10% to recursively reduce the  number of visible boxes by
              // rank. Once all boxes are within the maxDistance, we're good.
              if (Math.abs(origBoxes[i].pos - origBoxes[i].target) >
                  maxDistance) {
                  // Reset the positions that are already set
                  origBoxes.slice(0, i + 1).forEach(function (box) {
                      delete box.pos;
                  });
                  // Try with a smaller length
                  origBoxes.reducedLen =
                      (origBoxes.reducedLen || len) - (len * 0.1);
                  // Recurse
                  if (origBoxes.reducedLen > len * 0.1) {
                      H.distribute(origBoxes, len, maxDistance);
                  }
                  // Exceeded maxDistance => abort
                  return true;
              }
              posInCompositeBox += origBoxes[i].size;
              i++;
          })) {
              // Exceeded maxDistance => abort
              return true;
          }
      });
      // Add the rest (hidden) boxes and sort by target
      stableSort(origBoxes, sortByTarget);
  };
  /**
   * Draw the data labels
   *
   * @private
   * @function Highcharts.Series#drawDataLabels
   * @return {void}
   * @fires Highcharts.Series#event:afterDrawDataLabels
   */
  Series.prototype.drawDataLabels = function () {
      var series = this, chart = series.chart, seriesOptions = series.options, seriesDlOptions = seriesOptions.dataLabels,
          points = series.points, pointOptions, hasRendered = series.hasRendered || 0, dataLabelsGroup,
          seriesAnimDuration = animObject(seriesOptions.animation).duration,
          fadeInDuration = Math.min(seriesAnimDuration, 200),
          defer = !chart.renderer.forExport && pick(seriesDlOptions.defer, fadeInDuration > 0), renderer = chart.renderer;
  
      /**
       * Handle the dataLabels.filter option.
       * @private
       */
      function applyFilter(point, options) {
          var filter = options.filter, op, prop, val;
          if (filter) {
              op = filter.operator;
              prop = point[filter.property];
              val = filter.value;
              if ((op === '>' && prop > val) ||
                  (op === '<' && prop < val) ||
                  (op === '>=' && prop >= val) ||
                  (op === '<=' && prop <= val) ||
                  (op === '==' && prop == val) || // eslint-disable-line eqeqeq
                  (op === '===' && prop === val)) {
                  return true;
              }
              return false;
          }
          return true;
      }
  
      /**
       * Merge two objects that can be arrays. If one of them is an array, the
       * other is merged into each element. If both are arrays, each element is
       * merged by index. If neither are arrays, we use normal merge.
       * @private
       */
      function mergeArrays(one, two) {
          var res = [], i;
          if (isArray(one) && !isArray(two)) {
              res = one.map(function (el) {
                  return merge(el, two);
              });
          } else if (isArray(two) && !isArray(one)) {
              res = two.map(function (el) {
                  return merge(one, el);
              });
          } else if (!isArray(one) && !isArray(two)) {
              res = merge(one, two);
          } else {
              i = Math.max(one.length, two.length);
              while (i--) {
                  res[i] = merge(one[i], two[i]);
              }
          }
          return res;
      }
  
      // Merge in plotOptions.dataLabels for series
      seriesDlOptions = mergeArrays(mergeArrays(chart.options.plotOptions &&
          chart.options.plotOptions.series &&
          chart.options.plotOptions.series.dataLabels, chart.options.plotOptions &&
          chart.options.plotOptions[series.type] &&
          chart.options.plotOptions[series.type].dataLabels), seriesDlOptions);
      H.fireEvent(this, 'drawDataLabels');
      if (isArray(seriesDlOptions) ||
          seriesDlOptions.enabled ||
          series._hasPointLabels) {
          // Create a separate group for the data labels to avoid rotation
          dataLabelsGroup = series.plotGroup('dataLabelsGroup', 'data-labels', defer && !hasRendered ? 'hidden' : 'inherit', // #5133, #10220
              seriesDlOptions.zIndex || 6);
          if (defer) {
              dataLabelsGroup.attr({opacity: +hasRendered}); // #3300
              if (!hasRendered) {
                  setTimeout(function () {
                      var group = series.dataLabelsGroup;
                      if (group) {
                          if (series.visible) { // #2597, #3023, #3024
                              dataLabelsGroup.show(true);
                          }
                          group[seriesOptions.animation ? 'animate' : 'attr']({opacity: 1}, {duration: fadeInDuration});
                      }
                  }, seriesAnimDuration - fadeInDuration);
              }
          }
          // Make the labels for each point
          points.forEach(function (point) {
              // Merge in series options for the point.
              // @note dataLabelAttribs (like pointAttribs) would eradicate
              // the need for dlOptions, and simplify the section below.
              pointOptions = splat(mergeArrays(seriesDlOptions, point.dlOptions || // dlOptions is used in treemaps
                  (point.options && point.options.dataLabels)));
              // Handle each individual data label for this point
              pointOptions.forEach(function (labelOptions, i) {
                  // Options for one datalabel
                  var labelEnabled = (labelOptions.enabled &&
                      // #2282, #4641, #7112, #10049
                      (!point.isNull || point.dataLabelOnNull) &&
                      applyFilter(point, labelOptions)), labelConfig, formatString, labelText, style, rotation, attr,
                      dataLabel = point.dataLabels ? point.dataLabels[i] :
                          point.dataLabel, connector = point.connectors ? point.connectors[i] :
                      point.connector, labelDistance = pick(labelOptions.distance, point.labelDistance),
                      isNew = !dataLabel;
                  if (labelEnabled) {
                      // Create individual options structure that can be extended
                      // without affecting others
                      labelConfig = point.getLabelConfig();
                      formatString = pick(labelOptions[point.formatPrefix + 'Format'], labelOptions.format);
                      labelText = defined(formatString) ?
                          format(formatString, labelConfig, chart) :
                          (labelOptions[point.formatPrefix + 'Formatter'] ||
                              labelOptions.formatter).call(labelConfig, labelOptions);
                      style = labelOptions.style;
                      rotation = labelOptions.rotation;
                      if (!chart.styledMode) {
                          // Determine the color
                          style.color = pick(labelOptions.color, style.color, series.color, '#000000');
                          // Get automated contrast color
                          if (style.color === 'contrast') {
                              point.contrastColor = renderer.getContrast((point.color || series.color));
                              style.color = (!defined(labelDistance) &&
                                  labelOptions.inside) ||
                              labelDistance < 0 ||
                              !!seriesOptions.stacking ?
                                  point.contrastColor :
                                  '#000000';
                          } else {
                              delete point.contrastColor;
                          }
                          if (seriesOptions.cursor) {
                              style.cursor = seriesOptions.cursor;
                          }
                      }
                      attr = {
                          r: labelOptions.borderRadius || 0,
                          rotation: rotation,
                          padding: labelOptions.padding,
                          zIndex: 1
                      };
                      if (!chart.styledMode) {
                          attr.fill = labelOptions.backgroundColor;
                          attr.stroke = labelOptions.borderColor;
                          attr['stroke-width'] = labelOptions.borderWidth;
                      }
                      // Remove unused attributes (#947)
                      objectEach(attr, function (val, name) {
                          if (typeof val === 'undefined') {
                              delete attr[name];
                          }
                      });
                  }
                  // If the point is outside the plot area, destroy it. #678, #820
                  if (dataLabel && (!labelEnabled || !defined(labelText))) {
                      point.dataLabel =
                          point.dataLabel && point.dataLabel.destroy();
                      if (point.dataLabels) {
                          // Remove point.dataLabels if this was the last one
                          if (point.dataLabels.length === 1) {
                              delete point.dataLabels;
                          } else {
                              delete point.dataLabels[i];
                          }
                      }
                      if (!i) {
                          delete point.dataLabel;
                      }
                      if (connector) {
                          point.connector = point.connector.destroy();
                          if (point.connectors) {
                              // Remove point.connectors if this was the last one
                              if (point.connectors.length === 1) {
                                  delete point.connectors;
                              } else {
                                  delete point.connectors[i];
                              }
                          }
                      }
                      // Individual labels are disabled if the are explicitly disabled
                      // in the point options, or if they fall outside the plot area.
                  } else if (labelEnabled && defined(labelText)) {
                      if (!dataLabel) {
                          // Create new label element
                          point.dataLabels = point.dataLabels || [];
                          dataLabel = point.dataLabels[i] = rotation ?
                              // Labels don't rotate, use text element
                              renderer.text(labelText, 0, -9999)
                                  .addClass('highcharts-data-label') :
                              // We can use label
                              renderer.label(labelText, 0, -9999, labelOptions.shape, null, null, labelOptions.useHTML, null, 'data-label');
                          // Store for backwards compatibility
                          if (!i) {
                              point.dataLabel = dataLabel;
                          }
                          dataLabel.addClass(' highcharts-data-label-color-' + point.colorIndex +
                              ' ' + (labelOptions.className || '') +
                              ( // #3398
                                  labelOptions.useHTML ?
                                      ' highcharts-tracker' :
                                      ''));
                      } else {
                          // Use old element and just update text
                          attr.text = labelText;
                      }
                      // Store data label options for later access
                      dataLabel.options = labelOptions;
                      dataLabel.attr(attr);
                      if (!chart.styledMode) {
                          // Styles must be applied before add in order to read
                          // text bounding box
                          dataLabel.css(style).shadow(labelOptions.shadow);
                      }
                      if (!dataLabel.added) {
                          dataLabel.add(dataLabelsGroup);
                      }
                      if (labelOptions.textPath && !labelOptions.useHTML) {
                          dataLabel.setTextPath((point.getDataLabelPath &&
                              point.getDataLabelPath(dataLabel)) || point.graphic, labelOptions.textPath);
                          if (point.dataLabelPath &&
                              !labelOptions.textPath.enabled) {
                              // clean the DOM
                              point.dataLabelPath = point.dataLabelPath.destroy();
                          }
                      }
                      // Now the data label is created and placed at 0,0, so we
                      // need to align it
                      series.alignDataLabel(point, dataLabel, labelOptions, null, isNew);
                  }
              });
          });
      }
      H.fireEvent(this, 'afterDrawDataLabels');
  };
  /**
   * Align each individual data label.
   *
   * @private
   * @function Highcharts.Series#alignDataLabel
   * @param {Highcharts.Point} point
   * @param {Highcharts.SVGElement} dataLabel
   * @param {Highcharts.DataLabelsOptionsObject} options
   * @param {Highcharts.BBoxObject} alignTo
   * @param {boolean} [isNew]
   * @return {void}
   */
  Series.prototype.alignDataLabel = function (point, dataLabel, options, alignTo, isNew) {
      var series = this, chart = this.chart, inverted = this.isCartesian && chart.inverted,
          enabledDataSorting = this.enabledDataSorting,
          plotX = pick(point.dlBox && point.dlBox.centerX, point.plotX, -9999), plotY = pick(point.plotY, -9999),
          bBox = dataLabel.getBBox(), baseline, rotation = options.rotation, normRotation, negRotation,
          align = options.align, rotCorr, // rotation correction
          isInsidePlot = chart.isInsidePlot(plotX, Math.round(plotY), inverted),
          // Math.round for rounding errors (#2683), alignTo to allow column
          // labels (#2700)
          alignAttr, // the final position;
          justify = pick(options.overflow, (enabledDataSorting ? 'none' : 'justify')) === 'justify',
          visible = this.visible &&
              (point.series.forceDL ||
                  (enabledDataSorting && !justify) ||
                  isInsidePlot ||
                  (alignTo && chart.isInsidePlot(plotX, inverted ?
                      alignTo.x + 1 :
                      alignTo.y + alignTo.height - 1, inverted))), setStartPos = function (alignOptions) {
              if (enabledDataSorting && series.xAxis && !justify) {
                  series.setDataLabelStartPos(point, dataLabel, isNew, isInsidePlot, alignOptions);
              }
          };
      if (visible) {
          baseline = chart.renderer.fontMetrics(chart.styledMode ? void 0 : options.style.fontSize, dataLabel).b;
          // The alignment box is a singular point
          alignTo = extend({
              x: inverted ? this.yAxis.len - plotY : plotX,
              y: Math.round(inverted ? this.xAxis.len - plotX : plotY),
              width: 0,
              height: 0
          }, alignTo);
          // Add the text size for alignment calculation
          extend(options, {
              width: bBox.width,
              height: bBox.height
          });
          // Allow a hook for changing alignment in the last moment, then do the
          // alignment
          if (rotation) {
              justify = false; // Not supported for rotated text
              rotCorr = chart.renderer.rotCorr(baseline, rotation); // #3723
              alignAttr = {
                  x: (alignTo.x +
                      options.x +
                      alignTo.width / 2 +
                      rotCorr.x),
                  y: (alignTo.y +
                      options.y +
                      {top: 0, middle: 0.5, bottom: 1}[options.verticalAlign] *
                      alignTo.height)
              };
              setStartPos(alignAttr); // data sorting
              dataLabel[isNew ? 'attr' : 'animate'](alignAttr)
                  .attr({
                      align: align
                  });
              // Compensate for the rotated label sticking out on the sides
              normRotation = (rotation + 720) % 360;
              negRotation = normRotation > 180 && normRotation < 360;
              if (align === 'left') {
                  alignAttr.y -= negRotation ? bBox.height : 0;
              } else if (align === 'center') {
                  alignAttr.x -= bBox.width / 2;
                  alignAttr.y -= bBox.height / 2;
              } else if (align === 'right') {
                  alignAttr.x -= bBox.width;
                  alignAttr.y -= negRotation ? 0 : bBox.height;
              }
              dataLabel.placed = true;
              dataLabel.alignAttr = alignAttr;
          } else {
              setStartPos(alignTo); // data sorting
              dataLabel.align(options, null, alignTo);
              alignAttr = dataLabel.alignAttr;
          }
          // Handle justify or crop
          if (justify && alignTo.height >= 0) { // #8830
              this.justifyDataLabel(dataLabel, options, alignAttr, bBox, alignTo, isNew);
              // Now check that the data label is within the plot area
          } else if (pick(options.crop, true)) {
              visible =
                  chart.isInsidePlot(alignAttr.x, alignAttr.y) &&
                  chart.isInsidePlot(alignAttr.x + bBox.width, alignAttr.y + bBox.height);
          }
          // When we're using a shape, make it possible with a connector or an
          // arrow pointing to thie point
          if (options.shape && !rotation) {
              dataLabel[isNew ? 'attr' : 'animate']({
                  anchorX: inverted ?
                      chart.plotWidth - point.plotY :
                      point.plotX,
                  anchorY: inverted ?
                      chart.plotHeight - point.plotX :
                      point.plotY
              });
          }
      }
      // To use alignAttr property in hideOverlappingLabels
      if (isNew && enabledDataSorting) {
          dataLabel.placed = false;
      }
      // Show or hide based on the final aligned position
      if (!visible && (!enabledDataSorting || justify)) {
          dataLabel.hide(true);
          dataLabel.placed = false; // don't animate back in
      }
  };
  /**
   * Set starting position for data label sorting animation.
   *
   * @private
   * @function Highcharts.Series#setDataLabelStartPos
   * @param {Highcharts.SVGElement} dataLabel
   * @param {Highcharts.ColumnPoint} point
   * @param {boolean | undefined} [isNew]
   * @param {boolean} [isInside]
   * @param {Highcharts.AlignObject} [alignOptions]
   *
   * @return {void}
   */
  Series.prototype.setDataLabelStartPos = function (point, dataLabel, isNew, isInside, alignOptions) {
      var chart = this.chart, inverted = chart.inverted, xAxis = this.xAxis, reversed = xAxis.reversed,
          labelCenter = inverted ? dataLabel.height / 2 : dataLabel.width / 2, pointWidth = point.pointWidth,
          halfWidth = pointWidth ? pointWidth / 2 : 0, startXPos, startYPos;
      startXPos = inverted ?
          alignOptions.x :
          (reversed ?
              -labelCenter - halfWidth :
              xAxis.width - labelCenter + halfWidth);
      startYPos = inverted ?
          (reversed ?
              this.yAxis.height - labelCenter + halfWidth :
              -labelCenter - halfWidth) : alignOptions.y;
      dataLabel.startXPos = startXPos;
      dataLabel.startYPos = startYPos;
      // We need to handle visibility in case of sorting point outside plot area
      if (!isInside) {
          dataLabel
              .attr({opacity: 1})
              .animate({opacity: 0}, void 0, dataLabel.hide);
      } else if (dataLabel.visibility === 'hidden') {
          dataLabel.show();
          dataLabel
              .attr({opacity: 0})
              .animate({opacity: 1});
      }
      // Save start position on first render, but do not change position
      if (!chart.hasRendered) {
          return;
      }
      // Set start position
      if (isNew) {
          dataLabel.attr({x: dataLabel.startXPos, y: dataLabel.startYPos});
      }
      dataLabel.placed = true;
  };
  /**
   * If data labels fall partly outside the plot area, align them back in, in a
   * way that doesn't hide the point.
   *
   * @private
   * @function Highcharts.Series#justifyDataLabel
   * @param {Highcharts.SVGElement} dataLabel
   * @param {Highcharts.DataLabelsOptionsObject} options
   * @param {Highcharts.SVGAttributes} alignAttr
   * @param {Highcharts.BBoxObject} bBox
   * @param {Highcharts.BBoxObject} [alignTo]
   * @param {boolean} [isNew]
   * @return {boolean|undefined}
   */
  Series.prototype.justifyDataLabel = function (dataLabel, options, alignAttr, bBox, alignTo, isNew) {
      var chart = this.chart, align = options.align, verticalAlign = options.verticalAlign, off, justified,
          padding = dataLabel.box ? 0 : (dataLabel.padding || 0);
      // Off left
      off = alignAttr.x + padding;
      if (off < 0) {
          if (align === 'right') {
              options.align = 'left';
              options.inside = true;
          } else {
              options.x = -off;
          }
          justified = true;
      }
      // Off right
      off = alignAttr.x + bBox.width - padding;
      if (off > chart.plotWidth) {
          if (align === 'left') {
              options.align = 'right';
              options.inside = true;
          } else {
              options.x = chart.plotWidth - off;
          }
          justified = true;
      }
      // Off top
      off = alignAttr.y + padding;
      if (off < 0) {
          if (verticalAlign === 'bottom') {
              options.verticalAlign = 'top';
              options.inside = true;
          } else {
              options.y = -off;
          }
          justified = true;
      }
      // Off bottom
      off = alignAttr.y + bBox.height - padding;
      if (off > chart.plotHeight) {
          if (verticalAlign === 'top') {
              options.verticalAlign = 'bottom';
              options.inside = true;
          } else {
              options.y = chart.plotHeight - off;
          }
          justified = true;
      }
      if (justified) {
          dataLabel.placed = !isNew;
          dataLabel.align(options, null, alignTo);
      }
      return justified;
  };
  if (seriesTypes.pie) {
      seriesTypes.pie.prototype.dataLabelPositioners = {
          // Based on the value computed in Highcharts' distribute algorithm.
          radialDistributionY: function (point) {
              return point.top + point.distributeBox.pos;
          },
          // get the x - use the natural x position for labels near the
          // top and bottom, to prevent the top and botton slice
          // connectors from touching each other on either side
          // Based on the value computed in Highcharts' distribute algorithm.
          radialDistributionX: function (series, point, y, naturalY) {
              return series.getX(y < point.top + 2 || y > point.bottom - 2 ?
                  naturalY :
                  y, point.half, point);
          },
          // dataLabels.distance determines the x position of the label
          justify: function (point, radius, seriesCenter) {
              return seriesCenter[0] + (point.half ? -1 : 1) *
                  (radius + point.labelDistance);
          },
          // Left edges of the left-half labels touch the left edge of the plot
          // area. Right edges of the right-half labels touch the right edge of
          // the plot area.
          alignToPlotEdges: function (dataLabel, half, plotWidth, plotLeft) {
              var dataLabelWidth = dataLabel.getBBox().width;
              return half ? dataLabelWidth + plotLeft :
                  plotWidth - dataLabelWidth - plotLeft;
          },
          // Connectors of each side end in the same x position. Labels are
          // aligned to them. Left edge of the widest left-half label touches the
          // left edge of the plot area. Right edge of the widest right-half label
          // touches the right edge of the plot area.
          alignToConnectors: function (points, half, plotWidth, plotLeft) {
              var maxDataLabelWidth = 0, dataLabelWidth;
              // find widest data label
              points.forEach(function (point) {
                  dataLabelWidth = point.dataLabel.getBBox().width;
                  if (dataLabelWidth > maxDataLabelWidth) {
                      maxDataLabelWidth = dataLabelWidth;
                  }
              });
              return half ? maxDataLabelWidth + plotLeft :
                  plotWidth - maxDataLabelWidth - plotLeft;
          }
      };
      /**
       * Override the base drawDataLabels method by pie specific functionality
       *
       * @private
       * @function Highcharts.seriesTypes.pie#drawDataLabels
       * @return {void}
       */
      seriesTypes.pie.prototype.drawDataLabels = function () {
          var series = this, data = series.data, point, chart = series.chart, options = series.options.dataLabels,
              connectorPadding = options.connectorPadding, connectorWidth, plotWidth = chart.plotWidth,
              plotHeight = chart.plotHeight, plotLeft = chart.plotLeft, maxWidth = Math.round(chart.chartWidth / 3),
              connector, seriesCenter = series.center, radius = seriesCenter[2] / 2, centerY = seriesCenter[1], dataLabel,
              dataLabelWidth,
              // labelPos,
              labelPosition, labelHeight,
              // divide the points into right and left halves for anti collision
              halves = [
                  [],
                  [] // left
              ], x, y, visibility, j, overflow = [0, 0, 0, 0], // top, right, bottom, left
              dataLabelPositioners = series.dataLabelPositioners, pointDataLabelsOptions;
          // get out if not enabled
          if (!series.visible ||
              (!options.enabled &&
                  !series._hasPointLabels)) {
              return;
          }
          // Reset all labels that have been shortened
          data.forEach(function (point) {
              if (point.dataLabel && point.visible && point.dataLabel.shortened) {
                  point.dataLabel
                      .attr({
                          width: 'auto'
                      }).css({
                      width: 'auto',
                      textOverflow: 'clip'
                  });
                  point.dataLabel.shortened = false;
              }
          });
          // run parent method
          Series.prototype.drawDataLabels.apply(series);
          data.forEach(function (point) {
              if (point.dataLabel) {
                  if (point.visible) { // #407, #2510
                      // Arrange points for detection collision
                      halves[point.half].push(point);
                      // Reset positions (#4905)
                      point.dataLabel._pos = null;
                      // Avoid long labels squeezing the pie size too far down
                      if (!defined(options.style.width) &&
                          !defined(point.options.dataLabels &&
                              point.options.dataLabels.style &&
                              point.options.dataLabels.style.width)) {
                          if (point.dataLabel.getBBox().width > maxWidth) {
                              point.dataLabel.css({
                                  // Use a fraction of the maxWidth to avoid
                                  // wrapping close to the end of the string.
                                  width: maxWidth * 0.7
                              });
                              point.dataLabel.shortened = true;
                          }
                      }
                  } else {
                      point.dataLabel = point.dataLabel.destroy();
                      // Workaround to make pies destroy multiple datalabels
                      // correctly. This logic needs rewriting to support multiple
                      // datalabels fully.
                      if (point.dataLabels && point.dataLabels.length === 1) {
                          delete point.dataLabels;
                      }
                  }
              }
          });
          /* Loop over the points in each half, starting from the top and bottom
           * of the pie to detect overlapping labels.
           */
          halves.forEach(function (points, i) {
              var top, bottom, length = points.length, positions = [], naturalY, sideOverflow, size, distributionLength;
              if (!length) {
                  return;
              }
              // Sort by angle
              series.sortByAngle(points, i - 0.5);
              // Only do anti-collision when we have dataLabels outside the pie
              // and have connectors. (#856)
              if (series.maxLabelDistance > 0) {
                  top = Math.max(0, centerY - radius - series.maxLabelDistance);
                  bottom = Math.min(centerY + radius + series.maxLabelDistance, chart.plotHeight);
                  points.forEach(function (point) {
                      // check if specific points' label is outside the pie
                      if (point.labelDistance > 0 && point.dataLabel) {
                          // point.top depends on point.labelDistance value
                          // Used for calculation of y value in getX method
                          point.top = Math.max(0, centerY - radius - point.labelDistance);
                          point.bottom = Math.min(centerY + radius + point.labelDistance, chart.plotHeight);
                          size = point.dataLabel.getBBox().height || 21;
                          // point.positionsIndex is needed for getting index of
                          // parameter related to specific point inside positions
                          // array - not every point is in positions array.
                          point.distributeBox = {
                              target: point.labelPosition.natural.y -
                                  point.top + size / 2,
                              size: size,
                              rank: point.y
                          };
                          positions.push(point.distributeBox);
                      }
                  });
                  distributionLength = bottom + size - top;
                  H.distribute(positions, distributionLength, distributionLength / 5);
              }
              // Now the used slots are sorted, fill them up sequentially
              for (j = 0; j < length; j++) {
                  point = points[j];
                  // labelPos = point.labelPos;
                  labelPosition = point.labelPosition;
                  dataLabel = point.dataLabel;
                  visibility = point.visible === false ? 'hidden' : 'inherit';
                  naturalY = labelPosition.natural.y;
                  y = naturalY;
                  if (positions && defined(point.distributeBox)) {
                      if (typeof point.distributeBox.pos === 'undefined') {
                          visibility = 'hidden';
                      } else {
                          labelHeight = point.distributeBox.size;
                          // Find label's y position
                          y = dataLabelPositioners
                              .radialDistributionY(point);
                      }
                  }
                  // It is needed to delete point.positionIndex for
                  // dynamically added points etc.
                  delete point.positionIndex; // @todo unused
                  // Find label's x position
                  // justify is undocumented in the API - preserve support for it
                  if (options.justify) {
                      x = dataLabelPositioners.justify(point, radius, seriesCenter);
                  } else {
                      switch (options.alignTo) {
                          case 'connectors':
                              x = dataLabelPositioners.alignToConnectors(points, i, plotWidth, plotLeft);
                              break;
                          case 'plotEdges':
                              x = dataLabelPositioners.alignToPlotEdges(dataLabel, i, plotWidth, plotLeft);
                              break;
                          default:
                              x = dataLabelPositioners.radialDistributionX(series, point, y, naturalY);
                      }
                  }
                  // Record the placement and visibility
                  dataLabel._attr = {
                      visibility: visibility,
                      align: labelPosition.alignment
                  };
                  dataLabel._pos = {
                      x: (x +
                          options.x +
                          ({
                              left: connectorPadding,
                              right: -connectorPadding
                          }[labelPosition.alignment] || 0)),
                      // 10 is for the baseline (label vs text)
                      y: y + options.y - 10
                  };
                  // labelPos.x = x;
                  // labelPos.y = y;
                  labelPosition.final.x = x;
                  labelPosition.final.y = y;
                  // Detect overflowing data labels
                  if (pick(options.crop, true)) {
                      dataLabelWidth = dataLabel.getBBox().width;
                      sideOverflow = null;
                      // Overflow left
                      if (x - dataLabelWidth < connectorPadding &&
                          i === 1 // left half
                      ) {
                          sideOverflow = Math.round(dataLabelWidth - x + connectorPadding);
                          overflow[3] = Math.max(sideOverflow, overflow[3]);
                          // Overflow right
                      } else if (x + dataLabelWidth > plotWidth - connectorPadding &&
                          i === 0 // right half
                      ) {
                          sideOverflow = Math.round(x + dataLabelWidth - plotWidth + connectorPadding);
                          overflow[1] = Math.max(sideOverflow, overflow[1]);
                      }
                      // Overflow top
                      if (y - labelHeight / 2 < 0) {
                          overflow[0] = Math.max(Math.round(-y + labelHeight / 2), overflow[0]);
                          // Overflow left
                      } else if (y + labelHeight / 2 > plotHeight) {
                          overflow[2] = Math.max(Math.round(y + labelHeight / 2 - plotHeight), overflow[2]);
                      }
                      dataLabel.sideOverflow = sideOverflow;
                  }
              } // for each point
          }); // for each half
          // Do not apply the final placement and draw the connectors until we
          // have verified that labels are not spilling over.
          if (arrayMax(overflow) === 0 ||
              this.verifyDataLabelOverflow(overflow)) {
              // Place the labels in the final position
              this.placeDataLabels();
              this.points.forEach(function (point) {
                  // #8864: every connector can have individual options
                  pointDataLabelsOptions =
                      merge(options, point.options.dataLabels);
                  connectorWidth =
                      pick(pointDataLabelsOptions.connectorWidth, 1);
                  // Draw the connector
                  if (connectorWidth) {
                      var isNew;
                      connector = point.connector;
                      dataLabel = point.dataLabel;
                      if (dataLabel &&
                          dataLabel._pos &&
                          point.visible &&
                          point.labelDistance > 0) {
                          visibility = dataLabel._attr.visibility;
                          isNew = !connector;
                          if (isNew) {
                              point.connector = connector = chart.renderer
                                  .path()
                                  .addClass('highcharts-data-label-connector ' +
                                      ' highcharts-color-' + point.colorIndex +
                                      (point.className ?
                                          ' ' + point.className :
                                          ''))
                                  .add(series.dataLabelsGroup);
                              if (!chart.styledMode) {
                                  connector.attr({
                                      'stroke-width': connectorWidth,
                                      'stroke': (pointDataLabelsOptions.connectorColor ||
                                          point.color ||
                                          '#666666')
                                  });
                              }
                          }
                          connector[isNew ? 'attr' : 'animate']({
                              d: point.getConnectorPath()
                          });
                          connector.attr('visibility', visibility);
                      } else if (connector) {
                          point.connector = connector.destroy();
                      }
                  }
              });
          }
      };
      /**
       * Extendable method for getting the path of the connector between the data
       * label and the pie slice.
       *
       * @private
       * @function Highcharts.seriesTypes.pie#connectorPath
       *
       * @param {*} labelPos
       *
       * @return {Highcharts.SVGPathArray}
       */
      // TODO: depracated - remove it
      /*
      seriesTypes.pie.prototype.connectorPath = function (labelPos) {
          var x = labelPos.x,
              y = labelPos.y;
          return pick(this.options.dataLabels.softConnector, true) ? [
              'M',
              // end of the string at the label
              x + (labelPos[6] === 'left' ? 5 : -5), y,
              'C',
              x, y, // first break, next to the label
              2 * labelPos[2] - labelPos[4], 2 * labelPos[3] - labelPos[5],
              labelPos[2], labelPos[3], // second break
              'L',
              labelPos[4], labelPos[5] // base
          ] : [
              'M',
              // end of the string at the label
              x + (labelPos[6] === 'left' ? 5 : -5), y,
              'L',
              labelPos[2], labelPos[3], // second break
              'L',
              labelPos[4], labelPos[5] // base
          ];
      };
      */
      /**
       * Perform the final placement of the data labels after we have verified
       * that they fall within the plot area.
       *
       * @private
       * @function Highcharts.seriesTypes.pie#placeDataLabels
       * @return {void}
       */
      seriesTypes.pie.prototype.placeDataLabels = function () {
          this.points.forEach(function (point) {
              var dataLabel = point.dataLabel, _pos;
              if (dataLabel && point.visible) {
                  _pos = dataLabel._pos;
                  if (_pos) {
                      // Shorten data labels with ellipsis if they still overflow
                      // after the pie has reached minSize (#223).
                      if (dataLabel.sideOverflow) {
                          dataLabel._attr.width =
                              Math.max(dataLabel.getBBox().width -
                                  dataLabel.sideOverflow, 0);
                          dataLabel.css({
                              width: dataLabel._attr.width + 'px',
                              textOverflow: ((this.options.dataLabels.style || {})
                                      .textOverflow ||
                                  'ellipsis')
                          });
                          dataLabel.shortened = true;
                      }
                      dataLabel.attr(dataLabel._attr);
                      dataLabel[dataLabel.moved ? 'animate' : 'attr'](_pos);
                      dataLabel.moved = true;
                  } else if (dataLabel) {
                      dataLabel.attr({y: -9999});
                  }
              }
              // Clear for update
              delete point.distributeBox;
          }, this);
      };
      seriesTypes.pie.prototype.alignDataLabel = noop;
      /**
       * Verify whether the data labels are allowed to draw, or we should run more
       * translation and data label positioning to keep them inside the plot area.
       * Returns true when data labels are ready to draw.
       *
       * @private
       * @function Highcharts.seriesTypes.pie#verifyDataLabelOverflow
       * @param {Array<number>} overflow
       * @return {boolean}
       */
      seriesTypes.pie.prototype.verifyDataLabelOverflow = function (overflow) {
          var center = this.center, options = this.options, centerOption = options.center,
              minSize = options.minSize || 80, newSize = minSize,
              // If a size is set, return true and don't try to shrink the pie
              // to fit the labels.
              ret = options.size !== null;
          if (!ret) {
              // Handle horizontal size and center
              if (centerOption[0] !== null) { // Fixed center
                  newSize = Math.max(center[2] -
                      Math.max(overflow[1], overflow[3]), minSize);
              } else { // Auto center
                  newSize = Math.max(
                      // horizontal overflow
                      center[2] - overflow[1] - overflow[3], minSize);
                  // horizontal center
                  center[0] += (overflow[3] - overflow[1]) / 2;
              }
              // Handle vertical size and center
              if (centerOption[1] !== null) { // Fixed center
                  newSize = clamp(newSize, minSize, center[2] - Math.max(overflow[0], overflow[2]));
              } else { // Auto center
                  newSize = clamp(newSize, minSize,
                      // vertical overflow
                      center[2] - overflow[0] - overflow[2]);
                  // vertical center
                  center[1] += (overflow[0] - overflow[2]) / 2;
              }
              // If the size must be decreased, we need to run translate and
              // drawDataLabels again
              if (newSize < center[2]) {
                  center[2] = newSize;
                  center[3] = Math.min(// #3632
                      relativeLength(options.innerSize || 0, newSize), newSize);
                  this.translate(center);
                  if (this.drawDataLabels) {
                      this.drawDataLabels();
                  }
                  // Else, return true to indicate that the pie and its labels is
                  // within the plot area
              } else {
                  ret = true;
              }
          }
          return ret;
      };
  }
  if (seriesTypes.column) {
      /**
       * Override the basic data label alignment by adjusting for the position of
       * the column.
       *
       * @private
       * @function Highcharts.seriesTypes.column#alignDataLabel
       * @param {Highcharts.Point} point
       * @param {Highcharts.SVGElement} dataLabel
       * @param {Highcharts.DataLabelsOptionsObject} options
       * @param {Highcharts.BBoxObject} alignTo
       * @param {boolean} [isNew]
       * @return {void}
       */
      seriesTypes.column.prototype.alignDataLabel = function (point, dataLabel, options, alignTo, isNew) {
          var inverted = this.chart.inverted, series = point.series,
              // data label box for alignment
              dlBox = point.dlBox || point.shapeArgs, below = pick(point.below, // range series
              point.plotY >
              pick(this.translatedThreshold, series.yAxis.len)),
              // draw it inside the box?
              inside = pick(options.inside, !!this.options.stacking), overshoot;
          // Align to the column itself, or the top of it
          if (dlBox) { // Area range uses this method but not alignTo
              alignTo = merge(dlBox);
              if (alignTo.y < 0) {
                  alignTo.height += alignTo.y;
                  alignTo.y = 0;
              }
              overshoot = alignTo.y + alignTo.height - series.yAxis.len;
              if (overshoot > 0) {
                  alignTo.height -= overshoot;
              }
              if (inverted) {
                  alignTo = {
                      x: series.yAxis.len - alignTo.y - alignTo.height,
                      y: series.xAxis.len - alignTo.x - alignTo.width,
                      width: alignTo.height,
                      height: alignTo.width
                  };
              }
              // Compute the alignment box
              if (!inside) {
                  if (inverted) {
                      alignTo.x += below ? 0 : alignTo.width;
                      alignTo.width = 0;
                  } else {
                      alignTo.y += below ? alignTo.height : 0;
                      alignTo.height = 0;
                  }
              }
          }
          // When alignment is undefined (typically columns and bars), display the
          // individual point below or above the point depending on the threshold
          options.align = pick(options.align, !inverted || inside ? 'center' : below ? 'right' : 'left');
          options.verticalAlign = pick(options.verticalAlign, inverted || inside ? 'middle' : below ? 'top' : 'bottom');
          // Call the parent method
          Series.prototype.alignDataLabel.call(this, point, dataLabel, options, alignTo, isNew);
          // Hide dataLabel when column is outside plotArea (#12370).
          if (alignTo &&
              ((alignTo.height <= 0 && alignTo.y === this.chart.plotHeight) ||
                  (alignTo.width <= 0 && alignTo.x === 0))) {
              dataLabel.hide(true);
              dataLabel.placed = false; // don't animate back in
          }
          // If label was justified and we have contrast, set it:
          if (options.inside && point.contrastColor) {
              dataLabel.css({
                  color: point.contrastColor
              });
          }
      };
  }
  