vmui: implement heatmap improvements (#4078)

* fix: disabled limits for histogram

* fix: add sorted buckets by upper bound

* refactor: move line chart components to folder

* feat: implement heatmap improvements (https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3384#issuecomment-1484023162)

* app/vmselect/vmui: `make vmui-update`

---------

Co-authored-by: Aliaksandr Valialkin <valyala@victoriametrics.com>
This commit is contained in:
Yury Molodov 2023-04-06 07:13:57 +02:00 committed by Aliaksandr Valialkin
parent bf545fcc14
commit 7871ee0e43
No known key found for this signature in database
GPG key ID: A72BEC6CD3D0DED1
24 changed files with 202 additions and 149 deletions

View file

@ -1,14 +1,14 @@
{
"files": {
"main.css": "./static/css/main.ebde9e58.css",
"main.js": "./static/js/main.ee50e2ce.js",
"main.css": "./static/css/main.6fe27a94.css",
"main.js": "./static/js/main.22a4d00b.js",
"static/js/27.c1ccfd29.chunk.js": "./static/js/27.c1ccfd29.chunk.js",
"static/media/Lato-Regular.ttf": "./static/media/Lato-Regular.d714fec1633b69a9c2e9.ttf",
"static/media/Lato-Bold.ttf": "./static/media/Lato-Bold.32360ba4b57802daa4d6.ttf",
"index.html": "./index.html"
},
"entrypoints": [
"static/css/main.ebde9e58.css",
"static/js/main.ee50e2ce.js"
"static/css/main.6fe27a94.css",
"static/js/main.22a4d00b.js"
]
}

View file

@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"/><meta name="theme-color" content="#000000"/><meta name="description" content="UI for VictoriaMetrics"/><link rel="apple-touch-icon" href="./apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"><link rel="manifest" href="./manifest.json"/><title>VM UI</title><script src="./dashboards/index.js" type="module"></script><meta name="twitter:card" content="summary_large_image"><meta name="twitter:image" content="./preview.jpg"><meta name="twitter:title" content="UI for VictoriaMetrics"><meta name="twitter:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta name="twitter:site" content="@VictoriaMetrics"><meta property="og:title" content="Metric explorer for VictoriaMetrics"><meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta property="og:image" content="./preview.jpg"><meta property="og:type" content="website"><script defer="defer" src="./static/js/main.ee50e2ce.js"></script><link href="./static/css/main.ebde9e58.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"/><meta name="theme-color" content="#000000"/><meta name="description" content="UI for VictoriaMetrics"/><link rel="apple-touch-icon" href="./apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"><link rel="manifest" href="./manifest.json"/><title>VM UI</title><script src="./dashboards/index.js" type="module"></script><meta name="twitter:card" content="summary_large_image"><meta name="twitter:image" content="./preview.jpg"><meta name="twitter:title" content="UI for VictoriaMetrics"><meta name="twitter:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta name="twitter:site" content="@VictoriaMetrics"><meta property="og:title" content="Metric explorer for VictoriaMetrics"><meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data"><meta property="og:image" content="./preview.jpg"><meta property="og:type" content="website"><script defer="defer" src="./static/js/main.22a4d00b.js"></script><link href="./static/css/main.6fe27a94.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,18 +1,17 @@
import React, { FC, useEffect, useMemo, useRef, useState } from "preact/compat";
import uPlot from "uplot";
import ReactDOM from "react-dom";
import Button from "../../Main/Button/Button";
import { CloseIcon, DragIcon } from "../../Main/Icons";
import Button from "../../../Main/Button/Button";
import { CloseIcon, DragIcon } from "../../../Main/Icons";
import classNames from "classnames";
import { MouseEvent as ReactMouseEvent } from "react";
import "../ChartTooltip/style.scss";
import "../../Line/ChartTooltip/style.scss";
export interface TooltipHeatmapProps {
cursor: {left: number, top: number}
startDate: string,
endDate: string,
metricName: string,
fields: string[],
bucket: string,
value: number,
valueFormat: string
}
@ -36,8 +35,7 @@ const ChartTooltipHeatmap: FC<ChartTooltipHeatmapProps> = ({
onClose,
startDate,
endDate,
metricName,
fields,
bucket,
valueFormat,
value
}) => {
@ -141,18 +139,12 @@ const ChartTooltipHeatmap: FC<ChartTooltipHeatmapProps> = ({
</div>
<div className="vm-chart-tooltip-data">
<p>
{metricName}:
<b className="vm-chart-tooltip-data__value">{valueFormat}</b>
{unit}
value: <b className="vm-chart-tooltip-data__value">{valueFormat}</b>{unit}
</p>
</div>
{!!fields.length && (
<div className="vm-chart-tooltip-info">
{fields.map((f, i) => (
<div key={`${f}_${i}`}>{f}</div>
))}
</div>
)}
<div className="vm-chart-tooltip-info">
{bucket}
</div>
</div>
), targetPortal);
};

View file

@ -4,21 +4,21 @@ import uPlot, {
Options as uPlotOptions,
Range
} from "uplot";
import { defaultOptions, sizeAxis } from "../../../utils/uplot/helpers";
import { dragChart } from "../../../utils/uplot/events";
import { getAxes } from "../../../utils/uplot/axes";
import { MetricResult } from "../../../api/types";
import { dateFromSeconds, formatDateForNativeInput, limitsDurations } from "../../../utils/time";
import { defaultOptions, sizeAxis } from "../../../../utils/uplot/helpers";
import { dragChart } from "../../../../utils/uplot/events";
import { getAxes } from "../../../../utils/uplot/axes";
import { MetricResult } from "../../../../api/types";
import { dateFromSeconds, formatDateForNativeInput, limitsDurations } from "../../../../utils/time";
import throttle from "lodash.throttle";
import useResize from "../../../hooks/useResize";
import { TimeParams } from "../../../types";
import { YaxisState } from "../../../state/graph/reducer";
import useResize from "../../../../hooks/useResize";
import { TimeParams } from "../../../../types";
import { YaxisState } from "../../../../state/graph/reducer";
import "uplot/dist/uPlot.min.css";
import classNames from "classnames";
import dayjs from "dayjs";
import { useAppState } from "../../../state/common/StateContext";
import { heatmapPaths } from "../../../utils/uplot/heatmap";
import { DATE_FULL_TIMEZONE_FORMAT } from "../../../constants/date";
import { useAppState } from "../../../../state/common/StateContext";
import { heatmapPaths } from "../../../../utils/uplot/heatmap";
import { DATE_FULL_TIMEZONE_FORMAT } from "../../../../constants/date";
import ChartTooltipHeatmap, {
ChartTooltipHeatmapProps,
TooltipHeatmapProps
@ -33,7 +33,7 @@ export interface HeatmapChartProps {
setPeriod: ({ from, to }: {from: Date, to: Date}) => void;
container: HTMLDivElement | null;
height?: number;
onChangeLegend: (val: number) => void;
onChangeLegend: (val: TooltipHeatmapProps) => void;
}
enum typeChartUpdate {xRange = "xRange", yRange = "yRange"}
@ -62,7 +62,7 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
const [tooltipOffset, setTooltipOffset] = useState({ left: 0, top: 0 });
const [stickyTooltips, setStickyToolTips] = useState<ChartTooltipHeatmapProps[]>([]);
const tooltipId = useMemo(() => {
return `${tooltipProps?.fields.join(",")}_${tooltipProps?.startDate}`;
return `${tooltipProps?.bucket}_${tooltipProps?.startDate}`;
}, [tooltipProps]);
const setScale = ({ min, max }: { min: number, max: number }): void => {
@ -135,7 +135,7 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
const handleClick = () => {
if (!tooltipProps) return;
const id = `${tooltipProps?.fields.join(",")}_${tooltipProps?.startDate}`;
const id = `${tooltipProps?.bucket}_${tooltipProps?.startDate}`;
const props = {
id,
unit,
@ -171,12 +171,6 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
return;
}
const metric = result?.metric;
const metricName = metric["__name__"] || "value";
const labelNames = Object.keys(metric).filter(x => x != "__name__");
const fields = labelNames.map(key => `${key}=${JSON.stringify(metric[key])}`);
const [endTime = 0, value = ""] = result.values.find(v => v[0] === second) || [];
const valueFormat = `${+value}%`;
const startTime = xArr[xIdx];
@ -187,8 +181,7 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
cursor: { left, top },
startDate,
endDate,
metricName,
fields,
bucket: result?.metric?.vmrange || "",
value: +value,
valueFormat: valueFormat,
});
@ -228,7 +221,7 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
font: axes[0].font,
size: sizeAxis,
splits: metrics.map((m, i) => i),
values: metrics.map(m => Object.entries(m.metric).map(e => `${e[0]}=${JSON.stringify(e[1])}`)[0]),
values: metrics.map(m => m.metric.vmrange),
}
],
scales: {
@ -339,7 +332,7 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
}, [tooltipProps, stickyTooltips]);
useEffect(() => {
onChangeLegend(tooltipProps?.value || 0);
if (tooltipProps) onChangeLegend(tooltipProps);
}, [tooltipProps]);
return (

View file

@ -0,0 +1,68 @@
import React, { FC, useEffect, useState } from "preact/compat";
import { gradMetal16 } from "../../../../utils/uplot/heatmap";
import "./style.scss";
import { TooltipHeatmapProps } from "../ChartTooltipHeatmap/ChartTooltipHeatmap";
import { SeriesItem } from "../../../../utils/uplot/series";
import LegendItem from "../../Line/Legend/LegendItem/LegendItem";
import { LegendItemType } from "../../../../utils/uplot/types";
interface LegendHeatmapProps {
min: number
max: number
legendValue: TooltipHeatmapProps | null,
series: SeriesItem[]
}
const LegendHeatmap: FC<LegendHeatmapProps> = (
{
min,
max,
legendValue,
series,
}
) => {
const [percent, setPercent] = useState(0);
const [valueFormat, setValueFormat] = useState("");
const [minFormat, setMinFormat] = useState("");
const [maxFormat, setMaxFormat] = useState("");
useEffect(() => {
const value = legendValue?.value || 0;
setPercent(value ? (value - min) / (max - min) * 100 : 0);
setValueFormat(value ? `${value}%` : "");
setMinFormat(`${min}%`);
setMaxFormat(`${max}%`);
}, [legendValue, min, max]);
return (
<div className="vm-legend-heatmap__wrapper">
<div className="vm-legend-heatmap">
<div
className="vm-legend-heatmap-gradient"
style={{ background: `linear-gradient(to right, ${gradMetal16.join(", ")})` }}
>
{!!legendValue?.value && (
<div
className="vm-legend-heatmap-gradient__value"
style={{ left: `${percent}%` }}
>
<span>{valueFormat}</span>
</div>
)}
</div>
<div className="vm-legend-heatmap__value">{minFormat}</div>
<div className="vm-legend-heatmap__value">{maxFormat}</div>
</div>
{series[1] && (
<LegendItem
key={series[1]?.label}
legend={series[1] as LegendItemType}
isHeatmap
/>
)}
</div>
);
};
export default LegendHeatmap;

View file

@ -7,6 +7,14 @@
justify-content: space-between;
gap: 4px;
&__wrapper {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: $padding-global;
flex-wrap: wrap;
}
&__value {
color: $color-text;
font-size: $font-size-small;
@ -52,4 +60,8 @@
}
}
}
&__labels {
word-break: break-all;
}
}

View file

@ -1,46 +0,0 @@
import React, { FC, useEffect, useState } from "preact/compat";
import { gradMetal16 } from "../../../utils/uplot/heatmap";
import "./style.scss";
interface LegendHeatmapProps {
min: number
max: number
value?: number
}
const LegendHeatmap: FC<LegendHeatmapProps> = ({ min, max, value }) => {
const [percent, setPercent] = useState(0);
const [valueFormat, setValueFormat] = useState("");
const [minFormat, setMinFormat] = useState("");
const [maxFormat, setMaxFormat] = useState("");
useEffect(() => {
setPercent(value ? (value - min) / (max - min) * 100 : 0);
setValueFormat(value ? `${value}%` : "");
setMinFormat(`${min}%`);
setMaxFormat(`${max}%`);
}, [value, min, max]);
return (
<div className="vm-legend-heatmap">
<div
className="vm-legend-heatmap-gradient"
style={{ background: `linear-gradient(to right, ${gradMetal16.join(", ")})` }}
>
{!!value && (
<div
className="vm-legend-heatmap-gradient__value"
style={{ left: `${percent}%` }}
>
<span>{valueFormat}</span>
</div>
)}
</div>
<div className="vm-legend-heatmap__value">{minFormat}</div>
<div className="vm-legend-heatmap__value">{maxFormat}</div>
</div>
);
};
export default LegendHeatmap;

View file

@ -1,17 +1,17 @@
import React, { FC, useEffect, useMemo, useRef, useState } from "preact/compat";
import uPlot from "uplot";
import { MetricResult } from "../../../api/types";
import { formatPrettyNumber } from "../../../utils/uplot/helpers";
import { MetricResult } from "../../../../api/types";
import { formatPrettyNumber } from "../../../../utils/uplot/helpers";
import dayjs from "dayjs";
import { DATE_FULL_TIMEZONE_FORMAT } from "../../../constants/date";
import { DATE_FULL_TIMEZONE_FORMAT } from "../../../../constants/date";
import ReactDOM from "react-dom";
import get from "lodash.get";
import Button from "../../Main/Button/Button";
import { CloseIcon, DragIcon } from "../../Main/Icons";
import Button from "../../../Main/Button/Button";
import { CloseIcon, DragIcon } from "../../../Main/Icons";
import classNames from "classnames";
import { MouseEvent as ReactMouseEvent } from "react";
import "./style.scss";
import { SeriesItem } from "../../../utils/uplot/series";
import { SeriesItem } from "../../../../utils/uplot/series";
export interface ChartTooltipProps {
id: string,

View file

@ -78,5 +78,6 @@ $chart-tooltip-y: -1 * ($padding-small + $chart-tooltip-half-icon);
display: grid;
grid-gap: 4px;
word-break: break-all;
white-space: pre-wrap;
}
}

View file

@ -1,7 +1,7 @@
import React, { FC, useMemo } from "preact/compat";
import { LegendItemType } from "../../../utils/uplot/types";
import { LegendItemType } from "../../../../utils/uplot/types";
import LegendItem from "./LegendItem/LegendItem";
import Accordion from "../../Main/Accordion/Accordion";
import Accordion from "../../../Main/Accordion/Accordion";
import "./style.scss";
interface LegendProps {

View file

@ -1,19 +1,23 @@
import React, { FC, useState, useMemo } from "preact/compat";
import { MouseEvent } from "react";
import { LegendItemType } from "../../../../utils/uplot/types";
import { LegendItemType } from "../../../../../utils/uplot/types";
import "./style.scss";
import classNames from "classnames";
import Tooltip from "../../../Main/Tooltip/Tooltip";
import Tooltip from "../../../../Main/Tooltip/Tooltip";
import { getFreeFields } from "./helpers";
interface LegendItemProps {
legend: LegendItemType;
onChange: (item: LegendItemType, metaKey: boolean) => void;
onChange?: (item: LegendItemType, metaKey: boolean) => void;
isHeatmap?: boolean;
}
const LegendItem: FC<LegendItemProps> = ({ legend, onChange }) => {
const LegendItem: FC<LegendItemProps> = ({ legend, onChange, isHeatmap }) => {
const [copiedValue, setCopiedValue] = useState("");
const freeFormFields = useMemo(() => getFreeFields(legend), [legend]);
const freeFormFields = useMemo(() => {
const result = getFreeFields(legend);
return isHeatmap ? result.filter(f => f.key !== "vmrange") : result;
}, [legend, isHeatmap]);
const calculations = legend.calculations;
const showCalculations = Object.values(calculations).some(v => v);
@ -24,7 +28,7 @@ const LegendItem: FC<LegendItemProps> = ({ legend, onChange }) => {
};
const createHandlerClick = (legend: LegendItemType) => (e: MouseEvent<HTMLDivElement>) => {
onChange(legend, e.ctrlKey || e.metaKey);
onChange && onChange(legend, e.ctrlKey || e.metaKey);
};
const createHandlerCopy = (freeField: string, id: string) => (e: MouseEvent<HTMLDivElement>) => {
@ -37,18 +41,21 @@ const LegendItem: FC<LegendItemProps> = ({ legend, onChange }) => {
className={classNames({
"vm-legend-item": true,
"vm-legend-row": true,
"vm-legend-item_hide": !legend.checked,
"vm-legend-item_hide": !legend.checked && !isHeatmap,
"vm-legend-item_static": isHeatmap,
})}
onClick={createHandlerClick(legend)}
>
<div
className="vm-legend-item__marker"
style={{ backgroundColor: legend.color }}
/>
{!isHeatmap && (
<div
className="vm-legend-item__marker"
style={{ backgroundColor: legend.color }}
/>
)}
<div className="vm-legend-item-info">
<span className="vm-legend-item-info__label">
{legend.freeFormFields["__name__"]}
&#123;
{!!freeFormFields.length && <>&#123;</>}
{freeFormFields.map((f, i) => (
<Tooltip
key={f.id}
@ -66,10 +73,10 @@ const LegendItem: FC<LegendItemProps> = ({ legend, onChange }) => {
</span>
</Tooltip>
))}
&#125;
{!!freeFormFields.length && <>&#125;</>}
</span>
</div>
{showCalculations && (
{!isHeatmap && showCalculations && (
<div className="vm-legend-item-values">
median:{calculations.median}, min:{calculations.min}, max:{calculations.max}, last:{calculations.last}
</div>

View file

@ -1,4 +1,4 @@
import { LegendItemType } from "../../../../utils/uplot/types";
import { LegendItemType } from "../../../../../utils/uplot/types";
export const getFreeFields = (legend: LegendItemType) => {
const keys = Object.keys(legend.freeFormFields).filter(f => f !== "__name__");

View file

@ -21,6 +21,17 @@
opacity: 0.5;
}
&_static {
grid-template-columns: 1fr;
margin: 0;
padding: 0;
cursor: default;
&:hover {
background-color: $color-background-block;
}
}
&__marker {
width: 14px;
height: 14px;

View file

@ -7,22 +7,22 @@ import uPlot, {
Scales,
Scale,
} from "uplot";
import { defaultOptions } from "../../../utils/uplot/helpers";
import { dragChart } from "../../../utils/uplot/events";
import { getAxes, getMinMaxBuffer } from "../../../utils/uplot/axes";
import { MetricResult } from "../../../api/types";
import { dateFromSeconds, formatDateForNativeInput, limitsDurations } from "../../../utils/time";
import { defaultOptions } from "../../../../utils/uplot/helpers";
import { dragChart } from "../../../../utils/uplot/events";
import { getAxes, getMinMaxBuffer } from "../../../../utils/uplot/axes";
import { MetricResult } from "../../../../api/types";
import { dateFromSeconds, formatDateForNativeInput, limitsDurations } from "../../../../utils/time";
import throttle from "lodash.throttle";
import useResize from "../../../hooks/useResize";
import { TimeParams } from "../../../types";
import { YaxisState } from "../../../state/graph/reducer";
import useResize from "../../../../hooks/useResize";
import { TimeParams } from "../../../../types";
import { YaxisState } from "../../../../state/graph/reducer";
import "uplot/dist/uPlot.min.css";
import "./style.scss";
import classNames from "classnames";
import ChartTooltip, { ChartTooltipProps } from "../ChartTooltip/ChartTooltip";
import dayjs from "dayjs";
import { useAppState } from "../../../state/common/StateContext";
import { SeriesItem } from "../../../utils/uplot/series";
import { useAppState } from "../../../../state/common/StateContext";
import { SeriesItem } from "../../../../utils/uplot/series";
export interface LineChartProps {
metrics: MetricResult[];

View file

@ -1,10 +1,10 @@
import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from "preact/compat";
import { MetricResult } from "../../../api/types";
import LineChart from "../../Chart/LineChart/LineChart";
import LineChart from "../../Chart/Line/LineChart/LineChart";
import { AlignedData as uPlotData, Series as uPlotSeries } from "uplot";
import Legend from "../../Chart/Legend/Legend";
import LegendHeatmap from "../../Chart/LegendHeatmap/LegendHeatmap";
import { getHideSeries, getLegendItem, getSeriesItemContext } from "../../../utils/uplot/series";
import Legend from "../../Chart/Line/Legend/Legend";
import LegendHeatmap from "../../Chart/Heatmap/LegendHeatmap/LegendHeatmap";
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";
@ -12,11 +12,12 @@ import { AxisRange, YaxisState } from "../../../state/graph/reducer";
import { getAvgFromArray, getMaxFromArray, getMinFromArray } from "../../../utils/math";
import classNames from "classnames";
import { useTimeState } from "../../../state/time/TimeStateContext";
import HeatmapChart from "../../Chart/HeatmapChart/HeatmapChart";
import HeatmapChart from "../../Chart/Heatmap/HeatmapChart/HeatmapChart";
import "./style.scss";
import { promValueToNumber } from "../../../utils/metric";
import { normalizeData } from "../../../utils/uplot/heatmap";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
import { TooltipHeatmapProps } from "../../Chart/Heatmap/ChartTooltipHeatmap/ChartTooltipHeatmap";
export interface GraphViewProps {
data?: MetricResult[];
@ -60,7 +61,7 @@ const GraphView: FC<GraphViewProps> = ({
const [series, setSeries] = useState<uPlotSeries[]>([]);
const [legend, setLegend] = useState<LegendItemType[]>([]);
const [hideSeries, setHideSeries] = useState<string[]>([]);
const [legendValue, setLegendValue] = useState(0);
const [legendValue, setLegendValue] = useState<TooltipHeatmapProps | null>(null);
const setLimitsYaxis = (values: {[key: string]: number[]}) => {
const limits = getLimitsYAxis(values, !isHistogram);
@ -71,7 +72,7 @@ const GraphView: FC<GraphViewProps> = ({
setHideSeries(getHideSeries({ hideSeries, legend, metaKey, series }));
};
const handleChangeLegend = (val: number) => {
const handleChangeLegend = (val: TooltipHeatmapProps) => {
setLegendValue(val);
};
@ -209,9 +210,10 @@ const GraphView: FC<GraphViewProps> = ({
)}
{isHistogram && showLegend && (
<LegendHeatmap
series={series as SeriesItem[]}
min={yaxis.limits.range[1][0] || 0}
max={yaxis.limits.range[1][1] || 0}
value={legendValue}
legendValue={legendValue}
/>
)}
</div>

View file

@ -79,7 +79,7 @@ export const useFetchQuery = ({
setFetchQueue([...fetchQueue, controller]);
try {
const isDisplayChart = displayType === "chart";
const seriesLimit = showAllSeries ? Infinity : (+stateSeriesLimits[displayType] || Infinity);
let seriesLimit = showAllSeries ? Infinity : (+stateSeriesLimits[displayType] || Infinity);
const tempData: MetricBase[] = [];
const tempTraces: Trace[] = [];
let counter = 1;
@ -104,6 +104,8 @@ export const useFetchQuery = ({
tempTraces.push(trace);
}
const isHistogramResult = isDisplayChart && isHistogramData(resp.data.result);
if (isHistogramResult) seriesLimit = Infinity;
const freeTempSize = seriesLimit - tempData.length;
resp.data.result.slice(0, freeTempSize).forEach((d: MetricBase) => {
d.group = counter;

View file

@ -31,14 +31,13 @@ export const promValueToNumber = (s: string): number => {
export const isHistogramData = (result: MetricBase[]) => {
if (result.length < 2) return false;
const histogramNames = ["le", "vmrange"];
const histogramLabels = ["le", "vmrange"];
return result.every(r => {
const keys = Object.keys(r.metric);
const labels = Object.keys(r.metric).filter(n => !histogramNames.includes(n));
const byName = keys.length > labels.length;
const byLabels = labels.every(l => r.metric[l] === result[0].metric[l]);
return byName && byLabels;
const firstLabels = Object.keys(result[0].metric).filter(n => !histogramLabels.includes(n));
const isHistogram = result.every(r => {
const labels = Object.keys(r.metric).filter(n => !histogramLabels.includes(n));
return firstLabels.length === labels.length && labels.every(l => r.metric[l] === result[0].metric[l]);
});
return isHistogram && result.every(r => histogramLabels.some(l => l in r.metric));
};

View file

@ -1,6 +1,7 @@
import uPlot from "uplot";
import { generateGradient } from "../color";
import { MetricResult } from "../../api/types";
import { promValueToNumber } from "../metric";
// 16-color gradient from "rgb(246, 226, 219)" to "rgb(127, 39, 4)"
export const gradMetal16 = generateGradient([246, 226, 219], [127, 39, 4], 16);
@ -115,11 +116,11 @@ export const convertPrometheusToVictoriaMetrics = (buckets: MetricResult[]): Met
const sortedBuckets = buckets.sort((a,b) => parseFloat(a.metric.le) - parseFloat(b.metric.le));
const group = buckets[0]?.group || 1;
let prevBucket: MetricResult = { metric: { le: "0" }, values: [], group };
let prevBucket: MetricResult = { metric: { le: "" }, values: [], group };
const result: MetricResult[] = [];
for (const bucket of sortedBuckets) {
const vmrange = `${prevBucket.metric.le}..${bucket.metric.le}`;
const vmrange = [prevBucket.metric.le, bucket.metric.le].filter(n => n).join("...");
const values: [number, string][] = [];
for (const [timestamp, value] of bucket.values) {
@ -135,14 +136,25 @@ export const convertPrometheusToVictoriaMetrics = (buckets: MetricResult[]): Met
return result;
};
const getUpperBound = (bucket: MetricResult) => {
const values = (bucket.metric.vmrange || bucket.metric.le).split("...");
return promValueToNumber(values[values.length - 1]);
};
const sortBucketsByValues = (a: MetricResult, b: MetricResult) => getUpperBound(a) - getUpperBound(b);
export const normalizeData = (buckets: MetricResult[], isHistogram?: boolean): MetricResult[] => {
if (!isHistogram) return buckets;
const vmBuckets = convertPrometheusToVictoriaMetrics(buckets);
const sortedBuckets = buckets.sort(sortBucketsByValues);
const vmBuckets = convertPrometheusToVictoriaMetrics(sortedBuckets);
const allValues = vmBuckets.map(b => b.values).flat();
return 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);
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)}`];
});