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 { seriesBarsPlugin } from "../../../utils/uplot/plugin";
|
||||||
import { barDisp, getBarSeries } from "../../../utils/uplot/series";
|
import { barDisp, getBarSeries } from "../../../utils/uplot";
|
||||||
import { Fill, Stroke } from "../../../utils/uplot/types";
|
import { Fill, Stroke } from "../../../types";
|
||||||
import { PaddingSide, Series } from "uplot";
|
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 *;
|
@use "src/styles/variables" as *;
|
||||||
|
|
||||||
$chart-tooltip-width: 325px;
|
$chart-tooltip-width: 325px;
|
||||||
$chart-tooltip-icon-width: 25px;
|
$chart-tooltip-icon-width: 25px;
|
||||||
$chart-tooltip-half-icon: calc($chart-tooltip-icon-width/2);
|
$chart-tooltip-half-icon: calc($chart-tooltip-icon-width/2);
|
||||||
@ -53,10 +54,8 @@ $chart-tooltip-y: -1 * ($padding-small + $chart-tooltip-half-icon);
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__date {
|
&__date {
|
||||||
&_range {
|
display: grid;
|
||||||
display: grid;
|
gap: 2px;
|
||||||
gap: 2px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,6 +67,13 @@ $chart-tooltip-y: -1 * ($padding-small + $chart-tooltip-half-icon);
|
|||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
line-height: $font-size;
|
line-height: $font-size;
|
||||||
|
|
||||||
|
&__stats {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: $padding-small;
|
||||||
|
}
|
||||||
|
|
||||||
&__marker {
|
&__marker {
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 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, {
|
import uPlot, {
|
||||||
AlignedData as uPlotData,
|
AlignedData as uPlotData,
|
||||||
Options as uPlotOptions,
|
Options as uPlotOptions,
|
||||||
Range
|
|
||||||
} from "uplot";
|
} 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 { MetricResult } from "../../../../api/types";
|
||||||
import { dateFromSeconds, formatDateForNativeInput, limitsDurations } from "../../../../utils/time";
|
|
||||||
import throttle from "lodash.throttle";
|
|
||||||
import { TimeParams } from "../../../../types";
|
import { TimeParams } from "../../../../types";
|
||||||
import { YaxisState } from "../../../../state/graph/reducer";
|
|
||||||
import "uplot/dist/uPlot.min.css";
|
import "uplot/dist/uPlot.min.css";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import dayjs from "dayjs";
|
|
||||||
import { useAppState } from "../../../../state/common/StateContext";
|
import { useAppState } from "../../../../state/common/StateContext";
|
||||||
import { heatmapPaths } from "../../../../utils/uplot/heatmap";
|
import {
|
||||||
import { DATE_FULL_TIMEZONE_FORMAT } from "../../../../constants/date";
|
heatmapPaths,
|
||||||
import ChartTooltipHeatmap, {
|
handleDestroy,
|
||||||
ChartTooltipHeatmapProps,
|
getDefaultOptions,
|
||||||
TooltipHeatmapProps
|
sizeAxis,
|
||||||
} from "../ChartTooltipHeatmap/ChartTooltipHeatmap";
|
getAxes,
|
||||||
|
setSelect,
|
||||||
|
} from "../../../../utils/uplot";
|
||||||
import { ElementSize } from "../../../../hooks/useElementSize";
|
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 {
|
export interface HeatmapChartProps {
|
||||||
metrics: MetricResult[];
|
metrics: MetricResult[];
|
||||||
data: uPlotData;
|
data: uPlotData;
|
||||||
period: TimeParams;
|
period: TimeParams;
|
||||||
yaxis: YaxisState;
|
|
||||||
unit?: string;
|
unit?: string;
|
||||||
setPeriod: ({ from, to }: {from: Date, to: Date}) => void;
|
setPeriod: ({ from, to }: { from: Date, to: Date }) => void;
|
||||||
layoutSize: ElementSize,
|
layoutSize: ElementSize,
|
||||||
height?: number;
|
height?: number;
|
||||||
onChangeLegend: (val: TooltipHeatmapProps) => void;
|
onChangeLegend: (val: ChartTooltipProps) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum typeChartUpdate {xRange = "xRange", yRange = "yRange"}
|
|
||||||
|
|
||||||
const HeatmapChart: FC<HeatmapChartProps> = ({
|
const HeatmapChart: FC<HeatmapChartProps> = ({
|
||||||
data,
|
data,
|
||||||
metrics = [],
|
metrics = [],
|
||||||
period,
|
period,
|
||||||
yaxis,
|
|
||||||
unit,
|
unit,
|
||||||
setPeriod,
|
setPeriod,
|
||||||
layoutSize,
|
layoutSize,
|
||||||
@ -53,144 +48,39 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
|
|||||||
const { isDarkTheme } = useAppState();
|
const { isDarkTheme } = useAppState();
|
||||||
|
|
||||||
const uPlotRef = useRef<HTMLDivElement>(null);
|
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 [uPlotInst, setUPlotInst] = useState<uPlot>();
|
||||||
const [startTouchDistance, setStartTouchDistance] = useState(0);
|
|
||||||
|
|
||||||
const [tooltipProps, setTooltipProps] = useState<TooltipHeatmapProps | null>(null);
|
const { xRange, setPlotScale } = usePlotScale({ period, setPeriod });
|
||||||
const [tooltipOffset, setTooltipOffset] = useState({ left: 0, top: 0 });
|
const { onReadyChart, isPanning } = useReadyChart(setPlotScale);
|
||||||
const [stickyTooltips, setStickyToolTips] = useState<ChartTooltipHeatmapProps[]>([]);
|
useZoomChart({ uPlotInst, xRange, setPlotScale });
|
||||||
const tooltipId = useMemo(() => {
|
const {
|
||||||
return `${tooltipProps?.bucket}_${tooltipProps?.startDate}`;
|
stickyTooltips,
|
||||||
}, [tooltipProps]);
|
handleUnStick,
|
||||||
|
getTooltipProps,
|
||||||
|
setCursor,
|
||||||
|
resetTooltips
|
||||||
|
} = useHeatmapTooltip({ u: uPlotInst, metrics, unit });
|
||||||
|
|
||||||
const setScale = ({ min, max }: { min: number, max: number }): void => {
|
const tooltipProps = useMemo(() => getTooltipProps(), [getTooltipProps]);
|
||||||
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 onReadyChart = (u: uPlot) => {
|
const getHeatmapAxes = () => {
|
||||||
const factor = 0.9;
|
const baseAxes = getAxes([{}], unit);
|
||||||
setTooltipOffset({
|
|
||||||
left: parseFloat(u.over.style.left),
|
|
||||||
top: parseFloat(u.over.style.top)
|
|
||||||
});
|
|
||||||
|
|
||||||
u.over.addEventListener("mousedown", e => {
|
return [
|
||||||
const { ctrlKey, metaKey, button } = e;
|
...baseAxes,
|
||||||
const leftClick = button === 0;
|
{
|
||||||
const leftClickWithMeta = leftClick && (ctrlKey || metaKey);
|
scale: "y",
|
||||||
if (leftClickWithMeta) {
|
stroke: baseAxes[0].stroke,
|
||||||
// drag pan
|
font: baseAxes[0].font,
|
||||||
dragChart({ u, e, setPanning, setPlotScale, factor });
|
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 = {
|
const options: uPlotOptions = {
|
||||||
...defaultOptions,
|
...getDefaultOptions({ width: layoutSize.width, height }),
|
||||||
mode: 2,
|
mode: 2,
|
||||||
tzDate: ts => dayjs(formatDateForNativeInput(dateFromSeconds(ts))).local().toDate(),
|
|
||||||
series: [
|
series: [
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
@ -210,17 +100,7 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
axes: [
|
axes: getHeatmapAxes(),
|
||||||
...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),
|
|
||||||
}
|
|
||||||
],
|
|
||||||
scales: {
|
scales: {
|
||||||
x: {
|
x: {
|
||||||
time: true,
|
time: true,
|
||||||
@ -228,87 +108,35 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
|
|||||||
y: {
|
y: {
|
||||||
log: 2,
|
log: 2,
|
||||||
time: false,
|
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: {
|
hooks: {
|
||||||
setSelect: [
|
ready: [onReadyChart],
|
||||||
(u) => {
|
setCursor: [setCursor],
|
||||||
const min = u.posToVal(u.select.left, "x");
|
setSelect: [setSelect(setPlotScale)],
|
||||||
const max = u.posToVal(u.select.left + u.select.width, "x");
|
destroy: [handleDestroy],
|
||||||
setPlotScale({ min, max });
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
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(() => {
|
useEffect(() => {
|
||||||
setStickyToolTips([]);
|
resetTooltips();
|
||||||
setTooltipProps(null);
|
|
||||||
const isValidData = data[0] === null && Array.isArray(data[1]);
|
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);
|
const u = new uPlot(options, data, uPlotRef.current);
|
||||||
setUPlotInst(u);
|
setUPlotInst(u);
|
||||||
setXRange({ min: period.start, max: period.end });
|
|
||||||
return u.destroy;
|
return u.destroy;
|
||||||
}, [uPlotRef.current, layoutSize, height, isDarkTheme, data]);
|
}, [uPlotRef, data, isDarkTheme]);
|
||||||
|
|
||||||
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]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (tooltipProps) onChangeLegend(tooltipProps);
|
if (!uPlotInst) return;
|
||||||
}, [tooltipProps]);
|
uPlotInst.setSize({ width: layoutSize.width || 400, height: height || 500 });
|
||||||
|
uPlotInst.redraw();
|
||||||
|
}, [height, layoutSize]);
|
||||||
|
|
||||||
useEventListener("click", handleClick);
|
useEffect(() => {
|
||||||
useEventListener("keydown", handleKeyDown);
|
onChangeLegend(tooltipProps);
|
||||||
useEventListener("touchmove", handleTouchMove);
|
}, [tooltipProps]);
|
||||||
useEventListener("touchstart", handleTouchStart);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -325,25 +153,13 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
|
|||||||
className="vm-line-chart__u-plot"
|
className="vm-line-chart__u-plot"
|
||||||
ref={uPlotRef}
|
ref={uPlotRef}
|
||||||
/>
|
/>
|
||||||
{uPlotInst && tooltipProps && (
|
|
||||||
<ChartTooltipHeatmap
|
|
||||||
{...tooltipProps}
|
|
||||||
unit={unit}
|
|
||||||
u={uPlotInst}
|
|
||||||
tooltipOffset={tooltipOffset}
|
|
||||||
id={tooltipId}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{uPlotInst && stickyTooltips.map(t => (
|
<ChartTooltipWrapper
|
||||||
<ChartTooltipHeatmap
|
showTooltip={!!tooltipProps.show}
|
||||||
{...t}
|
tooltipProps={tooltipProps}
|
||||||
isSticky
|
stickyTooltips={stickyTooltips}
|
||||||
u={uPlotInst}
|
handleUnStick={handleUnStick}
|
||||||
key={t.id}
|
/>
|
||||||
onClose={handleUnStick}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,39 +1,39 @@
|
|||||||
import React, { FC, useEffect, useState } from "preact/compat";
|
import React, { FC, useEffect, useMemo, useState } from "preact/compat";
|
||||||
import { gradMetal16 } from "../../../../utils/uplot/heatmap";
|
import { gradMetal16 } from "../../../../utils/uplot";
|
||||||
|
import { SeriesItem, LegendItemType } from "../../../../types";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import { TooltipHeatmapProps } from "../ChartTooltipHeatmap/ChartTooltipHeatmap";
|
|
||||||
import { SeriesItem } from "../../../../utils/uplot/series";
|
|
||||||
import LegendItem from "../../Line/Legend/LegendItem/LegendItem";
|
import LegendItem from "../../Line/Legend/LegendItem/LegendItem";
|
||||||
import { LegendItemType } from "../../../../utils/uplot/types";
|
import { ChartTooltipProps } from "../../ChartTooltip/ChartTooltip";
|
||||||
|
|
||||||
interface LegendHeatmapProps {
|
interface LegendHeatmapProps {
|
||||||
min: number
|
min: number
|
||||||
max: number
|
max: number
|
||||||
legendValue: TooltipHeatmapProps | null,
|
legendValue: ChartTooltipProps | null,
|
||||||
series: SeriesItem[]
|
series: SeriesItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const LegendHeatmap: FC<LegendHeatmapProps> = (
|
const LegendHeatmap: FC<LegendHeatmapProps> = ({
|
||||||
{
|
min,
|
||||||
min,
|
max,
|
||||||
max,
|
legendValue,
|
||||||
legendValue,
|
series
|
||||||
series,
|
}) => {
|
||||||
}
|
|
||||||
) => {
|
|
||||||
|
|
||||||
const [percent, setPercent] = useState(0);
|
const [percent, setPercent] = useState(0);
|
||||||
const [valueFormat, setValueFormat] = useState("");
|
const [valueFormat, setValueFormat] = useState("");
|
||||||
const [minFormat, setMinFormat] = useState("");
|
const [minFormat, setMinFormat] = useState("");
|
||||||
const [maxFormat, setMaxFormat] = useState("");
|
const [maxFormat, setMaxFormat] = useState("");
|
||||||
|
|
||||||
|
const value = useMemo(() => {
|
||||||
|
return parseFloat(String(legendValue?.value || 0).replace("%", ""));
|
||||||
|
}, [legendValue]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const value = legendValue?.value || 0;
|
|
||||||
setPercent(value ? (value - min) / (max - min) * 100 : 0);
|
setPercent(value ? (value - min) / (max - min) * 100 : 0);
|
||||||
setValueFormat(value ? `${value}%` : "");
|
setValueFormat(value ? `${value}%` : "");
|
||||||
setMinFormat(`${min}%`);
|
setMinFormat(`${min}%`);
|
||||||
setMaxFormat(`${max}%`);
|
setMaxFormat(`${max}%`);
|
||||||
}, [legendValue, min, max]);
|
}, [value, min, max]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="vm-legend-heatmap__wrapper">
|
<div className="vm-legend-heatmap__wrapper">
|
||||||
@ -42,7 +42,7 @@ const LegendHeatmap: FC<LegendHeatmapProps> = (
|
|||||||
className="vm-legend-heatmap-gradient"
|
className="vm-legend-heatmap-gradient"
|
||||||
style={{ background: `linear-gradient(to right, ${gradMetal16.join(", ")})` }}
|
style={{ background: `linear-gradient(to right, ${gradMetal16.join(", ")})` }}
|
||||||
>
|
>
|
||||||
{!!legendValue?.value && (
|
{!!value && (
|
||||||
<div
|
<div
|
||||||
className="vm-legend-heatmap-gradient__value"
|
className="vm-legend-heatmap-gradient__value"
|
||||||
style={{ left: `${percent}%` }}
|
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 React, { FC, useMemo } from "preact/compat";
|
||||||
import { LegendItemType } from "../../../../utils/uplot/types";
|
import { LegendItemType } from "../../../../types";
|
||||||
import LegendItem from "./LegendItem/LegendItem";
|
import LegendItem from "./LegendItem/LegendItem";
|
||||||
import Accordion from "../../../Main/Accordion/Accordion";
|
import Accordion from "../../../Main/Accordion/Accordion";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { FC, useMemo } from "preact/compat";
|
import React, { FC, useMemo } from "preact/compat";
|
||||||
import { MouseEvent } from "react";
|
import { MouseEvent } from "react";
|
||||||
import { LegendItemType } from "../../../../../utils/uplot/types";
|
import { LegendItemType } from "../../../../../types";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { getFreeFields } from "./helpers";
|
import { getFreeFields } from "./helpers";
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { LegendItemType } from "../../../../../utils/uplot/types";
|
import { LegendItemType } from "../../../../../types";
|
||||||
|
|
||||||
export const getFreeFields = (legend: LegendItemType) => {
|
export const getFreeFields = (legend: LegendItemType) => {
|
||||||
const keys = Object.keys(legend.freeFormFields).filter(f => f !== "__name__");
|
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, {
|
import uPlot, {
|
||||||
AlignedData as uPlotData,
|
AlignedData as uPlotData,
|
||||||
Options as uPlotOptions,
|
Options as uPlotOptions,
|
||||||
Series as uPlotSeries,
|
Series as uPlotSeries,
|
||||||
} from "uplot";
|
} from "uplot";
|
||||||
import { defaultOptions } from "../../../../utils/uplot/helpers";
|
import {
|
||||||
import { dragChart } from "../../../../utils/uplot/events";
|
getDefaultOptions,
|
||||||
import { getAxes } from "../../../../utils/uplot/axes";
|
addSeries,
|
||||||
|
delSeries,
|
||||||
|
getRangeX,
|
||||||
|
getRangeY,
|
||||||
|
getScales,
|
||||||
|
handleDestroy,
|
||||||
|
getAxes,
|
||||||
|
setSelect
|
||||||
|
} from "../../../../utils/uplot";
|
||||||
import { MetricResult } from "../../../../api/types";
|
import { MetricResult } from "../../../../api/types";
|
||||||
import { dateFromSeconds, formatDateForNativeInput, limitsDurations } from "../../../../utils/time";
|
|
||||||
import { TimeParams } from "../../../../types";
|
import { TimeParams } from "../../../../types";
|
||||||
import { YaxisState } from "../../../../state/graph/reducer";
|
import { YaxisState } from "../../../../state/graph/reducer";
|
||||||
import "uplot/dist/uPlot.min.css";
|
import "uplot/dist/uPlot.min.css";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import ChartTooltip, { ChartTooltipProps } from "../ChartTooltip/ChartTooltip";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
import { useAppState } from "../../../../state/common/StateContext";
|
import { useAppState } from "../../../../state/common/StateContext";
|
||||||
import { SeriesItem } from "../../../../utils/uplot/series";
|
|
||||||
import { ElementSize } from "../../../../hooks/useElementSize";
|
import { ElementSize } from "../../../../hooks/useElementSize";
|
||||||
import useEventListener from "../../../../hooks/useEventListener";
|
import useReadyChart from "../../../../hooks/uplot/useReadyChart";
|
||||||
import { getRangeX, getRangeY, getScales } from "../../../../utils/uplot/scales";
|
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 {
|
export interface LineChartProps {
|
||||||
metrics: MetricResult[];
|
metrics: MetricResult[];
|
||||||
@ -29,7 +36,7 @@ export interface LineChartProps {
|
|||||||
yaxis: YaxisState;
|
yaxis: YaxisState;
|
||||||
series: uPlotSeries[];
|
series: uPlotSeries[];
|
||||||
unit?: string;
|
unit?: string;
|
||||||
setPeriod: ({ from, to }: {from: Date, to: Date}) => void;
|
setPeriod: ({ from, to }: { from: Date, to: Date }) => void;
|
||||||
layoutSize: ElementSize;
|
layoutSize: ElementSize;
|
||||||
height?: number;
|
height?: number;
|
||||||
}
|
}
|
||||||
@ -48,208 +55,41 @@ const LineChart: FC<LineChartProps> = ({
|
|||||||
const { isDarkTheme } = useAppState();
|
const { isDarkTheme } = useAppState();
|
||||||
|
|
||||||
const uPlotRef = useRef<HTMLDivElement>(null);
|
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 [uPlotInst, setUPlotInst] = useState<uPlot>();
|
||||||
const [startTouchDistance, setStartTouchDistance] = useState(0);
|
|
||||||
|
|
||||||
const [showTooltip, setShowTooltip] = useState(false);
|
const { xRange, setPlotScale } = usePlotScale({ period, setPeriod });
|
||||||
const [tooltipIdx, setTooltipIdx] = useState({ seriesIdx: -1, dataIdx: -1 });
|
const { onReadyChart, isPanning } = useReadyChart(setPlotScale);
|
||||||
const [tooltipOffset, setTooltipOffset] = useState({ left: 0, top: 0 });
|
useZoomChart({ uPlotInst, xRange, setPlotScale });
|
||||||
const [stickyTooltips, setStickyToolTips] = useState<ChartTooltipProps[]>([]);
|
const {
|
||||||
|
showTooltip,
|
||||||
const setPlotScale = ({ min, max }: { min: number, max: number }) => {
|
stickyTooltips,
|
||||||
const delta = (max - min) * 1000;
|
handleUnStick,
|
||||||
if ((delta < limitsDurations.min) || (delta > limitsDurations.max)) return;
|
getTooltipProps,
|
||||||
setXRange({ min, max });
|
seriesFocus,
|
||||||
setPeriod({
|
setCursor,
|
||||||
from: dayjs(min * 1000).toDate(),
|
resetTooltips
|
||||||
to: dayjs(max * 1000).toDate()
|
} = useLineTooltip({ u: uPlotInst, metrics, series, unit });
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
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 options: uPlotOptions = {
|
const options: uPlotOptions = {
|
||||||
...defaultOptions,
|
...getDefaultOptions({ width: layoutSize.width, height }),
|
||||||
tzDate: ts => dayjs(formatDateForNativeInput(dateFromSeconds(ts))).local().toDate(),
|
|
||||||
series,
|
series,
|
||||||
axes: getAxes( [{}, { scale: "1" }], unit),
|
axes: getAxes([{}, { scale: "1" }], unit),
|
||||||
scales: getScales(yaxis, xRange),
|
scales: getScales(yaxis, xRange),
|
||||||
width: layoutSize.width || 400,
|
|
||||||
height: height || 500,
|
|
||||||
hooks: {
|
hooks: {
|
||||||
ready: [onReadyChart],
|
ready: [onReadyChart],
|
||||||
setSeries: [seriesFocus],
|
setSeries: [seriesFocus],
|
||||||
setCursor: [setCursor],
|
setCursor: [setCursor],
|
||||||
setSelect: [setSelect],
|
setSelect: [setSelect(setPlotScale)],
|
||||||
destroy: [handleDestroy],
|
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(() => {
|
useEffect(() => {
|
||||||
setXRange({ min: period.start, max: period.end });
|
resetTooltips();
|
||||||
}, [period]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setStickyToolTips([]);
|
|
||||||
setTooltipIdx({ seriesIdx: -1, dataIdx: -1 });
|
|
||||||
if (!uPlotRef.current) return;
|
if (!uPlotRef.current) return;
|
||||||
if (uPlotInst) uPlotInst.destroy();
|
if (uPlotInst) uPlotInst.destroy();
|
||||||
const u = new uPlot(options, data, uPlotRef.current);
|
const u = new uPlot(options, data, uPlotRef.current);
|
||||||
setUPlotInst(u);
|
setUPlotInst(u);
|
||||||
setXRange({ min: period.start, max: period.end });
|
|
||||||
return u.destroy;
|
return u.destroy;
|
||||||
}, [uPlotRef, isDarkTheme]);
|
}, [uPlotRef, isDarkTheme]);
|
||||||
|
|
||||||
@ -287,15 +127,6 @@ const LineChart: FC<LineChartProps> = ({
|
|||||||
uPlotInst.redraw();
|
uPlotInst.redraw();
|
||||||
}, [height, layoutSize]);
|
}, [height, layoutSize]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setShowTooltip(tooltipIdx.dataIdx !== -1 && tooltipIdx.seriesIdx !== -1);
|
|
||||||
}, [tooltipIdx]);
|
|
||||||
|
|
||||||
useEventListener("click", handleClick);
|
|
||||||
useEventListener("keydown", handleKeyDown);
|
|
||||||
useEventListener("touchmove", handleTouchMove);
|
|
||||||
useEventListener("touchstart", handleTouchStart);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
@ -311,22 +142,12 @@ const LineChart: FC<LineChartProps> = ({
|
|||||||
className="vm-line-chart__u-plot"
|
className="vm-line-chart__u-plot"
|
||||||
ref={uPlotRef}
|
ref={uPlotRef}
|
||||||
/>
|
/>
|
||||||
{uPlotInst && showTooltip && (
|
<ChartTooltipWrapper
|
||||||
<ChartTooltip
|
showTooltip={showTooltip}
|
||||||
{...getChartProps()}
|
tooltipProps={getTooltipProps()}
|
||||||
u={uPlotInst}
|
stickyTooltips={stickyTooltips}
|
||||||
/>
|
handleUnStick={handleUnStick}
|
||||||
)}
|
/>
|
||||||
|
|
||||||
{uPlotInst && stickyTooltips.map(t => (
|
|
||||||
<ChartTooltip
|
|
||||||
{...t}
|
|
||||||
isSticky
|
|
||||||
u={uPlotInst}
|
|
||||||
key={t.id}
|
|
||||||
onClose={handleUnStick}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -8,11 +8,12 @@ import {
|
|||||||
getHideSeries,
|
getHideSeries,
|
||||||
getLegendItem,
|
getLegendItem,
|
||||||
getSeriesItemContext,
|
getSeriesItemContext,
|
||||||
SeriesItem
|
normalizeData,
|
||||||
} from "../../../utils/uplot/series";
|
getLimitsYAxis,
|
||||||
import { getLimitsYAxis, getMinMaxBuffer, getTimeSeries } from "../../../utils/uplot/axes";
|
getMinMaxBuffer,
|
||||||
import { LegendItemType } from "../../../utils/uplot/types";
|
getTimeSeries,
|
||||||
import { TimeParams } from "../../../types";
|
} from "../../../utils/uplot";
|
||||||
|
import { TimeParams, SeriesItem, LegendItemType } from "../../../types";
|
||||||
import { AxisRange, YaxisState } from "../../../state/graph/reducer";
|
import { AxisRange, YaxisState } from "../../../state/graph/reducer";
|
||||||
import { getAvgFromArray, getMaxFromArray, getMinFromArray } from "../../../utils/math";
|
import { getAvgFromArray, getMaxFromArray, getMinFromArray } from "../../../utils/math";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
@ -20,10 +21,9 @@ import { useTimeState } from "../../../state/time/TimeStateContext";
|
|||||||
import HeatmapChart from "../../Chart/Heatmap/HeatmapChart/HeatmapChart";
|
import HeatmapChart from "../../Chart/Heatmap/HeatmapChart/HeatmapChart";
|
||||||
import "./style.scss";
|
import "./style.scss";
|
||||||
import { promValueToNumber } from "../../../utils/metric";
|
import { promValueToNumber } from "../../../utils/metric";
|
||||||
import { normalizeData } from "../../../utils/uplot/heatmap";
|
|
||||||
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
import useDeviceDetect from "../../../hooks/useDeviceDetect";
|
||||||
import { TooltipHeatmapProps } from "../../Chart/Heatmap/ChartTooltipHeatmap/ChartTooltipHeatmap";
|
|
||||||
import useElementSize from "../../../hooks/useElementSize";
|
import useElementSize from "../../../hooks/useElementSize";
|
||||||
|
import { ChartTooltipProps } from "../../Chart/ChartTooltip/ChartTooltip";
|
||||||
|
|
||||||
export interface GraphViewProps {
|
export interface GraphViewProps {
|
||||||
data?: MetricResult[];
|
data?: MetricResult[];
|
||||||
@ -35,7 +35,7 @@ export interface GraphViewProps {
|
|||||||
unit?: string;
|
unit?: string;
|
||||||
showLegend?: boolean;
|
showLegend?: boolean;
|
||||||
setYaxisLimits: (val: AxisRange) => void
|
setYaxisLimits: (val: AxisRange) => void
|
||||||
setPeriod: ({ from, to }: {from: Date, to: Date}) => void
|
setPeriod: ({ from, to }: { from: Date, to: Date }) => void
|
||||||
fullWidth?: boolean
|
fullWidth?: boolean
|
||||||
height?: number
|
height?: number
|
||||||
isHistogram?: boolean
|
isHistogram?: boolean
|
||||||
@ -48,7 +48,7 @@ const GraphView: FC<GraphViewProps> = ({
|
|||||||
query,
|
query,
|
||||||
yaxis,
|
yaxis,
|
||||||
unit,
|
unit,
|
||||||
showLegend= true,
|
showLegend = true,
|
||||||
setYaxisLimits,
|
setYaxisLimits,
|
||||||
setPeriod,
|
setPeriod,
|
||||||
alias = [],
|
alias = [],
|
||||||
@ -66,13 +66,13 @@ const GraphView: FC<GraphViewProps> = ({
|
|||||||
const [series, setSeries] = useState<uPlotSeries[]>([]);
|
const [series, setSeries] = useState<uPlotSeries[]>([]);
|
||||||
const [legend, setLegend] = useState<LegendItemType[]>([]);
|
const [legend, setLegend] = useState<LegendItemType[]>([]);
|
||||||
const [hideSeries, setHideSeries] = useState<string[]>([]);
|
const [hideSeries, setHideSeries] = useState<string[]>([]);
|
||||||
const [legendValue, setLegendValue] = useState<TooltipHeatmapProps | null>(null);
|
const [legendValue, setLegendValue] = useState<ChartTooltipProps | null>(null);
|
||||||
|
|
||||||
const getSeriesItem = useMemo(() => {
|
const getSeriesItem = useMemo(() => {
|
||||||
return getSeriesItemContext(data, hideSeries, alias);
|
return getSeriesItemContext(data, hideSeries, alias);
|
||||||
}, [data, hideSeries, alias]);
|
}, [data, hideSeries, alias]);
|
||||||
|
|
||||||
const setLimitsYaxis = (values: {[key: string]: number[]}) => {
|
const setLimitsYaxis = (values: { [key: string]: number[] }) => {
|
||||||
const limits = getLimitsYAxis(values, !isHistogram);
|
const limits = getLimitsYAxis(values, !isHistogram);
|
||||||
setYaxisLimits(limits);
|
setYaxisLimits(limits);
|
||||||
};
|
};
|
||||||
@ -105,7 +105,7 @@ const GraphView: FC<GraphViewProps> = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const tempTimes: number[] = [];
|
const tempTimes: number[] = [];
|
||||||
const tempValues: {[key: string]: number[]} = {};
|
const tempValues: { [key: string]: number[] } = {};
|
||||||
const tempLegend: LegendItemType[] = [];
|
const tempLegend: LegendItemType[] = [];
|
||||||
const tempSeries: uPlotSeries[] = [{}];
|
const tempSeries: uPlotSeries[] = [{}];
|
||||||
|
|
||||||
@ -199,7 +199,6 @@ const GraphView: FC<GraphViewProps> = ({
|
|||||||
data={dataChart}
|
data={dataChart}
|
||||||
metrics={data}
|
metrics={data}
|
||||||
period={period}
|
period={period}
|
||||||
yaxis={yaxis}
|
|
||||||
unit={unit}
|
unit={unit}
|
||||||
setPeriod={setPeriod}
|
setPeriod={setPeriod}
|
||||||
layoutSize={containerSize}
|
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]);
|
setFetchQueue([...fetchQueue, controller]);
|
||||||
try {
|
try {
|
||||||
const isDisplayChart = displayType === "chart";
|
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 tempData: MetricBase[] = [];
|
||||||
const tempTraces: Trace[] = [];
|
const tempTraces: Trace[] = [];
|
||||||
let counter = 1;
|
let counter = 1;
|
||||||
let totalLength = 0;
|
let totalLength = 0;
|
||||||
|
let isHistogramResult = false;
|
||||||
|
|
||||||
for await (const url of fetchUrl) {
|
for await (const url of fetchUrl) {
|
||||||
|
|
||||||
@ -115,9 +117,8 @@ export const useFetchQuery = ({
|
|||||||
tempTraces.push(trace);
|
tempTraces.push(trace);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isHistogramResult = isDisplayChart && isHistogramData(resp.data.result);
|
isHistogramResult = isDisplayChart && isHistogramData(resp.data.result);
|
||||||
if (resp.data.result.length) setIsHistogram(isHistogramResult);
|
seriesLimit = isHistogramResult ? Infinity : Math.max(totalLength, defaultLimit);
|
||||||
if (isHistogramResult) seriesLimit = Infinity;
|
|
||||||
const freeTempSize = seriesLimit - tempData.length;
|
const freeTempSize = seriesLimit - tempData.length;
|
||||||
resp.data.result.slice(0, freeTempSize).forEach((d: MetricBase) => {
|
resp.data.result.slice(0, freeTempSize).forEach((d: MetricBase) => {
|
||||||
d.group = counter;
|
d.group = counter;
|
||||||
@ -136,6 +137,7 @@ export const useFetchQuery = ({
|
|||||||
setWarning(totalLength > seriesLimit ? limitText : "");
|
setWarning(totalLength > seriesLimit ? limitText : "");
|
||||||
isDisplayChart ? setGraphData(tempData as MetricResult[]) : setLiveData(tempData as InstantMetricResult[]);
|
isDisplayChart ? setGraphData(tempData as MetricResult[]) : setLiveData(tempData as InstantMetricResult[]);
|
||||||
setTraces(tempTraces);
|
setTraces(tempTraces);
|
||||||
|
setIsHistogram(totalLength ? isHistogramResult: false);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error && e.name !== "AbortError") {
|
if (e instanceof Error && e.name !== "AbortError") {
|
||||||
setError(`${e.name}: ${e.message}`);
|
setError(`${e.name}: ${e.message}`);
|
||||||
|
@ -6,7 +6,7 @@ import { useQueryState } from "../../../state/query/QueryStateContext";
|
|||||||
import { displayTypeTabs } from "../DisplayTypeSwitch";
|
import { displayTypeTabs } from "../DisplayTypeSwitch";
|
||||||
import { compactObject } from "../../../utils/object";
|
import { compactObject } from "../../../utils/object";
|
||||||
import { useGraphState } from "../../../state/graph/GraphStateContext";
|
import { useGraphState } from "../../../state/graph/GraphStateContext";
|
||||||
import useSearchParamsFromObject from "../../../hooks/useSearchParamsFromObject";
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
export const useSetQueryParams = () => {
|
export const useSetQueryParams = () => {
|
||||||
const { tenantId } = useAppState();
|
const { tenantId } = useAppState();
|
||||||
@ -14,7 +14,7 @@ export const useSetQueryParams = () => {
|
|||||||
const { query } = useQueryState();
|
const { query } = useQueryState();
|
||||||
const { duration, relativeTime, period: { date, step } } = useTimeState();
|
const { duration, relativeTime, period: { date, step } } = useTimeState();
|
||||||
const { customStep } = useGraphState();
|
const { customStep } = useGraphState();
|
||||||
const { setSearchParamsFromKeys } = useSearchParamsFromObject();
|
const [, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
const setSearchParamsFromState = () => {
|
const setSearchParamsFromState = () => {
|
||||||
const params: Record<string, unknown> = {};
|
const params: Record<string, unknown> = {};
|
||||||
@ -30,7 +30,7 @@ export const useSetQueryParams = () => {
|
|||||||
if ((step !== customStep) && customStep) params[`${group}.step_input`] = customStep;
|
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]);
|
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 { useFetchTopQueries } from "./hooks/useFetchTopQueries";
|
||||||
import Spinner from "../../components/Main/Spinner/Spinner";
|
import Spinner from "../../components/Main/Spinner/Spinner";
|
||||||
import TopQueryPanel from "./TopQueryPanel/TopQueryPanel";
|
import TopQueryPanel from "./TopQueryPanel/TopQueryPanel";
|
||||||
import { formatPrettyNumber } from "../../utils/uplot/helpers";
|
import { formatPrettyNumber } from "../../utils/uplot";
|
||||||
import { isSupportedDuration } from "../../utils/time";
|
import { isSupportedDuration } from "../../utils/time";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { TopQueryStats } from "../../types";
|
import { TopQueryStats } from "../../types";
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { MetricBase } from "../api/types";
|
import { MetricBase } from "../api/types";
|
||||||
|
export * from "./uplot";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
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 {
|
export interface HideSeriesArgs {
|
||||||
hideSeries: string[],
|
hideSeries: string[],
|
||||||
@ -7,13 +19,9 @@ export interface HideSeriesArgs {
|
|||||||
series: Series[]
|
series: Series[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DragArgs {
|
export type MinMax = { min: number, max: number }
|
||||||
e: MouseEvent | TouchEvent,
|
|
||||||
u: uPlot,
|
export type SetMinMax = ({ min, max }: MinMax) => void
|
||||||
factor: number,
|
|
||||||
setPanning: (enable: boolean) => void,
|
|
||||||
setPlotScale: ({ min, max }: { min: number, max: number }) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LegendItemType {
|
export interface LegendItemType {
|
||||||
group: number;
|
group: number;
|
||||||
@ -53,3 +61,7 @@ export interface Fill {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type ArrayRGB = [number, number, number]
|
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 = [
|
export const baseContrastColors = [
|
||||||
"#e54040",
|
"#e54040",
|
||||||
|
@ -2,9 +2,10 @@ import uPlot, { Axis, Series } from "uplot";
|
|||||||
import { getMaxFromArray, getMinFromArray } from "../math";
|
import { getMaxFromArray, getMinFromArray } from "../math";
|
||||||
import { getSecondsFromDuration, roundToMilliseconds } from "../time";
|
import { getSecondsFromDuration, roundToMilliseconds } from "../time";
|
||||||
import { AxisRange } from "../../state/graph/reducer";
|
import { AxisRange } from "../../state/graph/reducer";
|
||||||
import { formatTicks, sizeAxis } from "./helpers";
|
import { formatTicks, getTextWidth } from "./helpers";
|
||||||
import { TimeParams } from "../../types";
|
import { TimeParams } from "../../types";
|
||||||
import { getCssVariable } from "../theme";
|
import { getCssVariable } from "../theme";
|
||||||
|
import { AxisExtend } from "../../types";
|
||||||
|
|
||||||
// see https://github.com/leeoniya/uPlot/tree/master/docs#axis--grid-opts
|
// see https://github.com/leeoniya/uPlot/tree/master/docs#axis--grid-opts
|
||||||
const timeValues = [
|
const timeValues = [
|
||||||
@ -77,3 +78,16 @@ export const getLimitsYAxis = (values: { [key: string]: number[] }, buffer: bool
|
|||||||
result[key] = buffer ? getMinMaxBuffer(min, max) : [min, max];
|
result[key] = buffer ? getMinMaxBuffer(min, max) : [min, max];
|
||||||
return result;
|
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";
|
import uPlot from "uplot";
|
||||||
|
import { MetricResult } from "../../api/types";
|
||||||
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const formatTicks = (u: uPlot, ticks: number[], unit = ""): string[] => {
|
export const formatTicks = (u: uPlot, ticks: number[], unit = ""): string[] => {
|
||||||
const min = ticks[0];
|
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}`);
|
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) {
|
if (n === undefined || n === null) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@ -59,11 +41,7 @@ export const formatPrettyNumber = (n: number | null | undefined, min: number | n
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
interface AxisExtend extends Axis {
|
export const getTextWidth = (val: string, font: string): number => {
|
||||||
_size?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getTextWidth = (val: string, font: string): number => {
|
|
||||||
const span = document.createElement("span");
|
const span = document.createElement("span");
|
||||||
span.innerText = val;
|
span.innerText = val;
|
||||||
span.style.cssText = `position: absolute; z-index: -1; pointer-events: none; opacity: 0; font: ${font}`;
|
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;
|
return width;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sizeAxis = (u: uPlot, values: string[], axisIdx: number, cycleNum: number): number => {
|
export const getDashLine = (group: number): number[] => {
|
||||||
const axis = u.axes[axisIdx] as AxisExtend;
|
return group <= 1 ? [] : [group*4, group*1.2];
|
||||||
|
|
||||||
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[] => 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 uPlot, { Range, Scale, Scales } from "uplot";
|
||||||
import { getMinMaxBuffer } from "./axes";
|
import { getMinMaxBuffer } from "./axes";
|
||||||
import { YaxisState } from "../../state/graph/reducer";
|
import { YaxisState } from "../../state/graph/reducer";
|
||||||
|
import { MinMax, SetMinMax } from "../../types";
|
||||||
|
|
||||||
interface XRangeType {
|
export const getRangeX = ({ min, max }: MinMax): Range.MinMax => [min, max];
|
||||||
min: number,
|
|
||||||
max: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getRangeX = (xRange: XRangeType): Range.MinMax => {
|
|
||||||
return [xRange.min, xRange.max];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getRangeY = (u: uPlot, min = 0, max = 1, axis: string, yaxis: YaxisState): Range.MinMax => {
|
export const getRangeY = (u: uPlot, min = 0, max = 1, axis: string, yaxis: YaxisState): Range.MinMax => {
|
||||||
if (yaxis.limits.enable) return yaxis.limits.range[axis];
|
if (yaxis.limits.enable) return yaxis.limits.range[axis];
|
||||||
return getMinMaxBuffer(min, max);
|
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 scales: { [key: string]: { range: Scale.Range } } = { x: { range: () => getRangeX(xRange) } };
|
||||||
const ranges = Object.keys(yaxis.limits.range);
|
const ranges = Object.keys(yaxis.limits.range);
|
||||||
(ranges.length ? ranges : ["1"]).forEach(axis => {
|
(ranges.length ? ranges : ["1"]).forEach(axis => {
|
||||||
@ -24,3 +18,9 @@ export const getScales = (yaxis: YaxisState, xRange: XRangeType): Scales => {
|
|||||||
});
|
});
|
||||||
return 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 { MetricResult } from "../../api/types";
|
||||||
import { Series } from "uplot";
|
import uPlot, { Series as uPlotSeries } from "uplot";
|
||||||
import { getNameForMetric, promValueToNumber } from "../metric";
|
import { getNameForMetric, promValueToNumber } from "../metric";
|
||||||
import { BarSeriesItem, Disp, Fill, LegendItemType, Stroke } from "./types";
|
import { HideSeriesArgs, BarSeriesItem, Disp, Fill, LegendItemType, Stroke, SeriesItem } from "../../types";
|
||||||
import { HideSeriesArgs } from "./types";
|
|
||||||
import { baseContrastColors, getColorFromString } from "../color";
|
import { baseContrastColors, getColorFromString } from "../color";
|
||||||
import { getMedianFromArray, getMaxFromArray, getMinFromArray, getLastFromArray } from "../math";
|
import { getMedianFromArray, getMaxFromArray, getMinFromArray, getLastFromArray } from "../math";
|
||||||
import { formatPrettyNumber } from "./helpers";
|
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[]) => {
|
export const getSeriesItemContext = (data: MetricResult[], hideSeries: string[], alias: string[]) => {
|
||||||
const colorState: {[key: string]: string} = {};
|
const colorState: {[key: string]: string} = {};
|
||||||
const calculations = data.map(d => {
|
const calculations = data.map(d => {
|
||||||
@ -108,3 +97,15 @@ export const barDisp = (stroke: Stroke, fill: Fill): Disp => {
|
|||||||
fill: fill
|
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