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; }