This commit is contained in:
2026-03-23 15:18:23 -03:00
parent 5ed876d694
commit b57da622cb
17 changed files with 554 additions and 103 deletions

View File

@@ -11,7 +11,8 @@
},
"dependencies": {
"vue": "^3.5",
"pinia": "^2.2"
"pinia": "^2.2",
"uplot": "^1.6"
},
"devDependencies": {
"typescript": "^5.6",

View File

@@ -11,6 +11,9 @@ importers:
pinia:
specifier: ^2.2
version: 2.3.1(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3))
uplot:
specifier: ^1.6
version: 1.6.32
vue:
specifier: ^3.5
version: 3.5.30(typescript@5.9.3)
@@ -748,6 +751,9 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
uplot@1.6.32:
resolution: {integrity: sha512-KIMVnG68zvu5XXUbC4LQEPnhwOxBuLyW1AHtpm6IKTXImkbLgkMy+jabjLgSLMasNuGGzQm/ep3tOkyTxpiQIw==}
vite-node@2.1.9:
resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==}
engines: {node: ^18.0.0 || >=20.0.0}
@@ -1460,6 +1466,8 @@ snapshots:
typescript@5.9.3: {}
uplot@1.6.32: {}
vite-node@2.1.9:
dependencies:
cac: 6.7.14

View File

@@ -10,3 +10,4 @@ export { default as LayoutGrid } from './components/LayoutGrid.vue'
// Renderers
export { default as LogRenderer } from './renderers/LogRenderer.vue'
export { default as TimeSeriesRenderer } from './renderers/TimeSeriesRenderer.vue'

View File

@@ -0,0 +1,101 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
import uPlot from 'uplot'
import 'uplot/dist/uPlot.min.css'
export interface TimeSeriesSeries {
label: string
color: string
}
const props = withDefaults(defineProps<{
/** Array of series configs (label + color) */
series: TimeSeriesSeries[]
/** Data: [timestamps[], series1[], series2[], ...] */
data: uPlot.AlignedData
/** Chart title (optional) */
title?: string
/** Stacked area mode */
stacked?: boolean
}>(), {
stacked: false,
})
const container = ref<HTMLElement | null>(null)
let chart: uPlot | null = null
function buildOpts(): uPlot.Options {
const seriesOpts: uPlot.Series[] = [
{ label: 'Time' },
...props.series.map((s) => ({
label: s.label,
stroke: s.color,
fill: props.stacked ? s.color + '40' : undefined,
width: 2,
})),
]
return {
width: container.value?.clientWidth ?? 400,
height: container.value?.clientHeight ?? 200,
series: seriesOpts,
axes: [
{ stroke: '#555568', grid: { stroke: '#2e2e3822' } },
{ stroke: '#555568', grid: { stroke: '#2e2e3822' } },
],
cursor: { show: true },
legend: { show: true },
}
}
function createChart() {
if (!container.value) return
if (chart) chart.destroy()
chart = new uPlot(buildOpts(), props.data, container.value)
}
function resize() {
if (!chart || !container.value) return
chart.setSize({
width: container.value.clientWidth,
height: container.value.clientHeight,
})
}
watch(() => props.data, (newData) => {
if (chart) {
chart.setData(newData)
} else {
nextTick(createChart)
}
}, { deep: true })
onMounted(() => {
nextTick(createChart)
const observer = new ResizeObserver(resize)
if (container.value) observer.observe(container.value)
onUnmounted(() => {
observer.disconnect()
chart?.destroy()
chart = null
})
})
</script>
<template>
<div ref="container" class="timeseries-renderer" />
</template>
<style scoped>
.timeseries-renderer {
width: 100%;
height: 100%;
min-height: 150px;
}
.timeseries-renderer :deep(.u-legend) {
font-family: var(--font-mono);
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
</style>