import { FC, useEffect, useId } from "react"; 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 "./Graph.module.css"; import { GraphDisplayMode } from "../../state/queryPageSlice"; import { formatSeries } from "../../lib/formatSeries"; 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; endTime: number | null; range: number; resolution: number | null; showExemplars: boolean; displayMode: GraphDisplayMode; 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 = ({ expr, endTime, range, resolution, displayMode, retriggerIdx, }) => { const { ref, width, height } = useElementSize(); const realEndTime = (endTime !== null ? endTime : Date.now()) / 1000; const { data, error, isFetching, isLoading, refetch } = useAPIQuery({ key: useId(), path: "/query_range", params: { query: expr, step: ( resolution || Math.max(Math.floor(range / 250000), 1) ).toString(), start: (realEndTime - range / 1000).toString(), end: realEndTime.toString(), }, enabled: expr !== "", }); useEffect(() => { expr !== "" && refetch(); }, [retriggerIdx, refetch, expr, endTime, range, resolution]); // TODO: Share all the loading/error/empty data notices with the DataTable. // Show a skeleton only on the first load, not on subsequent ones. if (isLoading) { return ( {Array.from(Array(5), (_, i) => ( ))} ); } if (error) { return ( } > {error.message} ); } if (data === undefined) { return No data queried yet; } const { result, resultType } = data.data; if (resultType !== "matrix") { return ( } > This query returned a result of type "{resultType}", but a matrix was expected. ); } if (result.length === 0) { return ( }> This query returned no data. ); } // 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: { show: true, live: false, markers: { fill: ( self: uPlot, seriesIdx: number ): CSSStyleDeclaration["borderColor"] => { return colorPool[seriesIdx % colorPool.length]; }, }, }, 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], })), ], }; const seriesData: uPlot.AlignedData = [ result[0].values?.map((v) => v[0]), ...result.map((r) => r.values?.map((v) => parseFloat(v[1]))), ]; return ( , }} styles={{ loader: { width: "100%", height: "100%" } }} /> ); }; export default Graph;