mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-19 00:41:41 +00:00
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.
238 lines
5.8 KiB
Vue
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>
|