diff --git a/app/vmui/packages/vmui/src/components/Chart/Heatmap/HeatmapChart/HeatmapChart.tsx b/app/vmui/packages/vmui/src/components/Chart/Heatmap/HeatmapChart/HeatmapChart.tsx index f483ff83b..9afa6e411 100644 --- a/app/vmui/packages/vmui/src/components/Chart/Heatmap/HeatmapChart/HeatmapChart.tsx +++ b/app/vmui/packages/vmui/src/components/Chart/Heatmap/HeatmapChart/HeatmapChart.tsx @@ -73,10 +73,9 @@ const HeatmapChart: FC = ({ }); }; 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 = ({ 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 = ({ 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 = ({ (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 = ({ const zoomFactor = dur / 50 * dir; uPlotInst.batch(() => setPlotScale({ - u: uPlotInst, min: min + zoomFactor, max: max - zoomFactor })); diff --git a/app/vmui/packages/vmui/src/components/Chart/Line/ChartTooltip/ChartTooltip.tsx b/app/vmui/packages/vmui/src/components/Chart/Line/ChartTooltip/ChartTooltip.tsx index 1c7e21d47..4866be6b6 100644 --- a/app/vmui/packages/vmui/src/components/Chart/Line/ChartTooltip/ChartTooltip.tsx +++ b/app/vmui/packages/vmui/src/components/Chart/Line/ChartTooltip/ChartTooltip.tsx @@ -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 = ({ u, id, unit = "", - metrics, - series, - yRange, + metricItem, + seriesItem, tooltipIdx, tooltipOffset, isSticky, + showQueryNum, onClose }) => { const tooltipRef = useRef(null); @@ -49,21 +49,17 @@ const ChartTooltip: FC = ({ 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 = ({ metricName += "{" + labels.join(",") + "}"; } return metricName; - }, [metrics, seriesIdx]); + }, [metricItem]); const handleClose = () => { onClose && onClose(id); @@ -97,7 +93,7 @@ const ChartTooltip: FC = ({ 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 = ({ >
- {showQueryNum && ( -
Query {group}
- )} + {showQueryNum && (
Query {group}
)} {date}
{isSticky && ( diff --git a/app/vmui/packages/vmui/src/components/Chart/Line/Legend/LegendItem/LegendItem.tsx b/app/vmui/packages/vmui/src/components/Chart/Line/Legend/LegendItem/LegendItem.tsx index 7893d7ab7..d455d98c5 100644 --- a/app/vmui/packages/vmui/src/components/Chart/Line/Legend/LegendItem/LegendItem.tsx +++ b/app/vmui/packages/vmui/src/components/Chart/Line/Legend/LegendItem/LegendItem.tsx @@ -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 = ({ legend, onChange, isHeatmap }) => { const copyToClipboard = useCopyToClipboard(); - const [copiedValue, setCopiedValue] = useState(""); const freeFormFields = useMemo(() => { const result = getFreeFields(legend); @@ -25,20 +23,17 @@ const LegendItem: FC = ({ 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) => { onChange && onChange(legend, e.ctrlKey || e.metaKey); }; - const createHandlerCopy = (freeField: string, id: string) => (e: MouseEvent) => { + const createHandlerCopy = (freeField: string) => (e: MouseEvent) => { e.stopPropagation(); - handleClickFreeField(freeField, id); + handleClickFreeField(freeField); }; return ( @@ -62,21 +57,14 @@ const LegendItem: FC = ({ legend, onChange, isHeatmap }) => { {legend.freeFormFields["__name__"]} {!!freeFormFields.length && <>{} {freeFormFields.map((f, i) => ( - - - {f.freeField}{i + 1 < freeFormFields.length && ","} - - + {f.freeField}{i + 1 < freeFormFields.length && ","} + ))} {!!freeFormFields.length && <>}} diff --git a/app/vmui/packages/vmui/src/components/Chart/Line/LineChart/LineChart.tsx b/app/vmui/packages/vmui/src/components/Chart/Line/LineChart/LineChart.tsx index e47d6cfba..d1fbc9c64 100644 --- a/app/vmui/packages/vmui/src/components/Chart/Line/LineChart/LineChart.tsx +++ b/app/vmui/packages/vmui/src/components/Chart/Line/LineChart/LineChart.tsx @@ -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 = ({ data, series, @@ -55,7 +50,6 @@ const LineChart: FC = ({ const uPlotRef = useRef(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(); const [startTouchDistance, setStartTouchDistance] = useState(0); @@ -63,24 +57,18 @@ const LineChart: FC = ({ const [tooltipIdx, setTooltipIdx] = useState({ seriesIdx: -1, dataIdx: -1 }); const [tooltipOffset, setTooltipOffset] = useState({ left: 0, top: 0 }); const [stickyTooltips, setStickyToolTips] = useState([]); - 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ /> {uPlotInst && showTooltip && ( )} diff --git a/app/vmui/packages/vmui/src/components/Main/Tooltip/Tooltip.tsx b/app/vmui/packages/vmui/src/components/Main/Tooltip/Tooltip.tsx index 30ad88f1b..29aed9537 100644 --- a/app/vmui/packages/vmui/src/components/Main/Tooltip/Tooltip.tsx +++ b/app/vmui/packages/vmui/src/components/Main/Tooltip/Tooltip.tsx @@ -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 = ({ const popperRef = useRef(null); const onScrollWindow = () => setIsOpen(false); - useEventListener("scroll", onScrollWindow); useEffect(() => { if (!popperRef.current || !isOpen) return; @@ -38,6 +36,11 @@ const Tooltip: FC = ({ width: popperRef.current.clientWidth, height: popperRef.current.clientHeight }); + window.addEventListener("scroll", onScrollWindow); + + return () => { + window.removeEventListener("scroll", onScrollWindow); + }; }, [isOpen]); const popperStyle = useMemo(() => { diff --git a/app/vmui/packages/vmui/src/components/Views/GraphView/GraphView.tsx b/app/vmui/packages/vmui/src/components/Views/GraphView/GraphView.tsx index 1cdab1387..2bbb027a2 100644 --- a/app/vmui/packages/vmui/src/components/Views/GraphView/GraphView.tsx +++ b/app/vmui/packages/vmui/src/components/Views/GraphView/GraphView.tsx @@ -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 = ({ 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([[]]); const [series, setSeries] = useState([]); @@ -64,6 +68,10 @@ const GraphView: FC = ({ const [hideSeries, setHideSeries] = useState([]); const [legendValue, setLegendValue] = useState(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 = ({ 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 = ({ 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 = ({ 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 = ({ setPeriod={setPeriod} layoutSize={containerSize} height={height} - onChangeLegend={handleChangeLegend} + onChangeLegend={setLegendValue} /> )} {!isHistogram && showLegend && ( diff --git a/app/vmui/packages/vmui/src/styles/components/table.scss b/app/vmui/packages/vmui/src/styles/components/table.scss index 68c26fc8c..22dbe2b71 100644 --- a/app/vmui/packages/vmui/src/styles/components/table.scss +++ b/app/vmui/packages/vmui/src/styles/components/table.scss @@ -51,6 +51,7 @@ font-weight: bold; text-transform: capitalize; text-align: left; + overflow-wrap: normal; } &_gray { diff --git a/app/vmui/packages/vmui/src/utils/uplot/events.ts b/app/vmui/packages/vmui/src/utils/uplot/events.ts index 6410f701f..9bf4d3c11 100644 --- a/app/vmui/packages/vmui/src/utils/uplot/events.ts +++ b/app/vmui/packages/vmui/src/utils/uplot/events.ts @@ -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); diff --git a/app/vmui/packages/vmui/src/utils/uplot/heatmap.ts b/app/vmui/packages/vmui/src/utils/uplot/heatmap.ts index 0f57a3ce2..f95efc1a7 100644 --- a/app/vmui/packages/vmui/src/utils/uplot/heatmap.ts +++ b/app/vmui/packages/vmui/src/utils/uplot/heatmap.ts @@ -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 }; diff --git a/app/vmui/packages/vmui/src/utils/uplot/scales.ts b/app/vmui/packages/vmui/src/utils/uplot/scales.ts new file mode 100644 index 000000000..59a7535af --- /dev/null +++ b/app/vmui/packages/vmui/src/utils/uplot/scales.ts @@ -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; +}; diff --git a/app/vmui/packages/vmui/src/utils/uplot/series.ts b/app/vmui/packages/vmui/src/utils/uplot/series.ts index f8a45315d..36e751c00 100644 --- a/app/vmui/packages/vmui/src/utils/uplot/series.ts +++ b/app/vmui/packages/vmui/src/utils/uplot/series.ts @@ -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: { diff --git a/app/vmui/packages/vmui/src/utils/uplot/types.ts b/app/vmui/packages/vmui/src/utils/uplot/types.ts index c8c9ddba3..083b67c97 100644 --- a/app/vmui/packages/vmui/src/utils/uplot/types.ts +++ b/app/vmui/packages/vmui/src/utils/uplot/types.ts @@ -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 { diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 987dfac9d..aa99769bb 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -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)