Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RaRBJFZKSTA7naHGuQHfnQ
1148 lines
33 KiB
Vue
1148 lines
33 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>
|
|
// The Lucia project.
|
|
// Copyright 2024-2026 DSP, inc. All rights reserved.
|
|
// Authors:
|
|
// chiayin.kuo@dsp.im (chiayin), 2023/1/31
|
|
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30
|
|
// imacat.yang@dsp.im (imacat), 2023/9/23
|
|
/**
|
|
* @module views/Discover/Performance
|
|
* Performance analysis page with activity duration
|
|
* charts, frequency data, and statistical summaries.
|
|
*/
|
|
|
|
import { useConformanceStore } from "@/stores/conformance";
|
|
|
|
export default {
|
|
async beforeRouteEnter(to, from, next) {
|
|
const isCheckPage = to.name.includes("Check");
|
|
|
|
if (isCheckPage) {
|
|
const conformanceStore = useConformanceStore();
|
|
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.allRouteFile;
|
|
}
|
|
next();
|
|
},
|
|
};
|
|
</script>
|
|
<script setup>
|
|
import { ref, onMounted } from "vue";
|
|
import { useRoute } from "vue-router";
|
|
import { storeToRefs } from "pinia";
|
|
import moment from "moment";
|
|
import { useLoadingStore } from "@/stores/loading";
|
|
import { usePerformanceStore } from "@/stores/performance";
|
|
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;
|
|
|
|
const route = useRoute();
|
|
|
|
// Stores
|
|
const loadingStore = useLoadingStore();
|
|
const performanceStore = usePerformanceStore();
|
|
const { isLoading } = storeToRefs(loadingStore);
|
|
const { performanceData } = storeToRefs(performanceStore);
|
|
|
|
// Data
|
|
const timeUsageData = [
|
|
{ tagId: "#cycleTime", label: "Cycle Time & Efficiency" },
|
|
{ tagId: "#processingTime", label: "Processing Time" },
|
|
{ tagId: "#waitingTime", label: "Waiting Time" },
|
|
];
|
|
const frequencyData = [{ tagId: "#cases", label: "Number of Cases" }];
|
|
const 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",
|
|
},
|
|
};
|
|
const 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,
|
|
},
|
|
};
|
|
|
|
const isActive = ref(null);
|
|
const avgCycleTimeData = ref(null);
|
|
const avgCycleTimeOptions = ref(null);
|
|
const avgCycleEfficiencyData = ref(null);
|
|
const avgCycleEfficiencyOptions = ref(null);
|
|
const avgProcessTimeData = ref(null);
|
|
const avgProcessTimeOptions = ref(null);
|
|
const avgProcessTimeByTaskData = ref(null);
|
|
const avgProcessTimeByTaskOptions = ref(null);
|
|
const avgWaitingTimeData = ref(null);
|
|
const avgWaitingTimeOptions = ref(null);
|
|
const avgWaitingTimeByEdgeData = ref(null);
|
|
const avgWaitingTimeByEdgeOptions = ref(null);
|
|
const freqData = ref(null);
|
|
const freqOptions = ref(null);
|
|
const casesByTaskData = ref(null);
|
|
const casesByTaskOptions = ref(null);
|
|
const horizontalBarHeight = 500;
|
|
const avgProcessTimeByTaskHeight = ref(500);
|
|
const avgWaitingTimeByEdgeHeight = ref(500);
|
|
const casesByTaskHeight = ref(500);
|
|
|
|
// Methods
|
|
/**
|
|
* Scrolls to the selected chart section.
|
|
* @param {string} tagId - The anchor tag ID to navigate to.
|
|
*/
|
|
function handleClick(tagId) {
|
|
isActive.value = tagId;
|
|
|
|
if (isSafeTagId(tagId)) {
|
|
globalThis.location.href = tagId;
|
|
} else {
|
|
console.warn("Unsafe tagId: ", tagId);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validates that a tag ID is safe for navigation.
|
|
* @param {string} tagId - The tag ID to validate.
|
|
* @returns {boolean} True if the tag ID is safe.
|
|
*/
|
|
function isSafeTagId(tagId) {
|
|
const pattern = /^#?[a-zA-Z0-9-]*$/;
|
|
return pattern.test(tagId);
|
|
}
|
|
|
|
/**
|
|
* Generates evenly-spaced X-axis label timestamps.
|
|
* @param {object} valueData - Object with min and max date strings.
|
|
* @returns {Array<number>} Array of timestamp values.
|
|
*/
|
|
function 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;
|
|
}
|
|
|
|
/**
|
|
* Calculates the height for a horizontal bar chart based on bar count.
|
|
* @param {object} chartData - The chart data.
|
|
* @returns {string} The CSS height string.
|
|
*/
|
|
function getHorizontalBarHeight(chartData) {
|
|
const totalBars = chartData.data.length;
|
|
let hBarHeight = horizontalBarHeight;
|
|
|
|
if (totalBars > 10) hBarHeight = (totalBars - 10) * 16 + horizontalBarHeight;
|
|
|
|
return hBarHeight + "px";
|
|
}
|
|
|
|
/**
|
|
* Builds a bar chart configuration for Chart.js.
|
|
* @param {object} chartData - The chart data from the API.
|
|
* @param {object} content - The axis label content.
|
|
* @returns {Array} [chartData, chartOptions] tuple.
|
|
*/
|
|
function getBarChart(chartData, content) {
|
|
const getMoment = (time) => 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,
|
|
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];
|
|
}
|
|
|
|
/**
|
|
* Builds a horizontal bar chart configuration for Chart.js.
|
|
* @param {object} chartData - The chart data from the API.
|
|
* @param {object} content - The axis label content.
|
|
* @param {boolean} isSingle - Whether labels are single values.
|
|
* @param {string} xUnit - 'date' or 'count'.
|
|
* @returns {Array} [chartData, chartOptions] tuple.
|
|
*/
|
|
function 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,
|
|
color: "#64748b",
|
|
},
|
|
grid: {
|
|
color: "#64748b",
|
|
},
|
|
border: {
|
|
display: false,
|
|
},
|
|
},
|
|
y: {
|
|
beginAtZero: true,
|
|
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;
|
|
primeVueSetOption.plugins.tooltip.callbacks.label = function (context) {
|
|
return `${content.x}: ${context.parsed.x}`;
|
|
};
|
|
break;
|
|
}
|
|
if (isSingle) {
|
|
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 {
|
|
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];
|
|
}
|
|
|
|
/**
|
|
* Customizes Chart.js scale options with content and tick data.
|
|
* @param {object} whichScaleObj - The base scale options object.
|
|
* @param {object} options - Options containing content and ticksOfXAxis.
|
|
* @returns {object} The customized scale options.
|
|
*/
|
|
function getCustomizedScaleOption(
|
|
whichScaleObj,
|
|
{ customizeOptions: { content, ticksOfXAxis } },
|
|
) {
|
|
let resultScaleObj;
|
|
resultScaleObj = customizeScaleChartOptionTitleByContent(
|
|
whichScaleObj,
|
|
content,
|
|
);
|
|
resultScaleObj = customizeScaleChartOptionTicks(resultScaleObj, ticksOfXAxis);
|
|
return resultScaleObj;
|
|
}
|
|
|
|
/**
|
|
* Sets axis titles on a scale options object.
|
|
* @param {object} whichScaleObj - The base scale options.
|
|
* @param {object} content - Object with x and y axis title text.
|
|
* @returns {object} The scale options with updated titles.
|
|
*/
|
|
function customizeScaleChartOptionTitleByContent(whichScaleObj, content) {
|
|
if (!content) {
|
|
return whichScaleObj;
|
|
}
|
|
|
|
return {
|
|
...whichScaleObj,
|
|
x: {
|
|
...whichScaleObj.x,
|
|
title: {
|
|
...whichScaleObj.x.title,
|
|
text: content.x,
|
|
},
|
|
},
|
|
y: {
|
|
...whichScaleObj.y,
|
|
title: {
|
|
...whichScaleObj.y.title,
|
|
text: content.y,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Sets custom tick callbacks on a scale options object.
|
|
* @param {object} scaleObjectToAlter - The scale options to modify.
|
|
* @param {Array} ticksOfXAxis - The formatted tick labels.
|
|
* @returns {object} The scale options with custom ticks.
|
|
*/
|
|
function customizeScaleChartOptionTicks(scaleObjectToAlter, ticksOfXAxis) {
|
|
return {
|
|
...scaleObjectToAlter,
|
|
x: {
|
|
...scaleObjectToAlter.x,
|
|
ticks: {
|
|
...scaleObjectToAlter.x.ticks,
|
|
callback: function (value, index) {
|
|
return ticksOfXAxis[index];
|
|
},
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Builds a line chart with explicit scale declarations.
|
|
* @param {object} chartData - The chart data from the API.
|
|
* @param {object} content - The axis label content.
|
|
* @param {string} yUnit - 'date' or 'count'.
|
|
* @returns {Array} [chartData, chartOptions] tuple.
|
|
*/
|
|
function 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 = setXLabelsData(chartData.x_axis);
|
|
|
|
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",
|
|
minute: "M/d h:mm",
|
|
hour: "M/d h:mm",
|
|
day: "M/d h",
|
|
month: "y/M/d",
|
|
},
|
|
round: true,
|
|
},
|
|
ticks: {
|
|
maxTicksLimit: primeVueTicksLimit,
|
|
padding: 8,
|
|
display: true,
|
|
maxRotation: 0,
|
|
color: "#64748b",
|
|
source: "labels",
|
|
callback: function (value, index) {
|
|
return ticksOfXAxis[index];
|
|
},
|
|
},
|
|
border: {
|
|
color: "#64748b",
|
|
},
|
|
},
|
|
y: {
|
|
beginAtZero: true,
|
|
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) {
|
|
const { resultStepSize, unitToUse } = getStepSizeOfYTicks(
|
|
ticks[ticks.length - 1].value,
|
|
ticks.length,
|
|
);
|
|
return getYTicksByIndex(resultStepSize, index, unitToUse);
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
return [primeVueSetData, primeVueSetOption];
|
|
}
|
|
|
|
/**
|
|
* Builds a line chart for average waiting time data.
|
|
* @param {object} chartData - The chart data from the API.
|
|
* @param {object} content - The axis label content.
|
|
* @param {string} yUnit - 'date' or 'count'.
|
|
* @returns {Array} [chartData, chartOptions] tuple.
|
|
*/
|
|
function getAvgWaitingTimeLineChart(chartData, content, yUnit) {
|
|
const getMoment = (time) => moment(time).format("YYYY/M/D HH:mm:ss");
|
|
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 = setXLabelsData(chartData.x_axis);
|
|
|
|
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",
|
|
minute: "M/d h:mm",
|
|
hour: "M/d h:mm",
|
|
day: "M/d h",
|
|
month: "y/M/d",
|
|
},
|
|
round: true,
|
|
},
|
|
ticks: {
|
|
display: true,
|
|
maxRotation: 0,
|
|
color: "#64748b",
|
|
source: "labels",
|
|
callback: function (value, index) {
|
|
return ticksOfXAxis[index];
|
|
},
|
|
},
|
|
border: {
|
|
color: "#64748b",
|
|
},
|
|
},
|
|
y: {
|
|
beginAtZero: true,
|
|
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) {
|
|
const { resultStepSize, unitToUse } = getStepSizeOfYTicks(
|
|
ticks[ticks.length - 1].value,
|
|
ticks.length,
|
|
);
|
|
return getYTicksByIndex(resultStepSize, index, unitToUse);
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
return [primeVueSetData, primeVueSetOption];
|
|
}
|
|
|
|
onMounted(async () => {
|
|
isLoading.value = true;
|
|
try {
|
|
const routeParams = route.params;
|
|
const isCheckPage = route.name.includes("Check");
|
|
const type = routeParams.type;
|
|
const file = route.meta.file;
|
|
let id;
|
|
|
|
if (isCheckPage) {
|
|
id = file.parent?.id;
|
|
} else {
|
|
id = routeParams.fileId;
|
|
}
|
|
|
|
// Fetch Performance Data
|
|
await performanceStore.getPerformance(type, id);
|
|
if (!performanceData.value?.time) {
|
|
return;
|
|
}
|
|
avgProcessTimeByTaskHeight.value = getHorizontalBarHeight(
|
|
performanceData.value.time.avg_process_time_by_task,
|
|
);
|
|
if (performanceData.value.time.avg_waiting_time_by_edge !== null) {
|
|
avgWaitingTimeByEdgeHeight.value = getHorizontalBarHeight(
|
|
performanceData.value.time.avg_waiting_time_by_edge,
|
|
);
|
|
}
|
|
casesByTaskHeight.value = getHorizontalBarHeight(
|
|
performanceData.value.freq.cases_by_task,
|
|
);
|
|
// create chart
|
|
[avgCycleTimeData.value, avgCycleTimeOptions.value] =
|
|
getExplicitDeclaredLineChart(
|
|
performanceData.value.time.avg_cycle_time,
|
|
contentData.avgCycleTime,
|
|
"date",
|
|
);
|
|
[avgCycleEfficiencyData.value, avgCycleEfficiencyOptions.value] =
|
|
getBarChart(
|
|
performanceData.value.time.avg_cycle_efficiency,
|
|
contentData.avgCycleEfficiency,
|
|
);
|
|
[avgProcessTimeData.value, avgProcessTimeOptions.value] =
|
|
getExplicitDeclaredLineChart(
|
|
performanceData.value.time.avg_process_time,
|
|
contentData.avgProcessTime,
|
|
"date",
|
|
);
|
|
[avgProcessTimeByTaskData.value, avgProcessTimeByTaskOptions.value] =
|
|
getHorizontalBarChart(
|
|
performanceData.value.time.avg_process_time_by_task,
|
|
contentData.avgProcessTimeByTask,
|
|
true,
|
|
"date",
|
|
);
|
|
[avgWaitingTimeData.value, avgWaitingTimeOptions.value] =
|
|
getExplicitDeclaredLineChart(
|
|
performanceData.value.time.avg_waiting_time,
|
|
contentData.avgWaitingTime,
|
|
"date",
|
|
);
|
|
if (performanceData.value.time.avg_waiting_time_by_edge === null) {
|
|
[avgWaitingTimeByEdgeData.value, avgWaitingTimeByEdgeOptions.value] = [
|
|
null,
|
|
null,
|
|
];
|
|
} else {
|
|
[avgWaitingTimeByEdgeData.value, avgWaitingTimeByEdgeOptions.value] =
|
|
getHorizontalBarChart(
|
|
performanceData.value.time.avg_waiting_time_by_edge,
|
|
contentData.avgWaitingTimeByEdge,
|
|
false,
|
|
"date",
|
|
);
|
|
}
|
|
|
|
[casesByTaskData.value, casesByTaskOptions.value] = getHorizontalBarChart(
|
|
performanceData.value.freq.cases_by_task,
|
|
contentData.casesByTask,
|
|
true,
|
|
"count",
|
|
);
|
|
} catch (error) {
|
|
console.error("Failed to initialize performance:", error);
|
|
} finally {
|
|
isLoading.value = false;
|
|
}
|
|
});
|
|
</script>
|
|
<style scoped>
|
|
@reference "../../../assets/tailwind.css";
|
|
.active {
|
|
@apply text-primary;
|
|
}
|
|
</style>
|