vmui: chart refactoring to enhance code structure (#4830)

(cherry picked from commit 8287749c05)
This commit is contained in:
Yury Molodov 2023-08-21 15:35:47 +02:00 committed by hagen1778
parent f48962e834
commit 931c63f602
No known key found for this signature in database
GPG Key ID: 3BF75F3741CA9640
33 changed files with 899 additions and 943 deletions

View File

@ -1,6 +1,6 @@
import { seriesBarsPlugin } from "../../../utils/uplot/plugin";
import { barDisp, getBarSeries } from "../../../utils/uplot/series";
import { Fill, Stroke } from "../../../utils/uplot/types";
import { barDisp, getBarSeries } from "../../../utils/uplot";
import { Fill, Stroke } from "../../../types";
import { PaddingSide, Series } from "uplot";

View File

@ -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;

View File

@ -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;

View File

@ -1,4 +1,5 @@
@use "src/styles/variables" as *;
$chart-tooltip-width: 325px;
$chart-tooltip-icon-width: 25px;
$chart-tooltip-half-icon: calc($chart-tooltip-icon-width/2);
@ -53,12 +54,10 @@ $chart-tooltip-y: -1 * ($padding-small + $chart-tooltip-half-icon);
}
&__date {
&_range {
display: grid;
gap: 2px;
}
}
}
&-data {
display: grid;
@ -68,6 +67,13 @@ $chart-tooltip-y: -1 * ($padding-small + $chart-tooltip-half-icon);
word-break: break-all;
line-height: $font-size;
&__stats {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: $padding-small;
}
&__marker {
width: 12px;
height: 12px;

View File

@ -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;

View File

@ -1,49 +1,44 @@
import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from "preact/compat";
import React, { FC, useEffect, useMemo, useRef, useState } from "preact/compat";
import uPlot, {
AlignedData as uPlotData,
Options as uPlotOptions,
Range
} from "uplot";
import { defaultOptions, sizeAxis } from "../../../../utils/uplot/helpers";
import { dragChart } from "../../../../utils/uplot/events";
import { getAxes } from "../../../../utils/uplot/axes";
import { MetricResult } from "../../../../api/types";
import { dateFromSeconds, formatDateForNativeInput, limitsDurations } from "../../../../utils/time";
import throttle from "lodash.throttle";
import { TimeParams } from "../../../../types";
import { YaxisState } from "../../../../state/graph/reducer";
import "uplot/dist/uPlot.min.css";
import classNames from "classnames";
import dayjs from "dayjs";
import { useAppState } from "../../../../state/common/StateContext";
import { heatmapPaths } from "../../../../utils/uplot/heatmap";
import { DATE_FULL_TIMEZONE_FORMAT } from "../../../../constants/date";
import ChartTooltipHeatmap, {
ChartTooltipHeatmapProps,
TooltipHeatmapProps
} from "../ChartTooltipHeatmap/ChartTooltipHeatmap";
import {
heatmapPaths,
handleDestroy,
getDefaultOptions,
sizeAxis,
getAxes,
setSelect,
} from "../../../../utils/uplot";
import { ElementSize } from "../../../../hooks/useElementSize";
import useEventListener from "../../../../hooks/useEventListener";
import useReadyChart from "../../../../hooks/uplot/useReadyChart";
import useZoomChart from "../../../../hooks/uplot/useZoomChart";
import usePlotScale from "../../../../hooks/uplot/usePlotScale";
import useHeatmapTooltip from "../../../../hooks/uplot/useHeatmapTooltip";
import ChartTooltipWrapper from "../../ChartTooltip";
import { ChartTooltipProps } from "../../ChartTooltip/ChartTooltip";
export interface HeatmapChartProps {
metrics: MetricResult[];
data: uPlotData;
period: TimeParams;
yaxis: YaxisState;
unit?: string;
setPeriod: ({ from, to }: {from: Date, to: Date}) => void;
setPeriod: ({ from, to }: { from: Date, to: Date }) => void;
layoutSize: ElementSize,
height?: number;
onChangeLegend: (val: TooltipHeatmapProps) => void;
onChangeLegend: (val: ChartTooltipProps) => void;
}
enum typeChartUpdate {xRange = "xRange", yRange = "yRange"}
const HeatmapChart: FC<HeatmapChartProps> = ({
data,
metrics = [],
period,
yaxis,
unit,
setPeriod,
layoutSize,
@ -53,144 +48,39 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
const { isDarkTheme } = useAppState();
const uPlotRef = useRef<HTMLDivElement>(null);
const [isPanning, setPanning] = useState(false);
const [xRange, setXRange] = useState({ min: period.start, max: period.end });
const [uPlotInst, setUPlotInst] = useState<uPlot>();
const [startTouchDistance, setStartTouchDistance] = useState(0);
const [tooltipProps, setTooltipProps] = useState<TooltipHeatmapProps | null>(null);
const [tooltipOffset, setTooltipOffset] = useState({ left: 0, top: 0 });
const [stickyTooltips, setStickyToolTips] = useState<ChartTooltipHeatmapProps[]>([]);
const tooltipId = useMemo(() => {
return `${tooltipProps?.bucket}_${tooltipProps?.startDate}`;
}, [tooltipProps]);
const { xRange, setPlotScale } = usePlotScale({ period, setPeriod });
const { onReadyChart, isPanning } = useReadyChart(setPlotScale);
useZoomChart({ uPlotInst, xRange, setPlotScale });
const {
stickyTooltips,
handleUnStick,
getTooltipProps,
setCursor,
resetTooltips
} = useHeatmapTooltip({ u: uPlotInst, metrics, unit });
const setScale = ({ min, max }: { min: number, max: number }): void => {
if (isNaN(min) || isNaN(max)) return;
setPeriod({
from: dayjs(min * 1000).toDate(),
to: dayjs(max * 1000).toDate()
});
};
const throttledSetScale = useCallback(throttle(setScale, 500), []);
const setPlotScale = ({ min, max }: { min: number, max: number }) => {
const delta = (max - min) * 1000;
if ((delta < limitsDurations.min) || (delta > limitsDurations.max)) return;
setXRange({ min, max });
throttledSetScale({ min, max });
};
const tooltipProps = useMemo(() => getTooltipProps(), [getTooltipProps]);
const onReadyChart = (u: uPlot) => {
const factor = 0.9;
setTooltipOffset({
left: parseFloat(u.over.style.left),
top: parseFloat(u.over.style.top)
});
const getHeatmapAxes = () => {
const baseAxes = getAxes([{}], unit);
u.over.addEventListener("mousedown", e => {
const { ctrlKey, metaKey, button } = e;
const leftClick = button === 0;
const leftClickWithMeta = leftClick && (ctrlKey || metaKey);
if (leftClickWithMeta) {
// drag pan
dragChart({ u, e, setPanning, setPlotScale, factor });
return [
...baseAxes,
{
scale: "y",
stroke: baseAxes[0].stroke,
font: baseAxes[0].font,
size: sizeAxis,
splits: metrics.map((m, i) => i),
values: metrics.map(m => m.metric.vmrange),
}
});
u.over.addEventListener("touchstart", e => {
dragChart({ u, e, setPanning, setPlotScale, factor });
});
u.over.addEventListener("wheel", e => {
if (!e.ctrlKey && !e.metaKey) return;
e.preventDefault();
const { width } = u.over.getBoundingClientRect();
const zoomPos = u.cursor.left && u.cursor.left > 0 ? u.cursor.left : 0;
const xVal = u.posToVal(zoomPos, "x");
const oxRange = (u.scales.x.max || 0) - (u.scales.x.min || 0);
const nxRange = e.deltaY < 0 ? oxRange * factor : oxRange / factor;
const min = xVal - (zoomPos / width) * nxRange;
const max = min + nxRange;
u.batch(() => setPlotScale({ min, max }));
});
];
};
const handleKeyDown = useCallback((e: KeyboardEvent) => {
const { target, ctrlKey, metaKey, key } = e;
const isInput = target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement;
if (!uPlotInst || isInput) return;
const minus = key === "-";
const plus = key === "+" || key === "=";
if ((minus || plus) && !(ctrlKey || metaKey)) {
e.preventDefault();
const factor = (xRange.max - xRange.min) / 10 * (plus ? 1 : -1);
setPlotScale({
min: xRange.min + factor,
max: xRange.max - factor
});
}
}, [uPlotInst, xRange]);
const handleClick = useCallback(() => {
if (!tooltipProps?.value) return;
const id = `${tooltipProps?.bucket}_${tooltipProps?.startDate}`;
const props = {
id,
unit,
tooltipOffset,
...tooltipProps
};
if (!stickyTooltips.find(t => t.id === id)) {
const res = JSON.parse(JSON.stringify(props));
setStickyToolTips(prev => [...prev, res]);
}
}, [stickyTooltips, tooltipProps, tooltipOffset, unit]);
const handleUnStick = (id: string) => {
setStickyToolTips(prev => prev.filter(t => t.id !== id));
};
const setCursor = (u: uPlot) => {
const left = u.cursor.left && u.cursor.left > 0 ? u.cursor.left : 0;
const top = u.cursor.top && u.cursor.top > 0 ? u.cursor.top : 0;
const xArr = (u.data[1][0] || []) as number[];
if (!Array.isArray(xArr)) return;
const xVal = u.posToVal(left, "x");
const yVal = u.posToVal(top, "y");
const xIdx = xArr.findIndex((t, i) => xVal >= t && xVal < xArr[i + 1]) || -1;
const second = xArr[xIdx + 1];
const result = metrics[Math.round(yVal)];
if (!result) {
setTooltipProps(null);
return;
}
const [endTime = 0, value = ""] = result.values.find(v => v[0] === second) || [];
const valueFormat = `${+value}%`;
const startTime = xArr[xIdx];
const startDate = dayjs(startTime * 1000).tz().format(DATE_FULL_TIMEZONE_FORMAT);
const endDate = dayjs(endTime * 1000).tz().format(DATE_FULL_TIMEZONE_FORMAT);
setTooltipProps({
cursor: { left, top },
startDate,
endDate,
bucket: result?.metric?.vmrange || "",
value: +value,
valueFormat: valueFormat,
});
};
const getRangeX = (): Range.MinMax => [xRange.min, xRange.max];
const axes = getAxes( [{}], unit);
const options: uPlotOptions = {
...defaultOptions,
...getDefaultOptions({ width: layoutSize.width, height }),
mode: 2,
tzDate: ts => dayjs(formatDateForNativeInput(dateFromSeconds(ts))).local().toDate(),
series: [
{},
{
@ -210,17 +100,7 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
],
},
],
axes: [
...axes,
{
scale: "y",
stroke: axes[0].stroke,
font: axes[0].font,
size: sizeAxis,
splits: metrics.map((m, i) => i),
values: metrics.map(m => m.metric.vmrange),
}
],
axes: getHeatmapAxes(),
scales: {
x: {
time: true,
@ -228,87 +108,35 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
y: {
log: 2,
time: false,
range: (self, initMin, initMax) => [initMin - 1, initMax + 1]
range: (u, initMin, initMax) => [initMin - 1, initMax + 1]
}
},
width: layoutSize.width || 400,
height: height || 500,
plugins: [{ hooks: { ready: onReadyChart, setCursor } }],
hooks: {
setSelect: [
(u) => {
const min = u.posToVal(u.select.left, "x");
const max = u.posToVal(u.select.left + u.select.width, "x");
setPlotScale({ min, max });
}
]
ready: [onReadyChart],
setCursor: [setCursor],
setSelect: [setSelect(setPlotScale)],
destroy: [handleDestroy],
},
};
const updateChart = (type: typeChartUpdate): void => {
if (!uPlotInst) return;
switch (type) {
case typeChartUpdate.xRange:
uPlotInst.scales.x.range = getRangeX;
break;
}
if (!isPanning) uPlotInst.redraw();
};
useEffect(() => setXRange({ min: period.start, max: period.end }), [period]);
useEffect(() => {
setStickyToolTips([]);
setTooltipProps(null);
resetTooltips();
const isValidData = data[0] === null && Array.isArray(data[1]);
if (!uPlotRef.current || !layoutSize.width || !layoutSize.height || !isValidData) return;
if (!uPlotRef.current || !isValidData) return;
const u = new uPlot(options, data, uPlotRef.current);
setUPlotInst(u);
setXRange({ min: period.start, max: period.end });
return u.destroy;
}, [uPlotRef.current, layoutSize, height, isDarkTheme, data]);
const handleTouchStart = (e: TouchEvent) => {
if (e.touches.length !== 2) return;
e.preventDefault();
const dx = e.touches[0].clientX - e.touches[1].clientX;
const dy = e.touches[0].clientY - e.touches[1].clientY;
setStartTouchDistance(Math.sqrt(dx * dx + dy * dy));
};
const handleTouchMove = useCallback((e: TouchEvent) => {
if (e.touches.length !== 2 || !uPlotInst) return;
e.preventDefault();
const dx = e.touches[0].clientX - e.touches[1].clientX;
const dy = e.touches[0].clientY - e.touches[1].clientY;
const endTouchDistance = Math.sqrt(dx * dx + dy * dy);
const diffDistance = startTouchDistance - endTouchDistance;
const max = (uPlotInst.scales.x.max || xRange.max);
const min = (uPlotInst.scales.x.min || xRange.min);
const dur = max - min;
const dir = (diffDistance > 0 ? -1 : 1);
const zoomFactor = dur / 50 * dir;
uPlotInst.batch(() => setPlotScale({
min: min + zoomFactor,
max: max - zoomFactor
}));
}, [uPlotInst, startTouchDistance, xRange]);
useEffect(() => updateChart(typeChartUpdate.xRange), [xRange]);
useEffect(() => updateChart(typeChartUpdate.yRange), [yaxis]);
}, [uPlotRef, data, isDarkTheme]);
useEffect(() => {
if (tooltipProps) onChangeLegend(tooltipProps);
}, [tooltipProps]);
if (!uPlotInst) return;
uPlotInst.setSize({ width: layoutSize.width || 400, height: height || 500 });
uPlotInst.redraw();
}, [height, layoutSize]);
useEventListener("click", handleClick);
useEventListener("keydown", handleKeyDown);
useEventListener("touchmove", handleTouchMove);
useEventListener("touchstart", handleTouchStart);
useEffect(() => {
onChangeLegend(tooltipProps);
}, [tooltipProps]);
return (
<div
@ -325,25 +153,13 @@ const HeatmapChart: FC<HeatmapChartProps> = ({
className="vm-line-chart__u-plot"
ref={uPlotRef}
/>
{uPlotInst && tooltipProps && (
<ChartTooltipHeatmap
{...tooltipProps}
unit={unit}
u={uPlotInst}
tooltipOffset={tooltipOffset}
id={tooltipId}
/>
)}
{uPlotInst && stickyTooltips.map(t => (
<ChartTooltipHeatmap
{...t}
isSticky
u={uPlotInst}
key={t.id}
onClose={handleUnStick}
<ChartTooltipWrapper
showTooltip={!!tooltipProps.show}
tooltipProps={tooltipProps}
stickyTooltips={stickyTooltips}
handleUnStick={handleUnStick}
/>
))}
</div>
);
};

View File

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

View File

@ -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;

View File

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

View File

@ -1,6 +1,6 @@
import React, { FC, useMemo } from "preact/compat";
import { MouseEvent } from "react";
import { LegendItemType } from "../../../../../utils/uplot/types";
import { LegendItemType } from "../../../../../types";
import "./style.scss";
import classNames from "classnames";
import { getFreeFields } from "./helpers";

View File

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

View File

@ -1,26 +1,33 @@
import React, { FC, useCallback, useEffect, useRef, useState } from "preact/compat";
import React, { FC, useEffect, useRef, useState } from "preact/compat";
import uPlot, {
AlignedData as uPlotData,
Options as uPlotOptions,
Series as uPlotSeries,
} from "uplot";
import { defaultOptions } from "../../../../utils/uplot/helpers";
import { dragChart } from "../../../../utils/uplot/events";
import { getAxes } from "../../../../utils/uplot/axes";
import {
getDefaultOptions,
addSeries,
delSeries,
getRangeX,
getRangeY,
getScales,
handleDestroy,
getAxes,
setSelect
} from "../../../../utils/uplot";
import { MetricResult } from "../../../../api/types";
import { dateFromSeconds, formatDateForNativeInput, limitsDurations } from "../../../../utils/time";
import { TimeParams } from "../../../../types";
import { YaxisState } from "../../../../state/graph/reducer";
import "uplot/dist/uPlot.min.css";
import "./style.scss";
import classNames from "classnames";
import ChartTooltip, { ChartTooltipProps } from "../ChartTooltip/ChartTooltip";
import dayjs from "dayjs";
import { useAppState } from "../../../../state/common/StateContext";
import { SeriesItem } from "../../../../utils/uplot/series";
import { ElementSize } from "../../../../hooks/useElementSize";
import useEventListener from "../../../../hooks/useEventListener";
import { getRangeX, getRangeY, getScales } from "../../../../utils/uplot/scales";
import useReadyChart from "../../../../hooks/uplot/useReadyChart";
import useZoomChart from "../../../../hooks/uplot/useZoomChart";
import usePlotScale from "../../../../hooks/uplot/usePlotScale";
import useLineTooltip from "../../../../hooks/uplot/useLineTooltip";
import ChartTooltipWrapper from "../../ChartTooltip";
export interface LineChartProps {
metrics: MetricResult[];
@ -29,7 +36,7 @@ export interface LineChartProps {
yaxis: YaxisState;
series: uPlotSeries[];
unit?: string;
setPeriod: ({ from, to }: {from: Date, to: Date}) => void;
setPeriod: ({ from, to }: { from: Date, to: Date }) => void;
layoutSize: ElementSize;
height?: number;
}
@ -48,208 +55,41 @@ const LineChart: FC<LineChartProps> = ({
const { isDarkTheme } = useAppState();
const uPlotRef = useRef<HTMLDivElement>(null);
const [isPanning, setPanning] = useState(false);
const [xRange, setXRange] = useState({ min: period.start, max: period.end });
const [uPlotInst, setUPlotInst] = useState<uPlot>();
const [startTouchDistance, setStartTouchDistance] = useState(0);
const [showTooltip, setShowTooltip] = useState(false);
const [tooltipIdx, setTooltipIdx] = useState({ seriesIdx: -1, dataIdx: -1 });
const [tooltipOffset, setTooltipOffset] = useState({ left: 0, top: 0 });
const [stickyTooltips, setStickyToolTips] = useState<ChartTooltipProps[]>([]);
const setPlotScale = ({ min, max }: { min: number, max: number }) => {
const delta = (max - min) * 1000;
if ((delta < limitsDurations.min) || (delta > limitsDurations.max)) return;
setXRange({ min, max });
setPeriod({
from: dayjs(min * 1000).toDate(),
to: dayjs(max * 1000).toDate()
});
};
const onReadyChart = (u: uPlot): void => {
const factor = 0.9;
setTooltipOffset({
left: parseFloat(u.over.style.left),
top: parseFloat(u.over.style.top)
});
u.over.addEventListener("mousedown", e => {
const { ctrlKey, metaKey, button } = e;
const leftClick = button === 0;
const leftClickWithMeta = leftClick && (ctrlKey || metaKey);
if (leftClickWithMeta) {
// drag pan
dragChart({ u, e, setPanning, setPlotScale, factor });
}
});
u.over.addEventListener("touchstart", e => {
dragChart({ u, e, setPanning, setPlotScale, factor });
});
u.over.addEventListener("wheel", e => {
if (!e.ctrlKey && !e.metaKey) return;
e.preventDefault();
const { width } = u.over.getBoundingClientRect();
const zoomPos = u.cursor.left && u.cursor.left > 0 ? u.cursor.left : 0;
const xVal = u.posToVal(zoomPos, "x");
const oxRange = (u.scales.x.max || 0) - (u.scales.x.min || 0);
const nxRange = e.deltaY < 0 ? oxRange * factor : oxRange / factor;
const min = xVal - (zoomPos / width) * nxRange;
const max = min + nxRange;
u.batch(() => setPlotScale({ min, max }));
});
};
const handleKeyDown = useCallback((e: KeyboardEvent) => {
const { target, ctrlKey, metaKey, key } = e;
const isInput = target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement;
if (!uPlotInst || isInput) return;
const minus = key === "-";
const plus = key === "+" || key === "=";
if ((minus || plus) && !(ctrlKey || metaKey)) {
e.preventDefault();
const factor = (xRange.max - xRange.min) / 10 * (plus ? 1 : -1);
setPlotScale({
min: xRange.min + factor,
max: xRange.max - factor
});
}
}, [uPlotInst, xRange]);
const getChartProps = useCallback(() => {
const { seriesIdx, dataIdx } = tooltipIdx;
const id = `${seriesIdx}_${dataIdx}`;
const metricItem = metrics[seriesIdx-1];
const seriesItem = series[seriesIdx] as SeriesItem;
const groups = new Set(metrics.map(m => m.group));
const showQueryNum = groups.size > 1;
return {
id,
unit,
seriesItem,
metricItem,
tooltipIdx,
tooltipOffset,
showQueryNum,
};
}, [uPlotInst, metrics, series, tooltipIdx, tooltipOffset, unit]);
const handleClick = useCallback(() => {
if (!showTooltip) return;
const props = getChartProps();
if (!stickyTooltips.find(t => t.id === props.id)) {
setStickyToolTips(prev => [...prev, props as ChartTooltipProps]);
}
}, [getChartProps, stickyTooltips, showTooltip]);
const handleUnStick = (id: string) => {
setStickyToolTips(prev => prev.filter(t => t.id !== id));
};
const setCursor = (u: uPlot) => {
const dataIdx = u.cursor.idx ?? -1;
setTooltipIdx(prev => ({ ...prev, dataIdx }));
};
const seriesFocus = (u: uPlot, sidx: (number | null)) => {
const seriesIdx = sidx ?? -1;
setTooltipIdx(prev => ({ ...prev, seriesIdx }));
};
const addSeries = (u: uPlot, series: uPlotSeries[]) => {
series.forEach((s) => {
u.addSeries(s);
});
};
const delSeries = (u: uPlot) => {
for (let i = u.series.length - 1; i >= 0; i--) {
u.delSeries(i);
}
};
const delHooks = (u: uPlot) => {
Object.keys(u.hooks).forEach(hook => {
u.hooks[hook as keyof uPlot.Hooks.Arrays] = [];
});
};
const handleDestroy = (u: uPlot) => {
delSeries(u);
delHooks(u);
u.setData([]);
};
const setSelect = (u: uPlot) => {
const min = u.posToVal(u.select.left, "x");
const max = u.posToVal(u.select.left + u.select.width, "x");
setPlotScale({ min, max });
};
const { xRange, setPlotScale } = usePlotScale({ period, setPeriod });
const { onReadyChart, isPanning } = useReadyChart(setPlotScale);
useZoomChart({ uPlotInst, xRange, setPlotScale });
const {
showTooltip,
stickyTooltips,
handleUnStick,
getTooltipProps,
seriesFocus,
setCursor,
resetTooltips
} = useLineTooltip({ u: uPlotInst, metrics, series, unit });
const options: uPlotOptions = {
...defaultOptions,
tzDate: ts => dayjs(formatDateForNativeInput(dateFromSeconds(ts))).local().toDate(),
...getDefaultOptions({ width: layoutSize.width, height }),
series,
axes: getAxes( [{}, { scale: "1" }], unit),
axes: getAxes([{}, { scale: "1" }], unit),
scales: getScales(yaxis, xRange),
width: layoutSize.width || 400,
height: height || 500,
hooks: {
ready: [onReadyChart],
setSeries: [seriesFocus],
setCursor: [setCursor],
setSelect: [setSelect],
setSelect: [setSelect(setPlotScale)],
destroy: [handleDestroy],
},
};
const handleTouchStart = (e: TouchEvent) => {
if (e.touches.length !== 2) return;
e.preventDefault();
const dx = e.touches[0].clientX - e.touches[1].clientX;
const dy = e.touches[0].clientY - e.touches[1].clientY;
setStartTouchDistance(Math.sqrt(dx * dx + dy * dy));
};
const handleTouchMove = useCallback((e: TouchEvent) => {
if (e.touches.length !== 2 || !uPlotInst) return;
e.preventDefault();
const dx = e.touches[0].clientX - e.touches[1].clientX;
const dy = e.touches[0].clientY - e.touches[1].clientY;
const endTouchDistance = Math.sqrt(dx * dx + dy * dy);
const diffDistance = startTouchDistance - endTouchDistance;
const max = (uPlotInst.scales.x.max || xRange.max);
const min = (uPlotInst.scales.x.min || xRange.min);
const dur = max - min;
const dir = (diffDistance > 0 ? -1 : 1);
const zoomFactor = dur / 50 * dir;
uPlotInst.batch(() => setPlotScale({
min: min + zoomFactor,
max: max - zoomFactor
}));
}, [uPlotInst, startTouchDistance, xRange]);
useEffect(() => {
setXRange({ min: period.start, max: period.end });
}, [period]);
useEffect(() => {
setStickyToolTips([]);
setTooltipIdx({ seriesIdx: -1, dataIdx: -1 });
resetTooltips();
if (!uPlotRef.current) return;
if (uPlotInst) uPlotInst.destroy();
const u = new uPlot(options, data, uPlotRef.current);
setUPlotInst(u);
setXRange({ min: period.start, max: period.end });
return u.destroy;
}, [uPlotRef, isDarkTheme]);
@ -287,15 +127,6 @@ const LineChart: FC<LineChartProps> = ({
uPlotInst.redraw();
}, [height, layoutSize]);
useEffect(() => {
setShowTooltip(tooltipIdx.dataIdx !== -1 && tooltipIdx.seriesIdx !== -1);
}, [tooltipIdx]);
useEventListener("click", handleClick);
useEventListener("keydown", handleKeyDown);
useEventListener("touchmove", handleTouchMove);
useEventListener("touchstart", handleTouchStart);
return (
<div
className={classNames({
@ -311,22 +142,12 @@ const LineChart: FC<LineChartProps> = ({
className="vm-line-chart__u-plot"
ref={uPlotRef}
/>
{uPlotInst && showTooltip && (
<ChartTooltip
{...getChartProps()}
u={uPlotInst}
<ChartTooltipWrapper
showTooltip={showTooltip}
tooltipProps={getTooltipProps()}
stickyTooltips={stickyTooltips}
handleUnStick={handleUnStick}
/>
)}
{uPlotInst && stickyTooltips.map(t => (
<ChartTooltip
{...t}
isSticky
u={uPlotInst}
key={t.id}
onClose={handleUnStick}
/>
))}
</div>
);
};

View File

@ -8,11 +8,12 @@ import {
getHideSeries,
getLegendItem,
getSeriesItemContext,
SeriesItem
} from "../../../utils/uplot/series";
import { getLimitsYAxis, getMinMaxBuffer, getTimeSeries } from "../../../utils/uplot/axes";
import { LegendItemType } from "../../../utils/uplot/types";
import { TimeParams } from "../../../types";
normalizeData,
getLimitsYAxis,
getMinMaxBuffer,
getTimeSeries,
} from "../../../utils/uplot";
import { TimeParams, SeriesItem, LegendItemType } from "../../../types";
import { AxisRange, YaxisState } from "../../../state/graph/reducer";
import { getAvgFromArray, getMaxFromArray, getMinFromArray } from "../../../utils/math";
import classNames from "classnames";
@ -20,10 +21,9 @@ import { useTimeState } from "../../../state/time/TimeStateContext";
import HeatmapChart from "../../Chart/Heatmap/HeatmapChart/HeatmapChart";
import "./style.scss";
import { promValueToNumber } from "../../../utils/metric";
import { normalizeData } from "../../../utils/uplot/heatmap";
import useDeviceDetect from "../../../hooks/useDeviceDetect";
import { TooltipHeatmapProps } from "../../Chart/Heatmap/ChartTooltipHeatmap/ChartTooltipHeatmap";
import useElementSize from "../../../hooks/useElementSize";
import { ChartTooltipProps } from "../../Chart/ChartTooltip/ChartTooltip";
export interface GraphViewProps {
data?: MetricResult[];
@ -35,7 +35,7 @@ export interface GraphViewProps {
unit?: string;
showLegend?: boolean;
setYaxisLimits: (val: AxisRange) => void
setPeriod: ({ from, to }: {from: Date, to: Date}) => void
setPeriod: ({ from, to }: { from: Date, to: Date }) => void
fullWidth?: boolean
height?: number
isHistogram?: boolean
@ -48,7 +48,7 @@ const GraphView: FC<GraphViewProps> = ({
query,
yaxis,
unit,
showLegend= true,
showLegend = true,
setYaxisLimits,
setPeriod,
alias = [],
@ -66,13 +66,13 @@ const GraphView: FC<GraphViewProps> = ({
const [series, setSeries] = useState<uPlotSeries[]>([]);
const [legend, setLegend] = useState<LegendItemType[]>([]);
const [hideSeries, setHideSeries] = useState<string[]>([]);
const [legendValue, setLegendValue] = useState<TooltipHeatmapProps | null>(null);
const [legendValue, setLegendValue] = useState<ChartTooltipProps | null>(null);
const getSeriesItem = useMemo(() => {
return getSeriesItemContext(data, hideSeries, alias);
}, [data, hideSeries, alias]);
const setLimitsYaxis = (values: {[key: string]: number[]}) => {
const setLimitsYaxis = (values: { [key: string]: number[] }) => {
const limits = getLimitsYAxis(values, !isHistogram);
setYaxisLimits(limits);
};
@ -105,7 +105,7 @@ const GraphView: FC<GraphViewProps> = ({
useEffect(() => {
const tempTimes: number[] = [];
const tempValues: {[key: string]: number[]} = {};
const tempValues: { [key: string]: number[] } = {};
const tempLegend: LegendItemType[] = [];
const tempSeries: uPlotSeries[] = [{}];
@ -199,7 +199,6 @@ const GraphView: FC<GraphViewProps> = ({
data={dataChart}
metrics={data}
period={period}
yaxis={yaxis}
unit={unit}
setPeriod={setPeriod}
layoutSize={containerSize}

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View File

@ -83,11 +83,13 @@ export const useFetchQuery = ({
setFetchQueue([...fetchQueue, controller]);
try {
const isDisplayChart = displayType === "chart";
let seriesLimit = showAllSeries ? Infinity : (+stateSeriesLimits[displayType] || Infinity);
const defaultLimit = showAllSeries ? Infinity : (+stateSeriesLimits[displayType] || Infinity);
let seriesLimit = defaultLimit;
const tempData: MetricBase[] = [];
const tempTraces: Trace[] = [];
let counter = 1;
let totalLength = 0;
let isHistogramResult = false;
for await (const url of fetchUrl) {
@ -115,9 +117,8 @@ export const useFetchQuery = ({
tempTraces.push(trace);
}
const isHistogramResult = isDisplayChart && isHistogramData(resp.data.result);
if (resp.data.result.length) setIsHistogram(isHistogramResult);
if (isHistogramResult) seriesLimit = Infinity;
isHistogramResult = isDisplayChart && isHistogramData(resp.data.result);
seriesLimit = isHistogramResult ? Infinity : Math.max(totalLength, defaultLimit);
const freeTempSize = seriesLimit - tempData.length;
resp.data.result.slice(0, freeTempSize).forEach((d: MetricBase) => {
d.group = counter;
@ -136,6 +137,7 @@ export const useFetchQuery = ({
setWarning(totalLength > seriesLimit ? limitText : "");
isDisplayChart ? setGraphData(tempData as MetricResult[]) : setLiveData(tempData as InstantMetricResult[]);
setTraces(tempTraces);
setIsHistogram(totalLength ? isHistogramResult: false);
} catch (e) {
if (e instanceof Error && e.name !== "AbortError") {
setError(`${e.name}: ${e.message}`);

View File

@ -6,7 +6,7 @@ import { useQueryState } from "../../../state/query/QueryStateContext";
import { displayTypeTabs } from "../DisplayTypeSwitch";
import { compactObject } from "../../../utils/object";
import { useGraphState } from "../../../state/graph/GraphStateContext";
import useSearchParamsFromObject from "../../../hooks/useSearchParamsFromObject";
import { useSearchParams } from "react-router-dom";
export const useSetQueryParams = () => {
const { tenantId } = useAppState();
@ -14,7 +14,7 @@ export const useSetQueryParams = () => {
const { query } = useQueryState();
const { duration, relativeTime, period: { date, step } } = useTimeState();
const { customStep } = useGraphState();
const { setSearchParamsFromKeys } = useSearchParamsFromObject();
const [, setSearchParams] = useSearchParams();
const setSearchParamsFromState = () => {
const params: Record<string, unknown> = {};
@ -30,7 +30,7 @@ export const useSetQueryParams = () => {
if ((step !== customStep) && customStep) params[`${group}.step_input`] = customStep;
});
setSearchParamsFromKeys(compactObject(params) as Record<string, string>);
setSearchParams(compactObject(params) as Record<string, string>);
};
useEffect(setSearchParamsFromState, [tenantId, displayType, query, duration, relativeTime, date, step, customStep]);

View File

@ -2,7 +2,7 @@ import React, { FC, useEffect, useMemo, KeyboardEvent } from "react";
import { useFetchTopQueries } from "./hooks/useFetchTopQueries";
import Spinner from "../../components/Main/Spinner/Spinner";
import TopQueryPanel from "./TopQueryPanel/TopQueryPanel";
import { formatPrettyNumber } from "../../utils/uplot/helpers";
import { formatPrettyNumber } from "../../utils/uplot";
import { isSupportedDuration } from "../../utils/time";
import dayjs from "dayjs";
import { TopQueryStats } from "../../types";

View File

@ -1,4 +1,5 @@
import { MetricBase } from "../api/types";
export * from "./uplot";
declare global {
interface Window {

View File

@ -1,4 +1,16 @@
import uPlot, { Series } from "uplot";
import { Axis, Series } from "uplot";
export interface SeriesItemStats {
min: string,
max: string,
median: string,
last: string
}
export interface SeriesItem extends Series {
freeFormFields: {[key: string]: string};
calculations: SeriesItemStats
}
export interface HideSeriesArgs {
hideSeries: string[],
@ -7,13 +19,9 @@ export interface HideSeriesArgs {
series: Series[]
}
export interface DragArgs {
e: MouseEvent | TouchEvent,
u: uPlot,
factor: number,
setPanning: (enable: boolean) => void,
setPlotScale: ({ min, max }: { min: number, max: number }) => void
}
export type MinMax = { min: number, max: number }
export type SetMinMax = ({ min, max }: MinMax) => void
export interface LegendItemType {
group: number;
@ -53,3 +61,7 @@ export interface Fill {
}
export type ArrayRGB = [number, number, number]
export interface AxisExtend extends Axis {
_size?: number;
}

View File

@ -1,4 +1,4 @@
import { ArrayRGB } from "./uplot/types";
import { ArrayRGB } from "../types";
export const baseContrastColors = [
"#e54040",

View File

@ -2,9 +2,10 @@ import uPlot, { Axis, Series } from "uplot";
import { getMaxFromArray, getMinFromArray } from "../math";
import { getSecondsFromDuration, roundToMilliseconds } from "../time";
import { AxisRange } from "../../state/graph/reducer";
import { formatTicks, sizeAxis } from "./helpers";
import { formatTicks, getTextWidth } from "./helpers";
import { TimeParams } from "../../types";
import { getCssVariable } from "../theme";
import { AxisExtend } from "../../types";
// see https://github.com/leeoniya/uPlot/tree/master/docs#axis--grid-opts
const timeValues = [
@ -77,3 +78,16 @@ export const getLimitsYAxis = (values: { [key: string]: number[] }, buffer: bool
result[key] = buffer ? getMinMaxBuffer(min, max) : [min, max];
return result;
};
export const sizeAxis = (u: uPlot, values: string[], axisIdx: number, cycleNum: number): number => {
const axis = u.axes[axisIdx] as AxisExtend;
if (cycleNum > 1) return axis._size || 60;
let axisSize = 6 + (axis?.ticks?.size || 0) + (axis.gap || 0);
const longestVal = (values ?? []).reduce((acc, val) => val?.length > acc.length ? val : acc, "");
if (longestVal != "") axisSize += getTextWidth(longestVal, "10px Arial");
return Math.ceil(axisSize);
};

View File

@ -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);
};

View File

@ -1,27 +1,5 @@
import uPlot, { Axis } from "uplot";
export const defaultOptions = {
legend: {
show: false
},
cursor: {
drag: {
x: true,
y: false
},
focus: {
prox: 30
},
points: {
size: 5.6,
width: 1.4
},
bind: {
click: (): null => null,
dblclick: (): null => null,
},
},
};
import uPlot from "uplot";
import { MetricResult } from "../../api/types";
export const formatTicks = (u: uPlot, ticks: number[], unit = ""): string[] => {
const min = ticks[0];
@ -32,7 +10,11 @@ export const formatTicks = (u: uPlot, ticks: number[], unit = ""): string[] => {
return ticks.map(v => `${formatPrettyNumber(v, min, max)} ${unit}`);
};
export const formatPrettyNumber = (n: number | null | undefined, min: number | null | undefined, max: number | null | undefined): string => {
export const formatPrettyNumber = (
n: number | null | undefined,
min: number | null | undefined,
max: number | null | undefined
): string => {
if (n === undefined || n === null) {
return "";
}
@ -59,11 +41,7 @@ export const formatPrettyNumber = (n: number | null | undefined, min: number | n
});
};
interface AxisExtend extends Axis {
_size?: number;
}
const getTextWidth = (val: string, font: string): number => {
export const getTextWidth = (val: string, font: string): number => {
const span = document.createElement("span");
span.innerText = val;
span.style.cssText = `position: absolute; z-index: -1; pointer-events: none; opacity: 0; font: ${font}`;
@ -73,17 +51,17 @@ const getTextWidth = (val: string, font: string): number => {
return width;
};
export const sizeAxis = (u: uPlot, values: string[], axisIdx: number, cycleNum: number): number => {
const axis = u.axes[axisIdx] as AxisExtend;
if (cycleNum > 1) return axis._size || 60;
let axisSize = 6 + (axis?.ticks?.size || 0) + (axis.gap || 0);
const longestVal = (values ?? []).reduce((acc, val) => val?.length > acc.length ? val : acc, "");
if (longestVal != "") axisSize += getTextWidth(longestVal, "10px Arial");
return Math.ceil(axisSize);
export const getDashLine = (group: number): number[] => {
return group <= 1 ? [] : [group*4, group*1.2];
};
export const getDashLine = (group: number): number[] => group <= 1 ? [] : [group*4, group*1.2];
export const getMetricName = (metricItem: MetricResult) => {
const metric = metricItem?.metric || {};
const labelNames = Object.keys(metric).filter(x => x != "__name__");
const labels = labelNames.map(key => `${key}=${JSON.stringify(metric[key])}`);
let metricName = metric["__name__"] || "";
if (labels.length > 0) {
metricName += "{" + labels.join(",") + "}";
}
return metricName;
};

View 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] = [];
});
};

View 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";

View 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([]);
};

View File

@ -1,22 +1,16 @@
import uPlot, { Range, Scale, Scales } from "uplot";
import { getMinMaxBuffer } from "./axes";
import { YaxisState } from "../../state/graph/reducer";
import { MinMax, SetMinMax } from "../../types";
interface XRangeType {
min: number,
max: number
}
export const getRangeX = (xRange: XRangeType): Range.MinMax => {
return [xRange.min, xRange.max];
};
export const getRangeX = ({ min, max }: MinMax): Range.MinMax => [min, max];
export const getRangeY = (u: uPlot, min = 0, max = 1, axis: string, yaxis: YaxisState): Range.MinMax => {
if (yaxis.limits.enable) return yaxis.limits.range[axis];
return getMinMaxBuffer(min, max);
};
export const getScales = (yaxis: YaxisState, xRange: XRangeType): Scales => {
export const getScales = (yaxis: YaxisState, xRange: MinMax): Scales => {
const scales: { [key: string]: { range: Scale.Range } } = { x: { range: () => getRangeX(xRange) } };
const ranges = Object.keys(yaxis.limits.range);
(ranges.length ? ranges : ["1"]).forEach(axis => {
@ -24,3 +18,9 @@ export const getScales = (yaxis: YaxisState, xRange: XRangeType): Scales => {
});
return scales;
};
export const setSelect = (setPlotScale: SetMinMax) => (u: uPlot) => {
const min = u.posToVal(u.select.left, "x");
const max = u.posToVal(u.select.left + u.select.width, "x");
setPlotScale({ min, max });
};

View File

@ -1,22 +1,11 @@
import { MetricResult } from "../../api/types";
import { Series } from "uplot";
import uPlot, { Series as uPlotSeries } from "uplot";
import { getNameForMetric, promValueToNumber } from "../metric";
import { BarSeriesItem, Disp, Fill, LegendItemType, Stroke } from "./types";
import { HideSeriesArgs } from "./types";
import { HideSeriesArgs, BarSeriesItem, Disp, Fill, LegendItemType, Stroke, SeriesItem } from "../../types";
import { baseContrastColors, getColorFromString } from "../color";
import { getMedianFromArray, getMaxFromArray, getMinFromArray, getLastFromArray } from "../math";
import { formatPrettyNumber } from "./helpers";
export interface SeriesItem extends Series {
freeFormFields: {[key: string]: string};
calculations: {
min: string,
max: string,
median: string,
last: string
}
}
export const getSeriesItemContext = (data: MetricResult[], hideSeries: string[], alias: string[]) => {
const colorState: {[key: string]: string} = {};
const calculations = data.map(d => {
@ -108,3 +97,15 @@ export const barDisp = (stroke: Stroke, fill: Fill): Disp => {
fill: fill
};
};
export const delSeries = (u: uPlot) => {
for (let i = u.series.length - 1; i >= 0; i--) {
u.delSeries(i);
}
};
export const addSeries = (u: uPlot, series: uPlotSeries[]) => {
series.forEach((s) => {
u.addSeries(s);
});
};