943 lines
34 KiB
Vue
943 lines
34 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="handleClick(item.tagId)" class="cursor-pointer hover:text-primary">
|
||
{{ item.label }}
|
||
</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="handleClick(item.tagId)" class="cursor-pointer hover:text-primary">
|
||
{{ item.label }}
|
||
</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 class="material-symbols-outlined align-middle ml-2 cursor-pointer !text-base"
|
||
v-tooltip.bottom="tooltip.avgWaitingTime">
|
||
info
|
||
</span></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>
|
||
<div>
|
||
<Chart v-if="avgWaitingTimeByEdgeData !== null" type="bar" :data="avgWaitingTimeByEdgeData"
|
||
:options="avgWaitingTimeByEdgeOptions" :style="{ height: avgWaitingTimeByEdgeHeight }"
|
||
class="h-[500px]" />
|
||
<div v-else class="h-96 bg-neutral-100 m-4 flex">
|
||
<p class="h2 text-danger m-auto">No waiting time.</p>
|
||
</div>
|
||
</div>
|
||
</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>
|
||
<FreqChart v-if="performanceData" :chartData="performanceData?.freq?.cases" :content="contentData?.freq" yUnit="count"
|
||
pageName="Performance"
|
||
/>
|
||
</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, mapActions, } from 'pinia';
|
||
import moment from 'moment';
|
||
import LoadingStore from '@/stores/loading.js';
|
||
import PerformanceStore from '@/stores/performance.js';
|
||
import ConformanceStore from '@/stores/conformance.js';
|
||
import StatusBar from '@/components/Discover/StatusBar.vue';
|
||
import { setLineChartData } from '@/module/setChartData.js';
|
||
import { simpleTimeLabel, followTimeLabel,
|
||
setTimeStringFormatBaseOnTimeDifference,
|
||
mapTimestampToAxisTicksByFormat,
|
||
getStepSizeOfYTicks,
|
||
getYTicksByIndex,
|
||
} from '@/module/timeLabel.js';
|
||
import FreqChart from './FreqChart.vue';
|
||
import { PRIME_VUE_TICKS_LIMIT } from '../../../constants/constants.js';
|
||
|
||
const primeVueTicksLimit = PRIME_VUE_TICKS_LIMIT;
|
||
|
||
export default {
|
||
setup() {
|
||
const loadingStore = LoadingStore();
|
||
const performanceStore = PerformanceStore();
|
||
const { isLoading } = storeToRefs(loadingStore);
|
||
const { performanceData } = storeToRefs(performanceStore);
|
||
|
||
return { isLoading, performanceStore, performanceData }
|
||
},
|
||
components: {
|
||
StatusBar,
|
||
FreqChart,
|
||
},
|
||
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 between Activity', x: 'Waiting time', y: 'Activities'},
|
||
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,
|
||
},
|
||
avgWaitingTime: {
|
||
value: 'Average Waiting Time: 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,
|
||
},
|
||
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: {
|
||
handleClick(tagId) {
|
||
this.isActive = tagId;
|
||
|
||
// 在進行導航前,檢查或處理 tagId 的值
|
||
if (this.isSafeTagId(tagId)) {
|
||
window.location.href = tagId; // 確保這個路徑是安全的
|
||
// 或者使用 Vue Router 進行導航
|
||
// this.$router.push({ path: tagId });
|
||
} else {
|
||
console.warn("不安全的 tagId: ", tagId);
|
||
}
|
||
},
|
||
// 避免直接使用動態 href:改用安全的方法來處理動態導航,避免直接將未經驗證的數據綁定到 href 屬性。
|
||
isSafeTagId(tagId) {
|
||
// 檢查 tagId 是否符合安全的格式(例如只允許特定的模式或路徑)
|
||
const pattern = /^#?[a-zA-Z0-9-]*$/; // 例如: #waitingTime
|
||
return pattern.test(tagId);
|
||
},
|
||
/**
|
||
* 手刻折線圖 x label 時間刻度
|
||
* @param { object } valueData {min: '2022-02-20T19:54:12', max: '2023-11-27T07:21:53'}
|
||
*/
|
||
setXLabelsData(valueData) {
|
||
const min = new Date(valueData.min).getTime();
|
||
const max = new Date(valueData.max).getTime();
|
||
const numPoints = 12;
|
||
const step = (max - min) / (numPoints - 1);
|
||
const 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 標題文字
|
||
*/
|
||
getBarChart(chartData, content) {
|
||
const getMoment = (time)=> this.$moment(time).format('YYYY/M/D hh:mm:ss');
|
||
let primeVueSetData = {};
|
||
let primeVueSetOption = {};
|
||
|
||
const datasets = chartData.data.map(value => {
|
||
return {
|
||
x: getMoment(value.x),
|
||
y: value.y * 100
|
||
}
|
||
}); // 轉為百分比
|
||
const xData = datasets.map(i => i.x);
|
||
const yData = datasets.map(i => i.y)
|
||
|
||
primeVueSetData = {
|
||
labels: xData,
|
||
datasets: [
|
||
{
|
||
label: content.title,
|
||
data: yData,
|
||
backgroundColor: '#0099FF'
|
||
}
|
||
]
|
||
};
|
||
primeVueSetOption = {
|
||
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(value, index, ticks) {
|
||
return moment(xData[index]).format('YYYY/MM/DD');
|
||
},
|
||
},
|
||
border: {
|
||
color: '#64748b',
|
||
},
|
||
grid: {
|
||
display: false,
|
||
},
|
||
},
|
||
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 [primeVueSetData, primeVueSetOption]
|
||
},
|
||
/**
|
||
* 建立水平長條圖
|
||
* @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 primeVueSetData = {};
|
||
let primeVueSetOption = {};
|
||
|
||
// 大到小排序
|
||
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);
|
||
|
||
primeVueSetData = {
|
||
labels: xData,
|
||
datasets: [
|
||
{
|
||
label: content.title,
|
||
data: yData,
|
||
backgroundColor: '#0099FF',
|
||
}
|
||
]
|
||
};
|
||
primeVueSetOption = {
|
||
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':
|
||
primeVueSetOption.plugins.tooltip.callbacks.label = function(context) {
|
||
return `${content.x}: ${getSimpleTimeLabel(context.parsed.x, 2)}`;
|
||
};
|
||
primeVueSetOption.scales.x.ticks.callback = function (value, index, ticks) {
|
||
return getFollowTimeLabel(value, maxY, 1)
|
||
};
|
||
break;
|
||
case 'count':
|
||
default:
|
||
primeVueSetOption.scales.x.ticks.precision = 0; // x 軸顯示小數點後 0 位
|
||
primeVueSetOption.plugins.tooltip.callbacks.label = function(context) {
|
||
return `${content.x}: ${context.parsed.x}`;
|
||
}
|
||
break;
|
||
}
|
||
if(isSingle) { // 設定一個活動的 y label、提示框文字
|
||
primeVueSetOption.plugins.tooltip.callbacks.title = function(context) {
|
||
return `${content.y}: ${context[0].label}`;
|
||
};
|
||
primeVueSetOption.scales.y.ticks.callback = function (value, index, ticks) {
|
||
const label = xData[index];
|
||
return label.length > 21 ? `${label.substring(0, 18)}...` : label
|
||
};
|
||
}else { // 設定「活動」到「活動」的 y label、提示框文字
|
||
primeVueSetOption.plugins.tooltip.callbacks.title = function(context) {
|
||
return `${content.y}: ${context[0].label.replace(',', ' - ')}`
|
||
};
|
||
primeVueSetOption.scales.y.ticks.callback = function (value, index, ticks) {
|
||
const 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 [primeVueSetData, primeVueSetOption]
|
||
},
|
||
/**
|
||
* Compare page and Performance have this same function.
|
||
* @param whichScaleObj PrimeVue scale option object to reference to
|
||
* @param customizeOptions
|
||
* @param customizeOptions.content
|
||
* @param customizeOptions.ticksOfXAxis
|
||
*/
|
||
getCustomizedScaleOption(whichScaleObj, {customizeOptions: {
|
||
content,
|
||
ticksOfXAxis,
|
||
},
|
||
}) {
|
||
let resultScaleObj;
|
||
resultScaleObj = this.customizeScaleChartOptionTitleByContent(whichScaleObj, content);
|
||
resultScaleObj = this.customizeScaleChartOptionTicks(resultScaleObj, ticksOfXAxis);
|
||
return resultScaleObj;
|
||
},
|
||
/** Compare page and Performance have this same function.
|
||
* 在一個基本的物件上加以客製化這個物件,客製化的參照來源是 content 的內容
|
||
* 之所以有辦法這樣撰寫,是因為我們知道物件的順序是先 x 再 title 再 text
|
||
* This function alters the title property of known scales object of Chart option
|
||
* This is based on the fact that we know the order must be x -> title -> text.
|
||
* @param {object} whichScaleObj PrimeVue scale option object to reference to
|
||
* @param content whose property includes x and y and stand for titles
|
||
*
|
||
* @returns { object } an object modified with two titles
|
||
*/
|
||
customizeScaleChartOptionTitleByContent(whichScaleObj, content){
|
||
if (!content) {
|
||
// Early return
|
||
return whichScaleObj;
|
||
}
|
||
|
||
return {
|
||
...whichScaleObj,
|
||
x: {
|
||
...whichScaleObj.x,
|
||
title: {
|
||
...whichScaleObj.x.title,
|
||
text: content.x
|
||
}
|
||
},
|
||
y: {
|
||
...whichScaleObj.y,
|
||
title: {
|
||
...whichScaleObj.y.title,
|
||
text: content.y
|
||
}
|
||
}
|
||
};
|
||
},
|
||
/**
|
||
* Compare page and Performance have this same function.
|
||
* @param {object} scaleObjectToAlter this object follows the format of prive vue chart
|
||
* @param {Array<string>} ticksOfXAxis For example, ['05/06', '05,07', '05/08']
|
||
* or ['08:03:01', '08:11:18', '09:03:41', ], and so on.
|
||
*/
|
||
customizeScaleChartOptionTicks(scaleObjectToAlter, ticksOfXAxis) {
|
||
return {
|
||
...scaleObjectToAlter,
|
||
x: {
|
||
...scaleObjectToAlter.x,
|
||
ticks: {
|
||
...scaleObjectToAlter.x.ticks,
|
||
callback: function(value, index) {
|
||
return ticksOfXAxis[index];
|
||
},
|
||
},
|
||
},
|
||
};
|
||
},
|
||
/**
|
||
* 建立Performance頁面的折線圖,並且避免同一個畫面中的設定值彼此覆蓋
|
||
* @param { object } chartData chart data
|
||
* @param { object } content titels 標題文字
|
||
* @param { string } yUnit y 軸單位 'date'
|
||
*/
|
||
getExplicitDeclaredLineChart(chartData, content, yUnit) {
|
||
const minX = chartData.x_axis.min;
|
||
const maxX = chartData.x_axis.max;
|
||
let primeVueSetData = {};
|
||
let primeVueSetOption = {};
|
||
const getSimpleTimeLabel = simpleTimeLabel;
|
||
|
||
const datasets = setLineChartData(chartData.data, chartData.x_axis.max, chartData.x_axis.min, false, chartData.y_axis.max,
|
||
chartData.y_axis.min);
|
||
const xData = this.setXLabelsData(chartData.x_axis);
|
||
|
||
|
||
// Customize X axis ticks due to different differences between min and max of data group
|
||
// Compare page and Performance page share the same logic
|
||
const formatToSet = setTimeStringFormatBaseOnTimeDifference(minX, maxX);
|
||
const ticksOfXAxis = mapTimestampToAxisTicksByFormat(xData, formatToSet);
|
||
primeVueSetData = {
|
||
labels: xData,
|
||
datasets: [
|
||
{
|
||
label: content.title,
|
||
data: datasets,
|
||
fill: false,
|
||
tension: 0, // 貝茲曲線張力
|
||
borderColor: '#0099FF',
|
||
}
|
||
]
|
||
};
|
||
primeVueSetOption = {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
layout: {
|
||
padding: {
|
||
top: 16,
|
||
left: 8,
|
||
right: 8,
|
||
}
|
||
},
|
||
plugins: {
|
||
legend: false, // 圖例
|
||
tooltip: {
|
||
displayColors: false,
|
||
titleFont: {weight: 'normal'},
|
||
callbacks: {
|
||
label: function(context) {
|
||
return `${content.y}: ${getSimpleTimeLabel(context.parsed.y, 2)}`;
|
||
}
|
||
},
|
||
},
|
||
title: {
|
||
display: false,
|
||
},
|
||
},
|
||
scales: {
|
||
x: {
|
||
min: minX,
|
||
max: maxX,
|
||
type: 'time',
|
||
title: {
|
||
display: true,
|
||
color: '#334155',
|
||
font: {
|
||
size: 12,
|
||
lineHeight: 2
|
||
},
|
||
text: content.x,
|
||
},
|
||
time: {
|
||
displayFormats: {
|
||
second: 'h:mm:ss', // ex: 1:11:11
|
||
minute: 'M/d h:mm', // ex: 1/1 1:11
|
||
hour: 'M/d h:mm', // ex: 1/1 1:11
|
||
day: 'M/d h', // ex: 1/1 1
|
||
month: 'y/M/d', // ex: 1911/1/1
|
||
},
|
||
round: true
|
||
},
|
||
ticks: {
|
||
maxTicksLimit: primeVueTicksLimit,
|
||
padding: 8,
|
||
display: true,
|
||
maxRotation: 0, // 不旋轉 lable 0~50
|
||
color: '#64748b',
|
||
source: 'labels', // 依比例彈性顯示 label 數量
|
||
callback: function(value, index) {
|
||
return ticksOfXAxis[index];
|
||
},
|
||
},
|
||
border: {
|
||
color: '#64748b',
|
||
},
|
||
},
|
||
y: {
|
||
beginAtZero: true, // scale 包含 0
|
||
title: {
|
||
display: true,
|
||
color: '#334155',
|
||
font: {
|
||
size: 12,
|
||
lineHeight: 2
|
||
},
|
||
text: content.y
|
||
},
|
||
grid: {
|
||
color: '#64748b',
|
||
},
|
||
border: {
|
||
display: false, // 隱藏左側多出來的線
|
||
},
|
||
ticks: {
|
||
|
||
color: '#64748b',
|
||
padding: 8,
|
||
callback: function (value, index, ticks) {
|
||
// resultStepSize: Y 軸一個刻度的高度的純數值部分,unitToUse則可能是 d,h,m,s 四者之一
|
||
const {resultStepSize, unitToUse} = getStepSizeOfYTicks(ticks[ticks.length - 1].value, ticks.length);
|
||
return getYTicksByIndex(resultStepSize, index, unitToUse);
|
||
},
|
||
}
|
||
},
|
||
},
|
||
};
|
||
|
||
return [primeVueSetData, primeVueSetOption]
|
||
},
|
||
/**
|
||
* 建立Average Waiting Time折線圖
|
||
* @param { object } chartData chart data
|
||
* @param { object } content titels 標題文字
|
||
* @param { string } yUnit y 軸單位 'date'
|
||
*/
|
||
getAvgWaitingTimeLineChart(chartData, content, yUnit) {
|
||
const getMoment = (time)=> this.$moment(time).format('YYYY/M/D hh:mm:ss');
|
||
const minX = chartData.x_axis.min;
|
||
let primeVueSetData = {};
|
||
let primeVueSetOption = {};
|
||
const getSimpleTimeLabel = simpleTimeLabel;
|
||
|
||
const datasets = setLineChartData(chartData.data, chartData.x_axis.max, chartData.x_axis.min, false, chartData.y_axis.max,
|
||
chartData.y_axis.min);
|
||
const xData = this.setXLabelsData(chartData.x_axis);
|
||
|
||
|
||
// Customize X axis ticks due to different differences between min and max of data group
|
||
// Compare page and Performance page share the same logic
|
||
const formatToSet = setTimeStringFormatBaseOnTimeDifference(minX, maxX);
|
||
const ticksOfXAxis = mapTimestampToAxisTicksByFormat(xData, formatToSet);
|
||
|
||
primeVueSetData = {
|
||
labels: xData,
|
||
datasets: [
|
||
{
|
||
label: content.title,
|
||
data: datasets,
|
||
fill: false,
|
||
tension: 0, // 貝茲曲線張力
|
||
borderColor: '#0099FF',
|
||
}
|
||
]
|
||
};
|
||
primeVueSetOption = {
|
||
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)}`;
|
||
},
|
||
label:function(context) {
|
||
return `${content.y}: ${getSimpleTimeLabel(context.parsed.y, 2)}`;
|
||
},
|
||
},
|
||
},
|
||
title: {
|
||
display: false,
|
||
},
|
||
},
|
||
scales: {
|
||
x: {
|
||
min: minX,
|
||
max: maxX,
|
||
type: 'time',
|
||
title: {
|
||
display: true,
|
||
color: '#334155',
|
||
font: {
|
||
size: 12,
|
||
lineHeight: 2
|
||
},
|
||
text: content.x,
|
||
},
|
||
time: {
|
||
displayFormats: {
|
||
second: 'h:mm:ss', // ex: 1:11:11
|
||
minute: 'M/d h:mm', // ex: 1/1 1:11
|
||
hour: 'M/d h:mm', // ex: 1/1 1:11
|
||
day: 'M/d h', // ex: 1/1 1
|
||
month: 'y/M/d', // ex: 1911/1/1
|
||
},
|
||
round: true
|
||
},
|
||
ticks: {
|
||
display: true,
|
||
maxRotation: 0, // 不旋轉 lable 0~50
|
||
color: '#64748b',
|
||
source: 'labels', // 依比例彈性顯示 label 數量
|
||
callback: function(value, index) {
|
||
return ticksOfXAxis[index];
|
||
},
|
||
},
|
||
border: {
|
||
color: '#64748b',
|
||
},
|
||
},
|
||
y: {
|
||
beginAtZero: true, // scale 包含 0
|
||
title: {
|
||
display: true,
|
||
color: '#334155',
|
||
font: {
|
||
size: 12,
|
||
lineHeight: 2
|
||
},
|
||
text: content.y
|
||
},
|
||
grid: {
|
||
color: '#64748b',
|
||
},
|
||
border: {
|
||
display: false, // 隱藏左側多出來的線
|
||
},
|
||
ticks: {
|
||
|
||
color: '#64748b',
|
||
padding: 8,
|
||
callback: function (value, index, ticks) {
|
||
// resultStepSize: Y 軸一個刻度的高度的純數值部分,unitToUse則可能是 d,h,m,s 四者之一
|
||
const {resultStepSize, unitToUse} = getStepSizeOfYTicks(ticks[ticks.length - 1].value, ticks.length);
|
||
return getYTicksByIndex(resultStepSize, index, unitToUse);
|
||
},
|
||
}
|
||
},
|
||
},
|
||
};
|
||
|
||
return [primeVueSetData, primeVueSetOption]
|
||
},
|
||
...mapActions(PerformanceStore, [
|
||
'setFreqChartData',
|
||
'setFreqChartOptions',
|
||
'setFreqChartXData'
|
||
]),
|
||
},
|
||
async created() {
|
||
this.isLoading = true; // moubeted 才停止 loading
|
||
const routeParams = this.$route.params;
|
||
const isCheckPage = this.$route.name.includes('Check');
|
||
const type = routeParams.type;
|
||
const file = this.$route.meta.file;
|
||
let id;
|
||
|
||
if(!isCheckPage) {
|
||
id = await routeParams.fileId;
|
||
} else {
|
||
id = await file.parent.id;
|
||
}
|
||
|
||
// 取得 Performance Data
|
||
await this.performanceStore.getPerformance(type, id);
|
||
this.avgProcessTimeByTaskHeight = await this.getHorizontalBarHeight(this.performanceData.time.avg_process_time_by_task);
|
||
if(this.performanceData.time.avg_waiting_time_by_edge !== null) {
|
||
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.getExplicitDeclaredLineChart(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.getExplicitDeclaredLineChart(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.getExplicitDeclaredLineChart(
|
||
this.performanceData.time.avg_waiting_time, this.contentData.avgWaitingTime, 'date');
|
||
if(this.performanceData.time.avg_waiting_time_by_edge !== null) {
|
||
[this.avgWaitingTimeByEdgeData, this.avgWaitingTimeByEdgeOptions] = this.getHorizontalBarChart(
|
||
this.performanceData.time.avg_waiting_time_by_edge, this.contentData.avgWaitingTimeByEdge, false, 'date');
|
||
} else {
|
||
[this.avgWaitingTimeByEdgeData, this.avgWaitingTimeByEdgeOptions] = [null, null]
|
||
}
|
||
|
||
[this.casesByTaskData, this.casesByTaskOptions] = this.getHorizontalBarChart(this.performanceData.freq.cases_by_task,
|
||
this.contentData.casesByTask, true, 'count');
|
||
// 停止 loading
|
||
this.isLoading = false;
|
||
},
|
||
async beforeRouteEnter(to, from, next) {
|
||
const isCheckPage = to.name.includes('Check');
|
||
|
||
if (isCheckPage) {
|
||
const conformanceStore = ConformanceStore();
|
||
switch (to.params.type) {
|
||
case 'log':
|
||
conformanceStore.conformanceLogCreateCheckId = to.params.fileId;
|
||
break;
|
||
case 'filter':
|
||
conformanceStore.conformanceFilterCreateCheckId = to.params.fileId;
|
||
break;
|
||
}
|
||
await conformanceStore.getConformanceReport(true);
|
||
to.meta.file = conformanceStore.routeFile; // 將 file data 存到 route
|
||
}
|
||
next();
|
||
}
|
||
}
|
||
</script>
|
||
<style scoped>
|
||
@reference "../../../assets/tailwind.css";
|
||
.active {
|
||
@apply text-primary
|
||
}
|
||
</style>
|