agnular Cytoscape javascript

Cytoscape.js

周志衠 Jed Jhou 2020/10/26 16:59:21
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' ;

配置 CommonJS 相依性

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 繪製圖形可使用下列四種基本元素

  1. Node types 節點,主要圖形元件,可在圖表中代表一個獨立角色,一個資料圖形中可包含多個 node。
  2. Edge types 關聯,表示節點之間的關聯元件。
  3. Edge arrow types 關聯線的方向圖示。
  4. 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);
    });
  }
周志衠 Jed Jhou