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 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;
|
const delta = (max - min) * 1000;
|
||||||
if ((delta < limitsDurations.min) || (delta > limitsDurations.max)) return;
|
if ((delta < limitsDurations.min) || (delta > limitsDurations.max)) return;
|
||||||
u.setScale("x", { min, max });
|
|
||||||
setXRange({ min, max });
|
setXRange({ min, max });
|
||||||
throttledSetScale({ min, max });
|
throttledSetScale({ min, max });
|
||||||
};
|
};
|
||||||
|
@ -112,7 +111,7 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
|
||||||
const nxRange = e.deltaY < 0 ? oxRange * factor : oxRange / factor;
|
const nxRange = e.deltaY < 0 ? oxRange * factor : oxRange / factor;
|
||||||
const min = xVal - (zoomPos / width) * nxRange;
|
const min = xVal - (zoomPos / width) * nxRange;
|
||||||
const max = min + 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();
|
e.preventDefault();
|
||||||
const factor = (xRange.max - xRange.min) / 10 * (plus ? 1 : -1);
|
const factor = (xRange.max - xRange.min) / 10 * (plus ? 1 : -1);
|
||||||
setPlotScale({
|
setPlotScale({
|
||||||
u: uPlotInst,
|
|
||||||
min: xRange.min + factor,
|
min: xRange.min + factor,
|
||||||
max: xRange.max - factor
|
max: xRange.max - factor
|
||||||
});
|
});
|
||||||
|
@ -241,7 +239,7 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
|
||||||
(u) => {
|
(u) => {
|
||||||
const min = u.posToVal(u.select.left, "x");
|
const min = u.posToVal(u.select.left, "x");
|
||||||
const max = u.posToVal(u.select.left + u.select.width, "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;
|
const zoomFactor = dur / 50 * dir;
|
||||||
uPlotInst.batch(() => setPlotScale({
|
uPlotInst.batch(() => setPlotScale({
|
||||||
u: uPlotInst,
|
|
||||||
min: min + zoomFactor,
|
min: min + zoomFactor,
|
||||||
max: max - zoomFactor
|
max: max - zoomFactor
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -17,11 +17,11 @@ import useEventListener from "../../../../hooks/useEventListener";
|
||||||
export interface ChartTooltipProps {
|
export interface ChartTooltipProps {
|
||||||
id: string,
|
id: string,
|
||||||
u: uPlot,
|
u: uPlot,
|
||||||
metrics: MetricResult[],
|
metricItem: MetricResult,
|
||||||
series: SeriesItem[],
|
seriesItem: SeriesItem,
|
||||||
yRange: number[];
|
|
||||||
unit?: string,
|
unit?: string,
|
||||||
isSticky?: boolean,
|
isSticky?: boolean,
|
||||||
|
showQueryNum?: boolean,
|
||||||
tooltipOffset: { left: number, top: number },
|
tooltipOffset: { left: number, top: number },
|
||||||
tooltipIdx: { seriesIdx: number, dataIdx: number },
|
tooltipIdx: { seriesIdx: number, dataIdx: number },
|
||||||
onClose?: (id: string) => void
|
onClose?: (id: string) => void
|
||||||
|
@ -31,12 +31,12 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
|
||||||
u,
|
u,
|
||||||
id,
|
id,
|
||||||
unit = "",
|
unit = "",
|
||||||
metrics,
|
metricItem,
|
||||||
series,
|
seriesItem,
|
||||||
yRange,
|
|
||||||
tooltipIdx,
|
tooltipIdx,
|
||||||
tooltipOffset,
|
tooltipOffset,
|
||||||
isSticky,
|
isSticky,
|
||||||
|
showQueryNum,
|
||||||
onClose
|
onClose
|
||||||
}) => {
|
}) => {
|
||||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||||
|
@ -49,21 +49,17 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
|
||||||
const [dataIdx, setDataIdx] = useState(tooltipIdx.dataIdx);
|
const [dataIdx, setDataIdx] = useState(tooltipIdx.dataIdx);
|
||||||
|
|
||||||
const value = get(u, ["data", seriesIdx, dataIdx], 0);
|
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 dataTime = u.data[0][dataIdx];
|
||||||
const date = dayjs(dataTime * 1000).tz().format(DATE_FULL_TIMEZONE_FORMAT);
|
const date = dayjs(dataTime * 1000).tz().format(DATE_FULL_TIMEZONE_FORMAT);
|
||||||
|
|
||||||
const color = series[seriesIdx]?.stroke+"";
|
const color = `${seriesItem?.stroke}`;
|
||||||
|
const calculations = seriesItem?.calculations || {};
|
||||||
const calculations = series[seriesIdx]?.calculations || {};
|
const group = metricItem?.group || 0;
|
||||||
|
|
||||||
const groups = new Set(metrics.map(m => m.group));
|
|
||||||
const showQueryNum = groups.size > 1;
|
|
||||||
const group = metrics[seriesIdx-1]?.group || 0;
|
|
||||||
|
|
||||||
|
|
||||||
const fullMetricName = useMemo(() => {
|
const fullMetricName = useMemo(() => {
|
||||||
const metric = metrics[seriesIdx-1]?.metric || {};
|
const metric = metricItem?.metric || {};
|
||||||
const labelNames = Object.keys(metric).filter(x => x != "__name__");
|
const labelNames = Object.keys(metric).filter(x => x != "__name__");
|
||||||
const labels = labelNames.map(key => `${key}=${JSON.stringify(metric[key])}`);
|
const labels = labelNames.map(key => `${key}=${JSON.stringify(metric[key])}`);
|
||||||
let metricName = metric["__name__"] || "";
|
let metricName = metric["__name__"] || "";
|
||||||
|
@ -71,7 +67,7 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
|
||||||
metricName += "{" + labels.join(",") + "}";
|
metricName += "{" + labels.join(",") + "}";
|
||||||
}
|
}
|
||||||
return metricName;
|
return metricName;
|
||||||
}, [metrics, seriesIdx]);
|
}, [metricItem]);
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
onClose && onClose(id);
|
onClose && onClose(id);
|
||||||
|
@ -97,7 +93,7 @@ const ChartTooltip: FC<ChartTooltipProps> = ({
|
||||||
const calcPosition = () => {
|
const calcPosition = () => {
|
||||||
if (!tooltipRef.current) return;
|
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 leftOnChart = u.valToPos(dataTime, "x");
|
||||||
const { width: tooltipWidth, height: tooltipHeight } = tooltipRef.current.getBoundingClientRect();
|
const { width: tooltipWidth, height: tooltipHeight } = tooltipRef.current.getBoundingClientRect();
|
||||||
const { width, height } = u.over.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">
|
||||||
<div className="vm-chart-tooltip-header__date">
|
<div className="vm-chart-tooltip-header__date">
|
||||||
{showQueryNum && (
|
{showQueryNum && (<div>Query {group}</div>)}
|
||||||
<div>Query {group}</div>
|
|
||||||
)}
|
|
||||||
{date}
|
{date}
|
||||||
</div>
|
</div>
|
||||||
{isSticky && (
|
{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 { MouseEvent } from "react";
|
||||||
import { LegendItemType } from "../../../../../utils/uplot/types";
|
import { LegendItemType } from "../../../../../utils/uplot/types";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import Tooltip from "../../../../Main/Tooltip/Tooltip";
|
|
||||||
import { getFreeFields } from "./helpers";
|
import { getFreeFields } from "./helpers";
|
||||||
import useCopyToClipboard from "../../../../../hooks/useCopyToClipboard";
|
import useCopyToClipboard from "../../../../../hooks/useCopyToClipboard";
|
||||||
|
|
||||||
|
@ -15,7 +14,6 @@ interface LegendItemProps {
|
||||||
|
|
||||||
const LegendItem: FC<LegendItemProps> = ({ legend, onChange, isHeatmap }) => {
|
const LegendItem: FC<LegendItemProps> = ({ legend, onChange, isHeatmap }) => {
|
||||||
const copyToClipboard = useCopyToClipboard();
|
const copyToClipboard = useCopyToClipboard();
|
||||||
const [copiedValue, setCopiedValue] = useState("");
|
|
||||||
|
|
||||||
const freeFormFields = useMemo(() => {
|
const freeFormFields = useMemo(() => {
|
||||||
const result = getFreeFields(legend);
|
const result = getFreeFields(legend);
|
||||||
|
@ -25,20 +23,17 @@ const LegendItem: FC<LegendItemProps> = ({ legend, onChange, isHeatmap }) => {
|
||||||
const calculations = legend.calculations;
|
const calculations = legend.calculations;
|
||||||
const showCalculations = Object.values(calculations).some(v => v);
|
const showCalculations = Object.values(calculations).some(v => v);
|
||||||
|
|
||||||
const handleClickFreeField = async (val: string, id: string) => {
|
const handleClickFreeField = async (val: string) => {
|
||||||
const copied = await copyToClipboard(val);
|
await copyToClipboard(val, `${val} has been copied`);
|
||||||
if (!copied) return;
|
|
||||||
setCopiedValue(id);
|
|
||||||
setTimeout(() => setCopiedValue(""), 2000);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const createHandlerClick = (legend: LegendItemType) => (e: MouseEvent<HTMLDivElement>) => {
|
const createHandlerClick = (legend: LegendItemType) => (e: MouseEvent<HTMLDivElement>) => {
|
||||||
onChange && onChange(legend, e.ctrlKey || e.metaKey);
|
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();
|
e.stopPropagation();
|
||||||
handleClickFreeField(freeField, id);
|
handleClickFreeField(freeField);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -62,21 +57,14 @@ const LegendItem: FC<LegendItemProps> = ({ legend, onChange, isHeatmap }) => {
|
||||||
{legend.freeFormFields["__name__"]}
|
{legend.freeFormFields["__name__"]}
|
||||||
{!!freeFormFields.length && <>{</>}
|
{!!freeFormFields.length && <>{</>}
|
||||||
{freeFormFields.map((f, i) => (
|
{freeFormFields.map((f, i) => (
|
||||||
<Tooltip
|
|
||||||
key={f.id}
|
|
||||||
open={copiedValue === f.id}
|
|
||||||
title={"copied!"}
|
|
||||||
placement="top-center"
|
|
||||||
>
|
|
||||||
<span
|
<span
|
||||||
className="vm-legend-item-info__free-fields"
|
className="vm-legend-item-info__free-fields"
|
||||||
key={f.key}
|
key={f.key}
|
||||||
onClick={createHandlerCopy(f.freeField, f.id)}
|
onClick={createHandlerCopy(f.freeField)}
|
||||||
title="copy to clipboard"
|
title="copy to clipboard"
|
||||||
>
|
>
|
||||||
{f.freeField}{i + 1 < freeFormFields.length && ","}
|
{f.freeField}{i + 1 < freeFormFields.length && ","}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
|
||||||
))}
|
))}
|
||||||
{!!freeFormFields.length && <>}</>}
|
{!!freeFormFields.length && <>}</>}
|
||||||
</span>
|
</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, {
|
import uPlot, {
|
||||||
AlignedData as uPlotData,
|
AlignedData as uPlotData,
|
||||||
Options as uPlotOptions,
|
Options as uPlotOptions,
|
||||||
Series as uPlotSeries,
|
Series as uPlotSeries,
|
||||||
Range,
|
|
||||||
Scales,
|
|
||||||
Scale,
|
|
||||||
} from "uplot";
|
} 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 } from "../../../../utils/uplot/axes";
|
||||||
import { MetricResult } from "../../../../api/types";
|
import { MetricResult } from "../../../../api/types";
|
||||||
import { dateFromSeconds, formatDateForNativeInput, limitsDurations } from "../../../../utils/time";
|
import { dateFromSeconds, formatDateForNativeInput, limitsDurations } from "../../../../utils/time";
|
||||||
import throttle from "lodash.throttle";
|
|
||||||
import { TimeParams } from "../../../../types";
|
import { TimeParams } from "../../../../types";
|
||||||
import { YaxisState } from "../../../../state/graph/reducer";
|
import { YaxisState } from "../../../../state/graph/reducer";
|
||||||
import "uplot/dist/uPlot.min.css";
|
import "uplot/dist/uPlot.min.css";
|
||||||
|
@ -24,6 +20,7 @@ import { useAppState } from "../../../../state/common/StateContext";
|
||||||
import { SeriesItem } from "../../../../utils/uplot/series";
|
import { SeriesItem } from "../../../../utils/uplot/series";
|
||||||
import { ElementSize } from "../../../../hooks/useElementSize";
|
import { ElementSize } from "../../../../hooks/useElementSize";
|
||||||
import useEventListener from "../../../../hooks/useEventListener";
|
import useEventListener from "../../../../hooks/useEventListener";
|
||||||
|
import { getRangeX, getRangeY, getScales } from "../../../../utils/uplot/scales";
|
||||||
|
|
||||||
export interface LineChartProps {
|
export interface LineChartProps {
|
||||||
metrics: MetricResult[];
|
metrics: MetricResult[];
|
||||||
|
@ -37,8 +34,6 @@ export interface LineChartProps {
|
||||||
height?: number;
|
height?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum typeChartUpdate {xRange = "xRange", yRange = "yRange"}
|
|
||||||
|
|
||||||
const LineChart: FC<LineChartProps> = ({
|
const LineChart: FC<LineChartProps> = ({
|
||||||
data,
|
data,
|
||||||
series,
|
series,
|
||||||
|
@ -55,7 +50,6 @@ const LineChart: FC<LineChartProps> = ({
|
||||||
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 [yRange, setYRange] = useState([0, 1]);
|
|
||||||
const [uPlotInst, setUPlotInst] = useState<uPlot>();
|
const [uPlotInst, setUPlotInst] = useState<uPlot>();
|
||||||
const [startTouchDistance, setStartTouchDistance] = useState(0);
|
const [startTouchDistance, setStartTouchDistance] = useState(0);
|
||||||
|
|
||||||
|
@ -63,24 +57,18 @@ const LineChart: FC<LineChartProps> = ({
|
||||||
const [tooltipIdx, setTooltipIdx] = useState({ seriesIdx: -1, dataIdx: -1 });
|
const [tooltipIdx, setTooltipIdx] = useState({ seriesIdx: -1, dataIdx: -1 });
|
||||||
const [tooltipOffset, setTooltipOffset] = useState({ left: 0, top: 0 });
|
const [tooltipOffset, setTooltipOffset] = useState({ left: 0, top: 0 });
|
||||||
const [stickyTooltips, setStickyToolTips] = useState<ChartTooltipProps[]>([]);
|
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({
|
setPeriod({
|
||||||
from: dayjs(min * 1000).toDate(),
|
from: dayjs(min * 1000).toDate(),
|
||||||
to: dayjs(max * 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;
|
const factor = 0.9;
|
||||||
setTooltipOffset({
|
setTooltipOffset({
|
||||||
left: parseFloat(u.over.style.left),
|
left: parseFloat(u.over.style.left),
|
||||||
|
@ -111,7 +99,7 @@ const LineChart: FC<LineChartProps> = ({
|
||||||
const nxRange = e.deltaY < 0 ? oxRange * factor : oxRange / factor;
|
const nxRange = e.deltaY < 0 ? oxRange * factor : oxRange / factor;
|
||||||
const min = xVal - (zoomPos / width) * nxRange;
|
const min = xVal - (zoomPos / width) * nxRange;
|
||||||
const max = min + 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();
|
e.preventDefault();
|
||||||
const factor = (xRange.max - xRange.min) / 10 * (plus ? 1 : -1);
|
const factor = (xRange.max - xRange.min) / 10 * (plus ? 1 : -1);
|
||||||
setPlotScale({
|
setPlotScale({
|
||||||
u: uPlotInst,
|
|
||||||
min: xRange.min + factor,
|
min: xRange.min + factor,
|
||||||
max: xRange.max - factor
|
max: xRange.max - factor
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [uPlotInst, xRange]);
|
}, [uPlotInst, xRange]);
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
const getChartProps = useCallback(() => {
|
||||||
if (!showTooltip) return;
|
const { seriesIdx, dataIdx } = tooltipIdx;
|
||||||
const id = `${tooltipIdx.seriesIdx}_${tooltipIdx.dataIdx}`;
|
const id = `${seriesIdx}_${dataIdx}`;
|
||||||
const props = {
|
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,
|
id,
|
||||||
unit,
|
unit,
|
||||||
series,
|
seriesItem,
|
||||||
metrics,
|
metricItem,
|
||||||
yRange,
|
|
||||||
tooltipIdx,
|
tooltipIdx,
|
||||||
tooltipOffset,
|
tooltipOffset,
|
||||||
|
showQueryNum,
|
||||||
};
|
};
|
||||||
|
}, [uPlotInst, metrics, series, tooltipIdx, tooltipOffset, unit]);
|
||||||
|
|
||||||
if (!stickyTooltips.find(t => t.id === id)) {
|
const handleClick = useCallback(() => {
|
||||||
const tooltipProps = JSON.parse(JSON.stringify(props));
|
if (!showTooltip) return;
|
||||||
setStickyToolTips(prev => [...prev, tooltipProps]);
|
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));
|
setStickyToolTips(prev => prev.filter(t => t.id !== id));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -165,23 +161,34 @@ const LineChart: FC<LineChartProps> = ({
|
||||||
setTooltipIdx(prev => ({ ...prev, seriesIdx }));
|
setTooltipIdx(prev => ({ ...prev, seriesIdx }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRangeX = (): Range.MinMax => [xRange.min, xRange.max];
|
const addSeries = (u: uPlot, series: uPlotSeries[]) => {
|
||||||
|
series.forEach((s) => {
|
||||||
const getRangeY = (u: uPlot, min = 0, max = 1, axis: string): Range.MinMax => {
|
u.addSeries(s);
|
||||||
if (axis == "1") {
|
});
|
||||||
setYRange([min, max]);
|
|
||||||
}
|
|
||||||
if (yaxis.limits.enable) return yaxis.limits.range[axis];
|
|
||||||
return getMinMaxBuffer(min, max);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getScales = (): Scales => {
|
const delSeries = (u: uPlot) => {
|
||||||
const scales: { [key: string]: { range: Scale.Range } } = { x: { range: getRangeX } };
|
for (let i = u.series.length - 1; i >= 0; i--) {
|
||||||
const ranges = Object.keys(yaxis.limits.range);
|
u.delSeries(i);
|
||||||
(ranges.length ? ranges : ["1"]).forEach(axis => {
|
}
|
||||||
scales[axis] = { range: (u: uPlot, min = 0, max = 1) => getRangeY(u, min, max, axis) };
|
};
|
||||||
|
|
||||||
|
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 = {
|
const options: uPlotOptions = {
|
||||||
|
@ -189,49 +196,18 @@ const LineChart: FC<LineChartProps> = ({
|
||||||
tzDate: ts => dayjs(formatDateForNativeInput(dateFromSeconds(ts))).local().toDate(),
|
tzDate: ts => dayjs(formatDateForNativeInput(dateFromSeconds(ts))).local().toDate(),
|
||||||
series,
|
series,
|
||||||
axes: getAxes( [{}, { scale: "1" }], unit),
|
axes: getAxes( [{}, { scale: "1" }], unit),
|
||||||
scales: { ...getScales() },
|
scales: getScales(yaxis, xRange),
|
||||||
width: layoutSize.width || 400,
|
width: layoutSize.width || 400,
|
||||||
height: height || 500,
|
height: height || 500,
|
||||||
plugins: [{ hooks: { ready: onReadyChart, setCursor, setSeries: seriesFocus } }],
|
|
||||||
hooks: {
|
hooks: {
|
||||||
setSelect: [
|
ready: [onReadyChart],
|
||||||
(u) => {
|
setSeries: [seriesFocus],
|
||||||
const min = u.posToVal(u.select.left, "x");
|
setCursor: [setCursor],
|
||||||
const max = u.posToVal(u.select.left + u.select.width, "x");
|
setSelect: [setSelect],
|
||||||
setPlotScale({ u, min, max });
|
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) => {
|
const handleTouchStart = (e: TouchEvent) => {
|
||||||
if (e.touches.length !== 2) return;
|
if (e.touches.length !== 2) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -257,19 +233,63 @@ const LineChart: FC<LineChartProps> = ({
|
||||||
|
|
||||||
const zoomFactor = dur / 50 * dir;
|
const zoomFactor = dur / 50 * dir;
|
||||||
uPlotInst.batch(() => setPlotScale({
|
uPlotInst.batch(() => setPlotScale({
|
||||||
u: uPlotInst,
|
|
||||||
min: min + zoomFactor,
|
min: min + zoomFactor,
|
||||||
max: max - zoomFactor
|
max: max - zoomFactor
|
||||||
}));
|
}));
|
||||||
}, [uPlotInst, startTouchDistance, xRange]);
|
}, [uPlotInst, startTouchDistance, xRange]);
|
||||||
|
|
||||||
useEffect(() => updateChart(typeChartUpdate.xRange), [xRange]);
|
useEffect(() => {
|
||||||
useEffect(() => updateChart(typeChartUpdate.yRange), [yaxis]);
|
setXRange({ min: period.start, max: period.end });
|
||||||
|
}, [period]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const show = tooltipIdx.dataIdx !== -1 && tooltipIdx.seriesIdx !== -1;
|
setStickyToolTips([]);
|
||||||
setShowTooltip(show);
|
setTooltipIdx({ seriesIdx: -1, dataIdx: -1 });
|
||||||
}, [tooltipIdx, stickyTooltips]);
|
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("click", handleClick);
|
||||||
useEventListener("keydown", handleKeyDown);
|
useEventListener("keydown", handleKeyDown);
|
||||||
|
@ -293,14 +313,8 @@ const LineChart: FC<LineChartProps> = ({
|
||||||
/>
|
/>
|
||||||
{uPlotInst && showTooltip && (
|
{uPlotInst && showTooltip && (
|
||||||
<ChartTooltip
|
<ChartTooltip
|
||||||
unit={unit}
|
{...getChartProps()}
|
||||||
u={uPlotInst}
|
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 { ReactNode } from "react";
|
||||||
import { ExoticComponent } from "react";
|
import { ExoticComponent } from "react";
|
||||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||||
import useEventListener from "../../../hooks/useEventListener";
|
|
||||||
|
|
||||||
interface TooltipProps {
|
interface TooltipProps {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
|
@ -30,7 +29,6 @@ const Tooltip: FC<TooltipProps> = ({
|
||||||
const popperRef = useRef<HTMLDivElement>(null);
|
const popperRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const onScrollWindow = () => setIsOpen(false);
|
const onScrollWindow = () => setIsOpen(false);
|
||||||
useEventListener("scroll", onScrollWindow);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!popperRef.current || !isOpen) return;
|
if (!popperRef.current || !isOpen) return;
|
||||||
|
@ -38,6 +36,11 @@ const Tooltip: FC<TooltipProps> = ({
|
||||||
width: popperRef.current.clientWidth,
|
width: popperRef.current.clientWidth,
|
||||||
height: popperRef.current.clientHeight
|
height: popperRef.current.clientHeight
|
||||||
});
|
});
|
||||||
|
window.addEventListener("scroll", onScrollWindow);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("scroll", onScrollWindow);
|
||||||
|
};
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
const popperStyle = useMemo(() => {
|
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 { MetricResult } from "../../../api/types";
|
||||||
import LineChart from "../../Chart/Line/LineChart/LineChart";
|
import LineChart from "../../Chart/Line/LineChart/LineChart";
|
||||||
import { AlignedData as uPlotData, Series as uPlotSeries } from "uplot";
|
import { AlignedData as uPlotData, Series as uPlotSeries } from "uplot";
|
||||||
import Legend from "../../Chart/Line/Legend/Legend";
|
import Legend from "../../Chart/Line/Legend/Legend";
|
||||||
import LegendHeatmap from "../../Chart/Heatmap/LegendHeatmap/LegendHeatmap";
|
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 { getLimitsYAxis, getMinMaxBuffer, getTimeSeries } from "../../../utils/uplot/axes";
|
||||||
import { LegendItemType } from "../../../utils/uplot/types";
|
import { LegendItemType } from "../../../utils/uplot/types";
|
||||||
import { TimeParams } from "../../../types";
|
import { TimeParams } from "../../../types";
|
||||||
|
@ -56,7 +61,6 @@ const GraphView: FC<GraphViewProps> = ({
|
||||||
const currentStep = useMemo(() => customStep || period.step || "1s", [period.step, customStep]);
|
const currentStep = useMemo(() => customStep || period.step || "1s", [period.step, customStep]);
|
||||||
|
|
||||||
const data = useMemo(() => normalizeData(dataRaw, isHistogram), [isHistogram, dataRaw]);
|
const data = useMemo(() => normalizeData(dataRaw, isHistogram), [isHistogram, dataRaw]);
|
||||||
const getSeriesItem = useCallback(getSeriesItemContext(), [data]);
|
|
||||||
|
|
||||||
const [dataChart, setDataChart] = useState<uPlotData>([[]]);
|
const [dataChart, setDataChart] = useState<uPlotData>([[]]);
|
||||||
const [series, setSeries] = useState<uPlotSeries[]>([]);
|
const [series, setSeries] = useState<uPlotSeries[]>([]);
|
||||||
|
@ -64,6 +68,10 @@ const GraphView: FC<GraphViewProps> = ({
|
||||||
const [hideSeries, setHideSeries] = useState<string[]>([]);
|
const [hideSeries, setHideSeries] = useState<string[]>([]);
|
||||||
const [legendValue, setLegendValue] = useState<TooltipHeatmapProps | null>(null);
|
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 setLimitsYaxis = (values: {[key: string]: number[]}) => {
|
||||||
const limits = getLimitsYAxis(values, !isHistogram);
|
const limits = getLimitsYAxis(values, !isHistogram);
|
||||||
setYaxisLimits(limits);
|
setYaxisLimits(limits);
|
||||||
|
@ -73,10 +81,6 @@ const GraphView: FC<GraphViewProps> = ({
|
||||||
setHideSeries(getHideSeries({ hideSeries, legend, metaKey, series }));
|
setHideSeries(getHideSeries({ hideSeries, legend, metaKey, series }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChangeLegend = (val: TooltipHeatmapProps) => {
|
|
||||||
setLegendValue(val);
|
|
||||||
};
|
|
||||||
|
|
||||||
const prepareHistogramData = (data: (number | null)[][]) => {
|
const prepareHistogramData = (data: (number | null)[][]) => {
|
||||||
const values = data.slice(1, data.length);
|
const values = data.slice(1, data.length);
|
||||||
const xs: (number | null | undefined)[] = [];
|
const xs: (number | null | undefined)[] = [];
|
||||||
|
@ -105,8 +109,9 @@ const GraphView: FC<GraphViewProps> = ({
|
||||||
const tempLegend: LegendItemType[] = [];
|
const tempLegend: LegendItemType[] = [];
|
||||||
const tempSeries: uPlotSeries[] = [{}];
|
const tempSeries: uPlotSeries[] = [{}];
|
||||||
|
|
||||||
data?.forEach((d) => {
|
data?.forEach((d, i) => {
|
||||||
const seriesItem = getSeriesItem(d, hideSeries, alias);
|
const seriesItem = getSeriesItem(d, i);
|
||||||
|
|
||||||
tempSeries.push(seriesItem);
|
tempSeries.push(seriesItem);
|
||||||
tempLegend.push(getLegendItem(seriesItem, d.group));
|
tempLegend.push(getLegendItem(seriesItem, d.group));
|
||||||
const tmpValues = tempValues[d.group] || [];
|
const tmpValues = tempValues[d.group] || [];
|
||||||
|
@ -156,8 +161,8 @@ const GraphView: FC<GraphViewProps> = ({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const tempLegend: LegendItemType[] = [];
|
const tempLegend: LegendItemType[] = [];
|
||||||
const tempSeries: uPlotSeries[] = [{}];
|
const tempSeries: uPlotSeries[] = [{}];
|
||||||
data?.forEach(d => {
|
data?.forEach((d, i) => {
|
||||||
const seriesItem = getSeriesItem(d, hideSeries, alias);
|
const seriesItem = getSeriesItem(d, i);
|
||||||
tempSeries.push(seriesItem);
|
tempSeries.push(seriesItem);
|
||||||
tempLegend.push(getLegendItem(seriesItem, d.group));
|
tempLegend.push(getLegendItem(seriesItem, d.group));
|
||||||
});
|
});
|
||||||
|
@ -199,7 +204,7 @@ const GraphView: FC<GraphViewProps> = ({
|
||||||
setPeriod={setPeriod}
|
setPeriod={setPeriod}
|
||||||
layoutSize={containerSize}
|
layoutSize={containerSize}
|
||||||
height={height}
|
height={height}
|
||||||
onChangeLegend={handleChangeLegend}
|
onChangeLegend={setLegendValue}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!isHistogram && showLegend && (
|
{!isHistogram && showLegend && (
|
||||||
|
|
|
@ -51,6 +51,7 @@
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
overflow-wrap: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
&_gray {
|
&_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 clientX = isMouseEvent ? e.clientX : e.touches[0].clientX;
|
||||||
const dx = xUnitsPerPx * ((clientX - leftStart) * factor);
|
const dx = xUnitsPerPx * ((clientX - leftStart) * factor);
|
||||||
setPlotScale({ u, min: scXMin - dx, max: scXMax - dx });
|
setPlotScale({ min: scXMin - dx, max: scXMax - dx });
|
||||||
};
|
};
|
||||||
const mouseUp = () => {
|
const mouseUp = () => {
|
||||||
setPanning(false);
|
setPanning(false);
|
||||||
|
|
|
@ -145,17 +145,22 @@ const sortBucketsByValues = (a: MetricResult, b: MetricResult) => getUpperBound(
|
||||||
|
|
||||||
export const normalizeData = (buckets: MetricResult[], isHistogram?: boolean): MetricResult[] => {
|
export const normalizeData = (buckets: MetricResult[], isHistogram?: boolean): MetricResult[] => {
|
||||||
if (!isHistogram) return buckets;
|
if (!isHistogram) return buckets;
|
||||||
|
|
||||||
const sortedBuckets = buckets.sort(sortBucketsByValues);
|
const sortedBuckets = buckets.sort(sortBucketsByValues);
|
||||||
const vmBuckets = convertPrometheusToVictoriaMetrics(sortedBuckets);
|
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 result = vmBuckets.map(bucket => {
|
||||||
const values = bucket.values.map((v) => {
|
const values = bucket.values.map(([timestamp, value]) => {
|
||||||
const totalHits = allValues
|
const totalHits = totalHitsPerTimestamp[timestamp];
|
||||||
.filter(av => av[0] === v[0])
|
return [timestamp, `${Math.round((+value / totalHits) * 100)}`];
|
||||||
.reduce((bucketSum, v) => bucketSum + +v[1], 0);
|
|
||||||
|
|
||||||
return [v[0], `${Math.round((+v[1] / totalHits) * 100)}`];
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return { ...bucket, values };
|
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} = {};
|
const colorState: {[key: string]: string} = {};
|
||||||
|
const calculations = data.map(d => {
|
||||||
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 values = d.values.map(v => promValueToNumber(v[1]));
|
const values = d.values.map(v => promValueToNumber(v[1]));
|
||||||
const min = getMinFromArray(values);
|
return {
|
||||||
const max = getMaxFromArray(values);
|
min: getMinFromArray(values),
|
||||||
const median = getMedianFromArray(values);
|
max: getMaxFromArray(values),
|
||||||
const last = getLastFromArray(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 {
|
return {
|
||||||
label,
|
label,
|
||||||
freeFormFields: d.metric,
|
freeFormFields: d.metric,
|
||||||
width: 1.4,
|
width: 1.4,
|
||||||
stroke: colorState[label] || getColorFromString(label),
|
stroke: color,
|
||||||
show: !includesHideSeries(label, hideSeries),
|
show: !includesHideSeries(label, hideSeries),
|
||||||
scale: "1",
|
scale: "1",
|
||||||
points: {
|
points: {
|
||||||
|
|
|
@ -12,7 +12,7 @@ export interface DragArgs {
|
||||||
u: uPlot,
|
u: uPlot,
|
||||||
factor: number,
|
factor: number,
|
||||||
setPanning: (enable: boolean) => void,
|
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 {
|
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: [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: [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: [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)
|
## [v1.91.2](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.91.2)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue