From d9520b1a792ad34c313e61f974fcc77c9af7aa06 Mon Sep 17 00:00:00 2001 From: Julius Volz <julius.volz@gmail.com> Date: Fri, 19 Jul 2024 12:13:14 +0200 Subject: [PATCH] Try out uPlot for new UI Signed-off-by: Julius Volz <julius.volz@gmail.com> --- web/ui/mantine-ui/package.json | 5 +- web/ui/mantine-ui/src/pages/query/EChart.tsx | 272 ---- .../src/pages/query/Graph.module.css | 11 + web/ui/mantine-ui/src/pages/query/Graph.tsx | 1221 +++++++++++++++-- web/ui/mantine-ui/src/pages/query/uplot.css | 26 + web/ui/package-lock.json | 55 +- 6 files changed, 1170 insertions(+), 420 deletions(-) delete mode 100644 web/ui/mantine-ui/src/pages/query/EChart.tsx create mode 100644 web/ui/mantine-ui/src/pages/query/Graph.module.css create mode 100644 web/ui/mantine-ui/src/pages/query/uplot.css diff --git a/web/ui/mantine-ui/package.json b/web/ui/mantine-ui/package.json index 9ed21e7f56..f9999a2472 100644 --- a/web/ui/mantine-ui/package.json +++ b/web/ui/mantine-ui/package.json @@ -30,13 +30,14 @@ "@types/lodash": "^4.17.7", "@uiw/react-codemirror": "^4.21.22", "dayjs": "^1.11.10", - "echarts": "^5.5.1", "lodash": "^4.17.21", "react": "^18.2.0", "react-dom": "^18.2.0", "react-infinite-scroll-component": "^6.1.0", "react-redux": "^9.1.0", - "react-router-dom": "^6.22.1" + "react-router-dom": "^6.22.1", + "uplot": "^1.6.30", + "uplot-react": "^1.2.2" }, "devDependencies": { "@types/react": "^18.2.55", diff --git a/web/ui/mantine-ui/src/pages/query/EChart.tsx b/web/ui/mantine-ui/src/pages/query/EChart.tsx deleted file mode 100644 index 84bd8ac925..0000000000 --- a/web/ui/mantine-ui/src/pages/query/EChart.tsx +++ /dev/null @@ -1,272 +0,0 @@ -// Copyright 2023 The Perses Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import React, { useEffect, useLayoutEffect, useRef } from "react"; -import { - ECharts, - EChartsCoreOption, - EChartsOption, - init, - connect, - BarSeriesOption, - LineSeriesOption, - GaugeSeriesOption, -} from "echarts"; -import isEqual from "lodash/isEqual"; -import debounce from "lodash/debounce"; -import { Box } from "@mantine/core"; - -// https://github.com/apache/echarts/issues/12489#issuecomment-643185207 -export interface EChartsTheme extends EChartsOption { - bar?: BarSeriesOption; - line?: LineSeriesOption; - gauge?: GaugeSeriesOption; -} - -// see docs for info about each property: https://echarts.apache.org/en/api.html#events -export interface MouseEventsParameters<T> { - componentType: string; - seriesType: string; - seriesIndex: number; - seriesName: string; - name: string; - dataIndex: number; - data: Record<string, unknown> & T; - dataType: string; - value: number | number[]; - color: string; - info: Record<string, unknown>; -} - -type OnEventFunction<T> = ( - params: MouseEventsParameters<T>, - // This is potentially undefined for testing purposes - instance?: ECharts -) => void; - -const mouseEvents = [ - "click", - "dblclick", - "mousedown", - "mousemove", - "mouseup", - "mouseover", - "mouseout", - "globalout", - "contextmenu", -] as const; - -export type MouseEventName = (typeof mouseEvents)[number]; - -// batch event types -export interface DataZoomPayloadBatchItem { - dataZoomId: string; - // start and end not returned unless dataZoom is based on percentProp, - // which is for cases when a dataZoom component controls multiple axes - start?: number; - end?: number; - // startValue and endValue return data index for 'category' axes, - // for axis types 'value' and 'time', actual values are returned - startValue?: number; - endValue?: number; -} - -export interface HighlightPayloadBatchItem { - dataIndex: number; - dataIndexInside: number; - seriesIndex: number; - // highlight action can effect multiple connected charts - escapeConnect?: boolean; - // whether blur state was triggered - notBlur?: boolean; -} - -export interface BatchEventsParameters { - type: BatchEventName; - batch: DataZoomPayloadBatchItem[] & HighlightPayloadBatchItem[]; -} - -type OnBatchEventFunction = (params: BatchEventsParameters) => void; - -const batchEvents = ["datazoom", "downplay", "highlight"] as const; - -export type BatchEventName = (typeof batchEvents)[number]; - -type ChartEventName = "finished"; - -type EventName = MouseEventName | ChartEventName | BatchEventName; - -export type OnEventsType<T> = { - [mouseEventName in MouseEventName]?: OnEventFunction<T>; -} & { - [batchEventName in BatchEventName]?: OnBatchEventFunction; -} & { - [eventName in ChartEventName]?: () => void; -}; - -export interface EChartsProps<T> { - option: EChartsCoreOption; - theme?: string | EChartsTheme; - renderer?: "canvas" | "svg"; - onEvents?: OnEventsType<T>; - _instance?: React.MutableRefObject<ECharts | undefined>; - syncGroup?: string; - onChartInitialized?: (instance: ECharts) => void; -} - -export const chartHeight = 500; -export const legendHeight = (numSeries: number) => numSeries * 24; -export const legendMargin = 25; - -export const EChart = React.memo(function EChart<T>({ - option, - theme, - renderer, - onEvents, - _instance, - syncGroup, - onChartInitialized, -}: EChartsProps<T>) { - const initialOption = useRef<EChartsCoreOption>(option); - const prevOption = useRef<EChartsCoreOption>(option); - const containerRef = useRef<HTMLDivElement | null>(null); - const chartElement = useRef<ECharts | null>(null); - - // Initialize chart, dispose on unmount - useLayoutEffect(() => { - if (containerRef.current === null || chartElement.current !== null) return; - chartElement.current = init(containerRef.current, theme, { - renderer: renderer ?? "canvas", - }); - if (chartElement.current === undefined) return; - chartElement.current.setOption(initialOption.current, true); - onChartInitialized?.(chartElement.current); - if (_instance !== undefined) { - _instance.current = chartElement.current; - } - return () => { - if (chartElement.current === null) return; - chartElement.current.dispose(); - chartElement.current = null; - }; - }, [_instance, onChartInitialized, theme, renderer]); - - // When syncGroup is explicitly set, charts within same group share interactions such as crosshair - useEffect(() => { - if (!chartElement.current || !syncGroup) return; - chartElement.current.group = syncGroup; - connect([chartElement.current]); // more info: https://echarts.apache.org/en/api.html#echarts.connect - }, [syncGroup, chartElement]); - - // Update chart data when option changes - useEffect(() => { - if (prevOption.current === undefined || isEqual(prevOption.current, option)) - return; - if (!chartElement.current) return; - chartElement.current.setOption(option, true); - prevOption.current = option; - }, [option]); - - // Resize chart, cleanup listener on unmount - useLayoutEffect(() => { - const updateSize = debounce(() => { - if (!chartElement.current) return; - chartElement.current.resize(); - }, 200); - window.addEventListener("resize", updateSize); - updateSize(); - return () => { - window.removeEventListener("resize", updateSize); - }; - }, []); - - // Bind and unbind chart events passed as prop - useEffect(() => { - const chart = chartElement.current; - if (!chart || onEvents === undefined) return; - bindEvents(chart, onEvents); - return () => { - if (chart === undefined) return; - if (chart.isDisposed() === true) return; - for (const event in onEvents) { - chart.off(event); - } - }; - }, [onEvents]); - - // // TODO: re-evaluate how this is triggered. It's technically working right - // // now because the sx prop is an object that gets re-created, but that also - // // means it runs unnecessarily some of the time and theoretically might - // // not run in some other cases. Maybe it should use a resize observer? - // useEffect(() => { - // // TODO: fix this debouncing. This likely isn't working as intended because - // // the debounced function is re-created every time this useEffect is called. - // const updateSize = debounce( - // () => { - // if (!chartElement.current) return; - // chartElement.current.resize(); - // }, - // 200, - // { - // leading: true, - // } - // ); - // updateSize(); - // }, [sx]); - - return ( - <Box - w="100%" - h={ - chartHeight + - legendMargin + - legendHeight((option as { series: unknown[] }).series.length) - } - ref={containerRef} - ></Box> - ); -}); - -// Validate event config and bind custom events -function bindEvents<T>(instance: ECharts, events?: OnEventsType<T>) { - if (events === undefined) return; - - function bindEvent(eventName: EventName, OnEventFunction: unknown) { - if (typeof OnEventFunction === "function") { - if (isMouseEvent(eventName)) { - instance.on(eventName, (params) => OnEventFunction(params, instance)); - } else if (isBatchEvent(eventName)) { - instance.on(eventName, (params) => OnEventFunction(params)); - } else { - instance.on(eventName, () => OnEventFunction(null, instance)); - } - } - } - - for (const eventName in events) { - if (Object.prototype.hasOwnProperty.call(events, eventName)) { - const customEvent = events[eventName as EventName] ?? null; - if (customEvent) { - bindEvent(eventName as EventName, customEvent); - } - } - } -} - -function isMouseEvent(eventName: EventName): eventName is MouseEventName { - return (mouseEvents as readonly string[]).includes(eventName); -} - -function isBatchEvent(eventName: EventName): eventName is BatchEventName { - return (batchEvents as readonly string[]).includes(eventName); -} diff --git a/web/ui/mantine-ui/src/pages/query/Graph.module.css b/web/ui/mantine-ui/src/pages/query/Graph.module.css new file mode 100644 index 0000000000..5b7133058c --- /dev/null +++ b/web/ui/mantine-ui/src/pages/query/Graph.module.css @@ -0,0 +1,11 @@ +.chartWrapper { + border: 1px solid + light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5)); + border-radius: var(--mantine-radius-default); +} + +.uplotChart { + width: 100%; + height: 100%; + padding: 15px; +} diff --git a/web/ui/mantine-ui/src/pages/query/Graph.tsx b/web/ui/mantine-ui/src/pages/query/Graph.tsx index 531b33278d..33b3b9b7e6 100644 --- a/web/ui/mantine-ui/src/pages/query/Graph.tsx +++ b/web/ui/mantine-ui/src/pages/query/Graph.tsx @@ -3,11 +3,14 @@ import { Alert, Skeleton, Box, LoadingOverlay } from "@mantine/core"; import { IconAlertTriangle, IconInfoCircle } from "@tabler/icons-react"; import { InstantQueryResult } from "../../api/responseTypes/query"; import { useAPIQuery } from "../../api/api"; -import classes from "./DataTable.module.css"; +import classes from "./Graph.module.css"; import { GraphDisplayMode } from "../../state/queryPageSlice"; -import { EChart, chartHeight, legendMargin } from "./EChart"; import { formatSeries } from "../../lib/formatSeries"; -import { EChartsOption } from "echarts"; +import uPlot from "uplot"; +import UplotReact from "uplot-react"; +import "uplot/dist/uPlot.min.css"; +import "./uplot.css"; +import { useElementSize } from "@mantine/hooks"; export interface GraphProps { expr: string; @@ -19,6 +22,942 @@ export interface GraphProps { retriggerIdx: number; } +export const colorPool = [ + "#008000", + "#008080", + "#800000", + "#800080", + "#808000", + "#808080", + "#0000c0", + "#008040", + "#0080c0", + "#800040", + "#8000c0", + "#808040", + "#8080c0", + "#00c000", + "#00c080", + "#804000", + "#804080", + "#80c000", + "#80c080", + "#0040c0", + "#00c040", + "#00c0c0", + "#804040", + "#8040c0", + "#80c040", + "#80c0c0", + "#408000", + "#408080", + "#c00000", + "#c00080", + "#c08000", + "#c08080", + "#4000c0", + "#408040", + "#4080c0", + "#c00040", + "#c000c0", + "#c08040", + "#c080c0", + "#404000", + "#404080", + "#40c000", + "#40c080", + "#c04000", + "#c04080", + "#c0c000", + "#c0c080", + "#404040", + "#4040c0", + "#40c040", + "#40c0c0", + "#c04040", + "#c040c0", + "#c0c040", + "#0000a0", + "#008020", + "#0080a0", + "#800020", + "#8000a0", + "#808020", + "#8080a0", + "#0000e0", + "#008060", + "#0080e0", + "#800060", + "#8000e0", + "#808060", + "#8080e0", + "#0040a0", + "#00c020", + "#00c0a0", + "#804020", + "#8040a0", + "#80c020", + "#80c0a0", + "#0040e0", + "#00c060", + "#00c0e0", + "#804060", + "#8040e0", + "#80c060", + "#80c0e0", + "#4000a0", + "#408020", + "#4080a0", + "#c00020", + "#c000a0", + "#c08020", + "#c080a0", + "#4000e0", + "#408060", + "#4080e0", + "#c00060", + "#c000e0", + "#c08060", + "#c080e0", + "#404020", + "#4040a0", + "#40c020", + "#40c0a0", + "#c04020", + "#c040a0", + "#c0c020", + "#c0c0a0", + "#404060", + "#4040e0", + "#40c060", + "#40c0e0", + "#c04060", + "#c040e0", + "#c0c060", + "#00a000", + "#00a080", + "#802000", + "#802080", + "#80a000", + "#80a080", + "#0020c0", + "#00a040", + "#00a0c0", + "#802040", + "#8020c0", + "#80a040", + "#80a0c0", + "#006000", + "#006080", + "#00e000", + "#00e080", + "#806000", + "#806080", + "#80e000", + "#80e080", + "#006040", + "#0060c0", + "#00e040", + "#00e0c0", + "#806040", + "#8060c0", + "#80e040", + "#80e0c0", + "#40a000", + "#40a080", + "#c02000", + "#c02080", + "#c0a000", + "#c0a080", + "#4020c0", + "#40a040", + "#40a0c0", + "#c02040", + "#c020c0", + "#c0a040", + "#c0a0c0", + "#406000", + "#406080", + "#40e000", + "#40e080", + "#c06000", + "#c06080", + "#c0e000", + "#c0e080", + "#406040", + "#4060c0", + "#40e040", + "#40e0c0", + "#c06040", + "#c060c0", + "#c0e040", + "#c0e0c0", + "#0020a0", + "#00a020", + "#00a0a0", + "#802020", + "#8020a0", + "#80a020", + "#80a0a0", + "#0020e0", + "#00a060", + "#00a0e0", + "#802060", + "#8020e0", + "#80a060", + "#80a0e0", + "#006020", + "#0060a0", + "#00e020", + "#00e0a0", + "#806020", + "#8060a0", + "#80e020", + "#80e0a0", + "#006060", + "#0060e0", + "#00e060", + "#00e0e0", + "#806060", + "#8060e0", + "#80e060", + "#80e0e0", + "#4020a0", + "#40a020", + "#40a0a0", + "#c02020", + "#c020a0", + "#c0a020", + "#c0a0a0", + "#4020e0", + "#40a060", + "#40a0e0", + "#c02060", + "#c020e0", + "#c0a060", + "#c0a0e0", + "#406020", + "#4060a0", + "#40e020", + "#40e0a0", + "#c06020", + "#c060a0", + "#c0e020", + "#c0e0a0", + "#406060", + "#4060e0", + "#40e060", + "#40e0e0", + "#c06060", + "#c060e0", + "#c0e060", + "#208000", + "#208080", + "#a00000", + "#a00080", + "#a08000", + "#a08080", + "#208040", + "#2080c0", + "#a00040", + "#a000c0", + "#a08040", + "#a080c0", + "#204080", + "#20c000", + "#20c080", + "#a04000", + "#a04080", + "#a0c000", + "#a0c080", + "#2040c0", + "#20c040", + "#20c0c0", + "#a04040", + "#a040c0", + "#a0c040", + "#a0c0c0", + "#608000", + "#608080", + "#e00000", + "#e00080", + "#e08000", + "#e08080", + "#6000c0", + "#608040", + "#6080c0", + "#e00040", + "#e000c0", + "#e08040", + "#e080c0", + "#604080", + "#60c000", + "#60c080", + "#e04000", + "#e04080", + "#e0c000", + "#e0c080", + "#604040", + "#6040c0", + "#60c040", + "#60c0c0", + "#e04040", + "#e040c0", + "#e0c040", + "#e0c0c0", + "#208020", + "#2080a0", + "#a00020", + "#a000a0", + "#a08020", + "#a080a0", + "#2000e0", + "#208060", + "#2080e0", + "#a00060", + "#a000e0", + "#a08060", + "#a080e0", + "#2040a0", + "#20c020", + "#20c0a0", + "#a04020", + "#a040a0", + "#a0c020", + "#2040e0", + "#20c060", + "#20c0e0", + "#a04060", + "#a040e0", + "#a0c060", + "#a0c0e0", + "#6000a0", + "#608020", + "#6080a0", + "#e00020", + "#e000a0", + "#e08020", + "#e080a0", + "#6000e0", + "#608060", + "#6080e0", + "#e00060", + "#e000e0", + "#e08060", + "#e080e0", + "#604020", + "#6040a0", + "#60c020", + "#60c0a0", + "#e04020", + "#e040a0", + "#e0c020", + "#e0c0a0", + "#604060", + "#6040e0", + "#60c060", + "#60c0e0", + "#e04060", + "#e040e0", + "#e0c060", + "#e0c0e0", + "#20a000", + "#20a080", + "#a02000", + "#a02080", + "#a0a000", + "#a0a080", + "#2020c0", + "#20a040", + "#20a0c0", + "#a02040", + "#a020c0", + "#a0a040", + "#a0a0c0", + "#206000", + "#206080", + "#20e000", + "#20e080", + "#a06000", + "#a06080", + "#a0e000", + "#a0e080", + "#206040", + "#2060c0", + "#20e040", + "#20e0c0", + "#a06040", + "#a060c0", + "#a0e040", + "#a0e0c0", + "#602080", + "#60a000", + "#60a080", + "#e02000", + "#e02080", + "#e0a000", + "#e0a080", + "#6020c0", + "#60a040", + "#60a0c0", + "#e02040", + "#e020c0", + "#e0a040", + "#e0a0c0", + "#606000", + "#606080", + "#60e000", + "#60e080", + "#e06000", + "#e06080", + "#e0e000", + "#e0e080", + "#606040", + "#6060c0", + "#60e040", + "#60e0c0", + "#e06040", + "#e060c0", + "#e0e040", + "#e0e0c0", + "#20a020", + "#20a0a0", + "#a02020", + "#a020a0", + "#a0a020", + "#a0a0a0", + "#2020e0", + "#20a060", + "#20a0e0", + "#a02060", + "#a020e0", + "#a0a060", + "#a0a0e0", + "#206020", + "#2060a0", + "#20e020", + "#20e0a0", + "#a06020", + "#a060a0", + "#a0e020", + "#a0e0a0", + "#206060", + "#2060e0", + "#20e060", + "#20e0e0", + "#a06060", + "#a060e0", + "#a0e060", + "#a0e0e0", + "#6020a0", + "#60a020", + "#60a0a0", + "#e02020", + "#e020a0", + "#e0a020", + "#e0a0a0", + "#602060", + "#6020e0", + "#60a060", + "#60a0e0", + "#e02060", + "#e020e0", + "#e0a060", + "#e0a0e0", + "#606020", + "#6060a0", + "#60e020", + "#60e0a0", + "#e06020", + "#e060a0", + "#e0e020", + "#e0e0a0", + "#606060", + "#6060e0", + "#60e060", + "#60e0e0", + "#e06060", + "#e060e0", + "#e0e060", + "#008010", + "#008090", + "#800010", + "#800090", + "#808010", + "#808090", + "#0000d0", + "#008050", + "#0080d0", + "#800050", + "#8000d0", + "#808050", + "#8080d0", + "#004010", + "#004090", + "#00c010", + "#00c090", + "#804010", + "#804090", + "#80c010", + "#80c090", + "#004050", + "#0040d0", + "#00c050", + "#00c0d0", + "#804050", + "#8040d0", + "#80c050", + "#80c0d0", + "#400090", + "#408010", + "#408090", + "#c00010", + "#c00090", + "#c08010", + "#c08090", + "#4000d0", + "#408050", + "#4080d0", + "#c00050", + "#c000d0", + "#c08050", + "#c080d0", + "#404010", + "#404090", + "#40c010", + "#40c090", + "#c04010", + "#c04090", + "#c0c010", + "#c0c090", + "#404050", + "#4040d0", + "#40c050", + "#40c0d0", + "#c04050", + "#c040d0", + "#c0c050", + "#0000b0", + "#008030", + "#0080b0", + "#800030", + "#8000b0", + "#808030", + "#8080b0", + "#0000f0", + "#008070", + "#0080f0", + "#800070", + "#8000f0", + "#808070", + "#8080f0", + "#004030", + "#0040b0", + "#00c030", + "#00c0b0", + "#804030", + "#8040b0", + "#80c030", + "#80c0b0", + "#004070", + "#0040f0", + "#00c070", + "#00c0f0", + "#804070", + "#8040f0", + "#80c070", + "#80c0f0", + "#4000b0", + "#408030", + "#4080b0", + "#c00030", + "#c000b0", + "#c08030", + "#c080b0", + "#400070", + "#4000f0", + "#408070", + "#4080f0", + "#c00070", + "#c000f0", + "#c08070", + "#c080f0", + "#404030", + "#4040b0", + "#40c030", + "#40c0b0", + "#c04030", + "#c040b0", + "#c0c030", + "#c0c0b0", + "#404070", + "#4040f0", + "#40c070", + "#40c0f0", + "#c04070", + "#c040f0", + "#c0c070", + "#c0c0f0", + "#002090", + "#00a010", + "#00a090", + "#802010", + "#802090", + "#80a010", + "#80a090", + "#0020d0", + "#00a050", + "#00a0d0", + "#802050", + "#8020d0", + "#80a050", + "#80a0d0", + "#006010", + "#006090", + "#00e010", + "#00e090", + "#806010", + "#806090", + "#80e010", + "#80e090", + "#006050", + "#0060d0", + "#00e050", + "#00e0d0", + "#806050", + "#8060d0", + "#80e050", + "#80e0d0", + "#402090", + "#40a010", + "#40a090", + "#c02010", + "#c02090", + "#c0a010", + "#c0a090", + "#402050", + "#4020d0", + "#40a050", + "#40a0d0", + "#c02050", + "#c020d0", + "#c0a050", + "#c0a0d0", + "#406010", + "#406090", + "#40e010", + "#40e090", + "#c06010", + "#c06090", + "#c0e010", + "#c0e090", + "#406050", + "#4060d0", + "#40e050", + "#40e0d0", + "#c06050", + "#c060d0", + "#c0e050", + "#c0e0d0", + "#0020b0", + "#00a030", + "#00a0b0", + "#802030", + "#8020b0", + "#80a030", + "#80a0b0", + "#0020f0", + "#00a070", + "#00a0f0", + "#802070", + "#8020f0", + "#80a070", + "#80a0f0", + "#006030", + "#0060b0", + "#00e030", + "#00e0b0", + "#806030", + "#8060b0", + "#80e030", + "#80e0b0", + "#006070", + "#0060f0", + "#00e070", + "#00e0f0", + "#806070", + "#8060f0", + "#80e070", + "#80e0f0", + "#4020b0", + "#40a030", + "#40a0b0", + "#c02030", + "#c020b0", + "#c0a030", + "#c0a0b0", + "#4020f0", + "#40a070", + "#40a0f0", + "#c02070", + "#c020f0", + "#c0a070", + "#c0a0f0", + "#406030", + "#4060b0", + "#40e030", + "#40e0b0", + "#c06030", + "#c060b0", + "#c0e030", + "#c0e0b0", + "#406070", + "#4060f0", + "#40e070", + "#40e0f0", + "#c06070", + "#c060f0", + "#c0e070", + "#208010", + "#208090", + "#a00010", + "#a00090", + "#a08010", + "#a08090", + "#2000d0", + "#208050", + "#2080d0", + "#a00050", + "#a000d0", + "#a08050", + "#a080d0", + "#204010", + "#204090", + "#20c010", + "#20c090", + "#a04010", + "#a04090", + "#a0c010", + "#a0c090", + "#204050", + "#2040d0", + "#20c050", + "#20c0d0", + "#a04050", + "#a040d0", + "#a0c050", + "#a0c0d0", + "#600090", + "#608010", + "#608090", + "#e00010", + "#e00090", + "#e08010", + "#e08090", + "#600050", + "#6000d0", + "#608050", + "#6080d0", + "#e00050", + "#e000d0", + "#e08050", + "#e080d0", + "#604010", + "#604090", + "#60c010", + "#60c090", + "#e04010", + "#e04090", + "#e0c010", + "#e0c090", + "#604050", + "#6040d0", + "#60c050", + "#60c0d0", + "#e04050", + "#e040d0", + "#e0c050", + "#e0c0d0", + "#2000b0", + "#208030", + "#2080b0", + "#a00030", + "#a000b0", + "#a08030", + "#a080b0", + "#2000f0", + "#208070", + "#2080f0", + "#a00070", + "#a000f0", + "#a08070", + "#a080f0", + "#204030", + "#2040b0", + "#20c030", + "#20c0b0", + "#a04030", + "#a040b0", + "#a0c030", + "#a0c0b0", + "#204070", + "#2040f0", + "#20c070", + "#20c0f0", + "#a04070", + "#a040f0", + "#a0c070", + "#a0c0f0", + "#6000b0", + "#608030", + "#6080b0", + "#e00030", + "#e000b0", + "#e08030", + "#e080b0", + "#600070", + "#6000f0", + "#608070", + "#e00070", + "#e000f0", + "#e08070", + "#e080f0", + "#604030", + "#6040b0", + "#60c030", + "#60c0b0", + "#e04030", + "#e040b0", + "#e0c030", + "#e0c0b0", + "#604070", + "#6040f0", + "#60c070", + "#60c0f0", + "#e04070", + "#e040f0", + "#e0c070", + "#e0c0f0", + "#20a010", + "#20a090", + "#a02010", + "#a02090", + "#a0a010", + "#a0a090", + "#2020d0", + "#20a050", + "#20a0d0", + "#a02050", + "#a020d0", + "#a0a050", + "#a0a0d0", + "#206010", + "#206090", + "#20e010", + "#20e090", + "#a06010", + "#a06090", + "#a0e010", + "#a0e090", + "#206050", + "#2060d0", + "#20e050", + "#20e0d0", + "#a06050", + "#a060d0", + "#a0e050", + "#a0e0d0", + "#602090", + "#60a010", + "#60a090", + "#e02010", + "#e02090", + "#e0a010", + "#e0a090", + "#602050", + "#6020d0", + "#60a050", + "#60a0d0", + "#e02050", + "#e020d0", + "#e0a050", + "#e0a0d0", + "#606010", + "#606090", + "#60e010", + "#60e090", + "#e06010", + "#e06090", + "#e0e010", + "#e0e090", + "#606050", + "#6060d0", + "#60e050", + "#60e0d0", + "#e06050", + "#e060d0", + "#e0e050", + "#2020b0", + "#20a030", + "#20a0b0", + "#a02030", + "#a020b0", + "#a0a030", + "#a0a0b0", + "#2020f0", + "#20a070", + "#20a0f0", + "#a02070", + "#a020f0", + "#a0a070", + "#a0a0f0", + "#206030", + "#2060b0", + "#20e030", + "#20e0b0", + "#a06030", + "#a060b0", + "#a0e030", + "#a0e0b0", + "#206070", + "#2060f0", + "#20e070", + "#20e0f0", + "#a06070", + "#a060f0", + "#a0e070", + "#a0e0f0", + "#6020b0", + "#60a030", + "#60a0b0", + "#e02030", + "#e020b0", + "#e0a030", + "#e0a0b0", + "#6020f0", + "#60a070", + "#60a0f0", + "#e02070", + "#e020f0", + "#e0a070", + "#e0a0f0", + "#606030", + "#6060b0", + "#60e030", + "#60e0b0", + "#e06030", + "#e060b0", + "#e0e030", + "#e0e0b0", + "#606070", + "#6060f0", + "#60e070", + "#60e0f0", + "#e06070", + "#e060f0", + "#e0e070", +]; + const Graph: FC<GraphProps> = ({ expr, endTime, @@ -27,6 +966,8 @@ const Graph: FC<GraphProps> = ({ displayMode, retriggerIdx, }) => { + const { ref, width, height } = useElementSize(); + const realEndTime = (endTime !== null ? endTime : Date.now()) / 1000; const { data, error, isFetching, isLoading, refetch } = useAPIQuery<InstantQueryResult>({ @@ -98,71 +1039,175 @@ const Graph: FC<GraphProps> = ({ ); } - const option: EChartsOption = { - animation: false, - grid: { - left: 20, - top: 20, - right: 20, - bottom: 20 + result.length * 24, - containLabel: true, + // const option: EChartsOption = { + // animation: false, + // grid: { + // left: 20, + // top: 20, + // right: 20, + // bottom: 20 + result.length * 24, + // containLabel: true, + // }, + // legend: { + // type: "scroll", + // icon: "square", + // orient: "vertical", + // top: chartHeight + legendMargin, + // bottom: 20, + // left: 30, + // right: 20, + // }, + // xAxis: { + // type: "category", + // // min: realEndTime * 1000 - range, + // // max: realEndTime * 1000, + // data: result[0].values?.map((v) => Math.round(v[0] * 1000)), + // axisLine: { + // show: true, + // }, + // }, + // yAxis: { + // type: "value", + // axisLabel: { + // formatter: formatValue, + // }, + // axisLine: { + // // symbol: "arrow", + // show: true, + // // lineStyle: { + // // type: "dashed", + // // color: "rgba(0, 0, 0, 0.5)", + // // }, + // }, + // }, + // tooltip: { + // show: true, + // trigger: "item", + // transitionDuration: 0, + // axisPointer: { + // type: "cross", + // // snap: true, + // }, + // }, + // series: result.map((series) => ({ + // name: formatSeries(series.metric), + // // data: series.values?.map((v) => [v[0] * 1000, parseFloat(v[1])]), + // data: series.values?.map((v) => parseFloat(v[1])), + // type: "line", + // stack: displayMode === "stacked" ? "total" : undefined, + // // showSymbol: false, + // // fill: displayMode === "stacked" ? "tozeroy" : undefined, + // })), + // }; + + function autoPadRight(self, side, sidesWithAxes, cycleNum) { + const xAxis = self.axes[0]; + + const xVals = xAxis._values; + + if (xVals != null) { + // bail out, force convergence + if (cycleNum > 2) return self._padding[1]; + + const xSplits = xAxis._splits; + const rightSplit = xSplits[xSplits.length - 1]; + const rightSplitCoord = self.valToPos(rightSplit, "x"); + const leftPlotEdge = self.bbox.left / devicePixelRatio; + const rightPlotEdge = leftPlotEdge + self.bbox.width / devicePixelRatio; + const rightChartEdge = rightPlotEdge + self._padding[1]; + + const pxPerChar = 8; + const rightVal = xVals[xVals.length - 1] + ""; + const valHalfWidth = pxPerChar * (rightVal.length / 2); + + const rightValEdge = leftPlotEdge + rightSplitCoord + valHalfWidth; + + if (rightValEdge >= rightChartEdge) { + return rightValEdge - rightPlotEdge; + } + } + + // default size + return 8; + } + + const options: uPlot.Options = { + id: "chart", + width: width - 30, + height: 500, + padding: [null, autoPadRight, null, null], + // plugins: [tooltipPlugin()], + scales: { + x: { + time: false, + }, }, legend: { - type: "scroll", - icon: "square", - orient: "vertical", - top: chartHeight + legendMargin, - bottom: 20, - left: 30, - right: 20, - }, - xAxis: { - type: "category", - // min: realEndTime * 1000 - range, - // max: realEndTime * 1000, - data: result[0].values?.map((v) => Math.round(v[0] * 1000)), - axisLine: { - show: true, - }, - }, - yAxis: { - type: "value", - axisLabel: { - formatter: formatValue, - }, - axisLine: { - // symbol: "arrow", - show: true, - // lineStyle: { - // type: "dashed", - // color: "rgba(0, 0, 0, 0.5)", - // }, - }, - }, - tooltip: { show: true, - trigger: "item", - transitionDuration: 0, - axisPointer: { - type: "cross", - // snap: true, + live: false, + markers: { + fill: ( + self: uPlot, + seriesIdx: number + ): CSSStyleDeclaration["borderColor"] => { + return colorPool[seriesIdx % colorPool.length]; + }, }, }, - series: result.map((series) => ({ - name: formatSeries(series.metric), - // data: series.values?.map((v) => [v[0] * 1000, parseFloat(v[1])]), - data: series.values?.map((v) => parseFloat(v[1])), - type: "line", - stack: displayMode === "stacked" ? "total" : undefined, - // showSymbol: false, - // fill: displayMode === "stacked" ? "tozeroy" : undefined, - })), + axes: [ + { + labelSize: 20, + stroke: "#333", + values(self, splits) { + return splits.map((s) => s); + }, + }, + { + labelGap: 8, + labelSize: 8 + 12 + 8, + stroke: "#333", + size(self, values, axisIdx, cycleNum) { + const axis = self.axes[axisIdx]; + + // bail out, force convergence + if (cycleNum > 1) { + return axis._size; + } + + let axisSize = axis.ticks.size + axis.gap; + + // find longest value + const longestVal = (values ?? []).reduce( + (acc, val) => (val.length > acc.length ? val : acc), + "" + ); + + if (longestVal != "") { + self.ctx.font = axis.font[0]; + axisSize += + self.ctx.measureText(longestVal).width / devicePixelRatio; + } + + return Math.ceil(axisSize); + }, + }, + ], + series: [ + ...result.map((r, idx) => ({ + label: formatSeries(r.metric), + width: 2, + stroke: colorPool[idx % colorPool.length], + })), + ], }; - console.log(option); + const seriesData: uPlot.AlignedData = [ + result[0].values?.map((v) => v[0]), + ...result.map((r) => r.values?.map((v) => parseFloat(v[1]))), + ]; return ( - <Box pos="relative" className={classes.tableWrapper}> + <Box pos="relative" ref={ref} className={classes.chartWrapper}> <LoadingOverlay visible={isFetching} zIndex={1000} @@ -172,63 +1217,13 @@ const Graph: FC<GraphProps> = ({ }} styles={{ loader: { width: "100%", height: "100%" } }} /> - <EChart - option={option} - // theme={chartsTheme.echartsTheme} - // onEvents={handleEvents} - // _instance={chartRef} - // syncGroup={syncGroup} + <UplotReact + options={options} + data={seriesData} + className={classes.uplotChart} /> </Box> ); }; -const formatValue = (y: number | null): string => { - if (y === null) { - return "null"; - } - const absY = Math.abs(y); - - if (absY >= 1e24) { - return (y / 1e24).toFixed(2) + "Y"; - } else if (absY >= 1e21) { - return (y / 1e21).toFixed(2) + "Z"; - } else if (absY >= 1e18) { - return (y / 1e18).toFixed(2) + "E"; - } else if (absY >= 1e15) { - return (y / 1e15).toFixed(2) + "P"; - } else if (absY >= 1e12) { - return (y / 1e12).toFixed(2) + "T"; - } else if (absY >= 1e9) { - return (y / 1e9).toFixed(2) + "G"; - } else if (absY >= 1e6) { - return (y / 1e6).toFixed(2) + "M"; - } else if (absY >= 1e3) { - return (y / 1e3).toFixed(2) + "k"; - } else if (absY >= 1) { - return y.toFixed(2); - } else if (absY === 0) { - return y.toFixed(2); - } else if (absY < 1e-23) { - return (y / 1e-24).toFixed(2) + "y"; - } else if (absY < 1e-20) { - return (y / 1e-21).toFixed(2) + "z"; - } else if (absY < 1e-17) { - return (y / 1e-18).toFixed(2) + "a"; - } else if (absY < 1e-14) { - return (y / 1e-15).toFixed(2) + "f"; - } else if (absY < 1e-11) { - return (y / 1e-12).toFixed(2) + "p"; - } else if (absY < 1e-8) { - return (y / 1e-9).toFixed(2) + "n"; - } else if (absY < 1e-5) { - return (y / 1e-6).toFixed(2) + "ยต"; - } else if (absY < 1e-2) { - return (y / 1e-3).toFixed(2) + "m"; - } else if (absY <= 1) { - return y.toFixed(2); - } - throw Error("couldn't format a value, this is a bug"); -}; - export default Graph; diff --git a/web/ui/mantine-ui/src/pages/query/uplot.css b/web/ui/mantine-ui/src/pages/query/uplot.css new file mode 100644 index 0000000000..3e363cf802 --- /dev/null +++ b/web/ui/mantine-ui/src/pages/query/uplot.css @@ -0,0 +1,26 @@ +.uplot { + .u-legend { + text-align: left; + margin-left: 25px; + + .u-marker { + margin-right: 8px; + height: 0.8em; + width: 0.8em; + } + + th { + font-weight: 500; + font-size: var(--mantine-font-size-xs); + } + } + + .u-inline tr { + display: block; + /* display: table; + + * { + display: table-cell; + } */ + } +} diff --git a/web/ui/package-lock.json b/web/ui/package-lock.json index 63b93b5da9..70d5c1d9e4 100644 --- a/web/ui/package-lock.json +++ b/web/ui/package-lock.json @@ -127,13 +127,14 @@ "@types/lodash": "^4.17.7", "@uiw/react-codemirror": "^4.21.22", "dayjs": "^1.11.10", - "echarts": "^5.5.1", "lodash": "^4.17.21", "react": "^18.2.0", "react-dom": "^18.2.0", "react-infinite-scroll-component": "^6.1.0", "react-redux": "^9.1.0", - "react-router-dom": "^6.22.1" + "react-router-dom": "^6.22.1", + "uplot": "^1.6.30", + "uplot-react": "^1.2.2" }, "devDependencies": { "@types/react": "^18.2.55", @@ -3855,22 +3856,6 @@ "csstype": "^3.0.2" } }, - "node_modules/echarts": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.5.1.tgz", - "integrity": "sha512-Fce8upazaAXUVUVsjgV6mBnGuqgO+JNDlcgF79Dksy4+wgGpQB2lmYoO4TSweFg/mZITdpGHomw/cNBJZj1icA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "2.3.0", - "zrender": "5.6.0" - } - }, - "node_modules/echarts/node_modules/tslib": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", - "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", - "license": "0BSD" - }, "node_modules/electron-to-chromium": { "version": "1.4.678", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.678.tgz", @@ -7365,6 +7350,25 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uplot": { + "version": "1.6.30", + "resolved": "https://registry.npmjs.org/uplot/-/uplot-1.6.30.tgz", + "integrity": "sha512-48oVVRALM/128ttW19F2a2xobc2WfGdJ0VJFX00099CfqbCTuML7L2OrTKxNzeFP34eo1+yJbqFSoFAp2u28/Q==", + "license": "MIT" + }, + "node_modules/uplot-react": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/uplot-react/-/uplot-react-1.2.2.tgz", + "integrity": "sha512-fCe48HsE0sJmHVUs4TC49roTK3FYNXfCxA44g8pe20TMZ8GD3OT/mtXN/S0gJ8bYVOUcheOZ5u7f1Vw09JbTrw==", + "license": "MIT", + "engines": { + "node": ">=8.10" + }, + "peerDependencies": { + "react": ">=16.8.6", + "uplot": "^1.6.20" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -7693,21 +7697,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/zrender": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.0.tgz", - "integrity": "sha512-uzgraf4njmmHAbEUxMJ8Oxg+P3fT04O+9p7gY+wJRVxo8Ge+KmYv0WJev945EH4wFuc4OY2NLXz46FZrWS9xJg==", - "license": "BSD-3-Clause", - "dependencies": { - "tslib": "2.3.0" - } - }, - "node_modules/zrender/node_modules/tslib": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", - "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", - "license": "0BSD" - }, "react-app": { "name": "@prometheus-io/react-app", "version": "0.51.2",