feat: unified datachange from changeData, addData, updateData, and removeData; feat: item definition and first drawing (#4259)

* feat: unified datachange from changeData, addData, updateData, and removeData; feat: item definition and first drawing

* feat: update canvas while addData; feat: update canvas while updateData (node); chore: unified additems, removeitems, updateitems to be itemchange hook

* feat: node and edge updating and drawing

* chore: neaten

* feat: draw and update labels for node and edge

* feat: icon for edge; feat: custom node and edge and register to lib

* feat: state related API for graph and item

* chore: update notes

* feat: state styles for item

* chore: update tests
This commit is contained in:
Yanyan Wang 2023-03-01 14:21:55 +08:00 committed by GitHub
parent bb82593eb8
commit 875a9eb39b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 5153 additions and 1427 deletions

View File

@ -0,0 +1 @@
TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false.

View File

@ -1,10 +1,4 @@
---
id: "index"
title: "@antv/g6"
sidebar_label: "README"
---
[@antv/g6](index.md) [Globals](globals.md)
@antv/g6 / [Exports](modules.md)
# G6图分析引擎

View File

@ -1,834 +0,0 @@
---
id: "_graph_.graph"
title: "Graph"
sidebar_label: "Graph"
---
[@antv/g6](../index.md) [Globals](../globals.md) ["graph"](../modules/_graph_.md) [Graph](_graph_.graph.md)
## Type parameters
**B**: *BehaviorRegistry*
## Hierarchy
* EventEmitter
↳ **Graph**
## Implements
* IGraphB
## Index
### Constructors
* [constructor](_graph_.graph.md#constructor)
### Properties
* [hooks](_graph_.graph.md#hooks)
### Methods
* [addBehaviors](_graph_.graph.md#addbehaviors)
* [addItem](_graph_.graph.md#additem)
* [clear](_graph_.graph.md#clear)
* [collapseCombo](_graph_.graph.md#collapsecombo)
* [createCombo](_graph_.graph.md#createcombo)
* [emit](_graph_.graph.md#emit)
* [expandCombo](_graph_.graph.md#expandcombo)
* [findIdByState](_graph_.graph.md#findidbystate)
* [fitCenter](_graph_.graph.md#fitcenter)
* [fitView](_graph_.graph.md#fitview)
* [focusItem](_graph_.graph.md#focusitem)
* [getComboData](_graph_.graph.md#getcombodata)
* [getEdgeData](_graph_.graph.md#getedgedata)
* [getEvents](_graph_.graph.md#getevents)
* [getNodeData](_graph_.graph.md#getnodedata)
* [getSpecification](_graph_.graph.md#getspecification)
* [hideItem](_graph_.graph.md#hideitem)
* [layout](_graph_.graph.md#layout)
* [move](_graph_.graph.md#move)
* [moveTo](_graph_.graph.md#moveto)
* [off](_graph_.graph.md#off)
* [on](_graph_.graph.md#on)
* [once](_graph_.graph.md#once)
* [read](_graph_.graph.md#read)
* [removeBehaviors](_graph_.graph.md#removebehaviors)
* [removeItem](_graph_.graph.md#removeitem)
* [setItemState](_graph_.graph.md#setitemstate)
* [setMode](_graph_.graph.md#setmode)
* [showItem](_graph_.graph.md#showitem)
* [uncombo](_graph_.graph.md#uncombo)
* [updateBehavior](_graph_.graph.md#updatebehavior)
* [updateItem](_graph_.graph.md#updateitem)
* [updateSpecification](_graph_.graph.md#updatespecification)
* [zoom](_graph_.graph.md#zoom)
* [zoomTo](_graph_.graph.md#zoomto)
## Constructors
### constructor
\+ **new Graph**(`spec`: SpecificationB): *[Graph](_graph_.graph.md)*
*Defined in [src/runtime/graph.ts:26](https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/graph.ts#L26)*
**Parameters:**
Name | Type |
------ | ------ |
`spec` | SpecificationB |
**Returns:** *[Graph](_graph_.graph.md)*
## Properties
### hooks
**hooks**: *Hooks*
*Defined in [src/runtime/graph.ts:19](https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/graph.ts#L19)*
## Methods
### addBehaviors
**addBehaviors**(`behaviors`: BehaviorOptionsOfB[], `modes`: string | string[]): *void*
*Defined in [src/runtime/graph.ts:374](https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/graph.ts#L374)*
Add behavior(s) to mode(s).
**`group`** Interaction
**Parameters:**
Name | Type | Description |
------ | ------ | ------ |
`behaviors` | BehaviorOptionsOfB[] | behavior names or configs |
`modes` | string | string[] | mode names |
**Returns:** *void*
___
### addItem
**addItem**(`itemType`: ITEM_TYPE, `models`: NodeUserModel | EdgeUserModel | ComboUserModel | NodeUserModel[] | EdgeUserModel[] | ComboUserModel[], `stack?`: boolean): *boolean*
*Defined in [src/runtime/graph.ts:235](https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/graph.ts#L235)*
Add an item or items to the graph.
**`group`** Item
**Parameters:**
Name | Type | Description |
------ | ------ | ------ |
`itemType` | ITEM_TYPE | item type |
`models` | NodeUserModel | EdgeUserModel | ComboUserModel | NodeUserModel[] | EdgeUserModel[] | ComboUserModel[] | - |
`stack?` | boolean | whether push this operation to stack |
**Returns:** *boolean*
whether success
___
### clear
**clear**(): *void*
*Defined in [src/runtime/graph.ts:102](https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/graph.ts#L102)*
Clear the graph, means remove all the items on the graph.
**Returns:** *void*
___
### collapseCombo
**collapseCombo**(`comboId`: string | number, `stack?`: boolean): *void*
*Defined in [src/runtime/graph.ts:329](https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/graph.ts#L329)*
Collapse a combo.
**`group`** Combo
**Parameters:**
Name | Type | Description |
------ | ------ | ------ |
`comboId` | string | number | combo id or item |
`stack?` | boolean | - |
**Returns:** *void*
___
### createCombo
**createCombo**(`combo`: string | ComboUserModel, `childrenIds`: string[], `stack?`: boolean): *void*
*Defined in [src/runtime/graph.ts:313](https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/graph.ts#L313)*
Create a new combo with existing child nodes and combos.
**`group`** Combo
**Parameters:**
Name | Type | Description |
------ | ------ | ------ |
`combo` | string | ComboUserModel | combo ID or Combo model |
`childrenIds` | string[] | id array of children of the new combo |
`stack?` | boolean | - |
**Returns:** *void*
___
### emit
**emit**(`evt`: string, ...`args`: any[]): *void*
*Inherited from [Graph](_graph_.graph.md).[emit](_graph_.graph.md#emit)*
Defined in node_modules/_@antv_event-emitter@0.1.3@@antv/event-emitter/lib/index.d.ts:25
触发一个事件
**Parameters:**
Name | Type | Description |
------ | ------ | ------ |
`evt` | string | - |
`...args` | any[] | |
**Returns:** *void*
___
### expandCombo
**expandCombo**(`comboId`: string | number, `stack?`: boolean): *void*
*Defined in [src/runtime/graph.ts:337](https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/graph.ts#L337)*
Expand a combo.
**`group`** Combo
**Parameters:**
Name | Type |
------ | ------ |
`comboId` | string | number |
`stack?` | boolean |
**Returns:** *void*
___
### findIdByState
**findIdByState**(`itemType`: ITEM_TYPE, `state`: string, `additionalFilter?`: function): *(string | number)[]*
*Defined in [src/runtime/graph.ts:223](https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/graph.ts#L223)*
Find items which has the state.
**`group`** Item
**Parameters:**
**itemType**: *ITEM_TYPE*
item type
**state**: *string*
state name
▪`Optional` **additionalFilter**: *function*
additional filter function
▸ (`item`: NodeModel | EdgeModel | ComboModel): *boolean*
**Parameters:**
Name | Type |
------ | ------ |
`item` | NodeModel | EdgeModel | ComboModel |
**Returns:** *(string | number)[]*
items that is the type and has the state
___
### fitCenter
**fitCenter**(`animateCfg?`: AnimateCfg): *void*
*Defined in [src/runtime/graph.ts:172](https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/graph.ts#L172)*
Fit the graph center to the view center.
**`group`** View
**Parameters:**
Name | Type | Description |
------ | ------ | ------ |
`animateCfg?` | AnimateCfg | animation configurations |
**Returns:** *void*
___
### fitView
**fitView**(`padding?`: Padding, `rules?`: FitViewRules, `animateCfg?`: AnimateCfg): *void*
*Defined in [src/runtime/graph.ts:163](https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/graph.ts#L163)*
Fit the graph content to the view.
**`group`** View
**Parameters:**
Name | Type | Description |
------ | ------ | ------ |
`padding?` | Padding | padding while fitting |
`rules?` | FitViewRules | rules for fitting |
`animateCfg?` | AnimateCfg | animation configurations |
**Returns:** *void*
___
### focusItem
**focusItem**(`ids`: string | number | (string | number)[], `animateCfg?`: AnimateCfg): *void*
*Defined in [src/runtime/graph.ts:182](https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/graph.ts#L182)*
Move the graph to make the item align the view center.
**`group`** View
**Parameters:**
Name | Type | Description |
------ | ------ | ------ |
`ids` | string | number | (string | number)[] | - |
`animateCfg?` | AnimateCfg | animation configurations |
**Returns:** *void*
___
### getComboData
**getComboData**(`condition`: string | Function): *ComboModel | undefined*
*Defined in [src/runtime/graph.ts:212](https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/graph.ts#L212)*
Find an combo's inner data according to id or function.
**`group`** Item
**Parameters:**
Name | Type | Description |
------ | ------ | ------ |
`condition` | string | Function | id or condition function |
**Returns:** *ComboModel | undefined*
result combo
___
### getEdgeData
**getEdgeData**(`condition`: string | Function): *EdgeModel | undefined*
*Defined in [src/runtime/graph.ts:203](https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/graph.ts#L203)*
Find an edge's inner data according to id or function.
**`group`** Item
**Parameters:**
Name | Type | Description |
------ | ------ | ------ |
`condition` | string | Function | id or condition function |
**Returns:** *EdgeModel | undefined*
result edge
___
### getEvents
**getEvents**(): *Recordstring, EventType[]*
*Inherited from [Graph](_graph_.graph.md).[getEvents](_graph_.graph.md#getevents)*
Defined in node_modules/_@antv_event-emitter@0.1.3@@antv/event-emitter/lib/index.d.ts:32
**Returns:** *Recordstring, EventType[]*
___
### getNodeData
**getNodeData**(`condition`: string | Function): *NodeModel | undefined*
*Defined in [src/runtime/graph.ts:194](https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/graph.ts#L194)*
Find a node's inner data according to id or function.
**`group`** Item
**Parameters:**
Name | Type | Description |
------ | ------ | ------ |
`condition` | string | Function | id or condition function |
**Returns:** *NodeModel | undefined*
result node
___
### getSpecification
**getSpecification**(): *SpecificationB*
*Defined in [src/runtime/graph.ts:80](https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/graph.ts#L80)*
Get the copy of specs(configurations).
**Returns:** *SpecificationB*
graph specs
___
### hideItem
**hideItem**(`ids`: string | number | (string | number)[]): *void*
*Defined in [src/runtime/graph.ts:290](https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/graph.ts#L290)*
Hide the item(s).
**`group`** Item
**Parameters:**
Name | Type |
------ | ------ |
`ids` | string | number | (string | number)[] |
**Returns:** *void*
___
### layout
**layout**(`cfg?`: LayoutCommonConfig, `align?`: GraphAlignment, `canvasPoint?`: Point, `stack?`: boolean): *void*
*Defined in [src/runtime/graph.ts:351](https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/graph.ts#L351)*
Layout the graph (with current configurations if cfg is not assigned).
**`group`** Layout
**Parameters:**
Name | Type | Description |
------ | ------ | ------ |
`cfg?` | LayoutCommonConfig | layout configurations. if assigned, the layout spec of the graph will be updated in the same time |
`align?` | GraphAlignment | align the result |
`canvasPoint?` | Point | align the result |
`stack?` | boolean | push it into stack |
**Returns:** *void*
___
### move
**move**(`dx`: number, `dy`: number, `animateCfg?`: AnimateCfg): *void*
*Defined in [src/runtime/graph.ts:114](https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/graph.ts#L114)*
Move the graph with a relative vector.
**`group`** View
**Parameters:**
Name | Type | Description |
------ | ------ | ------ |
`dx` | number | x of the relative vector |
`dy` | number | y of the relative vector |
`animateCfg?` | AnimateCfg | animation configurations |
**Returns:** *void*
___
### moveTo
**moveTo**(`x`: number, `y`: number, `alignment`: GraphAlignment, `animateCfg?`: AnimateCfg): *void*
*Defined in [src/runtime/graph.ts:127](https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/graph.ts#L127)*
Move the graph and align to a point.
**`group`** View
**Parameters:**
Name | Type | Description |
------ | ------ | ------ |
`x` | number | position on the canvas to align |
`y` | number | position on the canvas to align |
`alignment` | GraphAlignment | alignment of the graph content |
`animateCfg?` | AnimateCfg | animation configurations |
**Returns:** *void*
___
### off
**off**(`evt?`: string, `callback?`: Function): *this*
*Inherited from [Graph](_graph_.graph.md).[off](_graph_.graph.md#off)*
Defined in node_modules/_@antv_event-emitter@0.1.3@@antv/event-emitter/lib/index.d.ts:31
取消监听一个事件或者一个channel
**Parameters:**
Name | Type | Description |
------ | ------ | ------ |
`evt?` | string | - |
`callback?` | Function | |
**Returns:** *this*
___
### on
**on**(`evt`: string, `callback`: Function, `once?`: boolean): *this*
*Inherited from [Graph](_graph_.graph.md).[on](_graph_.graph.md#on)*
Defined in node_modules/_@antv_event-emitter@0.1.3@@antv/event-emitter/lib/index.d.ts:13
监听一个事件
**Parameters:**
Name | Type | Description |
------ | ------ | ------ |
`evt` | string | - |
`callback` | Function | - |
`once?` | boolean | |
**Returns:** *this*
___
### once
**once**(`evt`: string, `callback`: Function): *this*
*Inherited from [Graph](_graph_.graph.md).[once](_graph_.graph.md#once)*
Defined in node_modules/_@antv_event-emitter@0.1.3@@antv/event-emitter/lib/index.d.ts:19
监听一个事件一次
**Parameters:**
Name | Type | Description |
------ | ------ | ------ |
`evt` | string | - |
`callback` | Function | |
**Returns:** *this*
___
### read
**read**(`data`: GraphData): *void*
*Defined in [src/runtime/graph.ts:91](https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/graph.ts#L91)*
Input data and render the graph.
If there is old data, diffs and changes it.
**`group`** Data
**Parameters:**
Name | Type |
------ | ------ |
`data` | GraphData |
**Returns:** *void*
___
### removeBehaviors
**removeBehaviors**(`behaviorKeys`: string[], `modes`: string | string[]): *void*
*Defined in [src/runtime/graph.ts:394](https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/graph.ts#L394)*
Remove behavior(s) from mode(s).
**`group`** Interaction
**Parameters:**
Name | Type | Description |
------ | ------ | ------ |
`behaviorKeys` | string[] | - |
`modes` | string | string[] | mode names |
**Returns:** *void*
___
### removeItem
**removeItem**(`itemType`: ITEM_TYPE, `ids`: string | number | (string | number)[], `stack?`: boolean): *boolean*
*Defined in [src/runtime/graph.ts:251](https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/graph.ts#L251)*
Remove an item or items from the graph.
**`group`** Item
**Parameters:**
Name | Type | Description |
------ | ------ | ------ |
`itemType` | ITEM_TYPE | - |
`ids` | string | number | (string | number)[] | - |
`stack?` | boolean | whether push this operation to stack |
**Returns:** *boolean*
whether success
___
### setItemState
**setItemState**(`ids`: string | number | (string | number)[], `state`: string, `value`: boolean): *void*
*Defined in [src/runtime/graph.ts:301](https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/graph.ts#L301)*
Set state for the item.
**`group`** Item
**Parameters:**
Name | Type | Description |
------ | ------ | ------ |
`ids` | string | number | (string | number)[] | - |
`state` | string | the state name |
`value` | boolean | state value |
**Returns:** *void*
___
### setMode
**setMode**(`mode`: string): *void*
*Defined in [src/runtime/graph.ts:363](https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/graph.ts#L363)*
Switch mode.
**`group`** Interaction
**Parameters:**
Name | Type | Description |
------ | ------ | ------ |
`mode` | string | mode name |
**Returns:** *void*
___
### showItem
**showItem**(`ids`: string | number | (string | number)[]): *void*
*Defined in [src/runtime/graph.ts:281](https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/graph.ts#L281)*
Show the item(s).
**`group`** Item
**Parameters:**
Name | Type |
------ | ------ |
`ids` | string | number | (string | number)[] |
**Returns:** *void*
___
### uncombo
**uncombo**(`comboId`: string | number, `stack?`: boolean): *void*
*Defined in [src/runtime/graph.ts:321](https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/graph.ts#L321)*
dissolve combo
**`group`** Combo
**Parameters:**
Name | Type |
------ | ------ |
`comboId` | string | number |
`stack?` | boolean |
**Returns:** *void*
___
### updateBehavior
**updateBehavior**(`behavior`: BehaviorObjectOptionsOfB, `mode?`: string): *void*
*Defined in [src/runtime/graph.ts:417](https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/graph.ts#L417)*
Update a behavior on a mode.
**`group`** Interaction
**Parameters:**
Name | Type | Description |
------ | ------ | ------ |
`behavior` | BehaviorObjectOptionsOfB | behavior configs, whose name indicates the behavior to be updated |
`mode?` | string | mode name |
**Returns:** *void*
___
### updateItem
**updateItem**(`itemType`: ITEM_TYPE, `models`: PartialNodeUserModel | PartialEdgeUserModel | PartialComboUserModel | PartialNodeUserModel[] | PartialEdgeUserModel[] | PartialComboUserModel[], `stack?`: boolean): *boolean*
*Defined in [src/runtime/graph.ts:266](https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/graph.ts#L266)*
Update an item or items on the graph.
**`group`** Item
**Parameters:**
Name | Type | Description |
------ | ------ | ------ |
`itemType` | ITEM_TYPE | - |
`models` | PartialNodeUserModel | PartialEdgeUserModel | PartialComboUserModel | PartialNodeUserModel[] | PartialEdgeUserModel[] | PartialComboUserModel[] | - |
`stack?` | boolean | 本次操作是否入栈,默认为 true |
**Returns:** *boolean*
___
### updateSpecification
**updateSpecification**(`spec`: SpecificationB): *void*
*Defined in [src/runtime/graph.ts:72](https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/graph.ts#L72)*
Update the specs(configurations).
**Parameters:**
Name | Type |
------ | ------ |
`spec` | SpecificationB |
**Returns:** *void*
___
### zoom
**zoom**(`ratio`: number, `center?`: Point, `animateCfg?`: AnimateCfg): *void*
*Defined in [src/runtime/graph.ts:139](https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/graph.ts#L139)*
Zoom the graph with a relative ratio.
**`group`** View
**Parameters:**
Name | Type | Description |
------ | ------ | ------ |
`ratio` | number | relative ratio to zoom |
`center?` | Point | zoom center |
`animateCfg?` | AnimateCfg | animation configurations |
**Returns:** *void*
___
### zoomTo
**zoomTo**(`toRatio`: number, `center?`: Point, `animateCfg?`: AnimateCfg): *void*
*Defined in [src/runtime/graph.ts:151](https://github.com/antvis/G6/blob/v5/packages/g6/src/runtime/graph.ts#L151)*
Zoom the graph to a specified ratio.
**`group`** View
**Parameters:**
Name | Type | Description |
------ | ------ | ------ |
`toRatio` | number | specified ratio |
`center?` | Point | zoom center |
`animateCfg?` | AnimateCfg | animation configurations |
**Returns:** *void*

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +0,0 @@
---
id: "globals"
title: "@antv/g6"
sidebar_label: "Globals"
---
[@antv/g6](index.md) [Globals](globals.md)
## Index
### Modules
* ["graph"](modules/_graph_.md)

View File

@ -0,0 +1,9 @@
[@antv/g6](README.md) / Exports
# @antv/g6
## Table of contents
### Classes
- [default](classes/default.md)

View File

@ -1,13 +0,0 @@
---
id: "_graph_"
title: "graph"
sidebar_label: "graph"
---
[@antv/g6](../index.md) [Globals](../globals.md) ["graph"](_graph_.md)
## Index
### Classes
* [Graph](../classes/_graph_.graph.md)

View File

@ -43,7 +43,7 @@
"lint:src": "eslint --ext .ts --format=pretty \"./src\"",
"prettier": "prettier -c --write \"**/*\"",
"test": "jest",
"test-live": "DEBUG_MODE=1 jest --watch ./tests/unit/behavior-spec.ts",
"test-live": "DEBUG_MODE=1 jest --watch ./tests/unit/item-spec.ts",
"lint-staged:js": "eslint --ext .js,.jsx,.ts,.tsx",
"watch": "father build -w"
},
@ -59,8 +59,11 @@
},
"dependencies": {
"@antv/g": "^5.15.7",
"@antv/g-canvas": "^1.9.28",
"@antv/g-svg": "^1.8.36",
"@antv/graphlib": "^2.0.0-alpha.0",
"@antv/util": "~2.0.5"
"@antv/util": "~2.0.5",
"typedoc-plugin-markdown": "^3.14.0"
},
"devDependencies": {
"@types/jest": "^25.2.1",

View File

@ -1,5 +1,94 @@
import Item from "./item";
import { Group } from '@antv/g';
import { ID } from '@antv/graphlib';
import { EdgeDisplayModel, EdgeModel } from '../types';
import { EdgeModelData } from '../types/edge';
import { DisplayMapper, ITEM_TYPE, State } from '../types/item';
import { updateShapes } from '../util/shape';
import Item from './item';
import Node from './node';
interface IProps {
model: EdgeModel;
renderExtensions: any; // TODO: type
containerGroup: Group;
mapper: DisplayMapper;
stateMapper: {
[stateName: string]: DisplayMapper
};
sourceItem: Node;
targetItem: Node;
}
export default class Edge extends Item {
public destroyed: boolean = false;
// inner data model
public model: EdgeModel;
// display data model
public displayModel: EdgeDisplayModel;
/** Set to different value in implements */
public type: ITEM_TYPE = 'edge';
public sourceItem: Node;
public targetItem: Node;
}
constructor(props: IProps) {
super(props);
this.type = 'edge';
this.init(props);
const { sourceItem, targetItem } = props;
this.sourceItem = sourceItem;
this.targetItem = targetItem;
this.draw(this.displayModel);
}
public draw(displayModel: EdgeDisplayModel, diffData?: { previous: EdgeModelData; current: EdgeModelData }, diffState?: { previous: State[], current: State[] }) {
// get the end points
const sourceBBox = this.sourceItem.getKeyBBox();
const targetBBox = this.targetItem.getKeyBBox();
const sourcePoint = {
x: sourceBBox.center[0],
y: sourceBBox.center[1],
};
const targetPoint = {
x: targetBBox.center[0],
y: targetBBox.center[1],
};
const shapeMap = this.renderExt.draw(
this.displayModel,
sourcePoint,
targetPoint,
this.shapeMap,
diffData,
diffState
);
// add shapes to group, and update shapeMap
this.shapeMap = updateShapes(this.shapeMap, shapeMap, this.group);
const { labelShape } = this.shapeMap;
labelShape?.toFront();
super.draw(displayModel, diffData, diffState);
}
/**
* Sometimes no changes on edge data, but need to re-draw it
* e.g. source and target nodes' position changed
*/
public forceUpdate() {
this.draw(this.displayModel);
}
/**
* Update end item for item and re-draw the edge
* @param type update source or target
* @param endItem new item to ended
*/
public updateEnd(type: 'source' | 'target', endItem: Node) {
if (type === 'source') this.sourceItem = endItem;
else if (type === 'target') this.targetItem = endItem;
this.draw(this.displayModel);
}
// public update(model: EdgeModel) {
// super.update(model);
// }
}

View File

@ -1,38 +1,216 @@
import { Group } from "@antv/g";
import { IItem, ItemDisplayModel, ItemModel } from "../types/item";
import { Group, DisplayObject } from '@antv/g';
import { clone, isFunction } from '@antv/util';
import { EdgeLabelShapeStyle, EdgeShapeMap } from '../types/edge';
import {
DisplayMapper,
IItem,
ItemDisplayModel,
ItemModel,
ItemModelData,
ITEM_TYPE,
ShapeStyle,
State,
} from '../types/item';
import { NodeShapeMap, NodeLabelShapeStyle } from '../types/node';
import { isArrayOverlap } from '../util/array';
import { updateShapes } from '../util/shape';
import { isEncode } from '../util/type';
export default class Item implements IItem {
export const RESERVED_SHAPE_IDS = ['keyShape', 'labelShape', 'iconShape'];
const OTHER_SHAPES_FIELD_NAME = 'otherShapes';
export default abstract class Item implements IItem {
public destroyed: boolean = false;
// inner data model
public model: ItemModel;
private displayModel: ItemDisplayModel;
private group: Group;
private visible: boolean = true;
private states: {
name: string,
value: string | boolean
// display data model
public displayModel: ItemDisplayModel;
public mapper: DisplayMapper;
public stateMapper: {
[stateName: string]: DisplayMapper
};
public group: Group;
public keyShape: DisplayObject;
// render extension for this item
public renderExt;
public visible: boolean = true;
public states: {
name: string;
value: string | boolean;
}[] = [];
public shapeMap: NodeShapeMap | EdgeShapeMap = {
keyShape: undefined,
};
/** Set to different value in implements */
private type: 'node' | 'edge' | 'combo' = 'node';
public type: ITEM_TYPE;
public renderExtensions: any; // TODO
constructor(model: ItemModel) {
/** Cache the dirty tags for states when data changed, to re-map the state styles when state changed */
private stateDirtyMap: { [stateName: string]: boolean } = {};
private cacheStateStyles: {
keyShape?: ShapeStyle;
labelShape?: NodeLabelShapeStyle | EdgeLabelShapeStyle;
iconShape?: ShapeStyle;
[shapeId: string]: ShapeStyle
} = {}
private renderExtClass;
constructor(props) {}
public init(props) {
const { model, containerGroup, mapper, stateMapper, renderExtensions} = props;
this.group = new Group();
containerGroup.appendChild(this.group);
this.model = model;
this.draw();
this.mapper = mapper;
this.stateMapper = stateMapper;
this.displayModel = this.getDisplayModelAndChanges(model).model;
this.renderExtensions = renderExtensions;
const { type = this.type === 'node' ? 'circle-node' : 'line-edge' } = this.displayModel.data;
const RenderExtension = renderExtensions.find((ext) => ext.type === type);
this.renderExtClass = RenderExtension;
this.renderExt = new RenderExtension();
}
public draw() {
// TODO: 1. map this.model to displayModel
// TODO: 2. call element draw fn from useLib
public draw(displayModel: ItemDisplayModel, diffData?: { previous: ItemModelData; current: ItemModelData }, diffState?: { previous: State[], current: State[] }) {
// call this.renderExt.draw in extend implementations
const afterDrawShapes = this.renderExt.afterDraw?.(displayModel, this.shapeMap) || {};
this.shapeMap = updateShapes(this.shapeMap, afterDrawShapes, this.group, false, (id) => {
if (RESERVED_SHAPE_IDS.includes(id)) {
console.warn(
`Shape with id ${id} is reserved and should be returned in draw function, if the shape with ${id} returned by afterDraw is a new one, it will not be added to the group.`,
);
return false;
}
return true;
});
}
public update(model: ItemModel) {
// TODO: 1. merge model into this model
// TODO: 2. map new merged model to displayModel, keep prevModel and newModel for 3.
// TODO: 3. call element update fn from useLib
public update(
model: ItemModel,
diffData?: { previous: ItemModelData; current: ItemModelData },
isReplace?: boolean,
) {
// 1. merge model into this model
this.model = model;
// 2. map new merged model to displayModel, keep prevModel and newModel for 3.
const { model: displayModel, typeChange } = this.getDisplayModelAndChanges(
this.model,
diffData,
isReplace,
);
this.displayModel = displayModel;
if (typeChange) {
Object.values(this.shapeMap).forEach(child => child.remove(true));
this.shapeMap = { keyShape: undefined };
const { type = this.type === 'node' ? 'circle-node' : 'line-edge' } = displayModel.data;
const extension = this.renderExtensions.find((ext) => ext.type === type);
this.renderExt = new extension();
}
// 3. call element update fn from useLib
this.draw(this.displayModel, diffData);
// 4. tag all the states with 'dirty', for state style regenerating when state changed
this.stateDirtyMap = {}
this.states.forEach(({ name }) => this.stateDirtyMap[name] = true);
}
public getModel() {
return this.model;
/**
* Maps (mapper will be function, value, or encode format) model to displayModel and find out the shapes to be update for incremental updating.
* @param model inner model
* @param diffData changes from graphCore changed event
* @param isReplace whether replace the whole data or partial udpate
* @returns
*/
public getDisplayModelAndChanges(
innerModel: ItemModel,
diffData?: { previous: ItemModelData; current: ItemModelData },
isReplace?: boolean,
): {
model: ItemDisplayModel;
typeChange?: boolean;
} {
const { mapper } = this;
const { current = innerModel.data, previous } = diffData || {};
// === no mapper, displayModel = model ===
if (!mapper) {
this.displayModel = innerModel; // TODO: need clone?
// compare the previous data and current data to find shape changes
let typeChange = false;
if (current) {
typeChange = Boolean(current.type);
}
return {
model: innerModel,
typeChange,
};
}
// === mapper is function, displayModel is mapper(model), cannot diff the displayModel, so all the shapes need to be updated ===
if (isFunction(mapper)) return { model: (mapper as Function)(innerModel) };
// === fields' values in mapper are final value or Encode ===
const dataChangedFields = isReplace
? undefined
// ? Array.from(new Set(Object.keys(current).concat(Object.keys(previous)))) // all the fields for replacing all data
: Object.keys(current); // only fields in current data for partial updating
let typeChange = false;
const { data, ...otherProps } = innerModel;
const displayModelData = clone(data);
Object.keys(mapper).forEach((fieldName) => {
const subMapper = mapper[fieldName];
if (RESERVED_SHAPE_IDS.includes(fieldName)) {
// reserved shapes, fieldName is shapeId
if (!displayModelData.hasOwnProperty(fieldName)) {
displayModelData[fieldName] = {};
updateShapeChange({
innerModel,
mapper: subMapper,
dataChangedFields,
shapeConfig: displayModelData[fieldName],
});
}
} else if (fieldName === OTHER_SHAPES_FIELD_NAME) {
// other shapes
displayModelData[fieldName] = displayModelData[fieldName] || {};
Object.keys(subMapper).forEach((shapeId) => {
if (!displayModelData[fieldName]?.hasOwnProperty(shapeId)) {
displayModelData[fieldName][shapeId] = displayModelData[fieldName][shapeId] || {};
const shappStyle = subMapper[shapeId];
updateShapeChange({
innerModel,
mapper: shappStyle,
dataChangedFields,
shapeConfig: displayModelData[fieldName][shapeId],
});
}
});
} else {
// fields not about shape
if (!displayModelData.hasOwnProperty(fieldName)) {
const { changed, value: mappedValue } = updateChange({
innerModel,
mapper,
fieldName,
dataChangedFields,
});
displayModelData[fieldName] = mappedValue;
if (changed && fieldName === 'type') typeChange = true;
} else if (fieldName === 'type' && (!dataChangedFields || dataChangedFields.includes('type'))) {
typeChange = true;
}
}
});
const displayModel = {
...otherProps,
data: displayModelData,
};
return {
model: displayModel,
typeChange,
};
}
public getID() {
@ -44,11 +222,13 @@ export default class Item implements IItem {
}
public show() {
// TODO: utilize graphcore's view
this.group.style.visibility = 'visible';
this.visible = true;
}
public hide() {
// TODO: utilize graphcore's view
this.group.style.visibility = 'hidden';
this.visible = false;
}
@ -65,39 +245,223 @@ export default class Item implements IItem {
this.group.toFront();
}
/**
* The state value for the item, false if the item does not have the state.
* @param state state name
* @returns { boolean | string } the state value
*/
public hasState(state: string) {
const findState = this.states.find((item) => item.name === state);
return findState?.value || false;
}
/**
* Set the state for the item.
* @param state state name
* @param value state value
*/
public setState(state: string, value: string | boolean) {
const existState = this.states.find(item => item.name === state);
if (existState && value) {
existState.value = value;
const previousStates = clone(this.states);
const existState = this.states.find((item) => item.name === state);
if (value) {
if (existState) existState.value = value;
else this.states.push({
name: state,
value
});
} else {
const idx = this.states.indexOf(existState);
this.states.splice(idx, 1);
}
// TODO: call element setState fn from useLib
}
public hasState(state: string) {
const findState = this.states.find(item => item.name === state);
return findState || false;
// if the renderExt overwrote the setState, run the custom setState instead of the default
if (this.renderExt.constructor.prototype.hasOwnProperty('setState')) {
this.renderExt.setState(state, value, this.shapeMap);
return;
}
this.drawWithStates(previousStates);
}
/**
* Clear the states for the item.
* @param states the states to be cleared. All the states will be cleared if the states is not assigned
*/
public clearStates(states?: string[]) {
const newStates = this.states.filter(state => !states.includes(state.name));
// if states is not assigned, clear all the states on the item
const previousStates = clone(this.states);
const newStates = [];
let changedStates = [];
if (states) {
this.states.filter(state => {
if (!states.includes(state.name)) {
newStates.push(state);
} else {
changedStates.push(state);
}
});
} else {
changedStates = this.states.map(({ name, value }) => ({ name, value: false }));
}
this.states = newStates;
states.forEach(state => {
// TODO: call element setState fn with false from useLib
});
// if the renderExt overwrote the setState, run the custom setState instead of the default
if (this.renderExt.constructor.prototype.hasOwnProperty('setState')) {
changedStates.forEach(({ name, value }) => this.renderExt.setState(name, value, this.shapeMap));
return;
}
this.drawWithStates(previousStates);
}
/**
* Get all the states of the item.
* @retruns states array with has { name: string, value: string | boolean } format item
*/
public getStates() {
return this.states;
}
public destroy() {
// TODO: 1. stop animations
// TODO: 2. clear group and remove group
// 2. clear group and remove group
this.group.remove(true);
this.model = null;
this.displayModel = null;
this.destroyed = true;
}
}
/**
* Re-draw the item with merged state styles.
* @param previousStates previous states
* @returns
*/
private drawWithStates(previousStates: State[]) {
if (!this.stateMapper) return;
const { data: displayModelData } = this.displayModel;
const styles = {}; // merged styles
this.states.forEach(({ name: stateName, value }) => {
let stateStyles = this.cacheStateStyles[stateName] || {};
// re-mapper the state styles for states if they has dirty tag
if (value && (!this.stateDirtyMap.hasOwnProperty(stateName) || this.stateDirtyMap[stateName])) {
this.stateDirtyMap[stateName] = false;
const mapper = this.stateMapper[stateName];
if (!mapper) return;
Object.keys(mapper).forEach(shapeId => {
stateStyles[shapeId] = {
...(displayModelData[shapeId] as Object)
}
if (RESERVED_SHAPE_IDS.includes(shapeId)) {
// reserved shapes, fieldName is shapeId
if (!displayModelData.hasOwnProperty(shapeId)) {
updateShapeChange({
innerModel: this.model,
mapper: mapper[shapeId],
dataChangedFields: undefined,
shapeConfig: stateStyles[shapeId],
});
}
} else if (shapeId === OTHER_SHAPES_FIELD_NAME) {
// other shapes
Object.keys(mapper).forEach((otherShapeId) => {
if (!displayModelData[shapeId]?.hasOwnProperty(otherShapeId)) {
stateStyles[shapeId] = stateStyles[shapeId] || {};
updateShapeChange({
innerModel: this.model,
mapper: mapper[shapeId][otherShapeId],
dataChangedFields: undefined,
shapeConfig: stateStyles[shapeId],
});
}
});
}
});
}
// merge the state styles
Object.keys(stateStyles).forEach(shapeId => {
styles[shapeId] = Object.assign({}, styles[shapeId], stateStyles[shapeId]);
});
this.cacheStateStyles[stateName] = stateStyles;
});
// apply the merged styles
this.draw(
// displayModel
{
...this.displayModel,
data: {
...displayModelData,
...styles
},
} as ItemDisplayModel,
// diffData
undefined,
// diffState
{
previous: previousStates,
current: this.states,
}
);
}
}
/**
* Get the mapped value of a field on innerModel.
* @param param0: {
* innerModel, // find unmapped field value from innerModel
* fieldName, // name of the field to read from innerModel
* mapper, // mapper object, contains the field's mapper
* dataChangedFields, // fields' names which are changed in data
* }
* @returns { changed: boolean, value: unknown } return whether the mapper affects the value, and the mapped result
*/
const updateChange = ({
innerModel,
fieldName,
mapper,
dataChangedFields,
}): {
changed: boolean;
value?: unknown;
} => {
const value = mapper[fieldName] || '';
if (isEncode(value)) {
const { fields, formatter } = value;
// data changed fields and the encode fields are overlapped, display value should be changed
if (!dataChangedFields || isArrayOverlap(dataChangedFields, fields)) {
const formatedValue = formatter(innerModel);
return {
changed: true,
value: formatedValue || '',
};
}
return { changed: false };
} else {
// not an encode, take the mapper's value as the result directly
return {
changed: true,
value,
};
}
};
/**
* Update a shape's config according to the mapper.
* @param param0: {
* innerModel, // find unmapped field value from innerModel
* mapper, // mapper object, contains the field's mapper
* shapeId, // id of the shape where the fieldName belong to
* dataChangedFields, // fields' names which are changed in data
* shapeConfig, // the shape's config to be updated
* }
* @returns { changed: boolean, value: unknown } return whether the mapper affects the value, and the mapped result
*/
const updateShapeChange = ({ innerModel, mapper, dataChangedFields, shapeConfig }) => {
Object.keys(mapper).forEach((shapeAttrName) => {
const { value: mappedValue } = updateChange({
innerModel,
mapper,
fieldName: shapeAttrName,
dataChangedFields,
});
shapeConfig[shapeAttrName] = mappedValue;
});
};

View File

@ -1,5 +1,56 @@
import Item from "./item";
import { Group } from '@antv/g';
import { NodeModel } from '../types';
import { DisplayMapper, State } from '../types/item';
import { NodeDisplayModel, NodeModelData } from '../types/node';
import { updateShapes } from '../util/shape';
import Item from './item';
interface IProps {
model: NodeModel;
renderExtensions: any; // TODO: type
containerGroup: Group;
mapper: DisplayMapper;
stateMapper: {
[stateName: string]: DisplayMapper
};
}
export default class Node extends Item {
constructor(props: IProps) {
super(props);
this.type = 'node';
this.init(props);
this.draw(this.displayModel as NodeDisplayModel);
}
public draw(displayModel: NodeDisplayModel, diffData?: { previous: NodeModelData; current: NodeModelData }, diffState?: { previous: State[], current: State[] }) {
const { group, renderExt, shapeMap: prevShapeMap } = this;
const { data } = displayModel;
const { x = 0, y = 0 } = data;
group.style.x = x;
group.style.y = y;
const shapeMap = renderExt.draw(displayModel, this.shapeMap, diffData, diffState);
}
// add shapes to group, and update shapeMap
this.shapeMap = updateShapes(prevShapeMap, shapeMap, group);
this.shapeMap.labelShape?.toFront();
super.draw(displayModel, diffData, diffState);
}
public update(
model: NodeModel,
diffData: { previous: NodeModelData; current: NodeModelData },
isReplace?: boolean,
) {
super.update(model, diffData, isReplace);
const { data } = this.displayModel;
const { x = 0, y = 0 } = data;
this.group.style.x = x;
this.group.style.y = y;
}
public getKeyBBox() {
const { keyShape } = this.shapeMap;
return keyShape?.getRenderBounds() || { center: [0, 0, 0] };
}
}

View File

@ -1,13 +1,13 @@
import { Graph as GraphLib } from "@antv/graphlib";
import { NodeUserModel, EdgeUserModel, ComboUserModel, GraphData, IGraph } from "../../types";
import { Graph as GraphLib, ID } from '@antv/graphlib';
import { GraphData, IGraph, ComboModel, ComboUserModel } from '../../types';
import { registery } from '../../stdlib';
import { getExtension } from "../../util/extension";
import { clone, isArray, isNumber, isString } from "@antv/util";
import { NodeModelData } from "../../types/node";
import { EdgeModelData } from "../../types/edge";
import { GraphCore } from "../../types/data";
import { ITEM_TYPE } from "../../types/item";
import { isFunction } from "@antv/g-lite/dist/utils";
import { getExtension } from '../../util/extension';
import { clone, isArray, isNumber, isString, isFunction, isObject } from '@antv/util';
import { NodeModel, NodeModelData, NodeUserModel, NodeUserModelData } from '../../types/node';
import { EdgeModel, EdgeModelData, EdgeUserModel, EdgeUserModelData } from '../../types/edge';
import { DataChangeType, GraphCore } from '../../types/data';
import { ITEM_TYPE } from '../../types/item';
import { ComboUserModelData } from '../../types/combo';
/**
* Manages the data transform extensions;
@ -19,7 +19,7 @@ export class DataController {
/**
* User input data.
*/
public userData: GraphData;
public userGraphCore: GraphCore;
/**
* Inner data stored in graphCore structure.
*/
@ -27,19 +27,21 @@ export class DataController {
constructor(graph: IGraph<any>) {
this.graph = graph;
this.graphCore = new GraphLib<NodeModelData, EdgeModelData>();
this.tap();
}
public findData(type: ITEM_TYPE, condition: string | number | (string | number)[] | Function) {
public findData(
type: ITEM_TYPE,
condition: ID[] | Function,
): EdgeModel[] | NodeModel[] | ComboModel[] {
const { graphCore } = this;
if (isString(condition) || isNumber(condition) || isArray(condition)) {
const ids = isArray(condition) ? condition : [condition];
switch (type) {
case 'node':
return ids.map(id => graphCore.getNode(id));
return ids.map((id) => (graphCore.hasNode(id) ? graphCore.getNode(id) : undefined));
case 'edge':
return ids.map(id => graphCore.getEdge(id));
return ids.map((id) => (graphCore.hasEdge(id) ? graphCore.getEdge(id) : undefined));
case 'combo':
// TODO;
return;
@ -50,7 +52,20 @@ export class DataController {
// TODO getDatas = ?
}
const datas = getDatas() as any;
return datas.find(data => condition(data));
return datas.filter((data) => condition(data));
}
}
public findAllData(type: ITEM_TYPE): EdgeModel[] | NodeModel[] | ComboModel[] {
switch (type) {
case 'node':
return this.graphCore.getAllNodes();
case 'edge':
return this.graphCore.getAllEdges();
// case 'combo':
// TODO
default:
return [];
}
}
@ -59,10 +74,7 @@ export class DataController {
*/
private tap() {
this.extensions = this.getExtensions();
this.graph.hooks.datachange.tap(this.onDataChange);
this.graph.hooks.additems.tap(this.onAdd);
this.graph.hooks.removeitems.tap(this.onRemove);
this.graph.hooks.updateitems.tap(this.onUpdate);
this.graph.hooks.datachange.tap(this.onDataChange.bind(this));
}
/**
@ -70,158 +82,398 @@ export class DataController {
*/
private getExtensions() {
const { transform = [] } = this.graph.getSpecification();
return transform.map(config => ({
config,
func: getExtension(config, registery.useLib, 'transform')
})).filter(ext => !!ext.func);
return transform
.map((config) => ({
config,
func: getExtension(config, registery.useLib, 'transform'),
}))
.filter((ext) => !!ext.func);
}
/**
* Listener of graph's datachange hook.
* @param param contains new graph data
* @param param contains new graph data and type of data change
*/
private onDataChange(param: { data: GraphData }) {
const { data } = param;
this.userData = data;
let dataCloned: GraphData = clone(data);
private onDataChange(param: { data: GraphData; type: DataChangeType }) {
const { data, type: changeType } = param;
const change = () => {
switch (changeType) {
case 'remove':
this.removeData(data);
break;
case 'update':
this.updateData(data);
break;
default:
// 'replace' | 'mergeReplace' | 'union'
this.changeData(data, changeType);
break;
}
};
const { userGraphCore } = this;
if (userGraphCore) {
userGraphCore.batch(change);
} else {
change();
}
}
/**
* Change data by replace, merge repalce, or union.
* @param data new data
* @param changeType type of data change, 'replace' means discard the old data. 'mergeReplace' means merge the common part. 'union' means merge whole sets of old and new one
*/
private changeData(data: GraphData, changeType: 'replace' | 'mergeReplace' | 'union') {
const { userGraphCore } = this;
if (changeType === 'replace') {
this.userGraphCore = new GraphLib<NodeUserModelData, EdgeUserModelData>({
...data,
onChanged: (event) => this.updateGraphCore(event),
});
const { data: transformedData } = this.transformData();
this.graphCore = new GraphLib<NodeModelData, EdgeModelData>({ ...transformedData });
} else {
const prevNodes = userGraphCore.getAllNodes();
const { nodes, edges, combos } = data;
// TODO: distinguish combos
if (!prevNodes.length) {
userGraphCore.addNodes(nodes);
} else {
if (changeType === 'mergeReplace') {
// remove the nodes which are not in data but in userGraphCore
const nodeIds = nodes.map((node) => node.id);
prevNodes.forEach((prevNode) => {
if (!nodeIds.includes(prevNode.id)) userGraphCore.removeNode(prevNode.id);
});
}
// add or update node
nodes.forEach((node) => {
if (userGraphCore.hasNode(node.id)) {
// update node which is in the graphCore
userGraphCore.mergeNodeData(node.id, node.data);
} else {
// add node which is in data but not in graphCore
userGraphCore.addNode(node);
}
});
}
const prevEdges = userGraphCore.getAllEdges();
if (!prevEdges.length) {
userGraphCore.addEdges(edges);
} else {
if (changeType === 'mergeReplace') {
// remove the edges which are not in data but in userGraphCore
const edgeIds = edges.map((edge) => edge.id);
prevEdges.forEach((prevEdge) => {
if (!edgeIds.includes(prevEdge.id)) userGraphCore.removeEdge(prevEdge.id);
});
}
// add or update edge
edges.forEach((edge) => {
if (userGraphCore.hasEdge(edge.id)) {
// update edge which is in the graphCore
userGraphCore.mergeEdgeData(edge.id, edge.data);
} else {
// add edge which is in data but not in graphCore
userGraphCore.addEdge(edge);
}
});
}
}
}
/**
* Remove part of old data.
* @param data data to be removed which is part of old one
*/
private removeData(data: GraphData) {
const { userGraphCore } = this;
const { nodes, edges, combos } = data;
const prevNodes = userGraphCore.getAllNodes();
const prevEdges = userGraphCore.getAllEdges();
// TODO: distinguish combos
if (prevNodes.length && nodes.length) {
// remove the node
userGraphCore.removeNodes(nodes.map((node) => node.id));
}
if (prevEdges.length && edges.length) {
// add or update edge
userGraphCore.removeEdges(edges.map((edge) => edge.id));
}
// TODO: combo
}
/**
* Update part of old data.
* @param data data to be updated which is part of old one
*/
private updateData(data: GraphData) {
const { userGraphCore } = this;
const { nodes, edges, combos } = data;
const prevNodes = userGraphCore.getAllNodes();
const prevEdges = userGraphCore.getAllEdges();
// TODO: distinguish combos
if (prevNodes.length) {
// update node
nodes.forEach((newModel) => {
const { id, data } = newModel;
if (data) {
const mergedData = mergeOneLevelData(userGraphCore.getNode(id), newModel);
userGraphCore.mergeNodeData(id, mergedData);
}
});
}
if (prevEdges.length) {
// update edge
edges.forEach((newModel) => {
const oldModel = userGraphCore.getEdge(newModel.id);
if (!oldModel) return;
const { id, source, target, data } = newModel;
if (source && oldModel.source !== source) userGraphCore.updateEdgeSource(id, source);
if (target && oldModel.target !== target) userGraphCore.updateEdgeTarget(id, target);
if (data) {
const mergedData = mergeOneLevelData(userGraphCore.getEdge(id), newModel);
userGraphCore.mergeEdgeData(id, mergedData);
}
});
}
// TODO: combo
}
/**
* Update graphCore with transformed userGraphCore data.
*/
private updateGraphCore(event) {
const { graphCore } = this;
// Transform the data.
this.extensions.forEach(({ func, config }) => {
dataCloned = func(dataCloned, config);
})
// === step 1: clone data from userGraphCore (userData) ===
// === step 2: transform the data with transform extensions, output innerData and idMaps ===
const { data: transformedData, idMaps } = this.transformData();
const { nodes, edges, combos } = transformedData;
// Input and store in graphcore.
const { nodes = [], edges = [], combos = [] } = dataCloned;
// TODO: distinguish combos
if (!graphCore.getAllNodes().length) {
graphCore.addNodes(nodes);
} else {
nodes.forEach(node => {
if (graphCore.hasNode(node.id)) {
graphCore.mergeNodeData(node.id, node.data);
} else {
graphCore.addNode(node);
}
});
}
const prevNodes = graphCore.getAllNodes();
if (!graphCore.getAllEdges().length) {
graphCore.addEdges(edges);
} else {
edges.forEach(edge => {
if (graphCore.hasEdge(edge.id)) {
graphCore.mergeEdgeData(edge.id, edge.data);
} else {
graphCore.addEdge(edge);
}
});
}
}
/**
* Add models to graphCore.
* @param param item type and model list
*/
private onAdd(param: { type: ITEM_TYPE, models: NodeUserModel[] | EdgeUserModel[] | ComboUserModel[] }) {
const { type, models } = param;
const { userData } = this;
// merge new models into userData, and format the whole dataset with extensions
const useModels = (userData[`${type}s`] as any).concat(models);
let dataCloned: GraphData = clone(userData);
this.extensions.forEach(({ func, config }) => {
dataCloned = func(dataCloned, config);
});
const addIds = models.map(model => model.id);
const formattedModels = (dataCloned[`${type}s`] as any).filter(model => addIds.includes(model.id));
// add to graphCore
// TODO: batch
switch (type) {
case 'node':
this.graphCore.addNodes(formattedModels as NodeUserModel[]);
break;
case 'edge':
this.graphCore.addEdges(formattedModels as EdgeUserModel[]);
break;
case 'combo':
//TODO
break;
default:
break;
}
}
/**
* Remove models from graphCore.
* @param param item type and id list
*/
private onRemove(param: { type: ITEM_TYPE, ids: (string | number)[] }) {
const { type, ids } = param;
const { userData } = this;
// remove models from userData, and format the whole dataset with extensions
userData[`${type}s`] = (userData[`${type}s`] as any).filter(model => !ids.includes(model.id));
let dataCloned: GraphData = clone(userData);
this.extensions.forEach(({ func, config }) => {
dataCloned = func(dataCloned, config);
});
// remove from graphCore
// TODO: batch
switch (type) {
case 'node':
this.graphCore.removeNodes(ids);
break;
case 'edge':
this.graphCore.removeEdges(ids);
break;
case 'combo':
//TODO
break;
default:
break;
}
}
private onUpdate(param: { type: ITEM_TYPE, models: NodeUserModel[] | EdgeUserModel[] | ComboUserModel[] }) {
const { type, models } = param;
const { userData } = this;
// update models in userData, and format the whole dataset with extensions
userData[`${type}s`] = userData[`${type}s`].map(useModel => {
const model = (models as any).find(item => item.id === useModel);
if (model) {
useModel.data = {
...useModel.data,
...model.data
}
// function to update one data in graphCore with different model type ('node' or 'edge')
const syncUpdateToGraphCore = (id, newValue, oldValue, isNode, diff = []) => {
if (isNode) {
if (newValue.data) graphCore.updateNodeData(id, newValue.data);
} else {
if (diff.includes('data')) graphCore.updateEdgeData(id, newValue.data);
// source and target may be changed
if (diff.includes('source')) graphCore.updateEdgeSource(id, newValue.source);
if (diff.includes('target')) graphCore.updateEdgeTarget(id, newValue.target);
}
// TODO: combo
};
graphCore.batch(() => {
// === step 3: sync to graphCore according to the changes in userGraphCore ==
if (!idMaps?.length || idMaps.length !== this.extensions.length) {
// situation 1: not every extension has corresponding idMap, use default mapping: suppose id is not changed by transforms
// and diff the value in graphCore whose id is not in userGraphCore
const newModelMap: {
[id: string]: {
type: 'node' | 'edge' | 'combo';
model: NodeModel | EdgeModel | ComboModel;
};
} = {};
nodes.forEach((model) => (newModelMap[model.id] = { type: 'node', model }));
edges.forEach((model) => (newModelMap[model.id] = { type: 'edge', model }));
prevNodes.forEach((prevNode) => {
const { id } = prevNode;
const { model: newModel } = newModelMap[id] || {};
// remove
if (!newModel) graphCore.removeNode(id);
// update
else if (diffAt(newModel, prevNode, true)?.length)
syncUpdateToGraphCore(id, newModel, prevNode, true);
// delete from the map indicates this model is visited
delete newModelMap[id];
});
graphCore.getAllEdges().forEach((prevEdge) => {
const { id } = prevEdge;
const { model: newModel } = newModelMap[id] || {};
// remove
if (!newModel) graphCore.removeEdge(id);
// update
else {
const diff = diffAt(newModel, prevEdge, false);
if (diff?.length) syncUpdateToGraphCore(id, newModel, prevEdge, false, diff);
}
// delete from the map indicates this model is visited
delete newModelMap[id];
});
// add
Object.values(newModelMap).forEach(({ type, model }) => {
if (type === 'node') graphCore.addNode(model);
else if (type === 'edge') graphCore.addEdge(model as EdgeModel);
// TODO: combo
});
} else {
// situation 2: idMaps is complete
// calculate the final idMap which maps the ids from final transformed data to their comes from ids in userData
const finalIdMap = {};
const newModelMap = {};
const prevModelMap = {};
nodes.concat(edges).forEach((model) => {
finalIdMap[model.id] = getComesFromLinkedList(model.id, idMaps);
newModelMap[model.id] = model;
});
prevNodes.concat(graphCore.getAllEdges()).forEach((model) => {
prevModelMap[model.id] = model;
});
// TODO: combo
// map changes for search
const changeMap = {};
const { changes } = event;
changes.forEach((change) => {
const { value, id, type } = change;
// TODO: temporary skip. how to handle tree change events?
if (
['TreeStructureAttached', 'TreeStructureDetached', 'TreeStructureChanged'].includes(
type,
)
)
return;
const dataId = id || value.id;
changeMap[dataId] = changeMap[dataId] || [];
changeMap[dataId].push(type.toLawerCase());
});
// 1. remove or add model to userGraphCore according the existence
// 2. update or keep unchanged according to the source models' changes in userGraphCore
// if source models have any change, update the data in graphcore. Kepp unchanged otherwise
Object.keys(newModelMap).forEach((newId) => {
const comesFromIds = finalIdMap[newId];
const newValue = newModelMap[newId];
const oldValue = prevModelMap[newId];
const isNode = graphCore.hasNode(newId);
if (newValue && !oldValue) {
const addFunc = isNode ? graphCore.addNode : graphCore.addEdge;
addFunc(newValue);
} else if (!newValue && oldValue) {
const removeFunc = isNode ? graphCore.removeNode : graphCore.removeEdge;
removeFunc(newId);
} else {
if (!comesFromIds?.length) {
// no comesForm, find same id in userGraphCore to follow the change, if it not found, diff new and old data value of graphCore (inner data)
const diff = diffAt(newValue, oldValue, isNode);
if (diff?.length) syncUpdateToGraphCore(newId, newValue, oldValue, isNode, diff);
} else {
// follow the corresponding data event in userGraphCore
const comesFromChanges = changeMap[comesFromIds[0]];
if (comesFromChanges?.length)
syncUpdateToGraphCore(newId, newValue, oldValue, isNode);
}
}
});
}
return useModel;
})
let dataCloned: GraphData = clone(userData);
this.extensions.forEach(({ func, config }) => {
dataCloned = func(dataCloned, config);
});
const updateIds = models.map(model => model.id);
const formattedModels = (dataCloned[`${type}s`] as any).filter(model => updateIds.includes(model.id));
// TODO: batch
switch (type) {
case 'node':
formattedModels.forEach(model => {
this.graphCore.mergeNodeData(model.id, model.data);
});
break;
case 'edge':
formattedModels.forEach(model => {
this.graphCore.mergeEdgeData(model.id, model.data);
});
break;
case 'combo':
//TODO
break;
default:
break;
}
}
}
/**
* Clone data from userGraphCore, and run transforms
* @returns transformed data and the id map list
*/
private transformData() {
const { userGraphCore } = this;
// === step 1: clone data from userGraphCore (userData) ===
const userData = {
// TODO: should be deepClone
nodes: userGraphCore.getAllNodes(),
edges: userGraphCore.getAllEdges(),
// combos:
};
let dataCloned: GraphData = clone(userData);
// === step 2: transform the data with transform extensions, output innerData and idMaps ===
const idMaps = [];
this.extensions.forEach(({ func, config }) => {
const result = func(dataCloned, config);
dataCloned = result.data;
const idMap = result.idMap;
if (idMap) idMaps.push(idMap);
});
return { data: dataCloned, idMaps };
}
}
/**
* Get the source id list of the id from tail to head in linkedList.
* @param id target id to find its source id list
* @param linkedList id map list
* @param index index in linkedList to start from, from the tail by defailt
* @returns source id list
*/
const getComesFromLinkedList = (id, linkedList, index = linkedList.length - 1) => {
let comesFrom = [];
linkedList[index][id]?.forEach((comesFromId) => {
if (index === 0) comesFrom.push(comesFromId);
else comesFrom = comesFrom.concat(getComesFromLinkedList(comesFromId, linkedList, index - 1));
});
return comesFrom;
};
/**
* Diff new and old model.
* @param newModel
* @param oldModel
* @param isNode
* @returns false for no different, ['data'] for data different
*/
const diffAt = (newModel, oldModel, isNode): ('data' | 'source' | 'target')[] => {
// edge's source or target is changed
const diff = [];
if (!isNode) {
if (newModel.source !== oldModel.source) diff.push('source');
if (newModel.target !== oldModel.target) diff.push('target');
}
if (!newModel.data) return diff;
// value in data is changed
const newKeys = Object.keys(newModel.data);
const oldKeys = Object.keys(oldModel.data);
if (oldKeys.length === 0 && oldKeys.length === newKeys.length) return diff;
if (oldKeys.length !== newKeys.length) return diff.concat('data');
for (let i = 0; i < newKeys.length; i++) {
const key = newKeys[i];
const newValue = newModel.data[key];
const oldValue = oldModel.data[key];
const newValueIsObject = isObject(newValue);
const oldValueIsObject = isObject(oldValue);
if (newValueIsObject !== oldValueIsObject) return diff.concat('data');
if (newValueIsObject && oldValueIsObject) {
if (JSON.stringify(newValue) !== JSON.stringify(oldValue)) return diff.concat('data');
else continue;
}
if (newValue !== oldValue) return diff.concat('data');
}
return diff;
};
/**
* Merge the first level fields in data of model
* @param prevModel previous model
* @param newModel incoming new model
* @returns merged model data
*/
const mergeOneLevelData = (
prevModel: NodeUserModel | EdgeUserModel | ComboUserModel,
newModel: NodeUserModel | EdgeUserModel | ComboUserModel,
): NodeUserModelData | EdgeUserModelData | ComboUserModelData => {
const { data: newData } = newModel;
const { data: prevData } = prevModel;
const mergedData = {};
Object.keys(newData).forEach((key) => {
if (isObject(prevData[key]) && isObject(newData[key])) {
mergedData[key] = {
...(prevData[key] as object),
...(newData[key] as object),
};
} else {
mergedData[key] = newData[key];
}
});
return mergedData;
};

View File

@ -71,7 +71,6 @@ export class InteractionController {
behaviors.forEach(config => {
if (isObject(config) && config.hasOwnProperty('key')) {
const behaviorItem = self.extensions[mode].find(behavior => behavior.getKey() === config.key);
debugger
if (behaviorItem) behaviorItem.updateConfig(config);
}
});

View File

@ -1,12 +1,16 @@
import { Graph as GraphLib } from "@antv/graphlib";
import { IGraph } from "../../types";
import { GraphChange, ID } from '@antv/graphlib';
import { ComboModel, IGraph } from '../../types';
import { registery } from '../../stdlib';
import { getExtension } from "../../util/extension";
import { DisplayGraphCore, GraphCore } from "../../types/data";
import { NodeDisplayModelData } from "../../types/node";
import { EdgeDisplayModelData } from "../../types/edge";
import Edge from "../../item/edge";
import Combo from "../../item/combo";
import { getExtension } from '../../util/extension';
import { GraphCore } from '../../types/data';
import { NodeDisplayModel, NodeEncode, NodeModel, NodeModelData } from '../../types/node';
import { EdgeDisplayModel, EdgeEncode, EdgeModel, EdgeModelData } from '../../types/edge';
import Node from '../../item/node';
import Edge from '../../item/edge';
import Combo from '../../item/combo';
import { Group } from '@antv/g';
import { ITEM_TYPE } from '../../types/item';
import { ComboDisplayModel, ComboEncode } from '../../types/combo';
/**
* Manages and stores the node / edge / combo items.
@ -16,21 +20,44 @@ export class ItemController {
public nodeExtensions = [];
public edgeExtensions = [];
public comboExtensions = [];
/**
* Display data stored in graphCore structure.
*/
public graphCore: DisplayGraphCore;
/**
* Node / edge / combo items array
* Node / edge / combo items map
*/
public nodes: Node[];
public edges: Edge[];
public combos: Combo[];
private itemMap: { [id: ID]: Node | Edge | Combo } = {};
/**
* node / edge / combo 's mapper in graph config
*/
private nodeMapper: ((data: NodeModel) => NodeDisplayModel) | NodeEncode;
private edgeMapper: ((data: EdgeModel) => EdgeDisplayModel) | EdgeEncode;
private comboMapper: ((data: ComboModel) => ComboDisplayModel) | ComboEncode;
private nodeStateMapper: {
[stateName: string]: ((data: NodeModel) => NodeDisplayModel) | NodeEncode;
};
private edgeStateMapper: {
[stateName: string]: ((data: EdgeModel) => EdgeDisplayModel) | EdgeEncode;
};
private comboStateMapper: {
[stateName: string]: ((data: ComboModel) => ComboDisplayModel) | ComboEncode;
};
private nodeGroup: Group;
private edgeGroup: Group;
// TODO: combo? not a independent group
constructor(graph: IGraph<any>) {
this.graph = graph;
this.graphCore = new GraphLib<NodeDisplayModelData, EdgeDisplayModelData>();
// get mapper for node / edge / combo
const { node, edge, combo, nodeState, edgeState, comboState } = graph.getSpecification();
this.nodeMapper = node;
this.edgeMapper = edge;
this.comboMapper = combo;
this.nodeStateMapper = nodeState;
this.edgeStateMapper = edgeState;
this.comboStateMapper = comboState;
this.tap();
}
@ -43,28 +70,286 @@ export class ItemController {
this.nodeExtensions = extensions.node;
this.edgeExtensions = extensions.edge;
this.comboExtensions = extensions.combo;
this.graph.hooks.render.tap(this.onRender);
this.graph.hooks.render.tap(this.onRender.bind(this));
this.graph.hooks.itemchange.tap(this.onChange.bind(this));
this.graph.hooks.itemstatechange.tap(this.onItemStateChange.bind(this));
}
/**
* Get the extensions from useLib, stdLib is a subset of useLib.
*/
private getExtensions() {
const { transform = [] } = this.graph.getSpecification();
// TODO: user need to config using node/edge/combo types from useLib to spec?
const { node, edge, combo } = this.graph.getSpecification();
const nodeTypes = ['circle-node', 'custom-node']; // TODO: WIP
const edgeTypes = ['line-edge', 'custom-edge']; // TODO: WIP
const comboTypes = ['circle-combo', 'rect-combo']; // TODO: WIP
return {
node: transform.map(config => getExtension(config, registery.useLib, 'node')).filter(transformer => !!transformer),
edge: transform.map(config => getExtension(config, registery.useLib, 'edge')).filter(transformer => !!transformer),
combo: transform.map(config => getExtension(config, registery.useLib, 'combo')).filter(transformer => !!transformer),
}
node: nodeTypes
.map((config) => getExtension(config, registery.useLib, 'node'))
.filter(Boolean),
edge: edgeTypes
.map((config) => getExtension(config, registery.useLib, 'edge'))
.filter(Boolean),
combo: comboTypes
.map((config) => getExtension(config, registery.useLib, 'combo'))
.filter(Boolean),
};
}
/**
* Listener of graph's render hook.
* Listener of runtime's render hook.
* @param param contains inner data stored in graphCore structure
*/
private onRender(param: { graphCore: GraphCore }) {
const { graphCore } = param;
const { graph } = this;
// TODO: 0. clear groups on canvas, and create new groups
graph.canvas.removeChildren();
const edgeGroup = new Group({ id: 'edge-group' });
const nodeGroup = new Group({ id: 'node-group' });
graph.canvas.appendChild(edgeGroup);
graph.canvas.appendChild(nodeGroup);
this.nodeGroup = nodeGroup;
this.edgeGroup = edgeGroup;
// TODO: 1. create node / edge / combo items, classes from ../../item, and element drawing and updating fns from node/edge/comboExtensions
// TODO: 2. draw them
const nodeModels = graphCore.getAllNodes();
const edgeModels = graphCore.getAllEdges();
// const combos = graphCore.getAllCombos();
this.renderNodes(nodeModels);
this.renderEdges(edgeModels);
// TODO: combo
}
}
/**
* Listener of runtime's itemchange lifecycle hook.
* @param param
*/
private onChange(param: {
type: ITEM_TYPE;
changes: GraphChange<NodeModelData, EdgeModelData>[];
graphCore: GraphCore;
}) {
const { changes, graphCore } = param;
const groupedChanges = {
NodeRemoved: [],
EdgeRemoved: [],
NodeAdded: [],
EdgeAdded: [],
NodeDataUpdated: [],
EdgeUpdated: [],
EdgeDataUpdated: [],
};
changes.forEach((change) => {
const { type: changeType } = change;
groupedChanges[changeType].push(change);
});
const { itemMap } = this;
// change items according to the order of the keys in groupedChanges
// === 1. remove edges; 2. remove nodes ===
groupedChanges.EdgeRemoved.concat(groupedChanges.NodeRemoved).forEach(({ value }) => {
const { id } = value;
const item = itemMap[id];
if (item) {
item.destroy();
delete itemMap[id];
}
});
// === 3. add nodes ===
if (groupedChanges.NodeAdded.length) {
this.renderNodes(groupedChanges.NodeAdded.map((change) => change.value));
}
// === 4. add edges ===
if (groupedChanges.EdgeAdded.length) {
this.renderEdges(groupedChanges.EdgeAdded.map((change) => change.value));
}
// === 5. update nodes's data ===
// merge changes for each node
if (groupedChanges.NodeDataUpdated.length) {
const nodeUpdate = {};
groupedChanges.NodeDataUpdated.forEach((change) => {
const { id, propertyName, newValue, oldValue } = change;
nodeUpdate[id] = nodeUpdate[id] || { previous: {}, current: {} };
if (!propertyName) {
nodeUpdate[id] = {
isReplace: true, // whether replace the whole data
previous: oldValue,
current: newValue,
};
} else {
nodeUpdate[id].previous[propertyName] = oldValue;
nodeUpdate[id].current[propertyName] = newValue;
}
});
const edgeToUpdate = {};
Object.keys(nodeUpdate).forEach((id) => {
const { isReplace, previous, current } = nodeUpdate[id];
const item = itemMap[id];
const innerModel = graphCore.getNode(id);
item.update(innerModel, { previous, current }, isReplace);
const relatedEdgeInnerModels = graphCore.getRelatedEdges(id);
relatedEdgeInnerModels.forEach((edge) => (edgeToUpdate[edge.id] = edge));
});
Object.keys(edgeToUpdate).forEach((id) => {
const item = itemMap[id] as Edge;
item.forceUpdate();
});
}
// === 6. update edges' data ===
if (groupedChanges.EdgeDataUpdated.length) {
const edgeUpdate = {};
groupedChanges.EdgeDataUpdated.forEach((change) => {
const { id, propertyName, newValue, oldValue } = change;
edgeUpdate[id] = edgeUpdate[id] || { previous: {}, current: {} };
if (!propertyName) {
edgeUpdate[id] = {
isReplace: true, // whether replace the whole data
previous: oldValue,
current: newValue,
};
} else {
edgeUpdate[id].previous[propertyName] = oldValue;
edgeUpdate[id].current[propertyName] = newValue;
}
});
Object.keys(edgeUpdate).forEach((id) => {
const { isReplace, current, previous } = edgeUpdate[id];
const item = itemMap[id];
const innerModel = graphCore.getEdge(id);
item.update(innerModel, { current, previous }, isReplace);
});
}
// === 7. update edges' source target ===
if (groupedChanges.EdgeUpdated.length) {
const edgeUpdate = {};
groupedChanges.EdgeUpdated.forEach((change) => {
// propertyName is 'source' or 'target'
const { id, propertyName, newValue } = change;
edgeUpdate[id] = edgeUpdate[id] || {};
edgeUpdate[id][propertyName] = newValue;
});
Object.keys(edgeUpdate).forEach((id) => {
const { source, target } = edgeUpdate[id];
const item = itemMap[id] as Edge;
if (source !== undefined) item.updateEnd('source', this.itemMap[source] as Node);
if (target !== undefined) item.updateEnd('target', this.itemMap[target] as Node);
});
}
}
/**
* The listener for item state changing.
* @param param
* {
* ids: ids of the items to be set state
* states: state names to set
* value: state value
* }
*/
private onItemStateChange(param: { ids: ID[], states: string[], value: boolean }) {
const { ids, states, value } = param;
ids.forEach(id => {
const item = this.itemMap[id];
if (!item) {
console.warn(`Fail to set state for item ${id}, which is not exist.`);
return;
}
if (!states || !value) {
// clear all the states
item.clearStates(states);
} else {
states.forEach(state => item.setState(state, value));
}
});
}
/**
* Create nodes with inner data to canvas.
* @param models nodes' inner datas
*/
private renderNodes(models: NodeModel[]) {
const { nodeExtensions, nodeGroup } = this;
models.forEach((node) => {
// TODO: get mapper from theme controller which is analysed from graph spec;
this.itemMap[node.id] = new Node({
model: node,
renderExtensions: nodeExtensions,
containerGroup: nodeGroup,
mapper: this.nodeMapper,
stateMapper: this.nodeStateMapper,
});
});
}
/**
* Create edges with inner data to canvas.
* @param models edges' inner datas
*/
private renderEdges(models: EdgeModel[]) {
const { edgeExtensions, edgeGroup, itemMap } = this;
models.forEach((edge) => {
const { source, target, id } = edge;
const sourceItem = itemMap[source] as Node;
const targetItem = itemMap[target] as Node;
if (!sourceItem) {
console.warn(
`The source node ${source} is not exist in the graph for edge ${id}, please add the node first`,
);
}
if (!targetItem) {
console.warn(
`The source node ${source} is not exist in the graph for edge ${id}, please add the node first`,
);
}
itemMap[id] = new Edge({
model: edge,
renderExtensions: edgeExtensions,
containerGroup: edgeGroup,
mapper: this.edgeMapper,
stateMapper: this.edgeStateMapper,
sourceItem,
targetItem,
});
});
}
/**
* Get the id of the item which have the state with true value
* @param itemType item's type
* @param state state name
* @param value state value, true by default
* @returns
*/
public findIdByState(itemType: ITEM_TYPE, state: string, value: string | boolean = true) {
const ids = [];
Object.values(this.itemMap).forEach(item => {
if (item.getType() !== itemType) return;
if (item.hasState(state) === value) ids.push(item.getID());
});
return ids;
}
/**
* Get the state value for the item with id
* @param id item' id
* @param state state name
* @returns {boolean | string} the state value
*/
public getItemState(id: ID, state: string) {
const item = this.itemMap[id];
if (!item) {
console.warn(`Fail to item state, the item with id ${id} does not exist.`);
return false;
}
return item.hasState(state);
}
}

View File

@ -1,22 +1,49 @@
import EventEmitter from '@antv/event-emitter';
import { clone, isArray, isObject } from '@antv/util';
import { ComboUserModel, EdgeUserModel, GraphData, IGraph, NodeUserModel, Specification } from '../types';
import { Canvas } from '@antv/g';
import { GraphChange, ID } from '@antv/graphlib';
import { isArray, isNumber, isObject, isString } from '@antv/util';
import {
ComboUserModel,
EdgeUserModel,
GraphData,
IGraph,
NodeUserModel,
Specification,
} from '../types';
import { AnimateCfg } from '../types/animate';
import { BehaviorObjectOptionsOf, BehaviorOptionsOf, BehaviorRegistry } from '../types/behavior';
import { ComboModel } from '../types/combo';
import { Padding, Point } from '../types/common';
import { GraphCore } from '../types/data';
import { EdgeModel } from '../types/edge';
import { EdgeModel, EdgeModelData } from '../types/edge';
import { Hooks } from '../types/hook';
import { ITEM_TYPE } from '../types/item';
import { LayoutCommonConfig } from '../types/layout';
import { NodeModel } from '../types/node';
import { NodeModel, NodeModelData } from '../types/node';
import { FitViewRules, GraphAlignment } from '../types/view';
import { DataController, InteractionController, ItemController, LayoutController, ThemeController, ExtensionController } from './controller';
import { createCanvas } from '../util/canvas';
import {
DataController,
InteractionController,
ItemController,
LayoutController,
ThemeController,
ExtensionController,
} from './controller';
import Hook from './hooks';
export default class Graph<B extends BehaviorRegistry> extends EventEmitter implements IGraph<B> {
public hooks: Hooks;
// for nodes and edges, which will be separate into groups
public canvas: Canvas;
// the tag to indicate whether the graph instance is destroyed
public destroyed: boolean;
// for background shapes, e.g. grid, pipe indices
private backgroundCanvas: Canvas;
// for transient shapes for interactions, e.g. transient node and related edges while draging, delegates
private transientCanvas: Canvas;
// the tag indicates all the three canvases are all ready
private canvasReady: boolean;
private specification: Specification<B>;
private dataController: DataController;
private interactionController: InteractionController;
@ -31,7 +58,14 @@ export default class Graph<B extends BehaviorRegistry> extends EventEmitter impl
this.specification = spec;
this.initHooks();
this.initCanvas();
this.initControllers();
const { data } = spec;
if (data) {
// TODO: handle multiple type data configs
this.read(data as GraphData);
}
}
/**
@ -46,23 +80,44 @@ export default class Graph<B extends BehaviorRegistry> extends EventEmitter impl
this.extensionController = new ExtensionController(this);
}
private initCanvas() {
const { renderer, container, width, height } = this.specification;
let rendererType;
let pixelRatio;
if (renderer && !isString(renderer)) {
rendererType = renderer.type || 'canvas';
pixelRatio = renderer.pixelRatio;
} else {
rendererType = renderer || 'canvas';
}
this.backgroundCanvas = createCanvas(rendererType, container, width, height, pixelRatio);
this.canvas = createCanvas(rendererType, container, width, height, pixelRatio);
this.transientCanvas = createCanvas(rendererType, container, width, height, pixelRatio);
Promise.all(
[this.backgroundCanvas, this.canvas, this.transientCanvas].map((canvas) => canvas.ready),
).then(() => (this.canvasReady = true));
}
/**
* Initialize the hooks for graph's lifecycles.
*/
private initHooks() {
this.hooks = {
init: new Hook<void>({ name: 'init' }),
datachange: new Hook<{ data: GraphData }>({ name: 'datachange' }),
additems: new Hook<{ type: ITEM_TYPE, models: NodeUserModel[] | EdgeUserModel[] | ComboUserModel[] }>({ name: 'additems' }),
removeitems: new Hook<{ type: ITEM_TYPE, ids: (string | number)[] }>({ name: 'removeitems' }),
updateitems: new Hook<{ type: ITEM_TYPE, models: NodeUserModel[] | EdgeUserModel[] | ComboUserModel[] }>({ name: 'updateitems' }),
datachange: new Hook<{ data: GraphData; type: 'replace' }>({ name: 'datachange' }),
itemchange: new Hook<{
type: ITEM_TYPE;
changes: GraphChange<NodeModelData, EdgeModelData>[];
graphCore: GraphCore;
}>({ name: 'itemchange' }),
render: new Hook<{ graphCore: GraphCore }>({ name: 'render' }),
modechange: new Hook<{ mode: string }>({ name: 'modechange' }),
behaviorchange: new Hook<{
action: 'update' | 'add' | 'remove',
modes: string[],
behaviors: BehaviorOptionsOf<{}>[]
action: 'update' | 'add' | 'remove';
modes: string[];
behaviors: BehaviorOptionsOf<{}>[];
}>({ name: 'behaviorchange' }),
itemstatechange: new Hook<{ ids: ID[], state: string, value: boolean }>({ name: 'itemstatechange' })
};
}
@ -78,26 +133,51 @@ export default class Graph<B extends BehaviorRegistry> extends EventEmitter impl
* @returns graph specs
*/
public getSpecification(): Specification<B> {
return clone(this.specification);
return this.specification;
}
/**
* Input data and render the graph.
* If there is old data, diffs and changes it.
* @param data
* @returns
* @param data
* @returns
* @group Data
*/
public read(data: GraphData) {
this.hooks.datachange.emit({ data });
this.hooks.datachange.emit({ data, type: 'replace' });
const emitRender = () => {
this.hooks.render.emit({
graphCore: this.dataController.graphCore,
});
this.emit('afterrender');
};
if (this.canvasReady) {
emitRender();
} else {
Promise.all(
[this.backgroundCanvas, this.canvas, this.transientCanvas].map((canvas) => canvas.ready),
).then(emitRender);
}
}
/**
* Change graph data.
* @param data new data
* @param type the way to change data, 'replace' means discard the old data and use the new one; 'mergeReplace' means merge the common part, remove (old - new), add (new - old)
* @returns
* @group Data
*/
public changeData(data: GraphData, type: 'replace' | 'mergeReplace' = 'mergeReplace') {
this.hooks.datachange.emit({ data, type });
this.hooks.render.emit({
graphCore: this.dataController.graphCore
graphCore: this.dataController.graphCore,
});
this.emit('afterrender');
}
/**
* Clear the graph, means remove all the items on the graph.
* @returns
* @returns
*/
public clear() {
// TODO
@ -108,7 +188,7 @@ export default class Graph<B extends BehaviorRegistry> extends EventEmitter impl
* @param dx x of the relative vector
* @param dy y of the relative vector
* @param animateCfg animation configurations
* @returns
* @returns
* @group View
*/
public move(dx: number, dy: number, animateCfg?: AnimateCfg) {
@ -121,7 +201,7 @@ export default class Graph<B extends BehaviorRegistry> extends EventEmitter impl
* @param y position on the canvas to align
* @param alignment alignment of the graph content
* @param animateCfg animation configurations
* @returns
* @returns
* @group View
*/
public moveTo(x: number, y: number, alignment: GraphAlignment, animateCfg?: AnimateCfg) {
@ -133,7 +213,7 @@ export default class Graph<B extends BehaviorRegistry> extends EventEmitter impl
* @param ratio relative ratio to zoom
* @param center zoom center
* @param animateCfg animation configurations
* @returns
* @returns
* @group View
*/
public zoom(ratio: number, center?: Point, animateCfg?: AnimateCfg) {
@ -145,7 +225,7 @@ export default class Graph<B extends BehaviorRegistry> extends EventEmitter impl
* @param toRatio specified ratio
* @param center zoom center
* @param animateCfg animation configurations
* @returns
* @returns
* @group View
*/
public zoomTo(toRatio: number, center?: Point, animateCfg?: AnimateCfg) {
@ -157,7 +237,7 @@ export default class Graph<B extends BehaviorRegistry> extends EventEmitter impl
* @param padding padding while fitting
* @param rules rules for fitting
* @param animateCfg animation configurations
* @returns
* @returns
* @group View
*/
public fitView(padding?: Padding, rules?: FitViewRules, animateCfg?: AnimateCfg) {
@ -166,7 +246,7 @@ export default class Graph<B extends BehaviorRegistry> extends EventEmitter impl
/**
* Fit the graph center to the view center.
* @param animateCfg animation configurations
* @returns
* @returns
* @group View
*/
public fitCenter(animateCfg?: AnimateCfg) {
@ -176,41 +256,68 @@ export default class Graph<B extends BehaviorRegistry> extends EventEmitter impl
* Move the graph to make the item align the view center.
* @param item node/edge/combo item or its id
* @param animateCfg animation configurations
* @returns
* @returns
* @group View
*/
public focusItem(ids: string | number | (string | number)[], animateCfg?: AnimateCfg) {
public focusItem(ids: ID | ID[], animateCfg?: AnimateCfg) {
// TODO
}
// ===== item operations =====
/**
* Find a node's inner data according to id or function.
* @param { string | Function} condition id or condition function
* @returns result node
* @group Item
* @param { ID | Function } condition id or condition function
* @returns result node's inner data
* @group Data
*/
public getNodeData(condition: string | Function): NodeModel | undefined {
return this.dataController.findData('node', condition);
public getNodeData(condition: ID | Function): NodeModel | undefined {
const conds = isString(condition) || isNumber(condition) ? [condition] : condition;
return this.dataController.findData('node', conds)?.[0] as NodeModel;
}
/**
* Find an edge's inner data according to id or function.
* @param { string | Function} condition id or condition function
* @returns result edge
* @group Item
* @param { ID | Function } condition id or condition function
* @returns result edge's inner data
* @group Data
*/
public getEdgeData(condition: string | Function): EdgeModel | undefined {
return this.dataController.findData('edge', condition);
public getEdgeData(condition: ID | Function): EdgeModel | undefined {
const conds =
isString(condition) || isNumber(condition) || isNumber(condition) ? [condition] : condition;
return this.dataController.findData('edge', conds)?.[0] as EdgeModel;
}
/**
* Find an combo's inner data according to id or function.
* @param { string | Function} condition id or condition function
* @returns result combo
* @group Item
* @param { ID | Function } condition id or condition function
* @returns result combo's inner data
* @group Data
*/
public getComboData(condition: string | Function): ComboModel | undefined {
return this.dataController.findData('combo', condition);
public getComboData(condition: ID | Function): ComboModel | undefined {
const conds = isString(condition) || isNumber(condition) ? [condition] : condition;
return this.dataController.findData('combo', conds)?.[0] as ComboModel;
}
/**
* Get all the nodes' inner data
* @returns all nodes' inner data on the graph
* @group Data
*/
public getAllNodesData(): NodeModel[] {
return this.dataController.findAllData('node');
}
/**
* Get all the edges' inner data
* @returns all edges' inner data on the graph
* @group Data
*/
public getAllEdgesData(): EdgeModel[] {
return this.dataController.findAllData('edge') as EdgeModel[];
}
/**
* Get all the combos' inner data
* @returns all combos' inner data on the graph
* @group Data
*/
public getAllCombosData(): ComboModel[] {
return this.dataController.findAllData('combo') as ComboModel[];
}
/**
* Find items which has the state.
@ -220,74 +327,146 @@ export default class Graph<B extends BehaviorRegistry> extends EventEmitter impl
* @returns items that is the type and has the state
* @group Item
*/
public findIdByState(itemType: ITEM_TYPE, state: string, additionalFilter?: (item: NodeModel | EdgeModel | ComboModel) => boolean): (string | number)[] {
// TODO
return;
public findIdByState(
itemType: ITEM_TYPE,
state: string,
value: string | boolean = true,
additionalFilter?: (item: NodeModel | EdgeModel | ComboModel) => boolean,
): ID[] {
let ids = this.itemController.findIdByState(itemType, state, value);
if (additionalFilter) {
const getDataFunc = itemType === 'node' ? this.getNodeData : this.getEdgeData; // TODO: combo
ids = ids.filter(id => additionalFilter(getDataFunc(id)));
}
return ids;
}
/**
* Add an item or items to the graph.
* Add one or more node/edge/combo data to the graph.
* @param itemType item type
* @param model user data
* @param stack whether push this operation to stack
* @returns whether success
* @group Item
* @group Data
*/
public addItem(itemType: ITEM_TYPE, models: NodeUserModel | EdgeUserModel | ComboUserModel | NodeUserModel[] | EdgeUserModel[] | ComboUserModel[], stack?: boolean): boolean {
public addData(
itemType: ITEM_TYPE,
models:
| NodeUserModel
| EdgeUserModel
| ComboUserModel
| NodeUserModel[]
| EdgeUserModel[]
| ComboUserModel[],
stack?: boolean,
): NodeModel | EdgeModel | ComboModel | NodeModel[] | EdgeModel[] | ComboModel[] {
// data controller and item controller subscribe additem in order
const modelArr = isArray(models) ? models : [models];
this.hooks.additems.emit({
type: itemType,
models: modelArr
const { graphCore } = this.dataController;
graphCore.once('changed', (event) => {
this.hooks.itemchange.emit({
type: itemType,
changes: graphCore.reduceChanges(event.changes),
graphCore,
});
});
return this.dataController.findData(itemType, modelArr.map(model => model.id)).every(Boolean);
};
const modelArr = isArray(models) ? models : [models];
const data = { nodes: [], edges: [], combos: [] };
data[`${itemType}s`] = modelArr;
this.hooks.datachange.emit({
data,
type: 'union',
});
const dataList = this.dataController.findData(
itemType,
modelArr.map((model) => model.id),
);
return isArray(models) ? dataList : dataList[0];
}
/**
* Remove an item or items from the graph.
* Remove one or more node/edge/combo data from the graph.
* @param item the item to be removed
* @param stack whether push this operation to stack
* @returns whether success
* @group Item
* @group Data
*/
public removeItem(itemType: ITEM_TYPE, ids: string | number | (string | number)[], stack?: boolean): boolean {
const idArr = isArray(ids) ? ids : [ids]
this.hooks.removeitems.emit({
type: itemType,
ids: idArr
public removeData(itemType: ITEM_TYPE, ids: ID | ID[], stack?: boolean) {
const idArr = isArray(ids) ? ids : [ids];
const data = { nodes: [], edges: [], combos: [] };
const { userGraphCore, graphCore } = this.dataController;
const getItem = itemType === 'edge' ? userGraphCore.getEdge : userGraphCore.getNode; // TODO: combo
data[`${itemType}s`] = idArr.map((id) => getItem.bind(userGraphCore)(id));
graphCore.once('changed', (event) => {
this.hooks.itemchange.emit({
type: itemType,
changes: event.changes,
graphCore,
});
});
this.hooks.datachange.emit({
data,
type: 'remove',
});
return this.dataController.findData(itemType, idArr).every(Boolean);
}
/**
* Update an item or items on the graph.
* Update one or more node/edge/combo data on the graph.
* @param {Item} item item or id
* @param {EdgeConfig | NodeConfig} cfg incremental updated configs
* @param {boolean} stack true
* @group Item
* @group Data
*/
public updateItem(itemType: ITEM_TYPE, models: Partial<NodeUserModel> | Partial<EdgeUserModel> | Partial<ComboUserModel | Partial<NodeUserModel>[] | Partial<EdgeUserModel>[] | Partial<ComboUserModel>[]>, stack?: boolean): boolean {
public updateData(
itemType: ITEM_TYPE,
models:
| Partial<NodeUserModel>
| Partial<EdgeUserModel>
| Partial<
| ComboUserModel
| Partial<NodeUserModel>[]
| Partial<EdgeUserModel>[]
| Partial<ComboUserModel>[]
>,
stack?: boolean,
): NodeModel | EdgeModel | ComboModel | NodeModel[] | EdgeModel[] | ComboModel[] {
const modelArr = isArray(models) ? models : [models];
this.hooks.updateitems.emit({
type: itemType,
models: modelArr
const data = { nodes: [], edges: [], combos: [] };
data[`${itemType}s`] = modelArr;
const { graphCore } = this.dataController;
graphCore.once('changed', (event) => {
this.hooks.itemchange.emit({
type: itemType,
changes: event.changes,
graphCore,
});
});
// TODO
return true;
this.hooks.datachange.emit({
data,
type: 'update',
});
const dataList = this.dataController.findData(
itemType,
modelArr.map((model) => model.id),
);
return isArray(models) ? dataList : dataList[0];
}
/**
* Show the item(s).
* @param item the item to be shown
* @returns
* @returns
* @group Item
*/
public showItem(ids: string | number | (string | number)[]) {
public showItem(ids: ID | ID[]) {
// TODO
}
/**
* Hide the item(s).
* @param item the item to be hidden
* @returns
* @returns
* @group Item
*/
public hideItem(ids: string | number | (string | number)[]) {
public hideItem(ids: ID | ID[]) {
// TODO
}
/**
@ -295,21 +474,52 @@ export default class Graph<B extends BehaviorRegistry> extends EventEmitter impl
* @param item the item to be set
* @param state the state name
* @param value state value
* @returns
* @returns
* @group Item
*/
public setItemState(ids: string | number | (string | number)[], state: string, value: boolean) {
// TODO
public setItemState(ids: ID | ID[], states: string | string[], value: boolean) {
const idArr = isArray(ids) ? ids : [ids];
const stateArr = isArray(states) ? states : [states];
this.hooks.itemstatechange.emit({
ids: idArr as ID[],
states: stateArr as string[],
value
});
}
/**
* Get the state value for an item.
* @param id the id for the item
* @param states the state name
* @returns {boolean} the state value
* @group Item
*/
public getItemState(id: ID, state: string) {
return this.itemController.getItemState(id, state);
}
/**
* Clear all the states for item(s).
* @param ids the id(s) for the item(s) to be clear
* @param states the states' names, all the states wil be cleared if states is not assigned
* @returns
* @group Item
*/
public clearItemState(ids: ID | ID[], states?: string[]) {
const idArr = isArray(ids) ? ids : [ids];
this.hooks.itemstatechange.emit({
ids: idArr as ID[],
states,
value: false
});
}
// ===== combo operations =====
/**
* Create a new combo with existing child nodes and combos.
* @param combo combo ID or Combo model
* @param childrenIds id array of children of the new combo
* @group Combo
*/
* Create a new combo with existing child nodes and combos.
* @param combo combo ID or Combo model
* @param childrenIds id array of children of the new combo
* @group Combo
*/
public createCombo(combo: string | ComboUserModel, childrenIds: string[], stack?: boolean) {
// TODO
}
@ -318,27 +528,26 @@ export default class Graph<B extends BehaviorRegistry> extends EventEmitter impl
* @param {String | ICombo} item combo item or id to be dissolve
* @group Combo
*/
public uncombo(comboId: string | number, stack?: boolean) {
public uncombo(comboId: ID, stack?: boolean) {
// TODO
}
/**
* Collapse a combo.
* @param comboId combo id or item
* @group Combo
*/
public collapseCombo(comboId: string | number, stack?: boolean) {
* Collapse a combo.
* @param comboId combo id or item
* @group Combo
*/
public collapseCombo(comboId: ID, stack?: boolean) {
// TODO
}
/**
* Expand a combo.
* @param combo combo ID combo
* @group Combo
*/
public expandCombo(comboId: string | number, stack?: boolean) {
* Expand a combo.
* @param combo combo ID combo
* @group Combo
*/
public expandCombo(comboId: ID, stack?: boolean) {
// TODO
}
// ===== layout =====
/**
* Layout the graph (with current configurations if cfg is not assigned).
@ -348,16 +557,20 @@ export default class Graph<B extends BehaviorRegistry> extends EventEmitter impl
* @param {boolean} stack push it into stack
* @group Layout
*/
public layout(cfg?: LayoutCommonConfig, align?: GraphAlignment, canvasPoint?: Point, stack?: boolean) {
public layout(
cfg?: LayoutCommonConfig,
align?: GraphAlignment,
canvasPoint?: Point,
stack?: boolean,
) {
// TODO: LayoutConfig combination instead of LayoutCommonConfig
// TODO
}
/**
* Switch mode.
* @param mode mode name
* @returns
* @returns
* @group Interaction
*/
public setMode(mode: string) {
@ -368,7 +581,7 @@ export default class Graph<B extends BehaviorRegistry> extends EventEmitter impl
* Add behavior(s) to mode(s).
* @param behaviors behavior names or configs
* @param modes mode names
* @returns
* @returns
* @group Interaction
*/
public addBehaviors(behaviors: BehaviorOptionsOf<B>[], modes: string | string[]) {
@ -377,10 +590,10 @@ export default class Graph<B extends BehaviorRegistry> extends EventEmitter impl
this.hooks.behaviorchange.emit({
action: 'add',
modes: modesArr,
behaviors: behaviorsArr
behaviors: behaviorsArr,
});
// update the graph specification
modesArr.forEach(mode => {
modesArr.forEach((mode) => {
this.specification.modes[mode] = this.specification.modes[mode].concat(behaviorsArr);
});
}
@ -388,7 +601,7 @@ export default class Graph<B extends BehaviorRegistry> extends EventEmitter impl
* Remove behavior(s) from mode(s).
* @param behaviors behavior configs with unique key
* @param modes mode names
* @returns
* @returns
* @group Interaction
*/
public removeBehaviors(behaviorKeys: string[], modes: string | string[]) {
@ -396,22 +609,24 @@ export default class Graph<B extends BehaviorRegistry> extends EventEmitter impl
this.hooks.behaviorchange.emit({
action: 'remove',
modes: modesArr,
behaviors: behaviorKeys
behaviors: behaviorKeys,
});
// update the graph specification
modesArr.forEach(mode => {
behaviorKeys.forEach(key => {
const oldBehavior = this.specification.modes[mode].find(behavior => isObject(behavior) && behavior.key === key);
modesArr.forEach((mode) => {
behaviorKeys.forEach((key) => {
const oldBehavior = this.specification.modes[mode].find(
(behavior) => isObject(behavior) && behavior.key === key,
);
const indexOfOldBehavior = this.specification.modes[mode].indexOf(oldBehavior);
this.specification.modes[mode].splice(indexOfOldBehavior, 1);
})
});
});
}
/**
* Update a behavior on a mode.
* @param behavior behavior configs, whose name indicates the behavior to be updated
* @param mode mode name
* @returns
* @returns
* @group Interaction
*/
public updateBehavior(behavior: BehaviorObjectOptionsOf<B>, mode?: string) {
@ -420,11 +635,32 @@ export default class Graph<B extends BehaviorRegistry> extends EventEmitter impl
modes: [mode],
behaviors: [behavior],
});
// no need to update specification since the corresponding part is the same object as the behavior's option
// no need to update specification since the corresponding part is the same object as the behavior's option
// this.specification.modes[mode].forEach((b, i) => {
// if (isObject(b) && b.key === behavior.key) {
// this.specification.modes[mode][i] = behavior;
// }
// });
}
}
/**
* Destroy the graph instance and remove the related canvases.
* @returns
* @group Graph Instance
*/
public destroy() {
this.canvas.destroy();
this.backgroundCanvas.destroy();
this.transientCanvas.destroy();
// TODO: destroy controllers and off the listeners
// this.dataController.destroy();
// this.interactionController.destroy();
// this.layoutController.destroy();
// this.themeController.destroy();
// this.itemController.destroy();
// this.extensionController.destroy();
this.destroyed = true;
}
}

View File

@ -1,6 +1,8 @@
import { comboFromNode } from "./data/comboFromNode"
import DragCanvas from "./behavior/drag-canvas";
import { Lib } from "../types/stdlib";
import { CircleNode } from "./item/node";
import { LineEdge } from "./item/edge";
const stdLib = {
transforms: {
@ -10,11 +12,15 @@ const stdLib = {
layouts: {}, // from @antv/layout
behaviors: {
'drag-canvas': DragCanvas
}, // @antv/g6-pc
plugins: {}, // @antv/g6-plugin
nodes: {}, // @antv/g6-element
edges: {}, // @antv/g6-element
combos: {}, // @antv/g6-element
},
plugins: {},
nodes: {
'circle-node': CircleNode
},
edges: {
'line-edge': LineEdge
},
combos: {},
}
const useLib: Lib = {

View File

@ -0,0 +1,231 @@
import { DisplayObject, Line, Polyline } from '@antv/g';
import { isNumber } from '@antv/util';
import { Point } from '../../../types/common';
import {
EdgeDisplayModel,
EdgeLabelShapeStyle,
EdgeModelData,
EdgeShapeMap,
} from '../../../types/edge';
import { ShapeStyle, State } from '../../../types/item';
import {
DEFAULT_LABEL_BG_PADDING,
DEFAULT_SHAPE_STYLE,
DEFAULT_TEXT_STYLE,
formatPadding,
upsertShape,
} from '../../../util/shape';
export abstract class BaseEdge {
type: string;
baseDefaultStyles: {
keyShape: ShapeStyle;
labelShape: EdgeLabelShapeStyle;
iconShape: ShapeStyle;
[shapeId: string]: ShapeStyle;
} = {
keyShape: {
...DEFAULT_SHAPE_STYLE,
lineWidth: 1,
stroke: '#fff',
},
labelShape: {
...DEFAULT_TEXT_STYLE,
fill: '#000',
},
iconShape: {
...DEFAULT_TEXT_STYLE,
img: 'https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*wAmHQJbNVdwAAAAAAAAAAABkARQnAQ',
width: 15,
height: 15,
},
};
defaultStyles: {
keyShape?: ShapeStyle;
labelShape?: EdgeLabelShapeStyle;
iconShape?: ShapeStyle;
[shapeId: string]: ShapeStyle;
} = {};
protected getDefaultStyles() {
return this.defaultStyles;
}
abstract draw(
model: EdgeDisplayModel,
sourcePoint: Point,
targetPoint: Point,
shapeMap: { [shapeId: string]: DisplayObject },
diffData?: { previous: EdgeModelData; current: EdgeModelData },
diffState?: { previous: State[], current: State[] }
): {
keyShape: DisplayObject;
labelShape?: DisplayObject;
iconShape?: DisplayObject;
[otherShapeId: string]: DisplayObject;
};
public afterDraw(
model: EdgeDisplayModel,
shapeMap: { [shapeId: string]: DisplayObject },
shapesChanged?: string[],
): { [otherShapeId: string]: DisplayObject } {
return {};
}
// shouldUpdate: (model: EdgeDisplayModel, prevModel: EdgeDisplayModel) => boolean = () => true;
public setState: (
name: string,
value: boolean,
shapeMap: { [shapeId: string]: DisplayObject },
) => void;
public drawLabelShape(
model: EdgeDisplayModel,
shapeMap: EdgeShapeMap,
diffData?: { previous: EdgeModelData; current: EdgeModelData },
diffState?: { previous: State[], current: State[] }
): {
labelShape: DisplayObject;
[id: string]: DisplayObject;
} {
const { keyShape } = shapeMap;
const shapeStyle = Object.assign({}, model.data?.labelShape);
const {
position,
background,
offsetX: propsOffsetX,
offsetY: propsOffsetY,
autoRotate = true,
...otherStyle
} = shapeStyle;
const positionPreset = {
textAlign: 'center',
offsetX: 0,
offsetY: 0,
pointRatio: [0.5, 0.501],
};
if (isNumber(position)) {
positionPreset.pointRatio = [position, position + 0.01];
}
switch (position) {
case 'start':
positionPreset.pointRatio = [0, 0.01];
positionPreset.textAlign = 'left';
positionPreset.offsetX = 4;
break;
case 'end':
positionPreset.pointRatio = [0.99, 1];
positionPreset.textAlign = 'right';
positionPreset.offsetX = -4;
break;
default: // at middle by default
break;
}
const point = (keyShape as Line | Polyline).getPoint(positionPreset.pointRatio[0]);
let positionStyle: any = { x: point.x, y: point.y };
if (autoRotate) {
const pointOffset = (keyShape as Line | Polyline).getPoint(positionPreset.pointRatio[1]);
const angle = Math.atan((point.y - pointOffset.y) / (point.x - pointOffset.x)); // TODO: NaN
const offsetX = propsOffsetX === undefined ? positionPreset.offsetX : propsOffsetX;
const offsetY = propsOffsetY === undefined ? positionPreset.offsetY : propsOffsetY;
// the projection is |offsetX| away from point, along the tangent line of the keyShape's path at point
const projection = {
x: point.x + offsetX * Math.cos(angle),
y: point.y + offsetX * Math.sin(angle),
};
// the position of the text is |offsetY| away from projection, along the vertical line of the tangent line at point
positionStyle = {
x: projection.x + offsetY * Math.cos(Math.PI - angle),
y: projection.y + offsetY * Math.sin(Math.PI - angle),
transform: `rotate(${(angle / Math.PI) * 180})`,
};
}
const style = {
...this.defaultStyles.labelShape,
textAlign: positionPreset.textAlign,
...positionStyle,
...otherStyle,
};
const labelShape = upsertShape('text', 'labelShape', style, shapeMap);
const shapes = { labelShape };
if (background) {
const textBBox = labelShape.getGeometryBounds();
const { padding: propsPadding, ...backgroundStyle } = background;
const padding = formatPadding(propsPadding, DEFAULT_LABEL_BG_PADDING);
const bgStyle = {
fill: '#fff',
radius: 4,
...backgroundStyle,
x: textBBox.min[0] - padding[3] + style.x,
y: textBBox.min[1] - padding[0] + style.y,
width: textBBox.max[0] - textBBox.min[0] + padding[1] + padding[3],
height: textBBox.max[1] - textBBox.min[1] + padding[0] + padding[2],
transform: positionStyle.transform,
transformOrigin: 'center',
};
if (position === 'start') {
bgStyle.transformOrigin = `${padding[3]} ${padding[0] + bgStyle.height / 2}`;
}
if (position === 'end') {
bgStyle.transformOrigin = `${padding[3] + bgStyle.width} ${
padding[0] + bgStyle.height / 2
}`;
}
shapes['labelBgShape'] = upsertShape('rect', 'labelBgShape', bgStyle, shapeMap);
}
return shapes;
}
public drawIconShape(
model: EdgeDisplayModel,
shapeMap: EdgeShapeMap,
diffData?: { previous: EdgeModelData; current: EdgeModelData },
diffState?: { previous: State[], current: State[] }
): DisplayObject {
const { labelShape, labelBgShape, keyShape } = shapeMap;
const { iconShape: propsStyle, labelShape: labelShapeProps } = model.data || {};
const shapeStyle = Object.assign({}, this.defaultStyles.iconShape, propsStyle);
const iconShapeType = shapeStyle.text ? 'text' : 'image';
if (iconShapeType === 'text') {
shapeStyle.textAlign = 'left';
shapeStyle.textBaseline = 'top';
}
const { width, height, fontSize } = shapeStyle;
const w = (width || fontSize) as number;
const h = (height || fontSize) as number;
if (labelShapeProps) {
const referShape = labelBgShape || labelShape;
const { min: referMin, halfExtents: referHalExtents } = referShape.getGeometryBounds();
const {
x: referX,
y: referY,
transform: referTransform,
textAlign: labelAlign,
} = referShape.attributes;
shapeStyle.x = referMin[0] - w - 4 + referX;
shapeStyle.y = referMin[1] + 2 + referY;
if (referTransform) {
shapeStyle.transform = referTransform;
if (labelAlign === 'right') {
shapeStyle.transformOrigin = `${w + 4 + referHalExtents[0] * 2}px ${h / 2}px`;
} else if (labelAlign === 'left') {
shapeStyle.transformOrigin = `${w + 4}px ${h / 2}px`;
} else {
// labelShape align 'center'
shapeStyle.transformOrigin = `${w + 4 + referHalExtents[0]}px ${h / 2}px`;
}
}
} else {
const midPoint = (keyShape as Line | Polyline).getPoint(0.5);
shapeStyle.x = midPoint.x;
shapeStyle.y = midPoint.y;
// TODO: rotate
}
return upsertShape(iconShapeType, 'iconShape', shapeStyle, shapeMap);
}
}

View File

@ -0,0 +1 @@
export * from "./line";

View File

@ -0,0 +1,70 @@
import { Point } from '../../../types/common';
import { EdgeDisplayModel, EdgeModelData, EdgeShapeMap } from '../../../types/edge';
import { State } from '../../../types/item';
import { mergeStyles, upsertShape } from '../../../util/shape';
import { BaseEdge } from './base';
export class LineEdge extends BaseEdge {
public type = 'line-edge';
public defaultStyles = {
keyShape: {
x1: 0,
y1: 0,
x2: 0,
y2: 0,
stroke: '#ccc',
lineWidth: 1,
}
}
constructor() {
super();
// suggest to merge default styles like this to avoid style value missing
this.defaultStyles = mergeStyles(this.baseDefaultStyles, this.defaultStyles);
}
public draw(
model: EdgeDisplayModel,
sourcePoint: Point,
targetPoint: Point,
shapeMap: EdgeShapeMap,
diffData?: { previous: EdgeModelData; current: EdgeModelData },
diffState?: { previous: State[], current: State[] }
): EdgeShapeMap {
const { data = {} } = model;
let shapes: EdgeShapeMap = { keyShape: undefined };
shapes.keyShape = this.drawKeyShape(model, sourcePoint, targetPoint, shapeMap, diffData);
if (data.labelShape)
shapes = {
...shapes,
...this.drawLabelShape(model, shapeMap, diffData),
};
if (data.iconShape) shapes.iconShape = this.drawIconShape(model, shapeMap, diffData);
// TODO: other shapes
return shapes;
}
public drawKeyShape(
model: EdgeDisplayModel,
sourcePoint: Point,
targetPoint: Point,
shapeMap: EdgeShapeMap,
diffData?: { previous: EdgeModelData; current: EdgeModelData },
diffState?: { previous: State[], current: State[] }
) {
const keyShapeStyle = Object.assign({}, this.defaultStyles.keyShape, model.data?.keyShape);
const keyShape = upsertShape(
'line',
'keyShape',
{
...keyShapeStyle,
x1: sourcePoint.x,
y1: sourcePoint.y,
x2: targetPoint.x,
y2: targetPoint.y,
},
shapeMap,
);
return keyShape;
}
}

View File

@ -0,0 +1,205 @@
import { DisplayObject } from '@antv/g';
import { NodeDisplayModel } from '../../../types';
import { ShapeStyle, State } from '../../../types/item';
import { NodeLabelShapeStyle, NodeModelData, NodeShapeMap } from '../../../types/node';
import {
DEFAULT_LABEL_BG_PADDING,
DEFAULT_SHAPE_STYLE,
DEFAULT_TEXT_STYLE,
formatPadding,
upsertShape,
} from '../../../util/shape';
export abstract class BaseNode {
type: string;
baseDefaultStyles: {
keyShape: ShapeStyle;
labelShape: NodeLabelShapeStyle;
iconShape: ShapeStyle;
[shapeId: string]: ShapeStyle;
} = {
keyShape: {
...DEFAULT_SHAPE_STYLE,
x: 0,
y: 0,
fill: '#f00',
lineWidth: 0,
stroke: '#0f0',
},
labelShape: {
...DEFAULT_TEXT_STYLE,
fill: '#000',
position: 'bottom',
},
iconShape: {
...DEFAULT_TEXT_STYLE,
img: 'https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*wAmHQJbNVdwAAAAAAAAAAABkARQnAQ',
width: 15,
height: 15,
},
};
defaultStyles: {
keyShape?: ShapeStyle;
labelShape?: NodeLabelShapeStyle;
iconShape?: ShapeStyle;
[shapeId: string]: ShapeStyle;
} = {};
abstract draw(
model: NodeDisplayModel,
shapeMap: { [shapeId: string]: DisplayObject },
diffData?: { previous: NodeModelData; current: NodeModelData },
diffState?: { previous: State[], current: State[] }
): {
keyShape: DisplayObject;
labelShape?: DisplayObject;
iconShape?: DisplayObject;
[otherShapeId: string]: DisplayObject;
};
public afterDraw(
model: NodeDisplayModel,
shapeMap: { [shapeId: string]: DisplayObject },
shapesChanged?: string[],
): { [otherShapeId: string]: DisplayObject } {
return {};
}
// shouldUpdate: (model: NodeDisplayModel, prevModel: NodeDisplayModel) => boolean = () => true;
public setState: (
name: string,
value: boolean,
shapeMap: { [shapeId: string]: DisplayObject },
) => void;
abstract drawKeyShape(
model: NodeDisplayModel,
shapeMap: NodeShapeMap,
diffData?: { previous: NodeModelData; current: NodeModelData },
diffState?: { previous: State[], current: State[] }
): DisplayObject;
public drawLabelShape(
model: NodeDisplayModel,
shapeMap: NodeShapeMap,
diffData?: { previous: NodeModelData; current: NodeModelData },
diffState?: { oldState: State[], newState: State[] }
): {
labelShape: DisplayObject;
[id: string]: DisplayObject;
} {
const { keyShape } = shapeMap;
const keyShapeBox = keyShape.getGeometryBounds();
const shapeStyle = Object.assign({}, model.data?.labelShape);
const {
position,
background,
offsetX: propsOffsetX,
offsetY: propsOffsetY,
...otherStyle
} = shapeStyle;
const positionPreset = {
x: keyShapeBox.center[0],
y: keyShapeBox.max[1],
textBaseline: 'top',
textAlign: 'center',
offsetX: 0,
offsetY: 0,
};
switch (position) {
case 'center':
positionPreset.y = keyShapeBox.center[1];
break;
case 'top':
positionPreset.y = keyShapeBox.min[1];
positionPreset.textBaseline = 'bottom';
positionPreset.offsetY = -4;
break;
case 'left':
positionPreset.x = keyShapeBox.min[0];
positionPreset.y = keyShapeBox.center[1];
positionPreset.textAlign = 'right';
positionPreset.textBaseline = 'middle';
positionPreset.offsetX = -4;
break;
case 'right':
positionPreset.x = keyShapeBox.max[0];
positionPreset.y = keyShapeBox.center[1];
positionPreset.textAlign = 'left';
positionPreset.textBaseline = 'middle';
positionPreset.offsetX = 4;
break;
default: // at bottom by default
positionPreset.offsetY = 4;
break;
}
const offsetX = propsOffsetX === undefined ? positionPreset.offsetX : propsOffsetX;
const offsetY = propsOffsetY === undefined ? positionPreset.offsetY : propsOffsetY;
positionPreset.x += offsetX;
positionPreset.y += offsetY;
const style: any = {
...this.defaultStyles.labelShape,
...positionPreset,
...otherStyle,
};
const labelShape = upsertShape('text', 'labelShape', style, shapeMap);
const shapes = { labelShape };
if (background) {
const textBBox = labelShape.getGeometryBounds();
const { padding: propsPadding, ...backgroundStyle } = background;
const padding = formatPadding(propsPadding, DEFAULT_LABEL_BG_PADDING);
const bgStyle: any = {
fill: '#fff',
radius: 4,
...backgroundStyle,
x: textBBox.min[0] - padding[3] + style.x,
y: textBBox.min[1] - padding[0] + style.y,
width: textBBox.max[0] - textBBox.min[0] + padding[1] + padding[3],
height: textBBox.max[1] - textBBox.min[1] + padding[0] + padding[2],
};
if (style.stransform) {
bgStyle.transform = style.transform;
bgStyle.transformOrigin = 'center';
if (style.textAlign === 'left') {
bgStyle.transformOrigin = `${padding[3]} ${padding[0] + bgStyle.height / 2}`;
}
if (style.textAlign === 'right') {
bgStyle.transformOrigin = `${padding[3] + bgStyle.width} ${
padding[0] + bgStyle.height / 2
}`;
}
}
shapes['labelBgShape'] = upsertShape('rect', 'labelBgShape', bgStyle, shapeMap);
}
return shapes;
}
public drawIconShape(
model: NodeDisplayModel,
shapeMap: NodeShapeMap,
diffData?: { previous: NodeModelData; current: NodeModelData },
diffState?: { oldState: State[], newState: State[] }
): DisplayObject {
const { iconShape } = model.data || {};
const shapeStyle = Object.assign({}, this.defaultStyles.iconShape, iconShape);
const iconShapeType = shapeStyle.text ? 'text' : 'image';
if (iconShapeType === 'image') {
const { width, height } = shapeStyle;
if (!iconShape.hasOwnProperty('x')) shapeStyle.x = -width / 2;
if (!iconShape.hasOwnProperty('y')) shapeStyle.y = -height / 2;
} else {
shapeStyle.textAlign = 'center';
shapeStyle.textBaseline = 'middle';
}
return upsertShape(iconShapeType, 'iconShape', shapeStyle, shapeMap);
}
public drawOtherShapes(
model: NodeDisplayModel,
shapeMap: NodeShapeMap,
diffData?: { previous: NodeModelData; current: NodeModelData },
diffState?: { previous: State[], current: State[] }
): { [id: string]: DisplayObject; } {
return {}
}
}

View File

@ -0,0 +1,61 @@
import { DisplayObject } from '@antv/g';
import { NodeDisplayModel } from '../../../types';
import { State } from '../../../types/item';
import { NodeModelData, NodeShapeMap } from '../../../types/node';
import { mergeStyles, upsertShape } from '../../../util/shape';
import { BaseNode } from './base';
export class CircleNode extends BaseNode {
override defaultStyles = {
keyShape: {
r: 15,
x: 0,
y: 0,
fill: '#f00',
lineWidth: 0,
stroke: '#0f0',
}
};
constructor() {
super();
// suggest to merge default styles like this to avoid style value missing
this.defaultStyles = mergeStyles(this.baseDefaultStyles, this.defaultStyles);
}
public draw(
model: NodeDisplayModel,
shapeMap: NodeShapeMap,
diffData?: { previous: NodeModelData; current: NodeModelData },
diffState?: { previous: State[], current: State[] }
): NodeShapeMap {
const { data = {} } = model;
let shapes: NodeShapeMap = { keyShape: undefined };
shapes.keyShape = this.drawKeyShape(model, shapeMap, diffData);
if (data.labelShape) {
shapes = {
...shapes,
...this.drawLabelShape(model, shapeMap, diffData),
};
}
if (data.iconShape) {
shapes.iconShape = this.drawIconShape(model, shapeMap, diffData);
}
if (data.otherShapes && this.drawOtherShapes) {
shapes = {
...shapes,
...this.drawOtherShapes(model, shapeMap, diffData)
}
}
return shapes;
}
public drawKeyShape(
model: NodeDisplayModel,
shapeMap: NodeShapeMap,
diffData?: { previous: NodeModelData; current: NodeModelData },
diffState?: { previous: State[], current: State[] }
): DisplayObject {
const shapeStyle = Object.assign({}, this.defaultStyles.keyShape, model.data?.keyShape);
return upsertShape('circle', 'keyShape', shapeStyle, shapeMap);
}
}

View File

@ -0,0 +1 @@
export * from "./circle";

View File

@ -1,7 +1,7 @@
import { Node as GNode, PlainObject } from '@antv/graphlib';
import { AnimateAttr } from "./animate";
import { Padding } from "./common";
import { Encode, IItem, LabelBackground, ShapeAttrEncode, ShapesEncode } from "./item";
import { Encode, IItem, LabelBackground, ShapeAttrEncode, ShapesEncode, ShapeStyle } from "./item";
import { AnchorPoint } from "./node";
export type ComboLabelPosition = 'bottom' | 'top' | 'left' | 'left-top' | 'right' | 'ouside-top' | 'outside-left' | 'outside-right' | 'outside-bottom';
@ -18,29 +18,20 @@ export interface ComboModelData extends ComboUserModelData {
label?: string;
}
export interface ComboLabelShapeStyle extends ShapeStyle {
position?: ComboLabelPosition;
offsetX?: number;
offsetY?: number;
background?: LabelBackground;
}
/** Displayed data, only for drawing and not received by users. */
export interface ComboDisplayModelData extends ComboModelData {
keyShape?: {
[shapeAttr: string]: unknown;
animate: AnimateAttr;
};
labelShape?: {
position?: ComboLabelPosition;
offsetX?: number;
offsetY?: number;
background?: LabelBackground;
[shapeAttr: string]: unknown;
animate: AnimateAttr;
};
iconShape?: {
[shapeAttr: string]: unknown;
animate: AnimateAttr;
};
keyShape?: ShapeStyle;
labelShape?: ComboLabelShapeStyle;
iconShape?: ShapeStyle;
otherShapes?: {
[shapeName: string]: {
[shapeAttr: string]: unknown;
animate: AnimateAttr;
}
[shapeId: string]: ShapeStyle;
};
anchorPoints?: AnchorPoint[];
fixSize?: number | number[];

View File

@ -22,4 +22,6 @@ export interface FetchDataConfig {
export type GraphCore = IGraph<NodeModelData, EdgeModelData>;
export type DisplayGraphCore = IGraph<NodeDisplayModelData, EdgeDisplayModelData>;
export type TransformerFn = (data: GraphData) => GraphData
export type TransformerFn = (data: GraphData) => GraphData
export type DataChangeType = 'replace' | 'mergeReplace' | 'union' | 'remove' | 'update';

View File

@ -1,6 +1,7 @@
import { DisplayObject } from '@antv/g';
import { Edge as GEdge, PlainObject } from '@antv/graphlib';
import { AnimateAttr } from "./animate";
import { Encode, IItem, LabelBackground, ShapeAttrEncode, ShapesEncode } from "./item";
import { Encode, IItem, LabelBackground, ShapeAttrEncode, ShapesEncode, ShapeStyle } from "./item";
export interface EdgeUserModelData extends PlainObject { };
@ -8,29 +9,20 @@ export interface EdgeModelData extends EdgeUserModelData {
visible?: boolean;
label?: string;
};
export interface EdgeLabelShapeStyle extends ShapeStyle {
position?: EdgeLabelPosition;
offsetX?: number;
offsetY?: number;
background?: LabelBackground;
autoRotate?: boolean;
}
export interface EdgeDisplayModelData extends EdgeModelData {
keyShape?: {
[shapeAttr: string]: unknown;
animate: AnimateAttr;
};
labelShape?: {
position?: EdgeLabelPosition;
offsetX?: number;
offsetY?: number;
background?: LabelBackground;
autoRotate?: boolean;
[shapeAttr: string]: unknown;
animate: AnimateAttr;
};
iconShape?: {
[shapeAttr: string]: unknown;
animate: AnimateAttr;
};
keyShape?: ShapeStyle;
labelShape?: EdgeLabelShapeStyle;
iconShape?: ShapeStyle;
otherShapes?: {
[shapeName: string]: {
[shapeAttr: string]: unknown;
animate: AnimateAttr;
}
[shapeId: string]: ShapeStyle
};
sourceAnchor?: number;
targetAnchor?: number;
@ -64,6 +56,13 @@ export interface EdgeEncode extends EdgeShapesEncode {
type?: string | Encode<string>;
}
export interface EdgeShapeMap {
keyShape: DisplayObject,
labelShape?: DisplayObject,
iconShape?: DisplayObject,
[otherShapeId: string]: DisplayObject
}
// TODO
export interface IEdge extends IItem {

View File

@ -1,10 +1,12 @@
import EventEmitter from '@antv/event-emitter';
import { Canvas } from '@antv/g';
import { ID } from '@antv/graphlib';
import { Hooks } from '../types/hook';
import { AnimateCfg } from './animate';
import { BehaviorObjectOptionsOf, BehaviorOptionsOf, BehaviorRegistry } from './behavior';
import { ComboModel, ComboUserModel } from './combo';
import { Padding, Point } from './common';
import { GraphData } from './data';
import { DataChangeType, GraphData } from './data';
import { EdgeModel, EdgeUserModel } from './edge';
import { ITEM_TYPE } from './item';
import { LayoutCommonConfig } from './layout';
@ -13,9 +15,17 @@ import { Specification } from './spec';
import { FitViewRules, GraphAlignment } from './view';
export interface IGraph<B extends BehaviorRegistry = BehaviorRegistry> extends EventEmitter {
hooks: Hooks;
canvas: Canvas;
destroyed: boolean;
// ===== graph instance ===
/**
* Destroy the graph instance and remove the related canvases.
* @returns
* @group Graph Instance
*/
destroy: () => void;
/**
* Update the specs(configurations).
*/
@ -30,38 +40,125 @@ export interface IGraph<B extends BehaviorRegistry = BehaviorRegistry> extends E
/**
* Find a node's inner data according to id or function.
* @param { string | Function} condition id or condition function
* @returns result node
* @group Item
* @returns result node's inner data
* @group Data
*/
getNodeData: (condition: string | Function) => NodeModel | undefined;
/**
* Find an edge's inner data according to id or function.
* @param { string | Function} condition id or condition function
* @returns result edge
* @group Item
* @returns result edge's inner data
* @group Data
*/
getEdgeData: (condition: string | Function) => EdgeModel | undefined;
/**
* Find a combo's inner data according to id or function.
* @param { string | Function} condition id or condition function
* @returns result combo
* @group Item
* @returns result combo's inner data
* @group Data
*/
getComboData: (condition: string | Function) => ComboModel | undefined;
/**
* Get all the nodes' inner data
* @returns all nodes' inner data on the graph
* @group Data
*/
getAllNodesData: () => NodeModel[];
/**
* Get all the edges' inner data
* @returns all edges' inner data on the graph
* @group Data
*/
getAllEdgesData: () => EdgeModel[];
/**
* Get all the combos' inner data
* @returns all combos' inner data on the graph
* @group Data
*/
getAllCombosData: () => ComboModel[];
/**
* Input data and render the graph.
* If there is old data, diffs and changes it.
* @param data
* @returns
* @param data
* @returns
* @group Data
*/
read: (data: GraphData) => void;
/**
* Change graph data.
* @param data new data
* @param type the way to change data, 'replace' means discard the old data and use the new one; 'mergeReplace' means merge the common part, remove (old - new), add (new - old)
* @returns
* @group Data
*/
changeData: (data: GraphData, type: 'replace' | 'mergeReplace') => void;
/**
* Clear the graph, means remove all the items on the graph.
* @returns
* @returns
*/
clear: () => void;
/**
* Find items which has the state.
* @param itemType item type
* @param state state name
* @param value state value, true by default
* @param additionalFilter additional filter function
* @returns items that is the type and has the state
* @group Item
*/
findIdByState: (
itemType: ITEM_TYPE,
state: string,
value?: string | boolean,
additionalFilter?: (model: NodeModel | EdgeModel | ComboModel) => boolean,
) => ID[];
/**
* Add one or more node/edge/combo data to the graph.
* @param itemType item type
* @param model user data
* @param stack whether push this operation to stack
* @returns whehter success
* @group Data
*/
addData: (
itemType: ITEM_TYPE,
model:
| NodeUserModel
| EdgeUserModel
| ComboUserModel
| NodeUserModel[]
| EdgeUserModel[]
| ComboUserModel[],
stack?: boolean,
) => NodeModel | EdgeModel | ComboModel | NodeModel[] | EdgeModel[] | ComboModel[];
/**
* Remove one or more node/edge/combo data from the graph.
* @param item the item to be removed
* @param stack whether push this operation to stack
* @returns whehter success
* @group Data
*/
removeData: (itemType: ITEM_TYPE, id: ID | ID[], stack?: boolean) => void;
/**
* Update one or more node/edge/combo data on the graph.
* @param item the item to be updated
* @param model update configs
* @param {boolean} stack whether push this operation to stack
* @group Data
*/
updateData: (
itemType: ITEM_TYPE,
model:
| Partial<NodeUserModel>
| Partial<EdgeUserModel>
| Partial<
| ComboUserModel
| Partial<NodeUserModel>[]
| Partial<EdgeUserModel>[]
| Partial<ComboUserModel>[]
>,
stack?: boolean,
) => NodeModel | EdgeModel | ComboModel | NodeModel[] | EdgeModel[] | ComboModel[];
// ===== view operations =====
@ -70,7 +167,7 @@ export interface IGraph<B extends BehaviorRegistry = BehaviorRegistry> extends E
* @param dx x of the relative vector
* @param dy y of the relative vector
* @param animateCfg animation configurations
* @returns
* @returns
* @group View
*/
move: (dx: number, dy: number, animateCfg?: AnimateCfg) => void;
@ -80,7 +177,7 @@ export interface IGraph<B extends BehaviorRegistry = BehaviorRegistry> extends E
* @param y position on the canvas to align
* @param alignment alignment of the graph content
* @param animateCfg animation configurations
* @returns
* @returns
* @group View
*/
moveTo: (x: number, y: number, alignment: GraphAlignment, animateCfg?: AnimateCfg) => void;
@ -89,7 +186,7 @@ export interface IGraph<B extends BehaviorRegistry = BehaviorRegistry> extends E
* @param ratio relative ratio to zoom
* @param center zoom center
* @param animateCfg animation configurations
* @returns
* @returns
* @group View
*/
zoom: (ratio: number, center?: Point, animateCfg?: AnimateCfg) => void;
@ -98,7 +195,7 @@ export interface IGraph<B extends BehaviorRegistry = BehaviorRegistry> extends E
* @param toRatio specified ratio
* @param center zoom center
* @param animateCfg animation configurations
* @returns
* @returns
* @group View
*/
zoomTo: (toRatio: number, center?: Point, animateCfg?: AnimateCfg) => void;
@ -107,14 +204,14 @@ export interface IGraph<B extends BehaviorRegistry = BehaviorRegistry> extends E
* @param padding padding while fitting
* @param rules rules for fitting
* @param animateCfg animation configurations
* @returns
* @returns
* @group View
*/
fitView: (padding?: Padding, rules?: FitViewRules, animateCfg?: AnimateCfg) => void;
/**
* Fit the graph center to the view center.
* @param animateCfg animation configurations
* @returns
* @returns
* @group View
*/
fitCenter: (animateCfg?: AnimateCfg) => void;
@ -122,100 +219,78 @@ export interface IGraph<B extends BehaviorRegistry = BehaviorRegistry> extends E
* Move the graph to make the item align the view center.
* @param item node/edge/combo item or its id
* @param animateCfg animation configurations
* @returns
* @returns
* @group View
*/
focusItem: (ids: string | number | (string | number)[], animateCfg?: AnimateCfg) => void;
focusItem: (ids: ID | ID[], animateCfg?: AnimateCfg) => void;
// ===== item operations =====
/**
* Find items which has the state.
* @param itemType item type
* @param state state name
* @param additionalFilter additional filter function
* @returns items that is the type and has the state
* @group Item
*/
findIdByState: (itemType: ITEM_TYPE, state: string, additionalFilter?: (model: NodeModel | EdgeModel | ComboModel) => boolean) => (string | number)[];
/**
* Add an item or items to the graph.
* @param itemType item type
* @param model user data
* @param stack whether push this operation to stack
* @returns whehter success
* @group Item
*/
addItem: (itemType: ITEM_TYPE, model: NodeUserModel | EdgeUserModel | ComboUserModel | NodeUserModel[] | EdgeUserModel[] | ComboUserModel[], stack?: boolean) => boolean;
/**
* Remove an item or items from the graph.
* @param item the item to be removed
* @param stack whether push this operation to stack
* @returns whehter success
* @group Item
*/
removeItem: (itemType: ITEM_TYPE, id: string | number | (string | number)[], stack?: boolean) => boolean;
/**
* Update an item or items on the graph.
* @param item the item to be updated
* @param model update configs
* @param {boolean} stack whether push this operation to stack
* @group Item
*/
updateItem: (itemType: ITEM_TYPE, model: Partial<NodeUserModel> | Partial<EdgeUserModel> | Partial<ComboUserModel | Partial<NodeUserModel>[] | Partial<EdgeUserModel>[] | Partial<ComboUserModel>[]>, stack?: boolean) => boolean;
/**
* Show the item(s).
* @param ids the item id(s) to be shown
* @returns
* @group Item
* @returns
* @group Data
*/
showItem: (ids: string | number | (string | number)[]) => void;
showItem: (ids: ID | ID[]) => void;
/**
* Hide the item(s).
* @param ids the item id(s) to be hidden
* @returns
* @returns
* @group Item
*/
hideItem: (ids: string | number | (string | number)[]) => void;
hideItem: (ids: ID | ID[]) => void;
/**
* Set state for the item(s).
* @param ids the id(s) for the item(s) to be set
* @param state the state name
* @param value state value
* @returns
* @returns
* @group Item
*/
setItemState: (ids: string | number | (string | number)[], state: string, value: boolean) => void;
setItemState: (ids: ID | ID[], state: string, value: boolean) => void;
/**
* Get the state value for an item.
* @param id the id for the item
* @param states the state name
* @returns {boolean | string} the state value
* @group Item
*/
getItemState: (id: ID, state: string) => boolean | string;
/**
* Clear all the states for item(s).
* @param ids the id(s) for the item(s) to be clear
* @param states the states' names, all the states wil be cleared if states is not assigned
* @returns
* @group Item
*/
clearItemState: (ids: ID | ID[], states?: string[]) => void;
// ===== combo operations =====
/**
* Create a new combo with existing child nodes and combos.
* @param combo combo ID or Combo model
* @param childrenIds id array of children of the new combo
* @group Combo
*/
* Create a new combo with existing child nodes and combos.
* @param combo combo ID or Combo model
* @param childrenIds id array of children of the new combo
* @group Combo
*/
createCombo: (combo: string | ComboUserModel, childrenIds: string[], stack?: boolean) => void;
/**
* dissolve combo
* @param {String | ICombo} item combo item or id to be dissolve
*/
uncombo: (comboId: string | number, stack?: boolean) => void;
uncombo: (comboId: ID, stack?: boolean) => void;
/**
* Collapse a combo.
* @param comboId combo id or item
* @group Combo
*/
collapseCombo: (comboId: string | number, stack?: boolean) => void;
* Collapse a combo.
* @param comboId combo id or item
* @group Combo
*/
collapseCombo: (comboId: ID, stack?: boolean) => void;
/**
* Expand a combo.
* @group Combo
* @param combo combo ID combo
* @group Combo
*/
expandCombo: (comboId: string | number, stack?: boolean) => void;
* Expand a combo.
* @group Combo
* @param combo combo ID combo
* @group Combo
*/
expandCombo: (comboId: ID, stack?: boolean) => void;
// ===== layout =====
/**
@ -226,14 +301,18 @@ export interface IGraph<B extends BehaviorRegistry = BehaviorRegistry> extends E
* @param {boolean} stack push it into stack
* @group Layout
*/
layout: (cfg?: LayoutCommonConfig, align?: GraphAlignment, canvasPoint?: Point, stack?: boolean) => void;
layout: (
cfg?: LayoutCommonConfig,
align?: GraphAlignment,
canvasPoint?: Point,
stack?: boolean,
) => void;
// ===== interaction =====
/**
* Switch mode.
* @param mode mode name
* @returns
* @returns
* @group Interaction
*/
setMode: (mode: string) => void;
@ -241,14 +320,14 @@ export interface IGraph<B extends BehaviorRegistry = BehaviorRegistry> extends E
* Add behavior(s) to mode(s).
* @param behaviors behavior names or configs
* @param modes mode names
* @returns
* @returns
*/
addBehaviors: (behaviors: BehaviorOptionsOf<B>[], modes: string | string[]) => void;
/**
* Remove behavior(s) from mode(s).
* @param behaviors behavior names or configs
* @param modes mode names
* @returns
* @returns
* @group Interaction
*/
removeBehaviors: (behaviorKeys: string[], modes: string | string[]) => void;
@ -256,8 +335,8 @@ export interface IGraph<B extends BehaviorRegistry = BehaviorRegistry> extends E
* Update a behavior on a mode.
* @param behavior behavior configs, whose name indicates the behavior to be updated
* @param mode mode name
* @returns
* @returns
* @group Interaction
*/
updateBehavior: (behavior: BehaviorObjectOptionsOf<B>, mode?: string) => void;
}
}

View File

@ -1,8 +1,8 @@
import { GraphCore, GraphData } from "./data";
import { NodeUserModel } from "./node";
import { ComboUserModel } from "./combo";
import { EdgeUserModel } from "./edge";
import { DataChangeType, GraphCore, GraphData } from "./data";
import { NodeModel, NodeModelData, NodeUserModel } from "./node";
import { EdgeModel, EdgeModelData, EdgeUserModel } from "./edge";
import { ITEM_TYPE } from "./item";
import { GraphChange, ID } from "@antv/graphlib";
export interface IHook<T> {
name: string;
@ -15,14 +15,15 @@ export interface IHook<T> {
export interface Hooks {
'init': IHook<void>;
// data
'datachange': IHook<{ data: GraphData }>;
// data, item
'additems': IHook<{ type: ITEM_TYPE, models: NodeUserModel[] | EdgeUserModel[] | ComboUserModel[] }>
// data, item
'removeitems': IHook<{ type: ITEM_TYPE, ids: (string | number)[] }>
// data, item
'updateitems': IHook<{ type: ITEM_TYPE, models: Partial<NodeUserModel>[] | Partial<EdgeUserModel>[] | Partial<ComboUserModel>[] }>
'datachange': IHook<{
type: DataChangeType;
data: GraphData
}>;
'itemchange': IHook<{
type: ITEM_TYPE;
changes: GraphChange<NodeModelData, EdgeModelData>[];
graphCore: GraphCore;
}>;
'render': IHook<{ graphCore: GraphCore }>; // TODO: define param template
// 'layout': IHook<any>; // TODO: define param template
// 'updatelayout': IHook<any>; // TODO: define param template
@ -32,8 +33,12 @@ export interface Hooks {
modes: string[];
behaviors: (string | { type: string, key: string })[];
}>;
'itemstatechange': IHook<{
ids: ID[],
states?: string[],
value?: boolean
}>; // TODO: define param template
// 'viewportchange': IHook<any>; // TODO: define param template
// 'itemstatechange': IHook<any>; // TODO: define param template
// 'destroy': IHook<any>; // TODO: define param template
// TODO: more timecycles here
};

View File

@ -1,18 +1,34 @@
import { Group } from "@antv/g";
import { AnimateAttr } from "./animate";
import { ComboDisplayModel, ComboModel } from "./combo";
import { EdgeDisplayModel, EdgeModel } from "./edge";
import { NodeDisplayModel, NodeModel } from "./node";
import { ID } from '@antv/graphlib';
import { AnimateAttr } from './animate';
import {
ComboDisplayModel,
ComboEncode,
ComboModel,
ComboModelData,
ComboUserModel,
} from './combo';
import { EdgeDisplayModel, EdgeEncode, EdgeModel, EdgeModelData, EdgeUserModel } from './edge';
import { NodeDisplayModel, NodeEncode, NodeModel, NodeModelData, NodeUserModel } from './node';
export interface ShapeStyle {
[shapeAttr: string]: unknown;
animate?: AnimateAttr;
x?: number;
y?: number;
width?: number;
height?: number;
r?: number;
}
export interface Encode<T> {
fields: string[],
formatter: (values: unknown[]) => T;
fields: string[];
formatter: (values: NodeUserModel | EdgeUserModel | ComboUserModel) => T;
}
export interface ShapeAttrEncode {
[shapeAttr: string]: unknown | Encode<unknown>;
animate: AnimateAttr | Encode<AnimateAttr>;
};
animate?: AnimateAttr | Encode<AnimateAttr>;
}
export interface LabelBackground {
fill?: string;
@ -20,25 +36,38 @@ export interface LabelBackground {
lineWidth?: number;
radius?: number[] | number;
padding?: number[] | number;
};
}
export interface ShapesEncode {
keyShape?: ShapeAttrEncode;
iconShape?: ShapeAttrEncode;
otherShapes?: {
[shapeName: string]: {
[shapeId: string]: {
[shapeAtrr: string]: unknown | Encode<unknown>;
animate: AnimateAttr | Encode<AnimateAttr>;
}
};
};
}
export type ITEM_TYPE = 'node' | 'edge' | 'combo';
export type ItemModelData = NodeModelData | EdgeModelData | ComboModelData;
export type ItemModel = NodeModel | EdgeModel | ComboModel;
export type ItemDisplayModel = NodeDisplayModel | EdgeDisplayModel | ComboDisplayModel;
export type DisplayMapper =
| ((model: ItemModel) => ItemDisplayModel)
| NodeEncode
| EdgeEncode
| ComboEncode;
export type State = {
name: string;
value: boolean | string;
}
/**
* Base item of node / edge / combo.
*/
@ -60,21 +89,29 @@ export interface IItem {
// type: 'node' | 'edge' | 'combo';
/** Gets the inner model. */
getModel: () => ItemModel;
// getModel: () => ItemModel;
/** Gets the id in model. */
getID: () => string | number;
getID: () => ID;
/** Gets the item's type. */
getType: () => 'node' | 'edge' | 'combo';
/**
* Draws the shapes.
* @internal
* */
draw: () => void;
draw: (
displayModel: ItemDisplayModel,
diffData?: { previous: ItemModelData; current: ItemModelData },
diffState?: { previous: State[], current: State[] },
) => void;
/**
* Updates the shapes.
* @internal
* */
update: (model: ItemModel) => void;
update: (
model: ItemModel,
diffData: { previous: ItemModelData; current: ItemModelData },
isUpdate?: boolean,
) => void;
/** Puts the item to the front in its graphic group. */
toFront: () => void;
/** Puts the item to the back in its graphic group. */
@ -88,14 +125,14 @@ export interface IItem {
/** Sets a state value to the item. */
setState: (state: string, value: string | boolean) => void;
/** Returns the state if it is true/string. Returns false otherwise. */
hasState: (state: string) => { name: string, value: boolean | string } | false;
hasState: (state: string) => string | boolean;
/** Get all the true or string states of the item. */
getStates: () => {
name: string,
value: boolean | string
name: string;
value: boolean | string;
}[];
/** Set all the state to false. */
clearStates: (states?: string[]) => void;
/** Destroy the item. */
destroy: () => void;
}
}

View File

@ -1,6 +1,7 @@
import { DisplayObject } from '@antv/g';
import { Node as GNode, PlainObject } from '@antv/graphlib';
import { AnimateAttr } from "./animate";
import { Encode, IItem, LabelBackground, ShapeAttrEncode, ShapesEncode } from "./item";
import { AnimateAttr } from './animate';
import { Encode, IItem, LabelBackground, ShapeAttrEncode, ShapesEncode, ShapeStyle } from './item';
export type NodeLabelPosition = 'bottom' | 'center' | 'top' | 'left' | 'right';
@ -15,32 +16,22 @@ export interface NodeModelData extends NodeUserModelData {
label?: string;
}
export interface NodeLabelShapeStyle extends ShapeStyle {
position?: NodeLabelPosition;
offsetX?: number;
offsetY?: number;
background?: LabelBackground;
}
/** Data in display model. */
export interface NodeDisplayModelData extends NodeModelData {
keyShape?: {
[shapeAttr: string]: unknown;
animate: AnimateAttr;
};
labelShape?: {
position?: NodeLabelPosition;
offsetX?: number;
offsetY?: number;
background?: LabelBackground;
[shapeAttr: string]: unknown;
animate: AnimateAttr;
};
iconShape?: {
[shapeAttr: string]: unknown;
animate: AnimateAttr;
};
keyShape?: ShapeStyle;
labelShape?: NodeLabelShapeStyle;
iconShape?: ShapeStyle;
otherShapes?: {
[shapeName: string]: {
[shapeAttr: string]: unknown;
animate: AnimateAttr;
}
[shapeId: string]: ShapeStyle;
};
anchorPoints?: AnchorPoint[]
anchorPoints?: AnchorPoint[];
}
/** User input model. */
@ -60,7 +51,8 @@ export interface AnchorPoint {
animate: AnimateAttr;
}
interface NodeLabelShapeAttrEncode extends ShapeAttrEncode { // TODO: extends Text shape attr, import from G
interface NodeLabelShapeAttrEncode extends ShapeAttrEncode {
// TODO: extends Text shape attr, import from G
position?: NodeLabelPosition | Encode<NodeLabelPosition>;
offsetX?: number | Encode<number>;
offsetY?: number | Encode<number>;
@ -68,13 +60,18 @@ interface NodeLabelShapeAttrEncode extends ShapeAttrEncode { // TODO: extends Te
}
export interface NodeShapesEncode extends ShapesEncode {
labelShape?: NodeLabelShapeAttrEncode;
anchorPoints?: AnchorPoint[] | Encode<AnchorPoint[]>
anchorPoints?: AnchorPoint[] | Encode<AnchorPoint[]>;
}
export interface NodeEncode extends NodeShapesEncode {
type?: string | Encode<string>;
}
// TODO
export interface INode extends IItem {
export interface NodeShapeMap {
keyShape: DisplayObject;
labelShape?: DisplayObject;
iconShape?: DisplayObject;
[otherShapeId: string]: DisplayObject;
}
}
// TODO
export interface INode extends IItem {}

View File

@ -5,13 +5,14 @@ import { EdgeDisplayModel, EdgeEncode, EdgeModel, EdgeShapesEncode } from "./edg
import { NodeDisplayModel, NodeEncode, NodeModel, NodeShapesEncode } from "./node";
import { GraphAlignment } from "./view";
import { LayoutCommonConfig } from "./layout";
import { ComboDisplayModel, ComboEncode, ComboModel } from "./combo";
import { ComboDisplayModel, ComboEncode, ComboModel, ComboShapesEncode } from "./combo";
import { BehaviorOptionsOf, BehaviorRegistry } from "./behavior";
type rendererName = 'canvas' | 'svg' | 'webgl';
export interface Specification<B extends BehaviorRegistry> {
type: 'graph' | 'tree';
container: string | HTMLElement;
width?: number;
height?: number;
renderer?: rendererName | {
@ -34,19 +35,19 @@ export interface Specification<B extends BehaviorRegistry> {
}[] | TransformerFn[];
/** item */
node?: (data: NodeModel) => NodeDisplayModel | NodeEncode;
edge?: (data: EdgeModel) => EdgeDisplayModel | EdgeEncode;
combo?: (data: ComboModel) => ComboDisplayModel | ComboEncode;
node?: ((data: NodeModel) => NodeDisplayModel) | NodeEncode;
edge?: ((data: EdgeModel) => EdgeDisplayModel) | EdgeEncode;
combo?: ((data: ComboModel) => ComboDisplayModel) | ComboEncode;
/** item state styles */
nodeState?: {
[state: string]: (data: NodeModel) => NodeDisplayModel | NodeShapesEncode;
[stateName: string]: ((data: NodeModel) => NodeDisplayModel) | NodeShapesEncode;
};
edgeState?: {
[state: string]: (data: EdgeModel) => EdgeDisplayModel | EdgeShapesEncode;
[stateName: string]: ((data: EdgeModel) => EdgeDisplayModel) | EdgeShapesEncode;
};
comboState?: {
[state: string]: (data: NodeModel) => NodeDisplayModel | NodeShapesEncode;
[stateName: string]: ((data: ComboModel) => ComboDisplayModel) | ComboShapesEncode;
};
/** layout */

View File

@ -0,0 +1,9 @@
export const isArrayOverlap = (arr1, arr2): boolean => {
if (!arr1?.length || !arr2.length) return false;
for (let i = 0; i < arr1.length; i++) {
for (let j = 0; j < arr2.length; j++) {
if (arr1[i] === arr2[j]) return true;
}
}
return false;
}

View File

@ -0,0 +1,61 @@
import { Canvas } from '@antv/g';
import { Renderer as CanvasRenderer } from '@antv/g-canvas';
import { Renderer as SVGRenderer } from '@antv/g-svg';
// import { Renderer as WebGLRenderer } from '@antv/g-webgl';
import { isString } from '@antv/util';
/**
* Create a canvas
* @param { 'canvas' | 'svg' | 'webgl' } rendererType
* @param {string | HTMLElement} container
* @param {number} width
* @param {number} height
* @param {number} pixelRatio optional
* @param {boolean} customCanvasTag whether create a <canvas /> for multiple canvas under the container
* @returns
*/
export const createCanvas = (
rendererType: 'canvas' | 'svg' | 'webgl',
container: string | HTMLElement,
width: number,
height: number,
pixelRatio?: number,
customCanvasTag: boolean = true
) => {
let Renderer;
switch (rendererType.toLowerCase()) {
case 'svg':
Renderer = SVGRenderer;
break;
case 'webgl':
// Renderer = WebGLRenderer;
// TODO
break;
default:
Renderer = CanvasRenderer;
break;
}
if (typeof document !== 'undefined' && customCanvasTag) {
const canvasTag = document.createElement('canvas');
const dpr = pixelRatio || window.devicePixelRatio;
canvasTag.width = dpr * width;
canvasTag.height = dpr * height;
canvasTag.style.width = `${width}px`;
canvasTag.style.height = `${height}px`;
canvasTag.style.position = 'fixed';
const containerDOM = isString(container) ? document.getElementById('container') : container;
containerDOM.appendChild(canvasTag);
return new Canvas({
canvas: canvasTag,
devicePixelRatio: pixelRatio,
renderer: new Renderer(),
});
}
return new Canvas({
container,
width,
height,
devicePixelRatio: pixelRatio,
renderer: new Renderer()
});
}

View File

@ -1,4 +1,4 @@
import { BehaviorRegistry } from "../types/behavior";
import { BehaviorRegistry } from '../types/behavior';
import Graph from '../runtime/graph';
import registery from '../stdlib';
@ -13,13 +13,19 @@ import registery from '../stdlib';
export const extend = <B1 extends BehaviorRegistry, B2 extends BehaviorRegistry>(
GraphClass: typeof Graph<B2>,
extendLibrary: {
behaviors?: B1,
}
behaviors?: B1;
nodes?: any; // TODO
edges?: any; // TODO
},
): typeof Graph<B1 & B2> => {
// merged the extendLibrary to useLib for global usage
Object.keys(extendLibrary).forEach(cat => {
Object.keys(extendLibrary).forEach((cat) => {
registery.useLib[cat] = Object.assign({}, registery.useLib[cat], extendLibrary[cat] || {});
Object.keys(registery.useLib[cat]).forEach((type) => {
const extension = registery.useLib[cat][type];
extension.type = type;
});
});
// @ts-expect-error
return GraphClass;
}
};

View File

@ -17,6 +17,5 @@ export const getExtension = (config: string | Function | object, lib, cat: StdLi
}
const type = typeof config === 'string' ? config : (config as any).type;
const ext = lib[catKey]?.[type];
if (isFunction(ext)) ext;
return new ext(config);
return ext;
};

View File

@ -1,3 +1,4 @@
import { extend } from './extend';
const Util = { extend };
import { isEncode } from './type';
const Util = { extend, isEncode };
export default Util;

View File

@ -0,0 +1,173 @@
import {
Circle,
DisplayObject,
Ellipse,
Group,
IElement,
Line,
Polygon,
Polyline,
Rect,
Text,
Image,
} from '@antv/g';
import { clone, isArray, isNumber } from '@antv/util';
import { EdgeShapeMap } from '../types/edge';
import { NodeShapeMap } from '../types/node';
const shapeTagMap = {
circle: Circle,
rect: Rect,
ellipse: Ellipse,
polygon: Polygon,
line: Line,
polyline: Polyline,
text: Text,
image: Image,
};
const createShape = (type: string, style: { [shapeAttr: string]: unknown }, id: string) => {
const ShapeClass = shapeTagMap[type];
return new ShapeClass({ style, id, autoUpdate: true });
};
export const upsertShape = (
type: string,
id: string,
style: { [shapeAttr: string]: unknown },
shapeMap: { [shapeId: string]: DisplayObject },
): DisplayObject => {
let shape = shapeMap[id];
if (!shape) {
shape = createShape(type, style, id);
} else if (shape.nodeName !== type) {
shape.remove();
shape = createShape(type, style, id);
} else {
Object.keys(style).forEach((key) => {
shape.style[key] = style[key];
});
}
shapeMap[id] = shape;
return shape;
};
export const getGroupSucceedMap = (group: IElement, map?: { [id: string]: IElement }) => {
let useMap = map || {};
group.children.forEach((child) => {
if (child.tagName === 'group') getGroupSucceedMap(child, useMap);
useMap[child.id] = child;
});
return useMap;
};
/**
* Update shapes in the intersaction of prevShapeMap and newShapeMap;
* Remove shapes in the prevShapeMap - newShapeMap (if removeDiff is true);
* Add shapes in the newShapeMap - prevShapeMap;
* @param prevShapeMap previous shape map
* @param newShapeMap new shape map
* @param group container group
* @returns merged shape map
*/
export const updateShapes = (
prevShapeMap: { [id: string]: DisplayObject },
newShapeMap: { [id: string]: DisplayObject },
group: Group,
removeDiff: boolean = true,
shouldUpdate: (id: string) => boolean = () => true,
): NodeShapeMap | EdgeShapeMap => {
const tolalMap = {
...prevShapeMap,
...newShapeMap,
};
const finalShapeMap = {
...prevShapeMap,
};
Object.keys(tolalMap).forEach((id) => {
const prevShape = prevShapeMap[id];
const newShape = newShapeMap[id];
if (newShape && !shouldUpdate(id)) return;
if (prevShape && newShape) {
// update intersaction
finalShapeMap[id] = newShape;
if (prevShape !== newShape) {
prevShape.remove();
}
group.appendChild(newShape);
} else if (!prevShape && newShape) {
// add newShapeMap - prevShapeMap
finalShapeMap[id] = newShape;
group.appendChild(newShape);
} else if (prevShape && !newShape && removeDiff) {
// remove prevShapeMap - newShapeMap
delete finalShapeMap[id];
prevShape.remove();
}
});
return finalShapeMap as NodeShapeMap;
};
/**
* Format the number or array padding to an array with length 4, [padding-top, padding-right, padding-bottom, padding-left].
* @param value value to be formatted
* @param defaultArr default value
* @returns [padding-top, padding-right, padding-bottom, padding-left]
*/
export const formatPadding = (value, defaultArr = [4, 4, 4, 4]) => {
if (isArray(value)) {
switch (value.length) {
case 0:
return defaultArr;
case 1:
return [value[0], value[0], value[0], value[0]];
case 2:
return value.concat(value);
default:
return value;
}
}
if (isNumber(value)) return [value, value, value, value];
return defaultArr;
};
/**
* Merge two shape map including undefined value in incoming map.
* @param styleMap1 shapes' styles map as current map
* @param styleMap2 shapes' styles map as incoming map
* @returns
*/
export const mergeStyles = (styleMap1, styleMap2) => {
const mergedStyle = clone(styleMap1);
Object.keys(styleMap2).forEach(shapeId => {
const style = styleMap2[shapeId];
mergedStyle[shapeId] = mergedStyle[shapeId] || {};
Object.keys(style).forEach(styleName => {
const value = style[styleName];
mergedStyle[shapeId][styleName] = value;
});
});
return mergedStyle;
}
export const DEFAULT_LABEL_BG_PADDING = [4, 4, 4, 4];
/** Default shape style to avoid shape value missing */
export const DEFAULT_SHAPE_STYLE = {
opacity: 1,
fillOpacity: 1,
shadowColor: undefined,
shadowBlur: undefined,
lineDash: undefined,
};
/** Default text style to avoid shape value missing */
export const DEFAULT_TEXT_STYLE = {
...DEFAULT_SHAPE_STYLE,
fontSize: 12,
fontFamily: 'sans-serif',
fontWeight: 'normal',
fontVariant: 'normal',
fontStyle: 'normal',
textBaseline: 'middle',
textAlign: 'center',
lineWidth: 0,
};

View File

@ -0,0 +1,3 @@
import { Encode } from "../types/item"
export const isEncode = (value): value is Encode<any> => Boolean((value as Encode<any>).fields && (value as Encode<any>).formatter)

View File

@ -8,6 +8,7 @@ document.querySelector('body').appendChild(container);
describe('behavior', () => {
it('behavior in spec, add / remove / update a behavior in defualt mode', () => {
const graph = new G6.Graph({
container,
type: 'graph',
data: { nodes: [], edges: [] },
modes: {
@ -22,8 +23,11 @@ describe('behavior', () => {
});
let graphSpec = graph.getSpecification();
expect(graphSpec.modes.default[0]).toBe('drag-canvas');
// @ts-ignore
expect(graphSpec.modes.default[1].key).toBe('dragcanvaskey2');
// @ts-ignore
expect(graphSpec.modes.default[1].type).toBe('drag-canvas');
// @ts-ignore
expect(graphSpec.modes.default[1].assistKey).toBe('shift');
graph.addBehaviors([{
@ -34,8 +38,11 @@ describe('behavior', () => {
// graph.getSpecification() returns a copy, need to be called again to fetch the lastest
graphSpec = graph.getSpecification();
expect(graphSpec.modes.default.length).toBe(3);
// @ts-ignore
expect(graphSpec.modes.default[2].key).toBe('dragcanvaskey3');
// @ts-ignore
expect(graphSpec.modes.default[2].type).toBe('drag-canvas');
// @ts-ignore
expect(graphSpec.modes.default[2].assistKey).toBe('ctrl');
graph.removeBehaviors(['dragcanvaskey3', 'dragcanvaskey2'], 'default');
@ -59,6 +66,7 @@ describe('behavior', () => {
}], 'default');
graphSpec = graph.getSpecification();
expect(graphSpec.modes.default.length).toBe(2);
// @ts-ignore
expect(graphSpec.modes.default[1].assistKey).toBe('ctrl');
graph.updateBehavior({
key: 'newbehavior',
@ -67,6 +75,7 @@ describe('behavior', () => {
}, 'default');
graphSpec = graph.getSpecification();
expect(graphSpec.modes.default.length).toBe(2);
// @ts-ignore
expect(graphSpec.modes.default[1].assistKey).toBe('shift');
});
it('register behavior and extend G6', () => {
@ -86,6 +95,7 @@ describe('behavior', () => {
}
});
const graph = new CustomGraph({
container,
type: 'graph',
data: { nodes: [], edges: [] },
modes: {

View File

@ -0,0 +1,322 @@
// @ts-nocheck
import G6, { GraphData, IGraph } from '../../src/index';
const container = document.createElement('div');
document.querySelector('body').appendChild(container);
const data: GraphData = {
nodes: [
{ id: 'node1', data: { x: 100, y: 200 } },
{ id: 'node2', data: { x: 200, y: 250 } },
{ id: 'node3', data: { x: 300, y: 200 } },
{ id: 'node4', data: { x: 300, y: 250 } },
],
edges: [
{ id: 'edge1', source: 'node1', target: 'node2', data: {} },
{ id: 'edge2', source: 'node1', target: 'node3', data: {} },
],
};
describe('data', () => {
let graph: IGraph<any>;
it('new graph with data', (done) => {
graph = new G6.Graph({
container,
width: 500,
height: 500,
type: 'graph',
data, // with data, graph will be rendered in constructor
});
graph.on('afterrender', () => {
expect(graph.canvas.document.childNodes[0].childNodes.length).toBe(2);
done();
});
expect(graph.dataController.graphCore.getAllNodes().length).toBe(4);
expect(graph.dataController.graphCore.getAllEdges().length).toBe(2);
expect(graph.backgroundCanvas).not.toBe(undefined);
expect(graph.transientCanvas).not.toBe(undefined);
expect(graph.canvas).not.toBe(undefined);
});
it('updateData', () => {
// === update node ===
const node1UpdateUserData = {
id: 'node1',
data: {
x: 350,
},
};
graph.updateData('node', node1UpdateUserData);
const newNode1UserData = {
...data.nodes[0],
...node1UpdateUserData,
data: {
...data.nodes[0].data,
...node1UpdateUserData.data,
},
};
const node1InnerData = graph.getNodeData('node1');
expect(JSON.stringify(newNode1UserData)).toBe(JSON.stringify(node1InnerData));
// === update edge data ===
const edge2UpdateUserData = {
id: 'edge2',
data: {
keyShape: {
lineWidth: 8,
},
},
};
graph.updateData('edge', edge2UpdateUserData);
const newEdge2UserData = {
...data.edges[1],
...edge2UpdateUserData,
data: {
...data.edges[1].data,
...edge2UpdateUserData.data,
},
};
const edge2InnerData = graph.getEdgeData('edge2');
expect(JSON.stringify(newEdge2UserData)).toBe(JSON.stringify(edge2InnerData));
expect(graph.itemController.itemMap['edge2'].shapeMap['keyShape'].attributes.lineWidth).toBe(
edge2UpdateUserData.data.keyShape.lineWidth,
);
// === update edge source ===
const edge1UpdateUserData = {
id: 'edge1',
source: 'node3',
};
graph.updateData('edge', edge1UpdateUserData);
const newSourceData = graph.getNodeData('node3');
expect(graph.itemController.itemMap['edge1'].shapeMap['keyShape'].attributes.x1).toBe(
newSourceData.data.x,
);
expect(graph.itemController.itemMap['edge1'].shapeMap['keyShape'].attributes.y1).toBe(
newSourceData.data.y,
);
expect(graph.itemController.itemMap['edge1'].sourceItem).toBe(
graph.itemController.itemMap['node3'],
);
// === update edge source, target, and data in the same time ===
const edge1UpdateUserData2 = {
id: 'edge1',
source: 'node1',
target: 'node4',
data: {
keyShape: {
stroke: '#f00',
},
},
};
graph.updateData('edge', edge1UpdateUserData2);
const sourceData = graph.getNodeData('node1');
const targetData = graph.getNodeData('node4');
const edgeItem = graph.itemController.itemMap['edge1'];
expect(edgeItem.shapeMap['keyShape'].attributes.x1).toBe(sourceData.data.x);
expect(edgeItem.shapeMap['keyShape'].attributes.y1).toBe(sourceData.data.y);
expect(edgeItem.shapeMap['keyShape'].attributes.x2).toBe(targetData.data.x);
expect(edgeItem.shapeMap['keyShape'].attributes.y2).toBe(targetData.data.y);
expect(edgeItem.shapeMap['keyShape'].attributes.stroke).toBe(
edge1UpdateUserData2.data.keyShape.stroke,
);
// === update nodes ===
graph.updateData('node', [
{
id: 'node1',
data: {
keyShape: {
fill: '#0f0',
},
},
},
{
id: 'node2',
data: {
keyShape: {
lineWidth: 2,
stroke: '#0f0',
},
},
},
]);
const node1Item = graph.itemController.itemMap['node1'];
const node2Item = graph.itemController.itemMap['node2'];
expect(node1Item.shapeMap['keyShape'].attributes.fill).toBe('#0f0');
expect(node2Item.shapeMap['keyShape'].attributes.lineWidth).toBe(2);
expect(node2Item.shapeMap['keyShape'].attributes.stroke).toBe('#0f0');
// === update edges ===
graph.updateData('edge', [
{
id: 'edge1',
source: 'node2',
data: {
keyShape: {
stroke: '#0f0',
},
},
},
{
id: 'edge2',
data: {
keyShape: {
lineDash: [5, 5],
},
},
},
]);
const edge1Item = graph.itemController.itemMap['edge1'];
const edge2Item = graph.itemController.itemMap['edge2'];
expect(edge1Item.shapeMap['keyShape'].attributes.stroke).toBe('#0f0');
expect(edge1Item.sourceItem).toBe(graph.itemController.itemMap['node2']);
expect(JSON.stringify(edge2Item.shapeMap['keyShape'].attributes.lineDash)).toBe('[5,5]');
});
it('addData', () => {
graph.addData('node', {
id: 'node5',
data: {
x: 300,
y: 100,
},
});
graph.addData('node', {
id: 'node6',
data: {
x: 300,
y: 200,
},
});
graph.addData('edge', {
id: 'edge3',
source: 'node5',
target: 'node6',
data: {},
});
expect(graph.dataController.graphCore.getAllNodes().length).toBe(6);
expect(graph.dataController.graphCore.getAllEdges().length).toBe(3);
// === add nodes ===
graph.addData('node', [
{
id: 'node7',
data: {
x: 100,
y: 400,
},
},
{
id: 'node8',
data: {
x: 200,
y: 400,
},
},
{
id: 'node9',
data: {
x: 300,
y: 400,
},
},
]);
expect(graph.dataController.graphCore.getAllNodes().length).toBe(9);
graph.addData('edge', [
{
id: 'edge4',
source: 'node7',
target: 'node8',
data: {},
},
{
id: 'edge5',
source: 'node8',
target: 'node9',
data: {},
},
]);
expect(graph.dataController.graphCore.getAllEdges().length).toBe(5);
});
it('removeData', () => {
// === remove node ===
// remoev a node, related edges will be removed in the same time
graph.removeData('node', 'node5');
expect(graph.dataController.graphCore.getAllNodes().length).toBe(8);
expect(graph.dataController.graphCore.getAllEdges().length).toBe(4);
// === remove nodes ===
graph.removeData('node', ['node7', 'node8']);
expect(graph.dataController.graphCore.getAllNodes().length).toBe(6);
expect(graph.dataController.graphCore.getAllEdges().length).toBe(2);
// === remove edge ===
graph.removeData('edge', 'edge1');
expect(graph.dataController.graphCore.getAllEdges().length).toBe(1);
});
it('getNodeData', () => {
const foundNode = graph.getNodeData('node1');
expect(foundNode).toBe(graph.dataController.graphCore.getNode('node1'));
const inexisNode = graph.getNodeData('inexistnode');
expect(inexisNode).toBe(undefined);
});
it('getEdgeData', () => {
const foundEdge = graph.getEdgeData('edge2');
expect(foundEdge).toBe(graph.dataController.graphCore.getEdge('edge2'));
const removedEdge = graph.getEdgeData('edge1');
expect(removedEdge).toBe(undefined);
const inexisEdge = graph.getEdgeData('inexistedge');
expect(inexisEdge).toBe(undefined);
});
it('getAllNodesData getAllEdgesDat', () => {
expect(graph.getAllNodesData().length).toBe(6);
expect(graph.getAllEdgesData().length).toBe(1);
});
it('changeData with replace', () => {
const newData = {
nodes: [
{ id: 'node1', data: { x: 100, y: 200 } },
{ id: 'node2', data: { x: 400, y: 450 } },
{ id: 'node11', data: { x: 300, y: 200 } },
{ id: 'node12', data: { x: 300, y: 250 } },
],
edges: [
{ id: 'edge1', source: 'node1', target: 'node2', data: {} },
{ id: 'edge11', source: 'node1', target: 'node11', data: {} },
],
};
graph.changeData(newData, 'replace');
expect(graph.getNodeData('node2').data.x).toBe(400);
expect(graph.getNodeData('node2').data.y).toBe(450);
expect(graph.getNodeData('node3')).toBe(undefined);
expect(graph.getNodeData('node11')).not.toBe(undefined);
expect(graph.getNodeData('node12')).not.toBe(undefined);
});
it('changeData with mergeReplace', () => {
const newData = {
nodes: [{ id: 'node13', data: { x: 50, y: 50 } }],
edges: [{ id: 'edge1', source: 'node13', target: 'node13', data: {} }],
};
graph.changeData(newData, 'mergeReplace');
const allNodes = graph.getAllNodesData();
expect(allNodes.length).toBe(1);
expect(allNodes[0].id).toBe('node13');
const allEdges = graph.getAllEdgesData();
expect(allEdges.length).toBe(1);
expect(allEdges[0].id).toBe('edge1');
expect(allEdges[0].source).toBe('node13');
expect(allEdges[0].target).toBe('node13');
graph.destroy();
expect(graph.destroyed).toBe(true);
// expect(graph.canvas.destroyed).toBe(true);
// expect(graph.backgroundCanvas.destroyed).toBe(true);
// expect(graph.transientCanvas.destroyed).toBe(true);
});
});

View File

@ -0,0 +1,911 @@
// @ts-nocheck
import { DisplayObject } from '@antv/g';
import { clone } from '@antv/util';
import G6, { EdgeDisplayModel, Graph, GraphData, IGraph, NodeDisplayModel } from '../../src/index';
import { LineEdge } from '../../src/stdlib/item/edge';
import { CircleNode } from '../../src/stdlib/item/node';
import { BaseNode } from '../../src/stdlib/item/node/base';
import { NodeModelData, NodeShapeMap } from '../../src/types/node';
import { extend } from '../../src/util/extend';
import { upsertShape } from '../../src/util/shape';
const container = document.createElement('div');
document.querySelector('body').appendChild(container);
describe('node item', () => {
let graph: IGraph<any>;
it('new graph with one node', (done) => {
graph = new G6.Graph({
container,
width: 500,
height: 500,
type: 'graph',
data: {
nodes: [
{
id: 'node1',
data: { x: 100, y: 200 },
},
],
},
});
graph.on('afterrender', () => {
const nodeItem = graph.itemController.itemMap['node1'];
expect(nodeItem).not.toBe(undefined);
expect(nodeItem.shapeMap.labelShape).toBe(undefined);
done();
});
});
it('update node label', () => {
graph.updateData('node', {
id: 'node1',
data: {
labelShape: {
text: 'node-label',
position: 'left',
background: {},
},
},
});
const nodeItem = graph.itemController.itemMap['node1'];
expect(nodeItem.shapeMap.labelShape).not.toBe(undefined);
expect(nodeItem.shapeMap.labelShape.attributes.text).toBe('node-label');
expect(nodeItem.shapeMap.labelShape.attributes.fill).toBe('#000');
expect(nodeItem.shapeMap.labelBgShape).not.toBe(undefined);
const labelBounds = nodeItem.shapeMap.labelShape.getGeometryBounds();
expect(nodeItem.shapeMap.labelBgShape.attributes.x).toBe(
labelBounds.min[0] + nodeItem.shapeMap.labelShape.attributes.x - 4,
);
expect(nodeItem.shapeMap.labelBgShape.attributes.y).toBe(
labelBounds.min[1] + nodeItem.shapeMap.labelShape.attributes.y - 4,
);
graph.updateData('node', {
id: 'node1',
data: {
labelShape: {
fill: '#00f',
},
},
});
expect(nodeItem.shapeMap.labelShape.attributes.fill).toBe('#00f');
graph.updateData('node', {
id: 'node1',
data: {
labelShape: undefined,
},
});
expect(nodeItem.shapeMap.labelShape).toBe(undefined);
expect(nodeItem.shapeMap.labelBgShape).toBe(undefined);
});
it('update node icon', () => {
graph.updateData('node', {
id: 'node1',
data: {
iconShape: {
img: 'https://gw.alipayobjects.com/zos/basement_prod/012bcf4f-423b-4922-8c24-32a89f8c41ce.svg',
},
},
});
const nodeItem = graph.itemController.itemMap['node1'];
expect(nodeItem.shapeMap.iconShape).not.toBe(undefined);
expect(nodeItem.shapeMap.iconShape.attributes.width).toBe(15);
expect(nodeItem.shapeMap.iconShape.nodeName).toBe('image');
graph.updateData('node', {
id: 'node1',
data: {
iconShape: {
text: 'A',
fill: '#fff',
fontWeight: 500,
},
},
});
expect(nodeItem.shapeMap.iconShape).not.toBe(undefined);
expect(nodeItem.shapeMap.iconShape.attributes.text).toBe('A');
expect(nodeItem.shapeMap.iconShape.attributes.fontSize).toBe(12);
expect(nodeItem.shapeMap.iconShape.nodeName).toBe('text');
graph.destroy();
});
});
describe('edge item', () => {
let graph: IGraph<any>;
it('new graph with two nodes and one edge', (done) => {
graph = new G6.Graph({
container,
width: 500,
height: 500,
type: 'graph',
data: {
nodes: [
{
id: 'node1',
data: { x: 100, y: 100, keyShape: { opacity: 0.1 } },
},
{
id: 'node2',
data: { x: 300, y: 300 },
},
],
edges: [
{
id: 'edge1',
source: 'node1',
target: 'node2',
data: {},
},
],
},
});
graph.on('afterrender', () => {
const edgeItem = graph.itemController.itemMap['edge1'];
expect(edgeItem).not.toBe(undefined);
expect(edgeItem.shapeMap.labelShape).toBe(undefined);
done();
});
});
it('update edge label', () => {
const padding = [4, 8, 4, 8];
graph.updateData('edge', {
id: 'edge1',
data: {
labelShape: {
text: 'edge-label',
position: 'middle',
background: {
radius: 10,
padding,
},
},
},
});
const edgeItem = graph.itemController.itemMap['edge1'];
expect(edgeItem.shapeMap.labelShape).not.toBe(undefined);
expect(edgeItem.shapeMap.labelShape.attributes.text).toBe('edge-label');
expect(edgeItem.shapeMap.labelShape.attributes.fill).toBe('#000');
expect(edgeItem.shapeMap.labelShape.attributes.transform).toBe('rotate(45)');
expect(edgeItem.shapeMap.labelBgShape.attributes.transform).toBe('rotate(45)');
let labelBounds = edgeItem.shapeMap.labelShape.getGeometryBounds();
expect(edgeItem.shapeMap.labelBgShape.attributes.width).toBe(
labelBounds.max[0] - labelBounds.min[0] + padding[1] + padding[3],
);
expect(edgeItem.shapeMap.labelBgShape.attributes.height).toBe(
labelBounds.max[1] - labelBounds.min[1] + padding[0] + padding[2],
);
graph.updateData('edge', {
id: 'edge1',
data: {
labelShape: {
fill: '#00f',
position: 'start',
},
},
});
expect(edgeItem.shapeMap.labelShape.attributes.fill).toBe('#00f');
expect(
edgeItem.shapeMap.labelShape.attributes.x - edgeItem.shapeMap.labelBgShape.attributes.x,
).toBe(padding[3]);
labelBounds = edgeItem.shapeMap.labelShape.getGeometryBounds();
const labelWidth = labelBounds.max[0] - labelBounds.min[0];
const labelHeight = labelBounds.max[1] - labelBounds.min[1];
const labelBgBounds = edgeItem.shapeMap.labelBgShape.getGeometryBounds();
const labelBgWidth = labelBgBounds.max[0] - labelBgBounds.min[0];
const labelBgHeight = labelBgBounds.max[1] - labelBgBounds.min[1];
expect(labelBgWidth - labelWidth).toBe(padding[1] + padding[3]);
expect(labelBgHeight - labelHeight).toBe(padding[0] + padding[2]);
graph.updateData('edge', {
id: 'edge1',
data: {
labelShape: undefined,
},
});
expect(edgeItem.shapeMap.labelShape).toBe(undefined);
expect(edgeItem.shapeMap.labelBgShape).toBe(undefined);
});
it('update edge icon', () => {
// add image icon to follow the label at path's center
graph.updateData('edge', {
id: 'edge1',
data: {
labelShape: {
text: 'abcddd',
fill: '#f00',
position: 'center',
},
iconShape: {
img: 'https://gw.alipayobjects.com/zos/basement_prod/012bcf4f-423b-4922-8c24-32a89f8c41ce.svg',
// text: 'A',
fill: '#fff',
},
},
});
const edgeItem = graph.itemController.itemMap['edge1'];
let labelShape = edgeItem.shapeMap['labelShape'];
let iconShape = edgeItem.shapeMap['iconShape'];
expect(iconShape.attributes.x + iconShape.attributes.width + 4).toBe(
labelShape.getGeometryBounds().min[0] + labelShape.attributes.x,
);
expect(iconShape.attributes.transform).toBe(labelShape.attributes.transform);
expect(iconShape.attributes.y + iconShape.attributes.height / 2 - 2).toBe(
labelShape.getGeometryBounds().center[1] + labelShape.attributes.y,
);
// update icon to be a text
graph.updateData('edge', {
id: 'edge1',
data: {
iconShape: {
text: 'A',
fill: '#fff',
fontWeight: 500,
},
},
});
labelShape = edgeItem.shapeMap['labelShape'];
iconShape = edgeItem.shapeMap['iconShape'];
expect(iconShape.attributes.x + iconShape.attributes.width + 4).toBe(
labelShape.getGeometryBounds().min[0] + labelShape.attributes.x,
);
expect(iconShape.attributes.transform).toBe(labelShape.attributes.transform);
expect(iconShape.attributes.y + iconShape.attributes.height / 2 - 2).toBe(
labelShape.getGeometryBounds().center[1] + labelShape.attributes.y,
);
// move label to the start, and the icon follows
graph.updateData('edge', {
id: 'edge1',
data: {
labelShape: {
position: 'start',
},
},
});
labelShape = edgeItem.shapeMap['labelShape'];
iconShape = edgeItem.shapeMap['iconShape'];
expect(iconShape.attributes.x + iconShape.attributes.width + 4).toBe(
labelShape.getGeometryBounds().min[0] + labelShape.attributes.x,
);
expect(iconShape.attributes.transform).toBe(labelShape.attributes.transform);
expect(iconShape.attributes.y + iconShape.attributes.height / 2 - 2).toBe(
labelShape.getGeometryBounds().center[1] + labelShape.attributes.y,
);
graph.destroy();
});
});
describe('node mapper', () => {
const data = {
nodes: [
{
id: 'node1',
data: { x: 100, y: 200, buStatus: true, buType: 1 },
},
{
id: 'node2',
data: { x: 100, y: 300, buStatus: false, buType: 0 },
},
],
};
const graphConfig = {
container,
width: 500,
height: 500,
type: 'graph',
};
it('function mapper', (done) => {
const graph = new G6.Graph({
...graphConfig,
node: (innerModel) => {
const { x, y, buStatus } = innerModel.data;
return {
...innerModel,
data: {
x,
y,
keyShape: {
fill: buStatus ? '#0f0' : '#f00',
},
},
};
},
} as any);
graph.read(clone(data));
graph.on('afterrender', () => {
const node1 = graph.itemController.itemMap['node1'];
expect(node1.shapeMap.keyShape.attributes.fill).toBe('#0f0');
let node2 = graph.itemController.itemMap['node2'];
expect(node2.shapeMap.keyShape.attributes.fill).toBe('#f00');
// update user data
graph.updateData('node', {
id: 'node2',
data: {
buStatus: true,
},
});
node2 = graph.itemController.itemMap['node2'];
expect(node2.shapeMap.keyShape.attributes.fill).toBe('#0f0');
graph.destroy();
done();
});
});
it('value and encode mapper', (done) => {
const graph = new G6.Graph({
...graphConfig,
node: {
keyShape: {
fill: {
fields: ['buStatus'],
formatter: (innerModel) => (innerModel.data.buStatus ? '#0f0' : '#f00'),
},
lineWidth: 5,
stroke: {
fields: ['buStatus', 'buType'],
formatter: (innerModel) =>
innerModel.data.buStatus || innerModel.data.buType ? '#fff' : '#000',
},
},
labelShape: {},
},
} as any);
graph.read(clone(data));
graph.on('afterrender', () => {
const node1 = graph.itemController.itemMap['node1'];
expect(node1.shapeMap.keyShape.attributes.fill).toBe('#0f0');
expect(node1.shapeMap.keyShape.attributes.lineWidth).toBe(5);
expect(node1.shapeMap.keyShape.attributes.stroke).toBe('#fff');
let node2 = graph.itemController.itemMap['node2'];
expect(node2.shapeMap.keyShape.attributes.fill).toBe('#f00');
expect(node2.shapeMap.keyShape.attributes.lineWidth).toBe(5);
expect(node2.shapeMap.keyShape.attributes.stroke).toBe('#000');
// update user data
graph.updateData('node', {
id: 'node2',
data: {
buStatus: true,
},
});
node2 = graph.itemController.itemMap['node2'];
expect(node2.shapeMap.keyShape.attributes.fill).toBe('#0f0');
expect(node2.shapeMap.keyShape.attributes.stroke).toBe('#fff');
graph.destroy();
done();
});
});
});
describe('edge mapper', () => {
const data = {
nodes: [
{
id: 'node1',
data: { x: 100, y: 200 },
},
{
id: 'node2',
data: { x: 100, y: 300 },
},
{
id: 'node3',
data: { x: 200, y: 300 },
},
],
edges: [
{
id: 'edge1',
source: 'node1',
target: 'node2',
data: { buStatus: true, buType: 1, buName: 'edge-1' },
},
{
id: 'edge2',
source: 'node1',
target: 'node3',
data: { buStatus: false, buType: 0, buName: 'edge-2' },
},
],
};
const graphConfig = {
container,
width: 500,
height: 500,
type: 'graph',
};
it('function mapper', (done) => {
const graph = new G6.Graph({
...graphConfig,
edge: (innerModel) => {
const { x, y, buStatus } = innerModel.data;
return {
...innerModel,
data: {
x,
y,
keyShape: {
stroke: buStatus ? '#0f0' : '#f00',
},
},
};
},
} as any);
graph.read(clone(data));
graph.on('afterrender', () => {
const edge1 = graph.itemController.itemMap['edge1'];
expect(edge1.shapeMap.keyShape.attributes.stroke).toBe('#0f0');
let edge2 = graph.itemController.itemMap['edge2'];
expect(edge2.shapeMap.keyShape.attributes.stroke).toBe('#f00');
// update user data
graph.updateData('edge', {
id: 'edge2',
data: {
buStatus: true,
},
});
edge2 = graph.itemController.itemMap['edge2'];
expect(edge2.shapeMap.keyShape.attributes.stroke).toBe('#0f0');
graph.destroy();
done();
});
});
it('value and encode mapper', (done) => {
const graph = new G6.Graph({
...graphConfig,
edge: {
keyShape: {
stroke: {
fields: ['buStatus'],
formatter: (innerModel) => (innerModel.data.buStatus ? '#0f0' : '#f00'),
},
lineWidth: 5,
lineDash: {
fields: ['buStatus', 'buType'],
formatter: (innerModel) =>
innerModel.data.buStatus || innerModel.data.buType ? undefined : [5, 5],
},
},
labelShape: {
text: {
fields: ['buName', 'buType'],
formatter: (innerModel) => `${innerModel.data.buName}-${innerModel.data.buType}`,
},
},
},
} as any);
graph.read(clone(data));
graph.on('afterrender', () => {
const edge1 = graph.itemController.itemMap['edge1'];
expect(edge1.shapeMap.keyShape.attributes.stroke).toBe('#0f0');
expect(edge1.shapeMap.keyShape.attributes.lineWidth).toBe(5);
expect(edge1.shapeMap.keyShape.attributes.lineDash).toBe('');
expect(edge1.shapeMap.labelShape.attributes.text).toBe('edge-1-1');
let edge2 = graph.itemController.itemMap['edge2'];
expect(edge2.shapeMap.keyShape.attributes.stroke).toBe('#f00');
expect(edge2.shapeMap.keyShape.attributes.lineWidth).toBe(5);
expect(JSON.stringify(edge2.shapeMap.keyShape.attributes.lineDash)).toBe('[5,5]');
expect(edge2.shapeMap.labelShape.attributes.text).toBe('edge-2-0');
// update user data
graph.updateData('edge', {
id: 'edge2',
data: {
buStatus: true,
buName: 'newedge2name',
},
});
edge2 = graph.itemController.itemMap['edge2'];
expect(edge2.shapeMap.keyShape.attributes.stroke).toBe('#0f0');
expect(edge2.shapeMap.keyShape.attributes.lineDash).toBe('');
expect(edge2.shapeMap.labelShape.attributes.text).toBe('newedge2name-0');
graph.destroy();
done();
});
});
});
describe('register node', () => {
it('custom node extends circle', (done) => {
class CustomNode extends CircleNode {
public defaultStyles = {
keyShape: {
r: 25,
x: 0,
y: 0,
fill: '#ff0',
lineWidth: 0,
stroke: '#0f0',
},
}
constructor() {
super();
this.defaultStyles = Object.assign({}, this.baseDefaultStyles, this.defaultStyles);
}
public drawLabelShape(
model: NodeDisplayModel,
shapeMap: NodeShapeMap,
diffData?: { oldData: NodeModelData; newData: NodeModelData },
) {
const extraShape = upsertShape(
'circle',
'extraShape',
{
r: 4,
fill: '#0f0',
x: -20,
y: 0,
},
shapeMap,
);
const { labelShape: propsLabelStyle } = model.data;
const labelStyle = Object.assign({}, this.defaultStyles.labelShape, propsLabelStyle);
const labelShape = upsertShape(
'text',
'labelShape',
{
...labelStyle,
text: model.id,
},
shapeMap,
);
return { labelShape, extraShape };
}
}
class CustomEdge extends LineEdge {
public afterDraw(
model: EdgeDisplayModel,
shapeMap: { [shapeId: string]: DisplayObject<any, any> },
shapesChanged?: string[],
): { [otherShapeId: string]: DisplayObject } {
const { keyShape } = shapeMap;
const point = keyShape.getPoint(0.3);
return {
buShape: upsertShape(
'rect',
'buShape',
{
width: 6,
height: 6,
x: point.x,
y: point.y,
fill: '#0f0',
},
shapeMap,
),
};
}
}
const CustomGraph = extend(G6.Graph, {
nodes: {
'custom-node': CustomNode,
},
edges: {
'custom-edge': CustomEdge,
},
});
// TODO: G6.Graph is modified unexpectively
const graph = new CustomGraph({
container,
width: 500,
height: 500,
type: 'graph',
data: {
nodes: [
{
id: 'node1',
data: { x: 100, y: 200, type: 'custom-node' },
},
{
id: 'node2',
data: { x: 100, y: 300, type: 'circle-node' },
},
{
id: 'node3',
data: { x: 200, y: 300, labelShape: undefined },
},
],
edges: [
{
id: 'edge1',
source: 'node1',
target: 'node2',
data: { type: 'custom-edge' },
},
{
id: 'edge2',
source: 'node1',
target: 'node3',
data: {},
},
],
},
node: {
// affect the nodes without type field in their data object, which means configurations in the user data has higher priority than that in the mapper
type: 'custom-node',
// affect the nodes without labelShape field in their data object, which means configurations in the user data has higher priority than that in the mapper
labelShape: {}
},
});
graph.on('afterrender', () => {
const node1 = graph.itemController.itemMap['node1'];
expect(node1.shapeMap.extraShape).not.toBe(undefined);
expect(node1.shapeMap.keyShape.style.r).toBe(25);
const node2 = graph.itemController.itemMap['node2'];
expect(node2.shapeMap.extraShape).toBe(undefined);
const node3 = graph.itemController.itemMap['node3'];
// labelShape is assigned with undefined in node3's data, shapes defined in drawLabelShape will be undefined
expect(node3.shapeMap.extraShape).toBe(undefined);
expect(node3.shapeMap.keyShape.style.r).toBe(25);
const edge1 = graph.itemController.itemMap['edge1'];
expect(edge1.shapeMap.buShape).not.toBe(undefined);
const edge2 = graph.itemController.itemMap['edge2'];
expect(edge2.shapeMap.buShape).toBe(undefined);
// update node type
graph.updateData('node', {
id: 'node2',
data: {
type: 'custom-node'
}
});
expect(node2.shapeMap.extraShape).not.toBe(undefined);
expect(node2.shapeMap.keyShape.style.r).toBe(25);
// update edge type
graph.updateData('edge', {
id: 'edge2',
data: {
type: 'custom-edge'
}
});
expect(edge2.shapeMap.buShape).not.toBe(undefined);
graph.destroy();
done();
});
});
it('update node type with different keyShape shape type and other shapes', (done) => {
class CustomNode extends CircleNode {
public defaultStyles = {
keyShape: {
width: 50,
height: 25,
x: 0,
y: 0,
fill: '#ff0',
lineWidth: 0,
stroke: '#0f0',
opacity: 0.6
},
}
constructor() {
super();
this.defaultStyles = Object.assign({}, this.baseDefaultStyles, this.defaultStyles);
}
public drawKeyShape(model: NodeDisplayModel, shapeMap: NodeShapeMap, diffData?: { oldData: NodeModelData; newData: NodeModelData; }) {
const keyShapeStyle = Object.assign({}, this.defaultStyles.keyShape, model.data.labelShape);
const keyShape = upsertShape(
'rect',
'keyShape',
keyShapeStyle,
shapeMap
);
return keyShape
}
public drawOtherShapes(model: NodeDisplayModel, shapeMap: NodeShapeMap, diffData?: { oldData: NodeModelData; newData: NodeModelData; }) {
console.log('drawothershape')
const testShape = upsertShape(
'line',
'testShape',
{
x1: 0,
y1: 10,
x2: 50,
y2: 10,
stroke: '#f00',
lineWidth: 2
},
shapeMap
);
return { testShape };
}
}
const CustomGraph = extend(G6.Graph, {
nodes: {
'custom-node': CustomNode,
},
});
// TODO: G6.Graph is modified unexpectively
const graph = new CustomGraph({
container,
width: 500,
height: 500,
type: 'graph',
data: {
nodes: [
{
id: 'node1',
data: { x: 100, y: 200, type: 'custom-node' },
},
{
id: 'node2',
data: { x: 100, y: 300, type: 'circle-node' },
},
{
id: 'node3',
data: { x: 200, y: 300 },
},
],
edges: [
{
id: 'edge1',
source: 'node1',
target: 'node2',
data: { },
},
],
},
node: {
// affect the nodes without type field in their data object, which means configurations in the user data has higher priority than that in the mapper
type: 'custom-node',
// affect the nodes without labelShape field in their data object, which means configurations in the user data has higher priority than that in the mapper
labelShape: {},
otherShapes: {}
},
});
graph.on('afterrender', e => {
const node1 = graph.itemController.itemMap['node1'];
expect(node1.shapeMap.testShape).not.toBe(undefined);
expect(node1.shapeMap.keyShape.nodeName).toBe('rect');
const node3 = graph.itemController.itemMap['node3'];
expect(node3.shapeMap.testShape).not.toBe(undefined);
expect(node3.shapeMap.keyShape.nodeName).toBe('rect');
const node2 = graph.itemController.itemMap['node2'];
expect(node2.shapeMap.testShape).toBe(undefined);
expect(node2.shapeMap.keyShape.nodeName).toBe('circle');
console.log('goingto update')
// update circle-node to custom-node
graph.updateData('node', {
id: 'node2',
data: {
type: 'custom-node'
}
});
const node2 = graph.itemController.itemMap['node2'];
expect(node2.shapeMap.testShape).not.toBe(undefined);
expect(node2.shapeMap.keyShape.nodeName).toBe('rect');
graph.destroy();
done()
});
});
});
describe('state', () => {
it('node state', (done) => {
const graph = new Graph({
container,
width: 500,
height: 500,
type: 'graph',
data: {
nodes: [
{
id: 'node1',
data: { x: 100, y: 200 },
},
{
id: 'node2',
data: { x: 100, y: 300 },
},
{
id: 'node3',
data: { x: 200, y: 300 },
},
],
edges: [
{
id: 'edge1',
source: 'node1',
target: 'node2',
data: { },
},
{
id: 'edge2',
source: 'node1',
target: 'node3',
data: {},
},
],
},
nodeState: {
selected: {
keyShape: {
stroke: '#0f0',
lineWidth: 2
}
},
highlight: {
keyShape: {
stroke: '#00f',
r: 30,
opacity: 0.5
}
}
}
});
graph.on('afterrender', () => {
expect(graph.findIdByState('node', 'selected').length).toBe(0);
graph.setItemState('node1', 'selected', true);
expect(graph.findIdByState('node', 'selected').length).toBe(1);
expect(graph.findIdByState('node', 'selected')[0]).toBe('node1');
expect(graph.itemController.itemMap['node1'].shapeMap.keyShape.style.lineWidth).toBe(2);
expect(graph.itemController.itemMap['node1'].shapeMap.keyShape.style.stroke).toBe('#0f0');
graph.setItemState('node1', 'selected', false);
expect(graph.findIdByState('node', 'selected').length).toBe(0);
expect(graph.itemController.itemMap['node1'].shapeMap.keyShape.style.lineWidth).toBe(0);
// set multiple nodes state
graph.setItemState(['node1', 'node2'], 'selected', true);
expect(graph.findIdByState('node', 'selected').length).toBe(2);
expect(graph.itemController.itemMap['node1'].shapeMap.keyShape.style.lineWidth).toBe(2);
expect(graph.itemController.itemMap['node1'].shapeMap.keyShape.style.stroke).toBe('#0f0');
expect(graph.itemController.itemMap['node2'].shapeMap.keyShape.style.lineWidth).toBe(2);
expect(graph.itemController.itemMap['node2'].shapeMap.keyShape.style.stroke).toBe('#0f0');
graph.setItemState('node1', 'selected', false);
expect(graph.findIdByState('node', 'selected').length).toBe(1);
expect(graph.findIdByState('node', 'selected')[0]).toBe('node2');
expect(graph.itemController.itemMap['node1'].shapeMap.keyShape.style.lineWidth).toBe(0);
graph.setItemState(['node1', 'node2'], 'selected', false);
expect(graph.findIdByState('node', 'selected').length).toBe(0);
expect(graph.itemController.itemMap['node2'].shapeMap.keyShape.style.lineWidth).toBe(0);
// // set multiple states
graph.setItemState(['node1', 'node2'], ['selected', 'highlight'], true);
expect(graph.findIdByState('node', 'selected').length).toBe(2);
expect(graph.findIdByState('node', 'highlight').length).toBe(2);
// should be merged styles from selected and highlight
expect(graph.itemController.itemMap['node1'].shapeMap.keyShape.style.lineWidth).toBe(2);
expect(graph.itemController.itemMap['node1'].shapeMap.keyShape.style.stroke).toBe('#00f');
expect(graph.itemController.itemMap['node1'].shapeMap.keyShape.style.r).toBe(30);
expect(graph.itemController.itemMap['node1'].shapeMap.keyShape.style.opacity).toBe(0.5);
expect(graph.itemController.itemMap['node2'].shapeMap.keyShape.style.lineWidth).toBe(2);
expect(graph.itemController.itemMap['node2'].shapeMap.keyShape.style.stroke).toBe('#00f');
expect(graph.itemController.itemMap['node2'].shapeMap.keyShape.style.r).toBe(30);
expect(graph.itemController.itemMap['node2'].shapeMap.keyShape.style.opacity).toBe(0.5);
// clear states
graph.clearItemState(['node1', 'node2']);
expect(graph.findIdByState('node', 'selected').length).toBe(0);
expect(graph.findIdByState('node', 'highlight').length).toBe(0);
expect(graph.itemController.itemMap['node1'].shapeMap.keyShape.style.r).toBe(15);
expect(graph.itemController.itemMap['node1'].shapeMap.keyShape.style.lineWidth).toBe(0);
expect(graph.itemController.itemMap['node1'].shapeMap.keyShape.style.opacity).toBe(1);
graph.destroy();
done();
})
});
// TODO:
xit('edge state', () => {});
xit('custom node with setState', () => {});
xit('custom edge with setState', () => {});
});

View File

@ -12,18 +12,25 @@
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["esnext", "dom"],
"types": ["jest"],
"lib": [
"esnext",
"dom"
],
"types": [
"jest"
],
"skipLibCheck": true
},
"include": ["src", "tests"],
"include": [
"src",
"tests"
],
"typedocOptions": {
"mode": "modules",
"out": "docs/api-ts",
"excludePrivate": true,
"excludeProtected": true,
"excludeExternals": true,
"plugin": "typedoc-plugin-markdown",
"theme": "docusaurus2"
"theme": "markdown"
}
}
}