1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018 |
- /**
- * (c) 2009-2019 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- /**
- * Containing the position of a box that should be avoided by labels.
- *
- * @interface Highcharts.LabelIntersectBoxObject
- *//**
- * @name Highcharts.LabelIntersectBoxObject#bottom
- * @type {number}
- *//**
- * @name Highcharts.LabelIntersectBoxObject#left
- * @type {number}
- *//**
- * @name Highcharts.LabelIntersectBoxObject#right
- * @type {number}
- *//**
- * @name Highcharts.LabelIntersectBoxObject#top
- * @type {number}
- */
- /*
- * Highcharts module to place labels next to a series in a natural position.
- *
- * TODO:
- * - add column support (box collision detection, boxesToAvoid logic)
- * - avoid data labels, when data labels above, show series label below.
- * - add more options (connector, format, formatter)
- *
- * https://jsfiddle.net/highcharts/L2u9rpwr/
- * https://jsfiddle.net/highcharts/y5A37/
- * https://jsfiddle.net/highcharts/264Nm/
- * https://jsfiddle.net/highcharts/y5A37/
- */
- 'use strict';
- import H from '../parts/Globals.js';
- import '../parts/Utilities.js';
- import '../parts/Chart.js';
- import '../parts/Series.js';
- var labelDistance = 3,
- addEvent = H.addEvent,
- extend = H.extend,
- isNumber = H.isNumber,
- pick = H.pick,
- Series = H.Series,
- SVGRenderer = H.SVGRenderer,
- Chart = H.Chart;
- H.setOptions({
- /**
- * @optionparent plotOptions
- */
- plotOptions: {
- series: {
- /**
- * Series labels are placed as close to the series as possible in a
- * natural way, seeking to avoid other series. The goal of this
- * feature is to make the chart more easily readable, like if a
- * human designer placed the labels in the optimal position.
- *
- * The series labels currently work with series types having a
- * `graph` or an `area`.
- *
- * Requires the `series-label.js` module.
- *
- * @sample highcharts/series-label/line-chart
- * Line chart
- * @sample highcharts/demo/streamgraph
- * Stream graph
- * @sample highcharts/series-label/stock-chart
- * Stock chart
- *
- * @since 6.0.0
- * @product highcharts highstock gantt
- */
- label: {
- /**
- * Enable the series label per series.
- */
- enabled: true,
- /**
- * Allow labels to be placed distant to the graph if necessary,
- * and draw a connector line to the graph. Setting this option
- * to true may decrease the performance significantly, since the
- * algorithm with systematically search for open spaces in the
- * whole plot area. Visually, it may also result in a more
- * cluttered chart, though more of the series will be labeled.
- */
- connectorAllowed: false,
- /**
- * If the label is closer than this to a neighbour graph, draw a
- * connector.
- */
- connectorNeighbourDistance: 24,
- /**
- * For area-like series, allow the font size to vary so that
- * small areas get a smaller font size. The default applies this
- * effect to area-like series but not line-like series.
- *
- * @type {number|null}
- */
- minFontSize: null,
- /**
- * For area-like series, allow the font size to vary so that
- * small areas get a smaller font size. The default applies this
- * effect to area-like series but not line-like series.
- *
- * @type {number|null}
- */
- maxFontSize: null,
- /**
- * Draw the label on the area of an area series. By default it
- * is drawn on the area. Set it to `false` to draw it next to
- * the graph instead.
- *
- * @type {boolean|null}
- */
- onArea: null,
- /**
- * Styles for the series label. The color defaults to the series
- * color, or a contrast color if `onArea`.
- *
- * @type {Highcharts.CSSObject}
- * @default {"font-weight": "bold"}
- */
- style: {
- /**
- * @ignore
- */
- fontWeight: 'bold'
- },
- /**
- * An array of boxes to avoid when laying out the labels. Each
- * item has a `left`, `right`, `top` and `bottom` property.
- *
- * @type {Array<Highcharts.LabelIntersectBoxObject>}
- */
- boxesToAvoid: []
- }
- }
- }
- });
- /**
- * Counter-clockwise, part of the fast line intersection logic.
- *
- * @private
- * @function ccw
- *
- * @param {number} x1
- *
- * @param {number} y1
- *
- * @param {number} x2
- *
- * @param {number} y2
- *
- * @param {number} x3
- *
- * @param {number} y3
- *
- * @return {boolean}
- */
- function ccw(x1, y1, x2, y2, x3, y3) {
- var cw = ((y3 - y1) * (x2 - x1)) - ((y2 - y1) * (x3 - x1));
- return cw > 0 ? true : !(cw < 0);
- }
- /**
- * Detect if two lines intersect.
- *
- * @private
- * @function ccw
- *
- * @param {number} x1
- *
- * @param {number} y1
- *
- * @param {number} x2
- *
- * @param {number} y2
- *
- * @param {number} x3
- *
- * @param {number} y3
- *
- * @param {number} x4
- *
- * @param {number} y4
- *
- * @return {boolean}
- */
- function intersectLine(x1, y1, x2, y2, x3, y3, x4, y4) {
- return ccw(x1, y1, x3, y3, x4, y4) !== ccw(x2, y2, x3, y3, x4, y4) &&
- ccw(x1, y1, x2, y2, x3, y3) !== ccw(x1, y1, x2, y2, x4, y4);
- }
- /**
- * Detect if a box intersects with a line.
- *
- * @private
- * @function boxIntersectLine
- *
- * @param {number} x
- *
- * @param {number} y
- *
- * @param {number} w
- *
- * @param {number} h
- *
- * @param {number} x1
- *
- * @param {number} y1
- *
- * @param {number} x2
- *
- * @param {number} y2
- *
- * @return {boolean}
- */
- function boxIntersectLine(x, y, w, h, x1, y1, x2, y2) {
- return (
- intersectLine(x, y, x + w, y, x1, y1, x2, y2) || // top of label
- intersectLine(x + w, y, x + w, y + h, x1, y1, x2, y2) || // right
- intersectLine(x, y + h, x + w, y + h, x1, y1, x2, y2) || // bottom
- intersectLine(x, y, x, y + h, x1, y1, x2, y2) // left of label
- );
- }
- /**
- * General symbol definition for labels with connector.
- *
- * @private
- * @function Highcharts.SVGRenderer#symbols.connector
- *
- * @param {number} x
- *
- * @param {number} y
- *
- * @param {number} w
- *
- * @param {number} h
- *
- * @param {Highcharts.SymbolOptionsObject} options
- *
- * @return {Highcharts.SVGPathArray}
- */
- SVGRenderer.prototype.symbols.connector = function (x, y, w, h, options) {
- var anchorX = options && options.anchorX,
- anchorY = options && options.anchorY,
- path,
- yOffset,
- lateral = w / 2;
- if (isNumber(anchorX) && isNumber(anchorY)) {
- path = ['M', anchorX, anchorY];
- // Prefer 45 deg connectors
- yOffset = y - anchorY;
- if (yOffset < 0) {
- yOffset = -h - yOffset;
- }
- if (yOffset < w) {
- lateral = anchorX < x + (w / 2) ? yOffset : w - yOffset;
- }
- // Anchor below label
- if (anchorY > y + h) {
- path.push('L', x + lateral, y + h);
- // Anchor above label
- } else if (anchorY < y) {
- path.push('L', x + lateral, y);
- // Anchor left of label
- } else if (anchorX < x) {
- path.push('L', x, y + h / 2);
- // Anchor right of label
- } else if (anchorX > x + w) {
- path.push('L', x + w, y + h / 2);
- }
- }
- return path || [];
- };
- /**
- * Points to avoid. In addition to actual data points, the label should avoid
- * interpolated positions.
- *
- * @private
- * @function Highcharts.Series#getPointsOnGraph
- *
- * @return {Array<Highcharts.Point>}
- */
- Series.prototype.getPointsOnGraph = function () {
- if (!this.xAxis && !this.yAxis) {
- return;
- }
- var distance = 16,
- points = this.points,
- point,
- last,
- interpolated = [],
- i,
- deltaX,
- deltaY,
- delta,
- len,
- n,
- j,
- d,
- graph = this.graph || this.area,
- node = graph.element,
- inverted = this.chart.inverted,
- xAxis = this.xAxis,
- yAxis = this.yAxis,
- paneLeft = inverted ? yAxis.pos : xAxis.pos,
- paneTop = inverted ? xAxis.pos : yAxis.pos,
- onArea = pick(this.options.label.onArea, !!this.area),
- translatedThreshold = yAxis.getThreshold(this.options.threshold),
- grid = {};
- // Push the point to the interpolated points, but only if that position in
- // the grid has not been occupied. As a performance optimization, we divide
- // the plot area into a grid and only add one point per series (#9815).
- function pushDiscrete(point) {
- var cellSize = 8,
- key = Math.round(point.plotX / cellSize) + ',' +
- Math.round(point.plotY / cellSize);
- if (!grid[key]) {
- grid[key] = 1;
- interpolated.push(point);
- }
- }
- // For splines, get the point at length (possible caveat: peaks are not
- // correctly detected)
- if (
- this.getPointSpline &&
- node.getPointAtLength &&
- !onArea &&
- // Not performing well on complex series, node.getPointAtLength is too
- // heavy (#9815)
- points.length < this.chart.plotSizeX / distance
- ) {
- // If it is animating towards a path definition, use that briefly, and
- // reset
- if (graph.toD) {
- d = graph.attr('d');
- graph.attr({ d: graph.toD });
- }
- len = node.getTotalLength();
- for (i = 0; i < len; i += distance) {
- point = node.getPointAtLength(i);
- pushDiscrete({
- chartX: paneLeft + point.x,
- chartY: paneTop + point.y,
- plotX: point.x,
- plotY: point.y
- });
- }
- if (d) {
- graph.attr({ d: d });
- }
- // Last point
- point = points[points.length - 1];
- point.chartX = paneLeft + point.plotX;
- point.chartY = paneTop + point.plotY;
- pushDiscrete(point);
- // Interpolate
- } else {
- len = points.length;
- for (i = 0; i < len; i += 1) {
- point = points[i];
- last = points[i - 1];
- // Absolute coordinates so we can compare different panes
- point.chartX = paneLeft + point.plotX;
- point.chartY = paneTop + point.plotY;
- if (onArea) {
- // Vertically centered inside area
- point.chartCenterY = paneTop + (
- point.plotY +
- pick(point.yBottom, translatedThreshold)
- ) / 2;
- }
- // Add interpolated points
- if (i > 0) {
- deltaX = Math.abs(point.chartX - last.chartX);
- deltaY = Math.abs(point.chartY - last.chartY);
- delta = Math.max(deltaX, deltaY);
- if (delta > distance) {
- n = Math.ceil(delta / distance);
- for (j = 1; j < n; j += 1) {
- pushDiscrete({
- chartX: last.chartX +
- (point.chartX - last.chartX) * (j / n),
- chartY: last.chartY +
- (point.chartY - last.chartY) * (j / n),
- chartCenterY: last.chartCenterY +
- (point.chartCenterY - last.chartCenterY) *
- (j / n),
- plotX: last.plotX +
- (point.plotX - last.plotX) * (j / n),
- plotY: last.plotY +
- (point.plotY - last.plotY) * (j / n)
- });
- }
- }
- }
- // Add the real point in order to find positive and negative peaks
- if (isNumber(point.plotY)) {
- pushDiscrete(point);
- }
- }
- }
- // Get the bounding box so we can do a quick check first if the bounding
- // boxes overlap.
- /*
- interpolated.bBox = node.getBBox();
- interpolated.bBox.x += paneLeft;
- interpolated.bBox.y += paneTop;
- */
- return interpolated;
- };
- /**
- * Overridable function to return series-specific font sizes for the labels. By
- * default it returns bigger font sizes for series with the greater sum of y
- * values.
- *
- * @private
- * @function Highcharts.Series#labelFontSize
- *
- * @param {number} minFontSize
- *
- * @param {number} maxFontSize
- *
- * @return {string}
- */
- Series.prototype.labelFontSize = function (minFontSize, maxFontSize) {
- return minFontSize + (
- (this.sum / this.chart.labelSeriesMaxSum) *
- (maxFontSize - minFontSize)
- ) + 'px';
- };
- /**
- * Check whether a proposed label position is clear of other elements.
- *
- * @private
- * @function Highcharts.Series#checkClearPoint
- *
- * @param {number} x
- *
- * @param {number} y
- *
- * @param {Highcharts.BBoxObject}
- *
- * @param {boolean} [checkDistance]
- *
- * @return {false|*}
- */
- Series.prototype.checkClearPoint = function (x, y, bBox, checkDistance) {
- var distToOthersSquared = Number.MAX_VALUE, // distance to other graphs
- distToPointSquared = Number.MAX_VALUE,
- dist,
- connectorPoint,
- connectorEnabled = this.options.label.connectorAllowed,
- onArea = pick(this.options.label.onArea, !!this.area),
- chart = this.chart,
- series,
- points,
- leastDistance = 16,
- withinRange,
- xDist,
- yDist,
- i,
- j;
- function intersectRect(r1, r2) {
- return !(r2.left > r1.right ||
- r2.right < r1.left ||
- r2.top > r1.bottom ||
- r2.bottom < r1.top);
- }
- /**
- * Get the weight in order to determine the ideal position. Larger distance
- * to other series gives more weight. Smaller distance to the actual point
- * (connector points only) gives more weight.
- */
- function getWeight(distToOthersSquared, distToPointSquared) {
- return distToOthersSquared - distToPointSquared;
- }
- // First check for collision with existing labels
- for (i = 0; i < chart.boxesToAvoid.length; i += 1) {
- if (intersectRect(chart.boxesToAvoid[i], {
- left: x,
- right: x + bBox.width,
- top: y,
- bottom: y + bBox.height
- })) {
- return false;
- }
- }
- // For each position, check if the lines around the label intersect with any
- // of the graphs.
- for (i = 0; i < chart.series.length; i += 1) {
- series = chart.series[i];
- points = series.interpolatedPoints;
- if (series.visible && points) {
- for (j = 1; j < points.length; j += 1) {
- if (
- // To avoid processing, only check intersection if the X
- // values are close to the box.
- points[j].chartX >= x - leastDistance &&
- points[j - 1].chartX <= x + bBox.width + leastDistance
- ) {
- // If any of the box sides intersect with the line, return.
- if (boxIntersectLine(
- x,
- y,
- bBox.width,
- bBox.height,
- points[j - 1].chartX,
- points[j - 1].chartY,
- points[j].chartX,
- points[j].chartY
- )) {
- return false;
- }
- // But if it is too far away (a padded box doesn't
- // intersect), also return.
- if (this === series && !withinRange && checkDistance) {
- withinRange = boxIntersectLine(
- x - leastDistance,
- y - leastDistance,
- bBox.width + 2 * leastDistance,
- bBox.height + 2 * leastDistance,
- points[j - 1].chartX,
- points[j - 1].chartY,
- points[j].chartX,
- points[j].chartY
- );
- }
- }
- // Find the squared distance from the center of the label. On
- // area series, avoid its own graph.
- if (
- (connectorEnabled || withinRange) &&
- (this !== series || onArea)
- ) {
- xDist = x + bBox.width / 2 - points[j].chartX;
- yDist = y + bBox.height / 2 - points[j].chartY;
- distToOthersSquared = Math.min(
- distToOthersSquared,
- xDist * xDist + yDist * yDist
- );
- }
- }
- // Do we need a connector?
- if (
- !onArea &&
- connectorEnabled &&
- this === series &&
- (
- (checkDistance && !withinRange) ||
- distToOthersSquared < Math.pow(
- this.options.label.connectorNeighbourDistance,
- 2
- )
- )
- ) {
- for (j = 1; j < points.length; j += 1) {
- dist = Math.min(
- (
- Math.pow(x + bBox.width / 2 - points[j].chartX, 2) +
- Math.pow(y + bBox.height / 2 - points[j].chartY, 2)
- ),
- (
- Math.pow(x - points[j].chartX, 2) +
- Math.pow(y - points[j].chartY, 2)
- ),
- (
- Math.pow(x + bBox.width - points[j].chartX, 2) +
- Math.pow(y - points[j].chartY, 2)
- ),
- (
- Math.pow(x + bBox.width - points[j].chartX, 2) +
- Math.pow(y + bBox.height - points[j].chartY, 2)
- ),
- (
- Math.pow(x - points[j].chartX, 2) +
- Math.pow(y + bBox.height - points[j].chartY, 2)
- )
- );
- if (dist < distToPointSquared) {
- distToPointSquared = dist;
- connectorPoint = points[j];
- }
- }
- withinRange = true;
- }
- }
- }
- return !checkDistance || withinRange ? {
- x: x,
- y: y,
- weight: getWeight(
- distToOthersSquared,
- connectorPoint ? distToPointSquared : 0
- ),
- connectorPoint: connectorPoint
- } : false;
- };
- /**
- * The main initiator method that runs on chart level after initiation and
- * redraw. It runs in a timeout to prevent locking, and loops over all series,
- * taking all series and labels into account when placing the labels.
- *
- * @private
- * @function Highcharts.Chart#drawSeriesLabels
- */
- Chart.prototype.drawSeriesLabels = function () {
- // console.time('drawSeriesLabels');
- var chart = this,
- labelSeries = this.labelSeries;
- chart.boxesToAvoid = [];
- // Build the interpolated points
- labelSeries.forEach(function (series) {
- series.interpolatedPoints = series.getPointsOnGraph();
- (series.options.label.boxesToAvoid || []).forEach(function (box) {
- chart.boxesToAvoid.push(box);
- });
- });
- chart.series.forEach(function (series) {
- if (!series.xAxis && !series.yAxis) {
- return;
- }
- var bBox,
- x,
- y,
- results = [],
- clearPoint,
- i,
- best,
- labelOptions = series.options.label,
- inverted = chart.inverted,
- paneLeft = inverted ? series.yAxis.pos : series.xAxis.pos,
- paneTop = inverted ? series.xAxis.pos : series.yAxis.pos,
- paneWidth = chart.inverted ? series.yAxis.len : series.xAxis.len,
- paneHeight = chart.inverted ? series.xAxis.len : series.yAxis.len,
- points = series.interpolatedPoints,
- onArea = pick(labelOptions.onArea, !!series.area),
- label = series.labelBySeries,
- minFontSize = labelOptions.minFontSize,
- maxFontSize = labelOptions.maxFontSize;
- function insidePane(x, y, bBox) {
- return x > paneLeft && x <= paneLeft + paneWidth - bBox.width &&
- y >= paneTop && y <= paneTop + paneHeight - bBox.height;
- }
- function destroyLabel() {
- if (label) {
- series.labelBySeries = label.destroy();
- }
- }
- if (series.visible && !series.isSeriesBoosting && points) {
- if (!label) {
- series.labelBySeries = label = chart.renderer
- .label(series.name, 0, -9999, 'connector')
- .addClass(
- 'highcharts-series-label ' +
- 'highcharts-series-label-' + series.index + ' ' +
- (series.options.className || '')
- )
- .css(extend({
- color: onArea ?
- chart.renderer.getContrast(series.color) :
- series.color
- }, series.options.label.style));
- // Adapt label sizes to the sum of the data
- if (minFontSize && maxFontSize) {
- label.css({
- fontSize: series.labelFontSize(minFontSize, maxFontSize)
- });
- }
- label
- .attr({
- padding: 0,
- opacity: chart.renderer.forExport ? 1 : 0,
- stroke: series.color,
- 'stroke-width': 1,
- zIndex: 3
- })
- .add()
- .animate({ opacity: 1 }, { duration: 200 });
- }
- bBox = label.getBBox();
- bBox.width = Math.round(bBox.width);
- // Ideal positions are centered above or below a point on right side
- // of chart
- for (i = points.length - 1; i > 0; i -= 1) {
- if (onArea) {
- // Centered
- x = points[i].chartX - bBox.width / 2;
- y = points[i].chartCenterY - bBox.height / 2;
- if (insidePane(x, y, bBox)) {
- best = series.checkClearPoint(
- x,
- y,
- bBox
- );
- }
- if (best) {
- results.push(best);
- }
- } else {
- // Right - up
- x = points[i].chartX + labelDistance;
- y = points[i].chartY - bBox.height - labelDistance;
- if (insidePane(x, y, bBox)) {
- best = series.checkClearPoint(
- x,
- y,
- bBox,
- true
- );
- }
- if (best) {
- results.push(best);
- }
- // Right - down
- x = points[i].chartX + labelDistance;
- y = points[i].chartY + labelDistance;
- if (insidePane(x, y, bBox)) {
- best = series.checkClearPoint(
- x,
- y,
- bBox,
- true
- );
- }
- if (best) {
- results.push(best);
- }
- // Left - down
- x = points[i].chartX - bBox.width - labelDistance;
- y = points[i].chartY + labelDistance;
- if (insidePane(x, y, bBox)) {
- best = series.checkClearPoint(
- x,
- y,
- bBox,
- true
- );
- }
- if (best) {
- results.push(best);
- }
- // Left - up
- x = points[i].chartX - bBox.width - labelDistance;
- y = points[i].chartY - bBox.height - labelDistance;
- if (insidePane(x, y, bBox)) {
- best = series.checkClearPoint(
- x,
- y,
- bBox,
- true
- );
- }
- if (best) {
- results.push(best);
- }
- }
- }
- // Brute force, try all positions on the chart in a 16x16 grid
- if (labelOptions.connectorAllowed && !results.length && !onArea) {
- for (
- x = paneLeft + paneWidth - bBox.width;
- x >= paneLeft;
- x -= 16
- ) {
- for (
- y = paneTop;
- y < paneTop + paneHeight - bBox.height;
- y += 16
- ) {
- clearPoint = series.checkClearPoint(x, y, bBox, true);
- if (clearPoint) {
- results.push(clearPoint);
- }
- }
- }
- }
- if (results.length) {
- results.sort(function (a, b) {
- return b.weight - a.weight;
- });
- best = results[0];
- chart.boxesToAvoid.push({
- left: best.x,
- right: best.x + bBox.width,
- top: best.y,
- bottom: best.y + bBox.height
- });
- // Move it if needed
- var dist = Math.sqrt(
- Math.pow(Math.abs(best.x - label.x), 2),
- Math.pow(Math.abs(best.y - label.y), 2)
- );
- if (dist) {
- // Move fast and fade in - pure animation movement is
- // distractive...
- var attr = {
- opacity: chart.renderer.forExport ? 1 : 0,
- x: best.x,
- y: best.y
- },
- anim = {
- opacity: 1
- };
- // ... unless we're just moving a short distance
- if (dist <= 10) {
- anim = {
- x: attr.x,
- y: attr.y
- };
- attr = {};
- }
- series.labelBySeries
- .attr(extend(attr, {
- anchorX: best.connectorPoint &&
- best.connectorPoint.plotX + paneLeft,
- anchorY: best.connectorPoint &&
- best.connectorPoint.plotY + paneTop
- }))
- .animate(anim);
- // Record closest point to stick to for sync redraw
- series.options.kdNow = true;
- series.buildKDTree();
- var closest = series.searchPoint({
- chartX: best.x,
- chartY: best.y
- }, true);
- label.closest = [
- closest,
- best.x - closest.plotX,
- best.y - closest.plotY
- ];
- }
- } else {
- destroyLabel();
- }
- } else {
- destroyLabel();
- }
- });
- // console.timeEnd('drawSeriesLabels');
- };
- /**
- * Prepare drawing series labels.
- *
- * @private
- * @function drawLabels
- */
- function drawLabels(e) {
- var chart = this,
- delay = Math.max(
- H.animObject(chart.renderer.globalAnimation).duration,
- 250
- );
- chart.labelSeries = [];
- chart.labelSeriesMaxSum = 0;
- H.clearTimeout(chart.seriesLabelTimer);
- // Which series should have labels
- chart.series.forEach(function (series) {
- var options = series.options.label,
- label = series.labelBySeries,
- closest = label && label.closest;
- if (
- options.enabled &&
- series.visible &&
- (series.graph || series.area) &&
- !series.isSeriesBoosting
- ) {
- chart.labelSeries.push(series);
- if (options.minFontSize && options.maxFontSize) {
- series.sum = series.yData.reduce(function (pv, cv) {
- return (pv || 0) + (cv || 0);
- }, 0);
- chart.labelSeriesMaxSum = Math.max(
- chart.labelSeriesMaxSum,
- series.sum
- );
- }
- // The labels are processing heavy, wait until the animation is done
- if (e.type === 'load') {
- delay = Math.max(
- delay,
- H.animObject(series.options.animation).duration
- );
- }
- // Keep the position updated to the axis while redrawing
- if (closest) {
- if (closest[0].plotX !== undefined) {
- label.animate({
- x: closest[0].plotX + closest[1],
- y: closest[0].plotY + closest[2]
- });
- } else {
- label.attr({ opacity: 0 });
- }
- }
- }
- });
- chart.seriesLabelTimer = H.syncTimeout(function () {
- if (chart.series && chart.labelSeries) { // #7931, chart destroyed
- chart.drawSeriesLabels();
- }
- }, chart.renderer.forExport ? 0 : delay);
- }
- // Leave both events, we handle animation differently (#9815)
- addEvent(Chart, 'load', drawLabels);
- addEvent(Chart, 'redraw', drawLabels);
|