Files
PhotonVision/photon-client/src/components/settings/MetricsChart.vue
Sam Freund 2372e110f9 TypeCheck Frontend (#2394)
We recently had an error that would've been caught by type checking in the frontend (see #2393). This PR implements type checking so that future errors will be caught.

Additionally, this PR contains miscellaneous frontend cleanup that's tangentially related to type-checking.
2026-05-05 15:24:19 +00:00

238 lines
5.8 KiB
Vue

<script setup lang="ts">
import { onMounted, onBeforeUnmount, watch, useTemplateRef } from "vue";
import { useTheme } from "vuetify";
// Color - original (adjusted)
// blue - 59, 130, 246 (r: 92, g: 154, b: 255)
// purple - 154, 100, 180 (r: 167, g: 104, b: 196)
// green - 65, 181, 127 (r: 75, g: 209, b: 147)
// red - 238, 102, 102 (r: 238, g: 102, b: 102)
const colors = {
"blue-light": { r: 255, g: 216, b: 67 },
"blue-dark": { r: 92, g: 154, b: 255 },
"purple-light": { r: 255, g: 216, b: 67 },
"purple-dark": { r: 167, g: 104, b: 196 },
"red-light": { r: 255, g: 216, b: 67 },
"red-dark": { r: 238, g: 102, b: 102 },
"green-light": { r: 255, g: 216, b: 67 },
"green-dark": { r: 75, g: 209, b: 147 }
};
const DEFAULT_COLOR = "blue";
const typeLabels = {
percentage: "%",
temperature: "°C",
mb: " Mb/s"
};
const theme = useTheme();
const chartRef = useTemplateRef("chartRef");
let chart: echarts.ECharts | null = null;
interface TooltipSeriesParam {
value: [number, number];
}
const getOptions = (data: ChartData[] = []) => {
const now = Date.now();
return {
title: {
show: false
},
tooltip: {
trigger: "axis",
formatter: (params: TooltipSeriesParam[]) => {
const p = params[0];
const append = typeLabels[props.type];
const fmsLimitLabel = "FMS Limit - 7.000 Mb/s";
// prettier-ignore
let tooltip = "<div style=\"text-align: right;\">";
const seriesData = `${new Date(p.value[0]).toLocaleTimeString([], { hour12: false })} - ${p.value[1].toFixed(props.type === "mb" ? 3 : 2)}${append}`;
if (props.type === "mb") {
if (p.value[1] >= 7) tooltip += seriesData + `<br/>${fmsLimitLabel}`;
else tooltip += fmsLimitLabel + `<br/>${seriesData}`;
} else tooltip += seriesData;
return `${tooltip}</div>`;
},
backgroundColor: theme.global.current.value.colors.background,
textStyle: {
color: theme.global.current.value.colors.onBackground
},
axisPointer: {
animation: false
}
},
grid: {
top: 0,
bottom: 10,
left: 0,
right: 50,
containLabel: false
},
xAxis: {
type: "time",
splitLine: {
show: true,
lineStyle: {
color: "#ffffff18"
}
},
splitNumber: 4,
min: now - 55 * 1000,
axisLine: {
lineStyle: {
color: theme.global.current.value.dark ? "#777" : "#aaa"
}
},
axisLabel: {
align: "left",
color: theme.global.current.value.dark ? "#ddd" : "#fff",
formatter: (value: number) => {
const date = new Date(value);
return date.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false
});
}
}
},
yAxis: {
type: "value",
position: "right",
min:
props.min ??
function (value: { min: number; max: number }) {
return Math.max(0, (value.min - 10) | 0);
},
max:
props.max ??
function (value: { min: number; max: number }) {
return (value.max + 10) | 0;
},
splitNumber: 2,
splitLine: {
show: true,
lineStyle: {
color: "#ffffff18"
}
},
axisLabel: {
color: theme.global.current.value.dark ? "#ddd" : "#fff"
}
},
series: getSeries(data),
animation: false
};
};
const getSeries = (data: ChartData[] = []) => {
const color = colors[`${props.color ?? DEFAULT_COLOR}-${theme.global.current.value.dark ? "dark" : "light"}`];
return [
{
type: "line",
showSymbol: false,
data: data.map((d) => [d.time, d.value]),
markLine:
props.type === "mb"
? {
symbol: "none",
lineStyle: {
color: "red",
width: 1
},
label: {
show: false
},
data: [{ yAxis: 7 }]
}
: null,
lineStyle: {
width: 1.5,
color: theme.global.current.value.dark
? `rgb(${color.r}, ${color.g}, ${color.b})`
: theme.global.current.value.colors.primary
},
areaStyle: {
color: {
type: "linear",
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: theme.global.current.value.dark
? `rgba(${color.r}, ${color.g}, ${color.b}, 0.15)`
: `${theme.global.current.value.colors.primary}40`
},
{
offset: 1,
color: theme.global.current.value.dark
? `rgba(${color.r}, ${color.g}, ${color.b}, 0.15)`
: `${theme.global.current.value.colors.primary}40`
}
]
}
}
}
];
};
interface ChartData {
time: number;
value: number;
}
// Type options: "percentage", "temperature", "mb"
const props = defineProps<{
data: ChartData[];
type: keyof typeof typeLabels;
min?: number;
max?: number;
color?: "red" | "green" | "blue" | "purple";
}>();
onMounted(async () => {
const echarts = await import("echarts");
chart = echarts.init(chartRef.value);
chart.setOption(getOptions(props.data));
window.addEventListener("resize", resizeChart);
});
onBeforeUnmount(() => {
window.removeEventListener("resize", resizeChart);
chart?.dispose();
});
function resizeChart() {
chart?.resize();
}
watch(
() => props.data,
(data) => {
chart?.setOption(getOptions(data));
},
{ deep: true }
);
</script>
<template>
<div ref="chartRef"></div>
</template>
<style scoped>
div {
width: calc(100% + 20px);
height: 100px;
margin-right: -20px;
}
</style>