Initial uPlot work in Mantine UI

Signed-off-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
Julius Volz 2024-07-21 18:26:11 +02:00
parent d9520b1a79
commit 1c91b82206
10 changed files with 1585 additions and 1139 deletions

View file

@ -16,6 +16,7 @@
"@codemirror/lint": "^6.5.0",
"@codemirror/state": "^6.4.0",
"@codemirror/view": "^6.24.0",
"@floating-ui/dom": "^1.6.7",
"@lezer/common": "^1.2.1",
"@lezer/highlight": "^1.2.0",
"@mantine/code-highlight": "^7.6.1",

View file

@ -0,0 +1,935 @@
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",
];

View file

@ -107,6 +107,10 @@ export class HistoryCompleteStrategy implements CompleteStrategy {
}
}
// This is just a placeholder until query history is implemented, so disable the linter warning.
// eslint-disable-next-line react-hooks/exhaustive-deps
const queryHistory = [] as string[];
interface ExpressionInputProps {
initialExpr: string;
metricNames: string[];
@ -166,10 +170,6 @@ const ExpressionInput: FC<ExpressionInputProps> = ({
}
}, [formatResult, formatError]);
// This is just a placeholder until query history is implemented, so disable the linter warning.
// eslint-disable-next-line react-hooks/exhaustive-deps
const queryHistory = [] as string[];
// (Re)initialize editor based on settings / setting changes.
useEffect(() => {
// Build the dynamic part of the config.
@ -205,7 +205,7 @@ const ExpressionInput: FC<ExpressionInputProps> = ({
size="lg"
variant="transparent"
color="gray"
aria-label="Decrease range"
aria-label="Show query options"
>
<IconDotsVertical style={{ width: "1rem", height: "1rem" }} />
</ActionIcon>

File diff suppressed because it is too large Load diff

View file

@ -7,6 +7,7 @@ import {
Input,
SegmentedControl,
Stack,
Select,
} from "@mantine/core";
import {
IconChartAreaFilled,
@ -122,7 +123,53 @@ const QueryPanel: FC<PanelProps> = ({ idx, metricNames }) => {
}
/>
<Input value="" placeholder="Res. (s)" style={{ width: 80 }} />
<Select
placeholder="Resolution"
maxDropdownHeight={500}
data={[
{
group: "Automatic resolution",
items: [
{ label: "Low", value: "low" },
{ label: "Medium", value: "medium" },
{ label: "High", value: "high" },
],
},
{
group: "Fixed resolution",
items: [
{ label: "10s", value: "10" },
{ label: "30s", value: "30" },
{ label: "1m", value: "60" },
{ label: "5m", value: "300" },
{ label: "15m", value: "900" },
{ label: "1h", value: "3600" },
],
},
{
group: "Custom resolution",
items: [{ label: "Enter value", value: "custom" }],
},
]}
w={160}
// value={value ? value.value : null}
onChange={(_value, option) => {
dispatch(
setVisualizer({
idx,
visualizer: {
...panel.visualizer,
resolution: option
? option.value
? parseInt(option.value)
: null
: null,
},
})
);
}}
clearable
/>
</Group>
<SegmentedControl
@ -211,6 +258,18 @@ const QueryPanel: FC<PanelProps> = ({ idx, metricNames }) => {
showExemplars={panel.visualizer.showExemplars}
displayMode={panel.visualizer.displayMode}
retriggerIdx={retriggerIdx}
onSelectRange={(start: number, end: number) =>
dispatch(
setVisualizer({
idx,
visualizer: {
...panel.visualizer,
range: (end - start) * 1000,
endTime: end * 1000,
},
})
)
}
/>
</Tabs.Panel>
</Tabs>

View file

@ -1,4 +1,4 @@
import { FC, useState } from "react";
import { FC, useEffect, useState } from "react";
import { ActionIcon, Group, Input } from "@mantine/core";
import { IconMinus, IconPlus } from "@tabler/icons-react";
import {
@ -43,6 +43,10 @@ const RangeInput: FC<RangeInputProps> = ({ range, onChangeRange }) => {
formatPrometheusDuration(range)
);
useEffect(() => {
setRangeInput(formatPrometheusDuration(range));
}, [range]);
const onChangeRangeInput = (rangeText: string): void => {
const newRange = parsePrometheusDuration(rangeText);
if (newRange === null) {

View file

@ -0,0 +1,429 @@
import { FC, useEffect, useId, useState } from "react";
import { Alert, Skeleton, Box, LoadingOverlay } from "@mantine/core";
import { IconAlertTriangle, IconInfoCircle } from "@tabler/icons-react";
import {
InstantQueryResult,
RangeSamples,
} 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, { Series } from "uplot";
import UplotReact from "uplot-react";
import "uplot/dist/uPlot.min.css";
import "./uplot.css";
import { useElementSize } from "@mantine/hooks";
import { formatTimestamp } from "../../lib/formatTime";
import { computePosition, shift, flip, offset, Axis } from "@floating-ui/dom";
import { colorPool } from "./ColorPool";
const formatYAxisTickValue = (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");
};
const escapeHTML = (str: string): string => {
const entityMap: { [key: string]: string } = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;",
"/": "&#x2F;",
};
return String(str).replace(/[&<>"'/]/g, function (s) {
return entityMap[s];
});
};
const formatLabels = (labels: { [key: string]: string }): string => `
<div class="labels">
${Object.keys(labels).length === 0 ? '<div class="no-labels">no labels</div>' : ""}
${labels["__name__"] ? `<div><strong>${labels["__name__"]}</strong></div>` : ""}
${Object.keys(labels)
.filter((k) => k !== "__name__")
.map(
(k) =>
`<div><strong>${k}</strong>: ${escapeHTML(labels[k])}</div>`
)
.join("")}
</div>`;
const tooltipPlugin = () => {
let over: HTMLDivElement;
let boundingLeft: number;
let boundingTop: number;
let selectedSeriesIdx: number | null = null;
const overlay = document.createElement("div");
overlay.className = "u-tooltip";
overlay.style.display = "none";
return {
hooks: {
// Set up event handlers and append overlay.
init: (u: uPlot) => {
over = u.over;
over.addEventListener("mouseenter", () => {
overlay.style.display = "block";
});
over.addEventListener("mouseleave", () => {
overlay.style.display = "none";
});
document.body.appendChild(overlay);
},
// When the chart is destroyed, remove the overlay from the DOM.
destroy: () => {
overlay.remove();
},
// When the chart is resized, store the bounding box of the overlay.
setSize: () => {
const bbox = over.getBoundingClientRect();
boundingLeft = bbox.left;
boundingTop = bbox.top;
},
// When a series is selected by hovering close to it, store the
// index of the selected series.
setSeries: (self: uPlot, seriesIdx: number | null, opts: Series) => {
selectedSeriesIdx = seriesIdx;
},
// When the cursor is moved, update the tooltip with the current
// series value and position it near the cursor.
setCursor: (u: uPlot) => {
const { left, top, idx } = u.cursor;
if (
idx === null ||
idx === undefined ||
left === null ||
left === undefined ||
top === null ||
top === undefined ||
selectedSeriesIdx === null
) {
return;
}
const ts = u.data[0][idx];
const value = u.data[selectedSeriesIdx][idx];
const series = u.series[selectedSeriesIdx];
const labels = series.labels;
const color = series.stroke();
const x = left + boundingLeft;
const y = top + boundingTop;
// overlay.style.borderColor = color;
// TODO: Use local time in formatTimestamp!
overlay.innerHTML = `
<div class="date">${formatTimestamp(ts, false)}</div>
<div class="series-value">
<span class="detail-swatch" style="background-color: ${color}"></span>
<span>${labels.__name__ ? labels.__name__ + ": " : " "}<strong>${value}</strong></span>
</div>
${formatLabels(labels)}
`.trimEnd();
const virtualEl = {
getBoundingClientRect() {
return {
width: 0,
height: 0,
x: x,
y: y,
left: x,
right: x,
top: y,
bottom: y,
};
},
};
computePosition(virtualEl, overlay, {
placement: "right-start",
middleware: [offset(5), flip(), shift()],
}).then(({ x, y }) => {
Object.assign(overlay.style, {
top: `${y}px`,
left: `${x}px`,
});
});
},
},
};
};
// A helper function to automatically create enough space for the Y axis
// ticket labels depending on their length.
const autoPadLeft = (
self: uPlot,
values: string[],
axisIdx: number,
cycleNum: number
) => {
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);
};
const getOptions = (
width: number,
result: RangeSamples[],
onSelectRange: (start: number, end: number) => void
): uPlot.Options => ({
width: width - 30,
height: 550,
// padding: [null, autoPadRight, null, null],
cursor: {
focus: {
prox: 1000,
},
// Whether dragging on the chart should select a zoom area.
drag: {
x: true,
// Don't zoom into the existing via uPlot, we want to load new (finger-grained) data instead.
setScale: false,
},
},
plugins: [tooltipPlugin()],
legend: {
show: true,
live: false,
markers: {
fill: (
self: uPlot,
seriesIdx: number
): CSSStyleDeclaration["borderColor"] => {
return colorPool[seriesIdx % colorPool.length];
},
},
},
// @ts-expect-error - uPlot enum types don't work across module boundaries,
// see https://github.com/leeoniya/uPlot/issues/973.
drawOrder: ["series", "axes"],
focus: {
alpha: 0.4,
},
axes: [
// X axis (time).
{
labelSize: 20,
stroke: "#333",
grid: {
show: false,
stroke: "#eee",
width: 2,
dash: [],
},
},
// Y axis (sample value).
{
values: (u: uPlot, splits: number[]) => splits.map(formatYAxisTickValue),
border: {
show: true,
stroke: "#333",
width: 1,
},
labelGap: 8,
labelSize: 8 + 12 + 8,
stroke: "#333",
size: autoPadLeft,
},
],
series: [
...result.map((r, idx) => ({
label: formatSeries(r.metric),
width: 2,
labels: r.metric,
stroke: colorPool[idx % colorPool.length],
})),
],
hooks: {
setSelect: [
(self: uPlot) => {
onSelectRange(
self.posToVal(self.select.left, "x"),
self.posToVal(self.select.left + self.select.width, "x")
);
},
],
},
});
export const normalizeData = (
inputData: RangeSamples[],
startTime: number,
endTime: number,
resolution: number
): uPlot.AlignedData => {
const timeData: (number | null)[][] = [];
timeData[0] = [];
for (let t = startTime; t <= endTime; t += resolution) {
timeData[0].push(t);
}
return timeData.concat(
inputData.map(({ values, histograms }) => {
// Insert nulls for all missing steps.
const data: (number | null)[] = [];
let valuePos = 0;
let histogramPos = 0;
for (let t = startTime; t <= endTime; t += resolution) {
// Allow for floating point inaccuracy.
const currentValue = values && values[valuePos];
const currentHistogram = histograms && histograms[histogramPos];
if (
currentValue &&
values.length > valuePos &&
currentValue[0] < t + resolution / 100
) {
data.push(parseValue(currentValue[1]));
valuePos++;
} else if (
currentHistogram &&
histograms.length > histogramPos &&
currentHistogram[0] < t + resolution / 100
) {
data.push(parseValue(currentHistogram[1].sum));
histogramPos++;
} else {
data.push(null);
}
}
return data;
})
);
};
const parseValue = (value: string): null | number => {
const val = parseFloat(value);
// "+Inf", "-Inf", "+Inf" will be parsed into NaN by parseFloat(). They
// can't be graphed, so show them as gaps (null).
return isNaN(val) ? null : val;
};
export interface UPlotChartRange {
startTime: number;
endTime: number;
resolution: number;
}
export interface UPlotChartProps {
data: RangeSamples[];
range: UPlotChartRange;
width: number;
showExemplars: boolean;
displayMode: GraphDisplayMode;
onSelectRange: (start: number, end: number) => void;
}
const UPlotChart: FC<UPlotChartProps> = ({
data,
range: { startTime, endTime, resolution },
displayMode,
width,
onSelectRange,
}) => {
const [options, setOptions] = useState<uPlot.Options | null>(null);
useEffect(() => {
if (width === 0) {
return;
}
setOptions(getOptions(width, data, onSelectRange));
}, [width, data, onSelectRange]);
const seriesData: uPlot.AlignedData = normalizeData(
data,
startTime,
endTime,
resolution
);
// data[0].values?.map((v) => v[0]),
// ...data.map((r) => r.values?.map((v) => parseFloat(v[1]))),
// ...normalizeData(data, startTime, endTime, resolution),
// ];
if (options === null) {
return;
}
return (
<UplotReact
options={options}
data={seriesData}
className={classes.uplotChart}
/>
);
};
export default UPlotChart;

View file

@ -24,3 +24,75 @@
} */
}
}
.uplot {
display: inline-block;
vertical-align: top;
width: min-content;
}
.u-over {
box-shadow: 0px 0px 0px 0.5px #ccc;
cursor: crosshair;
}
.u-legend {
text-align: left;
margin-top: 20px;
padding-left: 20px;
}
.u-inline tr {
margin-right: 8px;
}
.u-label {
font-size: 12px;
}
.u-tooltip {
font-size: 0.8em;
white-space: nowrap;
/* background: var(--mantine-color-gray-7);
color: var(--mantine-color-gray-1); */
/* background: rgba(0, 0, 0, 0.8); */
/* color: #fff; */
background: rgba(255, 255, 255, 0.95);
border: 2px solid var(--mantine-color-gray-6);
color: var(--mantine-color-gray-9);
border-radius: 4px;
position: absolute;
padding: 0.8em 1.5em;
margin: 0.75rem;
z-index: 10;
pointer-events: none;
.series-value {
margin: 5px 0;
}
.series-label {
margin-top: 0.5em;
margin-bottom: 0.3em;
font-weight: bold;
}
.labels {
font-size: 0.9em;
line-height: 1.3em;
div {
margin-bottom: 0.2em;
}
.no-labels {
font-style: italic;
}
}
.detail-swatch {
display: inline-block;
width: 10px;
height: 10px;
}
}

View file

@ -7,10 +7,14 @@ export default defineConfig({
server: {
proxy: {
"/api": {
target: "http://localhost:9090",
target: "https://demo.promlabs.com",
changeOrigin: true,
secure: false,
},
"/-/": {
target: "http://localhost:9090",
target: "https://demo.promlabs.com",
changeOrigin: true,
secure: false,
},
},
},

View file

@ -113,6 +113,7 @@
"@codemirror/lint": "^6.5.0",
"@codemirror/state": "^6.4.0",
"@codemirror/view": "^6.24.0",
"@floating-ui/dom": "^1.6.7",
"@lezer/common": "^1.2.1",
"@lezer/highlight": "^1.2.0",
"@mantine/code-highlight": "^7.6.1",
@ -1647,12 +1648,13 @@
}
},
"node_modules/@floating-ui/dom": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.3.tgz",
"integrity": "sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==",
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.7.tgz",
"integrity": "sha512-wmVfPG5o2xnKDU4jx/m4w5qva9FWHcnZ8BvzEe90D/RpwsJaTAVYPEPdQ8sbr/N8zZTAHlZUTQdqg8ZUbzHmng==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.0.0",
"@floating-ui/utils": "^0.2.0"
"@floating-ui/core": "^1.6.0",
"@floating-ui/utils": "^0.2.4"
}
},
"node_modules/@floating-ui/react": {
@ -1682,9 +1684,10 @@
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz",
"integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q=="
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.4.tgz",
"integrity": "sha512-dWO2pw8hhi+WrXq1YJy2yCuWoL20PddgGaqTgVe4cOS9Q6qklXCiA1tJEqX6BEwRNSCP84/afac9hd4MS+zEUA==",
"license": "MIT"
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.14",