Files
lucia-frontend/src/views/Compare/Dashboard/index.vue
Cindy Chang 6c9322d1bc fix: #258
2024-06-18 09:25:13 +08:00

1318 lines
49 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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">{{ i18next.t("Compare.timeUsage") }}</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">{{ i18next.t("Compare.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 pr-2 mr-2 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">
{{ i18next.t("Compare.schedule") }}
</span>
{{ i18next.t("Compare.timeUsage") }}
</p>
<ul class="list-disc list-inside px-4 pl-7 text-sm">
<li id="cycleTime" class="scroll-smooth">
<span class="inline-block py-4">{{ i18next.t("Compare.cycleEfficiency") }}</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">
{{ i18next.t("Compare.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 text-sm">{{ i18next.t("Compare.ProcessingTime") }}</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">{{ i18next.t("Compare.WaitingTime") }}</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">{{ i18next.t("Compare.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">{{ i18next.t("Compare.NoWaitingTime") }}</p>
</div>
</div>
</li>
</ul>
</li>
</ul>
</section>
<section>
<p class="h1 px-4 border-b border-neutral-900 mt-2"><span class="material-symbols-outlined mr-2 align-middle">
{{i18next.t("Compare.moving")}}
</span>
{{ i18next.t("Compare.frequency") }}
</p>
<ul class="list-disc list-inside px-4 pl-7 text-sm">
<li id="cases">
<span class="inline-block py-4">{{ i18next.t("Compare.NumberOfCases") }}</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>
<!-- UNUSED <Chart type="line" :data="freqData" :options="freqOptions" class="h-96" /> -->
<FreqChart v-if="compareDashboardData" :chartData="compareDashboardData?.freq?.cases" :content="contentData?.freq" yUnit="count"
pageName='Compare'
/>
</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>
<!-- Sidebar: State -->
<div class="bg-transparent z-10">
<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="compareState">
<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>
</div>
</div>
</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, getDateLabelByMinMaxDate,
setTimeStringFormatBaseOnTimeDifference,
mapTimestampToAxisTicksByFormat,
getStepSizeOfYTicks,
getYTicksByIndex,
} from '@/module/timeLabel.js';
import i18next from '@/i18n/i18n';
import {
knownLayoutChartOption,
knownScaleLineChartOptions,
knownScaleHorizontalChartOptions,
knownScaleBarChartOptions,
} from "@/constants/constants.js";
import FreqChart from '../../Discover/Performance/FreqChart.vue';
export default {
setup() {
const loadingStore = LoadingStore();
const compareStore = CompareStore();
const { isLoading } = storeToRefs(loadingStore);
const { compareDashboardData } = storeToRefs(compareStore);
return { isLoading, compareStore, compareDashboardData }
},
components: {
SidebarStates,
FreqChart,
},
data() {
return {
i18next: i18next,
timeUsageData: [
{tagId: '#cycleTime', label: i18next.t("Compare.cycleEfficiency")},
{tagId: '#processingTime', label: i18next.t("Compare.ProcessingTime")},
{tagId: '#waitingTime', label: i18next.t("Compare.WaitingTime")},
],
frequencyData: [
{tagId: '#cases', label: i18next.t("Compare.NumberOfCases")},
// {tagId: '#trace', label: 'Number of Trace'},
// {tagId: '#resource', label: 'Resource'},
],
contentData: {
avgCycleTime: {title: i18next.t("Compare.avgCycleTimeTitle"), x: i18next.t("Compare.labelDateX"),
y: i18next.t("Compare.avgCycleTimeY")},
avgCycleEfficiency: {title: i18next.t("Compare.avgCycleEfficiencyTitle"), x: i18next.t("Compare.labelDateX"),
y: i18next.t("Compare.avgCycleEfficiencyY")},
avgProcessTime: {title: i18next.t("Compare.avgProcessTimeTitle"), x: i18next.t("Compare.labelDateX"),
y: i18next.t("Compare.avgProcessTimeY")},
avgProcessTimeByTask: {title: i18next.t("Compare.avgProcessTimeByTaskTitle"),
x: i18next.t("Compare.avgProcessTimeByTaskX"), y: i18next.t("Compare.labelActivityY")},
avgWaitingTime: {title: i18next.t("Compare.avgWaitingTimeTitle"), x: i18next.t("Compare.labelDateX"),
y: i18next.t("Compare.avgWaitingTimeY")},
avgWaitingTimeByEdge: {title: i18next.t("Compare.avgWaitingTimeByEdgeTitle"),
x: i18next.t("Compare.avgWaitingTimeByEdgeX"),
y: i18next.t("Compare.avgWaitingTimeByEdgeY")},
freq: {title: i18next.t("Compare.freqTitle"), x: i18next.t("Compare.labelDateX"), y: i18next.t("Compare.freqY")},
casesByTask: {title: i18next.t("Compare.casesByTaskTitle"), x: i18next.t("Compare.casesByTaskX"),
y: i18next.t("Compare.labelActivityY")},
},
tooltip: {
avgCycleEfficiency: {
value: i18next.t("Compare.avgCycleEfficiency"),
class: '!max-w-[212px] !text-[10px] !opacity-80',
autoHide: false,
},
avgWaitingTime: {
value: i18next.t("Compare.avgWaitingTime"),
class: '!max-w-[212px] !text-[10px] !opacity-80',
autoHide: false,
},
avgWaitingTimeByEdge: {
value: i18next.t("Compare.avgWaitingTimeByEdge"),
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,
colorPrimary: '#0099FF',
colorSecondary: '#FFAA44',
sidebarState: false, // SideBar: Summary
}
},
methods: {
/**
* 手刻折線圖 x label 時間刻度
* @param { object } valueData {min: '2022-02-20T19:54:12', max: '2023-11-27T07:21:53'}
*/
setXLabelsData(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.x_axis.labels.length;
let horizontalBar = this.horizontalBarHeight;
if(totalBars > 10){
horizontalBar = (totalBars - 10) * 16 * 2 + this.horizontalBarHeight
};
return horizontalBar + 'px'
},
/**
* 建立折線圖
* @param { object } chartData chart data
* @param { object } content titels 標題文字
* @param { string } yUnit y 軸單位 'date' | 'count',可傳入以上任一。
* @returns { [primeVueSetData, primeVueSetOption] } 這兩者為符合 primeVue 圖表格式的數據資料
*/
getLineChart(chartData, content, yUnit) {
let datasetsPrimary;
let datasetsSecondary;
let minX = chartData.x_axis.min;
let maxX = chartData.x_axis.max;
let maxY = chartData.y_axis.max;
let xLabelData;
let labelPrimary = chartData.data[0].label;
let labelSecondary = chartData.data[1].label;
let primeVueSetData = {};
let primeVueSetOption = {};
const getMoment = (time)=> {
return this.$moment(time).format('YYYY/M/D hh:mm:ss')
};
const getSimpleTimeLabel = simpleTimeLabel;
const getFollowTimeLabel = followTimeLabel;
// fabricate x label according to y unit
switch (yUnit) {
case 'date':
datasetsPrimary = setLineChartData(chartData.data[0].data, chartData.x_axis.max, chartData.x_axis.min, false,
chartData.y_axis.max, chartData.y_axis.min);
datasetsSecondary = setLineChartData(chartData.data[1].data, chartData.x_axis.max, chartData.x_axis.min, false,
chartData.y_axis.max, chartData.y_axis.min);
xLabelData = this.setXLabelsData(chartData.x_axis);
break;
case 'count': // 次數 10 個點
datasetsPrimary = chartData.data[0].data;
datasetsSecondary = chartData.data[1].data;
xLabelData = chartData.data[0].data.map(item => new Date(item.x).getTime());
break;
}
// 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(xLabelData, formatToSet);
const customizedScaleOption = this.getCustomizedScaleOption(
knownScaleLineChartOptions, {
customizeOptions: {
content, ticksOfXAxis,
}
});
primeVueSetData = {
labels: xLabelData,
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,
}
]
};
primeVueSetOption = {
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)}`;
},
},
},
title: {
display: false,
},
},
scales: customizedScaleOption,
};
switch (yUnit) {
case 'date':
primeVueSetOption.plugins.tooltip.callbacks.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}`;
}
};
primeVueSetOption.scales.x.min = minX;
primeVueSetOption.scales.x.max = maxX;
primeVueSetOption.scales.y.ticks.callback = function (value, index, ticks) {
const {resultStepSize: YTickStepSize, unitToUse} = getStepSizeOfYTicks(maxY, ticks.length); // Stepsize only needs to be calculated once
return getYTicksByIndex(YTickStepSize, index, unitToUse);
}
break;
case 'count':
primeVueSetOption.scales.y.ticks.precision = 0; // y 軸顯示小數點後 0 位
primeVueSetOption.plugins.tooltip.callbacks.label = function(context) {
let value = context.parsed.y;
switch (context.datasetIndex) {
case 0: // Primary
return `${labelPrimary}: ${value}`;
case 1: // Secondary
return `${labelSecondary}: ${value}`;
}
};
break;
}
// JSON.parse(JSON.stringify()) wouldn't work
return [primeVueSetData, primeVueSetOption];
},
/**
* 建立長條圖
* @param { object } chartData chart data
* @param { object } content titles 文字標題
*/
getBarChart(chartData, content, caller) {
const maxX = chartData.x_axis.max;
const minX = chartData.x_axis.min;
const getMoment = (time)=> this.$moment(time).format('YYYY/MM/DD');
const getDateLabel = getDateLabelByMinMaxDate;
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 primeVueSetData = {};
let primeVueSetOption = {};
// 轉為百分比
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);
primeVueSetData = {
labels: xDataPrimary,
datasets: [
{
label: labelPrimary,
data: yDataPrimary,
backgroundColor: this.colorPrimary,
},
{
label: labelSecondary,
data: yDataSecondary,
backgroundColor: this.colorSecondary,
},
]
};
primeVueSetOption = {
responsive: true,
maintainAspectRatio: false,
layout: knownLayoutChartOption,
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: this.customizeScaleChartOptionTitleByContent(knownScaleBarChartOptions, content),
};
if(caller === "Cycle Eff") { //針對 cycle efficiency 特別處理
primeVueSetOption.scales.y.reverse = true; //不明原因上下顛倒,所以要顛倒過來
primeVueSetOption.scales.y.ticks.callback = function (value, index, ticks) {
return 10 * index; //百分比
}
}
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;
const labelPrimary = chartData.data[0].label;
const labelSecondary = chartData.data[1].label;
let primeVueSetData = {};
let primeVueSetOption = {};
// 大到小排序: 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 xLabelData = datasetsPrimary.map(item => item.x);
const yDataPrimary = datasetsPrimary.map(item => item.y);
const yDataSecondary = datasetsSecondary.map(item => item.y);
primeVueSetData = {
labels: xLabelData,
datasets: [
{
label: labelPrimary,
data: yDataPrimary,
backgroundColor: this.colorPrimary,
},
{
label: labelSecondary,
data: yDataSecondary,
backgroundColor: this.colorSecondary,
},
]
};
primeVueSetOption = {
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: {}
},
title: {
display: false,
},
},
scales: this.customizeScaleChartOptionTitleByContent(knownScaleHorizontalChartOptions, null),
};
switch (xUnit) {
case 'date':
primeVueSetOption.plugins.tooltip.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}`;
}
};
primeVueSetOption.scales.x.ticks.callback = function (value, index, ticks) {
return getFollowTimeLabel(value, maxY, 1)
};
break;
case 'count':
primeVueSetOption.scales.x.ticks.precision = 0; // x 軸顯示小數點後 0 位
primeVueSetOption.plugins.tooltip.callbacks.label = function(context) {
let value = context.parsed.y;
value = context.parsed.x === null ? "n/a" : context.parsed.x;
switch (context.datasetIndex) {
case 0: // Primary
return `${labelPrimary}: ${value}`;
case 1: // Secondary
return `${labelSecondary}: ${value}`;
}
};
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) {
let label = xLabelData[index];
if(label) {
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) {
let label = xLabelData[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]
},
/**
*
* @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;
},
/**
* 在一個基本的物件上加以客製化這個物件,客製化的參照來源是 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
}
}
};
},
/**
*
* @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) {
// 根據不同的級距客製化 x 軸的時間刻度
return ticksOfXAxis[index];
},
},
},
};
},
/**
* 建立Compare頁面的折線圖並且避免同一個畫面中的設定值彼此覆蓋
* @param { object } chartData chart data
* @param { object } content titels 標題文字
* @param { string } yUnit y 軸單位 'date' | 'count',可傳入以上任一。
* @returns { [primeVueSetData, primeVueSetOption] } 這兩者為符合 primeVue 圖表格式的數據資料
*/
getLineChart(chartData, content, yUnit) {
let datasetsPrimary;
let datasetsSecondary;
let minX = chartData.x_axis.min;
let maxX = chartData.x_axis.max;
let maxY = chartData.y_axis.max;
let xLabelData;
let labelPrimary = chartData.data[0].label;
let labelSecondary = chartData.data[1].label;
let primeVueSetData = {};
let primeVueSetOption = {};
const getMoment = (time)=> {
return this.$moment(time).format('YYYY/M/D hh:mm:ss')
};
const getSimpleTimeLabel = simpleTimeLabel;
const getFollowTimeLabel = followTimeLabel;
// fabricate x label according to y unit
switch (yUnit) {
case 'date':
datasetsPrimary = setLineChartData(chartData.data[0].data, chartData.x_axis.max, chartData.x_axis.min, false,
chartData.y_axis.max, chartData.y_axis.min);
datasetsSecondary = setLineChartData(chartData.data[1].data, chartData.x_axis.max, chartData.x_axis.min, false,
chartData.y_axis.max, chartData.y_axis.min);
xLabelData = this.setXLabelsData(chartData.x_axis);
break;
case 'count': // 次數 10 個點
datasetsPrimary = chartData.data[0].data;
datasetsSecondary = chartData.data[1].data;
xLabelData = chartData.data[0].data.map(item => new Date(item.x).getTime());
break;
}
// 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(xLabelData, formatToSet);
primeVueSetData = {
labels: xLabelData,
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,
}
]
};
primeVueSetOption = {
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)}`;
},
},
},
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: {
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(maxY, ticks.length);
return getYTicksByIndex(resultStepSize, index, unitToUse);
},
}
},
},
plugins: {
tooltip: {
callbacks:{
label:function(context) {
return `${content.y}: ${getSimpleTimeLabel(context.parsed.y, 2)}`;
},
},
},
},
};
return [primeVueSetData, primeVueSetOption];
},
/**
* 建立Case By Task水平長條圖
* 避免共用 PrimeVue 設定值,避免覆蓋設定
* @param { object } chartData chart data
* @param { object } content titels 標題的文字
* @param { boolean } isSingle 單個或雙數 activity
* @param { string } xUnit x 軸單位
*/
getCaseByTaskHorizontalBarChart(chartData, content, isSingle, xUnit = 'count') {
const maxX = chartData.x_axis.max;
const getSimpleTimeLabel = simpleTimeLabel;
const getFollowTimeLabel = followTimeLabel;
const labelPrimary = chartData.data[0].label;
const labelSecondary = chartData.data[1].label;
let primeVueSetData = {};
let primeVueSetOption = {};
// 大到小排序: 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 xLabelData = datasetsPrimary.map(item => item.x);
const yDataPrimary = datasetsPrimary.map(item => item.y);
const yDataSecondary = datasetsSecondary.map(item => item.y);
primeVueSetData = {
labels: xLabelData,
datasets: [
{
label: labelPrimary,
data: yDataPrimary,
backgroundColor: this.colorPrimary,
},
{
label: labelSecondary,
data: yDataSecondary,
backgroundColor: this.colorSecondary,
},
]
};
primeVueSetOption = {
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: {}
},
title: {
display: false,
},
},
scales: {
x: {
title: {
display: true,
color: '#334155',
font: {
size: 12,
lineHeight: 2
},
text: content.x,
},
ticks: {
display: true,
maxRotation: 0, // 不旋轉 lable 0~50
color: '#64748b',
callback: function(value, index, ticks) {
return value; // 計算數量,沒有時間單位才對
}
},
grid: {
color: '#64748b',
tickLength: 0, // 網格是否超過邊線
},
border: {
display:false,
},
},
y: {
beginAtZero: true, // scale 包含 0
type: 'category',
title: {
display: true,
color: '#334155',
font: {
size: 12,
lineHeight: 2
},
text: content.y,
},
ticks:{
color: '#64748b',
padding: 8,
},
grid: {
display:false,
color: '#64748b',
},
border: {
display: false, // 隱藏左側多出來的線
},
},
},
};
switch (xUnit) {
case 'count':
default:
primeVueSetOption.scales.x.ticks.precision = 0; // x 軸顯示小數點後 0 位
primeVueSetOption.plugins.tooltip.callbacks.label = function(context) {
let value = context.parsed.y;
value = context.parsed.x === null ? "n/a" : context.parsed.x;
switch (context.datasetIndex) {
case 0: // Primary
return `${labelPrimary}: ${value}`;
case 1: // Secondary
return `${labelSecondary}: ${value}`;
}
};
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) {
let label = xLabelData[index];
if(label) {
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) {
let label = xLabelData[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]
},
/**
* 建立 Average Processing Time水平長條圖
* 避免共用 PrimeVue 設定值,避免覆蓋設定
* @param { object } chartData chart data
* @param { object } content titels 標題的文字
* @param { boolean } isSingle 單個或雙數 activity
* @param { string } xUnit x 軸單位 'date' | 'count',可傳入以上任一。
*/
getAvgProcessTimeHorizontalBarChart(chartData, content, isSingle, xUnit="date") {
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 primeVueSetData = {};
let primeVueSetOption = {};
// 大到小排序: 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 xLabelData = datasetsPrimary.map(item => item.x);
const yDataPrimary = datasetsPrimary.map(item => item.y);
const yDataSecondary = datasetsSecondary.map(item => item.y);
primeVueSetData = {
labels: xLabelData,
datasets: [
{
label: labelPrimary,
data: yDataPrimary,
backgroundColor: this.colorPrimary,
},
{
label: labelSecondary,
data: yDataSecondary,
backgroundColor: this.colorSecondary,
},
]
};
primeVueSetOption = {
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: {}
},
title: {
display: false,
},
},
scales: {
x: {
title: {
display: true,
color: '#334155',
font: {
size: 12,
lineHeight: 2
},
text: content.x,
},
ticks: {
display: true,
maxRotation: 0, // 不旋轉 lable 0~50
color: '#64748b',
},
grid: {
color: '#64748b',
tickLength: 0, // 網格是否超過邊線
},
border: {
display:false,
},
},
y: {
beginAtZero: true, // scale 包含 0
type: 'category',
title: {
display: true,
color: '#334155',
font: {
size: 12,
lineHeight: 2
},
text: content.y
},
ticks:{
color: '#64748b',
padding: 8,
},
grid: {
display:false,
color: '#64748b',
},
border: {
display: false, // 隱藏左側多出來的線
},
},
},
};
switch (xUnit) {
case 'date':
default:
primeVueSetOption.plugins.tooltip.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}`;
}
};
primeVueSetOption.scales.x.ticks.callback = function (value, index, ticks) {
return getFollowTimeLabel(value, maxY, 1)
};
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) {
let label = xLabelData[index];
if(label) {
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) {
let label = xLabelData[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]
},
},
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);
if(this.compareDashboardData.time.avg_waiting_time_by_edge !== null) {
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.avgCycleTimeData, this.avgCycleTimeOptions] = this.getLineChart(
this.compareDashboardData.time.avg_cycle_time, this.contentData.avgCycleTime, 'date');
[this.avgCycleEfficiencyData, this.avgCycleEfficiencyOptions] = this.getBarChart(
this.compareDashboardData.time.avg_cycle_efficiency, this.contentData.avgCycleEfficiency, "Cycle Eff");
[this.avgProcessTimeData, this.avgProcessTimeOptions] = this.getLineChart(
this.compareDashboardData.time.avg_process_time, this.contentData.avgProcessTime, 'date');
[this.avgProcessTimeByTaskData, this.avgProcessTimeByTaskOptions] = this.getAvgProcessTimeHorizontalBarChart(
this.compareDashboardData.time.avg_process_time_by_task,
this.contentData.avgProcessTimeByTask, true, 'date');
[this.avgWaitingTimeData, this.avgWaitingTimeOptions] = this.getLineChart(
this.compareDashboardData.time.avg_waiting_time, this.contentData.avgWaitingTime, 'date');
if(this.compareDashboardData.time.avg_waiting_time_by_edge !== null) {
[this.avgWaitingTimeByEdgeData, this.avgWaitingTimeByEdgeOptions] = this.getHorizontalBarChart(
this.compareDashboardData.time.avg_waiting_time_by_edge,
this.contentData.avgWaitingTimeByEdge, false, 'date');
} else {
[this.avgWaitingTimeByEdgeData, this.avgWaitingTimeByEdgeOptions] = [null, null]
}
[this.freqData, this.freqOptions] = this.getLineChart(
this.compareDashboardData.freq.cases, this.contentData.freq, 'count');
[this.casesByTaskData, this.casesByTaskOptions] = this.getCaseByTaskHorizontalBarChart(
this.compareDashboardData.freq.cases_by_task, this.contentData.casesByTask, true, 'count');
},
mounted() {
// 停止 loading
this.isLoading = false;
}
}
</script>
<style scoped>
.active {
@apply text-primary
}
</style>