mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2024-11-21 14:44:00 +00:00
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:
parent
bf545fcc14
commit
7871ee0e43
24 changed files with 202 additions and 149 deletions
|
@ -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"
|
||||
]
|
||||
}
|
|
@ -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>
|
1
app/vmselect/vmui/static/css/main.6fe27a94.css
Normal file
1
app/vmselect/vmui/static/css/main.6fe27a94.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -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);
|
||||
};
|
|
@ -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 (
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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,
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 {
|
|
@ -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__"]}
|
||||
{
|
||||
{!!freeFormFields.length && <>{</>}
|
||||
{freeFormFields.map((f, i) => (
|
||||
<Tooltip
|
||||
key={f.id}
|
||||
|
@ -66,10 +73,10 @@ const LegendItem: FC<LegendItemProps> = ({ legend, onChange }) => {
|
|||
</span>
|
||||
</Tooltip>
|
||||
))}
|
||||
}
|
||||
{!!freeFormFields.length && <>}</>}
|
||||
</span>
|
||||
</div>
|
||||
{showCalculations && (
|
||||
{!isHeatmap && showCalculations && (
|
||||
<div className="vm-legend-item-values">
|
||||
median:{calculations.median}, min:{calculations.min}, max:{calculations.max}, last:{calculations.last}
|
||||
</div>
|
|
@ -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__");
|
|
@ -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;
|
|
@ -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[];
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
};
|
||||
|
|
|
@ -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)}`];
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in a new issue