mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-21 14:44:00 +00:00
vmui: sticky tooltip (#3376)
* feat: add ability to make tooltip "sticky" * vmui: add ability to make tooltip "sticky"
This commit is contained in:
parent
5c65b3c7dc
commit
05712cfc8d
10 changed files with 372 additions and 120 deletions
|
@ -0,0 +1,181 @@
|
|||
import React, { FC, useEffect, useMemo, useRef, useState } from "preact/compat";
|
||||
import uPlot, { Series } from "uplot";
|
||||
import { MetricResult } from "../../../api/types";
|
||||
import { formatPrettyNumber, getColorLine, getLegendLabel } from "../../../utils/uplot/helpers";
|
||||
import dayjs from "dayjs";
|
||||
import { DATE_FULL_TIMEZONE_FORMAT } from "../../../constants/date";
|
||||
import ReactDOM from "react-dom";
|
||||
import get from "lodash.get";
|
||||
import Button from "../../Main/Button/Button";
|
||||
import { CloseIcon, DragIcon } from "../../Main/Icons";
|
||||
import classNames from "classnames";
|
||||
import { MouseEvent as ReactMouseEvent } from "react";
|
||||
import "./style.scss";
|
||||
|
||||
export interface ChartTooltipProps {
|
||||
id: string,
|
||||
u: uPlot,
|
||||
metrics: MetricResult[],
|
||||
series: Series[],
|
||||
unit?: string,
|
||||
isSticky?: boolean,
|
||||
tooltipOffset: { left: number, top: number },
|
||||
tooltipIdx: { seriesIdx: number, dataIdx: number },
|
||||
onClose?: (id: string) => void
|
||||
}
|
||||
|
||||
const ChartTooltip: FC<ChartTooltipProps> = ({
|
||||
u,
|
||||
id,
|
||||
unit = "",
|
||||
metrics,
|
||||
series,
|
||||
tooltipIdx,
|
||||
tooltipOffset,
|
||||
isSticky,
|
||||
onClose
|
||||
}) => {
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [position, setPosition] = useState({ top: -999, left: -999 });
|
||||
const [moving, setMoving] = useState(false);
|
||||
const [moved, setMoved] = useState(false);
|
||||
|
||||
const [seriesIdx, setSeriesIdx] = useState(tooltipIdx.seriesIdx);
|
||||
const [dataIdx, setDataIdx] = useState(tooltipIdx.dataIdx);
|
||||
|
||||
const targetPortal = useMemo(() => u.root.querySelector(".u-wrap"), [u]);
|
||||
|
||||
const value = useMemo(() => get(u, ["data", seriesIdx, dataIdx], 0), [u, seriesIdx, dataIdx]);
|
||||
const valueFormat = useMemo(() => formatPrettyNumber(value), [value]);
|
||||
const dataTime = useMemo(() => u.data[0][dataIdx], [u, dataIdx]);
|
||||
const date = useMemo(() => dayjs(new Date(dataTime * 1000)).format(DATE_FULL_TIMEZONE_FORMAT), [dataTime]);
|
||||
|
||||
const color = useMemo(() => getColorLine(series[seriesIdx]?.label || ""), [series, seriesIdx]);
|
||||
|
||||
const name = useMemo(() => {
|
||||
const metricName = (series[seriesIdx]?.label || "").replace(/{.+}/gmi, "").trim();
|
||||
return getLegendLabel(metricName);
|
||||
}, []);
|
||||
|
||||
const fields = useMemo(() => {
|
||||
const metric = metrics[seriesIdx - 1]?.metric || {};
|
||||
const fields = Object.keys(metric).filter(k => k !== "__name__");
|
||||
return fields.map(key => `${key}="${metric[key]}"`);
|
||||
}, [metrics, seriesIdx]);
|
||||
|
||||
const handleClose = () => {
|
||||
onClose && onClose(id);
|
||||
};
|
||||
|
||||
const handleMouseDown = (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
setMoved(true);
|
||||
setMoving(true);
|
||||
const { clientX, clientY } = e;
|
||||
setPosition({ top: clientY, left: clientX });
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!moving) return;
|
||||
const { clientX, clientY } = e;
|
||||
setPosition({ top: clientY, left: clientX });
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setMoving(false);
|
||||
};
|
||||
|
||||
const calcPosition = () => {
|
||||
if (!tooltipRef.current) return;
|
||||
|
||||
const topOnChart = u.valToPos((value || 0), series[seriesIdx]?.scale || "1");
|
||||
const leftOnChart = u.valToPos(dataTime, "x");
|
||||
const { width: tooltipWidth, height: tooltipHeight } = tooltipRef.current.getBoundingClientRect();
|
||||
const { width, height } = u.over.getBoundingClientRect();
|
||||
|
||||
const margin = 10;
|
||||
const overflowX = leftOnChart + tooltipWidth >= width ? tooltipWidth + (2 * margin) : 0;
|
||||
const overflowY = topOnChart + tooltipHeight >= height ? tooltipHeight + (2 * margin) : 0;
|
||||
|
||||
setPosition({
|
||||
top: topOnChart + tooltipOffset.top + margin - overflowY,
|
||||
left: leftOnChart + tooltipOffset.left + margin - overflowX
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(calcPosition, [u, value, dataTime, seriesIdx, tooltipOffset, tooltipRef]);
|
||||
|
||||
useEffect(() => {
|
||||
setSeriesIdx(tooltipIdx.seriesIdx);
|
||||
setDataIdx(tooltipIdx.dataIdx);
|
||||
}, [tooltipIdx]);
|
||||
|
||||
useEffect(() => {
|
||||
if (moving) {
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
};
|
||||
}, [moving]);
|
||||
|
||||
if (!targetPortal || tooltipIdx.seriesIdx < 0 || tooltipIdx.dataIdx < 0) return null;
|
||||
|
||||
return ReactDOM.createPortal((
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-chart-tooltip": true,
|
||||
"vm-chart-tooltip_sticky": isSticky,
|
||||
"vm-chart-tooltip_moved": moved
|
||||
|
||||
})}
|
||||
ref={tooltipRef}
|
||||
style={position}
|
||||
>
|
||||
<div className="vm-chart-tooltip-header">
|
||||
<div className="vm-chart-tooltip-header__date">{date}</div>
|
||||
{isSticky && (
|
||||
<>
|
||||
<Button
|
||||
className="vm-chart-tooltip-header__drag"
|
||||
variant="text"
|
||||
size="small"
|
||||
startIcon={<DragIcon/>}
|
||||
onMouseDown={handleMouseDown}
|
||||
/>
|
||||
<Button
|
||||
className="vm-chart-tooltip-header__close"
|
||||
variant="text"
|
||||
size="small"
|
||||
startIcon={<CloseIcon/>}
|
||||
onClick={handleClose}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="vm-chart-tooltip-data">
|
||||
<div
|
||||
className="vm-chart-tooltip-data__marker"
|
||||
style={{ background: color }}
|
||||
/>
|
||||
<p>
|
||||
{name}:
|
||||
<b className="vm-chart-tooltip-data__value">{valueFormat}</b>
|
||||
{unit}
|
||||
</p>
|
||||
</div>
|
||||
{!!fields.length && (
|
||||
<div className="vm-chart-tooltip-info">
|
||||
{fields.map((f, i) => (
|
||||
<div key={`${f}_${i}`}>{f}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
), targetPortal);
|
||||
};
|
||||
|
||||
export default ChartTooltip;
|
|
@ -0,0 +1,77 @@
|
|||
@use "src/styles/variables" as *;
|
||||
$chart-tooltip-width: 300px;
|
||||
$chart-tooltip-icon-width: 25px;
|
||||
$chart-tooltip-date-width: $chart-tooltip-width - (2*$chart-tooltip-icon-width) - (2*$padding-global) - $padding-small;
|
||||
$chart-tooltip-x: -1 * ($padding-small + $padding-global + $chart-tooltip-date-width + ($chart-tooltip-icon-width/2));
|
||||
$chart-tooltip-y: -1 * ($padding-small + ($chart-tooltip-icon-width/2));
|
||||
|
||||
.vm-chart-tooltip {
|
||||
position: absolute;
|
||||
display: grid;
|
||||
gap: $padding-global;
|
||||
width: $chart-tooltip-width;
|
||||
padding: $padding-small;
|
||||
border-radius: $border-radius-medium;
|
||||
background: $color-background-tooltip;
|
||||
color: $color-white;
|
||||
font-size: $font-size-small;
|
||||
font-weight: normal;
|
||||
line-height: 150%;
|
||||
word-wrap: break-word;
|
||||
font-family: $font-family-monospace;
|
||||
z-index: 98;
|
||||
user-select: text;
|
||||
pointer-events: none;
|
||||
|
||||
&_sticky {
|
||||
background-color: $color-dove-gray;
|
||||
pointer-events: auto;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
&_moved {
|
||||
position: fixed;
|
||||
margin-top: $chart-tooltip-y;
|
||||
margin-left: $chart-tooltip-x;
|
||||
}
|
||||
|
||||
&-header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr $chart-tooltip-icon-width $chart-tooltip-icon-width;
|
||||
gap: $padding-small;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 25px;
|
||||
|
||||
&__close {
|
||||
color: $color-white;
|
||||
}
|
||||
|
||||
&__drag {
|
||||
color: $color-white;
|
||||
cursor: move;
|
||||
}
|
||||
}
|
||||
|
||||
&-data {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
|
||||
&__value {
|
||||
padding: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&__marker {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-right: $padding-small;
|
||||
}
|
||||
}
|
||||
|
||||
&-info {
|
||||
display: grid;
|
||||
grid-gap: 4px;
|
||||
}
|
||||
}
|
|
@ -1,9 +1,15 @@
|
|||
import React, { FC, useCallback, useEffect, useRef, useState } from "preact/compat";
|
||||
import uPlot, { AlignedData as uPlotData, Options as uPlotOptions, Series as uPlotSeries, Range, Scales, Scale } from "uplot";
|
||||
import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from "preact/compat";
|
||||
import uPlot, {
|
||||
AlignedData as uPlotData,
|
||||
Options as uPlotOptions,
|
||||
Series as uPlotSeries,
|
||||
Range,
|
||||
Scales,
|
||||
Scale,
|
||||
} from "uplot";
|
||||
import { defaultOptions } from "../../../utils/uplot/helpers";
|
||||
import { dragChart } from "../../../utils/uplot/events";
|
||||
import { getAxes, getMinMaxBuffer } from "../../../utils/uplot/axes";
|
||||
import { setTooltip } from "../../../utils/uplot/tooltip";
|
||||
import { MetricResult } from "../../../api/types";
|
||||
import { limitsDurations } from "../../../utils/time";
|
||||
import throttle from "lodash.throttle";
|
||||
|
@ -13,6 +19,7 @@ import { YaxisState } from "../../../state/graph/reducer";
|
|||
import "uplot/dist/uPlot.min.css";
|
||||
import "./style.scss";
|
||||
import classNames from "classnames";
|
||||
import ChartTooltip, { ChartTooltipProps } from "../ChartTooltip/ChartTooltip";
|
||||
|
||||
export interface LineChartProps {
|
||||
metrics: MetricResult[];
|
||||
|
@ -24,21 +31,30 @@ export interface LineChartProps {
|
|||
setPeriod: ({ from, to }: {from: Date, to: Date}) => void;
|
||||
container: HTMLDivElement | null
|
||||
}
|
||||
|
||||
enum typeChartUpdate {xRange = "xRange", yRange = "yRange", data = "data"}
|
||||
|
||||
const LineChart: FC<LineChartProps> = ({ data, series, metrics = [],
|
||||
period, yaxis, unit, setPeriod, container }) => {
|
||||
|
||||
const LineChart: FC<LineChartProps> = ({
|
||||
data,
|
||||
series,
|
||||
metrics = [],
|
||||
period,
|
||||
yaxis,
|
||||
unit,
|
||||
setPeriod,
|
||||
container
|
||||
}) => {
|
||||
const uPlotRef = useRef<HTMLDivElement>(null);
|
||||
const [isPanning, setPanning] = useState(false);
|
||||
const [xRange, setXRange] = useState({ min: period.start, max: period.end });
|
||||
const [uPlotInst, setUPlotInst] = useState<uPlot>();
|
||||
const layoutSize = useResize(container);
|
||||
|
||||
const tooltip = document.createElement("div");
|
||||
tooltip.className = "u-tooltip";
|
||||
const tooltipIdx: {seriesIdx: number | null, dataIdx: number | undefined} = { seriesIdx: null, dataIdx: undefined };
|
||||
const tooltipOffset = { left: 0, top: 0 };
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
const [tooltipIdx, setTooltipIdx] = useState({ seriesIdx: -1, dataIdx: -1 });
|
||||
const [tooltipOffset, setTooltipOffset] = useState({ left: 0, top: 0 });
|
||||
const [stickyTooltips, setStickyToolTips] = useState<ChartTooltipProps[]>([]);
|
||||
const tooltipId = useMemo(() => `${tooltipIdx.seriesIdx}_${tooltipIdx.dataIdx}`, [tooltipIdx]);
|
||||
|
||||
const setScale = ({ min, max }: { min: number, max: number }): void => {
|
||||
setPeriod({ from: new Date(min * 1000), to: new Date(max * 1000) });
|
||||
|
@ -54,12 +70,13 @@ const LineChart: FC<LineChartProps> = ({ data, series, metrics = [],
|
|||
|
||||
const onReadyChart = (u: uPlot) => {
|
||||
const factor = 0.9;
|
||||
tooltipOffset.left = parseFloat(u.over.style.left);
|
||||
tooltipOffset.top = parseFloat(u.over.style.top);
|
||||
u.root.querySelector(".u-wrap")?.appendChild(tooltip);
|
||||
setTooltipOffset({
|
||||
left: parseFloat(u.over.style.left),
|
||||
top: parseFloat(u.over.style.top)
|
||||
});
|
||||
u.over.addEventListener("mousedown", e => {
|
||||
const { ctrlKey, metaKey } = e;
|
||||
const leftClick = e.button === 0;
|
||||
const { ctrlKey, metaKey, button } = e;
|
||||
const leftClick = button === 0;
|
||||
const leftClickWithMeta = leftClick && (ctrlKey || metaKey);
|
||||
if (leftClickWithMeta) {
|
||||
// drag pan
|
||||
|
@ -98,21 +115,37 @@ const LineChart: FC<LineChartProps> = ({ data, series, metrics = [],
|
|||
}
|
||||
};
|
||||
|
||||
const setCursor = (u: uPlot) => {
|
||||
if (tooltipIdx.dataIdx === u.cursor.idx) return;
|
||||
tooltipIdx.dataIdx = u.cursor.idx || 0;
|
||||
if (tooltipIdx.seriesIdx !== null && tooltipIdx.dataIdx !== undefined) {
|
||||
setTooltip({ u, tooltipIdx, metrics, series, tooltip, tooltipOffset, unit });
|
||||
const handleClick = () => {
|
||||
const id = `${tooltipIdx.seriesIdx}_${tooltipIdx.dataIdx}`;
|
||||
const props = {
|
||||
id,
|
||||
unit,
|
||||
series,
|
||||
metrics,
|
||||
tooltipIdx,
|
||||
tooltipOffset,
|
||||
};
|
||||
|
||||
if (!stickyTooltips.find(t => t.id === id)) {
|
||||
const tooltipProps = JSON.parse(JSON.stringify(props));
|
||||
setStickyToolTips(prev => [...prev, tooltipProps]);
|
||||
}
|
||||
};
|
||||
|
||||
const seriesFocus = (u: uPlot, sidx: (number | null)) => {
|
||||
if (tooltipIdx.seriesIdx === sidx) return;
|
||||
tooltipIdx.seriesIdx = sidx;
|
||||
sidx && tooltipIdx.dataIdx !== undefined
|
||||
? setTooltip({ u, tooltipIdx, metrics, series, tooltip, tooltipOffset, unit })
|
||||
: tooltip.style.display = "none";
|
||||
const handleUnStick = (id:string) => {
|
||||
setStickyToolTips(prev => prev.filter(t => t.id !== id));
|
||||
};
|
||||
|
||||
const setCursor = (u: uPlot) => {
|
||||
const dataIdx = u.cursor.idx ?? -1;
|
||||
setTooltipIdx(prev => ({ ...prev, dataIdx }));
|
||||
};
|
||||
|
||||
const seriesFocus = (u: uPlot, sidx: (number | null)) => {
|
||||
const seriesIdx = sidx ?? -1;
|
||||
setTooltipIdx(prev => ({ ...prev, seriesIdx }));
|
||||
};
|
||||
|
||||
const getRangeX = (): Range.MinMax => [xRange.min, xRange.max];
|
||||
const getRangeY = (u: uPlot, min = 0, max = 1, axis: string): Range.MinMax => {
|
||||
if (yaxis.limits.enable) return yaxis.limits.range[axis];
|
||||
|
@ -168,6 +201,8 @@ const LineChart: FC<LineChartProps> = ({ data, series, metrics = [],
|
|||
useEffect(() => setXRange({ min: period.start, max: period.end }), [period]);
|
||||
|
||||
useEffect(() => {
|
||||
setStickyToolTips([]);
|
||||
setTooltipIdx({ seriesIdx: -1, dataIdx: -1 });
|
||||
if (!uPlotRef.current) return;
|
||||
const u = new uPlot(options, data, uPlotRef.current);
|
||||
setUPlotInst(u);
|
||||
|
@ -187,6 +222,17 @@ const LineChart: FC<LineChartProps> = ({ data, series, metrics = [],
|
|||
useEffect(() => updateChart(typeChartUpdate.xRange), [xRange]);
|
||||
useEffect(() => updateChart(typeChartUpdate.yRange), [yaxis]);
|
||||
|
||||
useEffect(() => {
|
||||
const show = tooltipIdx.dataIdx !== -1 && tooltipIdx.seriesIdx !== -1;
|
||||
setShowTooltip(show);
|
||||
|
||||
if (show) window.addEventListener("click", handleClick);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("click", handleClick);
|
||||
};
|
||||
}, [tooltipIdx, stickyTooltips]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
|
@ -194,7 +240,31 @@ const LineChart: FC<LineChartProps> = ({ data, series, metrics = [],
|
|||
"vm-line-chart_panning": isPanning
|
||||
})}
|
||||
>
|
||||
<div ref={uPlotRef}/>
|
||||
<div
|
||||
className="vm-line-chart__u-plot"
|
||||
ref={uPlotRef}
|
||||
/>
|
||||
{uPlotInst && showTooltip && (
|
||||
<ChartTooltip
|
||||
unit={unit}
|
||||
u={uPlotInst}
|
||||
series={series}
|
||||
metrics={metrics}
|
||||
tooltipIdx={tooltipIdx}
|
||||
tooltipOffset={tooltipOffset}
|
||||
id={tooltipId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{uPlotInst && stickyTooltips.map(t => (
|
||||
<ChartTooltip
|
||||
{...t}
|
||||
isSticky
|
||||
u={uPlotInst}
|
||||
key={t.id}
|
||||
onClose={handleUnStick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,45 +7,8 @@
|
|||
&_panning {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.u-tooltip {
|
||||
position: absolute;
|
||||
display: none;
|
||||
grid-gap: $padding-global;
|
||||
max-width: 300px;
|
||||
padding: $padding-small;
|
||||
border-radius: $border-radius-medium;
|
||||
background: $color-background-tooltip;
|
||||
color: $color-white;
|
||||
font-size: $font-size-small;
|
||||
font-weight: normal;
|
||||
line-height: 1.4;
|
||||
word-wrap: break-word;
|
||||
font-family: monospace;
|
||||
pointer-events: none;
|
||||
z-index: 100;
|
||||
|
||||
&-data {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
line-height: 150%;
|
||||
|
||||
&__value {
|
||||
padding: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
&__info {
|
||||
display: grid;
|
||||
grid-gap: 4px;
|
||||
}
|
||||
|
||||
&__marker {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-right: $padding-small;
|
||||
&__u-plot {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ interface ButtonProps {
|
|||
children?: ReactNode
|
||||
className?: string
|
||||
onClick?: (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => void
|
||||
onMouseDown?: (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => void
|
||||
}
|
||||
|
||||
const Button: FC<ButtonProps> = ({
|
||||
|
@ -27,6 +28,7 @@ const Button: FC<ButtonProps> = ({
|
|||
className,
|
||||
disabled,
|
||||
onClick,
|
||||
onMouseDown,
|
||||
}) => {
|
||||
|
||||
const classesButton = classNames({
|
||||
|
@ -45,6 +47,7 @@ const Button: FC<ButtonProps> = ({
|
|||
className={classesButton}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
onMouseDown={onMouseDown}
|
||||
>
|
||||
<>
|
||||
{startIcon && <span className="vm-button__start-icon">{startIcon}</span>}
|
||||
|
|
|
@ -300,3 +300,12 @@ export const CopyIcon = () => (
|
|||
></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const DragIcon = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M20 9H4v2h16V9zM4 15h16v-2H4v2z"></path>
|
||||
</svg>
|
||||
);
|
||||
|
|
|
@ -18,6 +18,7 @@ $color-text-secondary: rgba($color-text, 0.6);
|
|||
$color-text-disabled: rgba($color-text, 0.4);
|
||||
|
||||
$color-black: #110f0f;
|
||||
$color-dove-gray: #616161;
|
||||
$color-silver: #C4C4C4;
|
||||
$color-alto: #D8D8D8;
|
||||
$color-white: #ffffff;
|
||||
|
@ -30,7 +31,7 @@ $color-tropical-blue: #C9E3F6;
|
|||
$color-background-body: var(--color-background-body);
|
||||
$color-background-block: var(--color-background-block);
|
||||
$color-background-modal: rgba($color-black, 0.7);
|
||||
$color-background-tooltip: rgba(97, 97, 97, 0.92);
|
||||
$color-background-tooltip: rgba($color-dove-gray, 0.92);
|
||||
|
||||
|
||||
/************* padding *************/
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
import dayjs from "dayjs";
|
||||
import { SetupTooltip } from "./types";
|
||||
import { getColorLine, formatPrettyNumber, getLegendLabel } from "./helpers";
|
||||
import { DATE_FULL_TIMEZONE_FORMAT } from "../../constants/date";
|
||||
|
||||
// TODO create jsx component
|
||||
export const setTooltip = ({ u, tooltipIdx, metrics, series, tooltip, tooltipOffset, unit = "" }: SetupTooltip): void => {
|
||||
const { seriesIdx, dataIdx } = tooltipIdx;
|
||||
if (seriesIdx === null || dataIdx === undefined) return;
|
||||
const dataSeries = u.data[seriesIdx][dataIdx];
|
||||
const dataTime = u.data[0][dataIdx];
|
||||
const metric = metrics[seriesIdx - 1]?.metric || {};
|
||||
const selectedSeries = series[seriesIdx];
|
||||
const color = getColorLine(selectedSeries.label || "");
|
||||
|
||||
const { width, height } = u.over.getBoundingClientRect();
|
||||
const top = u.valToPos((dataSeries || 0), series[seriesIdx]?.scale || "1");
|
||||
const lft = u.valToPos(dataTime, "x");
|
||||
const { width: tooltipWidth, height: tooltipHeight } = tooltip.getBoundingClientRect();
|
||||
const overflowX = lft + tooltipWidth >= width;
|
||||
const overflowY = top + tooltipHeight >= height;
|
||||
|
||||
tooltip.style.display = "grid";
|
||||
tooltip.style.top = `${tooltipOffset.top + top + 10 - (overflowY ? tooltipHeight + 10 : 0)}px`;
|
||||
tooltip.style.left = `${tooltipOffset.left + lft + 10 - (overflowX ? tooltipWidth + 20 : 0)}px`;
|
||||
const metricName = (selectedSeries.label || "").replace(/{.+}/gmi, "").trim();
|
||||
const name = getLegendLabel(metricName);
|
||||
const date = dayjs(new Date(dataTime * 1000)).format(DATE_FULL_TIMEZONE_FORMAT);
|
||||
const info = Object.keys(metric).filter(k => k !== "__name__").map(k => `<div><b>${k}</b>: ${metric[k]}</div>`).join("");
|
||||
const marker = `<div class="u-tooltip__marker" style="background: ${color}"></div>`;
|
||||
tooltip.innerHTML = `<div>${date}</div>
|
||||
<div class="u-tooltip-data">
|
||||
${marker}${name}: <b class="u-tooltip-data__value">${formatPrettyNumber(dataSeries)}</b> ${unit}
|
||||
</div>
|
||||
<div class="u-tooltip__info">${info}</div>`;
|
||||
};
|
|
@ -1,21 +1,4 @@
|
|||
import uPlot, { Series } from "uplot";
|
||||
import { MetricResult } from "../../api/types";
|
||||
|
||||
export interface SetupTooltip {
|
||||
u: uPlot,
|
||||
metrics: MetricResult[],
|
||||
series: Series[],
|
||||
tooltip: HTMLDivElement,
|
||||
unit?: string,
|
||||
tooltipOffset: {
|
||||
left: number,
|
||||
top: number
|
||||
},
|
||||
tooltipIdx: {
|
||||
seriesIdx: number | null,
|
||||
dataIdx: number | undefined
|
||||
}
|
||||
}
|
||||
|
||||
export interface HideSeriesArgs {
|
||||
hideSeries: string[],
|
||||
|
|
|
@ -24,6 +24,7 @@ The following tip changes can be tested by building VictoriaMetrics components f
|
|||
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add the ability to upload/paste JSON to investigate the trace. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3308) and [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/3310).
|
||||
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): reduce JS bundle size from 200Kb to 100Kb. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/3298).
|
||||
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add the ability to hide results of a particular query by clicking the `eye` icon. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/3359).
|
||||
* FEATURE: [vmui](https://docs.victoriametrics.com/#vmui): add the ability to "stick" a tooltip on the chart by clicking on a data point. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3321) and [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/3376)
|
||||
* FEATURE: [vmalert](https://docs.victoriametrics.com/vmalert.html): add default alert list for vmalert's metrics. See [alerts-vmalert.yml](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/deployment/docker/alerts-vmalert.yml).
|
||||
|
||||
* BUGFIX: [MetricsQL](https://docs.victoriametrics.com/MetricsQL.html): properly return an empty result from [limit_offset](https://docs.victoriametrics.com/MetricsQL.html#limit_offset) if the `offset` arg exceeds the number of inner time series. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3312).
|
||||
|
|
Loading…
Reference in a new issue