Files
lucia-frontend/src/module/cytoscapeMap.js
2024-06-28 16:20:41 +08:00

248 lines
9.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import cytoscape from 'cytoscape';
import dagre from 'cytoscape-dagre';
import tippy from 'tippy.js';
import 'tippy.js/dist/tippy.css';
import Gradient from 'javascript-color-gradient'; // 多個色階產生器
import { getTimeLabel } from '@/module/timeLabel.js'; // 時間格式轉換器
import CytoscapeStore from '@/stores/cytoscapeStore';
cytoscape.use( dagre );
/**
* @param {object} mapData processMapData | bpmnData可選以上任一。
* mapData:{
* startId: 0,
* endId: 1,
* nodes: [],
* edges: []
* }
* @param {string} dataLayerType DataLayer's type
* @param {string} dataLayerOption DataLayer's options
* @param {string} curve Curve's type
* @param {string} graphId cytoscape's container
* @return {cytoscape.Core} cy
*/
export default function cytoscapeMap(mapData, dataLayerType, dataLayerOption, curveStyle, rank, graphId) {
// create color Gradient
// 設定每個 node, edges 的顏色與樣式
let gradientArray = [];
let activityArray = [];
let edgeArray = [];
let nodeOption = [];
let edgeOption = [];
let nodes = mapData.nodes;
let edges = mapData.edges;
// 設定除了 start, end 的 node 顏色
// 找出 type activity's node
activityArray = nodes.filter(i => i.data.type === 'activity');
// 找出除了 start, end 以外所有的 node 的 option value
activityArray.map(node => nodeOption.push(node.data[dataLayerType][dataLayerOption]));
// 刪掉重複的元素,小到大排序(映對色階淺到深)
nodeOption = [...new Set(nodeOption)].sort((a, b) => a - b);
// 產生 node 色階
gradientArray = new Gradient()
.setColorGradient("#CCE5FF", "#66b2ff")
.setMidpoint(nodeOption.length)
.getColors();
// 設定每個 node 的背景色
activityArray.forEach(node => {
// 透過 index 找對應位置
let gradientIndex = nodeOption.indexOf(node.data[dataLayerType][dataLayerOption]);
node.data.backgroundColor = gradientArray[gradientIndex];
})
// 設定除了 start, end 的 edges 粗細
// 找出除了 start, end 以外所有的 edge
edgeArray = edges.filter(i => i.data.source !== mapData.startId && i.data.target !== mapData.endId);
// 找出所有 edge 的 option value
edgeArray.map(edge => edgeOption.push(edge.data[dataLayerType][dataLayerOption]));
// 刪掉重複的元素,小到大排序(映對色階淺到深)
edgeOption = [...new Set(edgeOption)].sort((a, b) => a - b);
// 設定每個 edge 的粗細
edgeArray.forEach(edge => {
let edgeIndex = edgeOption.indexOf(edge.data[dataLayerType][dataLayerOption])
edge.data.lineWidth = (parseInt(edgeIndex) + 1) * 0.15
edge.data.style = 'solid'
})
// create Cytoscape
let cy = cytoscape({
container: graphId,
elements: {
nodes: nodes, //nodes, // 節點的資料
edges: edges, //edges, // 關係線的資料
},
layout: {
name: 'dagre',
rankDir: rank, // 直向 TB | 橫向 LR, 'cytoscape-dagre' 套件裡的變數
},
style: [
// 點擊 node 後改變的樣式
{
selector:'node:selected',
style:{
'border-color':'red',
'border-width':'3',
},
},
// node 節點的樣式
{
selector: 'node',
style: {
'label':
function(node) { // 節點要顯示的文字
// node.data(this.dataLayerType+"."+this.dataLayerOption) 為原先陣列 node.data.key.value
let optionValue = node.data(`${dataLayerType}.${dataLayerOption}`);
let text = '';
// 文字超過 10 字尾巴要加「...」style 要換兩行(\n 換行符號)
// 使用 data() 是因為在 cytoscape 中從陣列轉為 function
text = node.data('label').length > 10 ? `${node.data('label').substr(0, 10)}...\n\n` : `${node.data('label')}\n\n`;
// 在 element 中 activity 歸類在 default所以要先判斷 node 是否為 activeity 才裝入文字。
// 可使用 parseInt(整數) parseFloat(浮點數) 將字串轉為數字
// Relative 要轉為百分比 %
if(node.data('type') === 'activity') {
switch(dataLayerType) {
case 'freq': // Frequency
let textInt = dataLayerOption === 'rel_freq' ? text + optionValue * 100 + "%" : text + optionValue;
let textFloat = dataLayerOption === 'rel_freq'? text + (optionValue * 100).toFixed(2) + "%" : text + optionValue.toFixed(2);
// 判斷是否為整數,若非整數要取小數點後面兩個值。
text = Math.trunc(optionValue) === optionValue ? textInt : textFloat;
break;
case 'duration': // Duration 除了 Relative 為百分比 % ,其他要轉變時間單位。
// Relative %
let textDurRel = text + (optionValue * 100).toFixed(2) + "%";
// Timelabel
let timeLabelInt = text + getTimeLabel(optionValue);
let timeLabelFloat = text + getTimeLabel(optionValue.toFixed(2));
// 判斷是否為整數,若非整數要取小數點後面兩個值。
let 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';
},
//'border-radius': '5',
'shape':'data(shape)',
'text-wrap': 'wrap',
'text-max-width': 'data(width)', // 在 div 內換行
'text-overflow-wrap': 'anywhere', // 在 div 內換行
'text-halign': 'center',
'text-valign': 'center',
'height': 'data(height)',
'width': 'data(width)',
'color': '#001933',
'font-size':
function(node) {
return node.data('type') === 'activity' ? 14 : 22;
},
},
},
// edge 關係線的樣式
{
selector: 'edge',
style: {
'content': function(edge) { // 關係線顯示的文字
let optionValue = edge.data(`${dataLayerType}.${dataLayerOption}`);
let result = '';
if(optionValue === '') return optionValue;
switch(dataLayerType) {
case 'freq':
let edgeInt = dataLayerOption === 'rel_freq' ? optionValue * 100 + "%" : optionValue;
let edgeFloat = dataLayerOption === 'rel_freq' ? (optionValue * 100).toFixed(2) + "%" : optionValue.toFixed(2);
// 判斷是否為整數,若非整數要取小數點後面兩個值。
result = Math.trunc(optionValue) === optionValue ? edgeInt : edgeFloat;
break;
case 'duration': // Duration 除了 Relative 為百分比 % ,其他要轉變時間單位。
// Relative %
let edgeDurRel = (optionValue * 100).toFixed(2) + "%";
// Timelabel
let timeLabelInt = getTimeLabel(optionValue);
let timeLabelFloat = getTimeLabel(optionValue.toFixed(2));
let edgeTimeLabel = Math.trunc(optionValue) === optionValue ? timeLabelInt : timeLabelFloat;
result = dataLayerOption === 'rel_duration' ? edgeDurRel : edgeTimeLabel;
break;
};
return result;
},
'curve-style': curveStyle, // unbundled-bezier | taxi
'target-arrow-shape': 'triangle', // 指向目標的箭頭形狀: 三角形
'color': 'gray', //#0066cc
//'control-point-step-size':100, // 從點到點的垂直線,指定貝茲取線邊緣間的距離
'width':'data(lineWidth)',
'line-style':'data(style)',
"text-margin-y": "15rem",
//"text-rotation": "autorotate",
}
},
],
});
// creat tippy.js
let tip;
cy.on('mouseover', 'node', function(event) {
var node = event.target
let ref = node.popperRef()
let dummyDomEle = document.createElement('div');
let content = document.createElement('div');
content.innerHTML = node.data("label")
tip = new tippy(dummyDomEle, { // tippy props:
getReferenceClientRect: ref.getBoundingClientRect,
trigger: 'manual',
content:content
});
if(node.data("label").length > 10) tip.show();
});
cy.on('mouseout', 'node', function(event) {
tip.hide();
});
const cytoscapeStore = CytoscapeStore();
cy.ready(() => {
cytoscapeStore.nodePositions.forEach(pos => {
const node = cy.getElementById(pos.id);
if (node) {
node.position(pos.position);
}
});
// 在改變節點位置後,盡可能地記錄節點線條的位置情報
cy.on('dragfree', 'node', (event) => {
const node = event.target;
const position = node.position();
cytoscapeStore.saveNodePosition(node.id(), position);
cytoscapeStore.savePositionsToStorage();
});
});
return cy;
}