225 lines
4.7 KiB
Vue

<template>
<component
:is="showCard ? Card : 'div'"
:title="title"
:class="showCard ? 'h-80' : ''"
:loading="loading"
:stopOverflow="true"
>
<template #actions>
<slot name="actions"></slot>
</template>
<div
v-if="loading && !showCard"
class="flex h-full items-center justify-center"
>
<LoadingText />
</div>
<div
v-else-if="
error ||
!data.datasets.length ||
(data.datasets[0].length !== undefined && !data.datasets[0].length)
"
class="flex h-full items-center justify-center"
>
<ErrorMessage v-if="error" :message="error" />
<span v-else class="text-base text-gray-700">No data</span>
</div>
<VChart
v-else
autoresize
class="chart"
:option="options"
:init-options="initOptions"
/>
</component>
</template>
<script setup>
import Card from '../global/Card.vue';
import { ref, toRefs } from 'vue';
import { DateTime } from 'luxon';
import { use, graphic } from 'echarts/core';
import { SVGRenderer } from 'echarts/renderers';
import { LineChart } from 'echarts/charts';
import {
GridComponent,
LegendComponent,
TooltipComponent,
MarkLineComponent
} from 'echarts/components';
import VChart from 'vue-echarts';
import theme from '../../../tailwind.theme.json';
import { formatBytes, getUnit } from './utils';
const props = defineProps({
showCard: {
type: Boolean,
required: false,
default: () => true
},
title: {
type: String,
required: false
},
unit: {
type: String,
required: false,
default: () => ''
},
data: {
type: Object,
required: true,
default: () => ({ labels: [], datasets: [] })
},
type: {
type: String,
required: false,
default: () => 'category'
},
chartTheme: {
type: Array,
required: false,
default: () => [
theme.colors.red[500],
theme.colors.blue[500],
theme.colors.green[500],
theme.colors.purple[500],
theme.colors.yellow[500],
theme.colors.teal[500],
theme.colors.pink[500],
theme.colors.cyan[500]
]
},
loading: {
type: Boolean,
required: false,
default: () => false
},
error: {
type: Error,
required: false
}
});
const { title, unit, data, type, chartTheme } = toRefs(props);
use([
SVGRenderer,
GridComponent,
LegendComponent,
LineChart,
TooltipComponent,
MarkLineComponent
]);
const initOptions = {
renderer: 'svg'
};
const options = ref({
grid: {
top: 20,
left: 50,
right: 20,
bottom: data.value.datasets.length > 1 ? 60 : 30 // if there's legend show more space for it
},
tooltip: {
trigger: 'axis',
formatter: params => {
// for the dot to follow the same color as the line 🗿
let tooltip = `<p>${DateTime.fromSQL(
params[0].axisValueLabel
).toLocaleString(DateTime.DATETIME_MED)}</p>`;
params.forEach(({ value, seriesName }, i) => {
let colorSpan = color =>
'<span style="display:inline-block;margin-right:4px;border-radius:10px;width:10px;height:10px;background-color:' +
color +
'"></span>';
tooltip += `<p>${colorSpan(chartTheme.value[i])} ${getUnit(
value[1],
unit.value
)} ${unit.value !== seriesName ? `- ${seriesName}` : ''}</p>`;
});
return tooltip;
}
},
xAxis: {
type: type,
boundaryGap: false,
data: data.value.labels,
axisLine: {
show: false
},
axisTick: {
show: false
}
},
yAxis: {
type: 'value',
max: data.value.yMax,
axisLabel: {
formatter: value => {
if (unit.value === 'bytes') {
return formatBytes(value, 0);
} else {
if (value >= 1000000000) return `${value / 1000000000}B`;
else if (value >= 1000000) return `${value / 1000000}M`;
else if (value >= 1000) return `${value / 1000}K`;
return value;
}
}
}
},
labelLine: {
smooth: 0.2,
length: 10,
length2: 20
},
legend: {
top: 'bottom',
icon: 'circle',
show: data.value.datasets.length > 1
},
series: data.value.datasets.map((dataset, i) => {
return {
name: dataset.name || unit,
type: 'line',
stack: dataset.stack,
showSymbol: false,
data: dataset.dataset || dataset,
markLine: data.value.markLine,
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
},
lineStyle: {
color: chartTheme.value[i]
},
itemStyle: {
color: chartTheme.value[i]
},
areaStyle: {
color: new graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: chartTheme.value[i]
},
{
offset: 1,
color: '#fff'
}
]),
opacity: 0.3
}
};
})
});
</script>