Chart
A universal chart component. Inspired by chartJS etc. But is'nt as bloated.
Basic
Chart... bla la la
<script setup lang="ts">
import { ref } from 'vue'
import type { ChartData } from 'bragi-vue'
const labels = [
'2023-01-01T12:00:00+01:00',
'2023-01-01T12:00:05+01:00',
'2023-01-01T12:00:10+01:00',
'2023-01-01T12:00:15+01:00',
'2023-01-01T12:00:20+01:00',
'2023-01-01T12:00:25+01:00',
'2023-01-01T12:00:30+01:00',
'2023-01-01T12:00:35+01:00',
'2023-01-01T12:00:40+01:00',
'2023-01-01T12:00:45+01:00',
]
const data = ref<ChartData>({
labels,
datasets: [
{
data: [10, 4, 4, 5, 6, 7, 3, 13, 14, 3],
type: 'bar',
stroke: {
fill: 'red',
stroke: 'red',
rx: '3',
},
},
{ data: [10, 4, 4, 5, 6, 7, 3, 13, 14, 3] },
],
})
</script>
<template>
<div class="flex flex-col gap-4">
<BChart
:from="labels[0]"
:to="labels[labels.length - 1]"
:data="data"
:max="14"
hoverable
color="tertiary"
class="h-42 w-full"
/>
</div>
</template>
Axis
There are two types of y-axises. category
and time
<script setup lang="ts">
import { ref } from 'vue'
import type { ChartData } from 'bragi-vue'
const labels = [
'VPK',
'MP',
'S',
'C',
'FP',
'M',
'KD',
'SD',
]
const data = ref<ChartData>({
axis: 'category',
labels,
datasets: [
{
data: [11, 6, 32, 5, 4, 19, 6, 22],
type: 'bar',
stroke: {
fill: 'red',
stroke: 'red',
rx: '3',
},
},
{
data: [0, 50, 30, 13, 8, 25, 3, 33],
type: 'line',
stroke: {
'stroke': 'darkgray',
'stroke-dasharray': '5 5',
},
},
],
})
</script>
<template>
<div class="flex flex-col gap-4">
<BChart
:data="data"
:max="50"
hoverable
color="tertiary"
class="h-42 w-full"
/>
</div>
</template>
Tooltip slot
There is a slot for a tooltip with some usable slot props
<script setup lang="ts">
import { ref } from 'vue'
import type { ChartData } from 'bragi-vue'
const labels = [
'2023-01-01T12:00:00+01:00',
'2023-01-01T12:00:05+01:00',
'2023-01-01T12:00:10+01:00',
'2023-01-01T12:00:15+01:00',
'2023-01-01T12:00:20+01:00',
'2023-01-01T12:00:25+01:00',
'2023-01-01T12:00:30+01:00',
'2023-01-01T12:00:35+01:00',
'2023-01-01T12:00:40+01:00',
'2023-01-01T12:00:45+01:00',
]
const data = ref<ChartData>({
labels,
datasets: [
{ data: [0, 1, 0, 1, 2, 1, 0, 10, 11, 0], stroke: { stroke: 'red' } },
{ data: [10, 4, 4, 5, 6, 7, 3, 13, 14, 3], stroke: { stroke: 'green' } },
],
})
</script>
<template>
<div class="flex flex-col gap-4">
<BChart
:from="labels[0]"
:to="labels[labels.length - 1]"
:data="data"
:max="14"
hoverable
color="tertiary"
class="h-42 w-full"
>
<template
#tooltip="{ epoch, hoverIndex }"
>
<p class="text-xs flex justify-between gap-4">
<span>@{{ new Date(epoch).toLocaleTimeString('sv-SE') }}</span>
</p>
<ul class="max-w-md list-inside">
<li
v-for="(dataset, index) in data.datasets"
:key="index"
class="flex items-center"
>
<i
class="i-mdi:bell-ring mr-2"
:style="{ color: dataset.stroke.stroke }"
/>
{{ dataset.data[hoverIndex] }}
</li>
</ul>
</template>
</BChart>
</div>
</template>
Streaming
Streaming with prefetch
<script setup lang="ts">
import { ref } from 'vue'
import { useRafFn } from '@vueuse/core'
import type { ChartData } from 'bragi-vue'
const data = ref<ChartData>({
labels: new Array<string>(),
datasets: [
{
data: [],
stroke: {
'stroke': 'red',
'stroke-width': 1,
'fill': 'none',
},
},
{
data: [],
stroke: {
'stroke': 'blue',
'opacity': 0.5,
'stroke-width': 3,
'fill': 'none',
},
},
{
data: [],
stroke: {
'stroke': 'magenta',
'opacity': 0.5,
'stroke-width': 1,
'fill': 'none',
},
},
],
})
const leadTime = 500
const windowSize = ref('10000')
const from = ref()
const to = ref()
// reactive request animation frame
// slides the "window" and prefetches data when needed
let timeout = 300
let flip = 0
const { pause, resume } = useRafFn(({ delta, timestamp }) => {
const end = new Date()
end.setMilliseconds(end.getMilliseconds() - leadTime)
to.value = end
const start = new Date(end)
start.setMilliseconds(end.getMilliseconds() - Number(windowSize.value))
from.value = start
// check if we need to fetch more data
timeout = timeout - delta
if (timeout < 0) {
timeout = 300
// add sample points to streams
data.value.labels.push(new Date().toISOString())
const sample0 = (Math.cos(timestamp / 800) + 3) * 2
data.value.datasets[0].data.push(sample0)
const sample1 = (Math.sin(timestamp / 1000) + 1) * 3
data.value.datasets[1].data.push(sample1)
const sample2 = (flip++ % 2) * 14
data.value.datasets[2].data.push(sample2)
// prune to max 100 samples
if (data.value.datasets[0].data.length > 100) {
data.value.labels.shift()
data.value.datasets[0].data.shift()
data.value.datasets[1].data.shift()
data.value.datasets[2].data.shift()
}
}
})
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>
<BChart
:from="from"
:to="to"
:data="data"
:max="14"
hoverable
color="tertiary"
class="h-42 w-full"
>
<template
#tooltip="{ epoch, hoverIndex }"
>
<p class="text-xs flex justify-between gap-4">
<span>@{{ new Date(epoch).toLocaleTimeString('sv-SE') }}</span>
</p>
<ul class="max-w-md list-inside">
<li
v-for="(dataset, index) in data.datasets"
:key="index"
class="flex items-center"
>
<i
class="i-mdi:bell-ring mr-2"
:style="{ color: dataset.stroke.stroke }"
/>
{{ dataset.data[hoverIndex] }}
</li>
</ul>
</template>
</BChart>
Samples in buffer: {{ data.datasets[0].data?.length }}
</div>
</template>
Auto range
Auto range is a feature that will automatically adjust the x-axis range to fit the data. This is useful when you have a lot variance in the data and don't want to manually set the range. Enable it by setting the auto-range
prop. This will also generate a y-axis with values for now TODO.
<script setup lang="ts">
import { ref } from 'vue'
import { useRafFn } from '@vueuse/core'
import type { ChartData } from 'bragi-vue'
const data = ref<ChartData>({
labels: new Array<string>(),
datasets: [
{
data: [],
stroke: {
'stroke': 'red',
'stroke-width': 1,
'fill': 'none',
},
},
{
data: [],
stroke: {
'stroke': 'blue',
'opacity': 0.5,
'stroke-width': 3,
'fill': 'none',
},
},
],
})
const leadTime = 500
const windowSize = ref('5000')
const from = ref()
const to = ref()
// reactive request animation frame
// slides the "window" and prefetches data when needed
let timeout = 300
let flip = 0
const { pause, resume } = useRafFn(({ delta, timestamp }) => {
const end = new Date()
end.setMilliseconds(end.getMilliseconds() - leadTime)
to.value = end
const start = new Date(end)
start.setMilliseconds(end.getMilliseconds() - Number(windowSize.value))
from.value = start
// check if we need to fetch more data
timeout = timeout - delta
if (timeout < 0) {
timeout = 300
// add sample points to streams
data.value.labels.push(new Date().toISOString())
const sample0 = (flip++ % 2) * 80000
data.value.datasets[0].data.push(sample0)
const sample1 = (Math.sin(timestamp / 9000) + 1) * 3000000
data.value.datasets[1].data.push(sample1)
// prune to max 100 samples
if (data.value.datasets[0].data.length > 20) {
data.value.labels.shift()
data.value.datasets[0].data.shift()
data.value.datasets[1].data.shift()
}
}
})
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="5000">
5 s
</BSegmentButton>
<BSegmentButton value="30000">
30 s
</BSegmentButton>
<BSegmentButton value="60000">
60 s
</BSegmentButton>
</BSegmentGroup>
</div>
<BChart
:from="from"
:to="to"
:data="data"
auto-range
hoverable
color="tertiary"
class="h-42 w-full"
>
<template
#tooltip="{ epoch, hoverIndex }"
>
<p class="text-xs flex justify-between gap-4">
<span>@{{ new Date(epoch).toLocaleTimeString('sv-SE') }}</span>
</p>
<ul class="max-w-md list-inside">
<li
v-for="(dataset, index) in data.datasets"
:key="index"
class="flex items-center"
>
<i
class="i-mdi:bell-ring mr-2"
:style="{ color: dataset.stroke.stroke }"
/>
{{ dataset.data[hoverIndex] }}
</li>
</ul>
</template>
</BChart>
Samples in buffer: {{ data.datasets[0].data?.length }}
</div>
</template>
Stacked
Stacked charts are useful when you want to show the total of multiple series. For example when you want to show the total of a series of data and the total of a subset of that data. For example the total of all sales and the total of sales for a specific product.
<script setup lang="ts">
import { ref } from 'vue'
import type { ChartData } from 'bragi-vue'
const labels = [
'2023-01-01T12:00:00+01:00',
'2023-01-01T12:00:05+01:00',
'2023-01-01T12:00:10+01:00',
'2023-01-01T12:00:15+01:00',
'2023-01-01T12:00:20+01:00',
'2023-01-01T12:00:25+01:00',
'2023-01-01T12:00:30+01:00',
'2023-01-01T12:00:35+01:00',
'2023-01-01T12:00:40+01:00',
'2023-01-01T12:00:45+01:00',
]
const data = ref<ChartData>({
labels,
stacked: true,
datasets: [
{
data: [1, 1, 4, 5, 6, 7, 3, 26, 14, 3].map(v => v * 1e6),
stroke: {
fill: 'red',
stroke: 'transparent',
},
},
{
data: [2, 2, 2, 2, 2, 2, 2, 2, 2, 2].map(v => v * 1e6),
stroke: {
fill: 'green',
stroke: 'transparent',
},
},
{
data: [0, 0, 4, 5, 6, 0, 3, 13, 3, 3].map(v => v * 1e6),
stroke: {
fill: 'blue',
stroke: 'transparent',
},
},
],
})
</script>
<template>
<div class="flex flex-col gap-4">
<BChart
:from="labels[0]"
:to="labels[labels.length - 1]"
:data="data"
:max="14"
hoverable
auto-range
color="tertiary"
class="h-42 w-full"
/>
</div>
</template>