import $ from 'jquery'; import { textUtil, systemDateFormats, LegacyGraphHoverClearEvent, LegacyGraphHoverEvent, DataHoverClearEvent, } from '@grafana/data'; import { appEvents } from 'app/core/core'; import { CoreEvents } from 'app/types'; export default function GraphTooltip(this: any, elem: any, dashboard: any, scope: any, getSeriesFn: any) { const self = this; const ctrl = scope.ctrl; const panel = ctrl.panel; const hoverEvent = new LegacyGraphHoverEvent({ pos: {}, point: {}, panel: this.panel }); const $tooltip = $('
'); this.destroy = () => { $tooltip.remove(); }; this.findHoverIndexFromDataPoints = (posX: number, series: any, last: number) => { const ps = series.datapoints.pointsize; const initial = last * ps; const len = series.datapoints.points.length; let j; for (j = initial; j < len; j += ps) { // Special case of a non stepped line, highlight the very last point just before a null point if ( (!series.lines.steps && series.datapoints.points[initial] != null && series.datapoints.points[j] == null) || //normal case series.datapoints.points[j] > posX ) { return Math.max(j - ps, 0) / ps; } } return j / ps - 1; }; this.findHoverIndexFromData = (posX: any, series: any) => { let lower = 0; let upper = series.data.length - 1; let middle; while (true) { if (lower > upper) { return Math.max(upper, 0); } middle = Math.floor((lower + upper) / 2); if (series.data[middle][0] === posX) { return middle; } else if (series.data[middle][0] < posX) { lower = middle + 1; } else { upper = middle - 1; } } }; this.renderAndShow = (absoluteTime: string, innerHtml: string, pos: { pageX: number; pageY: any }, xMode: string) => { if (xMode === 'time') { innerHtml = '
' + absoluteTime + '
' + innerHtml; } $tooltip.html(innerHtml).place_tt(pos.pageX, pos.pageY, { offset: 10 }); }; this.getMultiSeriesPlotHoverInfo = function (seriesList: any[], pos: { x: number }) { let value, i, series, hoverIndex, hoverDistance, pointTime, yaxis; // 3 sub-arrays, 1st for hidden series, 2nd for left yaxis, 3rd for right yaxis. let results: any = [[], [], []]; //now we know the current X (j) position for X and Y values let lastValue = 0; //needed for stacked values let minDistance, minTime; for (i = 0; i < seriesList.length; i++) { series = seriesList[i]; if (!series.data.length || (panel.legend.hideEmpty && series.allIsNull)) { // Init value so that it does not brake series sorting results[0].push({ hidden: true, value: 0 }); continue; } if (!series.data.length || (panel.legend.hideZero && series.allIsZero)) { // Init value so that it does not brake series sorting results[0].push({ hidden: true, value: 0 }); continue; } if (series.hideTooltip) { results[0].push({ hidden: true, value: 0 }); continue; } hoverIndex = this.findHoverIndexFromData(pos.x, series); hoverDistance = pos.x - series.data[hoverIndex][0]; pointTime = series.data[hoverIndex][0]; // Take the closest point before the cursor, or if it does not exist, the closest after if ( !minDistance || (hoverDistance >= 0 && (hoverDistance < minDistance || minDistance < 0)) || (hoverDistance < 0 && hoverDistance > minDistance) ) { minDistance = hoverDistance; minTime = pointTime; } value = series.data[hoverIndex][1]; if (series.stack && value !== null && panel.tooltip.value_type !== 'individual') { lastValue += value; value = lastValue; } // Highlighting multiple Points depending on the plot type if (series.lines.steps || series.stack) { // stacked and steppedLine plots can have series with different length. // Stacked series can increase its length on each new stacked serie if null points found, // to speed the index search we begin always on the last found hoverIndex. hoverIndex = this.findHoverIndexFromDataPoints(pos.x, series, hoverIndex); } // Be sure we have a yaxis so that it does not brake series sorting yaxis = 0; if (series.yaxis) { yaxis = series.yaxis.n; } results[yaxis].push({ value: value, hoverIndex: hoverIndex, color: series.color, label: series.aliasEscaped, time: pointTime, distance: hoverDistance, index: i, }); } // Contat the 3 sub-arrays results = results[0].concat(results[1], results[2]); // Time of the point closer to pointer results.time = minTime; return results; }; elem.mouseleave(() => { if (panel.tooltip?.shared) { const plot = elem.data().plot; if (plot) { $tooltip.detach(); plot.unhighlight(); } } dashboard.events.publish(new LegacyGraphHoverClearEvent()); dashboard.events.publish(new DataHoverClearEvent()); }); elem.bind('plothover', (event: any, pos: { panelRelY: number; pageY: number }, item: any) => { self.show(pos, item); // broadcast to other graph panels that we are hovering! if (!dashboard.panelInEdit) { pos.panelRelY = (pos.pageY - elem.offset().top) / elem.height(); hoverEvent.payload.pos = pos; hoverEvent.payload.panel = panel; hoverEvent.payload.point['time'] = (pos as any).x; dashboard.events.publish(hoverEvent); } }); elem.bind('plotclick', (event: any, pos: any, item: any) => { appEvents.emit(CoreEvents.graphClicked, { pos: pos, panel: panel, item: item }); }); elem.bind('plotleave', () => { if (!panel.tooltip.shared) { return; } const plot = elem.data().plot; if (plot) { $tooltip.detach(); plot.unhighlight(); } }); this.clear = (plot: { clearCrosshair: () => void; unhighlight: () => void }) => { $tooltip.detach(); plot.clearCrosshair(); plot.unhighlight(); }; this.show = (pos: any, item: any) => { const plot = elem.data().plot; const plotData = plot.getData(); const xAxes = plot.getXAxes(); const xMode = xAxes[0].options.mode; const seriesList = getSeriesFn(); let allSeriesMode = panel.tooltip.shared; let group, value, absoluteTime, hoverInfo, i, series, seriesHtml, tooltipFormat; // if panelRelY is defined another panel wants us to show a tooltip // get pageX from position on x axis and pageY from relative position in original panel if (pos.panelRelY) { const pointOffset = plot.pointOffset({ x: pos.x }); if (Number.isNaN(pointOffset.left) || pointOffset.left < 0 || pointOffset.left > elem.width()) { self.clear(plot); return; } pos.pageX = elem.offset().left + pointOffset.left; pos.pageY = elem.offset().top + elem.height() * pos.panelRelY; const scrollTop = $(window).scrollTop() ?? 0; const isVisible = pos.pageY >= scrollTop && pos.pageY <= $(window).innerHeight()! + scrollTop; if (!isVisible) { self.clear(plot); return; } plot.setCrosshair(pos); allSeriesMode = true; if (dashboard.sharedCrosshairModeOnly()) { // if only crosshair mode we are done return; } } if (seriesList.length === 0) { return; } if (seriesList[0].hasMsResolution) { tooltipFormat = systemDateFormats.fullDateMS; } else { tooltipFormat = systemDateFormats.fullDate; } if (allSeriesMode) { plot.unhighlight(); const seriesHoverInfo = self.getMultiSeriesPlotHoverInfo(plotData, pos); seriesHtml = ''; absoluteTime = dashboard.formatDate(seriesHoverInfo.time, tooltipFormat); // Dynamically reorder the hovercard for the current time point if the // option is enabled. if (panel.tooltip.sort === 2) { seriesHoverInfo.sort((a: { value: number }, b: { value: number }) => { return b.value - a.value; }); } else if (panel.tooltip.sort === 1) { seriesHoverInfo.sort((a: { value: number }, b: { value: number }) => { return a.value - b.value; }); } for (i = 0; i < seriesHoverInfo.length; i++) { hoverInfo = seriesHoverInfo[i]; if (hoverInfo.hidden) { continue; } let highlightClass = ''; if (item && hoverInfo.index === item.seriesIndex) { highlightClass = 'graph-tooltip-list-item--highlight'; } series = seriesList[hoverInfo.index]; value = textUtil.sanitize(series.formatValue(hoverInfo.value)); const color = textUtil.sanitize(hoverInfo.color); const label = textUtil.sanitize(hoverInfo.label); seriesHtml += '
'; seriesHtml += ' ' + label + ':
'; seriesHtml += '
' + value + '
'; plot.highlight(hoverInfo.index, hoverInfo.hoverIndex); } self.renderAndShow(absoluteTime, seriesHtml, pos, xMode); } else if (item) { // single series tooltip const color = textUtil.sanitize(item.series.color); series = seriesList[item.seriesIndex]; group = '
'; group += ' ' + series.aliasEscaped + ':
'; if (panel.stack && panel.tooltip.value_type === 'individual') { value = item.datapoint[1] - item.datapoint[2]; } else { value = item.datapoint[1]; } value = textUtil.sanitize(series.formatValue(value)); absoluteTime = dashboard.formatDate(item.datapoint[0], tooltipFormat); group += '
' + value + '
'; self.renderAndShow(absoluteTime, group, pos, xMode); } else { // no hit $tooltip.detach(); } }; }