fix: dotted solid edge style bug

This commit is contained in:
Cindy Chang
2024-09-05 17:49:01 +08:00
parent 8635c8d3e2
commit 5b3c0050b9
3 changed files with 370 additions and 353 deletions

View File

@@ -192,7 +192,7 @@ export default function cytoscapeMap(mapData, dataLayerType, dataLayerOption, cu
'color': 'gray', //#0066cc 'color': 'gray', //#0066cc
//'control-point-step-size':100, // 從點到點的垂直線,指定貝茲取線邊緣間的距離 //'control-point-step-size':100, // 從點到點的垂直線,指定貝茲取線邊緣間的距離
'width': 'data(lineWidth)', 'width': 'data(lineWidth)',
'line-style': 'data(style)', 'line-style': 'data(edgeStyle)',
"text-margin-y": "0.7rem", "text-margin-y": "0.7rem",
//"text-rotation": "autorotate", //"text-rotation": "autorotate",
} }

View File

@@ -14,288 +14,287 @@ const ImgCapsulesGlow = [ImgCapsuleGlow1, ImgCapsuleGlow2, ImgCapsuleGlow3, ImgC
const ImgCapsules = [ImgCapsule1, ImgCapsule2, ImgCapsule3, ImgCapsule4]; const ImgCapsules = [ImgCapsule1, ImgCapsule2, ImgCapsule3, ImgCapsule4];
export default defineStore('useMapPathStore', { export default defineStore('useMapPathStore', {
state: () => ({ state: () => ({
clickedPath: [], clickedPath: [],
insights: {}, insights: {},
insightWithPath: {}, insightWithPath: {},
cytoscape: { cytoscape: {
process: process:
{ {
curved:{ curved: {
horizontal:null, horizontal: null,
vertical:null, vertical: null,
},
elbow:{
horizontal:null,
vertical:null,
}
},
bpmn: {
curved:{
horizontal:null,
vertical:null,
},
elbow:{
horizontal:null,
vertical:null,
}
}
}, },
processOrBPMN: 'process', elbow: {
curveType: 'curved', horizontal: null,
directionType: 'horizontal', vertical: null,
allPaths: [], }
allPathsByEdge: [],
startNode: null,
mapGraphPathToInsight: {},
activeTrace: 0,
activeListIndex: 0,
lastClickedNode: null,
isBPMNOn: false,
}),
actions: {
async setCytoscape(cytoscape, processOrBPMN = 'process', curveType = 'curved', directionType = 'horizontal') {
this.processOrBPMN = processOrBPMN;
this.curveType = curveType;
this.directionType = directionType;
this.cytoscape[processOrBPMN][curveType][directionType] = cytoscape;
await this.createInsightWithPath();
if(processOrBPMN === 'process') {
await this.highlightMostFrequentPath();
}
}, },
async createInsightWithPath() { bpmn: {
const { insights } = AllMapData(); curved: {
this.insights = {...insights}; horizontal: null,
this.startNode = this.cytoscape[this.processOrBPMN][this.curveType][this.directionType]?.nodes() vertical: null,
.filter(function(elem) { },
elbow: {
horizontal: null,
vertical: null,
}
}
},
processOrBPMN: 'process',
curveType: 'curved',
directionType: 'horizontal',
allPaths: [],
allPathsByEdge: [],
startNode: null,
mapGraphPathToInsight: {},
activeTrace: 0,
activeListIndex: 0,
lastClickedNode: null,
isBPMNOn: false,
}),
actions: {
async setCytoscape(cytoscape, processOrBPMN = 'process', curveType = 'curved', directionType = 'horizontal') {
this.processOrBPMN = processOrBPMN;
this.curveType = curveType;
this.directionType = directionType;
this.cytoscape[processOrBPMN][curveType][directionType] = cytoscape;
await this.createInsightWithPath();
if (processOrBPMN === 'process') {
await this.highlightMostFrequentPath();
}
},
async createInsightWithPath() {
const { insights } = AllMapData();
this.insights = { ...insights };
this.startNode = this.cytoscape[this.processOrBPMN][this.curveType][this.directionType]?.nodes()
.filter(function (elem) {
return elem.data('label').toLocaleLowerCase() === 'start'; return elem.data('label').toLocaleLowerCase() === 'start';
}); });
for(let i = 0; i < INSIGHTS_FIELDS_AND_LABELS.length; i++) { for (let i = 0; i < INSIGHTS_FIELDS_AND_LABELS.length; i++) {
const curButton = this.insights[INSIGHTS_FIELDS_AND_LABELS[i][0]]; const curButton = this.insights[INSIGHTS_FIELDS_AND_LABELS[i][0]];
this.insightWithPath[INSIGHTS_FIELDS_AND_LABELS[i][0]] = {}; // first layer index this.insightWithPath[INSIGHTS_FIELDS_AND_LABELS[i][0]] = {}; // first layer index
for(let listIndex = 0; listIndex < curButton.length; listIndex++) { for (let listIndex = 0; listIndex < curButton.length; listIndex++) {
this.insightWithPath[INSIGHTS_FIELDS_AND_LABELS[i][0]][listIndex] = { this.insightWithPath[INSIGHTS_FIELDS_AND_LABELS[i][0]][listIndex] = {
edges: [], edges: [],
nodes: [], nodes: [],
}; // second layer index }; // second layer index
let curGraphNode, prevGraphNode, curEdge; // 配對 curGraphNode 與 nodeIndex 指向的 node let curGraphNode, prevGraphNode, curEdge; // 配對 curGraphNode 與 nodeIndex 指向的 node
for(let nodeIndex = 0; nodeIndex < curButton[listIndex].length; nodeIndex++){ for (let nodeIndex = 0; nodeIndex < curButton[listIndex].length; nodeIndex++) {
if(nodeIndex === 0) { // special case, initialize curGraphNode if (nodeIndex === 0) { // special case, initialize curGraphNode
curGraphNode = this.startNode.outgoers('node').filter(neighborOfStart => curGraphNode = this.startNode.outgoers('node').filter(neighborOfStart =>
neighborOfStart.data('label') === curButton[listIndex][nodeIndex] neighborOfStart.data('label') === curButton[listIndex][nodeIndex]
);
curEdge = this.startNode.edgesTo(curGraphNode);
} else {
if (prevGraphNode) {
curGraphNode = prevGraphNode.outgoers('node').filter(neighbor =>
neighbor.data('label') === curButton[listIndex][nodeIndex]
); );
curEdge = this.startNode.edgesTo(curGraphNode); curEdge = prevGraphNode.edgesWith(curGraphNode);
} else {
if(prevGraphNode){
curGraphNode = prevGraphNode.outgoers('node').filter(neighbor =>
neighbor.data('label') === curButton[listIndex][nodeIndex]
);
curEdge = prevGraphNode.edgesWith(curGraphNode);
}
} }
this.insightWithPath[INSIGHTS_FIELDS_AND_LABELS[i][0]][listIndex].nodes.push(curGraphNode);
this.insightWithPath[INSIGHTS_FIELDS_AND_LABELS[i][0]][listIndex].edges.push(curEdge);
// 特殊狀況在for迴圈之外額外插入最後一條線段
if(nodeIndex === curButton[listIndex].length - 1){
const endNode = curGraphNode.outgoers('node').filter(neighbor =>
neighbor.data('label').toLocaleLowerCase() === 'end'
);
const lastEdge = curGraphNode.edgesWith(endNode);
this.insightWithPath[INSIGHTS_FIELDS_AND_LABELS[i][0]][listIndex].edges.push(lastEdge);
}
prevGraphNode = curGraphNode;
} }
this.insightWithPath[INSIGHTS_FIELDS_AND_LABELS[i][0]][listIndex].nodes.push(curGraphNode);
this.insightWithPath[INSIGHTS_FIELDS_AND_LABELS[i][0]][listIndex].edges.push(curEdge);
// 特殊狀況在for迴圈之外額外插入最後一條線段
if (nodeIndex === curButton[listIndex].length - 1) {
const endNode = curGraphNode.outgoers('node').filter(neighbor =>
neighbor.data('label').toLocaleLowerCase() === 'end'
);
const lastEdge = curGraphNode.edgesWith(endNode);
this.insightWithPath[INSIGHTS_FIELDS_AND_LABELS[i][0]][listIndex].edges.push(lastEdge);
}
prevGraphNode = curGraphNode;
} }
} }
}, }
async createPaths() { },
this.startNode = this.cytoscape[this.processOrBPMN][this.curveType][this.directionType]?.nodes() async createPaths() {
.filter(function(elem) { this.startNode = this.cytoscape[this.processOrBPMN][this.curveType][this.directionType]?.nodes()
.filter(function (elem) {
return elem.data('label').toLocaleLowerCase() === 'start'; return elem.data('label').toLocaleLowerCase() === 'start';
}); });
// Depth First Search from the starting node // Depth First Search from the starting node
await this.depthFirstSearchCreatePath(this.startNode, [this.startNode], []); await this.depthFirstSearchCreatePath(this.startNode, [this.startNode], []);
const { insights } = AllMapData(); const { insights } = AllMapData();
this.insights = {...insights}; this.insights = { ...insights };
await this.matchGraphPathWithInsightsPath(); await this.matchGraphPathWithInsightsPath();
}, },
/** 從start節點開始建立所有的path /** 從start節點開始建立所有的path
* 於第二個參數逐漸推入節點,於第三個參數逐漸推入線段 * 於第二個參數逐漸推入節點,於第三個參數逐漸推入線段
*/ */
depthFirstSearchCreatePath(node, currentPathByNode, curPathByEdge){ depthFirstSearchCreatePath(node, currentPathByNode, curPathByEdge) {
const outgoingEdges = node.outgoers('edge'); const outgoingEdges = node.outgoers('edge');
if (outgoingEdges.length === 0) { if (outgoingEdges.length === 0) {
// 表示已經遇到尾聲 // 表示已經遇到尾聲
this.allPaths.push([...currentPathByNode]); this.allPaths.push([...currentPathByNode]);
this.allPathsByEdge.push([...curPathByEdge]) this.allPathsByEdge.push([...curPathByEdge])
} else { } else {
outgoingEdges.targets().forEach((targetNode) => { outgoingEdges.targets().forEach((targetNode) => {
if (!currentPathByNode.includes(targetNode)) { if (!currentPathByNode.includes(targetNode)) {
const connectingEdge = targetNode.edgesWith(currentPathByNode[currentPathByNode.length - 1]); const connectingEdge = targetNode.edgesWith(currentPathByNode[currentPathByNode.length - 1]);
// 避免loop只有當目標節點不在當前路徑中之時才繼續 // 避免loop只有當目標節點不在當前路徑中之時才繼續
this.depthFirstSearchCreatePath(targetNode, [...currentPathByNode, targetNode], this.depthFirstSearchCreatePath(targetNode, [...currentPathByNode, targetNode],
[...curPathByEdge, connectingEdge] [...curPathByEdge, connectingEdge]
); );
} }
}); });
} }
}, },
/** /**
* 比對兩條Paths是否相等。 * 比對兩條Paths是否相等。
* 第一條path是透過depthFirstSearchCreatePath所建立 * 第一條path是透過depthFirstSearchCreatePath所建立
* 而第二條path是從後端給的insights物件而來其資料結構是簡單的array而已。 * 而第二條path是從後端給的insights物件而來其資料結構是簡單的array而已。
* 在每條path沿路據節點上的label之 * 在每條path沿路據節點上的label之
* 字串來匹配這個path是屬於insights物件的哪一條path * 字串來匹配這個path是屬於insights物件的哪一條path
* 其中用curButton去記憶住insights[INSIGHTS_FIELDS_AND_LABELS[i][0]]內文 * 其中用curButton去記憶住insights[INSIGHTS_FIELDS_AND_LABELS[i][0]]內文
* 而curButton[listIndex][nodeIndex]是用來確認是否跟depthFirstSearchCreatePath內的 * 而curButton[listIndex][nodeIndex]是用來確認是否跟depthFirstSearchCreatePath內的
* node.data('label')字串完全相等,也就是 activity 節點的文字 * node.data('label')字串完全相等,也就是 activity 節點的文字
*/ */
matchGraphPathWithInsightsPath(){ matchGraphPathWithInsightsPath() {
for(let whichPath = 0; whichPath < this.allPaths.length; whichPath++) { for (let whichPath = 0; whichPath < this.allPaths.length; whichPath++) {
const curPath = this.allPaths[whichPath]; const curPath = this.allPaths[whichPath];
const curPathByEdge = this.allPathsByEdge[whichPath]; const curPathByEdge = this.allPathsByEdge[whichPath];
// 針對這個path的第一個節點找到它在insights中是對應到哪一個起點 // 針對這個path的第一個節點找到它在insights中是對應到哪一個起點
for(let i = 0; i < INSIGHTS_FIELDS_AND_LABELS.length; i++) { for (let i = 0; i < INSIGHTS_FIELDS_AND_LABELS.length; i++) {
const curButton = this.insights[INSIGHTS_FIELDS_AND_LABELS[i][0]]; const curButton = this.insights[INSIGHTS_FIELDS_AND_LABELS[i][0]];
for(let listIndex = 0; listIndex < curButton.length; listIndex++) { for (let listIndex = 0; listIndex < curButton.length; listIndex++) {
for(let nodeIndex = 0; nodeIndex < curButton[listIndex].length; nodeIndex++){ for (let nodeIndex = 0; nodeIndex < curButton[listIndex].length; nodeIndex++) {
if(curPath[1].data('label') === curButton[listIndex][nodeIndex]){ if (curPath[1].data('label') === curButton[listIndex][nodeIndex]) {
// 從 1 開始而不是從 0 開始是因為 0 的label是start字串 // 從 1 開始而不是從 0 開始是因為 0 的label是start字串
const matchResult = this.depthFirstSearchMatchTwoPaths(curPath, 1, curButton, listIndex, nodeIndex) const matchResult = this.depthFirstSearchMatchTwoPaths(curPath, 1, curButton, listIndex, nodeIndex)
if(matchResult){ if (matchResult) {
this.mapGraphPathToInsight[i] = { this.mapGraphPathToInsight[i] = {
[listIndex] : { [listIndex]: {
pathByNode: [...curPath], pathByNode: [...curPath],
pathByEdge: [...curPathByEdge], pathByEdge: [...curPathByEdge],
pathType: INSIGHTS_FIELDS_AND_LABELS[i][0], pathType: INSIGHTS_FIELDS_AND_LABELS[i][0],
}
} }
} // end if }
} } // end if
} // end fourth for }
} // end third for } // end fourth for
} // end second for } // end third for
} // end first for } // end second for
}, } // end first for
depthFirstSearchMatchTwoPaths(curPath, curPathIndex, curButton, listIndex, nodeIndex){ },
if(listIndex >= curButton.length) { // 邊界條件檢查,防止超出範圍 depthFirstSearchMatchTwoPaths(curPath, curPathIndex, curButton, listIndex, nodeIndex) {
return; // nodeIndex表示是當選訂了五顆按鈕之一之後清單上的第幾個path if (listIndex >= curButton.length) { // 邊界條件檢查,防止超出範圍
return; // nodeIndex表示是當選訂了五顆按鈕之一之後清單上的第幾個path
}
if (nodeIndex >= curButton[listIndex]) { // 邊界條件檢查,防止超出範圍
return; // 表示清單上這個path上的第幾個節點
}
// 如果 `curPath` 和 `curButton[listIndex]` 完全匹配
if (curPathIndex === curPath.length || nodeIndex === curButton[listIndex].length) {
return true;
}
// 邊界條件檢查,防止超出範圍
if (curPathIndex >= curPath.length || nodeIndex >= curButton[listIndex].length) {
return;
}
const nodeLabel = curPath[curPathIndex].data('label');
// 如果當前節點匹配
if (nodeLabel === curButton[listIndex][nodeIndex]) {
if (nodeIndex === curButton[listIndex].length - 1) {
return true; // Reach
} }
if(nodeIndex >= curButton[listIndex]) { // 邊界條件檢查,防止超出範圍 //從以下兩個選項選出答案可能是true的。但也可能答案都是false
return; // 表示清單上這個path上的第幾個節點 // 選項一是遞增insights的第一層的指標。這裡必須遞增path的指標
} if (this.depthFirstSearchMatchTwoPaths(curPath, curPathIndex + 1, curButton, listIndex + 1, nodeIndex)) {
// 如果 `curPath` 和 `curButton[listIndex]` 完全匹配
if (curPathIndex === curPath.length || nodeIndex === curButton[listIndex].length) {
return true; return true;
} }
// 選項二是遞增insights的第一層的指標。這裡必須遞增path的指標
// 邊界條件檢查,防止超出範圍 if (this.depthFirstSearchMatchTwoPaths(curPath, curPathIndex + 1, curButton, listIndex, nodeIndex + 1)) {
if (curPathIndex >= curPath.length || nodeIndex >= curButton[listIndex].length) { return true;
return;
} }
}
const nodeLabel = curPath[curPathIndex].data('label'); return false; // 當前節點不匹配時返回 false
// 如果當前節點匹配
if (nodeLabel === curButton[listIndex][nodeIndex]) {
if(nodeIndex === curButton[listIndex].length - 1) {
return true; // Reach
}
//從以下兩個選項選出答案可能是true的。但也可能答案都是false
// 選項一是遞增insights的第一層的指標。這裡必須遞增path的指標
if(this.depthFirstSearchMatchTwoPaths(curPath, curPathIndex + 1, curButton, listIndex + 1, nodeIndex)) {
return true;
}
// 選項二是遞增insights的第一層的指標。這裡必須遞增path的指標
if(this.depthFirstSearchMatchTwoPaths(curPath, curPathIndex + 1, curButton, listIndex, nodeIndex + 1)) {
return true;
}
}
return false; // 當前節點不匹配時返回 false
},
highlightClickedPath(clickedActiveTraceIndex: number, clickedPathListIndex: number){
this.insightWithPath[INSIGHTS_FIELDS_AND_LABELS[clickedActiveTraceIndex][0]][clickedPathListIndex].edges.forEach(edgeToHighlight => {
edgeToHighlight.addClass('highlight-edge');
});
this.insightWithPath[INSIGHTS_FIELDS_AND_LABELS[clickedActiveTraceIndex][0]][clickedPathListIndex].nodes.forEach(nodeToHighlight => {
nodeToHighlight.data('nodeImageUrl', ImgCapsulesGlow[nodeToHighlight.data('level')]);
});
},
highlightClickedPathUnused(clickedActiveTraceIndex: number, clickedPathListIndex: number) {
this.activeTrace = clickedActiveTraceIndex;
this.activeListIndex = clickedPathListIndex;
this.mapGraphPathToInsight[clickedActiveTraceIndex][clickedPathListIndex].pathByEdge
.forEach(pathToHighlight => {
pathToHighlight.addClass('highlight-edge');
});
this.mapGraphPathToInsight[clickedActiveTraceIndex][clickedPathListIndex].pathByNode
.forEach(nodeToHighlight => {
nodeToHighlight.data('nodeImageUrl', ImgCapsulesGlow[nodeToHighlight.data('level')]);
})
},
clearAllHighlight() {
this.cytoscape[this.processOrBPMN][this.curveType][this.directionType]?.edges().removeClass('highlight-edge');
this.cytoscape[this.processOrBPMN][this.curveType][this.directionType]?.nodes().removeClass('highlight-node');
this.cytoscape[this.processOrBPMN][this.curveType][this.directionType]?.nodes().forEach(nodeToReset => {
nodeToReset.data('nodeImageUrl', ImgCapsules[nodeToReset.data('level')])
});
},
onNodeClickHighlightEdges(clickedNode) {
this.clearAllHighlight();
clickedNode.addClass('highlight-node');
clickedNode.data('nodeImageUrl', ImgCapsulesGlow[clickedNode.data('level')]);
clickedNode.outgoers('edge').forEach(edgeToHighlight => edgeToHighlight.addClass('highlight-edge'));
clickedNode.incomers('edge').forEach(edgeToHighlight => edgeToHighlight.addClass('highlight-edge'));
this.lastClickedNode = clickedNode;
},
onEdgeClickHighlightNodes(clickedEdge) {
this.clearAllHighlight();
const sourceNode = clickedEdge.source();
const targetNode = clickedEdge.target();
sourceNode.addClass('highlight-node');
targetNode.addClass('highlight-node');
sourceNode.data('nodeImageUrl', ImgCapsulesGlow[sourceNode.data('level')]);
targetNode.data('nodeImageUrl', ImgCapsulesGlow[targetNode.data('level')]);
clickedEdge.addClass('highlight-edge');
},
async highlightMostFrequentPath() {
console.log('highlightMostFrequentPath', this.insightWithPath['most_freq_traces'][0]);
const LIST_INDEX = 0;
this.insightWithPath['most_freq_traces'][LIST_INDEX].nodes.map(nodeToHighlight => {
nodeToHighlight.data('nodeImageUrl', ImgCapsulesGlow[nodeToHighlight.data('level')]);
});
this.insightWithPath['most_freq_traces'][LIST_INDEX].edges.map(edgeToHighlight =>
edgeToHighlight.addClass('highlight-edge'));
},
async highlightMostFrequentPathUnused() {
for(let buttonIter = 0; buttonIter < INSIGHTS_FIELDS_AND_LABELS.length; buttonIter++) {
// 有可能遇到兩個以上的most frequent paths然而我們只取一個path點亮
if (this.mapGraphPathToInsight[buttonIter]) {
const keyLength = Object.keys(this.mapGraphPathToInsight[buttonIter]).length;
for(let i = 0; i < keyLength; i++) {
if(this.mapGraphPathToInsight[buttonIter][i]?.pathType === 'most_freq_traces'){
await this.mapGraphPathToInsight[buttonIter][i].pathByNode.map(async(nodeToHighlight) => {
await nodeToHighlight.data('nodeImageUrl', ImgCapsulesGlow[nodeToHighlight.data('level')]);
});
await this.mapGraphPathToInsight[buttonIter][i].pathByEdge.map(
async(edgeToHighlight) => {
await edgeToHighlight.addClass('highlight-edge');
});
return; // 之所以要此時就立刻return是因為要避免第二個以上的most freq path也被點亮
}
break;
}
}
}
},
setIsBPMNOn(isOn: boolean) {
this.isBPMNOn = isOn;
},
}, },
highlightClickedPath(clickedActiveTraceIndex: number, clickedPathListIndex: number) {
this.insightWithPath[INSIGHTS_FIELDS_AND_LABELS[clickedActiveTraceIndex][0]][clickedPathListIndex].edges.forEach(edgeToHighlight => {
edgeToHighlight.addClass('highlight-edge');
});
this.insightWithPath[INSIGHTS_FIELDS_AND_LABELS[clickedActiveTraceIndex][0]][clickedPathListIndex].nodes.forEach(nodeToHighlight => {
nodeToHighlight.data('nodeImageUrl', ImgCapsulesGlow[nodeToHighlight.data('level')]);
});
},
highlightClickedPathUnused(clickedActiveTraceIndex: number, clickedPathListIndex: number) {
this.activeTrace = clickedActiveTraceIndex;
this.activeListIndex = clickedPathListIndex;
this.mapGraphPathToInsight[clickedActiveTraceIndex][clickedPathListIndex].pathByEdge
.forEach(pathToHighlight => {
pathToHighlight.addClass('highlight-edge');
});
this.mapGraphPathToInsight[clickedActiveTraceIndex][clickedPathListIndex].pathByNode
.forEach(nodeToHighlight => {
nodeToHighlight.data('nodeImageUrl', ImgCapsulesGlow[nodeToHighlight.data('level')]);
})
},
clearAllHighlight() {
this.cytoscape[this.processOrBPMN][this.curveType][this.directionType]?.edges().removeClass('highlight-edge');
this.cytoscape[this.processOrBPMN][this.curveType][this.directionType]?.nodes().removeClass('highlight-node');
this.cytoscape[this.processOrBPMN][this.curveType][this.directionType]?.nodes().forEach(nodeToReset => {
nodeToReset.data('nodeImageUrl', ImgCapsules[nodeToReset.data('level')])
});
},
onNodeClickHighlightEdges(clickedNode) {
this.clearAllHighlight();
clickedNode.addClass('highlight-node');
clickedNode.data('nodeImageUrl', ImgCapsulesGlow[clickedNode.data('level')]);
clickedNode.outgoers('edge').forEach(edgeToHighlight => edgeToHighlight.addClass('highlight-edge'));
clickedNode.incomers('edge').forEach(edgeToHighlight => edgeToHighlight.addClass('highlight-edge'));
this.lastClickedNode = clickedNode;
},
onEdgeClickHighlightNodes(clickedEdge) {
this.clearAllHighlight();
const sourceNode = clickedEdge.source();
const targetNode = clickedEdge.target();
sourceNode.addClass('highlight-node');
targetNode.addClass('highlight-node');
sourceNode.data('nodeImageUrl', ImgCapsulesGlow[sourceNode.data('level')]);
targetNode.data('nodeImageUrl', ImgCapsulesGlow[targetNode.data('level')]);
clickedEdge.addClass('highlight-edge');
},
async highlightMostFrequentPath() {
const LIST_INDEX = 0;
this.insightWithPath['most_freq_traces'][LIST_INDEX].nodes.map(nodeToHighlight => {
nodeToHighlight.data('nodeImageUrl', ImgCapsulesGlow[nodeToHighlight.data('level')]);
});
this.insightWithPath['most_freq_traces'][LIST_INDEX].edges.map(edgeToHighlight =>
edgeToHighlight.addClass('highlight-edge'));
},
async highlightMostFrequentPathUnused() {
for (let buttonIter = 0; buttonIter < INSIGHTS_FIELDS_AND_LABELS.length; buttonIter++) {
// 有可能遇到兩個以上的most frequent paths然而我們只取一個path點亮
if (this.mapGraphPathToInsight[buttonIter]) {
const keyLength = Object.keys(this.mapGraphPathToInsight[buttonIter]).length;
for (let i = 0; i < keyLength; i++) {
if (this.mapGraphPathToInsight[buttonIter][i]?.pathType === 'most_freq_traces') {
await this.mapGraphPathToInsight[buttonIter][i].pathByNode.map(async (nodeToHighlight) => {
await nodeToHighlight.data('nodeImageUrl', ImgCapsulesGlow[nodeToHighlight.data('level')]);
});
await this.mapGraphPathToInsight[buttonIter][i].pathByEdge.map(
async (edgeToHighlight) => {
await edgeToHighlight.addClass('highlight-edge');
});
return; // 之所以要此時就立刻return是因為要避免第二個以上的most freq path也被點亮
}
break;
}
}
}
},
setIsBPMNOn(isOn: boolean) {
this.isBPMNOn = isOn;
},
},
}); });

View File

@@ -1,22 +1,29 @@
<template> <template>
<!-- Sidebar: Switch data type --> <!-- Sidebar: Switch data type -->
<div class="flex flex-col justify-between py-4 w-14 h-screen-main absolute bottom-0 left-0 z-10" :class="sidebarLeftValue? 'bg-neutral-50':''"> <div class="flex flex-col justify-between py-4 w-14 h-screen-main absolute bottom-0 left-0 z-10"
:class="sidebarLeftValue ? 'bg-neutral-50' : ''">
<ul class="space-y-4 flex flex-col justify-center items-center"> <ul class="space-y-4 flex flex-col justify-center items-center">
<li class="inline-flex items-center justify-center border border-neutral-500 rounded-full w-9 h-9 cursor-pointer bg-neutral-50 drop-shadow <li class="inline-flex items-center justify-center border border-neutral-500 rounded-full w-9 h-9 cursor-pointer bg-neutral-50 drop-shadow
hover:border-primary" @click="sidebarView = !sidebarView" :class="{'border-primary': sidebarView}" v-tooltip="tooltip.sidebarView"> hover:border-primary" @click="sidebarView = !sidebarView" :class="{ 'border-primary': sidebarView }"
<span class="material-symbols-outlined !text-2xl hover:text-primary p-1.5" :class="[sidebarView ? 'text-primary' : 'text-neutral-500']"> v-tooltip="tooltip.sidebarView">
<span class="material-symbols-outlined !text-2xl hover:text-primary p-1.5"
:class="[sidebarView ? 'text-primary' : 'text-neutral-500']">
track_changes track_changes
</span> </span>
</li> </li>
<li class="inline-flex items-center justify-center border border-neutral-500 rounded-full w-9 h-9 cursor-pointer bg-neutral-50 drop-shadow <li class="inline-flex items-center justify-center border border-neutral-500 rounded-full w-9 h-9 cursor-pointer bg-neutral-50 drop-shadow
hover:border-primary" @click="sidebarFilter = !sidebarFilter" :class="{'border-primary': sidebarFilter}" v-tooltip="tooltip.sidebarFilter"> hover:border-primary" @click="sidebarFilter = !sidebarFilter" :class="{ 'border-primary': sidebarFilter }"
<span class="material-symbols-outlined !text-2xl hover:text-primary p-1.5" :class="[sidebarFilter ? 'text-primary' : 'text-neutral-500']" id="iconFilter"> v-tooltip="tooltip.sidebarFilter">
<span class="material-symbols-outlined !text-2xl hover:text-primary p-1.5"
:class="[sidebarFilter ? 'text-primary' : 'text-neutral-500']" id="iconFilter">
tornado tornado
</span> </span>
</li> </li>
<li class="inline-flex items-center justify-center border border-neutral-500 rounded-full w-9 h-9 cursor-pointer bg-neutral-50 <li class="inline-flex items-center justify-center border border-neutral-500 rounded-full w-9 h-9 cursor-pointer bg-neutral-50
drop-shadow hover:border-primary" @click="sidebarTraces = !sidebarTraces" :class="{'border-primary': sidebarTraces}" v-tooltip="tooltip.sidebarTraces"> drop-shadow hover:border-primary" @click="sidebarTraces = !sidebarTraces"
<span class="material-symbols-outlined !text-2xl hover:text-primary p-1.5" :class="[sidebarTraces ? 'text-primary' : 'text-neutral-500']"> :class="{ 'border-primary': sidebarTraces }" v-tooltip="tooltip.sidebarTraces">
<span class="material-symbols-outlined !text-2xl hover:text-primary p-1.5"
:class="[sidebarTraces ? 'text-primary' : 'text-neutral-500']">
rebase rebase
</span> </span>
</li> </li>
@@ -33,7 +40,7 @@
<ul class="flex flex-col justify-center items-center"> <ul class="flex flex-col justify-center items-center">
<li class="inline-flex items-center justify-center border border-neutral-500 rounded-full w-9 h-9 cursor-pointer <li class="inline-flex items-center justify-center border border-neutral-500 rounded-full w-9 h-9 cursor-pointer
bg-neutral-50 drop-shadow hover:border-primary" @click="sidebarState = !sidebarState" bg-neutral-50 drop-shadow hover:border-primary" @click="sidebarState = !sidebarState"
:class="{'border-primary': sidebarState}" id="iconState" v-tooltip.left="tooltip.sidebarState"> :class="{ 'border-primary': sidebarState }" id="iconState" v-tooltip.left="tooltip.sidebarState">
<span class="material-symbols-outlined !text-2xl text-neutral-500 hover:text-primary p-1.5" <span class="material-symbols-outlined !text-2xl text-neutral-500 hover:text-primary p-1.5"
:class="[sidebarState ? 'text-primary' : 'text-neutral-500']"> :class="[sidebarState ? 'text-primary' : 'text-neutral-500']">
info info
@@ -43,13 +50,14 @@
</div> </div>
<!-- Sidebar Model --> <!-- Sidebar Model -->
<SidebarView v-model:visible="sidebarView" @switch-map-type="switchMapType" @switch-curve-styles="switchCurveStyles" @switch-rank="switchRank" <SidebarView v-model:visible="sidebarView" @switch-map-type="switchMapType" @switch-curve-styles="switchCurveStyles"
@switch-data-layer-type="switchDataLayerType" ></SidebarView> @switch-rank="switchRank" @switch-data-layer-type="switchDataLayerType"></SidebarView>
<SidebarState v-model:visible="sidebarState" :insights="insights" :stats="stats"></SidebarState> <SidebarState v-model:visible="sidebarState" :insights="insights" :stats="stats"></SidebarState>
<SidebarTraces v-model:visible="sidebarTraces" :cases="cases" @switch-Trace-Id="switchTraceId" ref="tracesView"></SidebarTraces> <SidebarTraces v-model:visible="sidebarTraces" :cases="cases" @switch-Trace-Id="switchTraceId" ref="tracesView">
</SidebarTraces>
<SidebarFilter v-model:visible="sidebarFilter" :filterTasks="filterTasks" :filterStartToEnd="filterStartToEnd" <SidebarFilter v-model:visible="sidebarFilter" :filterTasks="filterTasks" :filterStartToEnd="filterStartToEnd"
:filterEndToStart="filterEndToStart" :filterTimeframe="filterTimeframe" :filterTrace="filterTrace" :filterEndToStart="filterEndToStart" :filterTimeframe="filterTimeframe" :filterTrace="filterTrace"
@submit-all="createCy(mapType)" @switch-Trace-Id="switchTraceId" ref="sidevarFilterRef"></SidebarFilter> @submit-all="createCy(mapType)" @switch-Trace-Id="switchTraceId" ref="sidevarFilterRef"></SidebarFilter>
</template> </template>
<script> <script>
@@ -113,14 +121,15 @@ export default {
}); });
return { isLoading, processMap, bpmn, stats, insights, traceId, traces, baseTraces, return {
isLoading, processMap, bpmn, stats, insights, traceId, traces, baseTraces,
baseTraceId, filterTasks, filterStartToEnd, filterEndToStart, filterTimeframe, baseTraceId, filterTasks, filterStartToEnd, filterEndToStart, filterTimeframe,
filterTrace, logId, baseLogId, createFilterId, temporaryData, isRuleData, filterTrace, logId, baseLogId, createFilterId, temporaryData, isRuleData,
ruleData, allMapDataStore, cases, postRuleData, ruleData, allMapDataStore, cases, postRuleData,
setCurrentGraphId, setCurrentGraphId,
}; };
}, },
props:['type', 'checkType', 'checkId', 'checkFileId'], // 來自 router 的 props props: ['type', 'checkType', 'checkId', 'checkFileId'], // 來自 router 的 props
components: { components: {
SidebarView, SidebarView,
SidebarState, SidebarState,
@@ -142,7 +151,7 @@ export default {
edges: [], edges: [],
}, },
cytoscapeGraph: null, cytoscapeGraph: null,
curveStyle:'unbundled-bezier', // unbundled-bezier | taxi curveStyle: 'unbundled-bezier', // unbundled-bezier | taxi
mapType: 'processMap', // processMap | bpmn mapType: 'processMap', // processMap | bpmn
mapPathStore: MapPathStore(), mapPathStore: MapPathStore(),
dataLayerType: 'freq', // freq | duration dataLayerType: 'freq', // freq | duration
@@ -154,6 +163,8 @@ export default {
sidebarTraces: false, // SideBar: Traces sidebarTraces: false, // SideBar: Traces
sidebarFilter: false, // SideBar: Filter sidebarFilter: false, // SideBar: Filter
infiniteFirstCases: null, infiniteFirstCases: null,
startNodeId: -1,
endNodeId: -1,
tooltip: { tooltip: {
sidebarView: { sidebarView: {
value: 'Visualization Setting', value: 'Visualization Setting',
@@ -186,37 +197,37 @@ export default {
} }
} }
}, },
computed:{ computed: {
sidebarLeftValue: function() { sidebarLeftValue: function () {
let result = this.sidebarView === true || this.sidebarTraces === true || this.sidebarFilter === true; let result = this.sidebarView === true || this.sidebarTraces === true || this.sidebarFilter === true;
return result; return result;
} }
}, },
watch: { watch: {
sidebarView: function(newValue) { sidebarView: function (newValue) {
if(newValue) { if (newValue) {
this.sidebarFilter = false; this.sidebarFilter = false;
this.sidebarTraces = false; this.sidebarTraces = false;
} }
}, },
sidebarFilter: function(newValue) { sidebarFilter: function (newValue) {
if(newValue) { if (newValue) {
this.sidebarView = false; this.sidebarView = false;
this.sidebarState = false; this.sidebarState = false;
this.sidebarTraces = false; this.sidebarTraces = false;
this.sidebarState = false; this.sidebarState = false;
} }
}, },
sidebarTraces: function(newValue) { sidebarTraces: function (newValue) {
if(newValue) { if (newValue) {
this.sidebarView = false; this.sidebarView = false;
this.sidebarState = false; this.sidebarState = false;
this.sidebarFilter = false; this.sidebarFilter = false;
this.sidebarState = false; this.sidebarState = false;
} }
}, },
sidebarState: function(newValue) { sidebarState: function (newValue) {
if(newValue) { if (newValue) {
this.sidebarFilter = false; this.sidebarFilter = false;
this.sidebarTraces = false; this.sidebarTraces = false;
} }
@@ -252,7 +263,7 @@ export default {
* @param {string} type freq | duration * @param {string} type freq | duration
* @param {string} option 下拉選單中的選項 * @param {string} option 下拉選單中的選項
*/ */
async switchDataLayerType(type, option){ async switchDataLayerType(type, option) {
this.dataLayerType = type; this.dataLayerType = type;
this.dataLayerOption = option; this.dataLayerOption = option;
this.createCy(this.mapType); this.createCy(this.mapType);
@@ -262,7 +273,7 @@ export default {
* @param {event} e input 傳入的事件 * @param {event} e input 傳入的事件
*/ */
async switchTraceId(e) { async switchTraceId(e) {
if(e.id == this.traceId) return; if (e.id == this.traceId) return;
// 超過 1000 筆要 loading 畫面 // 超過 1000 筆要 loading 畫面
this.isLoading = true; // 都要 loading 畫面 this.isLoading = true; // 都要 loading 畫面
this.traceId = e.id; this.traceId = e.id;
@@ -274,9 +285,9 @@ export default {
* 將 element nodes 資料彙整 * 將 element nodes 資料彙整
* @param {object} type 'processMapData' | 'bpmnData',可傳入以上任一。 * @param {object} type 'processMapData' | 'bpmnData',可傳入以上任一。
*/ */
setNodesData(mapData) { setNodesData(mapData) {
let mapType = this.mapType; let mapType = this.mapType;
const logFreq = { const logFreq = {
"total": "", "total": "",
"rel_freq": "", "rel_freq": "",
"average": "", "average": "",
@@ -308,64 +319,70 @@ export default {
// add type of 'bpmn gateway' node // add type of 'bpmn gateway' node
case 'gateway': case 'gateway':
mapData.nodes.push({ mapData.nodes.push({
data:{ data: {
id:node.id, id: node.id,
type:node.type, type: node.type,
label:gateway[node.gateway_type], label: gateway[node.gateway_type],
height:60, height: 60,
width:60, width: 60,
backgroundColor:'#FFF', backgroundColor: '#FFF',
bordercolor:'#003366', bordercolor: '#003366',
shape:"diamond", shape: "diamond",
freq:logFreq, freq: logFreq,
duration:logDuration, duration: logDuration,
} }
}) })
break; break;
// add type of 'event' node // add type of 'event' node
case 'event': case 'event':
if(node.event_type === 'start') mapData.startId = node.id; if (node.event_type === 'start') {
else if(node.event_type === 'end') mapData.endId = node.id; mapData.startId = node.id;
this.startNodeId = node.id;
}
else if (node.event_type === 'end') {
mapData.endId = node.id;
this.endNodeId = node.id;
}
mapData.nodes.push({ mapData.nodes.push({
data:{ data: {
id:node.id, id: node.id,
type:node.type, type: node.type,
label:node.event_type, label: node.event_type,
height: 48, height: 48,
width: 48, width: 48,
backgroundColor:'#FFFFFF', backgroundColor: '#FFFFFF',
bordercolor:'#0F172A', bordercolor: '#0F172A',
textColor: '#FF3366', textColor: '#FF3366',
shape:"ellipse", shape: "ellipse",
freq:logFreq, freq: logFreq,
duration:logDuration, duration: logDuration,
} }
}); });
break; break;
// add type of 'activity' node // add type of 'activity' node
default: default:
mapData.nodes.push({ mapData.nodes.push({
data:{ data: {
id:node.id, id: node.id,
type:node.type, type: node.type,
label:node.label, label: node.label,
height: 48, height: 48,
width: 216, width: 216,
textColor: '#0F172A', textColor: '#0F172A',
backgroundColor:'rgba(0, 0, 0, 0)', backgroundColor: 'rgba(0, 0, 0, 0)',
borderradius: 999, borderradius: 999,
shape:"round-rectangle", shape: "round-rectangle",
freq:node.freq, freq: node.freq,
duration:node.duration, duration: node.duration,
backgroundOpacity: 0, backgroundOpacity: 0,
borderOpacity: 0, borderOpacity: 0,
} }
}) })
break; break;
} }
}); });
}, },
/** /**
* 將 element edges 資料彙整 * 將 element edges 資料彙整
* @param {object} type 'processMapData' | 'bpmnData',可傳入以上任一。 * @param {object} type 'processMapData' | 'bpmnData',可傳入以上任一。
@@ -387,28 +404,29 @@ export default {
this[mapType].edges.map(edge => { this[mapType].edges.map(edge => {
mapData.edges.push({ mapData.edges.push({
data: { data: {
source:edge.tail, source: edge.tail,
target:edge.head, target: edge.head,
freq:edge.freq, freq: edge.freq,
duration:edge.duration === null ? logDuration : edge.duration, duration: edge.duration === null ? logDuration : edge.duration,
style:'dotted', // Don't know why but tail is related to start and head is related to end
lineWidth:1, edgeStyle: edge.tail === this.startNodeId || edge.head === this.endNodeId ? 'dotted' : 'solid',
lineWidth: 1,
}, },
}); });
}); });
}, },
/** /**
* create cytoscape's map * create cytoscape's map
* @param {string} type this.mapType 'processMap' | 'bpmn',可傳入以上任一。 * @param {string} type this.mapType 'processMap' | 'bpmn',可傳入以上任一。
*/ */
async createCy(type) { async createCy(type) {
let graphId = document.getElementById('cy'); let graphId = document.getElementById('cy');
let mapData = type === 'processMap'? this.processMapData: this.bpmnData; let mapData = type === 'processMap' ? this.processMapData : this.bpmnData;
if(this[type].vertices.length !== 0){ if (this[type].vertices.length !== 0) {
this.setNodesData(mapData); this.setNodesData(mapData);
this.setEdgesData(mapData); this.setEdgesData(mapData);
this.setActivityBgImage(mapData); this.setActivityBgImage(mapData);
this.cytoscapeGraph = await cytoscapeMap(mapData, this.dataLayerType, this.dataLayerOption, this.curveStyle, this.rank, graphId); this.cytoscapeGraph = await cytoscapeMap(mapData, this.dataLayerType, this.dataLayerOption, this.curveStyle, this.rank, graphId);
const processOrBPMN = this.mapType === 'processMap' ? 'process' : 'bpmn'; const processOrBPMN = this.mapType === 'processMap' ? 'process' : 'bpmn';
const curveType = this.curveStyle === 'taxi' ? 'elbow' : 'curved'; const curveType = this.curveStyle === 'taxi' ? 'elbow' : 'curved';
@@ -429,24 +447,24 @@ export default {
activityNodeArray.map(node => nodeOptionArr.push(node.data[this.dataLayerType][this.dataLayerOption])); activityNodeArray.map(node => nodeOptionArr.push(node.data[this.dataLayerType][this.dataLayerOption]));
// 將node的option值從小到大排序(映對色階淺到深) // 將node的option值從小到大排序(映對色階淺到深)
nodeOptionArr = nodeOptionArr.sort((a, b) => a - b); nodeOptionArr = nodeOptionArr.sort((a, b) => a - b);
for(let i = 0; i < ImgCapsules.length; i++) { for (let i = 0; i < ImgCapsules.length; i++) {
const startIdx = i * groupSize; const startIdx = i * groupSize;
const endIdx = (i === ImgCapsules.length - 1) ? activityNodeArray.length : startIdx + groupSize; const endIdx = (i === ImgCapsules.length - 1) ? activityNodeArray.length : startIdx + groupSize;
leveledGroups.push(nodeOptionArr.slice(startIdx, endIdx)); leveledGroups.push(nodeOptionArr.slice(startIdx, endIdx));
} }
for(let level = 0; level < leveledGroups.length; level++) { for (let level = 0; level < leveledGroups.length; level++) {
leveledGroups[level].map(option => { leveledGroups[level].map(option => {
// 考慮可能有名次一樣的情形 // 考慮可能有名次一樣的情形
const curNodes = activityNodeArray.filter(activityNode => activityNode.data[this.dataLayerType][this.dataLayerOption] === option); const curNodes = activityNodeArray.filter(activityNode => activityNode.data[this.dataLayerType][this.dataLayerOption] === option);
curNodes.map(curNode => { curNodes.map(curNode => {
curNode.data = { curNode.data = {
...curNode.data, ...curNode.data,
nodeImageUrl: ImgCapsules[level], nodeImageUrl: ImgCapsules[level],
level, level,
}; };
}); });
}); });
} }
}, },
}, },
async created() { async created() {
@@ -459,7 +477,7 @@ export default {
// Log 檔前往 Map Log 頁, Filter 檔前往 Map Filter 頁 // Log 檔前往 Map Log 頁, Filter 檔前往 Map Filter 頁
switch (routeParams.type) { switch (routeParams.type) {
case 'log': case 'log':
if(!isCheckPage) { if (!isCheckPage) {
this.logId = await routeParams.fileId; this.logId = await routeParams.fileId;
this.baseLogId = await routeParams.fileId; this.baseLogId = await routeParams.fileId;
} else { } else {
@@ -468,7 +486,7 @@ export default {
} }
break; break;
case 'filter': case 'filter':
if(!isCheckPage) { if (!isCheckPage) {
this.createFilterId = await routeParams.fileId; this.createFilterId = await routeParams.fileId;
} else { } else {
this.createFilterId = await file.parent.id; this.createFilterId = await file.parent.id;
@@ -482,7 +500,7 @@ export default {
// 取得 logId 後才 call api // 取得 logId 後才 call api
await this.allMapDataStore.getAllMapData(); await this.allMapDataStore.getAllMapData();
await this.allMapDataStore.getAllTrace(); await this.allMapDataStore.getAllTrace();
// log、filter 檔切換過程中, trace id 不同,將初始 trace id 設定為該檔案的 trace 幣一筆資料的 id。 // log、filter 檔切換過程中, trace id 不同,將初始 trace id 設定為該檔案的 trace 幣一筆資料的 id。
this.traceId = await this.traces[0]?.id; this.traceId = await this.traces[0]?.id;
this.baseTraceId = await this.baseTraces[0]?.id; this.baseTraceId = await this.baseTraces[0]?.id;