Timeline
Events over time kinda like iss does in stream monitoring
Basic
The timeline is fully responsive. What ever height & width you set will be respected. Gridlines (if shown) will be rendered ~every 200px
vue
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useTransition } from '@vueuse/core'
import { data } from './mock'
function easeOutElastic(n: number) {
return n === 0
? 0
: n === 1
? 1
: (2 ** (-10 * n)) * Math.sin((n * 10 - 0.75) * ((2 * Math.PI) / 3)) + 1
}
const selected = ref('600000')
const smoothed = useTransition(computed(() => Number(selected.value)), {
duration: 1500,
transition: easeOutElastic,
})
const from = computed(() => {
const fromEpoch = new Date(data.to).valueOf() - Number(smoothed.value)
return new Date(fromEpoch).toISOString()
})
</script>
<template>
<div class="flex flex-col gap-4">
<BSegmentGroup
v-model="selected"
class="self-end mb-4"
>
<BSegmentButton value="600000">
10 minutes
</BSegmentButton>
<BSegmentButton value="1800000">
30 minutes
</BSegmentButton>
<BSegmentButton value="3600000">
1 hour
</BSegmentButton>
</BSegmentGroup>
<BTimeline
hoverable
:from="from"
:to="data.to"
:baseline="data.pipes[0].state"
:events="data.pipes[0].events"
class="w-1/2 h-10"
/>
<BTimeline
hoverable
:from="from"
:to="data.to"
:baseline="data.pipes[0].state"
:events="data.pipes[0].events"
:traps="data.pipes[0].traps"
class="w-2/3 h-15"
/>
<BTimeline
hoverable
:from="from"
:to="data.to"
:baseline="data.pipes[0].state"
:events="data.pipes[0].events"
locale="en-US"
class="w-full h-30"
/>
</div>
</template>
Tooltip slot
You know. tooltip on hover thingy. Slot props available is epoch
(ms since 1970:ish), events
(array of events intersected by the pointer) and baseline
(current "state")
vue
<script setup lang="ts">
import { computed, ref } from 'vue'
import { TransitionPresets, useTransition } from '@vueuse/core'
import { data } from './mock'
const selected = ref('600000')
const smoothed = useTransition(computed(() => Number(selected.value)), {
duration: 500,
transition: TransitionPresets.easeInOutQuad,
})
const from = computed(() => {
const fromEpoch = new Date(data.to).valueOf() - Number(smoothed.value)
return new Date(fromEpoch).toISOString()
})
</script>
<template>
<div class="flex flex-col gap-4">
<BSegmentGroup
v-model="selected"
class="self-end mb-4"
>
<BSegmentButton value="600000">
10 minutes
</BSegmentButton>
<BSegmentButton value="1800000">
30 minutes
</BSegmentButton>
<BSegmentButton value="3600000">
1 hour
</BSegmentButton>
</BSegmentGroup>
<BTimeline
hoverable
:from="from"
:to="data.to"
:baseline="data.pipes[0].state"
:events="data.pipes[0].events"
:traps="data.pipes[0].traps"
color="surface"
locale="sv-SE"
class="h-42"
>
<template
#tooltip="{ events, epoch, traps, baseline }"
>
<p class="text-xs flex justify-between gap-4">
<span>@{{ new Date(epoch).toLocaleTimeString('sv-SE') }}</span>
<span>{{ baseline?.type ?? 'inactive' }}</span>
</p>
<ul class="max-w-md list-inside">
<li
v-for="event in events"
:key="event.x"
class="flex items-center"
>
<i
class="i-mdi:bell-ring mr-2"
:class="event.severity ? 'text-error' : ''"
/>
{{ event.message }}
</li>
</ul>
<ul class="max-w-md list-inside">
<li
v-for="trap in traps"
:key="trap.x"
class="flex items-center"
>
<i
class="i-mdi:play-circle mr-2"
/>
{{ trap.type }}
</li>
</ul>
</template>
</BTimeline>
</div>
</template>
Events
Use the pick event handler on the timeline component to get all the information from the intersected events. Try clicking on the timeline
vue
<script setup lang="ts">
import { ref } from 'vue'
import { data } from './mock'
const isDialogShown = ref(false)
const clickedEvent = ref<{ traps: any[]; events: any[]; base: any }>()
function onEventPicked(event: any) {
clickedEvent.value = event
isDialogShown.value = true
}
</script>
<template>
<div class="flex flex-col gap-4">
<BTimeline
hoverable
:from="data.from"
:to="data.to"
:baseline="data.pipes[0].state"
:events="data.pipes[0].events"
:traps="data.pipes[0].traps"
color="surface"
locale="sv-SE"
class="h-42"
@pick="onEventPicked"
>
<template
#tooltip="{ events, epoch, traps, baseline }"
>
<p class="text-xs flex justify-between gap-4">
<span>@{{ new Date(epoch).toLocaleTimeString('sv-SE') }}</span>
<span>{{ baseline?.type ?? 'inactive' }}</span>
</p>
<ul class="max-w-md list-inside">
<li
v-for="event in events"
:key="event.x"
class="flex items-center"
>
<i
class="i-mdi:bell-ring mr-2"
:class="event.severity ? 'text-error' : ''"
/>
{{ event.message }}
</li>
</ul>
<ul class="max-w-md list-inside">
<li
v-for="trap in traps"
:key="trap.x"
class="flex items-center"
>
<i
class="i-mdi:play-circle mr-2"
/>
{{ trap.type }}
</li>
</ul>
</template>
</BTimeline>
<BDialog
v-model="isDialogShown"
title="What you got!"
description="This is the event data i found @ this time"
>
<div v-if="clickedEvent?.events?.length">
<p class="text-bold text-surface-on">
Events:
</p>
<pre class="text-xs">{{ JSON.stringify(clickedEvent.events, null, 2) }}</pre>
</div>
<div v-if="clickedEvent?.traps?.length">
<p class="text-bold text-surface-on">
Traps:
</p>
<pre class="text-xs">{{ JSON.stringify(clickedEvent.traps, null, 2) }}</pre>
</div>
<div v-if="clickedEvent?.base">
<p class="text-bold text-surface-on">
Base:
</p>
<pre class="text-xs">{{ JSON.stringify(clickedEvent.base, null, 2) }}</pre>
</div>
<template #actions>
<BBtn
variant="text"
@click="isDialogShown = false"
>
Close
</BBtn>
</template>
</BDialog>
</div>
</template>
Streaming
By prefetching data and making a sliding time window one can with a bit of code make a pretty neat realtime streaming timeline. In this case its also useful to set the tick-style
prop to "relative" to get relative time ticks
vue
<script setup lang="ts">
import { ref } from 'vue'
import { useRafFn } from '@vueuse/core'
import { fetchMockTimelineData } from './mock'
let trapBuffer: any = []
let stateBuffer: any = []
let eventBuffer: any = []
const leadTime = 5000
const windowSize = ref('10000')
const trapStream = ref()
const stateStream = ref()
const eventStream = ref()
const from = ref()
const to = ref()
// reactive request animation frame
// slides the "window" and prefetches data when needed
let timeout = 4000
const { pause, resume } = useRafFn(({ delta }) => {
const end = new Date()
end.setMilliseconds(end.getMilliseconds() - leadTime)
to.value = end
const start = new Date(end)
start.setMilliseconds(end.getMilliseconds() - windowSize.value)
from.value = start
// check if we need to fetch more data
timeout = timeout - delta
if (timeout < 0) {
timeout = 4000
fillBuffer()
}
})
let lastToDate: Date
function fillBuffer() {
const toDate = new Date()
let fromDate = new Date(toDate)
if (lastToDate)
fromDate = lastToDate
else
fromDate.setMilliseconds(toDate.getMilliseconds() - 4000)
const data = fetchMockTimelineData(fromDate, toDate)
lastToDate = toDate
trapBuffer = pruneBuffer([...trapBuffer, ...data.pipes[0].traps], from.value.valueOf())
stateBuffer = pruneBuffer([...stateBuffer, ...data.pipes[0].state], from.value.valueOf())
eventBuffer = pruneBuffer([...eventBuffer, ...data.pipes[0].events], from.value.valueOf())
trapStream.value = trapBuffer
stateStream.value = stateBuffer
eventStream.value = eventBuffer
}
function pruneBuffer(buffer: any[], maxAge: number) {
return buffer.filter(event => new Date(event.at ?? event.to).valueOf() > maxAge)
}
const running = ref(true)
function toggle() {
running.value ? pause() : resume()
running.value = !running.value
}
</script>
<template>
<div class="flex flex-col gap-4">
<div class="flex justify-between mb-4">
<BBtn @click="toggle">
{{ running ? 'Stop' : 'Resume' }}
</BBtn>
<BSegmentGroup v-model="windowSize">
<BSegmentButton value="10000">
10 s
</BSegmentButton>
<BSegmentButton value="30000">
30 s
</BSegmentButton>
<BSegmentButton value="60000">
60 s
</BSegmentButton>
</BSegmentGroup>
</div>
<BTimeline
hoverable
:from="from"
:to="to"
:baseline="stateStream"
:events="eventStream"
:traps="trapStream"
color="surface"
locale="sv-SE"
tick-style="relative"
class="h-42"
>
<template
#tooltip="{ events, epoch, traps, baseline }"
>
<p class="text-xs flex justify-between gap-4">
<span>@{{ new Date(epoch).toLocaleTimeString('sv-SE') }}</span>
<span>{{ baseline?.type ?? 'inactive' }}</span>
</p>
<ul class="max-w-md list-inside">
<li
v-for="event in events"
:key="event.x"
class="flex items-center"
>
<i
class="i-mdi:bell-ring mr-2"
:class="event.severity ? 'text-error' : ''"
/>
{{ event.message }}
</li>
</ul>
<ul class="max-w-md list-inside">
<li
v-for="trap in traps"
:key="trap.x"
class="flex items-center"
>
<i
class="i-mdi:play-circle mr-2"
/>
{{ trap.type }}
</li>
</ul>
</template>
</BTimeline>
<BTimeline
:from="from"
:to="to"
:baseline="stateStream"
:events="eventStream"
:traps="trapStream"
color="surface"
locale="sv-SE"
tick-style="relative"
class="h-20"
/>
<BTimeline
:from="from"
:to="to"
:baseline="stateStream"
:events="eventStream"
:traps="trapStream"
color="surface"
locale="sv-SE"
tick-style="relative"
class="h-15"
/>
</div>
</template>