From 875a9eb39b40dbc661942e4e8fce93c5b5f17b87 Mon Sep 17 00:00:00 2001 From: Yanyan Wang Date: Wed, 1 Mar 2023 14:21:55 +0800 Subject: [PATCH] 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 --- packages/g6/docs/api-ts/.nojekyll | 1 + .../g6/docs/api-ts/{index.md => README.md} | 8 +- .../g6/docs/api-ts/classes/_graph_.graph.md | 834 ------------ packages/g6/docs/api-ts/classes/default.md | 1115 +++++++++++++++++ packages/g6/docs/api-ts/globals.md | 13 - packages/g6/docs/api-ts/modules.md | 9 + packages/g6/docs/api-ts/modules/_graph_.md | 13 - packages/g6/package.json | 7 +- packages/g6/src/item/edge.ts | 93 +- packages/g6/src/item/item.ts | 436 ++++++- packages/g6/src/item/node.ts | 55 +- packages/g6/src/runtime/controller/data.ts | 576 ++++++--- .../g6/src/runtime/controller/interaction.ts | 1 - packages/g6/src/runtime/controller/item.ts | 337 ++++- packages/g6/src/runtime/graph.ts | 454 +++++-- packages/g6/src/stdlib/index.ts | 16 +- packages/g6/src/stdlib/item/edge/base.ts | 231 ++++ packages/g6/src/stdlib/item/edge/index.ts | 1 + packages/g6/src/stdlib/item/edge/line.ts | 70 ++ packages/g6/src/stdlib/item/node/base.ts | 205 +++ packages/g6/src/stdlib/item/node/circle.ts | 61 + packages/g6/src/stdlib/item/node/index.ts | 1 + packages/g6/src/types/combo.ts | 33 +- packages/g6/src/types/data.ts | 4 +- packages/g6/src/types/edge.ts | 43 +- packages/g6/src/types/graph.ts | 257 ++-- packages/g6/src/types/hook.ts | 31 +- packages/g6/src/types/item.ts | 77 +- packages/g6/src/types/node.ts | 53 +- packages/g6/src/types/spec.ts | 15 +- packages/g6/src/util/array.ts | 9 + packages/g6/src/util/canvas.ts | 61 + packages/g6/src/util/extend.ts | 16 +- packages/g6/src/util/extension.ts | 3 +- packages/g6/src/util/index.ts | 3 +- packages/g6/src/util/shape.ts | 173 +++ packages/g6/src/util/type.ts | 3 + packages/g6/tests/unit/behavior-spec.ts | 10 + packages/g6/tests/unit/data-spec.ts | 322 +++++ packages/g6/tests/unit/item-spec.ts | 911 ++++++++++++++ packages/g6/tsconfig.json | 19 +- 41 files changed, 5153 insertions(+), 1427 deletions(-) create mode 100644 packages/g6/docs/api-ts/.nojekyll rename packages/g6/docs/api-ts/{index.md => README.md} (98%) delete mode 100644 packages/g6/docs/api-ts/classes/_graph_.graph.md create mode 100644 packages/g6/docs/api-ts/classes/default.md delete mode 100644 packages/g6/docs/api-ts/globals.md create mode 100644 packages/g6/docs/api-ts/modules.md delete mode 100644 packages/g6/docs/api-ts/modules/_graph_.md create mode 100644 packages/g6/src/stdlib/item/edge/base.ts create mode 100644 packages/g6/src/stdlib/item/edge/index.ts create mode 100644 packages/g6/src/stdlib/item/edge/line.ts create mode 100644 packages/g6/src/stdlib/item/node/base.ts create mode 100644 packages/g6/src/stdlib/item/node/circle.ts create mode 100644 packages/g6/src/stdlib/item/node/index.ts create mode 100644 packages/g6/src/util/array.ts create mode 100644 packages/g6/src/util/canvas.ts create mode 100644 packages/g6/src/util/shape.ts create mode 100644 packages/g6/src/util/type.ts create mode 100644 packages/g6/tests/unit/data-spec.ts create mode 100644 packages/g6/tests/unit/item-spec.ts diff --git a/packages/g6/docs/api-ts/.nojekyll b/packages/g6/docs/api-ts/.nojekyll new file mode 100644 index 0000000000..e2ac6616ad --- /dev/null +++ b/packages/g6/docs/api-ts/.nojekyll @@ -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. \ No newline at end of file diff --git a/packages/g6/docs/api-ts/index.md b/packages/g6/docs/api-ts/README.md similarity index 98% rename from packages/g6/docs/api-ts/index.md rename to packages/g6/docs/api-ts/README.md index c5e4cf8a77..a1d935cb93 100644 --- a/packages/g6/docs/api-ts/index.md +++ b/packages/g6/docs/api-ts/README.md @@ -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:图分析引擎 diff --git a/packages/g6/docs/api-ts/classes/_graph_.graph.md b/packages/g6/docs/api-ts/classes/_graph_.graph.md deleted file mode 100644 index d393ded043..0000000000 --- a/packages/g6/docs/api-ts/classes/_graph_.graph.md +++ /dev/null @@ -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 - -* IGraph‹B› - -## 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`: Specification‹B›): *[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` | Specification‹B› | - -**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`: BehaviorOptionsOf‹B›[], `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` | BehaviorOptionsOf‹B›[] | 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**(): *Record‹string, 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:** *Record‹string, 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**(): *Specification‹B›* - -*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:** *Specification‹B›* - -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`: BehaviorObjectOptionsOf‹B›, `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` | BehaviorObjectOptionsOf‹B› | behavior configs, whose name indicates the behavior to be updated | -`mode?` | string | mode name | - -**Returns:** *void* - -___ - -### updateItem - -▸ **updateItem**(`itemType`: ITEM_TYPE, `models`: Partial‹NodeUserModel› | Partial‹EdgeUserModel› | Partial‹ComboUserModel | Partial‹NodeUserModel›[] | Partial‹EdgeUserModel›[] | Partial‹ComboUserModel›[]›, `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` | Partial‹NodeUserModel› | Partial‹EdgeUserModel› | Partial‹ComboUserModel | Partial‹NodeUserModel›[] | Partial‹EdgeUserModel›[] | Partial‹ComboUserModel›[]› | - | -`stack?` | boolean | 本次操作是否入栈,默认为 true | - -**Returns:** *boolean* - -___ - -### updateSpecification - -▸ **updateSpecification**(`spec`: Specification‹B›): *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` | Specification‹B› | - -**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* diff --git a/packages/g6/docs/api-ts/classes/default.md b/packages/g6/docs/api-ts/classes/default.md new file mode 100644 index 0000000000..d2f4ff7005 --- /dev/null +++ b/packages/g6/docs/api-ts/classes/default.md @@ -0,0 +1,1115 @@ +[@antv/g6](../README.md) / [Exports](../modules.md) / default + +# Class: default + +## Type parameters + +| Name | Type | +| :------ | :------ | +| `B` | extends `BehaviorRegistry` | + +## Hierarchy + +- `default` + + ↳ **`default`** + +## Implements + +- `IGraph`<`B`\> + +## Table of contents + +### Constructors + +- [constructor](default.md#constructor) + +### Properties + +- [canvas](default.md#canvas) +- [destroyed](default.md#destroyed) +- [hooks](default.md#hooks) + +### Interaction + +- [addBehaviors](default.md#addbehaviors) +- [removeBehaviors](default.md#removebehaviors) +- [setMode](default.md#setmode) +- [updateBehavior](default.md#updatebehavior) + +### Data + +- [addData](default.md#adddata) +- [changeData](default.md#changedata) +- [getAllCombosData](default.md#getallcombosdata) +- [getAllEdgesData](default.md#getalledgesdata) +- [getAllNodesData](default.md#getallnodesdata) +- [getComboData](default.md#getcombodata) +- [getEdgeData](default.md#getedgedata) +- [getNodeData](default.md#getnodedata) +- [read](default.md#read) +- [removeData](default.md#removedata) +- [updateData](default.md#updatedata) + +### Methods + +- [clear](default.md#clear) +- [getSpecification](default.md#getspecification) +- [updateSpecification](default.md#updatespecification) + +### Item + +- [clearItemState](default.md#clearitemstate) +- [findIdByState](default.md#findidbystate) +- [hideItem](default.md#hideitem) +- [setItemState](default.md#setitemstate) +- [showItem](default.md#showitem) + +### Combo + +- [collapseCombo](default.md#collapsecombo) +- [createCombo](default.md#createcombo) +- [expandCombo](default.md#expandcombo) +- [uncombo](default.md#uncombo) + +### Graph Instance + +- [destroy](default.md#destroy) + +### View + +- [fitCenter](default.md#fitcenter) +- [fitView](default.md#fitview) +- [focusItem](default.md#focusitem) +- [move](default.md#move) +- [moveTo](default.md#moveto) +- [zoom](default.md#zoom) +- [zoomTo](default.md#zoomto) + +### Layout + +- [layout](default.md#layout) + +## Constructors + +### constructor + +• **new default**<`B`\>(`spec`) + +#### Type parameters + +| Name | Type | +| :------ | :------ | +| `B` | extends `BehaviorRegistry` | + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `spec` | `Specification`<`B`\> | + +#### Overrides + +EventEmitter.constructor + +#### Defined in + +[graph.ts:55](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L55) + +## Properties + +### canvas + +• **canvas**: `Canvas` + +#### Implementation of + +IGraph.canvas + +#### Defined in + +[graph.ts:38](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L38) + +___ + +### destroyed + +• **destroyed**: `boolean` + +#### Implementation of + +IGraph.destroyed + +#### Defined in + +[graph.ts:40](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L40) + +___ + +### hooks + +• **hooks**: `Hooks` + +#### Implementation of + +IGraph.hooks + +#### Defined in + +[graph.ts:36](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L36) + +## Interaction + +### addBehaviors + +▸ **addBehaviors**(`behaviors`, `modes`): `void` + +Add behavior(s) to mode(s). + +#### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `behaviors` | `BehaviorOptionsOf`<`B`\>[] | behavior names or configs | +| `modes` | `string` \| `string`[] | mode names | + +#### Returns + +`void` + +#### Implementation of + +IGraph.addBehaviors + +#### Defined in + +[graph.ts:576](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L576) + +___ + +### removeBehaviors + +▸ **removeBehaviors**(`behaviorKeys`, `modes`): `void` + +Remove behavior(s) from mode(s). + +#### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `behaviorKeys` | `string`[] | - | +| `modes` | `string` \| `string`[] | mode names | + +#### Returns + +`void` + +#### Implementation of + +IGraph.removeBehaviors + +#### Defined in + +[graph.ts:596](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L596) + +___ + +### setMode + +▸ **setMode**(`mode`): `void` + +Switch mode. + +#### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `mode` | `string` | mode name | + +#### Returns + +`void` + +#### Implementation of + +IGraph.setMode + +#### Defined in + +[graph.ts:565](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L565) + +___ + +### updateBehavior + +▸ **updateBehavior**(`behavior`, `mode?`): `void` + +Update a behavior on a mode. + +#### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `behavior` | `BehaviorObjectOptionsOf`<`B`\> | behavior configs, whose name indicates the behavior to be updated | +| `mode?` | `string` | mode name | + +#### Returns + +`void` + +#### Implementation of + +IGraph.updateBehavior + +#### Defined in + +[graph.ts:621](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L621) + +## Data + +### addData + +▸ **addData**(`itemType`, `models`, `stack?`): `EdgeModel` \| `NodeModel` \| `ComboModel` \| `NodeModel`[] \| `EdgeModel`[] \| `ComboModel`[] + +Add one or more node/edge/combo data to the graph. + +#### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `itemType` | `ITEM_TYPE` | item type | +| `models` | `EdgeUserModel` \| `NodeUserModel` \| `ComboUserModel` \| `NodeUserModel`[] \| `EdgeUserModel`[] \| `ComboUserModel`[] | - | +| `stack?` | `boolean` | whether push this operation to stack | + +#### Returns + +`EdgeModel` \| `NodeModel` \| `ComboModel` \| `NodeModel`[] \| `EdgeModel`[] \| `ComboModel`[] + +whether success + +#### Implementation of + +IGraph.addData + +#### Defined in + +[graph.ts:350](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L350) + +___ + +### changeData + +▸ **changeData**(`data`, `type?`): `void` + +Change graph data. + +#### Parameters + +| Name | Type | Default value | Description | +| :------ | :------ | :------ | :------ | +| `data` | `GraphData` | `undefined` | new data | +| `type` | ``"replace"`` \| ``"mergeReplace"`` | `'mergeReplace'` | 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 + +`void` + +#### Implementation of + +IGraph.changeData + +#### Defined in + +[graph.ts:170](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L170) + +___ + +### getAllCombosData + +▸ **getAllCombosData**(): `ComboModel`[] + +Get all the combos' inner data + +#### Returns + +`ComboModel`[] + +all combos' inner data on the graph + +#### Implementation of + +IGraph.getAllCombosData + +#### Defined in + +[graph.ts:319](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L319) + +___ + +### getAllEdgesData + +▸ **getAllEdgesData**(): `EdgeModel`[] + +Get all the edges' inner data + +#### Returns + +`EdgeModel`[] + +all edges' inner data on the graph + +#### Implementation of + +IGraph.getAllEdgesData + +#### Defined in + +[graph.ts:311](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L311) + +___ + +### getAllNodesData + +▸ **getAllNodesData**(): `NodeModel`[] + +Get all the nodes' inner data + +#### Returns + +`NodeModel`[] + +all nodes' inner data on the graph + +#### Implementation of + +IGraph.getAllNodesData + +#### Defined in + +[graph.ts:303](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L303) + +___ + +### getComboData + +▸ **getComboData**(`condition`): `ComboModel` + +Find an combo's inner data according to id or function. + +#### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `condition` | `Function` \| `ID` | id or condition function | + +#### Returns + +`ComboModel` + +result combo's inner data + +#### Implementation of + +IGraph.getComboData + +#### Defined in + +[graph.ts:294](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L294) + +___ + +### getEdgeData + +▸ **getEdgeData**(`condition`): `EdgeModel` + +Find an edge's inner data according to id or function. + +#### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `condition` | `Function` \| `ID` | id or condition function | + +#### Returns + +`EdgeModel` + +result edge's inner data + +#### Implementation of + +IGraph.getEdgeData + +#### Defined in + +[graph.ts:283](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L283) + +___ + +### getNodeData + +▸ **getNodeData**(`condition`): `NodeModel` + +Find a node's inner data according to id or function. + +#### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `condition` | `Function` \| `ID` | id or condition function | + +#### Returns + +`NodeModel` + +result node's inner data + +#### Implementation of + +IGraph.getNodeData + +#### Defined in + +[graph.ts:273](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L273) + +___ + +### read + +▸ **read**(`data`): `void` + +Input data and render the graph. +If there is old data, diffs and changes it. + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `data` | `GraphData` | + +#### Returns + +`void` + +#### Implementation of + +IGraph.read + +#### Defined in + +[graph.ts:146](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L146) + +___ + +### removeData + +▸ **removeData**(`itemType`, `ids`, `stack?`): `void` + +Remove one or more node/edge/combo data from the graph. + +#### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `itemType` | `ITEM_TYPE` | - | +| `ids` | `ID` \| `ID`[] | - | +| `stack?` | `boolean` | whether push this operation to stack | + +#### Returns + +`void` + +whether success + +#### Implementation of + +IGraph.removeData + +#### Defined in + +[graph.ts:392](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L392) + +___ + +### updateData + +▸ **updateData**(`itemType`, `models`, `stack?`): `EdgeModel` \| `NodeModel` \| `ComboModel` \| `NodeModel`[] \| `EdgeModel`[] \| `ComboModel`[] + +Update one or more node/edge/combo data on the graph. + +#### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `itemType` | `ITEM_TYPE` | - | +| `models` | `Partial`<`NodeUserModel`\> \| `Partial`<`EdgeUserModel`\> \| `Partial`<`ComboUserModel` \| `Partial`<`NodeUserModel`\>[] \| `Partial`<`EdgeUserModel`\>[] \| `Partial`<`ComboUserModel`\>[]\> | - | +| `stack?` | `boolean` | 本次操作是否入栈,默认为 true | + +#### Returns + +`EdgeModel` \| `NodeModel` \| `ComboModel` \| `NodeModel`[] \| `EdgeModel`[] \| `ComboModel`[] + +#### Implementation of + +IGraph.updateData + +#### Defined in + +[graph.ts:417](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L417) + +## Methods + +### clear + +▸ **clear**(): `void` + +Clear the graph, means remove all the items on the graph. + +#### Returns + +`void` + +#### Implementation of + +IGraph.clear + +#### Defined in + +[graph.ts:182](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L182) + +___ + +### getSpecification + +▸ **getSpecification**(): `Specification`<`B`\> + +Get the copy of specs(configurations). + +#### Returns + +`Specification`<`B`\> + +graph specs + +#### Implementation of + +IGraph.getSpecification + +#### Defined in + +[graph.ts:135](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L135) + +___ + +### updateSpecification + +▸ **updateSpecification**(`spec`): `void` + +Update the specs(configurations). + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `spec` | `Specification`<`B`\> | + +#### Returns + +`void` + +#### Implementation of + +IGraph.updateSpecification + +#### Defined in + +[graph.ts:127](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L127) + +## Item + +### clearItemState + +▸ **clearItemState**(`ids`, `states?`): `void` + +Clear all the states for item(s). + +#### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `ids` | `ID` \| `ID`[] | the id(s) for the item(s) to be clear | +| `states?` | `string`[] | the states' names, all the states wil be cleared if states is not assigned | + +#### Returns + +`void` + +#### Implementation of + +IGraph.clearItemState + +#### Defined in + +[graph.ts:496](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L496) + +___ + +### findIdByState + +▸ **findIdByState**(`itemType`, `state`, `additionalFilter?`): `ID`[] + +Find items which has the state. + +#### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `itemType` | `ITEM_TYPE` | item type | +| `state` | `string` | state name | +| `additionalFilter?` | (`item`: `EdgeModel` \| `NodeModel` \| `ComboModel`) => `boolean` | additional filter function | + +#### Returns + +`ID`[] + +items that is the type and has the state + +#### Implementation of + +IGraph.findIdByState + +#### Defined in + +[graph.ts:330](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L330) + +___ + +### hideItem + +▸ **hideItem**(`ids`): `void` + +Hide the item(s). + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `ids` | `ID` \| `ID`[] | + +#### Returns + +`void` + +#### Implementation of + +IGraph.hideItem + +#### Defined in + +[graph.ts:468](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L468) + +___ + +### setItemState + +▸ **setItemState**(`ids`, `states`, `value`): `void` + +Set state for the item. + +#### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `ids` | `ID` \| `ID`[] | - | +| `states` | `string` \| `string`[] | - | +| `value` | `boolean` | state value | + +#### Returns + +`void` + +#### Implementation of + +IGraph.setItemState + +#### Defined in + +[graph.ts:479](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L479) + +___ + +### showItem + +▸ **showItem**(`ids`): `void` + +Show the item(s). + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `ids` | `ID` \| `ID`[] | + +#### Returns + +`void` + +#### Implementation of + +IGraph.showItem + +#### Defined in + +[graph.ts:459](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L459) + +## Combo + +### collapseCombo + +▸ **collapseCombo**(`comboId`, `stack?`): `void` + +Collapse a combo. + +#### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `comboId` | `ID` | combo id or item | +| `stack?` | `boolean` | - | + +#### Returns + +`void` + +#### Implementation of + +IGraph.collapseCombo + +#### Defined in + +[graph.ts:528](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L528) + +___ + +### createCombo + +▸ **createCombo**(`combo`, `childrenIds`, `stack?`): `void` + +Create a new combo with existing child nodes and combos. + +#### 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` + +#### Implementation of + +IGraph.createCombo + +#### Defined in + +[graph.ts:512](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L512) + +___ + +### expandCombo + +▸ **expandCombo**(`comboId`, `stack?`): `void` + +Expand a combo. + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `comboId` | `ID` | +| `stack?` | `boolean` | + +#### Returns + +`void` + +#### Implementation of + +IGraph.expandCombo + +#### Defined in + +[graph.ts:536](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L536) + +___ + +### uncombo + +▸ **uncombo**(`comboId`, `stack?`): `void` + +dissolve combo + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `comboId` | `ID` | +| `stack?` | `boolean` | + +#### Returns + +`void` + +#### Implementation of + +IGraph.uncombo + +#### Defined in + +[graph.ts:520](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L520) + +## Graph Instance + +### destroy + +▸ **destroy**(): `void` + +Destroy the graph instance and remove the related canvases. + +#### Returns + +`void` + +#### Implementation of + +IGraph.destroy + +#### Defined in + +[graph.ts:640](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L640) + +## View + +### fitCenter + +▸ **fitCenter**(`animateCfg?`): `void` + +Fit the graph center to the view center. + +#### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `animateCfg?` | `AnimateCfg` | animation configurations | + +#### Returns + +`void` + +#### Implementation of + +IGraph.fitCenter + +#### Defined in + +[graph.ts:252](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L252) + +___ + +### fitView + +▸ **fitView**(`padding?`, `rules?`, `animateCfg?`): `void` + +Fit the graph content to the view. + +#### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `padding?` | `Padding` | padding while fitting | +| `rules?` | `FitViewRules` | rules for fitting | +| `animateCfg?` | `AnimateCfg` | animation configurations | + +#### Returns + +`void` + +#### Implementation of + +IGraph.fitView + +#### Defined in + +[graph.ts:243](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L243) + +___ + +### focusItem + +▸ **focusItem**(`ids`, `animateCfg?`): `void` + +Move the graph to make the item align the view center. + +#### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `ids` | `ID` \| `ID`[] | - | +| `animateCfg?` | `AnimateCfg` | animation configurations | + +#### Returns + +`void` + +#### Implementation of + +IGraph.focusItem + +#### Defined in + +[graph.ts:262](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L262) + +___ + +### move + +▸ **move**(`dx`, `dy`, `animateCfg?`): `void` + +Move the graph with a relative vector. + +#### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `dx` | `number` | x of the relative vector | +| `dy` | `number` | y of the relative vector | +| `animateCfg?` | `AnimateCfg` | animation configurations | + +#### Returns + +`void` + +#### Implementation of + +IGraph.move + +#### Defined in + +[graph.ts:194](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L194) + +___ + +### moveTo + +▸ **moveTo**(`x`, `y`, `alignment`, `animateCfg?`): `void` + +Move the graph and align to a point. + +#### 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` + +#### Implementation of + +IGraph.moveTo + +#### Defined in + +[graph.ts:207](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L207) + +___ + +### zoom + +▸ **zoom**(`ratio`, `center?`, `animateCfg?`): `void` + +Zoom the graph with a relative ratio. + +#### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `ratio` | `number` | relative ratio to zoom | +| `center?` | `Point` | zoom center | +| `animateCfg?` | `AnimateCfg` | animation configurations | + +#### Returns + +`void` + +#### Implementation of + +IGraph.zoom + +#### Defined in + +[graph.ts:219](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L219) + +___ + +### zoomTo + +▸ **zoomTo**(`toRatio`, `center?`, `animateCfg?`): `void` + +Zoom the graph to a specified ratio. + +#### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `toRatio` | `number` | specified ratio | +| `center?` | `Point` | zoom center | +| `animateCfg?` | `AnimateCfg` | animation configurations | + +#### Returns + +`void` + +#### Implementation of + +IGraph.zoomTo + +#### Defined in + +[graph.ts:231](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L231) + +## Layout + +### layout + +▸ **layout**(`cfg?`, `align?`, `canvasPoint?`, `stack?`): `void` + +Layout the graph (with current configurations if cfg is not assigned). + +#### 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` + +#### Implementation of + +IGraph.layout + +#### Defined in + +[graph.ts:549](https://github.com/antvis/G6/blob/abc619f898/packages/g6/src/runtime/graph.ts#L549) diff --git a/packages/g6/docs/api-ts/globals.md b/packages/g6/docs/api-ts/globals.md deleted file mode 100644 index e93199ccf1..0000000000 --- a/packages/g6/docs/api-ts/globals.md +++ /dev/null @@ -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) diff --git a/packages/g6/docs/api-ts/modules.md b/packages/g6/docs/api-ts/modules.md new file mode 100644 index 0000000000..0b92e2da33 --- /dev/null +++ b/packages/g6/docs/api-ts/modules.md @@ -0,0 +1,9 @@ +[@antv/g6](README.md) / Exports + +# @antv/g6 + +## Table of contents + +### Classes + +- [default](classes/default.md) diff --git a/packages/g6/docs/api-ts/modules/_graph_.md b/packages/g6/docs/api-ts/modules/_graph_.md deleted file mode 100644 index 8e3bef0157..0000000000 --- a/packages/g6/docs/api-ts/modules/_graph_.md +++ /dev/null @@ -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) diff --git a/packages/g6/package.json b/packages/g6/package.json index 29e544229e..084dde4121 100644 --- a/packages/g6/package.json +++ b/packages/g6/package.json @@ -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", diff --git a/packages/g6/src/item/edge.ts b/packages/g6/src/item/edge.ts index f6f4893291..6e75020a18 100644 --- a/packages/g6/src/item/edge.ts +++ b/packages/g6/src/item/edge.ts @@ -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; -} \ No newline at end of file + 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); + // } +} diff --git a/packages/g6/src/item/item.ts b/packages/g6/src/item/item.ts index ecb9adff8e..fba8249bee 100644 --- a/packages/g6/src/item/item.ts +++ b/packages/g6/src/item/item.ts @@ -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; } -} \ No newline at end of file + + /** + * 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; + }); +}; diff --git a/packages/g6/src/item/node.ts b/packages/g6/src/item/node.ts index 8905723f8a..bf53bfdb50 100644 --- a/packages/g6/src/item/node.ts +++ b/packages/g6/src/item/node.ts @@ -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); -} \ No newline at end of file + // 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] }; + } +} diff --git a/packages/g6/src/runtime/controller/data.ts b/packages/g6/src/runtime/controller/data.ts index b0dc1f1eee..fa25afe917 100644 --- a/packages/g6/src/runtime/controller/data.ts +++ b/packages/g6/src/runtime/controller/data.ts @@ -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) { this.graph = graph; - this.graphCore = new GraphLib(); 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({ + ...data, + onChanged: (event) => this.updateGraphCore(event), + }); + const { data: transformedData } = this.transformData(); + this.graphCore = new GraphLib({ ...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; - } } -} \ No newline at end of file + + /** + * 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; +}; diff --git a/packages/g6/src/runtime/controller/interaction.ts b/packages/g6/src/runtime/controller/interaction.ts index cff6ae217c..bc5cf11f52 100644 --- a/packages/g6/src/runtime/controller/interaction.ts +++ b/packages/g6/src/runtime/controller/interaction.ts @@ -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); } }); diff --git a/packages/g6/src/runtime/controller/item.ts b/packages/g6/src/runtime/controller/item.ts index 0e9cbd1b55..ac5539d02d 100644 --- a/packages/g6/src/runtime/controller/item.ts +++ b/packages/g6/src/runtime/controller/item.ts @@ -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) { this.graph = graph; - this.graphCore = new GraphLib(); + // 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 } -} \ No newline at end of file + + /** + * Listener of runtime's itemchange lifecycle hook. + * @param param + */ + private onChange(param: { + type: ITEM_TYPE; + changes: GraphChange[]; + 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); + } +} diff --git a/packages/g6/src/runtime/graph.ts b/packages/g6/src/runtime/graph.ts index b068e6b06e..4343687e1b 100644 --- a/packages/g6/src/runtime/graph.ts +++ b/packages/g6/src/runtime/graph.ts @@ -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 extends EventEmitter implements IGraph { 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; private dataController: DataController; private interactionController: InteractionController; @@ -31,7 +58,14 @@ export default class Graph 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 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({ 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[]; + 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 extends EventEmitter impl * @returns graph specs */ public getSpecification(): Specification { - 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 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 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 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 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 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 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 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 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 | Partial | Partial[] | Partial[] | Partial[]>, stack?: boolean): boolean { + public updateData( + itemType: ITEM_TYPE, + models: + | Partial + | Partial + | Partial< + | ComboUserModel + | Partial[] + | Partial[] + | Partial[] + >, + 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 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 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 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 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[], modes: string | string[]) { @@ -377,10 +590,10 @@ export default class Graph 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 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 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, mode?: string) { @@ -420,11 +635,32 @@ export default class Graph 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; // } // }); } -} \ No newline at end of file + + /** + * 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; + } +} diff --git a/packages/g6/src/stdlib/index.ts b/packages/g6/src/stdlib/index.ts index 45ee4d6859..59704b7b37 100644 --- a/packages/g6/src/stdlib/index.ts +++ b/packages/g6/src/stdlib/index.ts @@ -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 = { diff --git a/packages/g6/src/stdlib/item/edge/base.ts b/packages/g6/src/stdlib/item/edge/base.ts new file mode 100644 index 0000000000..c80d5691d3 --- /dev/null +++ b/packages/g6/src/stdlib/item/edge/base.ts @@ -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); + } +} diff --git a/packages/g6/src/stdlib/item/edge/index.ts b/packages/g6/src/stdlib/item/edge/index.ts new file mode 100644 index 0000000000..5bfc1176e4 --- /dev/null +++ b/packages/g6/src/stdlib/item/edge/index.ts @@ -0,0 +1 @@ +export * from "./line"; \ No newline at end of file diff --git a/packages/g6/src/stdlib/item/edge/line.ts b/packages/g6/src/stdlib/item/edge/line.ts new file mode 100644 index 0000000000..474ee5a896 --- /dev/null +++ b/packages/g6/src/stdlib/item/edge/line.ts @@ -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; + } +} diff --git a/packages/g6/src/stdlib/item/node/base.ts b/packages/g6/src/stdlib/item/node/base.ts new file mode 100644 index 0000000000..2e8f805c3e --- /dev/null +++ b/packages/g6/src/stdlib/item/node/base.ts @@ -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 {} + } +} diff --git a/packages/g6/src/stdlib/item/node/circle.ts b/packages/g6/src/stdlib/item/node/circle.ts new file mode 100644 index 0000000000..28b53dc284 --- /dev/null +++ b/packages/g6/src/stdlib/item/node/circle.ts @@ -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); + } +} diff --git a/packages/g6/src/stdlib/item/node/index.ts b/packages/g6/src/stdlib/item/node/index.ts new file mode 100644 index 0000000000..888671d02a --- /dev/null +++ b/packages/g6/src/stdlib/item/node/index.ts @@ -0,0 +1 @@ +export * from "./circle"; \ No newline at end of file diff --git a/packages/g6/src/types/combo.ts b/packages/g6/src/types/combo.ts index 249bcb0a08..2aaf8151a5 100644 --- a/packages/g6/src/types/combo.ts +++ b/packages/g6/src/types/combo.ts @@ -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[]; diff --git a/packages/g6/src/types/data.ts b/packages/g6/src/types/data.ts index 822dad0f89..8403011637 100644 --- a/packages/g6/src/types/data.ts +++ b/packages/g6/src/types/data.ts @@ -22,4 +22,6 @@ export interface FetchDataConfig { export type GraphCore = IGraph; export type DisplayGraphCore = IGraph; -export type TransformerFn = (data: GraphData) => GraphData \ No newline at end of file +export type TransformerFn = (data: GraphData) => GraphData + +export type DataChangeType = 'replace' | 'mergeReplace' | 'union' | 'remove' | 'update'; \ No newline at end of file diff --git a/packages/g6/src/types/edge.ts b/packages/g6/src/types/edge.ts index 3bd7812518..194bf21eae 100644 --- a/packages/g6/src/types/edge.ts +++ b/packages/g6/src/types/edge.ts @@ -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; } +export interface EdgeShapeMap { + keyShape: DisplayObject, + labelShape?: DisplayObject, + iconShape?: DisplayObject, + [otherShapeId: string]: DisplayObject +} + // TODO export interface IEdge extends IItem { diff --git a/packages/g6/src/types/graph.ts b/packages/g6/src/types/graph.ts index 6f12166667..07be90c2e6 100644 --- a/packages/g6/src/types/graph.ts +++ b/packages/g6/src/types/graph.ts @@ -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 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 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 + | Partial + | Partial< + | ComboUserModel + | Partial[] + | Partial[] + | Partial[] + >, + stack?: boolean, + ) => NodeModel | EdgeModel | ComboModel | NodeModel[] | EdgeModel[] | ComboModel[]; // ===== view operations ===== @@ -70,7 +167,7 @@ export interface IGraph 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 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 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 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 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 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 | Partial | Partial[] | Partial[] | Partial[]>, 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 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 extends E * Add behavior(s) to mode(s). * @param behaviors behavior names or configs * @param modes mode names - * @returns + * @returns */ addBehaviors: (behaviors: BehaviorOptionsOf[], 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 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, mode?: string) => void; -} \ No newline at end of file +} diff --git a/packages/g6/src/types/hook.ts b/packages/g6/src/types/hook.ts index 360dca8a6a..1652691e57 100644 --- a/packages/g6/src/types/hook.ts +++ b/packages/g6/src/types/hook.ts @@ -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 { name: string; @@ -15,14 +15,15 @@ export interface IHook { export interface Hooks { 'init': IHook; // 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[] | Partial[] | Partial[] }> - + 'datachange': IHook<{ + type: DataChangeType; + data: GraphData + }>; + 'itemchange': IHook<{ + type: ITEM_TYPE; + changes: GraphChange[]; + graphCore: GraphCore; + }>; 'render': IHook<{ graphCore: GraphCore }>; // TODO: define param template // 'layout': IHook; // TODO: define param template // 'updatelayout': IHook; // 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; // TODO: define param template - // 'itemstatechange': IHook; // TODO: define param template // 'destroy': IHook; // TODO: define param template // TODO: more timecycles here }; diff --git a/packages/g6/src/types/item.ts b/packages/g6/src/types/item.ts index 6e5b79fc59..2f04eff83c 100644 --- a/packages/g6/src/types/item.ts +++ b/packages/g6/src/types/item.ts @@ -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 { - fields: string[], - formatter: (values: unknown[]) => T; + fields: string[]; + formatter: (values: NodeUserModel | EdgeUserModel | ComboUserModel) => T; } export interface ShapeAttrEncode { [shapeAttr: string]: unknown | Encode; - animate: AnimateAttr | Encode; -}; + animate?: AnimateAttr | Encode; +} 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; animate: AnimateAttr | Encode; - } + }; }; } 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; -} \ No newline at end of file +} diff --git a/packages/g6/src/types/node.ts b/packages/g6/src/types/node.ts index f646fc72ac..4e9ab69f8b 100644 --- a/packages/g6/src/types/node.ts +++ b/packages/g6/src/types/node.ts @@ -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; offsetX?: number | Encode; offsetY?: number | Encode; @@ -68,13 +60,18 @@ interface NodeLabelShapeAttrEncode extends ShapeAttrEncode { // TODO: extends Te } export interface NodeShapesEncode extends ShapesEncode { labelShape?: NodeLabelShapeAttrEncode; - anchorPoints?: AnchorPoint[] | Encode + anchorPoints?: AnchorPoint[] | Encode; } export interface NodeEncode extends NodeShapesEncode { type?: string | Encode; } -// TODO -export interface INode extends IItem { +export interface NodeShapeMap { + keyShape: DisplayObject; + labelShape?: DisplayObject; + iconShape?: DisplayObject; + [otherShapeId: string]: DisplayObject; +} -} \ No newline at end of file +// TODO +export interface INode extends IItem {} diff --git a/packages/g6/src/types/spec.ts b/packages/g6/src/types/spec.ts index cad0e1b24f..9bd6c2cb70 100644 --- a/packages/g6/src/types/spec.ts +++ b/packages/g6/src/types/spec.ts @@ -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 { type: 'graph' | 'tree'; + container: string | HTMLElement; width?: number; height?: number; renderer?: rendererName | { @@ -34,19 +35,19 @@ export interface Specification { }[] | 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 */ diff --git a/packages/g6/src/util/array.ts b/packages/g6/src/util/array.ts new file mode 100644 index 0000000000..55ef820f13 --- /dev/null +++ b/packages/g6/src/util/array.ts @@ -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; +} \ No newline at end of file diff --git a/packages/g6/src/util/canvas.ts b/packages/g6/src/util/canvas.ts new file mode 100644 index 0000000000..fdd2c03219 --- /dev/null +++ b/packages/g6/src/util/canvas.ts @@ -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 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() + }); +} \ No newline at end of file diff --git a/packages/g6/src/util/extend.ts b/packages/g6/src/util/extend.ts index bd8812acbc..c1dd4fee97 100644 --- a/packages/g6/src/util/extend.ts +++ b/packages/g6/src/util/extend.ts @@ -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 = ( GraphClass: typeof Graph, extendLibrary: { - behaviors?: B1, - } + behaviors?: B1; + nodes?: any; // TODO + edges?: any; // TODO + }, ): typeof Graph => { // 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; -} \ No newline at end of file +}; diff --git a/packages/g6/src/util/extension.ts b/packages/g6/src/util/extension.ts index 8e3385a79a..c1b7233822 100644 --- a/packages/g6/src/util/extension.ts +++ b/packages/g6/src/util/extension.ts @@ -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; }; \ No newline at end of file diff --git a/packages/g6/src/util/index.ts b/packages/g6/src/util/index.ts index 32306b67c0..9bdcb165af 100644 --- a/packages/g6/src/util/index.ts +++ b/packages/g6/src/util/index.ts @@ -1,3 +1,4 @@ import { extend } from './extend'; -const Util = { extend }; +import { isEncode } from './type'; +const Util = { extend, isEncode }; export default Util; diff --git a/packages/g6/src/util/shape.ts b/packages/g6/src/util/shape.ts new file mode 100644 index 0000000000..49b286cb4b --- /dev/null +++ b/packages/g6/src/util/shape.ts @@ -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, +}; diff --git a/packages/g6/src/util/type.ts b/packages/g6/src/util/type.ts new file mode 100644 index 0000000000..ac90c5875a --- /dev/null +++ b/packages/g6/src/util/type.ts @@ -0,0 +1,3 @@ +import { Encode } from "../types/item" + +export const isEncode = (value): value is Encode => Boolean((value as Encode).fields && (value as Encode).formatter) diff --git a/packages/g6/tests/unit/behavior-spec.ts b/packages/g6/tests/unit/behavior-spec.ts index 21dfb8b6db..5e00539c3a 100644 --- a/packages/g6/tests/unit/behavior-spec.ts +++ b/packages/g6/tests/unit/behavior-spec.ts @@ -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: { diff --git a/packages/g6/tests/unit/data-spec.ts b/packages/g6/tests/unit/data-spec.ts new file mode 100644 index 0000000000..c8d226d197 --- /dev/null +++ b/packages/g6/tests/unit/data-spec.ts @@ -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; + 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); + }); +}); diff --git a/packages/g6/tests/unit/item-spec.ts b/packages/g6/tests/unit/item-spec.ts new file mode 100644 index 0000000000..d18fe0fe86 --- /dev/null +++ b/packages/g6/tests/unit/item-spec.ts @@ -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; + 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; + 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 }, + 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', () => {}); +}); diff --git a/packages/g6/tsconfig.json b/packages/g6/tsconfig.json index d0a7d1bc71..0829cbc6f8 100644 --- a/packages/g6/tsconfig.json +++ b/packages/g6/tsconfig.json @@ -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" } -} +} \ No newline at end of file