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:
Yury Molodov 2023-06-20 11:29:24 +02:00 committed by GitHub
parent 7b2748e7a1
commit 66b42a6772
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 227 additions and 185 deletions

View file

@ -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
}));

View file

@ -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 && (

View file

@ -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 && <>&#123;</>}
{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 && <>&#125;</>}
</span>

View file

@ -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}
/>
)}

View file

@ -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(() => {

View file

@ -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 && (

View file

@ -51,6 +51,7 @@
font-weight: bold;
text-transform: capitalize;
text-align: left;
overflow-wrap: normal;
}
&_gray {

View file

@ -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);

View file

@ -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 };

View 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;
};

View file

@ -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: {

View file

@ -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 {

View file

@ -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)