phase 4
This commit is contained in:
@@ -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'
|
||||
|
||||
101
ui/framework/src/renderers/TimeSeriesRenderer.vue
Normal file
101
ui/framework/src/renderers/TimeSeriesRenderer.vue
Normal 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>
|
||||
Reference in New Issue
Block a user