248 lines
9.2 KiB
JavaScript
248 lines
9.2 KiB
JavaScript
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;
|
||
}
|