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 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 uPlot, {
|
||||||
|
AlignedData as uPlotData,
|
||||||
|
Options as uPlotOptions,
|
||||||
|
Series as uPlotSeries,
|
||||||
|
Range,
|
||||||
|
Scales,
|
||||||
|
Scale,
|
||||||
|
} from "uplot";
|
||||||
import { defaultOptions } from "../../../utils/uplot/helpers";
|
import { defaultOptions } from "../../../utils/uplot/helpers";
|
||||||
import { dragChart } from "../../../utils/uplot/events";
|
import { dragChart } from "../../../utils/uplot/events";
|
||||||
import { getAxes, getMinMaxBuffer } from "../../../utils/uplot/axes";
|
import { getAxes, getMinMaxBuffer } from "../../../utils/uplot/axes";
|
||||||
import { setTooltip } from "../../../utils/uplot/tooltip";
|
|
||||||
import { MetricResult } from "../../../api/types";
|
import { MetricResult } from "../../../api/types";
|
||||||
import { limitsDurations } from "../../../utils/time";
|
import { limitsDurations } from "../../../utils/time";
|
||||||
import throttle from "lodash.throttle";
|
import throttle from "lodash.throttle";
|
||||||
|
@ -13,6 +19,7 @@ import { YaxisState } from "../../../state/graph/reducer";
|
||||||
import "uplot/dist/uPlot.min.css";
|
import "uplot/dist/uPlot.min.css";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import ChartTooltip, { ChartTooltipProps } from "../ChartTooltip/ChartTooltip";
|
||||||
|
|
||||||
export interface LineChartProps {
|
export interface LineChartProps {
|
||||||
metrics: MetricResult[];
|
metrics: MetricResult[];
|
||||||
|
@ -24,21 +31,30 @@ export interface LineChartProps {
|
||||||
setPeriod: ({ from, to }: {from: Date, to: Date}) => void;
|
setPeriod: ({ from, to }: {from: Date, to: Date}) => void;
|
||||||
container: HTMLDivElement | null
|
container: HTMLDivElement | null
|
||||||
}
|
}
|
||||||
|
|
||||||
enum typeChartUpdate {xRange = "xRange", yRange = "yRange", data = "data"}
|
enum typeChartUpdate {xRange = "xRange", yRange = "yRange", data = "data"}
|
||||||
|
|
||||||
const LineChart: FC<LineChartProps> = ({ data, series, metrics = [],
|
const LineChart: FC<LineChartProps> = ({
|
||||||
period, yaxis, unit, setPeriod, container }) => {
|
data,
|
||||||
|
series,
|
||||||
|
metrics = [],
|
||||||
|
period,
|
||||||
|
yaxis,
|
||||||
|
unit,
|
||||||
|
setPeriod,
|
||||||
|
container
|
||||||
|
}) => {
|
||||||
const uPlotRef = useRef<HTMLDivElement>(null);
|
const uPlotRef = useRef<HTMLDivElement>(null);
|
||||||
const [isPanning, setPanning] = useState(false);
|
const [isPanning, setPanning] = useState(false);
|
||||||
const [xRange, setXRange] = useState({ min: period.start, max: period.end });
|
const [xRange, setXRange] = useState({ min: period.start, max: period.end });
|
||||||
const [uPlotInst, setUPlotInst] = useState<uPlot>();
|
const [uPlotInst, setUPlotInst] = useState<uPlot>();
|
||||||
const layoutSize = useResize(container);
|
const layoutSize = useResize(container);
|
||||||
|
|
||||||
const tooltip = document.createElement("div");
|
const [showTooltip, setShowTooltip] = useState(false);
|
||||||
tooltip.className = "u-tooltip";
|
const [tooltipIdx, setTooltipIdx] = useState({ seriesIdx: -1, dataIdx: -1 });
|
||||||
const tooltipIdx: {seriesIdx: number | null, dataIdx: number | undefined} = { seriesIdx: null, dataIdx: undefined };
|
const [tooltipOffset, setTooltipOffset] = useState({ left: 0, top: 0 });
|
||||||
const tooltipOffset = { 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 => {
|
const setScale = ({ min, max }: { min: number, max: number }): void => {
|
||||||
setPeriod({ from: new Date(min * 1000), to: new Date(max * 1000) });
|
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 onReadyChart = (u: uPlot) => {
|
||||||
const factor = 0.9;
|
const factor = 0.9;
|
||||||
tooltipOffset.left = parseFloat(u.over.style.left);
|
setTooltipOffset({
|
||||||
tooltipOffset.top = parseFloat(u.over.style.top);
|
left: parseFloat(u.over.style.left),
|
||||||
u.root.querySelector(".u-wrap")?.appendChild(tooltip);
|
top: parseFloat(u.over.style.top)
|
||||||
|
});
|
||||||
u.over.addEventListener("mousedown", e => {
|
u.over.addEventListener("mousedown", e => {
|
||||||
const { ctrlKey, metaKey } = e;
|
const { ctrlKey, metaKey, button } = e;
|
||||||
const leftClick = e.button === 0;
|
const leftClick = button === 0;
|
||||||
const leftClickWithMeta = leftClick && (ctrlKey || metaKey);
|
const leftClickWithMeta = leftClick && (ctrlKey || metaKey);
|
||||||
if (leftClickWithMeta) {
|
if (leftClickWithMeta) {
|
||||||
// drag pan
|
// drag pan
|
||||||
|
@ -98,21 +115,37 @@ const LineChart: FC<LineChartProps> = ({ data, series, metrics = [],
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const setCursor = (u: uPlot) => {
|
const handleClick = () => {
|
||||||
if (tooltipIdx.dataIdx === u.cursor.idx) return;
|
const id = `${tooltipIdx.seriesIdx}_${tooltipIdx.dataIdx}`;
|
||||||
tooltipIdx.dataIdx = u.cursor.idx || 0;
|
const props = {
|
||||||
if (tooltipIdx.seriesIdx !== null && tooltipIdx.dataIdx !== undefined) {
|
id,
|
||||||
setTooltip({ u, tooltipIdx, metrics, series, tooltip, tooltipOffset, unit });
|
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)) => {
|
const handleUnStick = (id:string) => {
|
||||||
if (tooltipIdx.seriesIdx === sidx) return;
|
setStickyToolTips(prev => prev.filter(t => t.id !== id));
|
||||||
tooltipIdx.seriesIdx = sidx;
|
|
||||||
sidx && tooltipIdx.dataIdx !== undefined
|
|
||||||
? setTooltip({ u, tooltipIdx, metrics, series, tooltip, tooltipOffset, unit })
|
|
||||||
: tooltip.style.display = "none";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 getRangeX = (): Range.MinMax => [xRange.min, xRange.max];
|
||||||
const getRangeY = (u: uPlot, min = 0, max = 1, axis: string): Range.MinMax => {
|
const getRangeY = (u: uPlot, min = 0, max = 1, axis: string): Range.MinMax => {
|
||||||
if (yaxis.limits.enable) return yaxis.limits.range[axis];
|
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(() => setXRange({ min: period.start, max: period.end }), [period]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
setStickyToolTips([]);
|
||||||
|
setTooltipIdx({ seriesIdx: -1, dataIdx: -1 });
|
||||||
if (!uPlotRef.current) return;
|
if (!uPlotRef.current) return;
|
||||||
const u = new uPlot(options, data, uPlotRef.current);
|
const u = new uPlot(options, data, uPlotRef.current);
|
||||||
setUPlotInst(u);
|
setUPlotInst(u);
|
||||||
|
@ -187,6 +222,17 @@ const LineChart: FC<LineChartProps> = ({ data, series, metrics = [],
|
||||||
useEffect(() => updateChart(typeChartUpdate.xRange), [xRange]);
|
useEffect(() => updateChart(typeChartUpdate.xRange), [xRange]);
|
||||||
useEffect(() => updateChart(typeChartUpdate.yRange), [yaxis]);
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
|
@ -194,7 +240,31 @@ const LineChart: FC<LineChartProps> = ({ data, series, metrics = [],
|
||||||
"vm-line-chart_panning": isPanning
|
"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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,45 +7,8 @@
|
||||||
&_panning {
|
&_panning {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.u-tooltip {
|
&__u-plot {
|
||||||
position: absolute;
|
position: relative;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ interface ButtonProps {
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
className?: string
|
className?: string
|
||||||
onClick?: (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => void
|
onClick?: (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => void
|
||||||
|
onMouseDown?: (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const Button: FC<ButtonProps> = ({
|
const Button: FC<ButtonProps> = ({
|
||||||
|
@ -27,6 +28,7 @@ const Button: FC<ButtonProps> = ({
|
||||||
className,
|
className,
|
||||||
disabled,
|
disabled,
|
||||||
onClick,
|
onClick,
|
||||||
|
onMouseDown,
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
const classesButton = classNames({
|
const classesButton = classNames({
|
||||||
|
@ -45,6 +47,7 @@ const Button: FC<ButtonProps> = ({
|
||||||
className={classesButton}
|
className={classesButton}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
onMouseDown={onMouseDown}
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
{startIcon && <span className="vm-button__start-icon">{startIcon}</span>}
|
{startIcon && <span className="vm-button__start-icon">{startIcon}</span>}
|
||||||
|
|
|
@ -300,3 +300,12 @@ export const CopyIcon = () => (
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</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-text-disabled: rgba($color-text, 0.4);
|
||||||
|
|
||||||
$color-black: #110f0f;
|
$color-black: #110f0f;
|
||||||
|
$color-dove-gray: #616161;
|
||||||
$color-silver: #C4C4C4;
|
$color-silver: #C4C4C4;
|
||||||
$color-alto: #D8D8D8;
|
$color-alto: #D8D8D8;
|
||||||
$color-white: #ffffff;
|
$color-white: #ffffff;
|
||||||
|
@ -30,7 +31,7 @@ $color-tropical-blue: #C9E3F6;
|
||||||
$color-background-body: var(--color-background-body);
|
$color-background-body: var(--color-background-body);
|
||||||
$color-background-block: var(--color-background-block);
|
$color-background-block: var(--color-background-block);
|
||||||
$color-background-modal: rgba($color-black, 0.7);
|
$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 *************/
|
/************* 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 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 {
|
export interface HideSeriesArgs {
|
||||||
hideSeries: string[],
|
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): 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): 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 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).
|
* 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).
|
* 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