agnular
Cytoscape
javascript
Cytoscape.js
2020/10/26 16:59:21
0
1630
Cytoscape.js 是一個處理資料分析與視覺化功能齊全的 javascript 類別庫,當我們要對資料關係進行可視化顯示時,例如社交網路關係或網路拓樸圖時,Cytoscape.js 是個不錯的選擇,以下使用 Angular 示範如何操作 Cytoscape.js 繪製基本關聯圖形。
如何開始
套件安裝
npm install cytoscape
npm install --save @types/cytoscape (TS型別擴充套件)
引用
app.component.ts
import
*
as cytoscape
from
'cytoscape'
;
angular.json
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"allowedCommonJsDependencies": [
"cytoscape"
]
...
}
...
},
準備容器
app.component.html
<div id="cy">
</div>
css
設定 canvas 畫布大小,如沒有設定會看不到內容,為了方便示範直接使用 app.component.scss 操作,建議另外準備 css 檔引用
#cy {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: 999;
width: 100%;
height: 500px;
}
基本功能
圖表組成
在 cytoscape.js 繪製圖形可使用下列四種基本元素
- Node types 節點,主要圖形元件,可在圖表中代表一個獨立角色,一個資料圖形中可包含多個 node。
- Edge types 關聯,表示節點之間的關聯元件。
- Edge arrow types 關聯線的方向圖示。
- labels Node 與 Edge 的顯示文字。
API
初始化
const cy = cytoscape({
container: document.getElementById('cy'),
// 樣式
style: [
{
selector: 'node',
css: {
// 圖形,可直接設定圖形類型,例如 rectangle,或動態從 data 物件下取得指定屬性,如下
shape: 'data(shape)' as NodeShape,
label: 'data(customerId)', // 文字標籤
'text-valign': 'top', // 文字標籤位置
'text-halign': 'center' // 文字標籤位置
}
},
{
selector: 'edge',
css: {
'curve-style': 'bezier', // edge 類型
'target-arrow-shape': 'triangle', // edge 箭頭圖形
width: 3, // 寬度
label: 'data(type)' // 文字標籤
}
}
],
// 可以先預設加入靜態 node 與 edge
elements: {
nodes: [
{ group: 'nodes', data: { id: '300', customerId: 'R30000', shape: 'rectangle' } },
{ group: 'nodes', data: { id: '301', customerId: 'R30001', shape: 'ellipse' } },
{ group: 'nodes', data: { id: '302', customerId: 'R30002', shape: 'rectangle' } }
],
edges: [
{
group: 'edges',
data: { id: '400', source: '300', target: '301' }, style: {
label: '文字'
}
}
]
}
});
設定 layout 選項
const options = {
name: 'breadthfirst',
fit: true, // whether to fit the viewport to the graph
directed: false, // whether the tree is directed downwards (or edges can point in any direction if false)
padding: 30, // padding on fit
circle: false, // put depths in concentric circles if true, put depths top down if false
grid: false, // whether to create an even grid into which the DAG is placed (circle:false only)
spacingFactor: 1.75, // positive spacing factor, larger => more space between nodes (N.B. n/a if causes overlap)
boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
avoidOverlap: true, // prevents node overlap, may overflow boundingBox if not enough space
nodeDimensionsIncludeLabels: false, // Excludes the label when calculating node bounding boxes for the layout algorithm
roots: undefined, // the roots of the trees
maximal: false, // whether to shift nodes down their natural BFS depths in order to avoid upwards edges (DAGS only)
animate: false, // whether to transition the node positions
animationDuration: 500, // duration of animation in ms if enabled
animationEasing: undefined, // easing of animation if enabled,
animateFilter: function (node, i) { return true; }, // a function that determines whether the node should be animated. All nodes animated by default on animate enabled. Non-animated nodes are positioned immediately when the layout starts
ready: undefined, // callback on layoutready
stop: undefined, // callback on layoutstop
transform: function (node, position) { return position; } // transform a given node position. Useful for changing flow direction in discrete layouts
};
cy.layout(options).start();
加入 node 與 edge
cy.add({ group: 'nodes', data: { id: 'g' } }); // 或 cy.add([ { group: 'nodes', data: { id: 'n0' }, position: { x: 100, y: 100 } }, { group: 'nodes', data: { id: 'n1' }, position: { x: 200, y: 200 } }, { group: 'edges', data: { id: 'e0', source: 'n0', target: 'n1' } } ]);
移除 node 與 edge
const collection = cy.elements('node[weight > 50]');
cy.remove( collection );
// 或
cy.remove('node');
完整範例程式碼
import { Component, OnInit } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import * as cytoscape from 'cytoscape';
import { ElementDefinition } from 'cytoscape';
type NodeShape = 'rectangle' | 'roundrectangle' | 'ellipse' | 'triangle'
| 'pentagon' | 'hexagon' | 'heptagon' | 'octagon' | 'star' | 'barrel'
| 'diamond' | 'vee' | 'rhomboid' | 'polygon' | 'tag' | 'round-rectangle'
| 'round-triangle' | 'round-diamond' | 'round-pentagon' | 'round-hexagon'
| 'round-heptagon' | 'round-octagon' | 'round-tag'
| 'cut-rectangle' | 'bottom-round-rectangle' | 'concave-hexagon';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
title = 'ngCytoscapeSample';
// 測試資料,假設 neo4j 回傳的資料,僅有節點與關聯內容,節點的位置由前端繪圖時另外設定
data = {
results: [
{
columns: [
'p'
],
data: [
{
row: [
[
{
CUSTOMER_ID: 'C10004',
PERSON_TYPE: '2'
},
{
KEY_TO_PROD_02: '35'
},
{
CUSTOMER_ID: 'C10001',
PERSON_TYPE: '2'
},
{},
{
CUSTOMER_ID: 'R10021',
PERSON_TYPE: '1'
},
{},
{
CUSTOMER_ID: 'R10042',
PERSON_TYPE: '1'
},
{},
{
CUSTOMER_ID: 'C10002',
PERSON_TYPE: '2'
},
{},
{
CUSTOMER_ID: 'R10044',
PERSON_TYPE: '1'
}
]
],
meta: [
[
{
id: 201,
type: 'node',
deleted: false
},
{
id: 55,
type: 'relationship',
deleted: false
},
{
id: 194,
type: 'node',
deleted: false
},
{
id: 20,
type: 'relationship',
deleted: false
},
{
id: 218,
type: 'node',
deleted: false
},
{
id: 102,
type: 'relationship',
deleted: false
},
{
id: 239,
type: 'node',
deleted: false
},
{
id: 116,
type: 'relationship',
deleted: false
},
{
id: 242,
type: 'node',
deleted: false
},
{
id: 118,
type: 'relationship',
deleted: false
},
{
id: 241,
type: 'node',
deleted: false
}
]
],
graph: {
nodes: [
{
id: '241',
labels: [
'Customer'
],
properties: {
CUSTOMER_ID: 'R10044',
PERSON_TYPE: '1'
}
},
{
id: '194',
labels: [
'Customer'
],
properties: {
CUSTOMER_ID: 'C10001',
PERSON_TYPE: '2'
}
},
{
id: '242',
labels: [
'Customer'
],
properties: {
CUSTOMER_ID: 'C10002',
PERSON_TYPE: '2'
}
},
{
id: '201',
labels: [
'Customer'
],
properties: {
CUSTOMER_ID: 'C10004',
PERSON_TYPE: '2'
}
},
{
id: '218',
labels: [
'Customer'
],
properties: {
CUSTOMER_ID: 'R10021',
PERSON_TYPE: '1'
}
},
{
id: '239',
labels: [
'Customer'
],
properties: {
CUSTOMER_ID: 'R10042',
PERSON_TYPE: '1'
}
}
],
relationships: [
{
id: '20',
type: '雇傭',
startNode: '194',
endNode: '218',
properties: {}
},
{
id: '116',
type: '雇傭',
startNode: '242',
endNode: '239',
properties: {}
},
{
id: '102',
type: '家人',
startNode: '239',
endNode: '218',
properties: {}
},
{
id: '118',
type: '雇傭',
startNode: '242',
endNode: '241',
properties: {}
},
{
id: '55',
type: '合併',
startNode: '201',
endNode: '194',
properties: {
KEY_TO_PROD_02: '35'
}
}
]
}
}
]
}
],
errors: []
};
constructor(
private http: HttpClient
) { }
ngOnInit(): void {
const cy = cytoscape({
container: document.getElementById('cy'),
style: [
{
selector: 'node',
css: {
// 圖形,可直接設定圖形類型,例如 rectangle,或動態從 data 物件下取得指定屬性,如下
shape: 'data(shape)' as NodeShape,
label: 'data(customerId)', // 文字標籤
'text-valign': 'top', // 文字標籤位置
'text-halign': 'center' // 文字標籤位置
}
},
{
selector: 'edge',
css: {
'curve-style': 'bezier', // edge 類型
'target-arrow-shape': 'triangle', // edge 箭頭圖形
width: 3, // 寬度
label: 'data(type)' // 文字標籤,
}
}
],
// 可以先預設加入 node 與 edge
elements: {
nodes: [
{ group: 'nodes', data: { id: '300', customerId: 'R30000', shape: 'rectangle' }, },
{ group: 'nodes', data: { id: '301', customerId: 'R30001', shape: 'ellipse' } },
{ group: 'nodes', data: { id: '302', customerId: 'R30002', shape: 'rectangle' } }
],
edges: [
{
group: 'edges',
data: { id: '400', source: '300', target: '301' }, style: {
label: '測試內容'
}
}
]
}
});
const data = this.data.results.find(x => x).data.find(x => x).graph;
const cyNodes = data.nodes.map(element => ({
group: 'nodes',
data: {
id: element.id,
customerId: element.properties.CUSTOMER_ID,
shape: element.properties.PERSON_TYPE === '1' ? 'rectangle' : 'ellipse'
}
})) as ElementDefinition[];
const cyEdges = data.relationships.map(element => ({
group: 'edges',
data: {
id: element.id,
source: element.startNode,
target: element.endNode,
type: element.type
}
})) as ElementDefinition[];
cy.add(cyNodes);
cy.add(cyEdges);
const options = {
name: 'breadthfirst',
fit: true, // whether to fit the viewport to the graph
directed: false, // whether the tree is directed downwards (or edges can point in any direction if false)
padding: 30, // padding on fit
circle: false, // put depths in concentric circles if true, put depths top down if false
grid: false, // whether to create an even grid into which the DAG is placed (circle:false only)
spacingFactor: 1.75, // positive spacing factor, larger => more space between nodes (N.B. n/a if causes overlap)
boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
avoidOverlap: true, // prevents node overlap, may overflow boundingBox if not enough space
nodeDimensionsIncludeLabels: false, // Excludes the label when calculating node bounding boxes for the layout algorithm
roots: undefined, // the roots of the trees
maximal: false, // whether to shift nodes down their natural BFS depths in order to avoid upwards edges (DAGS only)
animate: false, // whether to transition the node positions
animationDuration: 500, // duration of animation in ms if enabled
animationEasing: undefined, // easing of animation if enabled,
animateFilter: function (node, i) { return true; }, // a function that determines whether the node should be animated. All nodes animated by default on animate enabled. Non-animated nodes are positioned immediately when the layout starts
ready: undefined, // callback on layoutready
stop: undefined, // callback on layoutstop
transform: function (node, position) { return position; } // transform a given node position. Useful for changing flow direction in discrete layouts
};
cy.layout(options).start();
}
}
與 Neo4j 連線範例
search(): void { // cypher 語法 const query = 'match p=(m)-[r*5]-(n) return p limit 3'; const body = { statements: [{ statement: query, resultDataContents: ['row', 'graph'] }] }; this.http.post(`http://neo4j位置/db/neo4j/tx/commit`, body, { headers: new HttpHeaders().set('Authorization', 'Basic ' + btoa('帳號' + ':' + '密碼')) }).subscribe(graph => { console.log(graph); }); }