|
|
|
|
@ -1,12 +1,12 @@ |
|
|
|
|
import { css, cx } from '@emotion/css'; |
|
|
|
|
import React, { useLayoutEffect, useRef, useReducer, CSSProperties, useContext, useEffect } from 'react'; |
|
|
|
|
import React, { useLayoutEffect, useRef, useReducer, CSSProperties } from 'react'; |
|
|
|
|
import { createPortal } from 'react-dom'; |
|
|
|
|
import uPlot from 'uplot'; |
|
|
|
|
|
|
|
|
|
import { GrafanaTheme2 } from '@grafana/data'; |
|
|
|
|
|
|
|
|
|
import { useStyles2 } from '../../../themes'; |
|
|
|
|
import { LayoutItemContext } from '../../Layout/LayoutItemContext'; |
|
|
|
|
import { getPortalContainer } from '../../Portal/Portal'; |
|
|
|
|
import { UPlotConfigBuilder } from '../config/UPlotConfigBuilder'; |
|
|
|
|
|
|
|
|
|
import { CloseButton } from './CloseButton'; |
|
|
|
|
@ -29,6 +29,8 @@ interface TooltipPlugin2Props { |
|
|
|
|
config: UPlotConfigBuilder; |
|
|
|
|
hoverMode: TooltipHoverMode; |
|
|
|
|
|
|
|
|
|
syncTooltip?: () => boolean; |
|
|
|
|
|
|
|
|
|
// x only
|
|
|
|
|
queryZoom?: (range: { from: number; to: number }) => void; |
|
|
|
|
// y-only, via shiftKey
|
|
|
|
|
@ -80,14 +82,16 @@ function mergeState(prevState: TooltipContainerState, nextState: Partial<Tooltip |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const INITIAL_STATE: TooltipContainerState = { |
|
|
|
|
style: { transform: '', pointerEvents: 'none' }, |
|
|
|
|
isHovering: false, |
|
|
|
|
isPinned: false, |
|
|
|
|
contents: null, |
|
|
|
|
plot: null, |
|
|
|
|
dismiss: () => {}, |
|
|
|
|
}; |
|
|
|
|
function initState(): TooltipContainerState { |
|
|
|
|
return { |
|
|
|
|
style: { transform: '', pointerEvents: 'none' }, |
|
|
|
|
isHovering: false, |
|
|
|
|
isPinned: false, |
|
|
|
|
contents: null, |
|
|
|
|
plot: null, |
|
|
|
|
dismiss: () => {}, |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// min px width that triggers zoom
|
|
|
|
|
const MIN_ZOOM_DIST = 5; |
|
|
|
|
@ -105,13 +109,16 @@ export const TooltipPlugin2 = ({ |
|
|
|
|
queryZoom, |
|
|
|
|
maxWidth, |
|
|
|
|
maxHeight, |
|
|
|
|
syncTooltip = () => false, |
|
|
|
|
}: TooltipPlugin2Props) => { |
|
|
|
|
const domRef = useRef<HTMLDivElement>(null); |
|
|
|
|
const portalRoot = useRef<HTMLElement | null>(null); |
|
|
|
|
|
|
|
|
|
const [{ plot, isHovering, isPinned, contents, style, dismiss }, setState] = useReducer(mergeState, INITIAL_STATE); |
|
|
|
|
if (portalRoot.current == null) { |
|
|
|
|
portalRoot.current = getPortalContainer(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const { boostZIndex } = useContext(LayoutItemContext); |
|
|
|
|
useEffect(() => (isPinned ? boostZIndex() : undefined), [isPinned]); |
|
|
|
|
const [{ plot, isHovering, isPinned, contents, style, dismiss }, setState] = useReducer(mergeState, null, initState); |
|
|
|
|
|
|
|
|
|
const sizeRef = useRef<TooltipContainerSize>(); |
|
|
|
|
|
|
|
|
|
@ -150,20 +157,19 @@ export const TooltipPlugin2 = ({ |
|
|
|
|
let _isPinned = isPinned; |
|
|
|
|
let _style = style; |
|
|
|
|
|
|
|
|
|
let plotVisible = false; |
|
|
|
|
|
|
|
|
|
const updateHovering = () => { |
|
|
|
|
_isHovering = closestSeriesIdx != null || (hoverMode === TooltipHoverMode.xAll && _someSeriesIdx); |
|
|
|
|
if (viaSync) { |
|
|
|
|
_isHovering = plotVisible && _someSeriesIdx && syncTooltip(); |
|
|
|
|
} else { |
|
|
|
|
_isHovering = closestSeriesIdx != null || (hoverMode === TooltipHoverMode.xAll && _someSeriesIdx); |
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
let offsetX = 0; |
|
|
|
|
let offsetY = 0; |
|
|
|
|
|
|
|
|
|
let containRect = { |
|
|
|
|
lft: 0, |
|
|
|
|
top: 0, |
|
|
|
|
rgt: screen.width, |
|
|
|
|
btm: screen.height, |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
let selectedRange: TimeRange2 | null = null; |
|
|
|
|
let seriesIdxs: Array<number | null> = plot?.cursor.idxs!.slice()!; |
|
|
|
|
let closestSeriesIdx: number | null = null; |
|
|
|
|
@ -231,7 +237,6 @@ export const TooltipPlugin2 = ({ |
|
|
|
|
setState(state); |
|
|
|
|
|
|
|
|
|
selectedRange = null; |
|
|
|
|
viaSync = false; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
const dismiss = () => { |
|
|
|
|
@ -293,58 +298,11 @@ export const TooltipPlugin2 = ({ |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
const haltAncestorId = 'pageContent'; |
|
|
|
|
const scrollbarWidth = 16; |
|
|
|
|
|
|
|
|
|
// if we're in a container that can clip the tooltip, we should try to stay within that rather than window edges
|
|
|
|
|
u.over.addEventListener( |
|
|
|
|
'mouseenter', |
|
|
|
|
() => { |
|
|
|
|
// clamp to viewport bounds
|
|
|
|
|
let htmlEl = document.documentElement; |
|
|
|
|
let winWid = htmlEl.clientWidth - scrollbarWidth; |
|
|
|
|
let winHgt = htmlEl.clientHeight - scrollbarWidth; |
|
|
|
|
|
|
|
|
|
let lft = 0, |
|
|
|
|
top = 0, |
|
|
|
|
rgt = winWid, |
|
|
|
|
btm = winHgt; |
|
|
|
|
|
|
|
|
|
// find nearest scrollable container where overflow is not visible, (stop at #pageContent)
|
|
|
|
|
let par: HTMLElement | null = u.root; |
|
|
|
|
|
|
|
|
|
while (par != null && par.id !== haltAncestorId) { |
|
|
|
|
let style = getComputedStyle(par); |
|
|
|
|
let overflowX = style.getPropertyValue('overflow-x'); |
|
|
|
|
let overflowY = style.getPropertyValue('overflow-y'); |
|
|
|
|
|
|
|
|
|
if (overflowX !== 'visible' || overflowY !== 'visible') { |
|
|
|
|
let rect = par.getBoundingClientRect(); |
|
|
|
|
lft = Math.max(rect.x, lft); |
|
|
|
|
top = Math.max(rect.y, top); |
|
|
|
|
rgt = Math.min(lft + rect.width, rgt); |
|
|
|
|
btm = Math.min(top + rect.height, btm); |
|
|
|
|
break; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
par = par.parentElement; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
containRect.lft = lft; |
|
|
|
|
containRect.top = top; |
|
|
|
|
containRect.rgt = rgt; |
|
|
|
|
containRect.btm = btm; |
|
|
|
|
}, |
|
|
|
|
{ capture: true } |
|
|
|
|
); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
config.addHook('setSelect', (u) => { |
|
|
|
|
let e = u.cursor!.event; |
|
|
|
|
|
|
|
|
|
if (e != null && (clientZoom || queryZoom != null)) { |
|
|
|
|
if (maybeZoomAction(e)) { |
|
|
|
|
if (!viaSync && (clientZoom || queryZoom != null)) { |
|
|
|
|
if (maybeZoomAction(u.cursor!.event)) { |
|
|
|
|
if (clientZoom && yDrag) { |
|
|
|
|
if (u.select.height >= MIN_ZOOM_DIST) { |
|
|
|
|
for (let key in u.scales!) { |
|
|
|
|
@ -427,6 +385,8 @@ export const TooltipPlugin2 = ({ |
|
|
|
|
// TODO: we only need this for multi/all mode?
|
|
|
|
|
config.addHook('setSeries', (u, seriesIdx) => { |
|
|
|
|
closestSeriesIdx = seriesIdx; |
|
|
|
|
|
|
|
|
|
viaSync = u.cursor.event == null; |
|
|
|
|
updateHovering(); |
|
|
|
|
scheduleRender(); |
|
|
|
|
}); |
|
|
|
|
@ -436,78 +396,107 @@ export const TooltipPlugin2 = ({ |
|
|
|
|
seriesIdxs = _plot?.cursor!.idxs!.slice()!; |
|
|
|
|
_someSeriesIdx = seriesIdxs.some((v, i) => i > 0 && v != null); |
|
|
|
|
|
|
|
|
|
viaSync = u.cursor.event == null; |
|
|
|
|
updateHovering(); |
|
|
|
|
scheduleRender(); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
const scrollbarWidth = 16; |
|
|
|
|
let winWid = 0; |
|
|
|
|
let winHgt = 0; |
|
|
|
|
|
|
|
|
|
const updateWinSize = () => { |
|
|
|
|
_isHovering && !_isPinned && dismiss(); |
|
|
|
|
|
|
|
|
|
winWid = window.innerWidth - scrollbarWidth; |
|
|
|
|
winHgt = window.innerHeight - scrollbarWidth; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
const updatePlotVisible = () => { |
|
|
|
|
plotVisible = |
|
|
|
|
_plot!.rect.bottom <= winHgt && _plot!.rect.top >= 0 && _plot!.rect.left >= 0 && _plot!.rect.right <= winWid; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
updateWinSize(); |
|
|
|
|
config.addHook('ready', updatePlotVisible); |
|
|
|
|
|
|
|
|
|
// fires on mousemoves
|
|
|
|
|
config.addHook('setCursor', (u) => { |
|
|
|
|
let { left = -10, top = -10, event } = u.cursor; |
|
|
|
|
viaSync = u.cursor.event == null; |
|
|
|
|
|
|
|
|
|
if (!_isHovering) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let { left = -10, top = -10 } = u.cursor; |
|
|
|
|
|
|
|
|
|
if (left >= 0 || top >= 0) { |
|
|
|
|
viaSync = event == null; |
|
|
|
|
let clientX = u.rect.left + left; |
|
|
|
|
let clientY = u.rect.top + top; |
|
|
|
|
|
|
|
|
|
let transform = ''; |
|
|
|
|
|
|
|
|
|
// this means it's a synthetic event from uPlot's sync
|
|
|
|
|
if (viaSync) { |
|
|
|
|
// TODO: smarter positioning here to avoid viewport clipping?
|
|
|
|
|
transform = `translateX(${left}px) translateY(${u.rect.height / 2}px) translateY(-50%)`; |
|
|
|
|
} else { |
|
|
|
|
let { width, height } = sizeRef.current!; |
|
|
|
|
|
|
|
|
|
width += TOOLTIP_OFFSET; |
|
|
|
|
height += TOOLTIP_OFFSET; |
|
|
|
|
let { width, height } = sizeRef.current!; |
|
|
|
|
|
|
|
|
|
let clientX = u.rect.left + left; |
|
|
|
|
let clientY = u.rect.top + top; |
|
|
|
|
width += TOOLTIP_OFFSET; |
|
|
|
|
height += TOOLTIP_OFFSET; |
|
|
|
|
|
|
|
|
|
if (offsetY !== 0) { |
|
|
|
|
if (clientY + height < containRect.btm || clientY - height < 0) { |
|
|
|
|
offsetY = 0; |
|
|
|
|
} else if (offsetY !== -height) { |
|
|
|
|
offsetY = -height; |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
if (clientY + height > containRect.btm && clientY - height >= 0) { |
|
|
|
|
offsetY = -height; |
|
|
|
|
} |
|
|
|
|
if (offsetY !== 0) { |
|
|
|
|
if (clientY + height < winHgt || clientY - height < 0) { |
|
|
|
|
offsetY = 0; |
|
|
|
|
} else if (offsetY !== -height) { |
|
|
|
|
offsetY = -height; |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
if (clientY + height > winHgt && clientY - height >= 0) { |
|
|
|
|
offsetY = -height; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (offsetX !== 0) { |
|
|
|
|
if (clientX + width < containRect.rgt || clientX - width < 0) { |
|
|
|
|
offsetX = 0; |
|
|
|
|
} else if (offsetX !== -width) { |
|
|
|
|
offsetX = -width; |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
if (clientX + width > containRect.rgt && clientX - width >= 0) { |
|
|
|
|
offsetX = -width; |
|
|
|
|
} |
|
|
|
|
if (offsetX !== 0) { |
|
|
|
|
if (clientX + width < winWid || clientX - width < 0) { |
|
|
|
|
offsetX = 0; |
|
|
|
|
} else if (offsetX !== -width) { |
|
|
|
|
offsetX = -width; |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
if (clientX + width > winWid && clientX - width >= 0) { |
|
|
|
|
offsetX = -width; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const shiftX = left + (offsetX === 0 ? TOOLTIP_OFFSET : -TOOLTIP_OFFSET); |
|
|
|
|
const shiftY = top + (offsetY === 0 ? TOOLTIP_OFFSET : -TOOLTIP_OFFSET); |
|
|
|
|
const shiftX = clientX + (offsetX === 0 ? TOOLTIP_OFFSET : -TOOLTIP_OFFSET); |
|
|
|
|
const shiftY = clientY + (offsetY === 0 ? TOOLTIP_OFFSET : -TOOLTIP_OFFSET); |
|
|
|
|
|
|
|
|
|
const reflectX = offsetX === 0 ? '' : 'translateX(-100%)'; |
|
|
|
|
const reflectY = offsetY === 0 ? '' : 'translateY(-100%)'; |
|
|
|
|
const reflectX = offsetX === 0 ? '' : 'translateX(-100%)'; |
|
|
|
|
const reflectY = offsetY === 0 ? '' : 'translateY(-100%)'; |
|
|
|
|
|
|
|
|
|
// TODO: to a transition only when switching sides
|
|
|
|
|
// transition: transform 100ms;
|
|
|
|
|
// TODO: to a transition only when switching sides
|
|
|
|
|
// transition: transform 100ms;
|
|
|
|
|
|
|
|
|
|
transform = `translateX(${shiftX}px) ${reflectX} translateY(${shiftY}px) ${reflectY}`; |
|
|
|
|
} |
|
|
|
|
transform = `translateX(${shiftX}px) ${reflectX} translateY(${shiftY}px) ${reflectY}`; |
|
|
|
|
|
|
|
|
|
if (_isHovering) { |
|
|
|
|
if (domRef.current != null) { |
|
|
|
|
domRef.current.style.transform = transform; |
|
|
|
|
} else { |
|
|
|
|
_style.transform = transform; |
|
|
|
|
scheduleRender(); |
|
|
|
|
} |
|
|
|
|
if (domRef.current != null) { |
|
|
|
|
domRef.current.style.transform = transform; |
|
|
|
|
} else { |
|
|
|
|
_style.transform = transform; |
|
|
|
|
scheduleRender(); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
const onscroll = () => { |
|
|
|
|
updatePlotVisible(); |
|
|
|
|
_isHovering && !_isPinned && dismiss(); |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
window.addEventListener('resize', updateWinSize); |
|
|
|
|
window.addEventListener('scroll', onscroll, true); |
|
|
|
|
|
|
|
|
|
return () => { |
|
|
|
|
window.removeEventListener('resize', updateWinSize); |
|
|
|
|
window.removeEventListener('scroll', onscroll, true); |
|
|
|
|
}; |
|
|
|
|
}, [config]); |
|
|
|
|
|
|
|
|
|
useLayoutEffect(() => { |
|
|
|
|
@ -524,7 +513,7 @@ export const TooltipPlugin2 = ({ |
|
|
|
|
{isPinned && <CloseButton onClick={dismiss} />} |
|
|
|
|
{contents} |
|
|
|
|
</div>, |
|
|
|
|
plot.over |
|
|
|
|
portalRoot.current |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|