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
//'control-point-step-size':100, // 從點到點的垂直線,指定貝茲取線邊緣間的距離
'width': 'data(lineWidth)',
'line-style': 'data(style)',
'line-style': 'data(edgeStyle)',
"text-margin-y": "0.7rem",
//"text-rotation": "autorotate",
}

View File

@@ -21,23 +21,23 @@ export default defineStore('useMapPathStore', {
cytoscape: {
process:
{
curved:{
horizontal:null,
vertical:null,
curved: {
horizontal: null,
vertical: null,
},
elbow:{
horizontal:null,
vertical:null,
elbow: {
horizontal: null,
vertical: null,
}
},
bpmn: {
curved:{
horizontal:null,
vertical:null,
curved: {
horizontal: null,
vertical: null,
},
elbow:{
horizontal:null,
vertical:null,
elbow: {
horizontal: null,
vertical: null,
}
}
},
@@ -60,36 +60,36 @@ export default defineStore('useMapPathStore', {
this.directionType = directionType;
this.cytoscape[processOrBPMN][curveType][directionType] = cytoscape;
await this.createInsightWithPath();
if(processOrBPMN === 'process') {
if (processOrBPMN === 'process') {
await this.highlightMostFrequentPath();
}
},
async createInsightWithPath() {
const { insights } = AllMapData();
this.insights = {...insights};
this.insights = { ...insights };
this.startNode = this.cytoscape[this.processOrBPMN][this.curveType][this.directionType]?.nodes()
.filter(function(elem) {
.filter(function (elem) {
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]];
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] = {
edges: [],
nodes: [],
}; // second layer index
let curGraphNode, prevGraphNode, curEdge; // 配對 curGraphNode 與 nodeIndex 指向的 node
for(let nodeIndex = 0; nodeIndex < curButton[listIndex].length; nodeIndex++){
if(nodeIndex === 0) { // special case, initialize curGraphNode
for (let nodeIndex = 0; nodeIndex < curButton[listIndex].length; nodeIndex++) {
if (nodeIndex === 0) { // special case, initialize curGraphNode
curGraphNode = this.startNode.outgoers('node').filter(neighborOfStart =>
neighborOfStart.data('label') === curButton[listIndex][nodeIndex]
);
curEdge = this.startNode.edgesTo(curGraphNode);
} else {
if(prevGraphNode){
if (prevGraphNode) {
curGraphNode = prevGraphNode.outgoers('node').filter(neighbor =>
neighbor.data('label') === curButton[listIndex][nodeIndex]
);
@@ -99,7 +99,7 @@ export default defineStore('useMapPathStore', {
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){
if (nodeIndex === curButton[listIndex].length - 1) {
const endNode = curGraphNode.outgoers('node').filter(neighbor =>
neighbor.data('label').toLocaleLowerCase() === 'end'
);
@@ -113,19 +113,19 @@ export default defineStore('useMapPathStore', {
},
async createPaths() {
this.startNode = this.cytoscape[this.processOrBPMN][this.curveType][this.directionType]?.nodes()
.filter(function(elem) {
.filter(function (elem) {
return elem.data('label').toLocaleLowerCase() === 'start';
});
// Depth First Search from the starting node
await this.depthFirstSearchCreatePath(this.startNode, [this.startNode], []);
const { insights } = AllMapData();
this.insights = {...insights};
this.insights = { ...insights };
await this.matchGraphPathWithInsightsPath();
},
/** 從start節點開始建立所有的path
* 於第二個參數逐漸推入節點,於第三個參數逐漸推入線段
*/
depthFirstSearchCreatePath(node, currentPathByNode, curPathByEdge){
depthFirstSearchCreatePath(node, currentPathByNode, curPathByEdge) {
const outgoingEdges = node.outgoers('edge');
if (outgoingEdges.length === 0) {
// 表示已經遇到尾聲
@@ -153,21 +153,21 @@ export default defineStore('useMapPathStore', {
* 而curButton[listIndex][nodeIndex]是用來確認是否跟depthFirstSearchCreatePath內的
* node.data('label')字串完全相等,也就是 activity 節點的文字
*/
matchGraphPathWithInsightsPath(){
for(let whichPath = 0; whichPath < this.allPaths.length; whichPath++) {
matchGraphPathWithInsightsPath() {
for (let whichPath = 0; whichPath < this.allPaths.length; whichPath++) {
const curPath = this.allPaths[whichPath];
const curPathByEdge = this.allPathsByEdge[whichPath];
// 針對這個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]];
for(let listIndex = 0; listIndex < curButton.length; listIndex++) {
for(let nodeIndex = 0; nodeIndex < curButton[listIndex].length; nodeIndex++){
if(curPath[1].data('label') === curButton[listIndex][nodeIndex]){
for (let listIndex = 0; listIndex < curButton.length; listIndex++) {
for (let nodeIndex = 0; nodeIndex < curButton[listIndex].length; nodeIndex++) {
if (curPath[1].data('label') === curButton[listIndex][nodeIndex]) {
// 從 1 開始而不是從 0 開始是因為 0 的label是start字串
const matchResult = this.depthFirstSearchMatchTwoPaths(curPath, 1, curButton, listIndex, nodeIndex)
if(matchResult){
if (matchResult) {
this.mapGraphPathToInsight[i] = {
[listIndex] : {
[listIndex]: {
pathByNode: [...curPath],
pathByEdge: [...curPathByEdge],
pathType: INSIGHTS_FIELDS_AND_LABELS[i][0],
@@ -180,11 +180,11 @@ export default defineStore('useMapPathStore', {
} // end second for
} // end first for
},
depthFirstSearchMatchTwoPaths(curPath, curPathIndex, curButton, listIndex, nodeIndex){
if(listIndex >= curButton.length) { // 邊界條件檢查,防止超出範圍
depthFirstSearchMatchTwoPaths(curPath, curPathIndex, curButton, listIndex, nodeIndex) {
if (listIndex >= curButton.length) { // 邊界條件檢查,防止超出範圍
return; // nodeIndex表示是當選訂了五顆按鈕之一之後清單上的第幾個path
}
if(nodeIndex >= curButton[listIndex]) { // 邊界條件檢查,防止超出範圍
if (nodeIndex >= curButton[listIndex]) { // 邊界條件檢查,防止超出範圍
return; // 表示清單上這個path上的第幾個節點
}
// 如果 `curPath` 和 `curButton[listIndex]` 完全匹配
@@ -200,22 +200,22 @@ export default defineStore('useMapPathStore', {
const nodeLabel = curPath[curPathIndex].data('label');
// 如果當前節點匹配
if (nodeLabel === curButton[listIndex][nodeIndex]) {
if(nodeIndex === curButton[listIndex].length - 1) {
if (nodeIndex === curButton[listIndex].length - 1) {
return true; // Reach
}
//從以下兩個選項選出答案可能是true的。但也可能答案都是false
// 選項一是遞增insights的第一層的指標。這裡必須遞增path的指標
if(this.depthFirstSearchMatchTwoPaths(curPath, curPathIndex + 1, curButton, listIndex + 1, nodeIndex)) {
if (this.depthFirstSearchMatchTwoPaths(curPath, curPathIndex + 1, curButton, listIndex + 1, nodeIndex)) {
return true;
}
// 選項二是遞增insights的第一層的指標。這裡必須遞增path的指標
if(this.depthFirstSearchMatchTwoPaths(curPath, curPathIndex + 1, curButton, listIndex, nodeIndex + 1)) {
if (this.depthFirstSearchMatchTwoPaths(curPath, curPathIndex + 1, curButton, listIndex, nodeIndex + 1)) {
return true;
}
}
return false; // 當前節點不匹配時返回 false
},
highlightClickedPath(clickedActiveTraceIndex: number, clickedPathListIndex: number){
highlightClickedPath(clickedActiveTraceIndex: number, clickedPathListIndex: number) {
this.insightWithPath[INSIGHTS_FIELDS_AND_LABELS[clickedActiveTraceIndex][0]][clickedPathListIndex].edges.forEach(edgeToHighlight => {
edgeToHighlight.addClass('highlight-edge');
});
@@ -265,7 +265,6 @@ export default defineStore('useMapPathStore', {
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')]);
@@ -274,17 +273,17 @@ export default defineStore('useMapPathStore', {
edgeToHighlight.addClass('highlight-edge'));
},
async highlightMostFrequentPathUnused() {
for(let buttonIter = 0; buttonIter < INSIGHTS_FIELDS_AND_LABELS.length; buttonIter++) {
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) => {
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) => {
async (edgeToHighlight) => {
await edgeToHighlight.addClass('highlight-edge');
});
return; // 之所以要此時就立刻return是因為要避免第二個以上的most freq path也被點亮

View File

@@ -1,22 +1,29 @@
<template>
<!-- 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">
<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">
<span class="material-symbols-outlined !text-2xl hover:text-primary p-1.5" :class="[sidebarView ? 'text-primary' : 'text-neutral-500']">
hover:border-primary" @click="sidebarView = !sidebarView" :class="{ 'border-primary': sidebarView }"
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
</span>
</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
hover:border-primary" @click="sidebarFilter = !sidebarFilter" :class="{'border-primary': sidebarFilter}" 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">
hover:border-primary" @click="sidebarFilter = !sidebarFilter" :class="{ 'border-primary': sidebarFilter }"
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
</span>
</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 hover:border-primary" @click="sidebarTraces = !sidebarTraces" :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']">
drop-shadow hover:border-primary" @click="sidebarTraces = !sidebarTraces"
: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
</span>
</li>
@@ -33,7 +40,7 @@
<ul class="flex flex-col justify-center items-center">
<li class="inline-flex items-center justify-center border border-neutral-500 rounded-full w-9 h-9 cursor-pointer
bg-neutral-50 drop-shadow hover:border-primary" @click="sidebarState = !sidebarState"
:class="{'border-primary': sidebarState}" id="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"
:class="[sidebarState ? 'text-primary' : 'text-neutral-500']">
info
@@ -43,10 +50,11 @@
</div>
<!-- Sidebar Model -->
<SidebarView v-model:visible="sidebarView" @switch-map-type="switchMapType" @switch-curve-styles="switchCurveStyles" @switch-rank="switchRank"
@switch-data-layer-type="switchDataLayerType" ></SidebarView>
<SidebarView v-model:visible="sidebarView" @switch-map-type="switchMapType" @switch-curve-styles="switchCurveStyles"
@switch-rank="switchRank" @switch-data-layer-type="switchDataLayerType"></SidebarView>
<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"
:filterEndToStart="filterEndToStart" :filterTimeframe="filterTimeframe" :filterTrace="filterTrace"
@submit-all="createCy(mapType)" @switch-Trace-Id="switchTraceId" ref="sidevarFilterRef"></SidebarFilter>
@@ -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,
filterTrace, logId, baseLogId, createFilterId, temporaryData, isRuleData,
ruleData, allMapDataStore, cases, postRuleData,
setCurrentGraphId,
};
},
props:['type', 'checkType', 'checkId', 'checkFileId'], // 來自 router 的 props
props: ['type', 'checkType', 'checkId', 'checkFileId'], // 來自 router 的 props
components: {
SidebarView,
SidebarState,
@@ -142,7 +151,7 @@ export default {
edges: [],
},
cytoscapeGraph: null,
curveStyle:'unbundled-bezier', // unbundled-bezier | taxi
curveStyle: 'unbundled-bezier', // unbundled-bezier | taxi
mapType: 'processMap', // processMap | bpmn
mapPathStore: MapPathStore(),
dataLayerType: 'freq', // freq | duration
@@ -154,6 +163,8 @@ export default {
sidebarTraces: false, // SideBar: Traces
sidebarFilter: false, // SideBar: Filter
infiniteFirstCases: null,
startNodeId: -1,
endNodeId: -1,
tooltip: {
sidebarView: {
value: 'Visualization Setting',
@@ -186,37 +197,37 @@ export default {
}
}
},
computed:{
sidebarLeftValue: function() {
computed: {
sidebarLeftValue: function () {
let result = this.sidebarView === true || this.sidebarTraces === true || this.sidebarFilter === true;
return result;
}
},
watch: {
sidebarView: function(newValue) {
if(newValue) {
sidebarView: function (newValue) {
if (newValue) {
this.sidebarFilter = false;
this.sidebarTraces = false;
}
},
sidebarFilter: function(newValue) {
if(newValue) {
sidebarFilter: function (newValue) {
if (newValue) {
this.sidebarView = false;
this.sidebarState = false;
this.sidebarTraces = false;
this.sidebarState = false;
}
},
sidebarTraces: function(newValue) {
if(newValue) {
sidebarTraces: function (newValue) {
if (newValue) {
this.sidebarView = false;
this.sidebarState = false;
this.sidebarFilter = false;
this.sidebarState = false;
}
},
sidebarState: function(newValue) {
if(newValue) {
sidebarState: function (newValue) {
if (newValue) {
this.sidebarFilter = false;
this.sidebarTraces = false;
}
@@ -252,7 +263,7 @@ export default {
* @param {string} type freq | duration
* @param {string} option 下拉選單中的選項
*/
async switchDataLayerType(type, option){
async switchDataLayerType(type, option) {
this.dataLayerType = type;
this.dataLayerOption = option;
this.createCy(this.mapType);
@@ -262,7 +273,7 @@ export default {
* @param {event} e input 傳入的事件
*/
async switchTraceId(e) {
if(e.id == this.traceId) return;
if (e.id == this.traceId) return;
// 超過 1000 筆要 loading 畫面
this.isLoading = true; // 都要 loading 畫面
this.traceId = e.id;
@@ -308,56 +319,62 @@ export default {
// add type of 'bpmn gateway' node
case 'gateway':
mapData.nodes.push({
data:{
id:node.id,
type:node.type,
label:gateway[node.gateway_type],
height:60,
width:60,
backgroundColor:'#FFF',
bordercolor:'#003366',
shape:"diamond",
freq:logFreq,
duration:logDuration,
data: {
id: node.id,
type: node.type,
label: gateway[node.gateway_type],
height: 60,
width: 60,
backgroundColor: '#FFF',
bordercolor: '#003366',
shape: "diamond",
freq: logFreq,
duration: logDuration,
}
})
break;
// add type of 'event' node
case 'event':
if(node.event_type === 'start') mapData.startId = node.id;
else if(node.event_type === 'end') mapData.endId = node.id;
if (node.event_type === 'start') {
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({
data:{
id:node.id,
type:node.type,
label:node.event_type,
data: {
id: node.id,
type: node.type,
label: node.event_type,
height: 48,
width: 48,
backgroundColor:'#FFFFFF',
bordercolor:'#0F172A',
backgroundColor: '#FFFFFF',
bordercolor: '#0F172A',
textColor: '#FF3366',
shape:"ellipse",
freq:logFreq,
duration:logDuration,
shape: "ellipse",
freq: logFreq,
duration: logDuration,
}
});
break;
// add type of 'activity' node
default:
mapData.nodes.push({
data:{
id:node.id,
type:node.type,
label:node.label,
data: {
id: node.id,
type: node.type,
label: node.label,
height: 48,
width: 216,
textColor: '#0F172A',
backgroundColor:'rgba(0, 0, 0, 0)',
backgroundColor: 'rgba(0, 0, 0, 0)',
borderradius: 999,
shape:"round-rectangle",
freq:node.freq,
duration:node.duration,
shape: "round-rectangle",
freq: node.freq,
duration: node.duration,
backgroundOpacity: 0,
borderOpacity: 0,
}
@@ -387,12 +404,13 @@ export default {
this[mapType].edges.map(edge => {
mapData.edges.push({
data: {
source:edge.tail,
target:edge.head,
freq:edge.freq,
duration:edge.duration === null ? logDuration : edge.duration,
style:'dotted',
lineWidth:1,
source: edge.tail,
target: edge.head,
freq: edge.freq,
duration: edge.duration === null ? logDuration : edge.duration,
// Don't know why but tail is related to start and head is related to end
edgeStyle: edge.tail === this.startNodeId || edge.head === this.endNodeId ? 'dotted' : 'solid',
lineWidth: 1,
},
});
});
@@ -403,9 +421,9 @@ export default {
*/
async createCy(type) {
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.setEdgesData(mapData);
this.setActivityBgImage(mapData);
@@ -429,12 +447,12 @@ export default {
activityNodeArray.map(node => nodeOptionArr.push(node.data[this.dataLayerType][this.dataLayerOption]));
// 將node的option值從小到大排序(映對色階淺到深)
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 endIdx = (i === ImgCapsules.length - 1) ? activityNodeArray.length : startIdx + groupSize;
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 => {
// 考慮可能有名次一樣的情形
const curNodes = activityNodeArray.filter(activityNode => activityNode.data[this.dataLayerType][this.dataLayerOption] === option);
@@ -459,7 +477,7 @@ export default {
// Log 檔前往 Map Log 頁, Filter 檔前往 Map Filter 頁
switch (routeParams.type) {
case 'log':
if(!isCheckPage) {
if (!isCheckPage) {
this.logId = await routeParams.fileId;
this.baseLogId = await routeParams.fileId;
} else {
@@ -468,7 +486,7 @@ export default {
}
break;
case 'filter':
if(!isCheckPage) {
if (!isCheckPage) {
this.createFilterId = await routeParams.fileId;
} else {
this.createFilterId = await file.parent.id;