Apply repository-wide ESLint auto-fix formatting pass
Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 won’t be able to modify labels.',
|
||||
icon: 'warning',
|
||||
iconColor: '#FF3366',
|
||||
reverseButtons:true,
|
||||
confirmButtonText: 'Yes',
|
||||
confirmButtonColor: '#FF3366',
|
||||
title: "ARE YOU SURE?",
|
||||
html: "After importing, you won’t 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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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(".");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -25,5 +25,5 @@ export default function shortScaleNumber(number) {
|
||||
index++;
|
||||
}
|
||||
num = Math.ceil(num * 10) / 10;
|
||||
return num + abbreviations[index] + " " ;
|
||||
return num + abbreviations[index] + " ";
|
||||
}
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user