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",