mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2025-01-20 07:19:17 +01:00
vmui: chart refactoring to enhance code structure (#4830)
(cherry picked from commit 8287749c05
)
This commit is contained in:
parent
f48962e834
commit
931c63f602
@ -1,6 +1,6 @@
|
||||
import { seriesBarsPlugin } from "../../../utils/uplot/plugin";
|
||||
import { barDisp, getBarSeries } from "../../../utils/uplot/series";
|
||||
import { Fill, Stroke } from "../../../utils/uplot/types";
|
||||
import { barDisp, getBarSeries } from "../../../utils/uplot";
|
||||
import { Fill, Stroke } from "../../../types";
|
||||
import { PaddingSide, Series } from "uplot";
|
||||
|
||||
|
||||
|
@ -0,0 +1,178 @@
|
||||
import React, { FC, useCallback, useEffect, useRef, useState } from "preact/compat";
|
||||
import { MouseEvent as ReactMouseEvent } from "react";
|
||||
import useEventListener from "../../../hooks/useEventListener";
|
||||
import ReactDOM from "react-dom";
|
||||
import classNames from "classnames";
|
||||
import uPlot from "uplot";
|
||||
import Button from "../../Main/Button/Button";
|
||||
import { CloseIcon, DragIcon } from "../../Main/Icons";
|
||||
import { SeriesItemStats } from "../../../types";
|
||||
|
||||
export interface ChartTooltipProps {
|
||||
u?: uPlot;
|
||||
id: string;
|
||||
title?: string;
|
||||
dates: string[];
|
||||
value: string | number | null;
|
||||
point: { top: number, left: number };
|
||||
unit?: string;
|
||||
stats?: SeriesItemStats;
|
||||
isSticky?: boolean;
|
||||
info?: string;
|
||||
marker?: string;
|
||||
show?: boolean;
|
||||
onClose?: (id: string) => void;
|
||||
}
|
||||
|
||||
const ChartTooltip: FC<ChartTooltipProps> = ({
|
||||
u,
|
||||
id,
|
||||
title,
|
||||
dates,
|
||||
value,
|
||||
point,
|
||||
unit = "",
|
||||
info,
|
||||
stats,
|
||||
isSticky,
|
||||
marker,
|
||||
onClose
|
||||
}) => {
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [position, setPosition] = useState({ top: -999, left: -999 });
|
||||
const [moving, setMoving] = useState(false);
|
||||
const [moved, setMoved] = useState(false);
|
||||
|
||||
const handleClose = () => {
|
||||
onClose && onClose(id);
|
||||
};
|
||||
|
||||
const handleMouseDown = (e: ReactMouseEvent) => {
|
||||
setMoved(true);
|
||||
setMoving(true);
|
||||
const { clientX, clientY } = e;
|
||||
setPosition({ top: clientY, left: clientX });
|
||||
};
|
||||
|
||||
const handleMouseMove = useCallback((e: MouseEvent) => {
|
||||
if (!moving) return;
|
||||
const { clientX, clientY } = e;
|
||||
setPosition({ top: clientY, left: clientX });
|
||||
}, [moving]);
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setMoving(false);
|
||||
};
|
||||
|
||||
const calcPosition = () => {
|
||||
if (!tooltipRef.current || !u) return;
|
||||
|
||||
const { top, left } = point;
|
||||
const uPlotPosition = {
|
||||
left: parseFloat(u.over.style.left),
|
||||
top: parseFloat(u.over.style.top)
|
||||
};
|
||||
|
||||
const {
|
||||
width: uPlotWidth,
|
||||
height: uPlotHeight
|
||||
} = u.over.getBoundingClientRect();
|
||||
|
||||
const {
|
||||
width: tooltipWidth,
|
||||
height: tooltipHeight
|
||||
} = tooltipRef.current.getBoundingClientRect();
|
||||
|
||||
const margin = 10;
|
||||
const overflowX = left + tooltipWidth >= uPlotWidth ? tooltipWidth + (2 * margin) : 0;
|
||||
const overflowY = top + tooltipHeight >= uPlotHeight ? tooltipHeight + (2 * margin) : 0;
|
||||
|
||||
const position = {
|
||||
top: top + uPlotPosition.top + margin - overflowY,
|
||||
left: left + uPlotPosition.left + margin - overflowX
|
||||
};
|
||||
|
||||
if (position.left < 0) position.left = 20;
|
||||
if (position.top < 0) position.top = 20;
|
||||
|
||||
setPosition(position);
|
||||
};
|
||||
|
||||
useEffect(calcPosition, [u, value, point, tooltipRef]);
|
||||
|
||||
useEventListener("mousemove", handleMouseMove);
|
||||
useEventListener("mouseup", handleMouseUp);
|
||||
|
||||
if (!u) return null;
|
||||
|
||||
return ReactDOM.createPortal((
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-chart-tooltip": true,
|
||||
"vm-chart-tooltip_sticky": isSticky,
|
||||
"vm-chart-tooltip_moved": moved
|
||||
|
||||
})}
|
||||
ref={tooltipRef}
|
||||
style={position}
|
||||
>
|
||||
<div className="vm-chart-tooltip-header">
|
||||
{title && (
|
||||
<div className="vm-chart-tooltip-header__title">
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
<div className="vm-chart-tooltip-header__date">
|
||||
{dates.map((date, i) => <span key={i}>{date}</span>)}
|
||||
</div>
|
||||
{isSticky && (
|
||||
<>
|
||||
<Button
|
||||
className="vm-chart-tooltip-header__drag"
|
||||
variant="text"
|
||||
size="small"
|
||||
startIcon={<DragIcon/>}
|
||||
onMouseDown={handleMouseDown}
|
||||
/>
|
||||
<Button
|
||||
className="vm-chart-tooltip-header__close"
|
||||
variant="text"
|
||||
size="small"
|
||||
startIcon={<CloseIcon/>}
|
||||
onClick={handleClose}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="vm-chart-tooltip-data">
|
||||
{marker && (
|
||||
<span
|
||||
className="vm-chart-tooltip-data__marker"
|
||||
style={{ background: marker }}
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<p className="vm-chart-tooltip-data__value">
|
||||
<b>{value}</b>{unit}
|
||||
</p>
|
||||
{stats && (
|
||||
<p className="vm-chart-tooltip-data__stats">
|
||||
{Object.keys(stats).filter(key => key !== "last").map((key, i) => (
|
||||
<span key={i}>
|
||||
{key}:<b>{stats[key as keyof SeriesItemStats]}</b>
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="vm-chart-tooltip-info">
|
||||
{info}
|
||||
</div>
|
||||
</div>
|
||||
), u.root);
|
||||
};
|
||||
|
||||
export default ChartTooltip;
|
@ -0,0 +1,26 @@
|
||||
import React, { FC } from "preact/compat";
|
||||
import ChartTooltip, { ChartTooltipProps } from "./ChartTooltip";
|
||||
import "./style.scss";
|
||||
|
||||
interface LineTooltipHook {
|
||||
showTooltip: boolean;
|
||||
tooltipProps: ChartTooltipProps;
|
||||
stickyTooltips: ChartTooltipProps[];
|
||||
handleUnStick: (id: string) => void;
|
||||
}
|
||||
|
||||
const ChartTooltipWrapper: FC<LineTooltipHook> = ({ showTooltip, tooltipProps, stickyTooltips, handleUnStick }) => (
|
||||
<>
|
||||
{showTooltip && tooltipProps && <ChartTooltip {...tooltipProps}/>}
|
||||
{stickyTooltips.map(t => (
|
||||
<ChartTooltip
|
||||
{...t}
|
||||
isSticky
|
||||
key={t.id}
|
||||
onClose={handleUnStick}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
export default ChartTooltipWrapper;
|
@ -1,4 +1,5 @@
|
||||
@use "src/styles/variables" as *;
|
||||
|
||||
$chart-tooltip-width: 325px;
|
||||
$chart-tooltip-icon-width: 25px;
|
||||
$chart-tooltip-half-icon: calc($chart-tooltip-icon-width/2);
|
||||
@ -53,12 +54,10 @@ $chart-tooltip-y: -1 * ($padding-small + $chart-tooltip-half-icon);
|
||||
}
|
||||
|
||||
&__date {
|
||||
&_range {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-data {
|
||||
display: grid;
|
||||
@ -68,6 +67,13 @@ $chart-tooltip-y: -1 * ($padding-small + $chart-tooltip-half-icon);
|
||||
word-break: break-all;
|
||||
line-height: $font-size;
|
||||
|
||||
&__stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: $padding-small;
|
||||
}
|
||||
|
||||
&__marker {
|
||||
width: 12px;
|
||||
height: 12px;
|
@ -1,142 +0,0 @@
|
||||
import React, { FC, useCallback, useEffect, 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 classNames from "classnames";
|
||||
import { MouseEvent as ReactMouseEvent } from "react";
|
||||
import "../../Line/ChartTooltip/style.scss";
|
||||
import useEventListener from "../../../../hooks/useEventListener";
|
||||
|
||||
export interface TooltipHeatmapProps {
|
||||
cursor: {left: number, top: number}
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
bucket: string,
|
||||
value: number,
|
||||
valueFormat: string
|
||||
}
|
||||
|
||||
export interface ChartTooltipHeatmapProps extends TooltipHeatmapProps {
|
||||
id: string,
|
||||
u: uPlot,
|
||||
unit?: string,
|
||||
isSticky?: boolean,
|
||||
tooltipOffset: { left: number, top: number },
|
||||
onClose?: (id: string) => void
|
||||
}
|
||||
|
||||
const ChartTooltipHeatmap: FC<ChartTooltipHeatmapProps> = ({
|
||||
u,
|
||||
id,
|
||||
unit = "",
|
||||
cursor,
|
||||
tooltipOffset,
|
||||
isSticky,
|
||||
onClose,
|
||||
startDate,
|
||||
endDate,
|
||||
bucket,
|
||||
valueFormat,
|
||||
value
|
||||
}) => {
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [position, setPosition] = useState({ top: -999, left: -999 });
|
||||
const [moving, setMoving] = useState(false);
|
||||
const [moved, setMoved] = useState(false);
|
||||
|
||||
const handleClose = () => {
|
||||
onClose && onClose(id);
|
||||
};
|
||||
|
||||
const handleMouseDown = (e: ReactMouseEvent) => {
|
||||
setMoved(true);
|
||||
setMoving(true);
|
||||
const { clientX, clientY } = e;
|
||||
setPosition({ top: clientY, left: clientX });
|
||||
};
|
||||
|
||||
const handleMouseMove = useCallback((e: MouseEvent) => {
|
||||
if (!moving) return;
|
||||
const { clientX, clientY } = e;
|
||||
setPosition({ top: clientY, left: clientX });
|
||||
}, [moving]);
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setMoving(false);
|
||||
};
|
||||
|
||||
const calcPosition = () => {
|
||||
if (!tooltipRef.current) return;
|
||||
|
||||
const topOnChart = cursor.top;
|
||||
const leftOnChart = cursor.left;
|
||||
const { width: tooltipWidth, height: tooltipHeight } = tooltipRef.current.getBoundingClientRect();
|
||||
const { width, height } = u.over.getBoundingClientRect();
|
||||
|
||||
const margin = 10;
|
||||
const overflowX = leftOnChart + tooltipWidth >= width ? tooltipWidth + (2 * margin) : 0;
|
||||
const overflowY = topOnChart + tooltipHeight >= height ? tooltipHeight + (2 * margin) : 0;
|
||||
|
||||
setPosition({
|
||||
top: topOnChart + tooltipOffset.top + margin - overflowY,
|
||||
left: leftOnChart + tooltipOffset.left + margin - overflowX
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(calcPosition, [u, cursor, tooltipOffset, tooltipRef]);
|
||||
|
||||
useEventListener("mousemove", handleMouseMove);
|
||||
useEventListener("mouseup", handleMouseUp);
|
||||
|
||||
if (!cursor?.left || !cursor?.top || !value) return null;
|
||||
|
||||
return ReactDOM.createPortal((
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-chart-tooltip": true,
|
||||
"vm-chart-tooltip_sticky": isSticky,
|
||||
"vm-chart-tooltip_moved": moved
|
||||
|
||||
})}
|
||||
ref={tooltipRef}
|
||||
style={position}
|
||||
>
|
||||
<div className="vm-chart-tooltip-header">
|
||||
<div className="vm-chart-tooltip-header__date vm-chart-tooltip-header__date_range">
|
||||
<span>{startDate}</span>
|
||||
<span>{endDate}</span>
|
||||
</div>
|
||||
{isSticky && (
|
||||
<>
|
||||
<Button
|
||||
className="vm-chart-tooltip-header__drag"
|
||||
variant="text"
|
||||
size="small"
|
||||
startIcon={<DragIcon/>}
|
||||
onMouseDown={handleMouseDown}
|
||||
/>
|
||||
<Button
|
||||
className="vm-chart-tooltip-header__close"
|
||||
variant="text"
|
||||
size="small"
|
||||
startIcon={<CloseIcon/>}
|
||||
onClick={handleClose}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="vm-chart-tooltip-data">
|
||||
<p>
|
||||
value: <b className="vm-chart-tooltip-data__value">{valueFormat}</b>{unit}
|
||||
</p>
|
||||
</div>
|
||||
<div className="vm-chart-tooltip-info">
|
||||
{bucket}
|
||||
</div>
|
||||
</div>
|
||||
), u.root);
|
||||
};
|
||||
|
||||
export default ChartTooltipHeatmap;
|
@ -1,49 +1,44 @@
|
||||
import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from "preact/compat";
|
||||
import React, { FC, useEffect, useMemo, useRef, useState } from "preact/compat";
|
||||
import uPlot, {
|
||||
AlignedData as uPlotData,
|
||||
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 throttle from "lodash.throttle";
|
||||
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 ChartTooltipHeatmap, {
|
||||
ChartTooltipHeatmapProps,
|
||||
TooltipHeatmapProps
|
||||
} from "../ChartTooltipHeatmap/ChartTooltipHeatmap";
|
||||
import {
|
||||
heatmapPaths,
|
||||
handleDestroy,
|
||||
getDefaultOptions,
|
||||
sizeAxis,
|
||||
getAxes,
|
||||
setSelect,
|
||||
} from "../../../../utils/uplot";
|
||||
import { ElementSize } from "../../../../hooks/useElementSize";
|
||||
import useEventListener from "../../../../hooks/useEventListener";
|
||||
import useReadyChart from "../../../../hooks/uplot/useReadyChart";
|
||||
import useZoomChart from "../../../../hooks/uplot/useZoomChart";
|
||||
import usePlotScale from "../../../../hooks/uplot/usePlotScale";
|
||||
import useHeatmapTooltip from "../../../../hooks/uplot/useHeatmapTooltip";
|
||||
import ChartTooltipWrapper from "../../ChartTooltip";
|
||||
import { ChartTooltipProps } from "../../ChartTooltip/ChartTooltip";
|
||||
|
||||
export interface HeatmapChartProps {
|
||||
metrics: MetricResult[];
|
||||
data: uPlotData;
|
||||
period: TimeParams;
|
||||
yaxis: YaxisState;
|
||||
unit?: string;
|
||||
setPeriod: ({ from, to }: {from: Date, to: Date}) => void;
|
||||
setPeriod: ({ from, to }: { from: Date, to: Date }) => void;
|
||||
layoutSize: ElementSize,
|
||||
height?: number;
|
||||
onChangeLegend: (val: TooltipHeatmapProps) => void;
|
||||
onChangeLegend: (val: ChartTooltipProps) => void;
|
||||
}
|
||||
|
||||
enum typeChartUpdate {xRange = "xRange", yRange = "yRange"}
|
||||
|
||||
const HeatmapChart: FC<HeatmapChartProps> = ({
|
||||
data,
|
||||
metrics = [],
|
||||
period,
|
||||
yaxis,
|
||||
unit,
|
||||
setPeriod,
|
||||
layoutSize,
|
||||
@ -53,144 +48,39 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
|
||||
const { isDarkTheme } = useAppState();
|
||||
|
||||
const uPlotRef = useRef<HTMLDivElement>(null);
|
||||
const [isPanning, setPanning] = useState(false);
|
||||
const [xRange, setXRange] = useState({ min: period.start, max: period.end });
|
||||
const [uPlotInst, setUPlotInst] = useState<uPlot>();
|
||||
const [startTouchDistance, setStartTouchDistance] = useState(0);
|
||||
|
||||
const [tooltipProps, setTooltipProps] = useState<TooltipHeatmapProps | null>(null);
|
||||
const [tooltipOffset, setTooltipOffset] = useState({ left: 0, top: 0 });
|
||||
const [stickyTooltips, setStickyToolTips] = useState<ChartTooltipHeatmapProps[]>([]);
|
||||
const tooltipId = useMemo(() => {
|
||||
return `${tooltipProps?.bucket}_${tooltipProps?.startDate}`;
|
||||
}, [tooltipProps]);
|
||||
const { xRange, setPlotScale } = usePlotScale({ period, setPeriod });
|
||||
const { onReadyChart, isPanning } = useReadyChart(setPlotScale);
|
||||
useZoomChart({ uPlotInst, xRange, setPlotScale });
|
||||
const {
|
||||
stickyTooltips,
|
||||
handleUnStick,
|
||||
getTooltipProps,
|
||||
setCursor,
|
||||
resetTooltips
|
||||
} = useHeatmapTooltip({ u: uPlotInst, metrics, unit });
|
||||
|
||||
const setScale = ({ min, max }: { min: number, max: number }): void => {
|
||||
if (isNaN(min) || isNaN(max)) return;
|
||||
setPeriod({
|
||||
from: dayjs(min * 1000).toDate(),
|
||||
to: dayjs(max * 1000).toDate()
|
||||
});
|
||||
};
|
||||
const throttledSetScale = useCallback(throttle(setScale, 500), []);
|
||||
const setPlotScale = ({ min, max }: { min: number, max: number }) => {
|
||||
const delta = (max - min) * 1000;
|
||||
if ((delta < limitsDurations.min) || (delta > limitsDurations.max)) return;
|
||||
setXRange({ min, max });
|
||||
throttledSetScale({ min, max });
|
||||
};
|
||||
const tooltipProps = useMemo(() => getTooltipProps(), [getTooltipProps]);
|
||||
|
||||
const onReadyChart = (u: uPlot) => {
|
||||
const factor = 0.9;
|
||||
setTooltipOffset({
|
||||
left: parseFloat(u.over.style.left),
|
||||
top: parseFloat(u.over.style.top)
|
||||
});
|
||||
const getHeatmapAxes = () => {
|
||||
const baseAxes = getAxes([{}], unit);
|
||||
|
||||
u.over.addEventListener("mousedown", e => {
|
||||
const { ctrlKey, metaKey, button } = e;
|
||||
const leftClick = button === 0;
|
||||
const leftClickWithMeta = leftClick && (ctrlKey || metaKey);
|
||||
if (leftClickWithMeta) {
|
||||
// drag pan
|
||||
dragChart({ u, e, setPanning, setPlotScale, factor });
|
||||
return [
|
||||
...baseAxes,
|
||||
{
|
||||
scale: "y",
|
||||
stroke: baseAxes[0].stroke,
|
||||
font: baseAxes[0].font,
|
||||
size: sizeAxis,
|
||||
splits: metrics.map((m, i) => i),
|
||||
values: metrics.map(m => m.metric.vmrange),
|
||||
}
|
||||
});
|
||||
|
||||
u.over.addEventListener("touchstart", e => {
|
||||
dragChart({ u, e, setPanning, setPlotScale, factor });
|
||||
});
|
||||
|
||||
u.over.addEventListener("wheel", e => {
|
||||
if (!e.ctrlKey && !e.metaKey) return;
|
||||
e.preventDefault();
|
||||
const { width } = u.over.getBoundingClientRect();
|
||||
const zoomPos = u.cursor.left && u.cursor.left > 0 ? u.cursor.left : 0;
|
||||
const xVal = u.posToVal(zoomPos, "x");
|
||||
const oxRange = (u.scales.x.max || 0) - (u.scales.x.min || 0);
|
||||
const nxRange = e.deltaY < 0 ? oxRange * factor : oxRange / factor;
|
||||
const min = xVal - (zoomPos / width) * nxRange;
|
||||
const max = min + nxRange;
|
||||
u.batch(() => setPlotScale({ min, max }));
|
||||
});
|
||||
];
|
||||
};
|
||||
|
||||
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
const { target, ctrlKey, metaKey, key } = e;
|
||||
const isInput = target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement;
|
||||
if (!uPlotInst || isInput) return;
|
||||
const minus = key === "-";
|
||||
const plus = key === "+" || key === "=";
|
||||
if ((minus || plus) && !(ctrlKey || metaKey)) {
|
||||
e.preventDefault();
|
||||
const factor = (xRange.max - xRange.min) / 10 * (plus ? 1 : -1);
|
||||
setPlotScale({
|
||||
min: xRange.min + factor,
|
||||
max: xRange.max - factor
|
||||
});
|
||||
}
|
||||
}, [uPlotInst, xRange]);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (!tooltipProps?.value) return;
|
||||
const id = `${tooltipProps?.bucket}_${tooltipProps?.startDate}`;
|
||||
const props = {
|
||||
id,
|
||||
unit,
|
||||
tooltipOffset,
|
||||
...tooltipProps
|
||||
};
|
||||
|
||||
if (!stickyTooltips.find(t => t.id === id)) {
|
||||
const res = JSON.parse(JSON.stringify(props));
|
||||
setStickyToolTips(prev => [...prev, res]);
|
||||
}
|
||||
}, [stickyTooltips, tooltipProps, tooltipOffset, unit]);
|
||||
|
||||
const handleUnStick = (id: string) => {
|
||||
setStickyToolTips(prev => prev.filter(t => t.id !== id));
|
||||
};
|
||||
|
||||
const setCursor = (u: uPlot) => {
|
||||
const left = u.cursor.left && u.cursor.left > 0 ? u.cursor.left : 0;
|
||||
const top = u.cursor.top && u.cursor.top > 0 ? u.cursor.top : 0;
|
||||
|
||||
const xArr = (u.data[1][0] || []) as number[];
|
||||
if (!Array.isArray(xArr)) return;
|
||||
const xVal = u.posToVal(left, "x");
|
||||
const yVal = u.posToVal(top, "y");
|
||||
const xIdx = xArr.findIndex((t, i) => xVal >= t && xVal < xArr[i + 1]) || -1;
|
||||
const second = xArr[xIdx + 1];
|
||||
|
||||
const result = metrics[Math.round(yVal)];
|
||||
if (!result) {
|
||||
setTooltipProps(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const [endTime = 0, value = ""] = result.values.find(v => v[0] === second) || [];
|
||||
const valueFormat = `${+value}%`;
|
||||
const startTime = xArr[xIdx];
|
||||
const startDate = dayjs(startTime * 1000).tz().format(DATE_FULL_TIMEZONE_FORMAT);
|
||||
const endDate = dayjs(endTime * 1000).tz().format(DATE_FULL_TIMEZONE_FORMAT);
|
||||
|
||||
setTooltipProps({
|
||||
cursor: { left, top },
|
||||
startDate,
|
||||
endDate,
|
||||
bucket: result?.metric?.vmrange || "",
|
||||
value: +value,
|
||||
valueFormat: valueFormat,
|
||||
});
|
||||
};
|
||||
|
||||
const getRangeX = (): Range.MinMax => [xRange.min, xRange.max];
|
||||
|
||||
const axes = getAxes( [{}], unit);
|
||||
const options: uPlotOptions = {
|
||||
...defaultOptions,
|
||||
...getDefaultOptions({ width: layoutSize.width, height }),
|
||||
mode: 2,
|
||||
tzDate: ts => dayjs(formatDateForNativeInput(dateFromSeconds(ts))).local().toDate(),
|
||||
series: [
|
||||
{},
|
||||
{
|
||||
@ -210,17 +100,7 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
|
||||
],
|
||||
},
|
||||
],
|
||||
axes: [
|
||||
...axes,
|
||||
{
|
||||
scale: "y",
|
||||
stroke: axes[0].stroke,
|
||||
font: axes[0].font,
|
||||
size: sizeAxis,
|
||||
splits: metrics.map((m, i) => i),
|
||||
values: metrics.map(m => m.metric.vmrange),
|
||||
}
|
||||
],
|
||||
axes: getHeatmapAxes(),
|
||||
scales: {
|
||||
x: {
|
||||
time: true,
|
||||
@ -228,87 +108,35 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
|
||||
y: {
|
||||
log: 2,
|
||||
time: false,
|
||||
range: (self, initMin, initMax) => [initMin - 1, initMax + 1]
|
||||
range: (u, initMin, initMax) => [initMin - 1, initMax + 1]
|
||||
}
|
||||
},
|
||||
width: layoutSize.width || 400,
|
||||
height: height || 500,
|
||||
plugins: [{ hooks: { ready: onReadyChart, setCursor } }],
|
||||
hooks: {
|
||||
setSelect: [
|
||||
(u) => {
|
||||
const min = u.posToVal(u.select.left, "x");
|
||||
const max = u.posToVal(u.select.left + u.select.width, "x");
|
||||
setPlotScale({ min, max });
|
||||
}
|
||||
]
|
||||
ready: [onReadyChart],
|
||||
setCursor: [setCursor],
|
||||
setSelect: [setSelect(setPlotScale)],
|
||||
destroy: [handleDestroy],
|
||||
},
|
||||
};
|
||||
|
||||
const updateChart = (type: typeChartUpdate): void => {
|
||||
if (!uPlotInst) return;
|
||||
switch (type) {
|
||||
case typeChartUpdate.xRange:
|
||||
uPlotInst.scales.x.range = getRangeX;
|
||||
break;
|
||||
}
|
||||
if (!isPanning) uPlotInst.redraw();
|
||||
};
|
||||
|
||||
useEffect(() => setXRange({ min: period.start, max: period.end }), [period]);
|
||||
|
||||
useEffect(() => {
|
||||
setStickyToolTips([]);
|
||||
setTooltipProps(null);
|
||||
resetTooltips();
|
||||
const isValidData = data[0] === null && Array.isArray(data[1]);
|
||||
if (!uPlotRef.current || !layoutSize.width || !layoutSize.height || !isValidData) return;
|
||||
if (!uPlotRef.current || !isValidData) return;
|
||||
const u = new uPlot(options, data, uPlotRef.current);
|
||||
setUPlotInst(u);
|
||||
setXRange({ min: period.start, max: period.end });
|
||||
return u.destroy;
|
||||
}, [uPlotRef.current, layoutSize, height, isDarkTheme, data]);
|
||||
|
||||
const handleTouchStart = (e: TouchEvent) => {
|
||||
if (e.touches.length !== 2) return;
|
||||
e.preventDefault();
|
||||
|
||||
const dx = e.touches[0].clientX - e.touches[1].clientX;
|
||||
const dy = e.touches[0].clientY - e.touches[1].clientY;
|
||||
setStartTouchDistance(Math.sqrt(dx * dx + dy * dy));
|
||||
};
|
||||
|
||||
const handleTouchMove = useCallback((e: TouchEvent) => {
|
||||
if (e.touches.length !== 2 || !uPlotInst) return;
|
||||
e.preventDefault();
|
||||
|
||||
const dx = e.touches[0].clientX - e.touches[1].clientX;
|
||||
const dy = e.touches[0].clientY - e.touches[1].clientY;
|
||||
const endTouchDistance = Math.sqrt(dx * dx + dy * dy);
|
||||
const diffDistance = startTouchDistance - endTouchDistance;
|
||||
|
||||
const max = (uPlotInst.scales.x.max || xRange.max);
|
||||
const min = (uPlotInst.scales.x.min || xRange.min);
|
||||
const dur = max - min;
|
||||
const dir = (diffDistance > 0 ? -1 : 1);
|
||||
|
||||
const zoomFactor = dur / 50 * dir;
|
||||
uPlotInst.batch(() => setPlotScale({
|
||||
min: min + zoomFactor,
|
||||
max: max - zoomFactor
|
||||
}));
|
||||
}, [uPlotInst, startTouchDistance, xRange]);
|
||||
|
||||
useEffect(() => updateChart(typeChartUpdate.xRange), [xRange]);
|
||||
useEffect(() => updateChart(typeChartUpdate.yRange), [yaxis]);
|
||||
}, [uPlotRef, data, isDarkTheme]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tooltipProps) onChangeLegend(tooltipProps);
|
||||
}, [tooltipProps]);
|
||||
if (!uPlotInst) return;
|
||||
uPlotInst.setSize({ width: layoutSize.width || 400, height: height || 500 });
|
||||
uPlotInst.redraw();
|
||||
}, [height, layoutSize]);
|
||||
|
||||
useEventListener("click", handleClick);
|
||||
useEventListener("keydown", handleKeyDown);
|
||||
useEventListener("touchmove", handleTouchMove);
|
||||
useEventListener("touchstart", handleTouchStart);
|
||||
useEffect(() => {
|
||||
onChangeLegend(tooltipProps);
|
||||
}, [tooltipProps]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -325,25 +153,13 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
|
||||
className="vm-line-chart__u-plot"
|
||||
ref={uPlotRef}
|
||||
/>
|
||||
{uPlotInst && tooltipProps && (
|
||||
<ChartTooltipHeatmap
|
||||
{...tooltipProps}
|
||||
unit={unit}
|
||||
u={uPlotInst}
|
||||
tooltipOffset={tooltipOffset}
|
||||
id={tooltipId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{uPlotInst && stickyTooltips.map(t => (
|
||||
<ChartTooltipHeatmap
|
||||
{...t}
|
||||
isSticky
|
||||
u={uPlotInst}
|
||||
key={t.id}
|
||||
onClose={handleUnStick}
|
||||
<ChartTooltipWrapper
|
||||
showTooltip={!!tooltipProps.show}
|
||||
tooltipProps={tooltipProps}
|
||||
stickyTooltips={stickyTooltips}
|
||||
handleUnStick={handleUnStick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,39 +1,39 @@
|
||||
import React, { FC, useEffect, useState } from "preact/compat";
|
||||
import { gradMetal16 } from "../../../../utils/uplot/heatmap";
|
||||
import React, { FC, useEffect, useMemo, useState } from "preact/compat";
|
||||
import { gradMetal16 } from "../../../../utils/uplot";
|
||||
import { SeriesItem, LegendItemType } from "../../../../types";
|
||||
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";
|
||||
import { ChartTooltipProps } from "../../ChartTooltip/ChartTooltip";
|
||||
|
||||
interface LegendHeatmapProps {
|
||||
min: number
|
||||
max: number
|
||||
legendValue: TooltipHeatmapProps | null,
|
||||
legendValue: ChartTooltipProps | null,
|
||||
series: SeriesItem[]
|
||||
}
|
||||
|
||||
const LegendHeatmap: FC<LegendHeatmapProps> = (
|
||||
{
|
||||
const LegendHeatmap: FC<LegendHeatmapProps> = ({
|
||||
min,
|
||||
max,
|
||||
legendValue,
|
||||
series,
|
||||
}
|
||||
) => {
|
||||
series
|
||||
}) => {
|
||||
|
||||
const [percent, setPercent] = useState(0);
|
||||
const [valueFormat, setValueFormat] = useState("");
|
||||
const [minFormat, setMinFormat] = useState("");
|
||||
const [maxFormat, setMaxFormat] = useState("");
|
||||
|
||||
const value = useMemo(() => {
|
||||
return parseFloat(String(legendValue?.value || 0).replace("%", ""));
|
||||
}, [legendValue]);
|
||||
|
||||
useEffect(() => {
|
||||
const value = legendValue?.value || 0;
|
||||
setPercent(value ? (value - min) / (max - min) * 100 : 0);
|
||||
setValueFormat(value ? `${value}%` : "");
|
||||
setMinFormat(`${min}%`);
|
||||
setMaxFormat(`${max}%`);
|
||||
}, [legendValue, min, max]);
|
||||
}, [value, min, max]);
|
||||
|
||||
return (
|
||||
<div className="vm-legend-heatmap__wrapper">
|
||||
@ -42,7 +42,7 @@ const LegendHeatmap: FC<LegendHeatmapProps> = (
|
||||
className="vm-legend-heatmap-gradient"
|
||||
style={{ background: `linear-gradient(to right, ${gradMetal16.join(", ")})` }}
|
||||
>
|
||||
{!!legendValue?.value && (
|
||||
{!!value && (
|
||||
<div
|
||||
className="vm-legend-heatmap-gradient__value"
|
||||
style={{ left: `${percent}%` }}
|
||||
|
@ -1,180 +0,0 @@
|
||||
import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from "preact/compat";
|
||||
import uPlot from "uplot";
|
||||
import { MetricResult } from "../../../../api/types";
|
||||
import { formatPrettyNumber } from "../../../../utils/uplot/helpers";
|
||||
import dayjs from "dayjs";
|
||||
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 classNames from "classnames";
|
||||
import { MouseEvent as ReactMouseEvent } from "react";
|
||||
import "./style.scss";
|
||||
import { SeriesItem } from "../../../../utils/uplot/series";
|
||||
import useEventListener from "../../../../hooks/useEventListener";
|
||||
|
||||
export interface ChartTooltipProps {
|
||||
id: string,
|
||||
u: uPlot,
|
||||
metricItem: MetricResult,
|
||||
seriesItem: SeriesItem,
|
||||
unit?: string,
|
||||
isSticky?: boolean,
|
||||
showQueryNum?: boolean,
|
||||
tooltipOffset: { left: number, top: number },
|
||||
tooltipIdx: { seriesIdx: number, dataIdx: number },
|
||||
onClose?: (id: string) => void
|
||||
}
|
||||
|
||||
const ChartTooltip: FC<ChartTooltipProps> = ({
|
||||
u,
|
||||
id,
|
||||
unit = "",
|
||||
metricItem,
|
||||
seriesItem,
|
||||
tooltipIdx,
|
||||
tooltipOffset,
|
||||
isSticky,
|
||||
showQueryNum,
|
||||
onClose
|
||||
}) => {
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [position, setPosition] = useState({ top: -999, left: -999 });
|
||||
const [moving, setMoving] = useState(false);
|
||||
const [moved, setMoved] = useState(false);
|
||||
|
||||
const [seriesIdx, setSeriesIdx] = useState(tooltipIdx.seriesIdx);
|
||||
const [dataIdx, setDataIdx] = useState(tooltipIdx.dataIdx);
|
||||
|
||||
const value = get(u, ["data", seriesIdx, dataIdx], 0);
|
||||
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 = `${seriesItem?.stroke}`;
|
||||
const calculations = seriesItem?.calculations || {};
|
||||
const group = metricItem?.group || 0;
|
||||
|
||||
|
||||
const fullMetricName = useMemo(() => {
|
||||
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__"] || "";
|
||||
if (labels.length > 0) {
|
||||
metricName += "{" + labels.join(",") + "}";
|
||||
}
|
||||
return metricName;
|
||||
}, [metricItem]);
|
||||
|
||||
const handleClose = () => {
|
||||
onClose && onClose(id);
|
||||
};
|
||||
|
||||
const handleMouseDown = (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
setMoved(true);
|
||||
setMoving(true);
|
||||
const { clientX, clientY } = e;
|
||||
setPosition({ top: clientY, left: clientX });
|
||||
};
|
||||
|
||||
const handleMouseMove = useCallback((e: MouseEvent) => {
|
||||
if (!moving) return;
|
||||
const { clientX, clientY } = e;
|
||||
setPosition({ top: clientY, left: clientX });
|
||||
}, [moving]);
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setMoving(false);
|
||||
};
|
||||
|
||||
const calcPosition = () => {
|
||||
if (!tooltipRef.current) return;
|
||||
|
||||
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();
|
||||
|
||||
const margin = 10;
|
||||
const overflowX = leftOnChart + tooltipWidth >= width ? tooltipWidth + (2 * margin) : 0;
|
||||
const overflowY = topOnChart + tooltipHeight >= height ? tooltipHeight + (2 * margin) : 0;
|
||||
|
||||
const position = {
|
||||
top: topOnChart + tooltipOffset.top + margin - overflowY,
|
||||
left: leftOnChart + tooltipOffset.left + margin - overflowX
|
||||
};
|
||||
|
||||
if (position.left < 0) position.left = 20;
|
||||
if (position.top < 0) position.top = 20;
|
||||
|
||||
setPosition(position);
|
||||
};
|
||||
|
||||
useEffect(calcPosition, [u, value, dataTime, seriesIdx, tooltipOffset, tooltipRef]);
|
||||
|
||||
useEffect(() => {
|
||||
setSeriesIdx(tooltipIdx.seriesIdx);
|
||||
setDataIdx(tooltipIdx.dataIdx);
|
||||
}, [tooltipIdx]);
|
||||
|
||||
useEventListener("mousemove", handleMouseMove);
|
||||
useEventListener("mouseup", handleMouseUp);
|
||||
|
||||
if (tooltipIdx.seriesIdx < 0 || tooltipIdx.dataIdx < 0) return null;
|
||||
|
||||
return ReactDOM.createPortal((
|
||||
<div
|
||||
className={classNames({
|
||||
"vm-chart-tooltip": true,
|
||||
"vm-chart-tooltip_sticky": isSticky,
|
||||
"vm-chart-tooltip_moved": moved
|
||||
|
||||
})}
|
||||
ref={tooltipRef}
|
||||
style={position}
|
||||
>
|
||||
<div className="vm-chart-tooltip-header">
|
||||
<div className="vm-chart-tooltip-header__date">
|
||||
{showQueryNum && (<div>Query {group}</div>)}
|
||||
{date}
|
||||
</div>
|
||||
{isSticky && (
|
||||
<>
|
||||
<Button
|
||||
className="vm-chart-tooltip-header__drag"
|
||||
variant="text"
|
||||
size="small"
|
||||
startIcon={<DragIcon/>}
|
||||
onMouseDown={handleMouseDown}
|
||||
/>
|
||||
<Button
|
||||
className="vm-chart-tooltip-header__close"
|
||||
variant="text"
|
||||
size="small"
|
||||
startIcon={<CloseIcon/>}
|
||||
onClick={handleClose}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="vm-chart-tooltip-data">
|
||||
<div
|
||||
className="vm-chart-tooltip-data__marker"
|
||||
style={{ background: color }}
|
||||
/>
|
||||
<div>
|
||||
<b>{valueFormat}{unit}</b><br/>
|
||||
median:<b>{calculations.median}</b>, min:<b>{calculations.min}</b>, max:<b>{calculations.max}</b>
|
||||
</div>
|
||||
</div>
|
||||
<div className="vm-chart-tooltip-info">
|
||||
{fullMetricName}
|
||||
</div>
|
||||
</div>
|
||||
), u.root);
|
||||
};
|
||||
|
||||
export default ChartTooltip;
|
@ -1,5 +1,5 @@
|
||||
import React, { FC, useMemo } from "preact/compat";
|
||||
import { LegendItemType } from "../../../../utils/uplot/types";
|
||||
import { LegendItemType } from "../../../../types";
|
||||
import LegendItem from "./LegendItem/LegendItem";
|
||||
import Accordion from "../../../Main/Accordion/Accordion";
|
||||
import "./style.scss";
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { FC, useMemo } from "preact/compat";
|
||||
import { MouseEvent } from "react";
|
||||
import { LegendItemType } from "../../../../../utils/uplot/types";
|
||||
import { LegendItemType } from "../../../../../types";
|
||||
import "./style.scss";
|
||||
import classNames from "classnames";
|
||||
import { getFreeFields } from "./helpers";
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { LegendItemType } from "../../../../../utils/uplot/types";
|
||||
import { LegendItemType } from "../../../../../types";
|
||||
|
||||
export const getFreeFields = (legend: LegendItemType) => {
|
||||
const keys = Object.keys(legend.freeFormFields).filter(f => f !== "__name__");
|
||||
|
@ -1,26 +1,33 @@
|
||||
import React, { FC, useCallback, useEffect, useRef, useState } from "preact/compat";
|
||||
import React, { FC, useEffect, useRef, useState } from "preact/compat";
|
||||
import uPlot, {
|
||||
AlignedData as uPlotData,
|
||||
Options as uPlotOptions,
|
||||
Series as uPlotSeries,
|
||||
} from "uplot";
|
||||
import { defaultOptions } from "../../../../utils/uplot/helpers";
|
||||
import { dragChart } from "../../../../utils/uplot/events";
|
||||
import { getAxes } from "../../../../utils/uplot/axes";
|
||||
import {
|
||||
getDefaultOptions,
|
||||
addSeries,
|
||||
delSeries,
|
||||
getRangeX,
|
||||
getRangeY,
|
||||
getScales,
|
||||
handleDestroy,
|
||||
getAxes,
|
||||
setSelect
|
||||
} from "../../../../utils/uplot";
|
||||
import { MetricResult } from "../../../../api/types";
|
||||
import { dateFromSeconds, formatDateForNativeInput, limitsDurations } from "../../../../utils/time";
|
||||
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 { ElementSize } from "../../../../hooks/useElementSize";
|
||||
import useEventListener from "../../../../hooks/useEventListener";
|
||||
import { getRangeX, getRangeY, getScales } from "../../../../utils/uplot/scales";
|
||||
import useReadyChart from "../../../../hooks/uplot/useReadyChart";
|
||||
import useZoomChart from "../../../../hooks/uplot/useZoomChart";
|
||||
import usePlotScale from "../../../../hooks/uplot/usePlotScale";
|
||||
import useLineTooltip from "../../../../hooks/uplot/useLineTooltip";
|
||||
import ChartTooltipWrapper from "../../ChartTooltip";
|
||||
|
||||
export interface LineChartProps {
|
||||
metrics: MetricResult[];
|
||||
@ -29,7 +36,7 @@ export interface LineChartProps {
|
||||
yaxis: YaxisState;
|
||||
series: uPlotSeries[];
|
||||
unit?: string;
|
||||
setPeriod: ({ from, to }: {from: Date, to: Date}) => void;
|
||||
setPeriod: ({ from, to }: { from: Date, to: Date }) => void;
|
||||
layoutSize: ElementSize;
|
||||
height?: number;
|
||||
}
|
||||
@ -48,208 +55,41 @@ const LineChart: FC<LineChartProps> = ({
|
||||
const { isDarkTheme } = useAppState();
|
||||
|
||||
const uPlotRef = useRef<HTMLDivElement>(null);
|
||||
const [isPanning, setPanning] = useState(false);
|
||||
const [xRange, setXRange] = useState({ min: period.start, max: period.end });
|
||||
const [uPlotInst, setUPlotInst] = useState<uPlot>();
|
||||
const [startTouchDistance, setStartTouchDistance] = useState(0);
|
||||
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
const [tooltipIdx, setTooltipIdx] = useState({ seriesIdx: -1, dataIdx: -1 });
|
||||
const [tooltipOffset, setTooltipOffset] = useState({ left: 0, top: 0 });
|
||||
const [stickyTooltips, setStickyToolTips] = useState<ChartTooltipProps[]>([]);
|
||||
|
||||
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 onReadyChart = (u: uPlot): void => {
|
||||
const factor = 0.9;
|
||||
setTooltipOffset({
|
||||
left: parseFloat(u.over.style.left),
|
||||
top: parseFloat(u.over.style.top)
|
||||
});
|
||||
|
||||
u.over.addEventListener("mousedown", e => {
|
||||
const { ctrlKey, metaKey, button } = e;
|
||||
const leftClick = button === 0;
|
||||
const leftClickWithMeta = leftClick && (ctrlKey || metaKey);
|
||||
if (leftClickWithMeta) {
|
||||
// drag pan
|
||||
dragChart({ u, e, setPanning, setPlotScale, factor });
|
||||
}
|
||||
});
|
||||
|
||||
u.over.addEventListener("touchstart", e => {
|
||||
dragChart({ u, e, setPanning, setPlotScale, factor });
|
||||
});
|
||||
|
||||
u.over.addEventListener("wheel", e => {
|
||||
if (!e.ctrlKey && !e.metaKey) return;
|
||||
e.preventDefault();
|
||||
const { width } = u.over.getBoundingClientRect();
|
||||
const zoomPos = u.cursor.left && u.cursor.left > 0 ? u.cursor.left : 0;
|
||||
const xVal = u.posToVal(zoomPos, "x");
|
||||
const oxRange = (u.scales.x.max || 0) - (u.scales.x.min || 0);
|
||||
const nxRange = e.deltaY < 0 ? oxRange * factor : oxRange / factor;
|
||||
const min = xVal - (zoomPos / width) * nxRange;
|
||||
const max = min + nxRange;
|
||||
u.batch(() => setPlotScale({ min, max }));
|
||||
});
|
||||
};
|
||||
|
||||
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
const { target, ctrlKey, metaKey, key } = e;
|
||||
const isInput = target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement;
|
||||
if (!uPlotInst || isInput) return;
|
||||
const minus = key === "-";
|
||||
const plus = key === "+" || key === "=";
|
||||
if ((minus || plus) && !(ctrlKey || metaKey)) {
|
||||
e.preventDefault();
|
||||
const factor = (xRange.max - xRange.min) / 10 * (plus ? 1 : -1);
|
||||
setPlotScale({
|
||||
min: xRange.min + factor,
|
||||
max: xRange.max - factor
|
||||
});
|
||||
}
|
||||
}, [uPlotInst, xRange]);
|
||||
|
||||
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,
|
||||
seriesItem,
|
||||
metricItem,
|
||||
tooltipIdx,
|
||||
tooltipOffset,
|
||||
showQueryNum,
|
||||
};
|
||||
}, [uPlotInst, metrics, series, tooltipIdx, tooltipOffset, unit]);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (!showTooltip) return;
|
||||
const props = getChartProps();
|
||||
if (!stickyTooltips.find(t => t.id === props.id)) {
|
||||
setStickyToolTips(prev => [...prev, props as ChartTooltipProps]);
|
||||
}
|
||||
}, [getChartProps, stickyTooltips, showTooltip]);
|
||||
|
||||
const handleUnStick = (id: string) => {
|
||||
setStickyToolTips(prev => prev.filter(t => t.id !== id));
|
||||
};
|
||||
|
||||
const setCursor = (u: uPlot) => {
|
||||
const dataIdx = u.cursor.idx ?? -1;
|
||||
setTooltipIdx(prev => ({ ...prev, dataIdx }));
|
||||
};
|
||||
|
||||
const seriesFocus = (u: uPlot, sidx: (number | null)) => {
|
||||
const seriesIdx = sidx ?? -1;
|
||||
setTooltipIdx(prev => ({ ...prev, seriesIdx }));
|
||||
};
|
||||
|
||||
const addSeries = (u: uPlot, series: uPlotSeries[]) => {
|
||||
series.forEach((s) => {
|
||||
u.addSeries(s);
|
||||
});
|
||||
};
|
||||
|
||||
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] = [];
|
||||
});
|
||||
};
|
||||
|
||||
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 { xRange, setPlotScale } = usePlotScale({ period, setPeriod });
|
||||
const { onReadyChart, isPanning } = useReadyChart(setPlotScale);
|
||||
useZoomChart({ uPlotInst, xRange, setPlotScale });
|
||||
const {
|
||||
showTooltip,
|
||||
stickyTooltips,
|
||||
handleUnStick,
|
||||
getTooltipProps,
|
||||
seriesFocus,
|
||||
setCursor,
|
||||
resetTooltips
|
||||
} = useLineTooltip({ u: uPlotInst, metrics, series, unit });
|
||||
|
||||
const options: uPlotOptions = {
|
||||
...defaultOptions,
|
||||
tzDate: ts => dayjs(formatDateForNativeInput(dateFromSeconds(ts))).local().toDate(),
|
||||
...getDefaultOptions({ width: layoutSize.width, height }),
|
||||
series,
|
||||
axes: getAxes( [{}, { scale: "1" }], unit),
|
||||
axes: getAxes([{}, { scale: "1" }], unit),
|
||||
scales: getScales(yaxis, xRange),
|
||||
width: layoutSize.width || 400,
|
||||
height: height || 500,
|
||||
hooks: {
|
||||
ready: [onReadyChart],
|
||||
setSeries: [seriesFocus],
|
||||
setCursor: [setCursor],
|
||||
setSelect: [setSelect],
|
||||
setSelect: [setSelect(setPlotScale)],
|
||||
destroy: [handleDestroy],
|
||||
},
|
||||
};
|
||||
|
||||
const handleTouchStart = (e: TouchEvent) => {
|
||||
if (e.touches.length !== 2) return;
|
||||
e.preventDefault();
|
||||
|
||||
const dx = e.touches[0].clientX - e.touches[1].clientX;
|
||||
const dy = e.touches[0].clientY - e.touches[1].clientY;
|
||||
setStartTouchDistance(Math.sqrt(dx * dx + dy * dy));
|
||||
};
|
||||
|
||||
const handleTouchMove = useCallback((e: TouchEvent) => {
|
||||
if (e.touches.length !== 2 || !uPlotInst) return;
|
||||
e.preventDefault();
|
||||
|
||||
const dx = e.touches[0].clientX - e.touches[1].clientX;
|
||||
const dy = e.touches[0].clientY - e.touches[1].clientY;
|
||||
const endTouchDistance = Math.sqrt(dx * dx + dy * dy);
|
||||
const diffDistance = startTouchDistance - endTouchDistance;
|
||||
|
||||
const max = (uPlotInst.scales.x.max || xRange.max);
|
||||
const min = (uPlotInst.scales.x.min || xRange.min);
|
||||
const dur = max - min;
|
||||
const dir = (diffDistance > 0 ? -1 : 1);
|
||||
|
||||
const zoomFactor = dur / 50 * dir;
|
||||
uPlotInst.batch(() => setPlotScale({
|
||||
min: min + zoomFactor,
|
||||
max: max - zoomFactor
|
||||
}));
|
||||
}, [uPlotInst, startTouchDistance, xRange]);
|
||||
|
||||
useEffect(() => {
|
||||
setXRange({ min: period.start, max: period.end });
|
||||
}, [period]);
|
||||
|
||||
useEffect(() => {
|
||||
setStickyToolTips([]);
|
||||
setTooltipIdx({ seriesIdx: -1, dataIdx: -1 });
|
||||
resetTooltips();
|
||||
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]);
|
||||
|
||||
@ -287,15 +127,6 @@ const LineChart: FC<LineChartProps> = ({
|
||||
uPlotInst.redraw();
|
||||
}, [height, layoutSize]);
|
||||
|
||||
useEffect(() => {
|
||||
setShowTooltip(tooltipIdx.dataIdx !== -1 && tooltipIdx.seriesIdx !== -1);
|
||||
}, [tooltipIdx]);
|
||||
|
||||
useEventListener("click", handleClick);
|
||||
useEventListener("keydown", handleKeyDown);
|
||||
useEventListener("touchmove", handleTouchMove);
|
||||
useEventListener("touchstart", handleTouchStart);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
@ -311,22 +142,12 @@ const LineChart: FC<LineChartProps> = ({
|
||||
className="vm-line-chart__u-plot"
|
||||
ref={uPlotRef}
|
||||
/>
|
||||
{uPlotInst && showTooltip && (
|
||||
<ChartTooltip
|
||||
{...getChartProps()}
|
||||
u={uPlotInst}
|
||||
<ChartTooltipWrapper
|
||||
showTooltip={showTooltip}
|
||||
tooltipProps={getTooltipProps()}
|
||||
stickyTooltips={stickyTooltips}
|
||||
handleUnStick={handleUnStick}
|
||||
/>
|
||||
)}
|
||||
|
||||
{uPlotInst && stickyTooltips.map(t => (
|
||||
<ChartTooltip
|
||||
{...t}
|
||||
isSticky
|
||||
u={uPlotInst}
|
||||
key={t.id}
|
||||
onClose={handleUnStick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -8,11 +8,12 @@ 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";
|
||||
normalizeData,
|
||||
getLimitsYAxis,
|
||||
getMinMaxBuffer,
|
||||
getTimeSeries,
|
||||
} from "../../../utils/uplot";
|
||||
import { TimeParams, SeriesItem, LegendItemType } from "../../../types";
|
||||
import { AxisRange, YaxisState } from "../../../state/graph/reducer";
|
||||
import { getAvgFromArray, getMaxFromArray, getMinFromArray } from "../../../utils/math";
|
||||
import classNames from "classnames";
|
||||
@ -20,10 +21,9 @@ import { useTimeState } from "../../../state/time/TimeStateContext";
|
||||
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";
|
||||
import useElementSize from "../../../hooks/useElementSize";
|
||||
import { ChartTooltipProps } from "../../Chart/ChartTooltip/ChartTooltip";
|
||||
|
||||
export interface GraphViewProps {
|
||||
data?: MetricResult[];
|
||||
@ -35,7 +35,7 @@ export interface GraphViewProps {
|
||||
unit?: string;
|
||||
showLegend?: boolean;
|
||||
setYaxisLimits: (val: AxisRange) => void
|
||||
setPeriod: ({ from, to }: {from: Date, to: Date}) => void
|
||||
setPeriod: ({ from, to }: { from: Date, to: Date }) => void
|
||||
fullWidth?: boolean
|
||||
height?: number
|
||||
isHistogram?: boolean
|
||||
@ -48,7 +48,7 @@ const GraphView: FC<GraphViewProps> = ({
|
||||
query,
|
||||
yaxis,
|
||||
unit,
|
||||
showLegend= true,
|
||||
showLegend = true,
|
||||
setYaxisLimits,
|
||||
setPeriod,
|
||||
alias = [],
|
||||
@ -66,13 +66,13 @@ const GraphView: FC<GraphViewProps> = ({
|
||||
const [series, setSeries] = useState<uPlotSeries[]>([]);
|
||||
const [legend, setLegend] = useState<LegendItemType[]>([]);
|
||||
const [hideSeries, setHideSeries] = useState<string[]>([]);
|
||||
const [legendValue, setLegendValue] = useState<TooltipHeatmapProps | null>(null);
|
||||
const [legendValue, setLegendValue] = useState<ChartTooltipProps | 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);
|
||||
setYaxisLimits(limits);
|
||||
};
|
||||
@ -105,7 +105,7 @@ const GraphView: FC<GraphViewProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
const tempTimes: number[] = [];
|
||||
const tempValues: {[key: string]: number[]} = {};
|
||||
const tempValues: { [key: string]: number[] } = {};
|
||||
const tempLegend: LegendItemType[] = [];
|
||||
const tempSeries: uPlotSeries[] = [{}];
|
||||
|
||||
@ -199,7 +199,6 @@ const GraphView: FC<GraphViewProps> = ({
|
||||
data={dataChart}
|
||||
metrics={data}
|
||||
period={period}
|
||||
yaxis={yaxis}
|
||||
unit={unit}
|
||||
setPeriod={setPeriod}
|
||||
layoutSize={containerSize}
|
||||
|
65
app/vmui/packages/vmui/src/hooks/uplot/useDragChart.ts
Normal file
65
app/vmui/packages/vmui/src/hooks/uplot/useDragChart.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { useRef } from "preact/compat";
|
||||
import uPlot from "uplot";
|
||||
import { SetMinMax } from "../../types";
|
||||
|
||||
interface DragHookArgs {
|
||||
dragSpeed: number,
|
||||
setPanning: (enable: boolean) => void,
|
||||
setPlotScale: SetMinMax
|
||||
}
|
||||
|
||||
interface DragArgs {
|
||||
e: MouseEvent | TouchEvent,
|
||||
u: uPlot,
|
||||
}
|
||||
|
||||
const isMouseEvent = (e: MouseEvent | TouchEvent): e is MouseEvent => e instanceof MouseEvent;
|
||||
const getClientX = (e: MouseEvent | TouchEvent) => isMouseEvent(e) ? e.clientX : e.touches[0].clientX;
|
||||
|
||||
const useDragChart = ({ dragSpeed = 0.85, setPanning, setPlotScale }: DragHookArgs) => {
|
||||
const dragState = useRef({
|
||||
leftStart: 0,
|
||||
xUnitsPerPx: 0,
|
||||
scXMin: 0,
|
||||
scXMax: 0,
|
||||
});
|
||||
|
||||
const mouseMove = (e: MouseEvent | TouchEvent) => {
|
||||
e.preventDefault();
|
||||
const clientX = getClientX(e);
|
||||
const { leftStart, xUnitsPerPx, scXMin, scXMax } = dragState.current;
|
||||
const dx = xUnitsPerPx * ((clientX - leftStart) * dragSpeed);
|
||||
setPlotScale({ min: scXMin - dx, max: scXMax - dx });
|
||||
};
|
||||
|
||||
const mouseUp = () => {
|
||||
setPanning(false);
|
||||
document.removeEventListener("mousemove", mouseMove);
|
||||
document.removeEventListener("mouseup", mouseUp);
|
||||
document.removeEventListener("touchmove", mouseMove);
|
||||
document.removeEventListener("touchend", mouseUp);
|
||||
};
|
||||
|
||||
const mouseDown = () => {
|
||||
document.addEventListener("mousemove", mouseMove);
|
||||
document.addEventListener("mouseup", mouseUp);
|
||||
document.addEventListener("touchmove", mouseMove);
|
||||
document.addEventListener("touchend", mouseUp);
|
||||
};
|
||||
|
||||
return ({ e, u }: DragArgs): void => {
|
||||
e.preventDefault();
|
||||
setPanning(true);
|
||||
|
||||
dragState.current = {
|
||||
leftStart: getClientX(e),
|
||||
xUnitsPerPx: u.posToVal(1, "x") - u.posToVal(0, "x"),
|
||||
scXMin: u.scales.x.min || 0,
|
||||
scXMax: u.scales.x.max || 0,
|
||||
};
|
||||
|
||||
mouseDown();
|
||||
};
|
||||
};
|
||||
|
||||
export default useDragChart;
|
82
app/vmui/packages/vmui/src/hooks/uplot/useHeatmapTooltip.ts
Normal file
82
app/vmui/packages/vmui/src/hooks/uplot/useHeatmapTooltip.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import uPlot from "uplot";
|
||||
import { useCallback, useState } from "preact/compat";
|
||||
import { ChartTooltipProps } from "../../components/Chart/ChartTooltip/ChartTooltip";
|
||||
import dayjs from "dayjs";
|
||||
import { DATE_FULL_TIMEZONE_FORMAT } from "../../constants/date";
|
||||
import { MetricResult } from "../../api/types";
|
||||
import useEventListener from "../useEventListener";
|
||||
import get from "lodash.get";
|
||||
|
||||
interface LineTooltipHook {
|
||||
u?: uPlot;
|
||||
metrics: MetricResult[];
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
const useLineTooltip = ({ u, metrics, unit }: LineTooltipHook) => {
|
||||
const [point, setPoint] = useState({ left: 0, top: 0 });
|
||||
const [stickyTooltips, setStickyToolTips] = useState<ChartTooltipProps[]>([]);
|
||||
|
||||
const resetTooltips = () => {
|
||||
setStickyToolTips([]);
|
||||
setPoint({ left: 0, top: 0 });
|
||||
};
|
||||
|
||||
const setCursor = (u: uPlot) => {
|
||||
const left = u.cursor.left || 0;
|
||||
const top = u.cursor.top || 0;
|
||||
setPoint({ left, top });
|
||||
};
|
||||
|
||||
const getTooltipProps = useCallback((): ChartTooltipProps => {
|
||||
const { left, top } = point;
|
||||
const xArr = (get(u, ["data", 1, 0], []) || []) as number[];
|
||||
const xVal = u ? u.posToVal(left, "x") : 0;
|
||||
const yVal = u ? u.posToVal(top, "y") : 0;
|
||||
const xIdx = xArr.findIndex((t, i) => xVal >= t && xVal < xArr[i + 1]) || -1;
|
||||
const second = xArr[xIdx + 1];
|
||||
const result = metrics[Math.round(yVal)] || { values: [] };
|
||||
|
||||
const [endTime = 0, value = ""] = result.values.find(v => v[0] === second) || [];
|
||||
const startTime = xArr[xIdx];
|
||||
const startDate = dayjs(startTime * 1000).tz().format(DATE_FULL_TIMEZONE_FORMAT);
|
||||
const endDate = dayjs(endTime * 1000).tz().format(DATE_FULL_TIMEZONE_FORMAT);
|
||||
|
||||
const bucket = result?.metric?.vmrange || "";
|
||||
|
||||
return {
|
||||
unit,
|
||||
point,
|
||||
u: u,
|
||||
id: `${bucket}_${startDate}`,
|
||||
dates: [startDate, endDate],
|
||||
value: `${value}%`,
|
||||
info: bucket,
|
||||
show: +value > 0
|
||||
};
|
||||
}, [u, point, metrics, unit]);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
const props = getTooltipProps();
|
||||
if (!props.show) return;
|
||||
if (!stickyTooltips.find(t => t.id === props.id)) {
|
||||
setStickyToolTips(prev => [...prev, props]);
|
||||
}
|
||||
}, [getTooltipProps, stickyTooltips]);
|
||||
|
||||
const handleUnStick = (id: string) => {
|
||||
setStickyToolTips(prev => prev.filter(t => t.id !== id));
|
||||
};
|
||||
|
||||
useEventListener("click", handleClick);
|
||||
|
||||
return {
|
||||
stickyTooltips,
|
||||
handleUnStick,
|
||||
getTooltipProps,
|
||||
setCursor,
|
||||
resetTooltips
|
||||
};
|
||||
};
|
||||
|
||||
export default useLineTooltip;
|
101
app/vmui/packages/vmui/src/hooks/uplot/useLineTooltip.ts
Normal file
101
app/vmui/packages/vmui/src/hooks/uplot/useLineTooltip.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import uPlot, { Series as uPlotSeries } from "uplot";
|
||||
import { useCallback, useEffect, useState } from "preact/compat";
|
||||
import { ChartTooltipProps } from "../../components/Chart/ChartTooltip/ChartTooltip";
|
||||
import { SeriesItem } from "../../types";
|
||||
import get from "lodash.get";
|
||||
import dayjs from "dayjs";
|
||||
import { DATE_FULL_TIMEZONE_FORMAT } from "../../constants/date";
|
||||
import { formatPrettyNumber, getMetricName } from "../../utils/uplot";
|
||||
import { MetricResult } from "../../api/types";
|
||||
import useEventListener from "../useEventListener";
|
||||
|
||||
interface LineTooltipHook {
|
||||
u?: uPlot;
|
||||
metrics: MetricResult[];
|
||||
series: uPlotSeries[];
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
const useLineTooltip = ({ u, metrics, series, unit }: LineTooltipHook) => {
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
const [tooltipIdx, setTooltipIdx] = useState({ seriesIdx: -1, dataIdx: -1 });
|
||||
const [stickyTooltips, setStickyToolTips] = useState<ChartTooltipProps[]>([]);
|
||||
|
||||
const resetTooltips = () => {
|
||||
setStickyToolTips([]);
|
||||
setTooltipIdx({ seriesIdx: -1, dataIdx: -1 });
|
||||
};
|
||||
|
||||
const setCursor = (u: uPlot) => {
|
||||
const dataIdx = u.cursor.idx ?? -1;
|
||||
setTooltipIdx(prev => ({ ...prev, dataIdx }));
|
||||
};
|
||||
|
||||
const seriesFocus = (u: uPlot, sidx: (number | null)) => {
|
||||
const seriesIdx = sidx ?? -1;
|
||||
setTooltipIdx(prev => ({ ...prev, seriesIdx }));
|
||||
};
|
||||
|
||||
const getTooltipProps = useCallback((): ChartTooltipProps => {
|
||||
const { seriesIdx, dataIdx } = tooltipIdx;
|
||||
const metricItem = metrics[seriesIdx - 1];
|
||||
const seriesItem = series[seriesIdx] as SeriesItem;
|
||||
|
||||
const groups = new Set(metrics.map(m => m.group));
|
||||
const group = metricItem?.group || 0;
|
||||
|
||||
const value = get(u, ["data", seriesIdx, dataIdx], 0);
|
||||
const min = get(u, ["scales", "1", "min"], 0);
|
||||
const max = get(u, ["scales", "1", "max"], 1);
|
||||
|
||||
const date = get(u, ["data", 0, dataIdx], 0);
|
||||
|
||||
const point = {
|
||||
top: u ? u.valToPos((value || 0), seriesItem?.scale || "1") : 0,
|
||||
left: u ? u.valToPos(date, "x") : 0,
|
||||
};
|
||||
|
||||
return {
|
||||
unit,
|
||||
point,
|
||||
u: u,
|
||||
id: `${seriesIdx}_${dataIdx}`,
|
||||
title: groups.size > 1 ? `Query ${group}` : "",
|
||||
dates: [date ? dayjs(date * 1000).tz().format(DATE_FULL_TIMEZONE_FORMAT) : "-"],
|
||||
value: formatPrettyNumber(value, min, max),
|
||||
info: getMetricName(metricItem),
|
||||
stats: seriesItem?.calculations,
|
||||
marker: `${seriesItem?.stroke}`,
|
||||
};
|
||||
}, [u, tooltipIdx, metrics, series, unit]);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (!showTooltip) return;
|
||||
const props = getTooltipProps();
|
||||
if (!stickyTooltips.find(t => t.id === props.id)) {
|
||||
setStickyToolTips(prev => [...prev, props]);
|
||||
}
|
||||
}, [getTooltipProps, stickyTooltips, showTooltip]);
|
||||
|
||||
const handleUnStick = (id: string) => {
|
||||
setStickyToolTips(prev => prev.filter(t => t.id !== id));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setShowTooltip(tooltipIdx.dataIdx !== -1 && tooltipIdx.seriesIdx !== -1);
|
||||
}, [tooltipIdx]);
|
||||
|
||||
useEventListener("click", handleClick);
|
||||
|
||||
return {
|
||||
showTooltip,
|
||||
stickyTooltips,
|
||||
handleUnStick,
|
||||
getTooltipProps,
|
||||
seriesFocus,
|
||||
setCursor,
|
||||
resetTooltips,
|
||||
};
|
||||
};
|
||||
|
||||
export default useLineTooltip;
|
34
app/vmui/packages/vmui/src/hooks/uplot/usePlotScale.ts
Normal file
34
app/vmui/packages/vmui/src/hooks/uplot/usePlotScale.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { MinMax } from "../../types";
|
||||
import { limitsDurations } from "../../utils/time";
|
||||
import { useEffect, useState } from "preact/compat";
|
||||
import { TimeParams } from "../../types";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
interface PlotScaleHook {
|
||||
setPeriod: ({ from, to }: { from: Date, to: Date }) => void;
|
||||
period: TimeParams;
|
||||
}
|
||||
|
||||
const usePlotScale = ({ period, setPeriod }: PlotScaleHook) => {
|
||||
const [xRange, setXRange] = useState({ min: period.start, max: period.end });
|
||||
|
||||
const setPlotScale = ({ min, max }: MinMax) => {
|
||||
const delta = (max - min) * 1000;
|
||||
if ((delta < limitsDurations.min) || (delta > limitsDurations.max)) return;
|
||||
setPeriod({
|
||||
from: dayjs(min * 1000).toDate(),
|
||||
to: dayjs(max * 1000).toDate()
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setXRange({ min: period.start, max: period.end });
|
||||
}, [period]);
|
||||
|
||||
return {
|
||||
xRange,
|
||||
setPlotScale
|
||||
};
|
||||
};
|
||||
|
||||
export default usePlotScale;
|
53
app/vmui/packages/vmui/src/hooks/uplot/useReadyChart.ts
Normal file
53
app/vmui/packages/vmui/src/hooks/uplot/useReadyChart.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { useState } from "preact/compat";
|
||||
import uPlot from "uplot";
|
||||
import useDragChart from "./useDragChart";
|
||||
import { SetMinMax } from "../../types";
|
||||
|
||||
const isLiftClickWithMeta = (e: MouseEvent) => {
|
||||
const { ctrlKey, metaKey, button } = e;
|
||||
const leftClick = button === 0;
|
||||
return leftClick && (ctrlKey || metaKey);
|
||||
};
|
||||
|
||||
// coefficient for drag speed; the higher the value, the faster the graph moves.
|
||||
const dragSpeed = 0.9;
|
||||
|
||||
const useReadyChart = (setPlotScale: SetMinMax) => {
|
||||
const [isPanning, setPanning] = useState(false);
|
||||
|
||||
const dragChart = useDragChart({ dragSpeed, setPanning, setPlotScale });
|
||||
|
||||
const onReadyChart = (u: uPlot): void => {
|
||||
const handleInteractionStart = (e: MouseEvent | TouchEvent) => {
|
||||
const dragByMouse = e instanceof MouseEvent && isLiftClickWithMeta(e);
|
||||
const dragByTouch = e instanceof TouchEvent && e.touches.length > 1;
|
||||
if (dragByMouse || dragByTouch) {
|
||||
dragChart({ u, e });
|
||||
}
|
||||
};
|
||||
|
||||
const handleWheel = (e: WheelEvent) => {
|
||||
if (!e.ctrlKey && !e.metaKey) return;
|
||||
e.preventDefault();
|
||||
const { width } = u.over.getBoundingClientRect();
|
||||
const zoomPos = u.cursor.left && u.cursor.left > 0 ? u.cursor.left : 0;
|
||||
const xVal = u.posToVal(zoomPos, "x");
|
||||
const oxRange = (u.scales.x.max || 0) - (u.scales.x.min || 0);
|
||||
const nxRange = e.deltaY < 0 ? oxRange * dragSpeed : oxRange / dragSpeed;
|
||||
const min = xVal - (zoomPos / width) * nxRange;
|
||||
const max = min + nxRange;
|
||||
u.batch(() => setPlotScale({ min, max }));
|
||||
};
|
||||
|
||||
u.over.addEventListener("mousedown", handleInteractionStart);
|
||||
u.over.addEventListener("touchstart", handleInteractionStart);
|
||||
u.over.addEventListener("wheel", handleWheel);
|
||||
};
|
||||
|
||||
return {
|
||||
onReadyChart,
|
||||
isPanning,
|
||||
};
|
||||
};
|
||||
|
||||
export default useReadyChart;
|
66
app/vmui/packages/vmui/src/hooks/uplot/useZoomChart.ts
Normal file
66
app/vmui/packages/vmui/src/hooks/uplot/useZoomChart.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { useCallback, useState } from "preact/compat";
|
||||
import useEventListener from "../useEventListener";
|
||||
import { MinMax, SetMinMax } from "../../types";
|
||||
|
||||
interface ZoomChartHook {
|
||||
uPlotInst?: uPlot;
|
||||
xRange: MinMax;
|
||||
setPlotScale: SetMinMax;
|
||||
}
|
||||
|
||||
const calculateDistance = (touches: TouchList) => {
|
||||
const dx = touches[0].clientX - touches[1].clientX;
|
||||
const dy = touches[0].clientY - touches[1].clientY;
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
};
|
||||
|
||||
const useZoomChart = ({ uPlotInst, xRange, setPlotScale }: ZoomChartHook) => {
|
||||
const [startTouchDistance, setStartTouchDistance] = useState(0);
|
||||
|
||||
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
const { target, ctrlKey, metaKey, key } = e;
|
||||
const isInput = target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement;
|
||||
if (!uPlotInst || isInput) return;
|
||||
|
||||
const isPlus = key === "+" || key === "=";
|
||||
const isMinus = key === "-";
|
||||
const isNotControlKey = !(ctrlKey || metaKey);
|
||||
|
||||
if ((isMinus || isPlus) && isNotControlKey) {
|
||||
e.preventDefault();
|
||||
const factor = (xRange.max - xRange.min) / 10 * (isPlus ? 1 : -1);
|
||||
setPlotScale({ min: xRange.min + factor, max: xRange.max - factor });
|
||||
}
|
||||
}, [uPlotInst, xRange]);
|
||||
|
||||
const handleTouchStart = (e: TouchEvent) => {
|
||||
if (e.touches.length === 2) {
|
||||
e.preventDefault();
|
||||
setStartTouchDistance(calculateDistance(e.touches));
|
||||
}
|
||||
};
|
||||
|
||||
const handleTouchMove = useCallback((e: TouchEvent) => {
|
||||
if (!uPlotInst || e.touches.length !== 2) return;
|
||||
e.preventDefault();
|
||||
|
||||
const endTouchDistance = calculateDistance(e.touches);
|
||||
const diffDistance = startTouchDistance - endTouchDistance;
|
||||
|
||||
const max = (uPlotInst.scales.x.max || xRange.max);
|
||||
const min = (uPlotInst.scales.x.min || xRange.min);
|
||||
const dur = max - min;
|
||||
const dir = (diffDistance > 0 ? -1 : 1);
|
||||
|
||||
const zoomFactor = dur / 50 * dir;
|
||||
uPlotInst.batch(() => setPlotScale({ min: min + zoomFactor, max: max - zoomFactor }));
|
||||
}, [uPlotInst, startTouchDistance, xRange]);
|
||||
|
||||
useEventListener("keydown", handleKeyDown);
|
||||
useEventListener("touchmove", handleTouchMove);
|
||||
useEventListener("touchstart", handleTouchStart);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default useZoomChart;
|
@ -83,11 +83,13 @@ export const useFetchQuery = ({
|
||||
setFetchQueue([...fetchQueue, controller]);
|
||||
try {
|
||||
const isDisplayChart = displayType === "chart";
|
||||
let seriesLimit = showAllSeries ? Infinity : (+stateSeriesLimits[displayType] || Infinity);
|
||||
const defaultLimit = showAllSeries ? Infinity : (+stateSeriesLimits[displayType] || Infinity);
|
||||
let seriesLimit = defaultLimit;
|
||||
const tempData: MetricBase[] = [];
|
||||
const tempTraces: Trace[] = [];
|
||||
let counter = 1;
|
||||
let totalLength = 0;
|
||||
let isHistogramResult = false;
|
||||
|
||||
for await (const url of fetchUrl) {
|
||||
|
||||
@ -115,9 +117,8 @@ export const useFetchQuery = ({
|
||||
tempTraces.push(trace);
|
||||
}
|
||||
|
||||
const isHistogramResult = isDisplayChart && isHistogramData(resp.data.result);
|
||||
if (resp.data.result.length) setIsHistogram(isHistogramResult);
|
||||
if (isHistogramResult) seriesLimit = Infinity;
|
||||
isHistogramResult = isDisplayChart && isHistogramData(resp.data.result);
|
||||
seriesLimit = isHistogramResult ? Infinity : Math.max(totalLength, defaultLimit);
|
||||
const freeTempSize = seriesLimit - tempData.length;
|
||||
resp.data.result.slice(0, freeTempSize).forEach((d: MetricBase) => {
|
||||
d.group = counter;
|
||||
@ -136,6 +137,7 @@ export const useFetchQuery = ({
|
||||
setWarning(totalLength > seriesLimit ? limitText : "");
|
||||
isDisplayChart ? setGraphData(tempData as MetricResult[]) : setLiveData(tempData as InstantMetricResult[]);
|
||||
setTraces(tempTraces);
|
||||
setIsHistogram(totalLength ? isHistogramResult: false);
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.name !== "AbortError") {
|
||||
setError(`${e.name}: ${e.message}`);
|
||||
|
@ -6,7 +6,7 @@ import { useQueryState } from "../../../state/query/QueryStateContext";
|
||||
import { displayTypeTabs } from "../DisplayTypeSwitch";
|
||||
import { compactObject } from "../../../utils/object";
|
||||
import { useGraphState } from "../../../state/graph/GraphStateContext";
|
||||
import useSearchParamsFromObject from "../../../hooks/useSearchParamsFromObject";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
|
||||
export const useSetQueryParams = () => {
|
||||
const { tenantId } = useAppState();
|
||||
@ -14,7 +14,7 @@ export const useSetQueryParams = () => {
|
||||
const { query } = useQueryState();
|
||||
const { duration, relativeTime, period: { date, step } } = useTimeState();
|
||||
const { customStep } = useGraphState();
|
||||
const { setSearchParamsFromKeys } = useSearchParamsFromObject();
|
||||
const [, setSearchParams] = useSearchParams();
|
||||
|
||||
const setSearchParamsFromState = () => {
|
||||
const params: Record<string, unknown> = {};
|
||||
@ -30,7 +30,7 @@ export const useSetQueryParams = () => {
|
||||
if ((step !== customStep) && customStep) params[`${group}.step_input`] = customStep;
|
||||
});
|
||||
|
||||
setSearchParamsFromKeys(compactObject(params) as Record<string, string>);
|
||||
setSearchParams(compactObject(params) as Record<string, string>);
|
||||
};
|
||||
|
||||
useEffect(setSearchParamsFromState, [tenantId, displayType, query, duration, relativeTime, date, step, customStep]);
|
||||
|
@ -2,7 +2,7 @@ import React, { FC, useEffect, useMemo, KeyboardEvent } from "react";
|
||||
import { useFetchTopQueries } from "./hooks/useFetchTopQueries";
|
||||
import Spinner from "../../components/Main/Spinner/Spinner";
|
||||
import TopQueryPanel from "./TopQueryPanel/TopQueryPanel";
|
||||
import { formatPrettyNumber } from "../../utils/uplot/helpers";
|
||||
import { formatPrettyNumber } from "../../utils/uplot";
|
||||
import { isSupportedDuration } from "../../utils/time";
|
||||
import dayjs from "dayjs";
|
||||
import { TopQueryStats } from "../../types";
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { MetricBase } from "../api/types";
|
||||
export * from "./uplot";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
@ -1,4 +1,16 @@
|
||||
import uPlot, { Series } from "uplot";
|
||||
import { Axis, Series } from "uplot";
|
||||
|
||||
export interface SeriesItemStats {
|
||||
min: string,
|
||||
max: string,
|
||||
median: string,
|
||||
last: string
|
||||
}
|
||||
|
||||
export interface SeriesItem extends Series {
|
||||
freeFormFields: {[key: string]: string};
|
||||
calculations: SeriesItemStats
|
||||
}
|
||||
|
||||
export interface HideSeriesArgs {
|
||||
hideSeries: string[],
|
||||
@ -7,13 +19,9 @@ export interface HideSeriesArgs {
|
||||
series: Series[]
|
||||
}
|
||||
|
||||
export interface DragArgs {
|
||||
e: MouseEvent | TouchEvent,
|
||||
u: uPlot,
|
||||
factor: number,
|
||||
setPanning: (enable: boolean) => void,
|
||||
setPlotScale: ({ min, max }: { min: number, max: number }) => void
|
||||
}
|
||||
export type MinMax = { min: number, max: number }
|
||||
|
||||
export type SetMinMax = ({ min, max }: MinMax) => void
|
||||
|
||||
export interface LegendItemType {
|
||||
group: number;
|
||||
@ -53,3 +61,7 @@ export interface Fill {
|
||||
}
|
||||
|
||||
export type ArrayRGB = [number, number, number]
|
||||
|
||||
export interface AxisExtend extends Axis {
|
||||
_size?: number;
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { ArrayRGB } from "./uplot/types";
|
||||
import { ArrayRGB } from "../types";
|
||||
|
||||
export const baseContrastColors = [
|
||||
"#e54040",
|
||||
|
@ -2,9 +2,10 @@ import uPlot, { Axis, Series } from "uplot";
|
||||
import { getMaxFromArray, getMinFromArray } from "../math";
|
||||
import { getSecondsFromDuration, roundToMilliseconds } from "../time";
|
||||
import { AxisRange } from "../../state/graph/reducer";
|
||||
import { formatTicks, sizeAxis } from "./helpers";
|
||||
import { formatTicks, getTextWidth } from "./helpers";
|
||||
import { TimeParams } from "../../types";
|
||||
import { getCssVariable } from "../theme";
|
||||
import { AxisExtend } from "../../types";
|
||||
|
||||
// see https://github.com/leeoniya/uPlot/tree/master/docs#axis--grid-opts
|
||||
const timeValues = [
|
||||
@ -77,3 +78,16 @@ export const getLimitsYAxis = (values: { [key: string]: number[] }, buffer: bool
|
||||
result[key] = buffer ? getMinMaxBuffer(min, max) : [min, max];
|
||||
return result;
|
||||
};
|
||||
|
||||
export const sizeAxis = (u: uPlot, values: string[], axisIdx: number, cycleNum: number): number => {
|
||||
const axis = u.axes[axisIdx] as AxisExtend;
|
||||
|
||||
if (cycleNum > 1) return axis._size || 60;
|
||||
|
||||
let axisSize = 6 + (axis?.ticks?.size || 0) + (axis.gap || 0);
|
||||
|
||||
const longestVal = (values ?? []).reduce((acc, val) => val?.length > acc.length ? val : acc, "");
|
||||
if (longestVal != "") axisSize += getTextWidth(longestVal, "10px Arial");
|
||||
|
||||
return Math.ceil(axisSize);
|
||||
};
|
||||
|
@ -1,34 +0,0 @@
|
||||
import { DragArgs } from "./types";
|
||||
|
||||
export const dragChart = ({ e, factor = 0.85, u, setPanning, setPlotScale }: DragArgs): void => {
|
||||
e.preventDefault();
|
||||
const isMouseEvent = e instanceof MouseEvent;
|
||||
|
||||
setPanning(true);
|
||||
const leftStart = isMouseEvent ? e.clientX : e.touches[0].clientX;
|
||||
const xUnitsPerPx = u.posToVal(1, "x") - u.posToVal(0, "x");
|
||||
const scXMin = u.scales.x.min || 0;
|
||||
const scXMax = u.scales.x.max || 0;
|
||||
|
||||
const mouseMove = (e: MouseEvent | TouchEvent) => {
|
||||
const isMouseEvent = e instanceof MouseEvent;
|
||||
if (!isMouseEvent && e.touches.length > 1) return;
|
||||
e.preventDefault();
|
||||
|
||||
const clientX = isMouseEvent ? e.clientX : e.touches[0].clientX;
|
||||
const dx = xUnitsPerPx * ((clientX - leftStart) * factor);
|
||||
setPlotScale({ min: scXMin - dx, max: scXMax - dx });
|
||||
};
|
||||
const mouseUp = () => {
|
||||
setPanning(false);
|
||||
document.removeEventListener("mousemove", mouseMove);
|
||||
document.removeEventListener("mouseup", mouseUp);
|
||||
document.removeEventListener("touchmove", mouseMove);
|
||||
document.removeEventListener("touchend", mouseUp);
|
||||
};
|
||||
|
||||
document.addEventListener("mousemove", mouseMove);
|
||||
document.addEventListener("mouseup", mouseUp);
|
||||
document.addEventListener("touchmove", mouseMove);
|
||||
document.addEventListener("touchend", mouseUp);
|
||||
};
|
@ -1,27 +1,5 @@
|
||||
import uPlot, { Axis } from "uplot";
|
||||
|
||||
export const defaultOptions = {
|
||||
legend: {
|
||||
show: false
|
||||
},
|
||||
cursor: {
|
||||
drag: {
|
||||
x: true,
|
||||
y: false
|
||||
},
|
||||
focus: {
|
||||
prox: 30
|
||||
},
|
||||
points: {
|
||||
size: 5.6,
|
||||
width: 1.4
|
||||
},
|
||||
bind: {
|
||||
click: (): null => null,
|
||||
dblclick: (): null => null,
|
||||
},
|
||||
},
|
||||
};
|
||||
import uPlot from "uplot";
|
||||
import { MetricResult } from "../../api/types";
|
||||
|
||||
export const formatTicks = (u: uPlot, ticks: number[], unit = ""): string[] => {
|
||||
const min = ticks[0];
|
||||
@ -32,7 +10,11 @@ export const formatTicks = (u: uPlot, ticks: number[], unit = ""): string[] => {
|
||||
return ticks.map(v => `${formatPrettyNumber(v, min, max)} ${unit}`);
|
||||
};
|
||||
|
||||
export const formatPrettyNumber = (n: number | null | undefined, min: number | null | undefined, max: number | null | undefined): string => {
|
||||
export const formatPrettyNumber = (
|
||||
n: number | null | undefined,
|
||||
min: number | null | undefined,
|
||||
max: number | null | undefined
|
||||
): string => {
|
||||
if (n === undefined || n === null) {
|
||||
return "";
|
||||
}
|
||||
@ -59,11 +41,7 @@ export const formatPrettyNumber = (n: number | null | undefined, min: number | n
|
||||
});
|
||||
};
|
||||
|
||||
interface AxisExtend extends Axis {
|
||||
_size?: number;
|
||||
}
|
||||
|
||||
const getTextWidth = (val: string, font: string): number => {
|
||||
export const getTextWidth = (val: string, font: string): number => {
|
||||
const span = document.createElement("span");
|
||||
span.innerText = val;
|
||||
span.style.cssText = `position: absolute; z-index: -1; pointer-events: none; opacity: 0; font: ${font}`;
|
||||
@ -73,17 +51,17 @@ const getTextWidth = (val: string, font: string): number => {
|
||||
return width;
|
||||
};
|
||||
|
||||
export const sizeAxis = (u: uPlot, values: string[], axisIdx: number, cycleNum: number): number => {
|
||||
const axis = u.axes[axisIdx] as AxisExtend;
|
||||
|
||||
if (cycleNum > 1) return axis._size || 60;
|
||||
|
||||
let axisSize = 6 + (axis?.ticks?.size || 0) + (axis.gap || 0);
|
||||
|
||||
const longestVal = (values ?? []).reduce((acc, val) => val?.length > acc.length ? val : acc, "");
|
||||
if (longestVal != "") axisSize += getTextWidth(longestVal, "10px Arial");
|
||||
|
||||
return Math.ceil(axisSize);
|
||||
export const getDashLine = (group: number): number[] => {
|
||||
return group <= 1 ? [] : [group*4, group*1.2];
|
||||
};
|
||||
|
||||
export const getDashLine = (group: number): number[] => group <= 1 ? [] : [group*4, group*1.2];
|
||||
export const getMetricName = (metricItem: MetricResult) => {
|
||||
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__"] || "";
|
||||
if (labels.length > 0) {
|
||||
metricName += "{" + labels.join(",") + "}";
|
||||
}
|
||||
return metricName;
|
||||
};
|
||||
|
7
app/vmui/packages/vmui/src/utils/uplot/hooks.ts
Normal file
7
app/vmui/packages/vmui/src/utils/uplot/hooks.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import uPlot from "uplot";
|
||||
|
||||
export const delHooks = (u: uPlot) => {
|
||||
Object.keys(u.hooks).forEach(hook => {
|
||||
u.hooks[hook as keyof uPlot.Hooks.Arrays] = [];
|
||||
});
|
||||
};
|
7
app/vmui/packages/vmui/src/utils/uplot/index.ts
Normal file
7
app/vmui/packages/vmui/src/utils/uplot/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export * from "./axes";
|
||||
export * from "./heatmap";
|
||||
export * from "./helpers";
|
||||
export * from "./hooks";
|
||||
export * from "./instnance";
|
||||
export * from "./scales";
|
||||
export * from "./series";
|
43
app/vmui/packages/vmui/src/utils/uplot/instnance.ts
Normal file
43
app/vmui/packages/vmui/src/utils/uplot/instnance.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import uPlot, { Options as uPlotOptions } from "uplot";
|
||||
import { delSeries } from "./series";
|
||||
import { delHooks } from "./hooks";
|
||||
import dayjs from "dayjs";
|
||||
import { dateFromSeconds, formatDateForNativeInput } from "../time";
|
||||
|
||||
interface InstanceOptions {
|
||||
width?: number,
|
||||
height?: number
|
||||
}
|
||||
|
||||
export const getDefaultOptions = ({ width = 400, height = 500 }: InstanceOptions): uPlotOptions => ({
|
||||
width,
|
||||
height,
|
||||
series: [],
|
||||
tzDate: ts => dayjs(formatDateForNativeInput(dateFromSeconds(ts))).local().toDate(),
|
||||
legend: {
|
||||
show: false
|
||||
},
|
||||
cursor: {
|
||||
drag: {
|
||||
x: true,
|
||||
y: false
|
||||
},
|
||||
focus: {
|
||||
prox: 30
|
||||
},
|
||||
points: {
|
||||
size: 5.6,
|
||||
width: 1.4
|
||||
},
|
||||
bind: {
|
||||
click: (): null => null,
|
||||
dblclick: (): null => null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const handleDestroy = (u: uPlot) => {
|
||||
delSeries(u);
|
||||
delHooks(u);
|
||||
u.setData([]);
|
||||
};
|
@ -1,22 +1,16 @@
|
||||
import uPlot, { Range, Scale, Scales } from "uplot";
|
||||
import { getMinMaxBuffer } from "./axes";
|
||||
import { YaxisState } from "../../state/graph/reducer";
|
||||
import { MinMax, SetMinMax } from "../../types";
|
||||
|
||||
interface XRangeType {
|
||||
min: number,
|
||||
max: number
|
||||
}
|
||||
|
||||
export const getRangeX = (xRange: XRangeType): Range.MinMax => {
|
||||
return [xRange.min, xRange.max];
|
||||
};
|
||||
export const getRangeX = ({ min, max }: MinMax): Range.MinMax => [min, 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 => {
|
||||
export const getScales = (yaxis: YaxisState, xRange: MinMax): 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 => {
|
||||
@ -24,3 +18,9 @@ export const getScales = (yaxis: YaxisState, xRange: XRangeType): Scales => {
|
||||
});
|
||||
return scales;
|
||||
};
|
||||
|
||||
export const setSelect = (setPlotScale: SetMinMax) => (u: uPlot) => {
|
||||
const min = u.posToVal(u.select.left, "x");
|
||||
const max = u.posToVal(u.select.left + u.select.width, "x");
|
||||
setPlotScale({ min, max });
|
||||
};
|
||||
|
@ -1,22 +1,11 @@
|
||||
import { MetricResult } from "../../api/types";
|
||||
import { Series } from "uplot";
|
||||
import uPlot, { Series as uPlotSeries } from "uplot";
|
||||
import { getNameForMetric, promValueToNumber } from "../metric";
|
||||
import { BarSeriesItem, Disp, Fill, LegendItemType, Stroke } from "./types";
|
||||
import { HideSeriesArgs } from "./types";
|
||||
import { HideSeriesArgs, BarSeriesItem, Disp, Fill, LegendItemType, Stroke, SeriesItem } from "../../types";
|
||||
import { baseContrastColors, getColorFromString } from "../color";
|
||||
import { getMedianFromArray, getMaxFromArray, getMinFromArray, getLastFromArray } from "../math";
|
||||
import { formatPrettyNumber } from "./helpers";
|
||||
|
||||
export interface SeriesItem extends Series {
|
||||
freeFormFields: {[key: string]: string};
|
||||
calculations: {
|
||||
min: string,
|
||||
max: string,
|
||||
median: string,
|
||||
last: string
|
||||
}
|
||||
}
|
||||
|
||||
export const getSeriesItemContext = (data: MetricResult[], hideSeries: string[], alias: string[]) => {
|
||||
const colorState: {[key: string]: string} = {};
|
||||
const calculations = data.map(d => {
|
||||
@ -108,3 +97,15 @@ export const barDisp = (stroke: Stroke, fill: Fill): Disp => {
|
||||
fill: fill
|
||||
};
|
||||
};
|
||||
|
||||
export const delSeries = (u: uPlot) => {
|
||||
for (let i = u.series.length - 1; i >= 0; i--) {
|
||||
u.delSeries(i);
|
||||
}
|
||||
};
|
||||
|
||||
export const addSeries = (u: uPlot, series: uPlotSeries[]) => {
|
||||
series.forEach((s) => {
|
||||
u.addSeries(s);
|
||||
});
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user