1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318 |
- /**
- * (c) 2010-2019 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- /**
- * A callback function to place the tooltip in a specific position.
- *
- * @callback Highcharts.TooltipPositionerCallbackFunction
- *
- * @param {number} labelWidth
- * Width of the tooltip.
- *
- * @param {number} labelHeight
- * Height of the tooltip.
- *
- * @param {Highcharts.TooltipPositionerPointObject} point
- * Point information for positioning a tooltip.
- *
- * @return {Highcharts.PositionObject}
- * New position for the tooltip.
- */
- /**
- * Point information for positioning a tooltip.
- *
- * @interface Highcharts.TooltipPositionerPointObject
- *//**
- * If `tooltip.split` option is enabled and positioner is called for each of the
- * boxes separately, this property indicates the call on the xAxis header, which
- * is not a point itself.
- * @name Highcharts.TooltipPositionerPointObject#isHeader
- * @type {boolean}
- *//**
- * @name Highcharts.TooltipPositionerPointObject#negative
- * @type {boolean}
- *//**
- * The reference point relative to the plot area. Add chart.plotLeft to get the
- * full coordinates.
- * @name Highcharts.TooltipPositionerPointObject#plotX
- * @type {number}
- *//**
- * The reference point relative to the plot area. Add chart.plotTop to get the
- * full coordinates.
- * @name Highcharts.TooltipPositionerPointObject#plotY
- * @type {number}
- */
- 'use strict';
- import H from './Globals.js';
- import './Utilities.js';
- var doc = H.doc,
- extend = H.extend,
- format = H.format,
- isNumber = H.isNumber,
- merge = H.merge,
- pick = H.pick,
- splat = H.splat,
- syncTimeout = H.syncTimeout,
- timeUnits = H.timeUnits;
- /**
- * Tooltip of a chart.
- *
- * @class
- * @name Highcharts.Tooltip
- *
- * @param {Highcharts.Chart} chart
- * The chart instance.
- *
- * @param {Highcharts.TooltipOptions} options
- * Tooltip options.
- */
- H.Tooltip = function () {
- this.init.apply(this, arguments);
- };
- H.Tooltip.prototype = {
- /**
- * @private
- * @function Highcharts.Tooltip#init
- *
- * @param {Highcharts.Chart} chart
- * The chart instance.
- *
- * @param {Highcharts.TooltipOptions} options
- * Tooltip options.
- */
- init: function (chart, options) {
- /**
- * Chart of the tooltip.
- *
- * @readonly
- * @name Highcharts.Tooltip#chart
- * @type {Highcharts.Chart}
- */
- this.chart = chart;
- /**
- * Used tooltip options.
- *
- * @readonly
- * @name Highcharts.Tooltip#options
- * @type {Highcharts.TooltipOptions}
- */
- this.options = options;
- /**
- * List of crosshairs.
- *
- * @private
- * @readonly
- * @name Highcharts.Tooltip#crosshairs
- * @type {Array<*>}
- */
- this.crosshairs = [];
- /**
- * Current values of x and y when animating.
- *
- * @private
- * @readonly
- * @name Highcharts.Tooltip#now
- * @type {*}
- */
- this.now = { x: 0, y: 0 };
- /**
- * Tooltips are initially hidden.
- *
- * @readonly
- * @name Highcharts.Tooltip#isHidden
- * @type {boolean}
- */
- this.isHidden = true;
- /**
- * True, if the tooltip is splitted into one label per series, with the
- * header close to the axis.
- *
- * @readonly
- * @name Highcharts.Tooltip#split
- * @type {boolean}
- */
- this.split = options.split && !chart.inverted;
- /**
- * When the tooltip is shared, the entire plot area will capture mouse
- * movement or touch events.
- *
- * @readonly
- * @name Highcharts.Tooltip#shared
- * @type {boolean}
- */
- this.shared = options.shared || this.split;
- /**
- * Whether to allow the tooltip to render outside the chart's SVG
- * element box. By default (false), the tooltip is rendered within the
- * chart's SVG element, which results in the tooltip being aligned
- * inside the chart area.
- *
- * @readonly
- * @name Highcharts.Tooltip#outside
- * @type {boolean}
- *
- * @todo
- * Split tooltip does not support outside in the first iteration. Should
- * not be too complicated to implement.
- */
- this.outside = options.outside && !this.split;
- },
- /**
- * Destroy the single tooltips in a split tooltip.
- * If the tooltip is active then it is not destroyed, unless forced to.
- *
- * @private
- * @function Highcharts.Tooltip#cleanSplit
- *
- * @param {boolean} force
- * Force destroy all tooltips.
- */
- cleanSplit: function (force) {
- this.chart.series.forEach(function (series) {
- var tt = series && series.tt;
- if (tt) {
- if (!tt.isActive || force) {
- series.tt = tt.destroy();
- } else {
- tt.isActive = false;
- }
- }
- });
- },
- /**
- * In styled mode, apply the default filter for the tooltip drop-shadow. It
- * needs to have an id specific to the chart, otherwise there will be issues
- * when one tooltip adopts the filter of a different chart, specifically one
- * where the container is hidden.
- *
- * @private
- * @function Highcharts.Tooltip#applyFilter
- */
- applyFilter: function () {
- var chart = this.chart;
- chart.renderer.definition({
- tagName: 'filter',
- id: 'drop-shadow-' + chart.index,
- opacity: 0.5,
- children: [{
- tagName: 'feGaussianBlur',
- 'in': 'SourceAlpha',
- stdDeviation: 1
- }, {
- tagName: 'feOffset',
- dx: 1,
- dy: 1
- }, {
- tagName: 'feComponentTransfer',
- children: [{
- tagName: 'feFuncA',
- type: 'linear',
- slope: 0.3
- }]
- }, {
- tagName: 'feMerge',
- children: [{
- tagName: 'feMergeNode'
- }, {
- tagName: 'feMergeNode',
- 'in': 'SourceGraphic'
- }]
- }]
- });
- chart.renderer.definition({
- tagName: 'style',
- textContent: '.highcharts-tooltip-' + chart.index + '{' +
- 'filter:url(#drop-shadow-' + chart.index + ')' +
- '}'
- });
- },
- /**
- * Creates the Tooltip label element if it does not exist, then returns it.
- *
- * @function Highcharts.Tooltip#getLabel
- *
- * @return {Highcharts.SVGElement}
- */
- getLabel: function () {
- var tooltip = this,
- renderer = this.chart.renderer,
- styledMode = this.chart.styledMode,
- options = this.options,
- container,
- set;
- if (!this.label) {
- if (this.outside) {
- this.container = container = H.doc.createElement('div');
- container.className = 'highcharts-tooltip-container';
- H.css(container, {
- position: 'absolute',
- top: '1px',
- pointerEvents: options.style && options.style.pointerEvents
- });
- H.doc.body.appendChild(container);
- this.renderer = renderer = new H.Renderer(container, 0, 0);
- }
- // Create the label
- if (this.split) {
- this.label = renderer.g('tooltip');
- } else {
- this.label = renderer
- .label(
- '',
- 0,
- 0,
- options.shape || 'callout',
- null,
- null,
- options.useHTML,
- null,
- 'tooltip'
- )
- .attr({
- padding: options.padding,
- r: options.borderRadius
- });
- if (!styledMode) {
- this.label
- .attr({
- 'fill': options.backgroundColor,
- 'stroke-width': options.borderWidth
- })
- // #2301, #2657
- .css(options.style)
- .shadow(options.shadow);
- }
- }
- if (styledMode) {
- // Apply the drop-shadow filter
- this.applyFilter();
- this.label.addClass('highcharts-tooltip-' + this.chart.index);
- }
- if (this.outside) {
- set = {
- x: this.label.xSetter,
- y: this.label.ySetter
- };
- this.label.xSetter = function (value, key) {
- set[key].call(this.label, tooltip.distance);
- container.style.left = value + 'px';
- };
- this.label.ySetter = function (value, key) {
- set[key].call(this.label, tooltip.distance);
- container.style.top = value + 'px';
- };
- }
- this.label
- .attr({
- zIndex: 8
- })
- .add();
- }
- return this.label;
- },
- /**
- * Updates the tooltip with the provided tooltip options.
- *
- * @function Highcharts.Tooltip#update
- *
- * @param {Highcharts.TooltipOptions} options
- * The tooltip options to update.
- */
- update: function (options) {
- this.destroy();
- // Update user options (#6218)
- merge(true, this.chart.options.tooltip.userOptions, options);
- this.init(this.chart, merge(true, this.options, options));
- },
- /**
- * Removes and destroys the tooltip and its elements.
- *
- * @function Highcharts.Tooltip#destroy
- */
- destroy: function () {
- // Destroy and clear local variables
- if (this.label) {
- this.label = this.label.destroy();
- }
- if (this.split && this.tt) {
- this.cleanSplit(this.chart, true);
- this.tt = this.tt.destroy();
- }
- if (this.renderer) {
- this.renderer = this.renderer.destroy();
- H.discardElement(this.container);
- }
- H.clearTimeout(this.hideTimer);
- H.clearTimeout(this.tooltipTimeout);
- },
- /**
- * Moves the tooltip with a soft animation to a new position.
- *
- * @function Highcharts.Tooltip#move
- *
- * @param {number} x
- *
- * @param {number} y
- *
- * @param {number} anchorX
- *
- * @param {number} anchorY
- */
- move: function (x, y, anchorX, anchorY) {
- var tooltip = this,
- now = tooltip.now,
- animate = tooltip.options.animation !== false &&
- !tooltip.isHidden &&
- // When we get close to the target position, abort animation and
- // land on the right place (#3056)
- (Math.abs(x - now.x) > 1 || Math.abs(y - now.y) > 1),
- skipAnchor = tooltip.followPointer || tooltip.len > 1;
- // Get intermediate values for animation
- extend(now, {
- x: animate ? (2 * now.x + x) / 3 : x,
- y: animate ? (now.y + y) / 2 : y,
- anchorX: skipAnchor ?
- undefined :
- animate ? (2 * now.anchorX + anchorX) / 3 : anchorX,
- anchorY: skipAnchor ?
- undefined :
- animate ? (now.anchorY + anchorY) / 2 : anchorY
- });
- // Move to the intermediate value
- tooltip.getLabel().attr(now);
- // Run on next tick of the mouse tracker
- if (animate) {
- // Never allow two timeouts
- H.clearTimeout(this.tooltipTimeout);
- // Set the fixed interval ticking for the smooth tooltip
- this.tooltipTimeout = setTimeout(function () {
- // The interval function may still be running during destroy,
- // so check that the chart is really there before calling.
- if (tooltip) {
- tooltip.move(x, y, anchorX, anchorY);
- }
- }, 32);
- }
- },
- /**
- * Hides the tooltip with a fade out animation.
- *
- * @function Highcharts.Tooltip#hide
- *
- * @param {number} [delay]
- * The fade out in milliseconds. If no value is provided the value
- * of the tooltip.hideDelay option is used. A value of 0 disables
- * the fade out animation.
- */
- hide: function (delay) {
- var tooltip = this;
- // disallow duplicate timers (#1728, #1766)
- H.clearTimeout(this.hideTimer);
- delay = pick(delay, this.options.hideDelay, 500);
- if (!this.isHidden) {
- this.hideTimer = syncTimeout(function () {
- tooltip.getLabel()[delay ? 'fadeOut' : 'hide']();
- tooltip.isHidden = true;
- }, delay);
- }
- },
- /**
- * Extendable method to get the anchor position of the tooltip
- * from a point or set of points
- *
- * @private
- * @function Highcharts.Tooltip#getAnchor
- *
- * @param {Array<Highchart.Points>} points
- *
- * @param {global.Event} [mouseEvent]
- */
- getAnchor: function (points, mouseEvent) {
- var ret,
- chart = this.chart,
- pointer = chart.pointer,
- inverted = chart.inverted,
- plotTop = chart.plotTop,
- plotLeft = chart.plotLeft,
- plotX = 0,
- plotY = 0,
- yAxis,
- xAxis;
- points = splat(points);
- // When tooltip follows mouse, relate the position to the mouse
- if (this.followPointer && mouseEvent) {
- if (mouseEvent.chartX === undefined) {
- mouseEvent = pointer.normalize(mouseEvent);
- }
- ret = [
- mouseEvent.chartX - chart.plotLeft,
- mouseEvent.chartY - plotTop
- ];
- // Pie uses a special tooltipPos
- } else if (points[0].tooltipPos) {
- ret = points[0].tooltipPos;
- // When shared, use the average position
- } else {
- points.forEach(function (point) {
- yAxis = point.series.yAxis;
- xAxis = point.series.xAxis;
- plotX += point.plotX +
- (!inverted && xAxis ? xAxis.left - plotLeft : 0);
- plotY +=
- (
- point.plotLow ?
- (point.plotLow + point.plotHigh) / 2 :
- point.plotY
- ) +
- (!inverted && yAxis ? yAxis.top - plotTop : 0); // #1151
- });
- plotX /= points.length;
- plotY /= points.length;
- ret = [
- inverted ? chart.plotWidth - plotY : plotX,
- this.shared && !inverted && points.length > 1 && mouseEvent ?
- // place shared tooltip next to the mouse (#424)
- mouseEvent.chartY - plotTop :
- inverted ? chart.plotHeight - plotX : plotY
- ];
- }
- return ret.map(Math.round);
- },
- /**
- * Place the tooltip in a chart without spilling over
- * and not covering the point it self.
- *
- * @private
- * @function Highcharts.Tooltip#getPosition
- *
- * @param {number} boxWidth
- *
- * @param {number} boxHeight
- *
- * @param {Highcharts.Point} point
- *
- * @return {*}
- */
- getPosition: function (boxWidth, boxHeight, point) {
- var chart = this.chart,
- distance = this.distance,
- ret = {},
- // Don't use h if chart isn't inverted (#7242)
- h = (chart.inverted && point.h) || 0, // #4117
- swapped,
- outside = this.outside,
- outerWidth = outside ?
- // substract distance to prevent scrollbars
- doc.documentElement.clientWidth - 2 * distance :
- chart.chartWidth,
- outerHeight = outside ?
- Math.max(
- doc.body.scrollHeight,
- doc.documentElement.scrollHeight,
- doc.body.offsetHeight,
- doc.documentElement.offsetHeight,
- doc.documentElement.clientHeight
- ) :
- chart.chartHeight,
- chartPosition = chart.pointer.chartPosition,
- first = [
- 'y',
- outerHeight,
- boxHeight,
- (outside ? chartPosition.top - distance : 0) +
- point.plotY + chart.plotTop,
- outside ? 0 : chart.plotTop,
- outside ? outerHeight : chart.plotTop + chart.plotHeight
- ],
- second = [
- 'x',
- outerWidth,
- boxWidth,
- (outside ? chartPosition.left - distance : 0) +
- point.plotX + chart.plotLeft,
- outside ? 0 : chart.plotLeft,
- outside ? outerWidth : chart.plotLeft + chart.plotWidth
- ],
- // The far side is right or bottom
- preferFarSide = !this.followPointer && pick(
- point.ttBelow,
- !chart.inverted === !!point.negative
- ), // #4984
- /*
- * Handle the preferred dimension. When the preferred dimension is
- * tooltip on top or bottom of the point, it will look for space
- * there.
- *
- * @private
- */
- firstDimension = function (
- dim,
- outerSize,
- innerSize,
- point,
- min,
- max
- ) {
- var roomLeft = innerSize < point - distance,
- roomRight = point + distance + innerSize < outerSize,
- alignedLeft = point - distance - innerSize,
- alignedRight = point + distance;
- if (preferFarSide && roomRight) {
- ret[dim] = alignedRight;
- } else if (!preferFarSide && roomLeft) {
- ret[dim] = alignedLeft;
- } else if (roomLeft) {
- ret[dim] = Math.min(
- max - innerSize,
- alignedLeft - h < 0 ? alignedLeft : alignedLeft - h
- );
- } else if (roomRight) {
- ret[dim] = Math.max(
- min,
- alignedRight + h + innerSize > outerSize ?
- alignedRight :
- alignedRight + h
- );
- } else {
- return false;
- }
- },
- /*
- * Handle the secondary dimension. If the preferred dimension is
- * tooltip on top or bottom of the point, the second dimension is to
- * align the tooltip above the point, trying to align center but
- * allowing left or right align within the chart box.
- *
- * @private
- */
- secondDimension = function (dim, outerSize, innerSize, point) {
- var retVal;
- // Too close to the edge, return false and swap dimensions
- if (point < distance || point > outerSize - distance) {
- retVal = false;
- // Align left/top
- } else if (point < innerSize / 2) {
- ret[dim] = 1;
- // Align right/bottom
- } else if (point > outerSize - innerSize / 2) {
- ret[dim] = outerSize - innerSize - 2;
- // Align center
- } else {
- ret[dim] = point - innerSize / 2;
- }
- return retVal;
- },
- /*
- * Swap the dimensions
- */
- swap = function (count) {
- var temp = first;
- first = second;
- second = temp;
- swapped = count;
- },
- run = function () {
- if (firstDimension.apply(0, first) !== false) {
- if (
- secondDimension.apply(0, second) === false &&
- !swapped
- ) {
- swap(true);
- run();
- }
- } else if (!swapped) {
- swap(true);
- run();
- } else {
- ret.x = ret.y = 0;
- }
- };
- // Under these conditions, prefer the tooltip on the side of the point
- if (chart.inverted || this.len > 1) {
- swap();
- }
- run();
- return ret;
- },
- /**
- * In case no user defined formatter is given, this will be used. Note that
- * the context here is an object holding point, series, x, y etc.
- *
- * @private
- * @function Highcharts.Tooltip#defaultFormatter
- *
- * @param {Highcharts.Tooltip} tooltip
- *
- * @return {Array<string>}
- */
- defaultFormatter: function (tooltip) {
- var items = this.points || splat(this),
- s;
- // Build the header
- s = [tooltip.tooltipFooterHeaderFormatter(items[0])];
- // build the values
- s = s.concat(tooltip.bodyFormatter(items));
- // footer
- s.push(tooltip.tooltipFooterHeaderFormatter(items[0], true));
- return s;
- },
- /**
- * Refresh the tooltip's text and position.
- *
- * @function Highcharts.Tooltip#refresh
- *
- * @param {Highcharts.Point|Array<Highcharts.Point>} pointOrPoints
- * Either a point or an array of points.
- *
- * @param {global.Event} [mouseEvent]
- * Mouse event, that is responsible for the refresh and should be
- * used for the tooltip update.
- */
- refresh: function (pointOrPoints, mouseEvent) {
- var tooltip = this,
- label,
- options = tooltip.options,
- x,
- y,
- point = pointOrPoints,
- anchor,
- textConfig = {},
- text,
- pointConfig = [],
- formatter = options.formatter || tooltip.defaultFormatter,
- shared = tooltip.shared,
- currentSeries,
- styledMode = this.chart.styledMode;
- if (!options.enabled) {
- return;
- }
- H.clearTimeout(this.hideTimer);
- // get the reference point coordinates (pie charts use tooltipPos)
- tooltip.followPointer = splat(point)[0].series.tooltipOptions
- .followPointer;
- anchor = tooltip.getAnchor(point, mouseEvent);
- x = anchor[0];
- y = anchor[1];
- // shared tooltip, array is sent over
- if (shared && !(point.series && point.series.noSharedTooltip)) {
- point.forEach(function (item) {
- item.setState('hover');
- pointConfig.push(item.getLabelConfig());
- });
- textConfig = {
- x: point[0].category,
- y: point[0].y
- };
- textConfig.points = pointConfig;
- point = point[0];
- // single point tooltip
- } else {
- textConfig = point.getLabelConfig();
- }
- this.len = pointConfig.length; // #6128
- text = formatter.call(textConfig, tooltip);
- // register the current series
- currentSeries = point.series;
- this.distance = pick(currentSeries.tooltipOptions.distance, 16);
- // update the inner HTML
- if (text === false) {
- this.hide();
- } else {
- label = tooltip.getLabel();
- // show it
- if (tooltip.isHidden) {
- label.attr({
- opacity: 1
- }).show();
- }
- // update text
- if (tooltip.split) {
- this.renderSplit(text, splat(pointOrPoints));
- } else {
- // Prevent the tooltip from flowing over the chart box (#6659)
- if (!options.style.width || styledMode) {
- label.css({
- width: this.chart.spacingBox.width
- });
- }
- label.attr({
- text: text && text.join ? text.join('') : text
- });
- // Set the stroke color of the box to reflect the point
- label.removeClass(/highcharts-color-[\d]+/g)
- .addClass(
- 'highcharts-color-' +
- pick(point.colorIndex, currentSeries.colorIndex)
- );
- if (!styledMode) {
- label.attr({
- stroke: (
- options.borderColor ||
- point.color ||
- currentSeries.color ||
- '#666666'
- )
- });
- }
- tooltip.updatePosition({
- plotX: x,
- plotY: y,
- negative: point.negative,
- ttBelow: point.ttBelow,
- h: anchor[2] || 0
- });
- }
- this.isHidden = false;
- }
- },
- /**
- * Render the split tooltip. Loops over each point's text and adds
- * a label next to the point, then uses the distribute function to
- * find best non-overlapping positions.
- *
- * @private
- * @function Highcharts.Tooltip#renderSplit
- *
- * @param {Array<Highcharts.Label>} labels
- *
- * @param {Array<Highcharts.Point>} points
- */
- renderSplit: function (labels, points) {
- var tooltip = this,
- boxes = [],
- chart = this.chart,
- ren = chart.renderer,
- rightAligned = true,
- options = this.options,
- headerHeight = 0,
- headerTop,
- tooltipLabel = this.getLabel(),
- distributionBoxTop = chart.plotTop;
- // Graceful degradation for legacy formatters
- if (H.isString(labels)) {
- labels = [false, labels];
- }
- // Create the individual labels for header and points, ignore footer
- labels.slice(0, points.length + 1).forEach(function (str, i) {
- if (str !== false && str !== '') {
- var point = points[i - 1] ||
- {
- // Item 0 is the header. Instead of this, we could also
- // use the crosshair label
- isHeader: true,
- plotX: points[0].plotX,
- plotY: chart.plotHeight
- },
- owner = point.series || tooltip,
- tt = owner.tt,
- series = point.series || {},
- colorClass = 'highcharts-color-' + pick(
- point.colorIndex,
- series.colorIndex,
- 'none'
- ),
- target,
- x,
- bBox,
- boxWidth,
- attribs;
- // Store the tooltip referance on the series
- if (!tt) {
- attribs = {
- padding: options.padding,
- r: options.borderRadius
- };
- if (!chart.styledMode) {
- attribs.fill = options.backgroundColor;
- attribs.stroke = (
- options.borderColor ||
- point.color ||
- series.color ||
- '#333333'
- );
- attribs['stroke-width'] = options.borderWidth;
- }
- owner.tt = tt = ren
- .label(
- null,
- null,
- null,
- (
- point.isHeader ? options.headerShape :
- options.shape
- ) || 'callout',
- null,
- null,
- options.useHTML
- )
- .addClass('highcharts-tooltip-box ' + colorClass)
- .attr(attribs)
- .add(tooltipLabel);
- }
- tt.isActive = true;
- tt.attr({
- text: str
- });
- if (!chart.styledMode) {
- tt.css(options.style)
- .shadow(options.shadow);
- }
- // Get X position now, so we can move all to the other side in
- // case of overflow
- bBox = tt.getBBox();
- boxWidth = bBox.width + tt.strokeWidth();
- if (point.isHeader) {
- headerHeight = bBox.height;
- if (chart.xAxis[0].opposite) {
- headerTop = true;
- distributionBoxTop -= headerHeight;
- }
- x = Math.max(
- 0, // No left overflow
- Math.min(
- point.plotX + chart.plotLeft - boxWidth / 2,
- // No right overflow (#5794)
- chart.chartWidth +
- (
- // Scrollable plot area
- chart.scrollablePixels ?
- chart.scrollablePixels - chart.marginRight :
- 0
- ) -
- boxWidth
- )
- );
- } else {
- x = point.plotX + chart.plotLeft -
- pick(options.distance, 16) - boxWidth;
- }
- // If overflow left, we don't use this x in the next loop
- if (x < 0) {
- rightAligned = false;
- }
- // Prepare for distribution
- target = (point.series && point.series.yAxis &&
- point.series.yAxis.pos) + (point.plotY || 0);
- target -= distributionBoxTop;
- if (point.isHeader) {
- target = headerTop ?
- -headerHeight :
- chart.plotHeight + headerHeight;
- }
- boxes.push({
- target: target,
- rank: point.isHeader ? 1 : 0,
- size: owner.tt.getBBox().height + 1,
- point: point,
- x: x,
- tt: tt
- });
- }
- });
- // Clean previous run (for missing points)
- this.cleanSplit();
- if (options.positioner) {
- boxes.forEach(function (box) {
- var boxPosition = options.positioner.call(
- tooltip,
- box.tt.getBBox().width,
- box.size,
- box.point
- );
- box.x = boxPosition.x;
- box.align = 0; // 0-align to the top, 1-align to the bottom
- box.target = boxPosition.y;
- box.rank = pick(boxPosition.rank, box.rank);
- });
- }
- // Distribute and put in place
- H.distribute(boxes, chart.plotHeight + headerHeight);
- boxes.forEach(function (box) {
- var point = box.point,
- series = point.series;
- // Put the label in place
- box.tt.attr({
- visibility: box.pos === undefined ? 'hidden' : 'inherit',
- x: (rightAligned || point.isHeader || options.positioner ?
- box.x :
- point.plotX + chart.plotLeft + tooltip.distance),
- y: box.pos + distributionBoxTop,
- anchorX: point.isHeader ?
- point.plotX + chart.plotLeft :
- point.plotX + series.xAxis.pos,
- anchorY: point.isHeader ?
- chart.plotTop + chart.plotHeight / 2 :
- point.plotY + series.yAxis.pos
- });
- });
- },
- /**
- * Find the new position and perform the move
- *
- * @private
- * @function Highcharts.Tooltip#updatePosition
- *
- * @param {Highcharts.Point} point
- */
- updatePosition: function (point) {
- var chart = this.chart,
- label = this.getLabel(),
- pos = (this.options.positioner || this.getPosition).call(
- this,
- label.width,
- label.height,
- point
- ),
- anchorX = point.plotX + chart.plotLeft,
- anchorY = point.plotY + chart.plotTop,
- pad;
- // Set the renderer size dynamically to prevent document size to change
- if (this.outside) {
- pad = (this.options.borderWidth || 0) + 2 * this.distance;
- this.renderer.setSize(
- label.width + pad,
- label.height + pad,
- false
- );
- anchorX += chart.pointer.chartPosition.left - pos.x;
- anchorY += chart.pointer.chartPosition.top - pos.y;
- }
- // do the move
- this.move(
- Math.round(pos.x),
- Math.round(pos.y || 0), // can be undefined (#3977)
- anchorX,
- anchorY
- );
- },
- /**
- * Get the optimal date format for a point, based on a range.
- *
- * @private
- * @function Highcharts.Tooltip#getDateFormat
- *
- * @param {number} range
- * The time range
- *
- * @param {number|Date} date
- * The date of the point in question
- *
- * @param {number} startOfWeek
- * An integer representing the first day of the week, where 0 is
- * Sunday.
- *
- * @param {Highcharts.Dictionary<string>} dateTimeLabelFormats
- * A map of time units to formats.
- *
- * @return {string}
- * The optimal date format for a point.
- */
- getDateFormat: function (range, date, startOfWeek, dateTimeLabelFormats) {
- var time = this.chart.time,
- dateStr = time.dateFormat('%m-%d %H:%M:%S.%L', date),
- format,
- n,
- blank = '01-01 00:00:00.000',
- strpos = {
- millisecond: 15,
- second: 12,
- minute: 9,
- hour: 6,
- day: 3
- },
- lastN = 'millisecond'; // for sub-millisecond data, #4223
- for (n in timeUnits) {
- // If the range is exactly one week and we're looking at a
- // Sunday/Monday, go for the week format
- if (
- range === timeUnits.week &&
- +time.dateFormat('%w', date) === startOfWeek &&
- dateStr.substr(6) === blank.substr(6)
- ) {
- n = 'week';
- break;
- }
- // The first format that is too great for the range
- if (timeUnits[n] > range) {
- n = lastN;
- break;
- }
- // If the point is placed every day at 23:59, we need to show
- // the minutes as well. #2637.
- if (
- strpos[n] &&
- dateStr.substr(strpos[n]) !== blank.substr(strpos[n])
- ) {
- break;
- }
- // Weeks are outside the hierarchy, only apply them on
- // Mondays/Sundays like in the first condition
- if (n !== 'week') {
- lastN = n;
- }
- }
- if (n) {
- format = time.resolveDTLFormat(dateTimeLabelFormats[n]).main;
- }
- return format;
- },
- /**
- * Get the best X date format based on the closest point range on the axis.
- *
- * @private
- * @function Highcharts.Tooltip#getXDateFormat
- *
- * @param {Highcharts.Point} point
- *
- * @param {Highcharts.TooltipOptions} options
- *
- * @param {Highcharts.Axis} xAxis
- *
- * @return {string}
- */
- getXDateFormat: function (point, options, xAxis) {
- var xDateFormat,
- dateTimeLabelFormats = options.dateTimeLabelFormats,
- closestPointRange = xAxis && xAxis.closestPointRange;
- if (closestPointRange) {
- xDateFormat = this.getDateFormat(
- closestPointRange,
- point.x,
- xAxis.options.startOfWeek,
- dateTimeLabelFormats
- );
- } else {
- xDateFormat = dateTimeLabelFormats.day;
- }
- return xDateFormat || dateTimeLabelFormats.year; // #2546, 2581
- },
- /**
- * Format the footer/header of the tooltip
- * #3397: abstraction to enable formatting of footer and header
- *
- * @private
- * @function Highcharts.Tooltip#tooltipFooterHeaderFormatter
- *
- * @param {*} labelConfig
- *
- * @param {boolean} isFooter
- *
- * @return {string}
- */
- tooltipFooterHeaderFormatter: function (labelConfig, isFooter) {
- var footOrHead = isFooter ? 'footer' : 'header',
- series = labelConfig.series,
- tooltipOptions = series.tooltipOptions,
- xDateFormat = tooltipOptions.xDateFormat,
- xAxis = series.xAxis,
- isDateTime = (
- xAxis &&
- xAxis.options.type === 'datetime' &&
- isNumber(labelConfig.key)
- ),
- formatString = tooltipOptions[footOrHead + 'Format'],
- evt = { isFooter: isFooter, labelConfig: labelConfig };
- H.fireEvent(this, 'headerFormatter', evt, function (e) {
- // Guess the best date format based on the closest point distance
- // (#568, #3418)
- if (isDateTime && !xDateFormat) {
- xDateFormat = this.getXDateFormat(
- labelConfig,
- tooltipOptions,
- xAxis
- );
- }
- // Insert the footer date format if any
- if (isDateTime && xDateFormat) {
- ((labelConfig.point && labelConfig.point.tooltipDateKeys) ||
- ['key']).forEach(
- function (key) {
- formatString = formatString.replace(
- '{point.' + key + '}',
- '{point.' + key + ':' + xDateFormat + '}'
- );
- }
- );
- }
- // Replace default header style with class name
- if (series.chart.styledMode) {
- formatString = this.styledModeFormat(formatString);
- }
- e.text = format(formatString, {
- point: labelConfig,
- series: series
- }, this.chart.time);
- });
- return evt.text;
- },
- /**
- * Build the body (lines) of the tooltip by iterating over the items and
- * returning one entry for each item, abstracting this functionality allows
- * to easily overwrite and extend it.
- *
- * @private
- * @function Highcharts.Tooltip#bodyFormatter
- *
- * @param {Array<Highcharts.Point>} items
- *
- * @return {string}
- */
- bodyFormatter: function (items) {
- return items.map(function (item) {
- var tooltipOptions = item.series.tooltipOptions;
- return (
- tooltipOptions[
- (item.point.formatPrefix || 'point') + 'Formatter'
- ] ||
- item.point.tooltipFormatter
- ).call(
- item.point,
- tooltipOptions[
- (item.point.formatPrefix || 'point') + 'Format'
- ] || ''
- );
- });
- },
- styledModeFormat: function (formatString) {
- return formatString
- .replace(
- 'style="font-size: 10px"',
- 'class="highcharts-header"'
- )
- .replace(
- /style="color:{(point|series)\.color}"/g,
- 'class="highcharts-color-{$1.colorIndex}"'
- );
- }
- };
|