mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-21 14:44:00 +00:00
vmui: memory leak fix (#4455)
* fix: optimize the preparation of data for the graph * fix: optimize tooltip rendering * fix: optimize re-rendering of the chart * vmui: memory leak fix
This commit is contained in:
parent
7b2748e7a1
commit
66b42a6772
13 changed files with 227 additions and 185 deletions
|
@ -73,10 +73,9 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
|
|||
});
|
||||
};
|
||||
const throttledSetScale = useCallback(throttle(setScale, 500), []);
|
||||
const setPlotScale = ({ u, min, max }: { u: uPlot, min: number, max: number }) => {
|
||||
const setPlotScale = ({ min, max }: { min: number, max: number }) => {
|
||||
const delta = (max - min) * 1000;
|
||||
if ((delta < limitsDurations.min) || (delta > limitsDurations.max)) return;
|
||||
u.setScale("x", { min, max });
|
||||
setXRange({ min, max });
|
||||
throttledSetScale({ min, max });
|
||||
};
|
||||
|
@ -112,7 +111,7 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
|
|||
const nxRange = e.deltaY < 0 ? oxRange * factor : oxRange / factor;
|
||||
const min = xVal - (zoomPos / width) * nxRange;
|
||||
const max = min + nxRange;
|
||||
u.batch(() => setPlotScale({ u, min, max }));
|
||||
u.batch(() => setPlotScale({ min, max }));
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -126,7 +125,6 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
|
|||
e.preventDefault();
|
||||
const factor = (xRange.max - xRange.min) / 10 * (plus ? 1 : -1);
|
||||
setPlotScale({
|
||||
u: uPlotInst,
|
||||
min: xRange.min + factor,
|
||||
max: xRange.max - factor
|
||||
});
|
||||
|
@ -241,7 +239,7 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
|
|||
(u) => {
|
||||
const min = u.posToVal(u.select.left, "x");
|
||||
const max = u.posToVal(u.select.left + u.select.width, "x");
|
||||
setPlotScale({ u, min, max });
|
||||
setPlotScale({ min, max });
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -295,7 +293,6 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
|
|||
|
||||
const zoomFactor = dur / 50 * dir;
|
||||
uPlotInst.batch(() => setPlotScale({
|
||||
u: uPlotInst,
|
||||
min: min + zoomFactor,
|
||||
max: max - zoomFactor
|
||||
}));
|
||||
|
|
|
@ -17,11 +17,11 @@ import useEventListener from "../../../../hooks/useEventListener";
|
|||
export interface ChartTooltipProps {
|
||||
id: string,
|
||||
u: uPlot,
|
||||
metrics: MetricResult[],
|
||||
series: SeriesItem[],
|
||||
yRange: number[];
|
||||
metricItem: MetricResult,
|
||||
seriesItem: SeriesItem,
|
||||
unit?: string,
|
||||
isSticky?: boolean,
|
||||
showQueryNum?: boolean,
|
||||
tooltipOffset: { left: number, top: number },
|
||||
tooltipIdx: { seriesIdx: number, dataIdx: number },
|
||||
onClose?: (id: string) => void
|
||||
|
@ -31,12 +31,12 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
|
|||
u,
|
||||
id,
|
||||
unit = "",
|
||||
metrics,
|
||||
series,
|
||||
yRange,
|
||||
metricItem,
|
||||
seriesItem,
|
||||
tooltipIdx,
|
||||
tooltipOffset,
|
||||
isSticky,
|
||||
showQueryNum,
|
||||
onClose
|
||||
}) => {
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
|
@ -49,21 +49,17 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
|
|||
const [dataIdx, setDataIdx] = useState(tooltipIdx.dataIdx);
|
||||
|
||||
const value = get(u, ["data", seriesIdx, dataIdx], 0);
|
||||
const valueFormat = formatPrettyNumber(value, get(yRange, [0]), get(yRange, [1]));
|
||||
const valueFormat = formatPrettyNumber(value, get(u, ["scales", "1", "min"], 0), get(u, ["scales", "1", "max"], 1));
|
||||
const dataTime = u.data[0][dataIdx];
|
||||
const date = dayjs(dataTime * 1000).tz().format(DATE_FULL_TIMEZONE_FORMAT);
|
||||
|
||||
const color = series[seriesIdx]?.stroke+"";
|
||||
|
||||
const calculations = series[seriesIdx]?.calculations || {};
|
||||
|
||||
const groups = new Set(metrics.map(m => m.group));
|
||||
const showQueryNum = groups.size > 1;
|
||||
const group = metrics[seriesIdx-1]?.group || 0;
|
||||
const color = `${seriesItem?.stroke}`;
|
||||
const calculations = seriesItem?.calculations || {};
|
||||
const group = metricItem?.group || 0;
|
||||
|
||||
|
||||
const fullMetricName = useMemo(() => {
|
||||
const metric = metrics[seriesIdx-1]?.metric || {};
|
||||
const metric = metricItem?.metric || {};
|
||||
const labelNames = Object.keys(metric).filter(x => x != "__name__");
|
||||
const labels = labelNames.map(key => `${key}=${JSON.stringify(metric[key])}`);
|
||||
let metricName = metric["__name__"] || "";
|
||||
|
@ -71,7 +67,7 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
|
|||
metricName += "{" + labels.join(",") + "}";
|
||||
}
|
||||
return metricName;
|
||||
}, [metrics, seriesIdx]);
|
||||
}, [metricItem]);
|
||||
|
||||
const handleClose = () => {
|
||||
onClose && onClose(id);
|
||||
|
@ -97,7 +93,7 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
|
|||
const calcPosition = () => {
|
||||
if (!tooltipRef.current) return;
|
||||
|
||||
const topOnChart = u.valToPos((value || 0), series[seriesIdx]?.scale || "1");
|
||||
const topOnChart = u.valToPos((value || 0), seriesItem?.scale || "1");
|
||||
const leftOnChart = u.valToPos(dataTime, "x");
|
||||
const { width: tooltipWidth, height: tooltipHeight } = tooltipRef.current.getBoundingClientRect();
|
||||
const { width, height } = u.over.getBoundingClientRect();
|
||||
|
@ -142,9 +138,7 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
|
|||
>
|
||||
<div className="vm-chart-tooltip-header">
|
||||
<div className="vm-chart-tooltip-header__date">
|
||||
{showQueryNum && (
|
||||
<div>Query {group}</div>
|
||||
)}
|
||||
{showQueryNum && (<div>Query {group}</div>)}
|
||||
{date}
|
||||
</div>
|
||||
{isSticky && (
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import React, { FC, useState, useMemo } from "preact/compat";
|
||||
import React, { FC, useMemo } from "preact/compat";
|
||||
import { MouseEvent } from "react";
|
||||
import { LegendItemType } from "../../../../../utils/uplot/types";
|
||||
import "./style.scss";
|
||||
import classNames from "classnames";
|
||||
import Tooltip from "../../../../Main/Tooltip/Tooltip";
|
||||
import { getFreeFields } from "./helpers";
|
||||
import useCopyToClipboard from "../../../../../hooks/useCopyToClipboard";
|
||||
|
||||
|
@ -15,7 +14,6 @@ interface LegendItemProps {
|
|||
|
||||
const LegendItem: FC<LegendItemProps> = ({ legend, onChange, isHeatmap }) => {
|
||||
const copyToClipboard = useCopyToClipboard();
|
||||
const [copiedValue, setCopiedValue] = useState("");
|
||||
|
||||
const freeFormFields = useMemo(() => {
|
||||
const result = getFreeFields(legend);
|
||||
|
@ -25,20 +23,17 @@ const LegendItem: FC<LegendItemProps> = ({ legend, onChange, isHeatmap }) => {
|
|||
const calculations = legend.calculations;
|
||||
const showCalculations = Object.values(calculations).some(v => v);
|
||||
|
||||
const handleClickFreeField = async (val: string, id: string) => {
|
||||
const copied = await copyToClipboard(val);
|
||||
if (!copied) return;
|
||||
setCopiedValue(id);
|
||||
setTimeout(() => setCopiedValue(""), 2000);
|
||||
const handleClickFreeField = async (val: string) => {
|
||||
await copyToClipboard(val, `${val} has been copied`);
|
||||
};
|
||||
|
||||
const createHandlerClick = (legend: LegendItemType) => (e: MouseEvent<HTMLDivElement>) => {
|
||||
onChange && onChange(legend, e.ctrlKey || e.metaKey);
|
||||
};
|
||||
|
||||
const createHandlerCopy = (freeField: string, id: string) => (e: MouseEvent<HTMLDivElement>) => {
|
||||
const createHandlerCopy = (freeField: string) => (e: MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation();
|
||||
handleClickFreeField(freeField, id);
|
||||
handleClickFreeField(freeField);
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -62,21 +57,14 @@ const LegendItem: FC<LegendItemProps> = ({ legend, onChange, isHeatmap }) => {
|
|||
{legend.freeFormFields["__name__"]}
|
||||
{!!freeFormFields.length && <>{</>}
|
||||
{freeFormFields.map((f, i) => (
|
||||
<Tooltip
|
||||
key={f.id}
|
||||
open={copiedValue === f.id}
|
||||
title={"copied!"}
|
||||
placement="top-center"
|
||||
<span
|
||||
className="vm-legend-item-info__free-fields"
|
||||
key={f.key}
|
||||
onClick={createHandlerCopy(f.freeField)}
|
||||
title="copy to clipboard"
|
||||
>
|
||||
<span
|
||||
className="vm-legend-item-info__free-fields"
|
||||
key={f.key}
|
||||
onClick={createHandlerCopy(f.freeField, f.id)}
|
||||
title="copy to clipboard"
|
||||
>
|
||||
{f.freeField}{i + 1 < freeFormFields.length && ","}
|
||||
</span>
|
||||
</Tooltip>
|
||||
{f.freeField}{i + 1 < freeFormFields.length && ","}
|
||||
</span>
|
||||
))}
|
||||
{!!freeFormFields.length && <>}</>}
|
||||
</span>
|
||||
|
|
|
@ -1,18 +1,14 @@
|
|||
import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from "preact/compat";
|
||||
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 { defaultOptions } from "../../../../utils/uplot/helpers";
|
||||
import { dragChart } from "../../../../utils/uplot/events";
|
||||
import { getAxes, getMinMaxBuffer } from "../../../../utils/uplot/axes";
|
||||
import { getAxes } from "../../../../utils/uplot/axes";
|
||||
import { MetricResult } from "../../../../api/types";
|
||||
import { dateFromSeconds, formatDateForNativeInput, limitsDurations } from "../../../../utils/time";
|
||||
import throttle from "lodash.throttle";
|
||||
import { TimeParams } from "../../../../types";
|
||||
import { YaxisState } from "../../../../state/graph/reducer";
|
||||
import "uplot/dist/uPlot.min.css";
|
||||
|
@ -24,6 +20,7 @@ import { useAppState } from "../../../../state/common/StateContext";
|
|||
import { SeriesItem } from "../../../../utils/uplot/series";
|
||||
import { ElementSize } from "../../../../hooks/useElementSize";
|
||||
import useEventListener from "../../../../hooks/useEventListener";
|
||||
import { getRangeX, getRangeY, getScales } from "../../../../utils/uplot/scales";
|
||||
|
||||
export interface LineChartProps {
|
||||
metrics: MetricResult[];
|
||||
|
@ -37,8 +34,6 @@ export interface LineChartProps {
|
|||
height?: number;
|
||||
}
|
||||
|
||||
enum typeChartUpdate {xRange = "xRange", yRange = "yRange"}
|
||||
|
||||
const LineChart: FC<LineChartProps> = ({
|
||||
data,
|
||||
series,
|
||||
|
@ -55,7 +50,6 @@ const LineChart: FC<LineChartProps> = ({
|
|||
const uPlotRef = useRef<HTMLDivElement>(null);
|
||||
const [isPanning, setPanning] = useState(false);
|
||||
const [xRange, setXRange] = useState({ min: period.start, max: period.end });
|
||||
const [yRange, setYRange] = useState([0, 1]);
|
||||
const [uPlotInst, setUPlotInst] = useState<uPlot>();
|
||||
const [startTouchDistance, setStartTouchDistance] = useState(0);
|
||||
|
||||
|
@ -63,24 +57,18 @@ const LineChart: FC<LineChartProps> = ({
|
|||
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 => {
|
||||
const setPlotScale = ({ min, max }: { min: number, max: number }) => {
|
||||
const delta = (max - min) * 1000;
|
||||
if ((delta < limitsDurations.min) || (delta > limitsDurations.max)) return;
|
||||
setXRange({ min, max });
|
||||
setPeriod({
|
||||
from: dayjs(min * 1000).toDate(),
|
||||
to: dayjs(max * 1000).toDate()
|
||||
});
|
||||
};
|
||||
const throttledSetScale = useCallback(throttle(setScale, 500), []);
|
||||
const setPlotScale = ({ u, min, max }: { u: uPlot, min: number, max: number }) => {
|
||||
const delta = (max - min) * 1000;
|
||||
if ((delta < limitsDurations.min) || (delta > limitsDurations.max)) return;
|
||||
u.setScale("x", { min, max });
|
||||
setXRange({ min, max });
|
||||
throttledSetScale({ min, max });
|
||||
};
|
||||
|
||||
const onReadyChart = (u: uPlot) => {
|
||||
const onReadyChart = (u: uPlot): void => {
|
||||
const factor = 0.9;
|
||||
setTooltipOffset({
|
||||
left: parseFloat(u.over.style.left),
|
||||
|
@ -111,7 +99,7 @@ const LineChart: FC<LineChartProps> = ({
|
|||
const nxRange = e.deltaY < 0 ? oxRange * factor : oxRange / factor;
|
||||
const min = xVal - (zoomPos / width) * nxRange;
|
||||
const max = min + nxRange;
|
||||
u.batch(() => setPlotScale({ u, min, max }));
|
||||
u.batch(() => setPlotScale({ min, max }));
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -125,33 +113,41 @@ const LineChart: FC<LineChartProps> = ({
|
|||
e.preventDefault();
|
||||
const factor = (xRange.max - xRange.min) / 10 * (plus ? 1 : -1);
|
||||
setPlotScale({
|
||||
u: uPlotInst,
|
||||
min: xRange.min + factor,
|
||||
max: xRange.max - factor
|
||||
});
|
||||
}
|
||||
}, [uPlotInst, xRange]);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (!showTooltip) return;
|
||||
const id = `${tooltipIdx.seriesIdx}_${tooltipIdx.dataIdx}`;
|
||||
const props = {
|
||||
const getChartProps = useCallback(() => {
|
||||
const { seriesIdx, dataIdx } = tooltipIdx;
|
||||
const id = `${seriesIdx}_${dataIdx}`;
|
||||
const metricItem = metrics[seriesIdx-1];
|
||||
const seriesItem = series[seriesIdx] as SeriesItem;
|
||||
|
||||
const groups = new Set(metrics.map(m => m.group));
|
||||
const showQueryNum = groups.size > 1;
|
||||
|
||||
return {
|
||||
id,
|
||||
unit,
|
||||
series,
|
||||
metrics,
|
||||
yRange,
|
||||
seriesItem,
|
||||
metricItem,
|
||||
tooltipIdx,
|
||||
tooltipOffset,
|
||||
showQueryNum,
|
||||
};
|
||||
}, [uPlotInst, metrics, series, tooltipIdx, tooltipOffset, unit]);
|
||||
|
||||
if (!stickyTooltips.find(t => t.id === id)) {
|
||||
const tooltipProps = JSON.parse(JSON.stringify(props));
|
||||
setStickyToolTips(prev => [...prev, tooltipProps]);
|
||||
const handleClick = useCallback(() => {
|
||||
if (!showTooltip) return;
|
||||
const props = getChartProps();
|
||||
if (!stickyTooltips.find(t => t.id === props.id)) {
|
||||
setStickyToolTips(prev => [...prev, props as ChartTooltipProps]);
|
||||
}
|
||||
}, [metrics, series, stickyTooltips, tooltipIdx, tooltipOffset, showTooltip, unit, yRange]);
|
||||
}, [getChartProps, stickyTooltips, showTooltip]);
|
||||
|
||||
const handleUnStick = (id:string) => {
|
||||
const handleUnStick = (id: string) => {
|
||||
setStickyToolTips(prev => prev.filter(t => t.id !== id));
|
||||
};
|
||||
|
||||
|
@ -165,23 +161,34 @@ const LineChart: FC<LineChartProps> = ({
|
|||
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 (axis == "1") {
|
||||
setYRange([min, max]);
|
||||
}
|
||||
if (yaxis.limits.enable) return yaxis.limits.range[axis];
|
||||
return getMinMaxBuffer(min, max);
|
||||
const addSeries = (u: uPlot, series: uPlotSeries[]) => {
|
||||
series.forEach((s) => {
|
||||
u.addSeries(s);
|
||||
});
|
||||
};
|
||||
|
||||
const getScales = (): Scales => {
|
||||
const scales: { [key: string]: { range: Scale.Range } } = { x: { range: getRangeX } };
|
||||
const ranges = Object.keys(yaxis.limits.range);
|
||||
(ranges.length ? ranges : ["1"]).forEach(axis => {
|
||||
scales[axis] = { range: (u: uPlot, min = 0, max = 1) => getRangeY(u, min, max, axis) };
|
||||
const delSeries = (u: uPlot) => {
|
||||
for (let i = u.series.length - 1; i >= 0; i--) {
|
||||
u.delSeries(i);
|
||||
}
|
||||
};
|
||||
|
||||
const delHooks = (u: uPlot) => {
|
||||
Object.keys(u.hooks).forEach(hook => {
|
||||
u.hooks[hook as keyof uPlot.Hooks.Arrays] = [];
|
||||
});
|
||||
return scales;
|
||||
};
|
||||
|
||||
const handleDestroy = (u: uPlot) => {
|
||||
delSeries(u);
|
||||
delHooks(u);
|
||||
u.setData([]);
|
||||
};
|
||||
|
||||
const setSelect = (u: uPlot) => {
|
||||
const min = u.posToVal(u.select.left, "x");
|
||||
const max = u.posToVal(u.select.left + u.select.width, "x");
|
||||
setPlotScale({ min, max });
|
||||
};
|
||||
|
||||
const options: uPlotOptions = {
|
||||
|
@ -189,49 +196,18 @@ const LineChart: FC<LineChartProps> = ({
|
|||
tzDate: ts => dayjs(formatDateForNativeInput(dateFromSeconds(ts))).local().toDate(),
|
||||
series,
|
||||
axes: getAxes( [{}, { scale: "1" }], unit),
|
||||
scales: { ...getScales() },
|
||||
scales: getScales(yaxis, xRange),
|
||||
width: layoutSize.width || 400,
|
||||
height: height || 500,
|
||||
plugins: [{ hooks: { ready: onReadyChart, setCursor, setSeries: seriesFocus } }],
|
||||
hooks: {
|
||||
setSelect: [
|
||||
(u) => {
|
||||
const min = u.posToVal(u.select.left, "x");
|
||||
const max = u.posToVal(u.select.left + u.select.width, "x");
|
||||
setPlotScale({ u, min, max });
|
||||
}
|
||||
]
|
||||
}
|
||||
ready: [onReadyChart],
|
||||
setSeries: [seriesFocus],
|
||||
setCursor: [setCursor],
|
||||
setSelect: [setSelect],
|
||||
destroy: [handleDestroy],
|
||||
},
|
||||
};
|
||||
|
||||
const updateChart = (type: typeChartUpdate): void => {
|
||||
if (!uPlotInst) return;
|
||||
switch (type) {
|
||||
case typeChartUpdate.xRange:
|
||||
uPlotInst.scales.x.range = getRangeX;
|
||||
break;
|
||||
case typeChartUpdate.yRange:
|
||||
Object.keys(yaxis.limits.range).forEach(axis => {
|
||||
if (!uPlotInst.scales[axis]) return;
|
||||
uPlotInst.scales[axis].range = (u: uPlot, min = 0, max = 1) => getRangeY(u, min, max, axis);
|
||||
});
|
||||
break;
|
||||
}
|
||||
if (!isPanning) uPlotInst.redraw();
|
||||
};
|
||||
|
||||
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);
|
||||
setXRange({ min: period.start, max: period.end });
|
||||
return u.destroy;
|
||||
}, [uPlotRef.current, series, layoutSize, height, isDarkTheme]);
|
||||
|
||||
const handleTouchStart = (e: TouchEvent) => {
|
||||
if (e.touches.length !== 2) return;
|
||||
e.preventDefault();
|
||||
|
@ -257,19 +233,63 @@ const LineChart: FC<LineChartProps> = ({
|
|||
|
||||
const zoomFactor = dur / 50 * dir;
|
||||
uPlotInst.batch(() => setPlotScale({
|
||||
u: uPlotInst,
|
||||
min: min + zoomFactor,
|
||||
max: max - zoomFactor
|
||||
}));
|
||||
}, [uPlotInst, startTouchDistance, xRange]);
|
||||
|
||||
useEffect(() => updateChart(typeChartUpdate.xRange), [xRange]);
|
||||
useEffect(() => updateChart(typeChartUpdate.yRange), [yaxis]);
|
||||
useEffect(() => {
|
||||
setXRange({ min: period.start, max: period.end });
|
||||
}, [period]);
|
||||
|
||||
useEffect(() => {
|
||||
const show = tooltipIdx.dataIdx !== -1 && tooltipIdx.seriesIdx !== -1;
|
||||
setShowTooltip(show);
|
||||
}, [tooltipIdx, stickyTooltips]);
|
||||
setStickyToolTips([]);
|
||||
setTooltipIdx({ seriesIdx: -1, dataIdx: -1 });
|
||||
if (!uPlotRef.current) return;
|
||||
if (uPlotInst) uPlotInst.destroy();
|
||||
const u = new uPlot(options, data, uPlotRef.current);
|
||||
setUPlotInst(u);
|
||||
setXRange({ min: period.start, max: period.end });
|
||||
return u.destroy;
|
||||
}, [uPlotRef, isDarkTheme]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!uPlotInst) return;
|
||||
uPlotInst.setData(data);
|
||||
uPlotInst.redraw();
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!uPlotInst) return;
|
||||
delSeries(uPlotInst);
|
||||
addSeries(uPlotInst, series);
|
||||
uPlotInst.redraw();
|
||||
}, [series]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!uPlotInst) return;
|
||||
Object.keys(yaxis.limits.range).forEach(axis => {
|
||||
if (!uPlotInst.scales[axis]) return;
|
||||
uPlotInst.scales[axis].range = (u: uPlot, min = 0, max = 1) => getRangeY(u, min, max, axis, yaxis);
|
||||
});
|
||||
uPlotInst.redraw();
|
||||
}, [yaxis]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!uPlotInst) return;
|
||||
uPlotInst.scales.x.range = () => getRangeX(xRange);
|
||||
uPlotInst.redraw();
|
||||
}, [xRange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!uPlotInst) return;
|
||||
uPlotInst.setSize({ width: layoutSize.width || 400, height: height || 500 });
|
||||
uPlotInst.redraw();
|
||||
}, [height, layoutSize]);
|
||||
|
||||
useEffect(() => {
|
||||
setShowTooltip(tooltipIdx.dataIdx !== -1 && tooltipIdx.seriesIdx !== -1);
|
||||
}, [tooltipIdx]);
|
||||
|
||||
useEventListener("click", handleClick);
|
||||
useEventListener("keydown", handleKeyDown);
|
||||
|
@ -293,14 +313,8 @@ const LineChart: FC<LineChartProps> = ({
|
|||
/>
|
||||
{uPlotInst && showTooltip && (
|
||||
<ChartTooltip
|
||||
unit={unit}
|
||||
{...getChartProps()}
|
||||
u={uPlotInst}
|
||||
series={series as SeriesItem[]}
|
||||
metrics={metrics}
|
||||
yRange={yRange}
|
||||
tooltipIdx={tooltipIdx}
|
||||
tooltipOffset={tooltipOffset}
|
||||
id={tooltipId}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ import "./style.scss";
|
|||
import { ReactNode } from "react";
|
||||
import { ExoticComponent } from "react";
|
||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||
import useEventListener from "../../../hooks/useEventListener";
|
||||
|
||||
interface TooltipProps {
|
||||
children: ReactNode
|
||||
|
@ -30,7 +29,6 @@ const Tooltip: FC<TooltipProps> = ({
|
|||
const popperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const onScrollWindow = () => setIsOpen(false);
|
||||
useEventListener("scroll", onScrollWindow);
|
||||
|
||||
useEffect(() => {
|
||||
if (!popperRef.current || !isOpen) return;
|
||||
|
@ -38,6 +36,11 @@ const Tooltip: FC<TooltipProps> = ({
|
|||
width: popperRef.current.clientWidth,
|
||||
height: popperRef.current.clientHeight
|
||||
});
|
||||
window.addEventListener("scroll", onScrollWindow);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", onScrollWindow);
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
const popperStyle = useMemo(() => {
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
import React, { FC, useCallback, useEffect, useMemo, useState } from "preact/compat";
|
||||
import React, { FC, useEffect, useMemo, useState } from "preact/compat";
|
||||
import { MetricResult } from "../../../api/types";
|
||||
import LineChart from "../../Chart/Line/LineChart/LineChart";
|
||||
import { AlignedData as uPlotData, Series as uPlotSeries } from "uplot";
|
||||
import Legend from "../../Chart/Line/Legend/Legend";
|
||||
import LegendHeatmap from "../../Chart/Heatmap/LegendHeatmap/LegendHeatmap";
|
||||
import { getHideSeries, getLegendItem, getSeriesItemContext, SeriesItem } from "../../../utils/uplot/series";
|
||||
import {
|
||||
getHideSeries,
|
||||
getLegendItem,
|
||||
getSeriesItemContext,
|
||||
SeriesItem
|
||||
} from "../../../utils/uplot/series";
|
||||
import { getLimitsYAxis, getMinMaxBuffer, getTimeSeries } from "../../../utils/uplot/axes";
|
||||
import { LegendItemType } from "../../../utils/uplot/types";
|
||||
import { TimeParams } from "../../../types";
|
||||
|
@ -56,7 +61,6 @@ const GraphView: FC<GraphViewProps> = ({
|
|||
const currentStep = useMemo(() => customStep || period.step || "1s", [period.step, customStep]);
|
||||
|
||||
const data = useMemo(() => normalizeData(dataRaw, isHistogram), [isHistogram, dataRaw]);
|
||||
const getSeriesItem = useCallback(getSeriesItemContext(), [data]);
|
||||
|
||||
const [dataChart, setDataChart] = useState<uPlotData>([[]]);
|
||||
const [series, setSeries] = useState<uPlotSeries[]>([]);
|
||||
|
@ -64,6 +68,10 @@ const GraphView: FC<GraphViewProps> = ({
|
|||
const [hideSeries, setHideSeries] = useState<string[]>([]);
|
||||
const [legendValue, setLegendValue] = useState<TooltipHeatmapProps | null>(null);
|
||||
|
||||
const getSeriesItem = useMemo(() => {
|
||||
return getSeriesItemContext(data, hideSeries, alias);
|
||||
}, [data, hideSeries, alias]);
|
||||
|
||||
const setLimitsYaxis = (values: {[key: string]: number[]}) => {
|
||||
const limits = getLimitsYAxis(values, !isHistogram);
|
||||
setYaxisLimits(limits);
|
||||
|
@ -73,10 +81,6 @@ const GraphView: FC<GraphViewProps> = ({
|
|||
setHideSeries(getHideSeries({ hideSeries, legend, metaKey, series }));
|
||||
};
|
||||
|
||||
const handleChangeLegend = (val: TooltipHeatmapProps) => {
|
||||
setLegendValue(val);
|
||||
};
|
||||
|
||||
const prepareHistogramData = (data: (number | null)[][]) => {
|
||||
const values = data.slice(1, data.length);
|
||||
const xs: (number | null | undefined)[] = [];
|
||||
|
@ -105,8 +109,9 @@ const GraphView: FC<GraphViewProps> = ({
|
|||
const tempLegend: LegendItemType[] = [];
|
||||
const tempSeries: uPlotSeries[] = [{}];
|
||||
|
||||
data?.forEach((d) => {
|
||||
const seriesItem = getSeriesItem(d, hideSeries, alias);
|
||||
data?.forEach((d, i) => {
|
||||
const seriesItem = getSeriesItem(d, i);
|
||||
|
||||
tempSeries.push(seriesItem);
|
||||
tempLegend.push(getLegendItem(seriesItem, d.group));
|
||||
const tmpValues = tempValues[d.group] || [];
|
||||
|
@ -156,8 +161,8 @@ const GraphView: FC<GraphViewProps> = ({
|
|||
useEffect(() => {
|
||||
const tempLegend: LegendItemType[] = [];
|
||||
const tempSeries: uPlotSeries[] = [{}];
|
||||
data?.forEach(d => {
|
||||
const seriesItem = getSeriesItem(d, hideSeries, alias);
|
||||
data?.forEach((d, i) => {
|
||||
const seriesItem = getSeriesItem(d, i);
|
||||
tempSeries.push(seriesItem);
|
||||
tempLegend.push(getLegendItem(seriesItem, d.group));
|
||||
});
|
||||
|
@ -199,7 +204,7 @@ const GraphView: FC<GraphViewProps> = ({
|
|||
setPeriod={setPeriod}
|
||||
layoutSize={containerSize}
|
||||
height={height}
|
||||
onChangeLegend={handleChangeLegend}
|
||||
onChangeLegend={setLegendValue}
|
||||
/>
|
||||
)}
|
||||
{!isHistogram && showLegend && (
|
||||
|
|
|
@ -51,6 +51,7 @@
|
|||
font-weight: bold;
|
||||
text-transform: capitalize;
|
||||
text-align: left;
|
||||
overflow-wrap: normal;
|
||||
}
|
||||
|
||||
&_gray {
|
||||
|
|
|
@ -17,7 +17,7 @@ export const dragChart = ({ e, factor = 0.85, u, setPanning, setPlotScale }: Dra
|
|||
|
||||
const clientX = isMouseEvent ? e.clientX : e.touches[0].clientX;
|
||||
const dx = xUnitsPerPx * ((clientX - leftStart) * factor);
|
||||
setPlotScale({ u, min: scXMin - dx, max: scXMax - dx });
|
||||
setPlotScale({ min: scXMin - dx, max: scXMax - dx });
|
||||
};
|
||||
const mouseUp = () => {
|
||||
setPanning(false);
|
||||
|
|
|
@ -145,17 +145,22 @@ const sortBucketsByValues = (a: MetricResult, b: MetricResult) => getUpperBound(
|
|||
|
||||
export const normalizeData = (buckets: MetricResult[], isHistogram?: boolean): MetricResult[] => {
|
||||
if (!isHistogram) return buckets;
|
||||
|
||||
const sortedBuckets = buckets.sort(sortBucketsByValues);
|
||||
const vmBuckets = convertPrometheusToVictoriaMetrics(sortedBuckets);
|
||||
const allValues = vmBuckets.map(b => b.values).flat();
|
||||
|
||||
// Compute total hits for each timestamp upfront
|
||||
const totalHitsPerTimestamp: { [timestamp: number]: number } = {};
|
||||
vmBuckets.forEach(bucket =>
|
||||
bucket.values.forEach(([timestamp, value]) => {
|
||||
totalHitsPerTimestamp[timestamp] = (totalHitsPerTimestamp[timestamp] || 0) + +value;
|
||||
})
|
||||
);
|
||||
|
||||
const result = vmBuckets.map(bucket => {
|
||||
const values = bucket.values.map((v) => {
|
||||
const totalHits = allValues
|
||||
.filter(av => av[0] === v[0])
|
||||
.reduce((bucketSum, v) => bucketSum + +v[1], 0);
|
||||
|
||||
return [v[0], `${Math.round((+v[1] / totalHits) * 100)}`];
|
||||
const values = bucket.values.map(([timestamp, value]) => {
|
||||
const totalHits = totalHitsPerTimestamp[timestamp];
|
||||
return [timestamp, `${Math.round((+value / totalHits) * 100)}`];
|
||||
});
|
||||
|
||||
return { ...bucket, values };
|
||||
|
|
26
app/vmui/packages/vmui/src/utils/uplot/scales.ts
Normal file
26
app/vmui/packages/vmui/src/utils/uplot/scales.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import uPlot, { Range, Scale, Scales } from "uplot";
|
||||
import { getMinMaxBuffer } from "./axes";
|
||||
import { YaxisState } from "../../state/graph/reducer";
|
||||
|
||||
interface XRangeType {
|
||||
min: number,
|
||||
max: number
|
||||
}
|
||||
|
||||
export const getRangeX = (xRange: XRangeType): Range.MinMax => {
|
||||
return [xRange.min, xRange.max];
|
||||
};
|
||||
|
||||
export const getRangeY = (u: uPlot, min = 0, max = 1, axis: string, yaxis: YaxisState): Range.MinMax => {
|
||||
if (yaxis.limits.enable) return yaxis.limits.range[axis];
|
||||
return getMinMaxBuffer(min, max);
|
||||
};
|
||||
|
||||
export const getScales = (yaxis: YaxisState, xRange: XRangeType): Scales => {
|
||||
const scales: { [key: string]: { range: Scale.Range } } = { x: { range: () => getRangeX(xRange) } };
|
||||
const ranges = Object.keys(yaxis.limits.range);
|
||||
(ranges.length ? ranges : ["1"]).forEach(axis => {
|
||||
scales[axis] = { range: (u: uPlot, min = 0, max = 1) => getRangeY(u, min, max, axis, yaxis) };
|
||||
});
|
||||
return scales;
|
||||
};
|
|
@ -17,26 +17,34 @@ export interface SeriesItem extends Series {
|
|||
}
|
||||
}
|
||||
|
||||
export const getSeriesItemContext = () => {
|
||||
export const getSeriesItemContext = (data: MetricResult[], hideSeries: string[], alias: string[]) => {
|
||||
const colorState: {[key: string]: string} = {};
|
||||
|
||||
return (d: MetricResult, hideSeries: string[], alias: string[]): SeriesItem => {
|
||||
const label = getNameForMetric(d, alias[d.group - 1]);
|
||||
const countSavedColors = Object.keys(colorState).length;
|
||||
const hasBasicColors = countSavedColors < baseContrastColors.length;
|
||||
if (hasBasicColors) colorState[label] = colorState[label] || baseContrastColors[countSavedColors];
|
||||
|
||||
const calculations = data.map(d => {
|
||||
const values = d.values.map(v => promValueToNumber(v[1]));
|
||||
const min = getMinFromArray(values);
|
||||
const max = getMaxFromArray(values);
|
||||
const median = getMedianFromArray(values);
|
||||
const last = getLastFromArray(values);
|
||||
return {
|
||||
min: getMinFromArray(values),
|
||||
max: getMaxFromArray(values),
|
||||
median: getMedianFromArray(values),
|
||||
last: getLastFromArray(values),
|
||||
};
|
||||
});
|
||||
|
||||
const maxColors = Math.min(data.length, baseContrastColors.length);
|
||||
for (let i = 0; i < maxColors; i++) {
|
||||
const label = getNameForMetric(data[i], alias[data[i].group - 1]);
|
||||
colorState[label] = baseContrastColors[i];
|
||||
}
|
||||
|
||||
return (d: MetricResult, i: number): SeriesItem => {
|
||||
const label = getNameForMetric(d, alias[d.group - 1]);
|
||||
const color = colorState[label] || getColorFromString(label);
|
||||
const { min, max, median, last } = calculations[i];
|
||||
|
||||
return {
|
||||
label,
|
||||
freeFormFields: d.metric,
|
||||
width: 1.4,
|
||||
stroke: colorState[label] || getColorFromString(label),
|
||||
stroke: color,
|
||||
show: !includesHideSeries(label, hideSeries),
|
||||
scale: "1",
|
||||
points: {
|
||||
|
|
|
@ -12,7 +12,7 @@ export interface DragArgs {
|
|||
u: uPlot,
|
||||
factor: number,
|
||||
setPanning: (enable: boolean) => void,
|
||||
setPlotScale: ({ u, min, max }: { u: uPlot, min: number, max: number }) => void
|
||||
setPlotScale: ({ min, max }: { min: number, max: number }) => void
|
||||
}
|
||||
|
||||
export interface LegendItemType {
|
||||
|
|
|
@ -42,6 +42,7 @@ The following tip changes can be tested by building VictoriaMetrics components f
|
|||
* BUGFIX: [vmagent](https://docs.victoriametrics.com/vmagent.html): fixed service name detection for [consulagent service discovery](https://docs.victoriametrics.com/sd_configs.html?highlight=consulagent#consulagent_sd_configs) in case of a difference in service name and service id. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4390) for details.
|
||||
* BUGFIX: [vmbackupmanager](https://docs.victoriametrics.com/vmbackupmanager.html): fix an issue with `vmbackupmanager` not being able to restore data from a backup stored in GCS. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4420) for details.
|
||||
* BUGFIX: [storage](https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html): Properly creates `parts.json` after migration from versions below `v1.90.0. It must fix errors on start-up after unclean shutdown. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4336) for details.
|
||||
* BUGFIX: [vmui](https://docs.victoriametrics.com/#vmui): fix a memory leak issue associated with chart updates. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/4455).
|
||||
|
||||
## [v1.91.2](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.91.2)
|
||||
|
||||
|
|
Loading…
Reference in a new issue