Apply repository-wide ESLint auto-fix formatting pass

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
2026-03-08 12:11:57 +08:00
parent 7c48faaa3d
commit 847904c49b
172 changed files with 13629 additions and 9154 deletions

View File

@@ -19,23 +19,23 @@ export default function abbreviateNumber(totalSeconds) {
let minutes = 0;
let hours = 0;
let days = 0;
let result = '';
let symbols = ['d', 'h', 'm', 's'];
let result = "";
let symbols = ["d", "h", "m", "s"];
totalSeconds = parseInt(totalSeconds);
if(!isNaN(totalSeconds)) {
if (!isNaN(totalSeconds)) {
seconds = totalSeconds % 60;
minutes = (Math.floor(totalSeconds - seconds) / 60) % 60;
hours = (Math.floor(totalSeconds / 3600)) % 24;
hours = Math.floor(totalSeconds / 3600) % 24;
days = Math.floor(totalSeconds / (3600 * 24));
};
}
const units = [days, hours, minutes, seconds];
for(let i = 0; i < units.length; i++) {
if(units[i] > 0) result += units[i] + symbols[i] + " ";
for (let i = 0; i < units.length; i++) {
if (units[i] > 0) result += units[i] + symbols[i] + " ";
}
result = result.trim();
if(totalSeconds === 0) result = '0';
if (totalSeconds === 0) result = "0";
return result;
};
}

View File

@@ -6,24 +6,27 @@
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30
/** @module alertModal SweetAlert2 modal dialogs for user interactions. */
import Swal from 'sweetalert2';
import { useAllMapDataStore } from '@/stores/allMapData';
import { useConformanceStore } from '@/stores/conformance';
import { useFilesStore } from '@/stores/files';
import { usePageAdminStore } from '@/stores/pageAdmin';
import { useModalStore } from '@/stores/modal';
import { escapeHtml } from '@/utils/escapeHtml.js';
import Swal from "sweetalert2";
import { useAllMapDataStore } from "@/stores/allMapData";
import { useConformanceStore } from "@/stores/conformance";
import { useFilesStore } from "@/stores/files";
import { usePageAdminStore } from "@/stores/pageAdmin";
import { useModalStore } from "@/stores/modal";
import { escapeHtml } from "@/utils/escapeHtml.js";
const customClass = {
container: '!z-[99999]',
popup: '!w-[564px]',
title: '!text-xl !font-semibold !mb-2',
htmlContainer: '!text-sm !font-normal !h-full !mb-4 !leading-5',
inputLabel: '!text-sm !font-normal',
input: '!h-8 !text-sm !font-normal !shadow-inner !shadow-black !border-neutral-200',
validationMessage: '!bg-neutral-10 !text-danger',
confirmButton: '!inline-block !rounded-full !text-sm !font-medium !text-center !align-middle !transition-colors !duration-300 !px-5 !py-2 !w-[100px] !h-[40px]',
cancelButton: '!inline-block !rounded-full !text-sm !font-medium !text-center !align-middle !transition-colors !duration-300 !px-5 !py-2 !w-[100px] !h-[40px] ',
container: "!z-[99999]",
popup: "!w-[564px]",
title: "!text-xl !font-semibold !mb-2",
htmlContainer: "!text-sm !font-normal !h-full !mb-4 !leading-5",
inputLabel: "!text-sm !font-normal",
input:
"!h-8 !text-sm !font-normal !shadow-inner !shadow-black !border-neutral-200",
validationMessage: "!bg-neutral-10 !text-danger",
confirmButton:
"!inline-block !rounded-full !text-sm !font-medium !text-center !align-middle !transition-colors !duration-300 !px-5 !py-2 !w-[100px] !h-[40px]",
cancelButton:
"!inline-block !rounded-full !text-sm !font-medium !text-center !align-middle !transition-colors !duration-300 !px-5 !py-2 !w-[100px] !h-[40px] ",
};
/**
* Shows a modal dialog to save a new filter with a user-provided name.
@@ -33,47 +36,51 @@ const customClass = {
* @returns {Promise<boolean>} True if the filter was saved, false otherwise.
*/
export async function saveFilter(addFilterId, next = null) {
let fileName = '';
let fileName = "";
const pageAdminStore = usePageAdminStore();
const { value, isConfirmed } = await Swal.fire({
title: 'SAVE NEW FILTER',
input: 'text',
inputPlaceholder: 'Enter Filter Name.',
title: "SAVE NEW FILTER",
input: "text",
inputPlaceholder: "Enter Filter Name.",
inputValue: fileName,
inputAttributes: {
'maxlength': 200,
maxlength: 200,
},
inputValidator: (value) => {
if (!value) return 'You need to write something!';
if (!value) return "You need to write something!";
fileName = value;
},
icon: 'info',
iconHtml: '<span class="material-symbols-outlined !text-[58px]">cloud_upload</span>',
iconColor: '#0099FF',
reverseButtons:true,
confirmButtonColor: '#0099FF',
icon: "info",
iconHtml:
'<span class="material-symbols-outlined !text-[58px]">cloud_upload</span>',
iconColor: "#0099FF",
reverseButtons: true,
confirmButtonColor: "#0099FF",
showCancelButton: true,
cancelButtonColor: '#94a3b8',
cancelButtonColor: "#94a3b8",
customClass: customClass,
});
// Determine whether to redirect based on the return value
if(isConfirmed) { // Save succeeded
if (isConfirmed) {
// Save succeeded
await addFilterId(fileName);
// Show save complete notification
if (value) { // Example of value: yes
if (value) {
// Example of value: yes
savedSuccessfully(value);
}
// Clear the input field
fileName = '';
fileName = "";
return true;
} else { // Clicked cancel or outside the dialog; save failed.
} else {
// Clicked cancel or outside the dialog; save failed.
pageAdminStore.keepPreviousPage();
// Not every time we have nontrivial next value
if (next !== null) {
next(false);
}
}
return false;
}
}
@@ -84,17 +91,17 @@ export async function saveFilter(addFilterId, next = null) {
* @returns {Promise<void>}
*/
export async function savedSuccessfully(value) {
value = value || '';
value = value || "";
await Swal.fire({
title: 'SAVE COMPLETE',
title: "SAVE COMPLETE",
html: `<span class="text-primary">${escapeHtml(value)}</span> has been saved in Lucia.`,
timer: 3000, // Auto-close after 3 seconds
showConfirmButton: false,
icon: 'success',
iconColor: '#0099FF',
customClass: customClass
})
};
icon: "success",
iconColor: "#0099FF",
customClass: customClass,
});
}
/**
* Prompts the user to save unsaved filter changes before leaving the
* Map page. Handles confirm (save), cancel (discard), and backdrop
@@ -112,23 +119,23 @@ export async function leaveFilter(next, addFilterId, toPath, logOut) {
const pageAdminStore = usePageAdminStore();
const result = await Swal.fire({
title: 'SAVE YOUR FILTER?',
html: 'If you want to continue using this filter in any other page, please select [Yes].',
icon: 'warning',
iconColor: '#FF3366',
reverseButtons:true,
confirmButtonText: 'Yes',
confirmButtonColor: '#FF3366',
title: "SAVE YOUR FILTER?",
html: "If you want to continue using this filter in any other page, please select [Yes].",
icon: "warning",
iconColor: "#FF3366",
reverseButtons: true,
confirmButtonText: "Yes",
confirmButtonColor: "#FF3366",
showCancelButton: true,
cancelButtonText: 'No',
cancelButtonColor: '#94a3b8',
cancelButtonText: "No",
cancelButtonColor: "#94a3b8",
customClass: customClass,
});
if(result.isConfirmed) {
if(allMapDataStore.createFilterId) {
if (result.isConfirmed) {
if (allMapDataStore.createFilterId) {
await allMapDataStore.updateFilter();
if(allMapDataStore.isUpdateFilter) {
if (allMapDataStore.isUpdateFilter) {
await savedSuccessfully(allMapDataStore.filterName);
}
} else {
@@ -137,7 +144,7 @@ export async function leaveFilter(next, addFilterId, toPath, logOut) {
}
logOut ? logOut() : next(toPath);
} else if(result.dismiss === 'cancel') {
} else if (result.dismiss === "cancel") {
// console.log('popup cancel case', );
// Handle page admin issue
// console.log("usePageAdminStore.activePage", usePageAdminStore.activePage);
@@ -145,18 +152,17 @@ export async function leaveFilter(next, addFilterId, toPath, logOut) {
allMapDataStore.tempFilterId = null;
logOut ? logOut() : next(toPath);
} else if(result.dismiss === 'backdrop') {
} else if (result.dismiss === "backdrop") {
// console.log('popup backdrop case', );
// Handle page admin issue
// console.log("usePageAdminStore.activePage", usePageAdminStore.activePage);
pageAdminStore.keepPreviousPage();
if(!logOut){
if (!logOut) {
next(false);
};
}
}
};
}
/**
* Shows a modal dialog to save a new conformance rule with a
* user-provided name.
@@ -166,37 +172,40 @@ export async function leaveFilter(next, addFilterId, toPath, logOut) {
* @returns {Promise<boolean>} True if the rule was saved, false otherwise.
*/
export async function saveConformance(addConformanceCreateCheckId) {
let fileName = '';
let fileName = "";
const { value, isConfirmed } = await Swal.fire({
title: 'SAVE NEW RULE',
input: 'text',
inputPlaceholder: 'Enter Rule Name.',
title: "SAVE NEW RULE",
input: "text",
inputPlaceholder: "Enter Rule Name.",
inputValue: fileName,
inputAttributes: {
'maxlength': 200,
maxlength: 200,
},
inputValidator: (value) => {
if (!value) return 'You need to write something!';
if (!value) return "You need to write something!";
fileName = value;
},
icon: 'info',
iconHtml: '<span class="material-symbols-outlined !text-[58px]">cloud_upload</span>',
iconColor: '#0099FF',
reverseButtons:true,
confirmButtonColor: '#0099FF',
icon: "info",
iconHtml:
'<span class="material-symbols-outlined !text-[58px]">cloud_upload</span>',
iconColor: "#0099FF",
reverseButtons: true,
confirmButtonColor: "#0099FF",
showCancelButton: true,
cancelButtonColor: '#94a3b8',
cancelButtonColor: "#94a3b8",
customClass: customClass,
});
// Determine whether to redirect based on the return value
if(isConfirmed) { // Save succeeded
if (isConfirmed) {
// Save succeeded
await addConformanceCreateCheckId(fileName);
// Show save complete notification
if (value) savedSuccessfully(value);
// Clear the input field
fileName = '';
fileName = "";
return true;
} else { // Clicked cancel or outside the dialog; save failed.
} else {
// Clicked cancel or outside the dialog; save failed.
return false;
}
}
@@ -212,10 +221,15 @@ export async function saveConformance(addConformanceCreateCheckId) {
* @param {Function} [logOut] - Optional logout function.
* @returns {Promise<void>}
*/
export async function leaveConformance(next, addConformanceCreateCheckId, toPath, logOut) {
export async function leaveConformance(
next,
addConformanceCreateCheckId,
toPath,
logOut,
) {
const conformanceStore = useConformanceStore();
const result = await showConfirmationDialog();
if (result.isConfirmed) {
await handleConfirmed(conformanceStore, addConformanceCreateCheckId);
} else {
@@ -228,16 +242,16 @@ export async function leaveConformance(next, addConformanceCreateCheckId, toPath
*/
async function showConfirmationDialog() {
return Swal.fire({
title: 'SAVE YOUR RULE?',
icon: 'warning',
iconColor: '#FF3366',
title: "SAVE YOUR RULE?",
icon: "warning",
iconColor: "#FF3366",
reverseButtons: true,
confirmButtonText: 'Yes',
confirmButtonColor: '#FF3366',
confirmButtonText: "Yes",
confirmButtonColor: "#FF3366",
showCancelButton: true,
cancelButtonText: 'No',
cancelButtonColor: '#94a3b8',
customClass: customClass
cancelButtonText: "No",
cancelButtonColor: "#94a3b8",
customClass: customClass,
});
}
@@ -248,7 +262,10 @@ async function showConfirmationDialog() {
* @returns {Promise<void>}
*/
async function handleConfirmed(conformanceStore, addConformanceCreateCheckId) {
if (conformanceStore.conformanceFilterCreateCheckId || conformanceStore.conformanceLogCreateCheckId) {
if (
conformanceStore.conformanceFilterCreateCheckId ||
conformanceStore.conformanceLogCreateCheckId
) {
await conformanceStore.updateConformance();
if (conformanceStore.isUpdateConformance) {
await savedSuccessfully(conformanceStore.conformanceFileName);
@@ -267,13 +284,19 @@ async function handleConfirmed(conformanceStore, addConformanceCreateCheckId) {
* @param {Function} [logOut] - Optional logout function.
* @returns {Promise<void>}
*/
async function handleDismiss(dismissType, conformanceStore, next, toPath, logOut) {
async function handleDismiss(
dismissType,
conformanceStore,
next,
toPath,
logOut,
) {
switch (dismissType) {
case 'cancel':
case "cancel":
resetTempCheckId(conformanceStore);
logOut ? logOut() : next(toPath);
break;
case 'backdrop':
case "backdrop":
if (!logOut) {
next(false);
}
@@ -304,38 +327,38 @@ function resetTempCheckId(conformanceStore) {
export async function uploadFailedFirst(failureType, failureMsg, failureLoc) {
// msg: 'not in UTF-8' | 'insufficient columns' | 'the csv file is empty' | 'the filename does not ends with .csv' | 'not a CSV file'
// type: 'encoding' | 'insufficient_columns' | 'empty' | 'name_suffix' | mime_type
let value = '';
let value = "";
switch (failureType) {
case 'size':
value = 'File size exceeds 90MB.';
case "size":
value = "File size exceeds 90MB.";
break;
case 'encoding':
case "encoding":
value = `Please use UTF-8 for character encoding: (Row #${failureLoc})`;
break;
case 'insufficient_columns':
value = 'Need at least five columns of data.';
case "insufficient_columns":
value = "Need at least five columns of data.";
break;
case 'empty':
value = 'Need at least one record of data.';
case "empty":
value = "Need at least one record of data.";
break;
case 'name_suffix':
case 'mime_type':
value = 'File is not in csv format.';
case "name_suffix":
case "mime_type":
value = "File is not in csv format.";
break;
default:
value = escapeHtml(failureMsg);
break;
}
await Swal.fire({
title: 'IMPORT FAILED',
title: "IMPORT FAILED",
html: value,
timer: 3000, // Auto-close after 3 seconds
showConfirmButton: false,
icon: 'error',
iconColor: '#FF3366',
customClass: customClass
})
};
icon: "error",
iconColor: "#FF3366",
customClass: customClass,
});
}
/**
* Shows an error modal for the second stage of upload validation failure,
* listing individual row-level errors.
@@ -345,73 +368,73 @@ export async function uploadFailedFirst(failureType, failureMsg, failureLoc) {
* @returns {Promise<void>}
*/
export async function uploadFailedSecond(detail) {
let srt = '';
let manySrt = '';
let srt = "";
let manySrt = "";
detail.forEach(i => {
let content = '';
let key = '';
detail.forEach((i) => {
let content = "";
let key = "";
switch (i.type) {
case 'too_many':
manySrt = 'There are more errors.';
case "too_many":
manySrt = "There are more errors.";
break;
case 'unrecognized':
case "unrecognized":
content = `<li>Data unrecognizable in Status Column: (Row #${i.loc[1]}, "${escapeHtml(i.input)}")</li>`;
break;
case 'malformed':
case "malformed":
content = `<li>Data malformed in Timestamp Column: (Row #${i.loc[1]}, "${escapeHtml(i.input)}")</li>`;
break;
case 'missing':
case "missing":
switch (i.loc[2]) {
case 'case id':
key = 'Case ID';
case "case id":
key = "Case ID";
break;
case 'timestamp':
key = 'Timestamp';
case "timestamp":
key = "Timestamp";
break;
case 'name':
key = 'Activity';
case "name":
key = "Activity";
break;
case 'instance':
key = 'Activity Instance ID';
case "instance":
key = "Activity Instance ID";
break;
case 'status':
key = 'Status';
case "status":
key = "Status";
break;
default:
key = i.loc[2];
break;
}
content = `<li>Data missing in ${key} Column: (Row #${i.loc[1]})</li>`;
break;
break;
}
srt += content;
});
await Swal.fire({
title: 'IMPORT FAILED',
title: "IMPORT FAILED",
html: `<div class="text-left mx-3 space-y-1"><p>Error(s) detected:</p><ul class="list-disc ml-6">${srt}</ul><p>${manySrt} Please check.</p></div>`,
timer: 3000, // Auto-close after 3 seconds
showConfirmButton: false,
icon: 'error',
iconColor: '#FF3366',
customClass: customClass
icon: "error",
iconColor: "#FF3366",
customClass: customClass,
});
srt = '';
};
srt = "";
}
/**
* Shows a timed success notification after a file upload completes.
* @returns {Promise<void>}
*/
export async function uploadSuccess() {
await Swal.fire({
title: 'IMPORT COMPLETED',
title: "IMPORT COMPLETED",
timer: 3000, // Auto-close after 3 seconds
showConfirmButton: false,
icon: 'success',
iconColor: '#0099FF',
customClass: customClass
})
};
icon: "success",
iconColor: "#0099FF",
customClass: customClass,
});
}
/**
* Shows a confirmation dialog before uploading a file. Proceeds with
* the upload only if the user confirms.
@@ -422,23 +445,23 @@ export async function uploadSuccess() {
export async function uploadConfirm(fetchData) {
const filesStore = useFilesStore();
const result = await Swal.fire({
title: 'ARE YOU SURE?',
html: 'After importing, you wont be able to modify labels.',
icon: 'warning',
iconColor: '#FF3366',
reverseButtons:true,
confirmButtonText: 'Yes',
confirmButtonColor: '#FF3366',
title: "ARE YOU SURE?",
html: "After importing, you wont be able to modify labels.",
icon: "warning",
iconColor: "#FF3366",
reverseButtons: true,
confirmButtonText: "Yes",
confirmButtonColor: "#FF3366",
showCancelButton: true,
cancelButtonText: 'No',
cancelButtonColor: '#94a3b8',
cancelButtonText: "No",
cancelButtonColor: "#94a3b8",
customClass: customClass,
});
if(result.isConfirmed) {
if (result.isConfirmed) {
filesStore.uploadLog(fetchData);
}
};
}
/**
* Shows a non-dismissable loading spinner during file upload.
* @returns {Promise<void>}
@@ -449,8 +472,8 @@ export async function uploadloader() {
showConfirmButton: false,
allowOutsideClick: false,
customClass: customClass,
})
};
});
}
/**
* Shows a modal dialog for renaming a file.
*
@@ -463,39 +486,40 @@ export async function uploadloader() {
export async function renameModal(rename, type, id, baseName) {
const fileName = baseName;
const { value, isConfirmed } = await Swal.fire({
title: 'RENAME',
input: 'text',
inputPlaceholder: 'Enter File Name.',
title: "RENAME",
input: "text",
inputPlaceholder: "Enter File Name.",
inputValue: fileName,
inputAttributes: {
'maxlength': 200,
maxlength: 200,
},
icon: 'info',
iconHtml: '<span class="material-symbols-outlined !text-[58px]">edit_square</span>',
iconColor: '#0099FF',
icon: "info",
iconHtml:
'<span class="material-symbols-outlined !text-[58px]">edit_square</span>',
iconColor: "#0099FF",
reverseButtons: true,
confirmButtonColor: '#0099FF',
confirmButtonColor: "#0099FF",
showCancelButton: true,
cancelButtonColor: '#94a3b8',
cancelButtonColor: "#94a3b8",
customClass: customClass,
didOpen: () => {
const confirmButton = Swal.getConfirmButton();
const inputField = Swal.getInput();
inputField.addEventListener('input', function() {
inputField.addEventListener("input", function () {
if (!inputField.value.trim()) {
confirmButton.classList.add('disable-hover');
confirmButton.classList.add("disable-hover");
confirmButton.disabled = true;
} else {
confirmButton.classList.remove('disable-hover');
confirmButton.classList.remove("disable-hover");
confirmButton.disabled = false;
}
});
}
},
});
// Rename succeeded
if(isConfirmed) await rename(type, id, value);
if (isConfirmed) await rename(type, id, value);
// Clear the field: fileName = ''
}
/**
@@ -511,46 +535,50 @@ export async function deleteFileModal(files, type, id, name) {
const filesStore = useFilesStore();
const safeName = escapeHtml(name);
const htmlText = files.length === 0 ? `Do you really want to delete <span class="text-primary">${safeName}</span>?` : `<div class="text-left mx-4 space-y-1"><p class="mb-2">Do you really want to delete <span class="text-primary">${safeName}</span>?</p><p>The following dependent file(s) will also be deleted:</p><ul class="list-disc ml-6">${files}</ul></div>`;
const htmlText =
files.length === 0
? `Do you really want to delete <span class="text-primary">${safeName}</span>?`
: `<div class="text-left mx-4 space-y-1"><p class="mb-2">Do you really want to delete <span class="text-primary">${safeName}</span>?</p><p>The following dependent file(s) will also be deleted:</p><ul class="list-disc ml-6">${files}</ul></div>`;
const deleteCustomClass = { ...customClass };
deleteCustomClass.confirmButton = '!inline-block !rounded-full !text-sm !font-medium !text-center !align-middle !transition-colors !duration-300 !px-5 !py-2 !w-[100px] !h-[40px] !text-danger !bg-neutral-10 !border !border-danger';
deleteCustomClass.confirmButton =
"!inline-block !rounded-full !text-sm !font-medium !text-center !align-middle !transition-colors !duration-300 !px-5 !py-2 !w-[100px] !h-[40px] !text-danger !bg-neutral-10 !border !border-danger";
const result = await Swal.fire({
title: 'CONFIRM DELETION',
title: "CONFIRM DELETION",
html: htmlText,
icon: 'warning',
iconColor: '#FF3366',
reverseButtons:true,
confirmButtonColor: '#ffffff',
icon: "warning",
iconColor: "#FF3366",
reverseButtons: true,
confirmButtonColor: "#ffffff",
showCancelButton: true,
cancelButtonColor: '#FF3366',
cancelButtonColor: "#FF3366",
customClass: deleteCustomClass,
didOpen: () => {
const confirmButton = Swal.getConfirmButton();
confirmButton.style.border = '1px solid #FF3366';
}
confirmButton.style.border = "1px solid #FF3366";
},
});
if(result.isConfirmed) {
if (result.isConfirmed) {
filesStore.deleteFile(type, id);
}
};
}
/**
* Shows a timed success notification after file deletion.
* @returns {Promise<void>}
*/
export async function deleteSuccess() {
await Swal.fire({
title: 'FILE(S) DELETED',
title: "FILE(S) DELETED",
timer: 3000, // Auto-close after 3 seconds
showConfirmButton: false,
icon: 'success',
iconColor: '#0099FF',
customClass: customClass
})
};
icon: "success",
iconColor: "#0099FF",
customClass: customClass,
});
}
/**
* Shows an informational modal about files deleted by other users,
* then records the deletions and refreshes the file list.
@@ -565,22 +593,25 @@ export async function reallyDeleteInformation(files, reallyDeleteData) {
const deleteCustomClass = { ...customClass };
const htmlText = `<div class="text-left mx-4 space-y-1"><p>The following file(s) have been deleted by other user(s):</p><ul class="list-disc ml-6">${files}</ul></div>`;
deleteCustomClass.confirmButton = '!inline-block !rounded-full !text-sm !font-medium !text-center !align-middle !transition-colors !duration-300 !px-5 !py-2 !w-[100px] !h-[40px] !text-primary !bg-neutral-10 !border !border-primary';
deleteCustomClass.confirmButton =
"!inline-block !rounded-full !text-sm !font-medium !text-center !align-middle !transition-colors !duration-300 !px-5 !py-2 !w-[100px] !h-[40px] !text-primary !bg-neutral-10 !border !border-primary";
deleteCustomClass.cancelButton = null;
await Swal.fire({
title: 'FILE(S) DELETED BY OTHER USER(S)',
title: "FILE(S) DELETED BY OTHER USER(S)",
html: htmlText,
icon: 'info',
iconColor: '#0099FF',
icon: "info",
iconColor: "#0099FF",
customClass: deleteCustomClass,
confirmButtonColor: '#ffffff',
confirmButtonColor: "#ffffff",
didOpen: () => {
const confirmButton = Swal.getConfirmButton();
confirmButton.style.border = '1px solid #0099FF';
}
confirmButton.style.border = "1px solid #0099FF";
},
});
await Promise.all(reallyDeleteData.map(file => filesStore.deletionRecord(file.id)));
await Promise.all(
reallyDeleteData.map((file) => filesStore.deletionRecord(file.id)),
);
await filesStore.fetchAllFiles();
}
@@ -589,21 +620,21 @@ export async function reallyDeleteInformation(files, reallyDeleteData) {
* page with unsaved edits.
* @returns {Promise<void>}
*/
export async function leaveAccountManagementToRemind(){
export async function leaveAccountManagementToRemind() {
const modalStore = useModalStore();
const result = await Swal.fire({
title: 'SAVE YOUR EDIT?',
icon: 'info',
title: "SAVE YOUR EDIT?",
icon: "info",
showCancelButton: true,
didOpen: () => {
const confirmButton = Swal.getConfirmButton();
confirmButton.style.border = '1px solid #0099FF';
}
confirmButton.style.border = "1px solid #0099FF";
},
});
if(result.isConfirmed) {
if (result.isConfirmed) {
return;
} else {
modalStore.openModal();
}
};
}

View File

@@ -6,8 +6,8 @@
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30
/** @module apiError Centralized API error handler with toast notifications. */
import {useToast} from 'vue-toast-notification';
import 'vue-toast-notification/dist/theme-sugar.css';
import { useToast } from "vue-toast-notification";
import "vue-toast-notification/dist/theme-sugar.css";
/**
* Handles API errors by showing a toast notification.
@@ -19,5 +19,5 @@ import 'vue-toast-notification/dist/theme-sugar.css';
*/
export default function apiError(error, toastMessage) {
const $toast = useToast();
$toast.default(toastMessage, {position: 'bottom'});
$toast.default(toastMessage, { position: "bottom" });
}

View File

@@ -9,18 +9,18 @@
* interactive node/edge highlighting, tooltips, and position persistence.
*/
import cytoscape from 'cytoscape';
import spread from 'cytoscape-spread';
import dagre from 'cytoscape-dagre';
import fcose from 'cytoscape-fcose';
import cola from 'cytoscape-cola';
import tippy from 'tippy.js';
import 'tippy.js/dist/tippy.css';
import { useMapPathStore } from '@/stores/mapPathStore';
import { getTimeLabel } from '@/module/timeLabel.js';
import { createTooltipContent } from '@/module/tooltipContent.js';
import { useCytoscapeStore } from '@/stores/cytoscapeStore';
import { SAVE_KEY_NAME } from '@/constants/constants.js';
import cytoscape from "cytoscape";
import spread from "cytoscape-spread";
import dagre from "cytoscape-dagre";
import fcose from "cytoscape-fcose";
import cola from "cytoscape-cola";
import tippy from "tippy.js";
import "tippy.js/dist/tippy.css";
import { useMapPathStore } from "@/stores/mapPathStore";
import { getTimeLabel } from "@/module/timeLabel.js";
import { createTooltipContent } from "@/module/tooltipContent.js";
import { useCytoscapeStore } from "@/stores/cytoscapeStore";
import { SAVE_KEY_NAME } from "@/constants/constants.js";
/**
* Composes display text for frequency-type data layer values.
@@ -33,8 +33,14 @@ import { SAVE_KEY_NAME } from '@/constants/constants.js';
*/
const composeFreqTypeText = (baseText, dataLayerOption, optionValue) => {
let text = baseText;
const textInt = dataLayerOption === 'rel_freq' ? baseText + optionValue * 100 + "%" : baseText + optionValue;
const textFloat = dataLayerOption === 'rel_freq' ? baseText + (optionValue * 100).toFixed(2) + "%" : baseText + optionValue.toFixed(2);
const textInt =
dataLayerOption === "rel_freq"
? baseText + optionValue * 100 + "%"
: baseText + optionValue;
const textFloat =
dataLayerOption === "rel_freq"
? baseText + (optionValue * 100).toFixed(2) + "%"
: baseText + optionValue.toFixed(2);
// Check if the value is an integer; if not, round to 2 decimal places.
text = Math.trunc(optionValue) === optionValue ? textInt : textFloat;
return text;
@@ -65,7 +71,14 @@ cytoscape.use(cola);
* @param {HTMLElement} graphId - The DOM container element for Cytoscape.
* @returns {cytoscape.Core} The configured Cytoscape instance.
*/
export default function cytoscapeMap(mapData, dataLayerType, dataLayerOption, curveStyle, rank, graphId) {
export default function cytoscapeMap(
mapData,
dataLayerType,
dataLayerOption,
curveStyle,
rank,
graphId,
) {
// Set the color and style for each node and edge
let nodes = mapData.nodes;
let edges = mapData.edges;
@@ -78,209 +91,234 @@ export default function cytoscapeMap(mapData, dataLayerType, dataLayerOption, cu
edges: edges, // Edge data
},
layout: {
name: 'dagre',
name: "dagre",
rankDir: rank, // Vertical TB | Horizontal LR, variable from 'cytoscape-dagre' plugin
},
style: [
// Style changes when a node is selected
{
selector: 'node:selected',
selector: "node:selected",
style: {
'border-color': 'red',
'border-width': '3',
"border-color": "red",
"border-width": "3",
},
},
// Node styling
{
selector: 'node',
selector: "node",
style: {
'label':
function (node) { // Text to display on the node
// node.data(this.dataLayerType+"."+this.dataLayerOption) accesses the original array value at node.data.key.value
let optionValue = node.data(`${dataLayerType}.${dataLayerOption}`);
let text = '';
const STRING_LIMIT = 8;
if (node.data('label').length > STRING_LIMIT) {
// If text exceeds STRING_LIMIT, append "..." and add line breaks (\n)
// Using data() because Cytoscape converts array data to function calls
text = `${node.data('label').substr(0, STRING_LIMIT)}...\n\n`;
} else { // Pad with spaces to match the label width for consistent sizing
text = `${node.data('label').padEnd(STRING_LIMIT, ' ')}\n\n`
label: function (node) {
// Text to display on the node
// node.data(this.dataLayerType+"."+this.dataLayerOption) accesses the original array value at node.data.key.value
let optionValue = node.data(`${dataLayerType}.${dataLayerOption}`);
let text = "";
const STRING_LIMIT = 8;
if (node.data("label").length > STRING_LIMIT) {
// If text exceeds STRING_LIMIT, append "..." and add line breaks (\n)
// Using data() because Cytoscape converts array data to function calls
text = `${node.data("label").substr(0, STRING_LIMIT)}...\n\n`;
} else {
// Pad with spaces to match the label width for consistent sizing
text = `${node.data("label").padEnd(STRING_LIMIT, " ")}\n\n`;
}
// In elements, activity is categorized as default, so check if the node is an activity before adding text.
// Use parseInt (integer) or parseFloat (float) to convert strings to numbers.
// Relative values need to be converted to percentages (%)
if (node.data("type") === "activity") {
let textDurRel;
let timeLabelInt;
let timeLabelFloat;
let textTimeLabel;
switch (dataLayerType) {
case "freq": // Frequency
text = composeFreqTypeText(
text,
dataLayerOption,
optionValue,
);
break;
case "duration": // Duration: Relative is percentage %, others need time unit conversion.
// Relative %
textDurRel = text + (optionValue * 100).toFixed(2) + "%";
// Timelabel
timeLabelInt = text + getTimeLabel(optionValue);
timeLabelFloat = text + getTimeLabel(optionValue.toFixed(2));
// Check if the value is an integer; if not, round to 2 decimal places.
textTimeLabel =
Math.trunc(optionValue) === optionValue
? timeLabelInt
: timeLabelFloat;
text =
dataLayerOption === "rel_duration"
? textDurRel
: textTimeLabel;
break;
}
}
// In elements, activity is categorized as default, so check if the node is an activity before adding text.
// Use parseInt (integer) or parseFloat (float) to convert strings to numbers.
// Relative values need to be converted to percentages (%)
if (node.data('type') === 'activity') {
let textDurRel;
let timeLabelInt;
let timeLabelFloat;
let textTimeLabel;
switch (dataLayerType) {
case 'freq': // Frequency
text = composeFreqTypeText(text, dataLayerOption, optionValue);
break;
case 'duration': // Duration: Relative is percentage %, others need time unit conversion.
// Relative %
textDurRel = text + (optionValue * 100).toFixed(2) + "%";
// Timelabel
timeLabelInt = text + getTimeLabel(optionValue);
timeLabelFloat = text + getTimeLabel(optionValue.toFixed(2));
// Check if the value is an integer; if not, round to 2 decimal places.
textTimeLabel = Math.trunc(optionValue) === optionValue ? timeLabelInt : timeLabelFloat;
text = dataLayerOption === 'rel_duration' ? textDurRel : textTimeLabel;
break;
}
}
return text;
},
'text-opacity': 0.7,
'background-color': 'data(backgroundColor)',
'border-color': 'data(bordercolor)',
'border-width':
function (node) {
return node.data('type') === 'activity' ? '1' : '2';
},
'background-image': 'data(nodeImageUrl)',
'background-opacity': 'data(backgroundOpacity)', // Transparent background
'border-opacity': 'data(borderOpacity)', // Transparent border
'shape': 'data(shape)',
'text-wrap': 'wrap',
'text-max-width': 'data(width)', // Wrap text within the node
'text-overflow-wrap': 'anywhere', // Allow wrapping at any position
'text-margin-x': function (node) {
return node.data('type') === 'activity' ? -5 : 0;
return text;
},
'text-margin-y': function (node) {
return node.data('type') === 'activity' ? 2 : 0;
"text-opacity": 0.7,
"background-color": "data(backgroundColor)",
"border-color": "data(bordercolor)",
"border-width": function (node) {
return node.data("type") === "activity" ? "1" : "2";
},
'padding': function (node) {
return node.data('type') === 'activity' ? 0 : 0;
"background-image": "data(nodeImageUrl)",
"background-opacity": "data(backgroundOpacity)", // Transparent background
"border-opacity": "data(borderOpacity)", // Transparent border
shape: "data(shape)",
"text-wrap": "wrap",
"text-max-width": "data(width)", // Wrap text within the node
"text-overflow-wrap": "anywhere", // Allow wrapping at any position
"text-margin-x": function (node) {
return node.data("type") === "activity" ? -5 : 0;
},
"text-margin-y": function (node) {
return node.data("type") === "activity" ? 2 : 0;
},
padding: function (node) {
return node.data("type") === "activity" ? 0 : 0;
},
"text-justification": "left",
"text-halign": "center",
"text-valign": "center",
height: "data(height)",
width: "data(width)",
color: "data(textColor)",
"line-height": "0.7rem",
"font-size": function (node) {
return node.data("type") === "activity" ? 14 : 14;
},
'text-justification': 'left',
'text-halign': 'center',
'text-valign': 'center',
'height': 'data(height)',
'width': 'data(width)',
'color': 'data(textColor)',
'line-height': '0.7rem',
'font-size':
function (node) {
return node.data('type') === 'activity' ? 14 : 14;
},
},
},
// Edge styling
{
selector: 'edge',
selector: "edge",
style: {
'content': function (edge) { // Text displayed on the edge
content: function (edge) {
// Text displayed on the edge
let optionValue = edge.data(`${dataLayerType}.${dataLayerOption}`);
let result = '';
let result = "";
let edgeInt;
let edgeFloat;
let edgeDurRel;
let timeLabelInt;
let timeLabelFloat;
let edgeTimeLabel;
if (optionValue === '') return optionValue;
if (optionValue === "") return optionValue;
switch (dataLayerType) {
case 'freq':
edgeInt = dataLayerOption === 'rel_freq' ? optionValue * 100 + "%" : optionValue;
edgeFloat = dataLayerOption === 'rel_freq' ? (optionValue * 100).toFixed(2) + "%" : optionValue.toFixed(2);
case "freq":
edgeInt =
dataLayerOption === "rel_freq"
? optionValue * 100 + "%"
: optionValue;
edgeFloat =
dataLayerOption === "rel_freq"
? (optionValue * 100).toFixed(2) + "%"
: optionValue.toFixed(2);
// Check if the value is an integer; if not, round to 2 decimal places.
result = Math.trunc(optionValue) === optionValue ? edgeInt : edgeFloat;
result =
Math.trunc(optionValue) === optionValue ? edgeInt : edgeFloat;
break;
case 'duration': // Duration: Relative is percentage %, others need time unit conversion.
case "duration": // Duration: Relative is percentage %, others need time unit conversion.
// Relative %
edgeDurRel = (optionValue * 100).toFixed(2) + "%";
// Timelabel
timeLabelInt = getTimeLabel(optionValue);
timeLabelFloat = getTimeLabel(optionValue.toFixed(2));
edgeTimeLabel = Math.trunc(optionValue) === optionValue ? timeLabelInt : timeLabelFloat;
edgeTimeLabel =
Math.trunc(optionValue) === optionValue
? timeLabelInt
: timeLabelFloat;
result = dataLayerOption === 'rel_duration' ? edgeDurRel : edgeTimeLabel;
result =
dataLayerOption === "rel_duration"
? edgeDurRel
: edgeTimeLabel;
break;
};
}
return result;
},
'curve-style': curveStyle, // unbundled-bezier | taxi
'overlay-opacity': 0, // Set overlay-opacity to 0 to remove the gray shadow
'target-arrow-shape': 'triangle', // Arrow shape pointing to target: triangle
'color': 'gray', //#0066cc
"curve-style": curveStyle, // unbundled-bezier | taxi
"overlay-opacity": 0, // Set overlay-opacity to 0 to remove the gray shadow
"target-arrow-shape": "triangle", // Arrow shape pointing to target: triangle
color: "gray", //#0066cc
//'control-point-step-size':100, // Distance between Bezier curve control points
'width': 'data(lineWidth)',
'line-style': 'data(edgeStyle)',
width: "data(lineWidth)",
"line-style": "data(edgeStyle)",
"text-margin-y": "0.7rem",
//"text-rotation": "autorotate",
}
}, {
selector: '.highlight-edge',
style: {
'color': '#0099FF',
'line-color': '#0099FF',
'overlay-color': '#0099FF',
'overlay-opacity': 0.2,
'overlay-padding': '5px',
},
}, {
selector: '.highlight-node',
},
{
selector: ".highlight-edge",
style: {
'overlay-color': '#0099FF',
'overlay-opacity': 0.01,
'overlay-padding': '5px',
color: "#0099FF",
"line-color": "#0099FF",
"overlay-color": "#0099FF",
"overlay-opacity": 0.2,
"overlay-padding": "5px",
},
}, {
selector: 'edge[source = target]', // Select self-loop edges
},
{
selector: ".highlight-node",
style: {
'loop-direction': '0deg', // Control the loop direction
'loop-sweep': '-60deg', // Control the loop arc; adjust to change size
'control-point-step-size': 50 // Control the loop radius; increase to enlarge the loop
}
"overlay-color": "#0099FF",
"overlay-opacity": 0.01,
"overlay-padding": "5px",
},
},
{
selector: "edge[source = target]", // Select self-loop edges
style: {
"loop-direction": "0deg", // Control the loop direction
"loop-sweep": "-60deg", // Control the loop arc; adjust to change size
"control-point-step-size": 50, // Control the loop radius; increase to enlarge the loop
},
},
],
});
// When an edge is clicked, apply glow effect to the edge and its label
cy.on('tap', 'edge', function (event) {
cy.edges().removeClass('highlight-edge');
event.target.addClass('highlight-edge');
cy.on("tap", "edge", function (event) {
cy.edges().removeClass("highlight-edge");
event.target.addClass("highlight-edge");
});
// When a node is clicked, apply glow effect to the node and adjacent edges
cy.on('tap, mousedown', 'node', function (event) {
cy.on("tap, mousedown", "node", function (event) {
useMapPathStore().onNodeClickHighlightEdges(event.target);
});
// When an edge is clicked, apply glow effect to the edge and both endpoint nodes
cy.on('tap, mousedown', 'edge', function (event) {
cy.on("tap, mousedown", "edge", function (event) {
useMapPathStore().onEdgeClickHighlightNodes(event.target);
});
// creat tippy.js
let tip;
cy.on('mouseover', 'node', function (event) {
cy.on("mouseover", "node", function (event) {
const node = event.target;
let ref = node.popperRef()
let dummyDomEle = document.createElement('div');
let content = createTooltipContent(node.data('label'));
tip = new tippy(dummyDomEle, { // tippy props:
let ref = node.popperRef();
let dummyDomEle = document.createElement("div");
let content = createTooltipContent(node.data("label"));
tip = new tippy(dummyDomEle, {
// tippy props:
getReferenceClientRect: ref.getBoundingClientRect,
trigger: 'manual',
content: content
trigger: "manual",
content: content,
});
if (node.data("label").length > 10) tip.show();
});
cy.on('mouseout', 'node', function (event) {
cy.on("mouseout", "node", function (event) {
tip?.hide();
});
@@ -290,12 +328,20 @@ export default function cytoscapeMap(mapData, dataLayerType, dataLayerOption, cu
cytoscapeStore.loadPositionsFromStorage(rank);
// Check if localStorage has previously saved visit data.
// If saved node positions exist, restore them for rendering.
if (localStorage.getItem(SAVE_KEY_NAME) && JSON.parse(localStorage.getItem(SAVE_KEY_NAME))) {
const allGraphsRemembered = JSON.parse(localStorage.getItem(SAVE_KEY_NAME));
const currentGraphNodesRemembered =
allGraphsRemembered[cytoscapeStore.currentGraphId] ? allGraphsRemembered[cytoscapeStore.currentGraphId][rank] : null; // May be undefined
if (
localStorage.getItem(SAVE_KEY_NAME) &&
JSON.parse(localStorage.getItem(SAVE_KEY_NAME))
) {
const allGraphsRemembered = JSON.parse(
localStorage.getItem(SAVE_KEY_NAME),
);
const currentGraphNodesRemembered = allGraphsRemembered[
cytoscapeStore.currentGraphId
]
? allGraphsRemembered[cytoscapeStore.currentGraphId][rank]
: null; // May be undefined
if (currentGraphNodesRemembered) {
currentGraphNodesRemembered.forEach(nodeRemembered => {
currentGraphNodesRemembered.forEach((nodeRemembered) => {
const nodeToDecide = cy.getElementById(nodeRemembered.id);
if (nodeToDecide) {
nodeToDecide.position(nodeRemembered.position);
@@ -305,15 +351,23 @@ export default function cytoscapeMap(mapData, dataLayerType, dataLayerOption, cu
}
// Save the current positions of all nodes when the view is first entered
const allNodes = cy.nodes();
allNodes.forEach(nodeFirstlySave => {
cytoscapeStore.saveNodePosition(nodeFirstlySave.id(), nodeFirstlySave.position(), rank);
allNodes.forEach((nodeFirstlySave) => {
cytoscapeStore.saveNodePosition(
nodeFirstlySave.id(),
nodeFirstlySave.position(),
rank,
);
});
// After node positions change, save the updated positions.
// rank represents whether the user is in horizontal or vertical layout mode.
cy.on('dragfree', 'node', (event) => {
cy.on("dragfree", "node", (event) => {
const nodeToSave = event.target;
cytoscapeStore.saveNodePosition(nodeToSave.id(), nodeToSave.position(), rank);
cytoscapeStore.saveNodePosition(
nodeToSave.id(),
nodeToSave.position(),
rank,
);
});
});

View File

@@ -9,13 +9,13 @@
* individual trace visualization.
*/
import cytoscape from 'cytoscape';
import dagre from 'cytoscape-dagre';
import tippy from 'tippy.js';
import 'tippy.js/dist/tippy.css';
import { createTooltipContent } from '@/module/tooltipContent.js';
import cytoscape from "cytoscape";
import dagre from "cytoscape-dagre";
import tippy from "tippy.js";
import "tippy.js/dist/tippy.css";
import { createTooltipContent } from "@/module/tooltipContent.js";
cytoscape.use( dagre );
cytoscape.use(dagre);
/**
* Creates a Cytoscape.js instance for rendering a single trace's
@@ -36,75 +36,79 @@ export default function cytoscapeMapTrace(nodes, edges, graphId) {
edges: edges, // Edge data
},
layout: {
name: 'dagre',
rankDir: 'LR' // Vertical TB | Horizontal LR, variable from 'cytoscape-dagre' plugin
name: "dagre",
rankDir: "LR", // Vertical TB | Horizontal LR, variable from 'cytoscape-dagre' plugin
},
style: [
// Node styling
{
selector: 'node',
selector: "node",
style: {
'label':
function(node) { // Text to display on the node
let text = '';
label: function (node) {
// Text to display on the node
let text = "";
// node.data('label') accesses the original array value at node.data.label
text = node.data('label').length > 18 ? `${node.data('label').substr(0,15)}...` : `${node.data('label')}`;
text =
node.data("label").length > 18
? `${node.data("label").substr(0, 15)}...`
: `${node.data("label")}`;
return text
return text;
},
'text-opacity': 0.7,
'background-color': 'data(backgroundColor)',
'border-color': 'data(bordercolor)',
'border-width': '1',
'shape': 'data(shape)',
'text-wrap': 'wrap',
'text-max-width': 75,
'text-halign': 'center',
'text-valign': 'center',
'height': 'data(height)',
'width': 'data(width)',
'color': '#001933',
'font-size': 14,
}
"text-opacity": 0.7,
"background-color": "data(backgroundColor)",
"border-color": "data(bordercolor)",
"border-width": "1",
shape: "data(shape)",
"text-wrap": "wrap",
"text-max-width": 75,
"text-halign": "center",
"text-valign": "center",
height: "data(height)",
width: "data(width)",
color: "#001933",
"font-size": 14,
},
},
// Edge styling
{
selector: 'edge',
selector: "edge",
style: {
'curve-style': 'taxi', // unbundled-bezier | taxi
'target-arrow-shape': 'triangle', // Arrow shape pointing to target: triangle
'color': 'gray', //#0066cc
'width': 'data(lineWidth)',
'line-style': 'data(style)',
}
"curve-style": "taxi", // unbundled-bezier | taxi
"target-arrow-shape": "triangle", // Arrow shape pointing to target: triangle
color: "gray", //#0066cc
width: "data(lineWidth)",
"line-style": "data(style)",
},
},
// Style changes when a node is selected
{
selector: 'node:selected',
style:{
'border-color': 'red',
'border-width': '3',
}
selector: "node:selected",
style: {
"border-color": "red",
"border-width": "3",
},
},
],
});
// creat tippy.js
let tip;
cy.on('mouseover', 'node', function(event) {
const node = event.target
let ref = node.popperRef()
let dummyDomEle = document.createElement('div');
let content = createTooltipContent(node.data('label'));
tip = new tippy(dummyDomEle, { // tippy props:
getReferenceClientRect: ref.getBoundingClientRect,
trigger: 'manual',
content:content
});
cy.on("mouseover", "node", function (event) {
const node = event.target;
let ref = node.popperRef();
let dummyDomEle = document.createElement("div");
let content = createTooltipContent(node.data("label"));
tip = new tippy(dummyDomEle, {
// tippy props:
getReferenceClientRect: ref.getBoundingClientRect,
trigger: "manual",
content: content,
});
tip.show();
})
cy.on('mouseout', 'node', function(event) {
});
cy.on("mouseout", "node", function (event) {
tip.hide();
});
}

View File

@@ -12,13 +12,13 @@
* @returns {string} The formatted string with commas (e.g. "1,000,000").
*/
const formatNumberWithCommas = (numberStr) => {
let reversedStr = numberStr.split('').reverse().join('');
let reversedStr = numberStr.split("").reverse().join("");
let groupedStr = reversedStr.match(/.{1,3}/g);
let joinedStr = groupedStr.join(',');
let finalStr = joinedStr.split('').reverse().join('');
let joinedStr = groupedStr.join(",");
let finalStr = joinedStr.split("").reverse().join("");
return finalStr;
}
};
/**
* Converts a number to a string with comma-separated thousands.
@@ -29,7 +29,7 @@ const formatNumberWithCommas = (numberStr) => {
* @returns {string} The formatted number string (e.g. "1,234.56").
*/
export default function numberLabel(num) {
let parts = num.toString().split('.');
let parts = num.toString().split(".");
parts[0] = formatNumberWithCommas(parts[0]);
return parts.join('.');
return parts.join(".");
}

View File

@@ -6,7 +6,7 @@
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30
/** @module setChartData Chart.js data transformation utilities. */
import getMoment from 'moment';
import getMoment from "moment";
/**
* Extends backend chart data with extrapolated boundary points for
@@ -24,30 +24,30 @@ import getMoment from 'moment';
* with boundary points.
*/
export function setLineChartData(baseData, xMax, xMin, isPercent, yMax, yMin) {
// Convert baseData to an array of objects with x and y properties
let data = baseData.map(i => ({ x: i.x, y: i.y }));
// Convert baseData to an array of objects with x and y properties
let data = baseData.map((i) => ({ x: i.x, y: i.y }));
// Calculate the Y-axis minimum value
let b = calculateYMin(baseData, isPercent, yMin, yMax);
// Calculate the Y-axis maximum value
let mf = calculateYMax(baseData, isPercent, yMin, yMax);
// Prepend the minimum value
data.unshift({
x: xMin,
y: b,
});
// Append the maximum value
data.push({
x: xMax,
y: mf,
});
return data;
};
/**
// Calculate the Y-axis minimum value
let b = calculateYMin(baseData, isPercent, yMin, yMax);
// Calculate the Y-axis maximum value
let mf = calculateYMax(baseData, isPercent, yMin, yMax);
// Prepend the minimum value
data.unshift({
x: xMin,
y: b,
});
// Append the maximum value
data.push({
x: xMax,
y: mf,
});
return data;
}
/**
* Extrapolates the Y-axis minimum boundary value using linear
* interpolation from the first two data points.
*
@@ -65,7 +65,7 @@ function calculateYMin(baseData, isPercent, yMin, yMax) {
let f = baseData[1].y;
let b = (e * d - a * d - f * a - f * c) / (e - c - a);
return clampValue(b, isPercent, yMin, yMax);
};
}
/**
* Extrapolates the Y-axis maximum boundary value using linear
* interpolation from the last two data points.
@@ -84,7 +84,7 @@ function calculateYMax(baseData, isPercent, yMin, yMax) {
let me = 11;
let mf = (mb * me - mb * mc - md * me + md * ma) / (ma - mc);
return clampValue(mf, isPercent, yMin, yMax);
};
}
/**
* Clamps a value within a specified range. If isPercent is true, the
* value is clamped to [0, 1]; otherwise to [min, max].
@@ -107,11 +107,11 @@ function clampValue(value, isPercent, min, max) {
return max;
}
if (value <= min) {
return min;
return min;
}
}
return value;
};
}
/**
* Converts backend chart data timestamps to formatted date strings
* for bar charts.
@@ -122,14 +122,14 @@ function clampValue(value, isPercent, min, max) {
* as "YYYY/M/D hh:mm:ss".
*/
export function setBarChartData(baseData) {
let data = baseData.map(i =>{
let data = baseData.map((i) => {
return {
x: getMoment(i.x).format('YYYY/M/D hh:mm:ss'),
y: i.y
}
})
return data
};
x: getMoment(i.x).format("YYYY/M/D hh:mm:ss"),
y: i.y,
};
});
return data;
}
/**
* Divides a time range into evenly spaced time points.
*
@@ -149,9 +149,9 @@ export function timeRange(minTime, maxTime, amount) {
for (let i = 0; i < amount; i++) {
timeRange.push(startTime + timeGap * i);
}
timeRange = timeRange.map(value => Math.round(value));
timeRange = timeRange.map((value) => Math.round(value));
return timeRange;
};
}
/**
* Generates smooth Y-axis values using cubic Bezier interpolation
* to produce evenly spaced ticks matching the X-axis divisions.
@@ -163,39 +163,40 @@ export function timeRange(minTime, maxTime, amount) {
*/
export function yTimeRange(data, yAmount, yMax) {
const yRange = [];
const yGap = (1/ (yAmount-1));
const yGap = 1 / (yAmount - 1);
// Cubic Bezier curve formula
const threebsr = function (t, a1, a2, a3, a4) {
return (
(1 - t) * (1 - t) * (1 - t) * a1 +
3 * t * (1 - t)* (1 - t) * a2 +
3 * t * t * (1 - t) * a3 +
t * t * t * a4
)
3 * t * (1 - t) * (1 - t) * a2 +
3 * t * t * (1 - t) * a3 +
t * t * t * a4
);
};
for (let j = 0; j < data.length - 1; j++) {
for (let i = 0; i <= 1; i += yGap*11) {
yRange.push(threebsr(i, data[j].y, data[j].y, data[j + 1].y, data[j + 1].y));
for (let j = 0; j < data.length - 1; j++) {
for (let i = 0; i <= 1; i += yGap * 11) {
yRange.push(
threebsr(i, data[j].y, data[j].y, data[j + 1].y, data[j + 1].y),
);
}
}
if(yRange.length < yAmount) {
if (yRange.length < yAmount) {
let len = yAmount - yRange.length;
for (let i = 0; i < len; i++) {
yRange.push(yRange[yRange.length - 1]);
}
}
else if(yRange.length > yAmount) {
} else if (yRange.length > yAmount) {
let len = yRange.length - yAmount;
for(let i = 0; i < len; i++) {
for (let i = 0; i < len; i++) {
yRange.splice(1, 1);
}
}
return yRange;
};
}
/**
* Finds the index of the closest value in an array to the given target.
*
@@ -217,7 +218,7 @@ export function getXIndex(data, xValue) {
}
return closestIndex;
};
}
/**
* Formats a duration in seconds to a compact string with d/h/m/s units.
*
@@ -226,13 +227,13 @@ export function getXIndex(data, xValue) {
* or null if the input is NaN.
*/
export function formatTime(seconds) {
if(!isNaN(seconds)) {
if (!isNaN(seconds)) {
const remainingSeconds = seconds % 60;
const minutes = (Math.floor(seconds - remainingSeconds) / 60) % 60;
const hours = (Math.floor(seconds / 3600)) % 24;
const hours = Math.floor(seconds / 3600) % 24;
const days = Math.floor(seconds / (3600 * 24));
let result = '';
let result = "";
if (days > 0) {
result += `${days}d`;
}
@@ -258,20 +259,20 @@ export function formatTime(seconds) {
export function formatMaxTwo(times) {
const formattedTimes = [];
for (let time of times) {
// Match numbers and units (days, hours, minutes, seconds); assume numbers have at most 10 digits
let units = time.match(/\d{1,10}[dhms]/g);
let formattedTime = '';
let count = 0;
// Match numbers and units (days, hours, minutes, seconds); assume numbers have at most 10 digits
let units = time.match(/\d{1,10}[dhms]/g);
let formattedTime = "";
let count = 0;
// Keep only the two largest units
for (let unit of units) {
if (count >= 2) {
break;
}
formattedTime += unit + ' ';
count++;
// Keep only the two largest units
for (let unit of units) {
if (count >= 2) {
break;
}
formattedTimes.push(formattedTime.trim()); // Remove trailing whitespace
formattedTime += unit + " ";
count++;
}
formattedTimes.push(formattedTime.trim()); // Remove trailing whitespace
}
return formattedTimes;
}

View File

@@ -25,5 +25,5 @@ export default function shortScaleNumber(number) {
index++;
}
num = Math.ceil(num * 10) / 10;
return num + abbreviations[index] + " " ;
return num + abbreviations[index] + " ";
}

View File

@@ -25,7 +25,7 @@ export function sortNumEngZhtw(data) {
if (isANumber) return -1;
if (isBNumber) return 1;
return a.localeCompare(b, 'zh-Hant-TW', { sensitivity: 'accent' });
return a.localeCompare(b, "zh-Hant-TW", { sensitivity: "accent" });
});
}
@@ -48,5 +48,5 @@ export function sortNumEngZhtwForFilter(a, b) {
if (isANumber) return -1;
if (isBNumber) return 1;
return a.localeCompare(b, 'zh-Hant-TW', { sensitivity: 'accent' });
return a.localeCompare(b, "zh-Hant-TW", { sensitivity: "accent" });
}

View File

@@ -6,7 +6,7 @@
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30
/** @module timeLabel Time formatting and chart axis tick utilities. */
import moment from 'moment';
import moment from "moment";
/** @constant {number} Number of decimal places for formatted time values. */
const TOFIXED_DECIMAL = 1;
@@ -20,12 +20,12 @@ const TOFIXED_DECIMAL = 1;
* and the time unit character ("d", "h", "m", or "s").
*/
export const getStepSizeOfYTicks = (maxTimeInSecond, numOfParts) => {
const {unitToUse, timeValue} = getTimeUnitAndValueToUse(maxTimeInSecond);
const getLarger = 1 + (1 / (numOfParts - 1));
const resultStepSize = (timeValue * getLarger / numOfParts);
const { unitToUse, timeValue } = getTimeUnitAndValueToUse(maxTimeInSecond);
const getLarger = 1 + 1 / (numOfParts - 1);
const resultStepSize = (timeValue * getLarger) / numOfParts;
return {resultStepSize, unitToUse};
}
return { resultStepSize, unitToUse };
};
/**
* Determines the most appropriate time unit for a given number of seconds
@@ -62,7 +62,7 @@ const getTimeUnitAndValueToUse = (secondToDecide) => {
return {
unitToUse: "s",
timeValue: secondToDecide,
}
};
}
};
@@ -75,12 +75,14 @@ const getTimeUnitAndValueToUse = (secondToDecide) => {
* @param {string} unitToUse - The time unit suffix ("d", "h", "m", or "s").
* @returns {string} The formatted tick label (e.g. "2.5h").
*/
export function getYTicksByIndex(stepSize, index, unitToUse){
export function getYTicksByIndex(stepSize, index, unitToUse) {
const rawStepsizeMultIndex = (stepSize * index).toString();
const shortenStepsizeMultIndex = rawStepsizeMultIndex.substring(
0, rawStepsizeMultIndex.indexOf('.') + 1 + TOFIXED_DECIMAL);
0,
rawStepsizeMultIndex.indexOf(".") + 1 + TOFIXED_DECIMAL,
);
return `${shortenStepsizeMultIndex}${unitToUse}`;
};
}
/**
* Converts seconds to a human-readable time string with full unit names.
@@ -101,17 +103,15 @@ export function getTimeLabel(second, fixedNumber = 0) {
const hh = Math.floor((second % day) / hour);
const mm = Math.floor((second % hour) / minutes);
if(dd > 0){
return (second / day).toFixed(fixedNumber) + " days";
if (dd > 0) {
return (second / day).toFixed(fixedNumber) + " days";
} else if (hh > 0) {
return ((second % day) / hour).toFixed(fixedNumber) + " hrs";
} else if (mm > 0) {
return ((second % hour) / minutes).toFixed(fixedNumber) + " mins";
}
else if(hh > 0){
return ((second % day) / hour).toFixed(fixedNumber) + " hrs";
}
else if(mm > 0){
return ((second % hour) / minutes).toFixed(fixedNumber) + " mins";
}
if(second == 0){
return second + " sec";
if (second == 0) {
return second + " sec";
}
return second + " sec";
}
@@ -134,17 +134,15 @@ export function simpleTimeLabel(second, fixedNumber = 0) {
const hh = Math.floor((second % day) / hour);
const mm = Math.floor((second % hour) / minutes);
if(dd > 0){
return (second / day).toFixed(fixedNumber) + "d";
if (dd > 0) {
return (second / day).toFixed(fixedNumber) + "d";
} else if (hh > 0) {
return ((second % day) / hour).toFixed(fixedNumber) + "h";
} else if (mm > 0) {
return ((second % hour) / minutes).toFixed(fixedNumber) + "m";
}
else if(hh > 0){
return ((second % day) / hour).toFixed(fixedNumber) + "h";
}
else if(mm > 0){
return ((second % hour) / minutes).toFixed(fixedNumber) + "m";
}
if(second == 0){
return second + "s";
if (second == 0) {
return second + "s";
}
return second + "s";
}
@@ -167,49 +165,48 @@ export function followTimeLabel(second, max, fixedNumber = 0) {
const dd = max / day;
const hh = max / hour;
const mm = max/ minutes;
let maxUnit = '';
const mm = max / minutes;
let maxUnit = "";
let result = "";
if (dd > 1) {
maxUnit = 'd';
maxUnit = "d";
} else if (hh > 1) {
maxUnit = 'h';
maxUnit = "h";
} else if (mm > 1) {
maxUnit = 'm';
maxUnit = "m";
} else {
maxUnit = 's';
maxUnit = "s";
}
switch (maxUnit) {
case 'd':
if((second / day) === 0) {
case "d":
if (second / day === 0) {
fixedNumber = 0;
}
result = (second / day).toFixed(fixedNumber) + 'd';
result = (second / day).toFixed(fixedNumber) + "d";
break;
case 'h':
if((second / hour) === 0) {
case "h":
if (second / hour === 0) {
fixedNumber = 0;
}
result = (second / hour).toFixed(fixedNumber) + 'h';
result = (second / hour).toFixed(fixedNumber) + "h";
break;
case 'm':
if((second / minutes) === 0) {
case "m":
if (second / minutes === 0) {
fixedNumber = 0;
}
result = (second / minutes).toFixed(fixedNumber) + 'm';
result = (second / minutes).toFixed(fixedNumber) + "m";
break;
case 's':
if(second === 0) {
case "s":
if (second === 0) {
fixedNumber = 0;
}
result = second.toFixed(fixedNumber) + 's';
result = second.toFixed(fixedNumber) + "s";
break;
}
return result;
}
/**
* Selects an appropriate moment.js date format string based on the
* difference between the minimum and maximum timestamps.
@@ -221,20 +218,25 @@ export function followTimeLabel(second, max, fixedNumber = 0) {
* @param {number} maxTimeStamp - The maximum timestamp in seconds.
* @returns {string} A moment.js format string.
*/
export const setTimeStringFormatBaseOnTimeDifference = (minTimeStamp, maxTimeStamp) => {
export const setTimeStringFormatBaseOnTimeDifference = (
minTimeStamp,
maxTimeStamp,
) => {
const timeDifferenceInSeconds = maxTimeStamp - minTimeStamp;
let dateFormat;
if (timeDifferenceInSeconds < 60) {
dateFormat = 'HH:mm:ss'; // Seconds range
dateFormat = "HH:mm:ss"; // Seconds range
} else if (timeDifferenceInSeconds < 3600) {
dateFormat = 'MM/DD HH:mm'; // Minutes range
} else if (timeDifferenceInSeconds < 86400) { // 86400 seconds = 24 hours
dateFormat = 'MM/DD HH:mm'; // Hours range
} else if (timeDifferenceInSeconds < 2592000) { // 2592000 seconds = 30 days
dateFormat = 'YYYY/MM/DD'; // Days range
dateFormat = "MM/DD HH:mm"; // Minutes range
} else if (timeDifferenceInSeconds < 86400) {
// 86400 seconds = 24 hours
dateFormat = "MM/DD HH:mm"; // Hours range
} else if (timeDifferenceInSeconds < 2592000) {
// 2592000 seconds = 30 days
dateFormat = "YYYY/MM/DD"; // Days range
} else {
dateFormat = 'YYYY/MM/DD'; // Months range
dateFormat = "YYYY/MM/DD"; // Months range
}
return dateFormat;
@@ -251,6 +253,6 @@ export const setTimeStringFormatBaseOnTimeDifference = (minTimeStamp, maxTimeSta
*/
export const mapTimestampToAxisTicksByFormat = (timeStampArr, timeFormat) => {
if (timeStampArr) {
return timeStampArr.map(ts => moment(ts).format(timeFormat));
}
};
return timeStampArr.map((ts) => moment(ts).format(timeFormat));
}
};

View File

@@ -11,7 +11,7 @@
* @returns {HTMLDivElement} A div element with text-only content.
*/
export function createTooltipContent(label) {
const content = document.createElement('div');
content.textContent = String(label ?? '');
const content = document.createElement("div");
content.textContent = String(label ?? "");
return content;
}