Files
lucia-frontend/src/views/Discover/Performance/index.vue

625 lines
23 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="h1">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="h1">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="h1 px-4 border-b border-neutral-900"><span class="material-symbols-outlined mr-2 align-middle">schedule</span>Time Usage</p>
</section>
<section>
<ul class="list-disc list-inside px-4 pl-7 text-sm">
<li id="cycleTime" class="scroll-smooth">
<span class="inline-block py-4">Cycle Time & Efficiency</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.avgCycleTime.title }}</span>
<Chart type="line" :data="avgCycleTimeData" :options="avgCycleTimeOptions" 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.avgCycleEfficiency.title }}<span class="material-symbols-outlined align-middle ml-2 cursor-pointer !text-base" v-tooltip.bottom="tooltip.avgCycleEfficiency">info</span></span>
<Chart type="bar" :data="avgCycleEfficiencyData" :options="avgCycleEfficiencyOptions" class="h-96" />
</li>
</ul>
</li>
<li id="processingTime">
<span class="inline-block py-4">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">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 class="material-symbols-outlined align-middle ml-2 cursor-pointer !text-base" v-tooltip.bottom="tooltip.avgWaitingTimeByEdge">info</span></span>
<Chart type="bar" :data="avgWaitingTimeByEdgeData" :options="avgWaitingTimeByEdgeOptions" :style="{ height: avgWaitingTimeByEdgeHeight }" class="h-[500px]" />
</li>
</ul>
</li>
</ul>
</section>
<section>
<p class="h1 px-4 border-b border-neutral-900"><span class="material-symbols-outlined mr-2 align-middle">moving</span>Frequency</p>
<ul class="list-disc list-inside px-4 pl-7 text-sm">
<li id="cases">
<span class="inline-block py-4">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>
<StatusBar></StatusBar>
</main>
</template>
<script>
import { storeToRefs } from 'pinia';
import LoadingStore from '@/stores/loading.js';
import PerformanceStore from '@/stores/performance.js';
import StatusBar from '@/components/Discover/StatusBar.vue';
import { setLineChartData } from '@/module/setChartData.js';
import { simpleTimeLabel, followTimeLabel, dateLabel } from '@/module/timeLabel.js';
export default {
setup() {
const loadingStore = LoadingStore();
const performanceStore = PerformanceStore();
const { isLoading } = storeToRefs(loadingStore);
const { performanceData } = storeToRefs(performanceStore);
return { isLoading, performanceStore, performanceData }
},
components: {
StatusBar,
},
data() {
return {
timeUsageData: [
{tagId: '#cycleTime', label: 'Cycle Time & 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: {
avgCycleTime: {title: 'Average Cycle Time', x: 'Date', y: 'Cycle time'},
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: 'Count', y: 'Activity'},
},
tooltip: {
avgCycleEfficiency: {
value: 'Cycle Efficiency: The ratio of the total productive time to the total cycle time of a process. Productive time refers to the time during which value-adding activities are performed, while cycle time is the total time from the start to the end of the process, including both productive and non-productive periods.',
class: '!max-w-[212px] !text-[10px] !opacity-80',
autoHide: false,
},
avgWaitingTimeByEdge: {
value: 'Average Waiting Time Between Activities : The average duration during which a task or an item (like a work order, a product, or a piece of information) is on hold or idle before the next step in the process can begin. This time does not contribute to the active progression of the task but is instead a period of inactivity.',
class: '!max-w-[212px] !text-[10px] !opacity-80',
autoHide: false,
}
},
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
}
},
methods: {
/**
* 手刻折線圖 x label 時間刻度
* @param { object } valueData {min: '2022-02-20T19:54:12', max: '2023-11-27T07:21:53'}
*/
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;
},
/**
* 讓長條圖依 data 數量增加高度
* @param { object } chartData chart data
*/
getHorizontalBarHeight(chartData) {
const totalBars = chartData.data.length;
let horizontalBar = this.horizontalBarHeight;
if(totalBars > 10) horizontalBar = (totalBars - 10) * 16 + this.horizontalBarHeight;
return horizontalBar + 'px'
},
/**
* 建立折線圖
* @param { object } chartData chart data
* @param { object } content titels
* @param { string } yUnit y 軸單位 date | count
*/
getLineChart(chartData, content, yUnit) {
let datasets = setLineChartData(chartData.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;
let setData = {};
let setOption = {};
const getMoment = (time)=> {
return this.$moment(time).format('YYYY/M/D hh:mm:ss')
};
const getSimpleTimeLabel = simpleTimeLabel;
const getFollowTimeLabel = followTimeLabel;
switch (yUnit) {
case 'date':
datasets = setLineChartData(chartData.data, chartData.x_axis.max, chartData.x_axis.min, false, chartData.y_axis.max, chartData.y_axis.min);
xData = this.xLabelsData(chartData.x_axis);
break;
case 'count': // 次數 10 個點
datasets = chartData.data;
xData = chartData.data.map(item => new Date(item.x).getTime());
break;
}
setData = {
labels: xData,
datasets: [
{
label: content.title,
data: datasets,
fill: false,
tension: 0, // 貝茲曲線張力
borderColor: '#0099FF',
}
]
};
setOption = {
responsive: true,
maintainAspectRatio: false,
layout: {
padding: {
top: 16,
left: 8,
right: 8,
}
},
plugins: {
legend: false, // 圖例
tooltip: {
displayColors: false,
titleFont: {weight: 'normal'},
callbacks: {
title: function(context) {
return `${content.x}: ${getMoment(context[0].parsed.x)}`;
}
},
},
title: {
display: false,
},
},
scales: {
x: {
type: 'time',
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',
},
},
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, // 隱藏左側多出來的線
},
},
},
};
switch (yUnit) {
case 'date':
setOption.plugins.tooltip.callbacks.label = function(context) {
return `${content.y}: ${getSimpleTimeLabel(context.parsed.y, 2)}`;
};
setOption.scales.x.min = minX;
setOption.scales.x.max = maxX;
setOption.scales.y.ticks.callback = function (value, index, ticks) {
return getFollowTimeLabel(value, maxY, 1)
};
break;
case 'count':
setOption.plugins.tooltip.callbacks.label = function(context) {
return `${content.y}: ${context.parsed.y}`;
};
break;
}
return [setData, setOption]
},
/**
* 建立長條圖
* @param { object } chartData chart data
* @param { object } content titels
*/
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 datasets = chartData.data;
let xData;
let yData;
let setData = {};
let setOption = {};
datasets = datasets.map(value => {
return {
x: getMoment(value.x),
y: value.y * 100
}
}); // 轉為百分比
xData = datasets.map(i => i.x);
yData = datasets.map(i => i.y)
setData = {
labels: xData,
datasets: [
{
label: content.title,
data: yData,
backgroundColor: '#0099FF'
}
]
};
setOption = {
responsive: true,
maintainAspectRatio: false,
layout: {
padding: {
top: 16,
left: 8,
right: 8,
}
},
plugins: {
legend: false, // 圖例
tooltip: {
displayColors: false,
titleFont: {weight: 'normal'},
callbacks: {
title: function(context) {
return `${content.x}: ${context[0].label}`;
},
label: function(context) {
return `${content.y}: ${context.parsed.y.toFixed(2)}`;
}
},
},
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 = xData[i]
return getDateLabel(label, maxX, minX)
},
},
border: {
color: '#64748b',
},
},
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]
},
/**
* 建立水平長條圖
* @param { object } chartData chart data
* @param { object } content titels
* @param { boolean } isSingle 單個或雙數 activity
* @param { string } xUnit x 軸單位 date | count
*/
getHorizontalBarChart(chartData, content, isSingle, xUnit) {
const maxY = chartData.y_axis.max;
const getSimpleTimeLabel = simpleTimeLabel;
const getFollowTimeLabel = followTimeLabel;
let setData = {};
let setOption = {};
// 大到小排序
chartData.data.sort((a, b) => b.y - a.y);
const xData = chartData.data.map(item => item.x);
const yData = chartData.data.map(item => item.y);
setData = {
labels: xData,
datasets: [
{
label: content.title,
data: yData,
backgroundColor: '#0099FF',
}
]
};
setOption = {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
layout: {
padding: {
top: 16,
left: 8,
right: 8,
}
},
plugins: {
legend: false, // 圖例
tooltip: {
displayColors: false,
titleFont: {weight: 'normal'},
callbacks: {}
},
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',
},
grid: {
color: '#64748b',
},
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, // 隱藏左側多出來的線
},
},
},
};
switch (xUnit) {
case 'date':
setOption.plugins.tooltip.callbacks.label = function(context) {
return `${content.x}: ${getSimpleTimeLabel(context.parsed.x, 2)}`;
};
setOption.scales.x.ticks.callback = function (value, index, ticks) {
return getFollowTimeLabel(value, maxY, 1)
};
break;
case 'count':
setOption.plugins.tooltip.callbacks.label = function(context) {
return `${content.x}: ${context.parsed.x}`;
}
break;
}
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;
let id = routeParams.fileId;
let type = routeParams.type;
// 取得 Performance Data
await this.performanceStore.getPerformance(type, id);
this.avgProcessTimeByTaskHeight = await this.getHorizontalBarHeight(this.performanceData.time.avg_process_time_by_task);
this.avgWaitingTimeByEdgeHeight = await this.getHorizontalBarHeight(this.performanceData.time.avg_waiting_time_by_edge);
this.casesByTaskHeight = await this.getHorizontalBarHeight(this.performanceData.freq.cases_by_task);
// create chart
[this.avgCycleTimeData, this.avgCycleTimeOptions] = this.getLineChart(this.performanceData.time.avg_cycle_time, this.contentData.avgCycleTime, 'date');
[this.avgCycleEfficiencyData, this.avgCycleEfficiencyOptions] = this.getBarChart(this.performanceData.time.avg_cycle_efficiency, this.contentData.avgCycleEfficiency);
[this.avgProcessTimeData, this.avgProcessTimeOptions] = this.getLineChart(this.performanceData.time.avg_process_time, this.contentData.avgProcessTime, 'date');
[this.avgProcessTimeByTaskData, this.avgProcessTimeByTaskOptions] = this.getHorizontalBarChart(this.performanceData.time.avg_process_time_by_task, this.contentData.avgProcessTimeByTask, true, 'date');
[this.avgWaitingTimeData, this.avgWaitingTimeOptions] = this.getLineChart(this.performanceData.time.avg_waiting_time, this.contentData.avgWaitingTime, 'date');
[this.avgWaitingTimeByEdgeData, this.avgWaitingTimeByEdgeOptions] = this.getHorizontalBarChart(this.performanceData.time.avg_waiting_time_by_edge, this.contentData.avgWaitingTimeByEdge, false, 'date');
[this.freqData, this.freqOptions] = this.getLineChart(this.performanceData.freq.cases, this.contentData.freq, 'count');
[this.casesByTaskData, this.casesByTaskOptions] = this.getHorizontalBarChart(this.performanceData.freq.cases_by_task, this.contentData.casesByTask, true, 'count');
// 停止 loading
this.isLoading = false;
},
}
</script>
<style scoped>
.active {
@apply text-primary
}
</style>