Files
lucia-frontend/src/views/Compare/Dashboard/index.vue
2024-02-20 12:44:30 +08:00

670 lines
25 KiB
Vue

<template>
<main class="h-screen-main relative">
<div class="h-full relative bg-neutral-50">
<div class="flex justify-start items-start p-4 h-full">
<!-- tag -->
<aside class="border-r border-neutral-300 pr-4 mr-4 h-full sticky top-0 self-start">
<section class="px-2 space-y-2 w-56 h-full">
<div>
<p class="h2 text-base">Time Usage</p>
<ul class="list-disc list-inside text-sm leading-5 pl-3">
<li v-for="(item, index) in timeUsageData" :key="index" :class="{active: isActive === item.tagId}" @click="isActive = item.tagId" class="cursor-pointer hover:text-primary"><a :href="item.tagId">{{ item.label }}</a></li>
</ul>
</div>
<div>
<p class="h2 text-base">Frequency</p>
<ul class="list-disc list-inside text-sm leading-5 pl-3">
<li v-for="(item, index) in frequencyData" :key="index" :class="{active: isActive === item.tagId}" @click="isActive = item.tagId" class="cursor-pointer hover:text-primary"><a :href="item.tagId">{{ item.label }}</a></li>
</ul>
</div>
</section>
</aside>
<!-- graph -->
<article class="w-full h-full overflow-x-hidden overflow-y-auto scrollbar scroll-smooth">
<section>
<p class="h2 px-4 border-b border-neutral-900"><span class="material-symbols-outlined mr-2 align-middle">schedule</span>Time Usage</p>
<ul class="list-disc list-inside px-4 pl-7">
<li id="cycleTime" class="scroll-smooth">
<span class="inline-block py-4 text-sm">Cycle Efficiency</span>
<ul>
<li class="bg-neutral-10 p-1 border border-neutral-300 rounded">
<span class="block font-bold text-sm leading-loose text-center my-2">{{ contentData.avgCycleEfficiency.title }}</span>
<Chart type="bar" :data="avgCycleEfficiencyData" :options="avgCycleEfficiencyOptions" class="h-96" />
</li>
</ul>
</li>
<li id="processingTime">
<span class="inline-block py-4 text-sm">Processing Time</span>
<ul>
<li class="bg-neutral-10 mb-4 p-1 border border-neutral-300 rounded">
<span class="block font-bold text-sm leading-loose text-center my-2">{{ contentData.avgProcessTime.title }}</span>
<Chart type="line" :data="avgProcessTimeData" :options="avgProcessTimeOptions" class="h-96" />
</li>
<li class="bg-neutral-10 p-1 border border-neutral-300 rounded">
<span class="block font-bold text-sm leading-loose text-center my-2">{{ contentData.avgProcessTimeByTask.title }}</span>
<Chart type="bar" :data="avgProcessTimeByTaskData" :options="avgProcessTimeByTaskOptions" class="h-[500px]" :style="{ height: avgProcessTimeByTaskHeight }"/>
</li>
</ul>
</li>
<li id="waitingTime">
<span class="inline-block py-4 text-sm">Waiting Time</span>
<ul>
<li class="bg-neutral-10 mb-4 p-1 border border-neutral-300 rounded">
<span class="block font-bold text-sm leading-loose text-center my-2">{{ contentData.avgWaitingTime.title }}</span>
<Chart type="line" :data="avgWaitingTimeData" :options="avgWaitingTimeOptions" class="h-96" />
</li>
<li class="bg-neutral-10 p-1 border border-neutral-300 rounded">
<span class="block font-bold text-sm leading-loose text-center my-2">{{ contentData.avgWaitingTimeByEdge.title }}</span>
<Chart type="bar" :data="avgWaitingTimeByEdgeData" :options="avgWaitingTimeByEdgeOptions" :style="{ height: avgWaitingTimeByEdgeHeight }" class="h-[500px]" />
</li>
</ul>
</li>
</ul>
</section>
<section>
<p class="h2 px-4 border-b border-neutral-900 mt-2"><span class="material-symbols-outlined mr-2 align-middle">moving</span>Frequency</p>
<ul class="list-disc list-inside px-4 pl-7">
<li id="cases">
<span class="inline-block py-4 text-sm">Number of Cases</span>
<ul>
<li class="bg-neutral-10 mb-4 p-1 border border-neutral-300 rounded">
<span class="block font-bold text-sm leading-loose text-center my-2">{{ contentData.freq.title }}</span>
<Chart type="line" :data="freqData" :options="freqOptions" class="h-96" />
</li>
<li class="bg-neutral-10 p-1 border border-neutral-300 rounded">
<span class="block font-bold text-sm leading-loose text-center my-2">{{ contentData.casesByTask.title }}</span>
<Chart type="bar" :data="casesByTaskData" :options="casesByTaskOptions" :style="{ height: casesByTaskHeight }" class="h-[500px]"/>
</li>
</ul>
</li>
</ul>
</section>
</article>
</div>
</div>
<!-- Sidebar: State -->
<div class="bg-transparent py-4 w-14 h-screen-main z-10 bottom-0 right-0 absolute">
<ul class="flex flex-col justify-center items-center">
<li class="inline-flex items-center justify-center border border-neutral-500 rounded-full w-9 h-9 cursor-pointer bg-neutral-50 drop-shadow hover:border-primary" @click="sidebarState = !sidebarState" :class="{'border-primary': sidebarState}" id="iconState">
<span class="material-symbols-outlined text-2xl text-neutral-500 hover:text-primary p-1.5" :class="[sidebarState ? 'text-primary' : 'text-neutral-500']">
info
</span>
</li>
</ul>
</div>
<SidebarStates v-model:visible="sidebarState"></SidebarStates>
</main>
</template>
<script>
import { storeToRefs } from 'pinia';
import LoadingStore from '@/stores/loading.js';
import CompareStore from '@/stores/compare.js';
import SidebarStates from '@/components/Compare/SidebarStates.vue';
import { setLineChartData } from '@/module/setChartData.js';
import { simpleTimeLabel, followTimeLabel, dateLabel } from '@/module/timeLabel.js';
// import { compareDashboardData } from '@/views/Compare/Dashboard/data.js';
export default {
setup() {
const loadingStore = LoadingStore();
const compareStore = CompareStore();
const { isLoading } = storeToRefs(loadingStore);
const { compareDashboardData } = storeToRefs(compareStore);
return { isLoading, compareStore, compareDashboardData }
},
components: {
SidebarStates,
},
data() {
return {
timeUsageData: [
{tagId: '#cycleTime', label: 'Cycle Efficiency'},
{tagId: '#processingTime', label: 'Processing Time'},
{tagId: '#waitingTime', label: 'Waiting Time'},
],
frequencyData: [
{tagId: '#cases', label: 'Number of Cases'},
// {tagId: '#trace', label: 'Number of Trace'},
// {tagId: '#resource', label: 'Resource'},
],
contentData: {
avgCycleEfficiency: {title: 'Cycle Efficiency', x: 'Date', y: 'Cycle efficiency (%)'},
avgProcessTime: {title: 'Average Processing Time', x: 'Date', y: 'Processing time'},
avgProcessTimeByTask: {title: 'Average Processing Time by Activity', x: 'Processing time', y: 'Activity'},
avgWaitingTime: {title: 'Average Waiting Time', x: 'Date', y: 'Waiting time'},
avgWaitingTimeByEdge: {title: 'Average Waiting Time by Activity', x: 'Waiting time', y: 'Activity'},
freq: {title: 'New Cases', x: 'Date', y: 'Count'},
casesByTask: {title: 'Number of Cases by Activity', x: 'Times', y: 'Activity'},
},
isActive: null,
avgCycleTimeData: null,
avgCycleTimeOptions: null,
avgCycleEfficiencyData: null,
avgCycleEfficiencyOptions: null,
avgProcessTimeData: null,
avgProcessTimeOptions: null,
avgProcessTimeByTaskData: null,
avgProcessTimeByTaskOptions: null,
avgWaitingTimeData: null,
avgWaitingTimeOptions: null,
avgWaitingTimeByEdgeData: null,
avgWaitingTimeByEdgeOptions: null,
freqData: null,
freqOptions: null,
casesByTaskData: null,
casesByTaskOptions: null,
horizontalBarHeight: 500, // horizontal Bar default height
avgProcessTimeByTaskHeight: 500,
avgWaitingTimeByEdgeHeight: 500,
casesByTaskHeight: 500,
colorPrimary: '#0099FF',
colorSecondary: '#FFAA44',
sidebarState: false, // SideBar: Summary
}
},
methods: {
xLabelsData(valueData) {
let min = new Date(valueData.min).getTime();
let max = new Date(valueData.max).getTime();
let numPoints = 12;
let step = (max - min) / (numPoints - 1);
let data = [];
for(let i = 0; i< numPoints; i++) {
const x = min + i * step;
data.push(x);
}
return data;
},
getHorizontalBarHeight(chartData) {
const totalBars = chartData.x_axis.labels.length;
let horizontalBar = this.horizontalBarHeight;
if(totalBars > 10) horizontalBar = (totalBars - 10) * 16 * 2 + this.horizontalBarHeight;
return horizontalBar + 'px'
},
getLineChart(chartData, content) {
let datasetsPrimary = setLineChartData(chartData.data[0].data, chartData.x_axis.max, chartData.x_axis.min, false, chartData.y_axis.max, chartData.y_axis.min);
let datasetsSecondary = setLineChartData(chartData.data[1].data, chartData.x_axis.max, chartData.x_axis.min, false, chartData.y_axis.max, chartData.y_axis.min);
let minX = chartData.x_axis.min;
let maxX = chartData.x_axis.max;
let maxY = chartData.y_axis.max;
let xData = this.xLabelsData(chartData.x_axis)
let labelPrimary = chartData.data[0].label;
let labelSecondary = chartData.data[1].label;
let setData = {};
let setOption = {};
const getMoment = (time)=> {
return this.$moment(time).format('YYYY/M/D hh:mm:ss')
};
const getSimpleTimeLabel = simpleTimeLabel;
const getFollowTimeLabel = followTimeLabel;
setData = {
labels: xData,
datasets: [
{
label: labelPrimary,
data: datasetsPrimary,
fill: false,
tension: 0, // 貝茲曲線張力
borderColor: this.colorPrimary,
pointBackgroundColor: this.colorPrimary,
},
{
label: labelSecondary,
data: datasetsSecondary,
fill: false,
tension: 0, // 貝茲曲線張力
borderColor: this.colorSecondary,
pointBackgroundColor: this.colorSecondary,
}
]
};
setOption = {
responsive: true,
maintainAspectRatio: false,
layout: {
padding: {
top: 16,
left: 8,
right: 8,
}
},
plugins: {
legend: false, // 圖例
tooltip: {
// displayColors: false,
mode: 'index',
titleFont: {weight: 'normal'},
callbacks: {
title: function(context) {
return `${content.x}: ${getMoment(context[0].parsed.x)}`;
},
label: function(context) {
let value = getSimpleTimeLabel(context.parsed.y, 2);
switch (context.datasetIndex) {
case 0: // Primary
return `${labelPrimary}: ${value}`;
case 1: // Secondary
return `${labelSecondary}: ${value}`;
}
}
},
},
title: {
display: false,
},
},
scales: {
x: {
type: 'time',
min: minX,
max: maxX,
title: {
display: true,
text: content.x,
color: '#334155',
font: {
size: 12,
lineHeight: 2
}
},
time: {
displayFormats: {
day: 'yyyy/M/d'
}
},
ticks: {
display: true,
maxRotation: 0, // 不旋轉 lable 0~50
color: '#64748b',
source: 'labels', // 依比例彈性顯示 label 數量
},
border: {
color: '#64748b',
},
grid: {
tickLength: 0, // 網格是否超過邊線
}
},
y: {
beginAtZero: true, // scale 包含 0
title: {
display: true,
text: content.y,
color: '#334155',
font: {
size: 12,
lineHeight: 2
},
},
ticks:{
color: '#64748b',
padding: 8,
callback: function (value, index, ticks) {
return getFollowTimeLabel(value, maxY, 1)
}
},
grid: {
color: '#64748b',
},
border: {
display: false, // 隱藏左側多出來的線
},
},
},
};
return [setData, setOption]
},
getBarChart(chartData, content) {
const maxX = chartData.x_axis.max;
const minX = chartData.x_axis.min;
const getMoment = (time)=> this.$moment(time).format('YYYY/M/D hh:mm:ss');
const getDateLabel = dateLabel;
let datasetsPrimary = chartData.data[0].data;
let xDataPrimary;
let yDataPrimary;
let labelPrimary = chartData.data[0].label;
let datasetsSecondary = chartData.data[1].data;
let xDataSecondary;
let yDataSecondary;
let labelSecondary = chartData.data[1].label;
let setData = {};
let setOption = {};
// 轉為百分比
datasetsPrimary = datasetsPrimary.map(value => {
return {
x: getMoment(value.x),
y: value.y === null ? null : value.y * 100
}
});
xDataPrimary = datasetsPrimary.map(i => i.x);
yDataPrimary = datasetsPrimary.map(i => i.y);
datasetsSecondary = datasetsSecondary.map(value => {
return {
x: getMoment(value.x),
y: value.y === null ? null : value.y * 100
}
});
xDataSecondary = datasetsSecondary.map(i => i.x);
yDataSecondary = datasetsSecondary.map(i => i.y);
setData = {
labels: xDataPrimary,
datasets: [
{
label: labelPrimary,
data: yDataPrimary,
backgroundColor: this.colorPrimary,
},
{
label: labelSecondary,
data: yDataSecondary,
backgroundColor: this.colorSecondary,
},
]
};
setOption = {
responsive: true,
maintainAspectRatio: false,
layout: {
padding: {
top: 16,
left: 8,
right: 8,
}
},
interaction: {
intersect: false,
mode: 'index', // 可顯示全部的 data label
},
plugins: {
legend: false, // 圖例
tooltip: {
// displayColors: false,
titleFont: {weight: 'normal'},
callbacks: {
title: function(context) {
return `${content.x}: ${context[0].label}`;
},
label: function(context) {
let value = context.parsed.y;
value = context.parsed.y === 0 ? 0 :
context.parsed.y === null ? "n/a" :
context.parsed.y.toFixed(2);
switch (context.datasetIndex) {
case 0: // Primary
return `${labelPrimary}: ${value}`;
case 1: // Secondary
return `${labelSecondary}: ${value}`;
}
}
},
},
title: {
display: false,
},
},
scales: {
x: {
title: {
display: true,
text: content.x,
color: '#334155',
font: {
size: 12,
lineHeight: 2
}
},
ticks: {
display: true,
color: '#64748b',
callback: function(v, i, t) {
let label = xDataPrimary[i]
return getDateLabel(label, maxX, minX)
},
},
border: {
color: '#64748b',
},
grid: {
tickLength: 0, // 網格是否超過邊線
}
},
y: {
beginAtZero: true, // scale 包含 0
title: {
display: true,
text: content.y,
color: '#334155',
font: {
size: 12,
lineHeight: 2
},
},
ticks:{
color: '#64748b',
padding: 8,
},
grid: {
color: '#64748b',
},
border: {
display: false, // 隱藏左側多出來的線
},
},
},
};
return [setData, setOption]
},
getHorizontalBarChart(chartData, content, isSingle) {
const maxY = chartData.y_axis.max;
const getSimpleTimeLabel = simpleTimeLabel;
const getFollowTimeLabel = followTimeLabel;
const labelPrimary = chartData.data[0].label;
const labelSecondary = chartData.data[1].label;
let setData = {};
let setOption = {};
// 大到小排序: Primary 為主
const datasetsPrimary = chartData.data[0].data;
const datasetsSecondary = chartData.data[1].data;
datasetsPrimary.sort((a, b) => b.y - a.y);
datasetsSecondary.sort((a, b) => {
// Find the index of a.x in data1
let indexA = datasetsPrimary.findIndex(item => item.x === a.x);
// Find the index of b.x in data1
let indexB = datasetsPrimary.findIndex(item => item.x === b.x);
// Compare the indexes
return indexA - indexB;
});
// 找出排序後,分別拉出 x, y Data
const xData = datasetsPrimary.map(item => item.x);
const yDataPrimary = datasetsPrimary.map(item => item.y);
const yDataSecondary = datasetsSecondary.map(item => item.y);
setData = {
labels: xData,
datasets: [
{
label: labelPrimary,
data: yDataPrimary,
backgroundColor: this.colorPrimary,
},
{
label: labelSecondary,
data: yDataSecondary,
backgroundColor: this.colorSecondary,
},
]
};
setOption = {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
layout: {
padding: {
top: 16,
left: 8,
right: 8,
}
},
plugins: {
legend: false, // 圖例
tooltip: {
// displayColors: false,
mode: 'index', // 可顯示全部的 data label
titleFont: {weight: 'normal'},
callbacks: {
label: function(context) {
let value = context.parsed.y;
value = context.parsed.x === null ? "n/a" : getSimpleTimeLabel(context.parsed.x, 2);
switch (context.datasetIndex) {
case 0: // Primary
return `${labelPrimary}: ${value}`;
case 1: // Secondary
return `${labelSecondary}: ${value}`;
}
}
},
},
title: {
display: false,
},
},
scales: {
x: {
title: {
display: true,
text: content.x,
color: '#334155',
font: {
size: 12,
lineHeight: 2
}
},
ticks: {
display: true,
maxRotation: 0, // 不旋轉 lable 0~50
color: '#64748b',
callback: function (value, index, ticks) {
return getFollowTimeLabel(value, maxY, 1)
}
},
grid: {
color: '#64748b',
tickLength: 0, // 網格是否超過邊線
},
border: {
display:false,
},
},
y: {
beginAtZero: true, // scale 包含 0
type: 'category',
title: {
display: true,
text: content.y,
color: '#334155',
font: {
size: 12,
lineHeight: 2
},
},
ticks:{
color: '#64748b',
padding: 8,
},
grid: {
display:false,
color: '#64748b',
},
border: {
display: false, // 隱藏左側多出來的線
},
},
},
};
if(isSingle) { // 設定一個活動的 y label、提示框文字
setOption.plugins.tooltip.callbacks.title = function(context) {
return `${content.y}: ${context[0].label}`;
};
setOption.scales.y.ticks.callback = function (value, index, ticks) {
let label = xData[index];
return label.length > 21 ? `${label.substring(0, 18)}...` : label
};
}else { // 設定「活動」到「活動」的 y label、提示框文字
setOption.plugins.tooltip.callbacks.title = function(context) {
return `${content.y}: ${context[0].label.replace(',', ' - ')}`
};
setOption.scales.y.ticks.callback = function (value, index, ticks) {
let label = xData[index];
let labelStart = label[0];
let labelEnd = label[1];
labelStart = labelStart.length > 10 ? `${labelStart.substring(0,7)}...` : labelStart;
labelEnd = labelEnd.length > 10 ? `${labelEnd.substring(0,7)}...` : labelEnd;
return labelStart + " - " + labelEnd
};
}
return [setData, setOption]
},
},
async created() {
this.isLoading = true; // moubeted 才停止 loading
const routeParams = this.$route.params;
const primaryType = routeParams.primaryType;
const secondaryType = routeParams.secondaryType;
const primaryId = routeParams.primaryId;
const secondaryId = routeParams.secondaryId;
const typeMap = {
'log': 'log_id',
'filter': 'filter_id'
};
const primaryTypeParam = typeMap[primaryType];
const secondaryTypeParam = typeMap[secondaryType];
const queryParams = [
{ [primaryTypeParam]: primaryId },
{ [secondaryTypeParam]: secondaryId }
];
// 取得 Compare Data
await this.compareStore.getCompare(queryParams);
this.avgProcessTimeByTaskHeight = await this.getHorizontalBarHeight(this.compareDashboardData.time.avg_process_time_by_task);
this.avgWaitingTimeByEdgeHeight = await this.getHorizontalBarHeight(this.compareDashboardData.time.avg_waiting_time_by_edge);
this.casesByTaskHeight = await this.getHorizontalBarHeight(this.compareDashboardData.freq.cases_by_task);
// create chart
[this.avgCycleEfficiencyData, this.avgCycleEfficiencyOptions] = this.getBarChart(this.compareDashboardData.time.avg_cycle_efficiency, this.contentData.avgCycleEfficiency);
[this.avgProcessTimeData, this.avgProcessTimeOptions] = this.getLineChart(this.compareDashboardData.time.avg_process_time, this.contentData.avgProcessTime);
[this.avgProcessTimeByTaskData, this.avgProcessTimeByTaskOptions] = this.getHorizontalBarChart(this.compareDashboardData.time.avg_process_time_by_task, this.contentData.avgProcessTimeByTask, true);
[this.avgWaitingTimeData, this.avgWaitingTimeOptions] = this.getLineChart(this.compareDashboardData.time.avg_waiting_time, this.contentData.avgWaitingTime);
[this.avgWaitingTimeByEdgeData, this.avgWaitingTimeByEdgeOptions] = this.getHorizontalBarChart(this.compareDashboardData.time.avg_waiting_time_by_edge, this.contentData.avgWaitingTimeByEdge, false);
[this.freqData, this.freqOptions] = this.getLineChart(this.compareDashboardData.freq.cases, this.contentData.freq);
[this.casesByTaskData, this.casesByTaskOptions] = this.getHorizontalBarChart(this.compareDashboardData.freq.cases_by_task, this.contentData.casesByTask, true);
},
mounted() {
// 停止 loading
this.isLoading = false;
}
}
</script>
<style scoped>
.active {
@apply text-primary
}
</style>