diff --git a/package-lock.json b/package-lock.json index e8aaa8b..7182ff2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,9 @@ "autoprefixer": "^10.4.13", "axios": "^1.2.2", "cytoscape": "^3.23.0", + "cytoscape-dagre": "^2.5.0", "cytoscape-klay": "^3.1.4", + "cytoscape-popper": "^2.0.0", "javascript-color-gradient": "^2.4.4", "mitt": "^3.0.0", "moment": "^2.29.4", @@ -629,6 +631,15 @@ "node": ">= 8" } }, + "node_modules/@popperjs/core": { + "version": "2.11.6", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz", + "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@rushstack/eslint-patch": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz", @@ -2057,6 +2068,17 @@ "node": ">=0.10" } }, + "node_modules/cytoscape-dagre": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cytoscape-dagre/-/cytoscape-dagre-2.5.0.tgz", + "integrity": "sha512-VG2Knemmshop4kh5fpLO27rYcyUaaDkRw+6PiX4bstpB+QFt0p2oauMrsjVbUamGWQ6YNavh7x2em2uZlzV44g==", + "dependencies": { + "dagre": "^0.8.5" + }, + "peerDependencies": { + "cytoscape": "^3.2.22" + } + }, "node_modules/cytoscape-klay": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/cytoscape-klay/-/cytoscape-klay-3.1.4.tgz", @@ -2068,6 +2090,26 @@ "cytoscape": "^3.2.0" } }, + "node_modules/cytoscape-popper": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cytoscape-popper/-/cytoscape-popper-2.0.0.tgz", + "integrity": "sha512-b7WSOn8qXHWtdIXFNmrgc8qkaOs16tMY0EwtRXlxzvn8X+al6TAFrUwZoYATkYSlotfd/36ZMoeKMEoUck6feA==", + "dependencies": { + "@popperjs/core": "^2.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/dagre": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz", + "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", + "dependencies": { + "graphlib": "^2.1.8", + "lodash": "^4.17.15" + } + }, "node_modules/dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -3243,6 +3285,14 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "dev": true }, + "node_modules/graphlib": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", + "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", + "dependencies": { + "lodash": "^4.17.15" + } + }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -6606,6 +6656,11 @@ "fastq": "^1.6.0" } }, + "@popperjs/core": { + "version": "2.11.6", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz", + "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==" + }, "@rushstack/eslint-patch": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz", @@ -7743,6 +7798,14 @@ "lodash": "^4.17.21" } }, + "cytoscape-dagre": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cytoscape-dagre/-/cytoscape-dagre-2.5.0.tgz", + "integrity": "sha512-VG2Knemmshop4kh5fpLO27rYcyUaaDkRw+6PiX4bstpB+QFt0p2oauMrsjVbUamGWQ6YNavh7x2em2uZlzV44g==", + "requires": { + "dagre": "^0.8.5" + } + }, "cytoscape-klay": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/cytoscape-klay/-/cytoscape-klay-3.1.4.tgz", @@ -7751,6 +7814,23 @@ "klayjs": "^0.4.1" } }, + "cytoscape-popper": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cytoscape-popper/-/cytoscape-popper-2.0.0.tgz", + "integrity": "sha512-b7WSOn8qXHWtdIXFNmrgc8qkaOs16tMY0EwtRXlxzvn8X+al6TAFrUwZoYATkYSlotfd/36ZMoeKMEoUck6feA==", + "requires": { + "@popperjs/core": "^2.0.0" + } + }, + "dagre": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz", + "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", + "requires": { + "graphlib": "^2.1.8", + "lodash": "^4.17.15" + } + }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -8632,6 +8712,14 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "dev": true }, + "graphlib": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", + "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", + "requires": { + "lodash": "^4.17.15" + } + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", diff --git a/package.json b/package.json index b2ea4a5..bfeef85 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "autoprefixer": "^10.4.13", "axios": "^1.2.2", "cytoscape": "^3.23.0", - "cytoscape-klay": "^3.1.4", + "cytoscape-dagre": "^2.5.0", "javascript-color-gradient": "^2.4.4", "mitt": "^3.0.0", "moment": "^2.29.4", diff --git a/src/main.js b/src/main.js index 19b1083..126abed 100644 --- a/src/main.js +++ b/src/main.js @@ -9,7 +9,7 @@ import moment from 'moment'; import mitt from 'mitt'; import ToastPlugin from 'vue-toast-notification'; import cytoscape from 'cytoscape'; -import klay from 'cytoscape-klay'; +import dagre from 'cytoscape-dagre'; import "./assets/main.css"; import 'vue-toast-notification/dist/theme-sugar.css'; @@ -29,7 +29,7 @@ app.config.globalProperties.$moment = moment; app.config.globalProperties.emitter = emitter; app.config.globalProperties.$cytoscape = cytoscape; -cytoscape.use( klay ); +cytoscape.use( dagre ); app.use(pinia); app.use(router); diff --git a/src/module/cytoscapeMap.js b/src/module/cytoscapeMap.js new file mode 100644 index 0000000..9b43b8e --- /dev/null +++ b/src/module/cytoscapeMap.js @@ -0,0 +1,197 @@ +import cytoscape from 'cytoscape'; +import dagre from 'cytoscape-dagre'; +import Gradient from 'javascript-color-gradient'; // 多個色階產生器 +import TimeLabel from '@/module/timeLabel.js'; // 時間格式轉換器 + +cytoscape.use( dagre ); + +export default function cytoscapeMap(mapData, mapType, dataLayerType, dataLayerOption, curveStyle, rank, graphId) { + // console.log(mapData); + // console.log(mapType); + // console.log(dataLayerType); + // console.log(dataLayerOption); + // console.log(curveStyle); + // console.log(rank); + // 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 + TimeLabel(optionValue); + let timeLabelFloat = text + TimeLabel(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':75, + '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 = TimeLabel(optionValue); + let timeLabelFloat = TimeLabel(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", + } + }, + ], + }); + +} diff --git a/src/stores/allMapData.js b/src/stores/allMapData.js index b85c270..ebcf30c 100644 --- a/src/stores/allMapData.js +++ b/src/stores/allMapData.js @@ -18,11 +18,9 @@ export default defineStore('allMapDataStore', { try { const response = await this.$axios.get(api); - // console.log(response); this.processMap = response.data.process_map; this.bpmn = response.data.bpmn; } catch(error) { - // console.dir(error); }; }, } diff --git a/src/views/Discover/index.vue b/src/views/Discover/index.vue index 6253603..8d68f78 100644 --- a/src/views/Discover/index.vue +++ b/src/views/Discover/index.vue @@ -3,43 +3,44 @@