mirror of
https://gitee.com/antv/g6.git
synced 2024-12-02 03:38:20 +08:00
feat: refactor G6
This commit is contained in:
parent
a7ebea3352
commit
bdcd19e1cc
26
packages/core/.babelrc.js
Normal file
26
packages/core/.babelrc.js
Normal file
@ -0,0 +1,26 @@
|
||||
module.exports = api => {
|
||||
api.cache(() => process.env.NODE_ENV);
|
||||
|
||||
if (process.env.GATSBY === 'true') {
|
||||
return {
|
||||
presets: ['@babel/preset-env', 'babel-preset-gatsby'],
|
||||
};
|
||||
}
|
||||
return {
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
loose: true,
|
||||
modules: false,
|
||||
},
|
||||
],
|
||||
'@babel/preset-react',
|
||||
{
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-class-properties"
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
22
packages/core/.eslintignore
Normal file
22
packages/core/.eslintignore
Normal file
@ -0,0 +1,22 @@
|
||||
build/
|
||||
coverage/
|
||||
lib/
|
||||
dist/
|
||||
mocks/
|
||||
node_modules/
|
||||
demos/
|
||||
.cache
|
||||
public
|
||||
bin
|
||||
esm/
|
||||
es/
|
||||
examples/
|
||||
tests/
|
||||
stories/
|
||||
gatsby-browser.js
|
||||
site/
|
||||
webpack.*.js
|
||||
gatsby-*.js
|
||||
global.d.ts
|
||||
jest.config.js
|
||||
.eslintrc.*
|
42
packages/core/.eslintrc.js
Normal file
42
packages/core/.eslintrc.js
Normal file
@ -0,0 +1,42 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@umijs/fabric/dist/eslint')],
|
||||
globals: {
|
||||
$: true,
|
||||
_: true,
|
||||
},
|
||||
rules: {
|
||||
'no-bitwise': 0,
|
||||
'import/order': 0,
|
||||
'no-plusplus': 0,
|
||||
'no-console': ['error', { allow: ['warn', 'error'] }],
|
||||
'operator-assignment': 0,
|
||||
'consistent-return': 0,
|
||||
'lines-between-class-members': 0,
|
||||
'class-methods-use-this': 0,
|
||||
'lines-between-class-members': 0,
|
||||
'no-multi-assign': 0,
|
||||
'no-continue': 0,
|
||||
'no-underscore-dangle': 0,
|
||||
'no-useless-constructor': 0,
|
||||
'prefer-destructuring': 0,
|
||||
'guard-for-in': 0,
|
||||
'no-restricted-globals': 0,
|
||||
'max-classes-per-file': 0,
|
||||
// 后面需要去掉
|
||||
'no-restricted-syntax': 0,
|
||||
'prefer-spread': 0,
|
||||
'@typescript-eslint/camelcase': 0,
|
||||
'no-loop-func': 0,
|
||||
'@typescript-eslint/no-loop-func': 0,
|
||||
'@typescript-eslint/no-redeclare': 0,
|
||||
'@typescript-eslint/no-shadow': 0,
|
||||
'@typescript-eslint/no-unused-vars': 0,
|
||||
'no-param-reassign': 0,
|
||||
'import/no-extraneous-dependencies': 0,
|
||||
'no-unused-expressions': 0,
|
||||
'dot-notation': 0,
|
||||
'array-callback-return': 0,
|
||||
'one-var': 0,
|
||||
'no-lonely-if': 0
|
||||
},
|
||||
};
|
5
packages/core/.fatherrc.js
Normal file
5
packages/core/.fatherrc.js
Normal file
@ -0,0 +1,5 @@
|
||||
export default {
|
||||
entry: './src/index.ts',
|
||||
esm: 'babel',
|
||||
cjs: 'babel',
|
||||
};
|
83
packages/core/.npmignore
Normal file
83
packages/core/.npmignore
Normal file
@ -0,0 +1,83 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# lock
|
||||
package-lock.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Typescript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
.DS_Store
|
||||
|
||||
# npmignore - content above this line is automatically generated and modifications may be omitted
|
||||
# see npmjs.com/npmignore for more details.
|
||||
test
|
||||
|
||||
*.sw*
|
||||
*.un~
|
||||
.idea
|
||||
bin
|
||||
demos
|
||||
docs
|
||||
temp
|
||||
webpack-dev.config.js
|
||||
webpack.config.js
|
||||
public
|
||||
.cache
|
||||
site
|
||||
examples
|
||||
gatsby-browser.js
|
||||
gatsby-config.js
|
10
packages/core/.prettierignore
Normal file
10
packages/core/.prettierignore
Normal file
@ -0,0 +1,10 @@
|
||||
.cache
|
||||
package.json
|
||||
package-lock.json
|
||||
public
|
||||
dist
|
||||
es
|
||||
lib
|
||||
.*
|
||||
*.png
|
||||
**/assets/**
|
5
packages/core/.prettierrc.js
Normal file
5
packages/core/.prettierrc.js
Normal file
@ -0,0 +1,5 @@
|
||||
const fabric = require('@umijs/fabric');
|
||||
|
||||
module.exports = {
|
||||
...fabric.prettier,
|
||||
};
|
957
packages/core/CHANGELOG.md
Normal file
957
packages/core/CHANGELOG.md
Normal file
@ -0,0 +1,957 @@
|
||||
# ChangeLog
|
||||
|
||||
#### 4.0.3
|
||||
- fix: state style restore for non-circle shapes;
|
||||
|
||||
#### 4.0.2
|
||||
- fix: node and edge state style with update problem;
|
||||
- fix: import lib problem;
|
||||
- fix: import node module problem;
|
||||
- fix: hidden shapes show up after zoom-canvas or drag-canvas with enableOptimize;
|
||||
- fix: tooltip for combo;
|
||||
- fix: update edge with false endArrow and startArrow;
|
||||
|
||||
#### 4.0.1
|
||||
- fix: glslang problem;
|
||||
|
||||
#### 4.0.0-beta.0
|
||||
- feat: fruchterman and gforce layout with gpu;
|
||||
- feat: gforce;
|
||||
- feat: updateChildren API for TreeGraph;
|
||||
- feat: louvain clustering algorithm;
|
||||
- feat: container of plugins with dom id;
|
||||
- feat: label propagation clustering algorithm;
|
||||
- feat: get color sets by subject color array;
|
||||
- feat: canvas context menu;
|
||||
- feat: stop gforce;
|
||||
- feat: dark rules for colors;
|
||||
- fix: text redidual problem, closes: #2045 #2193;
|
||||
- fix: graph on callback parameter type problem, closes: #2250;
|
||||
- fix: combo zIndex problem;
|
||||
- fix: webworker updateLayoutCfg problem;
|
||||
- fix: drag-canvas and click node on mobile;
|
||||
|
||||
#### 3.8.5
|
||||
- fix: get fontFamily of the window in global leads to DOM depending when using bigfish;
|
||||
|
||||
#### 3.8.4
|
||||
- feat: new version of basic styles for light version;
|
||||
- feat: shortcuts-call behavior for calling a Graph function by shortcuts;
|
||||
- feat: color generate util function getColorsWithSubjectColor;
|
||||
- fix: drag-canvas on mobile problem;
|
||||
- fix: style update problem with stateStyles in the options of registerNode;
|
||||
|
||||
#### 3.8.3
|
||||
- feat: drag the viewport of the minimap out of the the view;
|
||||
- fix: extend modelRect with description problem, closes: #2235;
|
||||
|
||||
#### 3.8.2
|
||||
- feat: graph.setImageWaterMarker, graph.setTextWaterMarker API;
|
||||
- feat: zoom-canvas support mobile;
|
||||
- fix: drag-canvas behavior support scalable range, closes: #2136;
|
||||
- fix: TreeGraph changeData clear all states, closes: #2173;
|
||||
- chore: auto zoom tooltip & contextMenu component when zoom-canvas;
|
||||
- chore: upgrade @antv/g-canvas;
|
||||
- feat: destroyLayout API for graph, closes: #2140;
|
||||
- feat: clustering for force layout, closes: #2196;
|
||||
- fix: svg renderer minimap hidden elements probem, closes: #2174;
|
||||
- feat: add extra parameter graph for menu plugin, closes: #2204;
|
||||
- fix: tooltip plugin, crossing different shape cant execute the getContent function, closes: #2153;
|
||||
- feat: add edgeConfig for create-edge behavior, closes: #2195;
|
||||
- fix: remove the source node while creat-edge;
|
||||
- feat: create-edge for combo, closes: #2211;
|
||||
- fix: update the typings for G6Event;
|
||||
|
||||
#### 3.8.1
|
||||
- fix: update edge states with updateItem problem, closes: #2142;
|
||||
- fix: create-edge behavior with polyline problem, closes: #2165;
|
||||
- fix: console.warn show duplicate ID, closes: #2163;
|
||||
- feat: support the drag-canvas behavior on the mobile device, closes: #816;
|
||||
- chore: timeBar component docs;
|
||||
|
||||
#### 3.8.0
|
||||
|
||||
- fix: treeGraph render with addItem and stack problem, closes: #2084;
|
||||
- feat: G6 Interactive Document GraphMarker;
|
||||
- feat: registerNode with jsx support afterDraw and setState;
|
||||
- feat: edge filter lens plugin;
|
||||
- feat: timebar plugin;
|
||||
|
||||
#### 3.7.3
|
||||
|
||||
- fix: update G to fix the shape disappear when it has been dragged out of the view port problem, closes: #2078, #2030, #2007;
|
||||
- fix: redo undo with treeGraph problem;
|
||||
- fix: remove item with itemType problem, closes: #2096.
|
||||
|
||||
#### 3.7.2
|
||||
|
||||
- fix: toolbar redo undo addItem with type problem, closes #2043;
|
||||
- fix: optimized drag-canvas with hidden items;
|
||||
- fix: state style with 0 value problem, closes: #2039;
|
||||
- fix: layout with webworker leads to twice beforelayout, closes: #2052;
|
||||
- fix: context menu with sibling doms of graph container leads to position problem, closes: #2053;
|
||||
- fix: changeData with combos problem, closes: #2064;
|
||||
- fix: improve the position of the context menu before showing up;
|
||||
- feat: fisheye allows user to config the trigger of scaling range(r) and magnify factor(d) by scaleRBy and scaleDBy respectively;
|
||||
- feat: add the percent text of magnify factor(d) for fisheye and users are allowed to configure it by show showDPercent.
|
||||
|
||||
#### 3.7.1
|
||||
|
||||
- fix: hide the tooltip plugin when drag node and contextmenu, closes #1975;
|
||||
- fix: processParellelEdges without edge id problem;
|
||||
- fix: label background with left, right position problem, closes #1861;
|
||||
- fix: create-edge and redo undo problem, #1976;
|
||||
- fix: tooltip plugin with shouldBegin problem, closes #2006;
|
||||
- fix: tooltip behavior with shouldBegin problem, closes #2016;
|
||||
- fix: the position of grid plugins when there is something on the top of the canvas, closes: #2012;
|
||||
- fix: fisheye destroy and new problem, closes: #2018;
|
||||
- fix: node event with wrong canvasX and canvasY problem, closes: #2027;
|
||||
- fix: drag combo and drag node to drop on canvas/combo/node problem;
|
||||
- feat: improve the performance on the combos;
|
||||
- fix: redo and undo problem when update item after additem, closes #2019;
|
||||
- feat: hide shapes beside keyShape while zooming;
|
||||
- feat: improve the performance on the combos.
|
||||
|
||||
#### 3.7.0
|
||||
|
||||
- feat: chart node;
|
||||
- feat: bubble set;
|
||||
- feat: custom node with JSX;
|
||||
- feat: minimum spanning tree algorithm;
|
||||
- feat: path finding algorithm;
|
||||
- feat: cycle finding algorithm;
|
||||
- chore: update antv/hierarchy to fix indented tree with dropCap problem.
|
||||
|
||||
#### 3.6.2
|
||||
|
||||
- feat: find all paths and the shortest path between two nodes;
|
||||
- feat: fisheye with dragging;
|
||||
- feat: fisheye with scaling range and d;
|
||||
- feat: click and drag to create an edge by create-edge behavior;
|
||||
- feat: process multiple parallel edges to quadratic with proper curveOffset;
|
||||
- fix: polyline with rect and radius=0 problem;
|
||||
- fix: arrow state & linkpoint;
|
||||
- fix: the position of the tooltip plugin;
|
||||
- fix: drop a node onto a sub node of a combo;
|
||||
- chore: update hierarchy to solve the children ordering problem for indented tree layout;
|
||||
- chore: extract the public calculation to enhance the performance of fisheye.
|
||||
|
||||
#### 3.6.1-beta
|
||||
|
||||
- chore: update g-canvas to support quickHit and pruning the rendering of the graph outside the viewport;
|
||||
- feat: add statistical chart nodes;
|
||||
- feat: add hull for create smooth contour to include specific items;
|
||||
- fix: clear combos before render;
|
||||
- fix: menu plugin with clickHandler problem.
|
||||
|
||||
#### 3.6.1
|
||||
|
||||
- feat: image minimap;
|
||||
- feat: visible can be controlled in the data;
|
||||
- feat: item type for tooltip plugin;
|
||||
- feat: menu plugin with shouldUpdate;
|
||||
- fix: tooltip plugin position and hidden by removeItem;
|
||||
- fix: tooltip behavior hidden by removeItem;
|
||||
- fix: menu plugin with clicking on canvas problem;
|
||||
- fix: menu plugin with clickHandler problem;
|
||||
- fix: createCombo with double nodes problem.
|
||||
|
||||
#### 3.6.0
|
||||
|
||||
- feat: fisheye lens plugin;
|
||||
- feat: lasso-select behavior;
|
||||
- feat: TimeBar plugin;
|
||||
- feat: ToolBar plugin.
|
||||
|
||||
#### 3.5.12
|
||||
|
||||
- fix: node:click is triggered twice while clicking a node;
|
||||
- fix: update combo edge when drag node out of it problem;
|
||||
- feat: animate configuration for combo, true by default;
|
||||
- fix: calling canvas.on('\*', ...) instead of origin way in event controller leads to malposition while dragging nodes with zoomed graph.
|
||||
|
||||
#### 3.5.11
|
||||
|
||||
- feat: graph.priorityState api;
|
||||
- feat: graph.on support name:event mode.
|
||||
- fix: combo edge with uncorrect end points;
|
||||
- fix: combo polyline edge with wrong path;
|
||||
- fix: getViewCenter with padding problem;
|
||||
- fix: cannot read property 'getModel' of null problem on contextmenu when the target is not an item;
|
||||
- feat: allow user to configure the initial positions for empty combos;
|
||||
- feat: optimize by hiding edges and shapes which are not keyShape while dragging canvas;
|
||||
- feat: fix the initial positions by equably distributing for layout to produce similar result.
|
||||
|
||||
#### 3.5.10
|
||||
|
||||
- fix: fitView and fitCenter with animate in the initial state;
|
||||
- fix: dulplicated edges in nodeselectchange event of brush-select;
|
||||
- fix: triple click and drag canvas problem;
|
||||
- fix: sync the minZoom and maxZoom in drag-canvas and graph;
|
||||
- fix: integrate getSourceNeighbors and getTargetNeighbors to getNeighbors(node, type);
|
||||
- feat: initial x and y for combo data;
|
||||
- feat: dagre layout supports sortByCombo;
|
||||
- feat: allow user to disable relayout in collapse-expand-combo behavior;
|
||||
- feat: dijkstra shortest path lenght algorithm.
|
||||
|
||||
#### 3.5.9
|
||||
|
||||
- fix: multiple animate update shape for combo;
|
||||
- fix: removeItem from a combo.
|
||||
|
||||
#### 3.5.8
|
||||
|
||||
- fix: combo edge problem, issues #1722;
|
||||
- feat: adjacency matrix algorithm;
|
||||
- feat: Floyd Warshall shortest path algorithm;
|
||||
- feat: built-in arrows;
|
||||
- feat: built-in markers;
|
||||
- fix: force layout with addItem and relayout;
|
||||
- fix: create combo with parentId problem;
|
||||
- feat: allow user to configure the pixelRatio for Canvas;
|
||||
- chore: update G to resolve the blur canvas problem.
|
||||
|
||||
#### 3.5.7
|
||||
|
||||
- feat: shouldBegin for click-select behavior;
|
||||
- feat: graph.getGroup, graph.getContainer, graph.getMinZoom, graph.setMinZoom, graph.getMaxZoom, graph.setMaxZoom, graph.getWidth, graph.getHeight API;
|
||||
- fix: combo edge dashLine attribute;
|
||||
- fix: combo collapse and expand with edges problem;
|
||||
- fix: destroy the tooltip DOMs when destroy the graph;
|
||||
- fix: unify the shape names for custom node and extended node;
|
||||
- fix: update the edges after first render with collapsed combos.
|
||||
|
||||
#### 3.5.6
|
||||
|
||||
- feat: dropCap for indented TreeGraph layout.
|
||||
|
||||
#### 3.5.5
|
||||
|
||||
- fix: custom node with setState problem;
|
||||
- fix: validationCombo in drag-combo and drag-node.
|
||||
|
||||
#### 3.5.3
|
||||
|
||||
- feat: focusItem with animation;
|
||||
- feat: generate the image url of the full graph by graph.toFullDataUrl;
|
||||
- fix: graph dispears after being dragged out of the canvas and back;
|
||||
- fix: the graph cannot be dragged back if it is already out of the view;
|
||||
- fix: size and radius of the linkPoints problem;
|
||||
- fix: combo graph with unused state name in comboStateStyles;
|
||||
- fix: preventDefault in drag-canvas behavior.
|
||||
|
||||
#### 3.5.2
|
||||
|
||||
- feat: degree algorithm;
|
||||
- feat: graph.getNodeDegree;
|
||||
- fix: downloadFullImage changes the matrix of the graph problem;
|
||||
- fix: circular layout modifies the origin data with infinite hierarchy problem.
|
||||
|
||||
#### 3.5.1
|
||||
|
||||
- feat: graph.fitCenter to align the graph center to canvas center;
|
||||
- fix: getType is not a function error occurs when addItem with point;
|
||||
- fix: checking comboTrees avaiability;
|
||||
- fix: error occurs when createCombo into the graph without any combos;
|
||||
- fix: endPoint and startPoint are missing in modelConfig type;
|
||||
- fix: edge background leads to empty canvas when the autoRotate is false;
|
||||
- fix: combo state style bug.
|
||||
|
||||
#### 3.5.0
|
||||
|
||||
- feat: combo and combo layout;
|
||||
- feat: graph algorithms: DFS, BFS and circle detection;
|
||||
- feat: add `getNeighbors`, `getSourceNeighbors`, `getTargetNeighbors` methods on Graph and Node;
|
||||
- feat: add `getID` method on Item;
|
||||
- fix: All Configuration type declarations are migrated to types folder, refer [here](https://github.com/antvis/G6/commit/3691cb51264df8529f75222147ac3f248b71f2f6?diff=unified#diff-76cf0eb5e3d8032945f1ac79ffc5e815R6);
|
||||
- fix: Some configuration type declarations have removed the `I` prefix, refer [here](https://github.com/antvis/G6/commit/3691cb51264df8529f75222147ac3f248b71f2f6?diff=unified#diff-aa582974831cee2972b8c96cfcce503aR16);
|
||||
- feat: Util.getLetterWidth and Util.getTextSize.
|
||||
|
||||
#### 3.4.10
|
||||
|
||||
- fix: TreeGraphData type with style and stateStyles;
|
||||
- fix: wrong controlpoint position for bezier curves with getControlPoint.
|
||||
|
||||
#### 3.4.9
|
||||
|
||||
- fix: transplie d3-force to support IE11.
|
||||
|
||||
#### 3.4.8
|
||||
|
||||
- feat: update the keyShape type minimap when the node or edge's style is updated;
|
||||
- fix: problem about switching to another applications or browser menu and then switch back, the drag-canvas does not take effect;
|
||||
- fix: fix the problem about fail to render the graph when the animate and fitView are true by turn off the animate for rendering temporary;
|
||||
- fix: curveOffset for arc, quadratic, cubic edge.
|
||||
|
||||
#### 3.4.7
|
||||
|
||||
- feat: downloadFullImage when the (part of) graph is out of the screen;
|
||||
- feat: With pre-graph has no layout configurations and no positions in data, calling changeData to change into a new data with positions, results in show the node with positions in data;
|
||||
- feat: allow user to assign curveOffset and curvePostion for Bezier curves;
|
||||
- fix: moveTo wrong logic problem;
|
||||
- fix: removeItem to update the minimap.
|
||||
|
||||
#### 3.4.6
|
||||
|
||||
- same as 3.4.5, published wrongly.
|
||||
|
||||
#### 3.4.5
|
||||
|
||||
- feat: background of the label on node or edge;
|
||||
- feat: better performance of minimap;
|
||||
- fix: minimap viewport displace problem;
|
||||
- feat: offset of tooltip;
|
||||
- fix: the length of the node's name affects the tree layout;
|
||||
- fix: toFront does not work for svg renderer;
|
||||
- fix: error occurs when the fontSize is smaller than 12 with svg renderer;
|
||||
- fix: changeData clears states;
|
||||
- fix: state does not work when default labelCfg is not assigned.
|
||||
|
||||
#### 3.4.4
|
||||
|
||||
- feat: background color for downloadImage and toDataURL;
|
||||
- feat: support configure image for grid plugin;
|
||||
- fix: initial position for fruchterman layout;
|
||||
- refactor: clip for image node.
|
||||
- fix: cubic with only one controlPoint error;
|
||||
- fix: polyline without L attributes.
|
||||
|
||||
#### 3.4.3
|
||||
|
||||
- fix: support extends BehaviorOption;
|
||||
- fix: click-select Behavior support multiple selection using ctrl key.
|
||||
|
||||
#### 3.4.2
|
||||
|
||||
- feat: zoom-canvas behavior supports hiding non-keyshape elements when scaling canvas;
|
||||
- refactor: when the second parameter is null, clearItemStates will clear all states of the item;
|
||||
- fix: [changeData bug](https://github.com/antvis/G6/issues/1323);
|
||||
- fix: update antv/hierarchy to fix fixedRoot for TreeGraph;
|
||||
- fix: problem of a graph has multiple polyline edges;
|
||||
- fix: problem of dagre with controlPoints and loop edges.
|
||||
|
||||
#### 3.4.1
|
||||
|
||||
- feat: force layout clone original data model to allow the customized properties;
|
||||
- fix: BehaviorOptions type error;
|
||||
- fix: fitView the graph with data whose nodes and edges are empty arrays;
|
||||
- fix: rect node positions are changed after calling graph.changeData;
|
||||
- fix: drag behavior is disabled when the keys are released invalidly;
|
||||
- refactor: update G and the fill of custom arrow should be assigned by user.
|
||||
|
||||
#### 3.4.0
|
||||
|
||||
- feat: SVG renderer;
|
||||
- refactor: new state mechanism with multiple values, sub graphics shape style settings.
|
||||
|
||||
#### 3.3.7
|
||||
|
||||
- feat: beforeaddchild and afteraddchild emit for TreeGraph;
|
||||
- feat: built-in nodes' labels can be captured;
|
||||
- fix: drag shadow caused by localRefresh, update the g-canvas version;
|
||||
- fix: abnormal polyline bendding;
|
||||
- fix: collapse-expand trigger problem;
|
||||
- fix: update nodes with empty string label;
|
||||
- fix: abnormal rendering when a graph has image nodes and other type nodes.
|
||||
|
||||
#### 3.3.6
|
||||
|
||||
- feat: support edge weight for dagre layout;
|
||||
- feat: automatically add draggable to keyShape, users do not need to assign it when custom a node or an edge;
|
||||
- fix: cannot read 0 or null problem in getPointByCanvas;
|
||||
- fix: brush-select bug;
|
||||
- fix: set autoDraw to canvas when graph's setAutoPaint is called;
|
||||
- fix: modify the usage of bbox in view controller since the interface is chagned by G;
|
||||
- fix: the shape.attr error in updateShapeStyle;
|
||||
- fix: local refresh influence on changeData;
|
||||
- refactor: upgrade g-canvas to 0.3.23 to solve lacking of removeChild function;
|
||||
- doc: update the demo fo custom behavior doc;
|
||||
- doc: add plugin demos and cases for site;
|
||||
- doc: fix shouldUpdate problem in treeWithLargeData demo on the site.
|
||||
|
||||
#### 3.3.5
|
||||
|
||||
- fix: 3.3.4 is not published successfully;
|
||||
|
||||
#### 3.3.4
|
||||
|
||||
- fix: 3.3.3 is not published successfully;
|
||||
- fix: delegate or keyShape type minimap does not display bug;
|
||||
- fix: dragging bug on minimap with a graph whose bbox is nagtive;
|
||||
- fix: null matrix bug, create a unit matrix for null.
|
||||
|
||||
#### 3.3.3
|
||||
|
||||
- fix: delegate or keyShape type minimap does not display bug;
|
||||
- fix: null matrix in focus() and getLoopCfgs() bug.
|
||||
|
||||
#### 3.3.2
|
||||
|
||||
- fix: ts type export problem;
|
||||
- fix: edge with endArrow and autoRotate label bug;
|
||||
- fix: code prettier;
|
||||
- fix: line with control points bug;
|
||||
- fix: matrix null bug.
|
||||
|
||||
#### 3.3.1
|
||||
|
||||
- fix: resolve 3.3.0 compatibility problem.
|
||||
|
||||
#### 3.3.0
|
||||
|
||||
- Graph API
|
||||
- refactor: delete removeEvent function, use off;
|
||||
- refactor: parameters of Shape animate changed, shape.animate(toAttrs, animateCfg) or shape.animate(onFrame, animateCfg);
|
||||
- feat: descriptionCfg for modelRect to define the style of description by user;
|
||||
- feat: update a node from without some shapes to with them, such as linkPoints, label, logo icon and state icon for modelRect;
|
||||
- feat: the callback paramter of event nodeselectchange is changed to { target, selectedItems, ... };
|
||||
- feat: support stateStyles in node and edge data;
|
||||
- feat: calculate pixelRatio by G automatically, user do not need to assign it to graph instance;
|
||||
- chore: G 4.0
|
||||
- refactor: refreshLayout of TreeGraph is renamed as layout
|
||||
- fix: no fan shape in G any more
|
||||
- feat: recommand to assign name for each shape when addShape
|
||||
- fix: do not support SVG renderer anymore. no renderer for graph configuration anymore
|
||||
- refactor: plugins usage is changed into new G6.PluginName()
|
||||
|
||||
#### 3.2.7
|
||||
|
||||
- feat: supports create the group without nodes in node-group;
|
||||
- fix: supports destoryed properties and fix issue 1094;
|
||||
|
||||
#### 3.2.6
|
||||
|
||||
- feat: supports sort the nodes on one circle according to the data ordering or some attribute in radial layout
|
||||
- fix: grid layout with cols and rows
|
||||
- feat: fix the nodes with position information in their original data and random the positions of others when the layout is not defined for graph
|
||||
|
||||
#### 3.2.5
|
||||
|
||||
- fix: click-select trigger error
|
||||
- fix: solved position problem for minimap
|
||||
|
||||
#### 3.2.4
|
||||
|
||||
- fix: typescript compile error
|
||||
- fix: delete sankey lib
|
||||
|
||||
#### 3.2.3
|
||||
|
||||
- fix: group position error
|
||||
- fix: supports not set layout type
|
||||
|
||||
#### 3.1.5
|
||||
|
||||
- feat: supports g6 types file
|
||||
- fix: set brush-select trigger param to ctrl not work
|
||||
- fix: when set fitView to true, drag-group Behavior not get desired positon
|
||||
|
||||
#### 3.1.3
|
||||
|
||||
- feat: radial layout nonoverlap iterations can be controlled by user
|
||||
- feat: add lock, unlock and hasLocked function, supports lock and unlock node
|
||||
- fix: mds with discrete points problem
|
||||
- fix: fruchterman-group layout title position for rect groups
|
||||
|
||||
#### 3.1.2
|
||||
|
||||
- feat: default behavior supports configuration trigger mode
|
||||
- feat: node combining supports configuration title
|
||||
- fix: update demo state styles
|
||||
|
||||
#### 3.1.1
|
||||
|
||||
- fix: update node use custom config
|
||||
- fix: update demo
|
||||
- feat: default node implement getShapeStyle function
|
||||
|
||||
#### 3.1.0
|
||||
|
||||
- feat: support for rich layouts:random, radial, mds, circular, fruchterman, force, dagre
|
||||
- feat: more flexible configuration for shape
|
||||
- feat: build-in rich default nodes
|
||||
- feat: cases that provide layout and default nodes
|
||||
|
||||
#### 3.0.7-beta.1
|
||||
|
||||
`2019-09-11`
|
||||
|
||||
- fix: zoom-canvas support IE and Firefox
|
||||
|
||||
#### 3.0.6
|
||||
|
||||
`2019-09-11`
|
||||
|
||||
- fix: group data util function use module.exports
|
||||
- feat: update @antv/hierarchy version
|
||||
|
||||
#### 3.0.5
|
||||
|
||||
`2019-09-10`
|
||||
|
||||
- feat: support add and remove group
|
||||
- feat: support collapse and expand group
|
||||
- feat: add graph api: collapseGroup and expandGroup
|
||||
|
||||
#### 3.0.5-beta.12
|
||||
|
||||
- feat: add rect group
|
||||
- feat: add rect group demo
|
||||
- feat: add chart node
|
||||
|
||||
---
|
||||
|
||||
#### 3.0.5-beta.10
|
||||
|
||||
- feat: add 5 chart node
|
||||
- feat: collapse-expand tree support click and dblclick by trigger option
|
||||
- fix: drag group bug fix
|
||||
|
||||
#### 3.0.5-beta.9
|
||||
|
||||
- feat: support render group
|
||||
- feat: support drag group, collapse and expand group, drag node in/out group
|
||||
- feat: add drag-group、collapse-expand-group and drag-node-with-group behavior
|
||||
- feat: add drag-group and collapse-expand-group demo
|
||||
- feat: add register list node demo
|
||||
|
||||
#### 3.0.5-beta.8
|
||||
|
||||
`2019-07-19`
|
||||
|
||||
- feat: add five demos
|
||||
- refactor: update three behaviors
|
||||
|
||||
#### 2.2.5
|
||||
|
||||
`2018-12-20`
|
||||
|
||||
- feat: add saveimage limitRatio
|
||||
|
||||
#### 2.2.4
|
||||
|
||||
`2018-12-20`
|
||||
|
||||
- fix: bug fix
|
||||
|
||||
#### 2.2.3
|
||||
|
||||
`2018-12-10`
|
||||
|
||||
- fix: bug fix
|
||||
|
||||
#### 2.2.2
|
||||
|
||||
`2018-11-30`
|
||||
|
||||
- fix: tree remove guide will not getEdges.closes #521
|
||||
|
||||
#### 2.2.1
|
||||
|
||||
`2018-11-25`
|
||||
|
||||
- fix: Compatible with MOUSEWHEEL
|
||||
- fix: fadeIn aniamtion
|
||||
- fix: fix wheelZoom behaviour by removing the deprecated mousewheel event
|
||||
|
||||
#### 2.2.0
|
||||
|
||||
`2018-11-22`
|
||||
|
||||
- fix: Graph read zIndex
|
||||
- refactor: Animation
|
||||
|
||||
#### 2.1.5
|
||||
|
||||
`2018-10-26`
|
||||
|
||||
- fix: svg pixelRatio bug
|
||||
- feat: add wheel event
|
||||
|
||||
#### 2.1.4
|
||||
|
||||
`2018-10-06`
|
||||
|
||||
- fix: custom math.sign to compatible with ie browser.Closes #516.
|
||||
- fix: legend component from @antv/component
|
||||
- feat: update svg minimap && fix svg dom event
|
||||
|
||||
#### 2.1.3
|
||||
|
||||
`2018-09-27`
|
||||
|
||||
- feat: add label rotate
|
||||
- feat: if there is no items the graph box equal canvas size
|
||||
|
||||
#### 2.1.2
|
||||
|
||||
`2018-09-19`
|
||||
|
||||
- fix: dom getShape bug.Closes #472
|
||||
- fix: template.maxSpanningForest bug
|
||||
|
||||
#### 2.1.1
|
||||
|
||||
`2018-09-17`
|
||||
|
||||
- fix: tool.highlightSubgraph calculate box bug
|
||||
- fix: plugin.grid.Closes #479
|
||||
- chore(dev): upgrade babel & torchjs
|
||||
|
||||
#### 2.1.0
|
||||
|
||||
`2018-09-03`
|
||||
|
||||
- feat: svg render
|
||||
- feat: plugin.layout.forceAtlas2
|
||||
- feat: plugin.tool.fisheye
|
||||
- feat: plugin.tool.textDisplay
|
||||
- feat: plugin.tool.grid
|
||||
- feat: plugin.template.tableSankey
|
||||
- feat: plugin.edge.polyline
|
||||
|
||||
#### 2.0.5
|
||||
|
||||
`2018-07-12`
|
||||
|
||||
- improve: add g6 arrow
|
||||
|
||||
#### 2.0.4
|
||||
|
||||
`2018-07-12`
|
||||
|
||||
- feat: layout export group.Closes #355
|
||||
- feat(plugin): add tool.tooltip. Closes #360.
|
||||
- style: change the calling way of forceAtlas2 on template.maxSpanningForest
|
||||
- fix: origin tree data collapsed is true tree edge visible bug.Closes #357
|
||||
- fix: remove the forceAtlas.js in template.maxSpanningForest, use forceAtlas from layout.forceAtlas2
|
||||
- fix: add demos: plugin-fisheye, plugin-forceAtlas2, gallery-graphanalyzer
|
||||
- fix: add demos: plugin-forceAtlas2, plugin-fisheye
|
||||
|
||||
#### 2.0.3
|
||||
|
||||
`2018-06-29`
|
||||
|
||||
- feat: update g to 3.0.x. Closes #346
|
||||
- fix: group should use rect intersect box. Close #297
|
||||
- fix(plugin): dagre edge controlpoints remove start point and end point
|
||||
- style: remove some annotations
|
||||
- chore: update torchjs && improve demo name
|
||||
|
||||
#### 2.0.2
|
||||
|
||||
`2018-06-13`
|
||||
|
||||
- chore(plugin): require g6 by src/index
|
||||
- chore(dev test): remove useless test script
|
||||
- fix(plugin) minimap destroy Closes #308
|
||||
- fix(saveImage) saveImage bug
|
||||
- fix(event): fix dom coord. Closes #305
|
||||
|
||||
#### 2.0.1
|
||||
|
||||
`2018-06-11`
|
||||
|
||||
- fix: reDraw edge after layout
|
||||
- feat: add quadraticCurve config cpd
|
||||
- feat: add beforelayout && afterlayout event
|
||||
- chore: .travis.yml add add Node.js
|
||||
- chore: .travis.yml cache node_modules
|
||||
|
||||
#### 2.0.0
|
||||
|
||||
`2018-06-06`
|
||||
|
||||
- refactor: refactor architecture && code
|
||||
|
||||
#### 1.2.1
|
||||
|
||||
`2018-03-15`
|
||||
|
||||
- feat: layout interface
|
||||
|
||||
#### 1.2.0
|
||||
|
||||
`2018-01-15`
|
||||
|
||||
- fix: nodeActivedBoxStyle spelling error
|
||||
- fix: error when deleting a circle
|
||||
- fix: trigger dragstart while right clicking and moveing
|
||||
- feat: Unify Layout mechanism
|
||||
- feat: Plugin mechanism
|
||||
- feat: Data filter mechanism
|
||||
- feat: Activated interface
|
||||
- feat: Action wheelZoomAutoLabel
|
||||
- feat: configuration of graph -- preciseAnchor
|
||||
- remove: Global.preciseAnchor
|
||||
- remove: Layout.Flow、Layout.Force
|
||||
- improve: html container strategy
|
||||
|
||||
#### 1.1.6
|
||||
|
||||
`2017-10-15`
|
||||
|
||||
- fix: pack problem in layout algorithm
|
||||
|
||||
#### 1.1.5
|
||||
|
||||
`2017-09-15`
|
||||
|
||||
- fix: dragCanvas is effective while mousemove, prevent it from affecting click events
|
||||
- fix: unactivate pick-up in activeRectBox of node
|
||||
|
||||
#### 1.1.4
|
||||
|
||||
`2017-08-15`
|
||||
|
||||
- feat: graph.invertPoint()
|
||||
- feat: third configuration of anchor to support style setting, float style, connection
|
||||
- feat: item.getGroup()
|
||||
- feat: events -- afteritemrender、itemremove、itemadd
|
||||
- feat: behaviourSignal
|
||||
- improve: mouseWheel is affective after focusing the canvas
|
||||
|
||||
#### 1.1.3
|
||||
|
||||
`2017-08-8`
|
||||
|
||||
- feat: Graph configuration -- useNodeSortGroup
|
||||
- feat: Global.nodeDelegationStyle, Global.edgeDelegationStyle, isolate the delegation of edge and node on graph
|
||||
- fix: itemremove is triggered before destroying a graph
|
||||
|
||||
#### 1.1.2
|
||||
|
||||
`2017-08-01`
|
||||
|
||||
- feat: dragBlankX dragBlankY
|
||||
|
||||
#### 1.1.1
|
||||
|
||||
`2017-07-18`
|
||||
|
||||
- improve: dragNode protect mechanism
|
||||
|
||||
#### 1.1.0
|
||||
|
||||
`2017-07-05`
|
||||
|
||||
- feat: HTML node
|
||||
- feat: mapper support callback function
|
||||
- feat: Graph interfaces -- updateMatrix、changeSize、showAnchor、hideAnchor、updataNodesPosition
|
||||
- feat: tool functions -- Util.isNode()、Util.isEdge()
|
||||
- feat: Shape polyLineFlow
|
||||
- feat: dragEdgeEndHideAnchor、dragNodeEndHideAnchor、hoverAnchorSetActived、hoverNodeShowAnchor
|
||||
|
||||
#### 1.0.7
|
||||
|
||||
`2017-06-21`
|
||||
|
||||
- fix: draw one more time in 16ms after first draw
|
||||
- improve: add zoom by scroll in edit mode
|
||||
|
||||
#### 1.0.6
|
||||
|
||||
`2017-06-15`
|
||||
|
||||
- fix: compatible in chrome in windows. triggering mousemove after first click leads to wrong click event.
|
||||
- feat: support fix size graphics
|
||||
- feat: analysis mode
|
||||
- feat: updateNodesPositon update a set of nodes' position
|
||||
- improve: change useAnchor to be a configuration of edge
|
||||
|
||||
#### 1.0.5
|
||||
|
||||
`2017-06-01`
|
||||
|
||||
- feat: downloadImage support saving with name
|
||||
- feat: automatically detect tooltip padding
|
||||
- improve: stop the action while mouse dragging out of the canvas
|
||||
|
||||
#### 1.0.4
|
||||
|
||||
`2017-05-20`
|
||||
|
||||
- fix: tree changeData Bug
|
||||
- fix: when getAnchorPoints returns auto, anchor is the intersection of edge and the bounding box
|
||||
- fix: generate node label according to isNull
|
||||
- feat: viewport parameters -- tl、tc、tr、rc、br、bc、bl、lc、cc
|
||||
- improve: reduce tolerance to improve the accuracy of interception
|
||||
- improve: improve tooltip event mechanisom to enhance performance
|
||||
|
||||
#### 1.0.3
|
||||
|
||||
`2017-05-10`
|
||||
|
||||
- feat: graph.guide().link()
|
||||
|
||||
#### 1.0.2
|
||||
|
||||
`2017-05-10`
|
||||
|
||||
- fix: Object.values => Util.getObjectValues
|
||||
- fix: when anchorPoints is auto, there is only anchorpoint on edge, it will also return the intersection
|
||||
- fix: tree update interface Bug
|
||||
- improve: represent positions information by group.transfrom()
|
||||
|
||||
#### 1.0.1
|
||||
|
||||
`2017-04-22`
|
||||
|
||||
- fix: copy and paste bug
|
||||
- feat: draw once in 16ms
|
||||
- feat: itemactived itemunactived itemhover itemupdate itemmouseenter itemmouseleave
|
||||
- improve: be clear the status of graphics before activating graphics by frame selection
|
||||
- improve: dragAddEdge, linkable to anchor
|
||||
- improve: performance of animation
|
||||
|
||||
#### 1.0.0
|
||||
|
||||
`2017-03-31`
|
||||
|
||||
- feat: fitView configurations
|
||||
- feat: graph.zoom()
|
||||
- feat: wheelZoomHideEdges hide the edges while zooming by wheel
|
||||
- feat: dragHideEdges hide the edge while dragging edge
|
||||
- feat: graph.filterBehaviour()
|
||||
- feat: graph.addBehaviour()
|
||||
- feat: graph.changeLayout()
|
||||
- feat: read interface, re-define save interface
|
||||
- feat: graph.snapshot, graph.downloadImage
|
||||
- feat: graph.autoSize()
|
||||
- feat: graph.focusPoint()
|
||||
- feat: tree graph、net graph
|
||||
- feat: interaction mechanism -- event => action => mode
|
||||
- feat: animation mechanism
|
||||
- feat: itemmouseleave、itemmouseenter
|
||||
- remove: graph.refresh()
|
||||
- remove: graph.changeNodes()
|
||||
- remove: graph attributes -- zoomable、dragable、resizeable、selectable
|
||||
- improve: anchor mechanism
|
||||
- improve: hide G6.GraphUtil functions, unified in G6.Util
|
||||
- improve: replace g-canvas-core to g-canvas to improve performance
|
||||
- improve: Global.nodeAcitveBoxStyle instead of Global.nodeBoxStyle
|
||||
- improve: afterAdd => afteradd
|
||||
- improve: G6.Graph to be an abstract class
|
||||
|
||||
#### 0.2.3
|
||||
|
||||
`2017-03-2`
|
||||
|
||||
- fix: dragable for controlling dragable under default mode
|
||||
- feat: graph.converPoint()
|
||||
- feat: graph.autoSize()
|
||||
- feat: rightmousedown leftmousedown wheeldown
|
||||
- improve: use try catch to prevent the length of getPoint of path equals zero
|
||||
|
||||
#### 0.2.2
|
||||
|
||||
`2017-02-24`
|
||||
|
||||
- fix: add px totooltip css padding
|
||||
- fix: tooltip mapping error
|
||||
- fix: accurate intersection
|
||||
- fix: zoom error on double accuracy screen
|
||||
- fix: buonding box extended from keyShape
|
||||
- feat: afterAdd
|
||||
- feat: dblclick
|
||||
- improve: width、height default null
|
||||
- improve: remove hovershape on node
|
||||
- improve: tooltip defense mechanism
|
||||
|
||||
#### 0.2.1
|
||||
|
||||
`2017-02-14`
|
||||
|
||||
- fix: rollback when add node
|
||||
- fix: apply tranformation of parent container while calculating bounding box
|
||||
- feat: waterPath
|
||||
- feat: tooltip tip information
|
||||
- feat: mouseover
|
||||
- feat: multiSelectable, default false
|
||||
- feat: set forceFit to true while width is undefined
|
||||
- improve: zoomable、dragable、resizeable、selectable default true
|
||||
|
||||
#### 0.2.0
|
||||
|
||||
`2017-02-07`
|
||||
|
||||
- feat: accurate anchor mechanism
|
||||
- feat: GraphUtil.getEllipsePath
|
||||
- feat: GraphUtil.pointsToPolygon
|
||||
- feat: GraphUtil.pointsToBezier
|
||||
- feat: GraphUtil.snapPreciseAnchor
|
||||
- feat: GraphUtil.arrowTo
|
||||
- feat: GraphUtil.drawEdge
|
||||
- feat: bezierQuadratic
|
||||
- feat: node.show
|
||||
- feat: node.hide
|
||||
- feat: node.getLinkNodes
|
||||
- feat: node.getUnLinkNodes
|
||||
- feat: node.getRelativeItems
|
||||
- feat: node.getUnRelativeItems
|
||||
- feat: edge.show
|
||||
- feat: edge.hide
|
||||
- feat: Shape afterDraw
|
||||
- improve: the controlling point positions of Bezier Curve 改进贝塞尔曲线控制点位置
|
||||
- improve: grpah.delete => graph.del
|
||||
- improve: error when adding id
|
||||
|
||||
#### 0.1.4
|
||||
|
||||
`2017-01-17`
|
||||
|
||||
- fix: delegator of dragging a node is the center of bbox
|
||||
- fix: use cardinality sort for all the sorting algorithm
|
||||
- fix: random id on edges
|
||||
- feat: level sort on edges, edge labels on the top level
|
||||
- feat: while extending shape is undefined when register an edge, find the extending shaoe automatically
|
||||
|
||||
#### 0.1.3
|
||||
|
||||
`2017-01-15`
|
||||
|
||||
- fix: judge the existance of the object while operating assistGrid
|
||||
- feat: rollback judgement, default unactivate
|
||||
- feat: style mapping channel
|
||||
- feat: return the intersections while getAnchorPoints is null or returns false
|
||||
- feat: bezierHorizontal、bezierVertical
|
||||
- improve: 'eventEnd'
|
||||
|
||||
#### 0.1.2
|
||||
|
||||
`2017-01-12`
|
||||
|
||||
- fix: judge the configuration before updating grid
|
||||
- fix: the size of graphContainer in unsetable, setted by inner canvas
|
||||
- fix: will not add an edge if the target or source is undefined
|
||||
- fix: changeSize() maximum tolerance for error
|
||||
- feat: graph.get('el') to get canvas DOM
|
||||
- feat: event exposures shape
|
||||
|
||||
#### 0.1.1
|
||||
|
||||
`2017-01-09`
|
||||
|
||||
- feat: entrance of graph is G6.Graph
|
||||
|
||||
#### 0.1.0
|
||||
|
||||
`2017-01-07`
|
||||
|
||||
- feat: color calculation library
|
||||
- feat: hot key
|
||||
- feat: updo, redo
|
||||
- feat: copy, paste
|
||||
- feat: reset zoom, auto zoom
|
||||
- feat: tree graph, linear graph, sankey graph, flow laout
|
||||
- feat: flow chart package
|
||||
- feat: timing diagram package
|
||||
- feat: single selection, frame selection
|
||||
- feat: node deformation
|
||||
- feat: edge deformation
|
||||
- feat: drag node and edge
|
||||
- feat: link edge and node
|
||||
- feat: drag canvas
|
||||
- feat: zoom
|
||||
- feat: select mode
|
||||
- feat: integrate g-graph
|
139
packages/core/CONTRIBUTING.md
Normal file
139
packages/core/CONTRIBUTING.md
Normal file
@ -0,0 +1,139 @@
|
||||
# Contribution Guide
|
||||
|
||||
If you have any comment or advice, please report your [issue](https://github.com/antvis/g6/issues), or make any change as you wish and submit an [PR](https://github.com/antvis/g6/pulls).
|
||||
|
||||
## Reporting New Issues
|
||||
|
||||
- Please specify what kind of issue it is.
|
||||
- Before you report an issue, please search for related issues. Make sure you are not going to open a duplicate issue.
|
||||
- Explain your purpose clearly in tags(see **Useful Tags**), title, or content.
|
||||
|
||||
AntV group members will confirm the purpose of the issue, replace more accurate tags for it, identify related milestone, and assign developers working on it.
|
||||
|
||||
## Submitting Code
|
||||
|
||||
### Pull Request Guide
|
||||
|
||||
If you are developer of AntV repo and you are willing to contribute, feel free to create a new branch, finish your modification and submit a PR. AntV group will review your work and merge it to master branch.
|
||||
|
||||
```bash
|
||||
# Create a new branch for development. The name of branch should be semantic, avoiding words like 'update' or 'tmp'. We suggest to use feature/xxx, if the modification is about to implement a new feature.
|
||||
$ git checkout -b branch-name
|
||||
|
||||
# Run the test after you finish your modification. Add new test cases or change old ones if you feel necessary
|
||||
$ npm test
|
||||
|
||||
# If your modification pass the tests, congradulations it's time to push your work back to us. Notice that the commit message should be wirtten in the following format.
|
||||
$ git add . # git add -u to delete files
|
||||
$ git commit -m "fix(role): role.use must xxx"
|
||||
$ git push origin branch-name
|
||||
```
|
||||
|
||||
Then you can create a Pull Request at [g6](https://github.com/antvis/g6/pulls).
|
||||
|
||||
No one can guarantee how much will be remembered about certain PR after some time. To make sure we can easily recap what happened previously, please provide the following information in your PR.
|
||||
|
||||
1. Need: What function you want to achieve (Generally, please point out which issue is related).
|
||||
2. Updating Reason: Different with issue. Briefly describe your reason and logic about why you need to make such modification.
|
||||
3. Related Testing: Briefly describe what part of testing is relevant to your modification.
|
||||
4. User Tips: Notice for g6 users. You can skip this part, if the PR is not about update in API or potential compatibility problem.
|
||||
|
||||
### Style Guide
|
||||
|
||||
Eslint can help to identify styling issues that may exist in your code. Your code is required to pass the test from eslint. Run the test locally by `$ npm run lint`.
|
||||
|
||||
### Commit Message Format
|
||||
|
||||
You are encouraged to use [angular commit-message-format](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#commit-message-format) to write commit message. In this way, we could have a more trackable history and an automatically generated changelog.
|
||||
|
||||
```xml
|
||||
<type>(<scope>): <subject>
|
||||
<BLANK LINE>
|
||||
<body>
|
||||
<BLANK LINE>
|
||||
<footer>
|
||||
```
|
||||
|
||||
(1)type
|
||||
|
||||
Must be one of the following:
|
||||
|
||||
- feat: A new feature
|
||||
- fix: A bug fix
|
||||
- docs: Documentation-only changes
|
||||
- style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
|
||||
- refactor: A code change that neither fixes a bug nor adds a feature
|
||||
- perf: A code change that improves performance
|
||||
- test: Adding missing tests
|
||||
- chore: Changes to the build process or auxiliary tools and libraries such as documentation generation
|
||||
- deps: Updates about dependencies
|
||||
|
||||
(2)scope
|
||||
|
||||
The scope could be anything specifying place of the commit change.
|
||||
|
||||
(3)subject
|
||||
|
||||
Use succinct words to describe what did you do in the commit change.
|
||||
|
||||
(4)body
|
||||
|
||||
Feel free to add more content in the body, if you think subject is not self-explanatory enough, such as what it is the purpose or reasons of you commit.
|
||||
|
||||
(5)footer
|
||||
|
||||
- **If the commit is a Breaking Change, please note it clearly in this part.**
|
||||
- related issues, like `Closes #1, Closes #2, #3`
|
||||
|
||||
e.g.
|
||||
|
||||
```
|
||||
fix($compile): [BREAKING_CHANGE] couple of unit tests for IE9
|
||||
|
||||
Older IEs serialize html uppercased, but IE9 does not...
|
||||
Would be better to expect case insensitive, unfortunately jasmine does
|
||||
not allow to user regexps for throw expectations.
|
||||
|
||||
Document change on antvis/g6#123
|
||||
|
||||
Closes #392
|
||||
|
||||
BREAKING CHANGE:
|
||||
|
||||
Breaks foo.bar api, foo.baz should be used instead
|
||||
```
|
||||
|
||||
Look at [these files](https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit) for more details.
|
||||
|
||||
## Release
|
||||
|
||||
g6 uses semantic versioning in release process based on [semver].
|
||||
|
||||
### Branch Strategy
|
||||
|
||||
`master` branch is the latest stable version.
|
||||
|
||||
- just checkout develop branch from `master`
|
||||
- All new features will be added into `master` or `next` branch as well as all bug-fix except security issues. In such way, we can motivate developers to update to the latest stable version.
|
||||
|
||||
### Release Strategy
|
||||
|
||||
In the release of every stable version, there will be a PM who has the following responsibilities in different stages of the release.
|
||||
|
||||
#### Preparation
|
||||
|
||||
- Set up milestone. Confirm that request is related to milestone.
|
||||
|
||||
#### Before Release
|
||||
|
||||
- Confirm that performance test is passed and all issues in current Milestone are either closed or can be delayed to later versions.
|
||||
- Open a new [Release Proposal MR], and write `History` as [node CHANGELOG]. Don't forget to correct content in documentation which is related to the releasing version. Commits can be generated automatically.
|
||||
```
|
||||
$ npm run commits
|
||||
```
|
||||
- Nominate PM for next stable version.
|
||||
|
||||
[semver]: http://semver.org/lang/zh-CN/
|
||||
[release proposal mr]: https://github.com/nodejs/node/pull/4181
|
||||
[node changelog]: https://github.com/nodejs/node/blob/master/CHANGELOG.md
|
||||
[『我是如何发布一个 npm 包的』]: https://fengmk2.com/blog/2016/how-i-publish-a-npm-package
|
145
packages/core/CONTRIBUTING.zh-CN.md
Normal file
145
packages/core/CONTRIBUTING.zh-CN.md
Normal file
@ -0,0 +1,145 @@
|
||||
# 代码贡献规范
|
||||
|
||||
有任何疑问,欢迎提交 [issue](https://github.com/antvis/g6/issues),或者直接修改提交 [PR](https://github.com/antvis/g6/pulls)!
|
||||
|
||||
## 提交 issue
|
||||
|
||||
- 请确定 issue 的类型。
|
||||
- 请避免提交重复的 issue,在提交之前搜索现有的 issue。
|
||||
- 在标签(分类参考**标签分类**), 标题 或者内容中体现明确的意图。
|
||||
|
||||
随后 AntV 负责人会确认 issue 意图,更新合适的标签,关联 milestone,指派开发者。
|
||||
|
||||
## 提交代码
|
||||
|
||||
### 提交 Pull Request
|
||||
|
||||
如果你有仓库的开发者权限,而且希望贡献代码,那么你可以创建分支修改代码提交 PR,AntV 开发团队会 review 代码合并到主干。
|
||||
|
||||
```bash
|
||||
# 先创建开发分支开发,分支名应该有含义,避免使用 update、tmp 之类的
|
||||
$ git checkout -b branch-name
|
||||
|
||||
# 开发完成后跑下测试是否通过,必要时需要新增或修改测试用例
|
||||
$ npm test
|
||||
|
||||
# 测试通过后,提交代码,message 见下面的规范
|
||||
|
||||
$ git add . # git add -u 删除文件
|
||||
$ git commit -m "fix(role): role.use must xxx"
|
||||
$ git push origin branch-name
|
||||
```
|
||||
|
||||
提交后就可以在 [g6](https://github.com/antvis/g6/pulls) 创建 Pull Request 了。
|
||||
|
||||
由于谁也无法保证过了多久之后还记得多少,为了后期回溯历史的方便,请在提交 MR 时确保提供了以下信息。
|
||||
|
||||
1. 需求点(一般关联 issue 或者注释都算)
|
||||
2. 升级原因(不同于 issue,可以简要描述下为什么要处理)
|
||||
3. 框架测试点(可以关联到测试文件,不用详细描述,关键点即可)
|
||||
4. 关注点(针对用户而言,可以没有,一般是不兼容更新等,需要额外提示)
|
||||
|
||||
### 代码风格
|
||||
|
||||
你的代码风格必须通过 eslint,你可以运行 `$ npm run lint` 本地测试。
|
||||
|
||||
### Commit 提交规范
|
||||
|
||||
根据 [angular 规范](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#commit-message-format)提交 commit,这样 history 看起来更加清晰,还可以自动生成 changelog。
|
||||
|
||||
```xml
|
||||
<type>(<scope>): <subject>
|
||||
<BLANK LINE>
|
||||
<body>
|
||||
<BLANK LINE>
|
||||
<footer>
|
||||
```
|
||||
|
||||
(1)type
|
||||
|
||||
提交 commit 的类型,包括以下几种
|
||||
|
||||
- feat: 新功能
|
||||
- fix: 修复问题
|
||||
- docs: 修改文档
|
||||
- style: 修改代码格式,不影响代码逻辑
|
||||
- refactor: 重构代码,理论上不影响现有功能
|
||||
- perf: 提升性能
|
||||
- test: 增加修改测试用例
|
||||
- chore: 修改工具相关(包括但不限于文档、代码生成等)
|
||||
- deps: 升级依赖
|
||||
|
||||
(2)scope
|
||||
|
||||
修改文件的范围
|
||||
|
||||
(3)subject
|
||||
|
||||
用一句话清楚的描述这次提交做了什么
|
||||
|
||||
(4)body
|
||||
|
||||
补充 subject,适当增加原因、目的等相关因素,也可不写。
|
||||
|
||||
(5)footer
|
||||
|
||||
- **当有非兼容修改(Breaking Change)时必须在这里描述清楚**
|
||||
- 关联相关 issue,如 `Closes #1, Closes #2, #3`
|
||||
|
||||
示例
|
||||
|
||||
```
|
||||
fix($compile): [BREAKING_CHANGE] couple of unit tests for IE9
|
||||
|
||||
Older IEs serialize html uppercased, but IE9 does not...
|
||||
Would be better to expect case insensitive, unfortunately jasmine does
|
||||
not allow to user regexps for throw expectations.
|
||||
|
||||
Document change on antvis/g6#12
|
||||
|
||||
Closes #392
|
||||
|
||||
BREAKING CHANGE:
|
||||
|
||||
Breaks foo.bar api, foo.baz should be used instead
|
||||
```
|
||||
|
||||
查看具体[文档](https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit)
|
||||
|
||||
## 发布管理
|
||||
|
||||
g6 基于 [semver] 语义化版本号进行发布。
|
||||
|
||||
`master` 分支为当前稳定发布的版本。
|
||||
|
||||
- 直接从 `master` 切出开发分支
|
||||
- 所有 API 的废弃都需要在当前的稳定版本上 `deprecate` 提示,并保证在当前的稳定版本上一直兼容到新版本的发布。
|
||||
|
||||
### 发布策略
|
||||
|
||||
每个大版本都有一个发布经理管理(PM),他/她要做的事情
|
||||
|
||||
#### 准备工作:
|
||||
|
||||
- 建立 milestone,确认需求关联 milestone,指派和更新 issues。
|
||||
|
||||
#### 发布前:
|
||||
|
||||
- 确认当前 Milestone 所有的 issue 都已关闭或可延期,完成性能测试。
|
||||
- 发起一个新的 [Release Proposal MR],按照 [node CHANGELOG] 进行 `History` 的编写,修正文档中与版本相关的内容,commits 可以自动生成。
|
||||
```bash
|
||||
$ npm run commits
|
||||
```
|
||||
- 指定下一个大版本的 PM。
|
||||
|
||||
#### 发布时:
|
||||
|
||||
- 将老的稳定版本(master)备份到以当前大版本为名字的分支上(例如 `1.x`),并设置 tag 为 {v}.x`( v 为当前版本,例如`1.x`)。
|
||||
- 发布新的稳定版本到 [npm],并通知上层框架进行更新。
|
||||
- `npm publish` 之前,请先阅读[『我是如何发布一个 npm 包的』]。
|
||||
|
||||
[semver]: http://semver.org/lang/zh-CN/
|
||||
[release proposal mr]: https://github.com/nodejs/node/pull/4181
|
||||
[node changelog]: https://github.com/nodejs/node/blob/master/CHANGELOG.md
|
||||
[npm]: http://npmjs.com/
|
||||
[『我是如何发布一个 npm 包的』]: https://fengmk2.com/blog/2016/how-i-publish-a-npm-package
|
156
packages/core/README-zh_CN.md
Normal file
156
packages/core/README-zh_CN.md
Normal file
@ -0,0 +1,156 @@
|
||||
![](https://user-images.githubusercontent.com/6113694/45008751-ea465300-b036-11e8-8e2a-166cbb338ce2.png)
|
||||
|
||||
[![travis-ci](https://img.shields.io/travis/antvis/g6.svg)](https://travis-ci.org/antvis/g6) [![codecov](https://codecov.io/gh/antvis/G6/branch/master/graph/badge.svg)](https://codecov.io/gh/antvis/G6) ![typescript](https://img.shields.io/badge/language-typescript-red.svg) ![MIT](https://img.shields.io/badge/license-MIT-000000.svg) [![npm package](https://img.shields.io/npm/v/@antv/g6.svg)](https://www.npmjs.com/package/@antv/g6) [![NPM downloads](http://img.shields.io/npm/dm/@antv/g6.svg)](https://npmjs.org/package/@antv/g6) [![Percentage of issues still open](http://isitmaintained.com/badge/open/antvis/g6.svg)](http://isitmaintained.com/project/antvis/g6 'Percentage of issues still open')
|
||||
|
||||
[English README](README.md)
|
||||
|
||||
## 什么是 G6
|
||||
|
||||
[G6](https://github.com/antvis/g6) 是一个图可视化引擎。它提供了图的绘制、布局、分析、交互、动画等图可视化的基础能力。旨在让关系变得透明,简单。让用户获得关系数据的 Insight。
|
||||
|
||||
<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*zTjwQaXokeQAAAAAAAAAAABkARQnAQ' width=550 />
|
||||
|
||||
基于 G6,用户可以快速搭建自己的 **图分析** 或 **图编辑** 应用。
|
||||
|
||||
<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*zau8QJcVpDQAAAAAAAAAAABkARQnAQ' height=200 /><img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*RIlETY_S6IoAAAAAAAAAAABkARQnAQ' height=200 />
|
||||
|
||||
<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*cDzXR4jIWr8AAAAAAAAAAABkARQnAQ' height=150 /><img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*DifbSahOblAAAAAAAAAAAABkARQnAQ' height=150 /><img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*HTasSJGC4koAAAAAAAAAAABkARQnAQ' height=150 />
|
||||
|
||||
> 强大的动画及交互
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/6113694/44995293-02858600-afd5-11e8-840c-349e4730d63d.gif" height=150><img src="https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*I9OdTbXJIi0AAAAAAAAAAABkARQnAQ" height=150><img src="https://user-images.githubusercontent.com/6113694/44995332-2ba61680-afd5-11e8-8cab-db0e9d08ceb7.gif" height=150>
|
||||
|
||||
<img src="https://gw.alipayobjects.com/zos/rmsportal/HQxYguinFOMIXrGQOABY.gif" height=150><img src="https://gw.alipayobjects.com/zos/rmsportal/nAugyFgrbrUWPmDIDiQm.gif" height=150><img src="https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*xoufSYcjK2AAAAAAAAAAAABkARQnAQ" height=150>
|
||||
|
||||
> 强大的布局
|
||||
|
||||
## G6 的特性
|
||||
|
||||
G6 作为一款专业的图可视化引擎,具有以下特性:
|
||||
|
||||
- 丰富的元素:内置丰富的节点与边元素,自由配置,支持自定义;
|
||||
- 可控的交互:内置 10+ 交互行为,支持自定义交互;
|
||||
- 强大的布局:内置了 10+ 常用的图布局,支持自定义布局;
|
||||
- 便捷的组件:优化内置组件功能及性能;
|
||||
- 友好的体验:根据用户需求分层梳理文档,支持 TypeScript 类型推断。
|
||||
|
||||
除了默认好用、配置自由的内置功能,元素、交互、布局均具有高可扩展的自定义机制。
|
||||
|
||||
<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*Y0c6S7cxjVkAAAAAAAAAAABkARQnAQ' width=800 height=200 />
|
||||
|
||||
> 丰富的图元素
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
$ npm install @antv/g6
|
||||
```
|
||||
|
||||
## 使用
|
||||
|
||||
<img src="https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*khbvSrptr0kAAAAAAAAAAABkARQnAQ" width=437 height=148 />
|
||||
|
||||
```js
|
||||
import G6 from '@antv/g6';
|
||||
|
||||
const data = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'node1',
|
||||
label: 'Circle1',
|
||||
x: 150,
|
||||
y: 150,
|
||||
},
|
||||
{
|
||||
id: 'node2',
|
||||
label: 'Circle2',
|
||||
x: 400,
|
||||
y: 150,
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: 'node1',
|
||||
target: 'node2',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const graph = new G6.Graph({
|
||||
container: 'container',
|
||||
width: 500,
|
||||
height: 500,
|
||||
defaultNode: {
|
||||
type: 'circle',
|
||||
size: [100],
|
||||
color: '#5B8FF9',
|
||||
style: {
|
||||
fill: '#9EC9FF',
|
||||
lineWidth: 3,
|
||||
},
|
||||
labelCfg: {
|
||||
style: {
|
||||
fill: '#fff',
|
||||
fontSize: 20,
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultEdge: {
|
||||
style: {
|
||||
stroke: '#e2e2e2',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
```
|
||||
|
||||
[![Edit compassionate-lalande-5lxm7](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/compassionate-lalande-5lxm7?fontsize=14&hidenavigation=1&theme=dark)
|
||||
|
||||
更多关于 G6 使用的问题,请参考[快速上手](https://antv-g6.gitee.io/zh/docs/manual/getting-started)。
|
||||
|
||||
## 开发
|
||||
|
||||
```bash
|
||||
$ npm install
|
||||
|
||||
# run test case
|
||||
$ npm test
|
||||
|
||||
# run test case in watch mode
|
||||
npm test -- --watch ./tests/unit/algorithm/find-path-spec
|
||||
DEBUG_MODE=1 npm test -- --watch ./tests/unit/algorithm/find-path-spec
|
||||
|
||||
# build watching file changes and run demos
|
||||
$ npm run demos
|
||||
```
|
||||
|
||||
## 文档
|
||||
|
||||
- <a href='https://g6.antv.vision/zh/docs/manual/tutorial/preface' target='_blank'>入门教程</a>
|
||||
- <a href='https://g6.antv.vision/zh/docs/manual/middle/overview' target='_blank'>核心概念</a>
|
||||
- <a href='https://g6.antv.vision/zh/docs/manual/advanced/coordinate-system' target='_blank'>扩展阅读</a>
|
||||
- <a href='https://g6.antv.vision/zh/docs/api/Graph' target='_blank'>API</a>
|
||||
|
||||
## G6 图可视化交流群
|
||||
|
||||
欢迎各界 G6 使用者、图可视化爱好者加入 **G6 图可视化交流群** 及 **G6 图可视化交流二群**(钉钉群,使用钉钉扫一扫加入)讨论与交流。
|
||||
|
||||
> **G6 图可视化交流群** 已满员,该群会不定期移除不活跃的成员。
|
||||
|
||||
> 由于值班同学的时间和精力有限,**G6 图可视化交流二群** 中的问题我们会不定期回复。欢迎对 G6 感兴趣的同学加入到答疑中来,非常感谢!交流群-1 已满,请加交流群-2。
|
||||
|
||||
<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*LFppR6_BfdYAAAAAAAAAAABkARQnAQ' width=400 alt="">
|
||||
|
||||
<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*hzfaSrAj0jkAAAAAAAAAAABkARQnAQ' width=400 alt="">
|
||||
|
||||
## 如何贡献
|
||||
|
||||
请让我们知道您要解决或贡献什么,所以在贡献之前请先提交 [issues](https://github.com/antvis/g6/issues) 描述 bug 或建议。
|
||||
|
||||
成为一个贡献者前请阅读 [代码贡献规范](https://github.com/antvis/g6/blob/master/CONTRIBUTING.zh-CN.md).
|
||||
|
||||
## License
|
||||
|
||||
[MIT license](./LICENSE).
|
152
packages/core/README.md
Normal file
152
packages/core/README.md
Normal file
@ -0,0 +1,152 @@
|
||||
# G6: A Graph Visualization Framework in TypeScript.
|
||||
|
||||
![](https://user-images.githubusercontent.com/6113694/45008751-ea465300-b036-11e8-8e2a-166cbb338ce2.png)
|
||||
|
||||
[![travis-ci](https://img.shields.io/travis/antvis/g6/master.svg)](https://travis-ci.org/antvis/g6) [![codecov](https://codecov.io/gh/antvis/G6/branch/master/graph/badge.svg)](https://codecov.io/gh/antvis/G6) ![typescript](https://img.shields.io/badge/language-typescript-red.svg) ![MIT](https://img.shields.io/badge/license-MIT-000000.svg) [![npm package](https://img.shields.io/npm/v/@antv/g6.svg)](https://www.npmjs.com/package/@antv/g6) [![NPM downloads](http://img.shields.io/npm/dm/@antv/g6.svg)](https://npmjs.org/package/@antv/g6) [![Percentage of issues still open](http://isitmaintained.com/badge/open/antvis/g6.svg)](http://isitmaintained.com/project/antvis/g6 'Percentage of issues still open')
|
||||
|
||||
[中文 README](README-zh_CN.md)
|
||||
|
||||
## What is G6
|
||||
|
||||
[G6](https://github.com/antvis/g6) is a graph visualization engine, which provides a set of basic mechanisms, including rendering, layout, analysis, interaction, animation, and other auxiliary tools. G6 aims to simplify the relationships, and help people to obtain the insight of relational data.
|
||||
|
||||
<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*zTjwQaXokeQAAAAAAAAAAABkARQnAQ' width=550 />
|
||||
|
||||
Developers are able to build graph visualization **analysis** applications or graph visualization **modeling** applications easily.
|
||||
|
||||
<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*zau8QJcVpDQAAAAAAAAAAABkARQnAQ' height=200 /><img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*RIlETY_S6IoAAAAAAAAAAABkARQnAQ' height=200 />
|
||||
|
||||
<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*cDzXR4jIWr8AAAAAAAAAAABkARQnAQ' height=150 /><img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*DifbSahOblAAAAAAAAAAAABkARQnAQ' height=150 /><img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*HTasSJGC4koAAAAAAAAAAABkARQnAQ' height=150 />
|
||||
|
||||
> Powerful Animation and Interactions
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/6113694/44995293-02858600-afd5-11e8-840c-349e4730d63d.gif" height=150><img src="https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*I9OdTbXJIi0AAAAAAAAAAABkARQnAQ" height=150><img src="https://user-images.githubusercontent.com/6113694/44995332-2ba61680-afd5-11e8-8cab-db0e9d08ceb7.gif" height=150>
|
||||
|
||||
<img src="https://gw.alipayobjects.com/zos/rmsportal/HQxYguinFOMIXrGQOABY.gif" height=150><img src="https://gw.alipayobjects.com/zos/rmsportal/nAugyFgrbrUWPmDIDiQm.gif" height=150><img src="https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*xoufSYcjK2AAAAAAAAAAAABkARQnAQ" height=150>
|
||||
|
||||
> Powerful Layouts
|
||||
|
||||
## Features
|
||||
|
||||
- Abundant Built-in Items: Nodes and edges with free configurations;
|
||||
- Steerable Interactions: More than 10 basic interaction behaviors ;
|
||||
- Powerful Layout: More than 10 layout algorithms;
|
||||
- Convenient Components: Outstanding ability and performance;
|
||||
- Friendly User Experience: Complete documents for different levels of user requirements. TypeScript supported.
|
||||
|
||||
G6 concentrates on the principle of 'good by default'. In addition, the custom mechanism of the item, interation behavior, and layout satisfies the customazation requirements.
|
||||
|
||||
<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*Y0c6S7cxjVkAAAAAAAAAAABkARQnAQ' width=800 height=200 />
|
||||
|
||||
> Abundant Built-in Items
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
$ npm install @antv/g6
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
<img src="https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*khbvSrptr0kAAAAAAAAAAABkARQnAQ" width=437 height=148 />
|
||||
|
||||
```js
|
||||
import G6 from '@antv/g6';
|
||||
|
||||
const data = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'node1',
|
||||
label: 'Circle1',
|
||||
x: 150,
|
||||
y: 150,
|
||||
},
|
||||
{
|
||||
id: 'node2',
|
||||
label: 'Circle2',
|
||||
x: 400,
|
||||
y: 150,
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: 'node1',
|
||||
target: 'node2',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const graph = new G6.Graph({
|
||||
container: 'container',
|
||||
width: 500,
|
||||
height: 500,
|
||||
defaultNode: {
|
||||
type: 'circle',
|
||||
size: [100],
|
||||
color: '#5B8FF9',
|
||||
style: {
|
||||
fill: '#9EC9FF',
|
||||
lineWidth: 3,
|
||||
},
|
||||
labelCfg: {
|
||||
style: {
|
||||
fill: '#fff',
|
||||
fontSize: 20,
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultEdge: {
|
||||
style: {
|
||||
stroke: '#e2e2e2',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
```
|
||||
|
||||
[![Edit compassionate-lalande-5lxm7](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/compassionate-lalande-5lxm7?fontsize=14&hidenavigation=1&theme=dark)
|
||||
|
||||
For more information of the usage, please refer to [Getting Started](https://antv-g6.gitee.io/en/docs/manual/getting-started).
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
$ npm install
|
||||
|
||||
# run test case
|
||||
$ npm test
|
||||
|
||||
# run test case in watch mode
|
||||
npm test -- --watch ./tests/unit/algorithm/find-path-spec
|
||||
DEBUG_MODE=1 npm test -- --watch ./tests/unit/algorithm/find-path-spec
|
||||
|
||||
# build watching file changes and run demos
|
||||
$ npm run demos
|
||||
```
|
||||
|
||||
## Documents
|
||||
|
||||
- <a href='https://g6.antv.vision/en/docs/manual/tutorial/preface' target='_blank'>Tutorial</a>
|
||||
- <a href='https://g6.antv.vision/en/docs/manual/middle/overview' target='_blank'>Middle Guides</a>
|
||||
- <a href='https://g6.antv.vision/en/docs/manual/advanced/coordinate-system' target='_blank'>Further Reading</a>
|
||||
- <a href='https://g6.antv.vision/en/docs/api/Graph' target='_blank'>API Reference</a>
|
||||
|
||||
## G6 Communication Group
|
||||
|
||||
Users are welcome to join the **G6 Communication Group** or **G6 Communication Group-2** (They are DingTalk groups). We also welcome the github issues. The Group is full, join Group-2 instead.
|
||||
|
||||
<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*LFppR6_BfdYAAAAAAAAAAABkARQnAQ' width=400 alt="">
|
||||
|
||||
<img src='https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*hzfaSrAj0jkAAAAAAAAAAABkARQnAQ' width=400 alt="">
|
||||
|
||||
## How to Contribute
|
||||
|
||||
Please let us know what you are you going to help. Do check out [issues](https://github.com/antvis/g6/issues) for bug reports or suggestions first.
|
||||
|
||||
To become a contributor, please follow our [contributing guide](https://github.com/antvis/g6/blob/master/CONTRIBUTING.md).
|
||||
|
||||
## License
|
||||
|
||||
[MIT license](./LICENSE).
|
19
packages/core/jest.config.js
Normal file
19
packages/core/jest.config.js
Normal file
@ -0,0 +1,19 @@
|
||||
module.exports = {
|
||||
runner: 'jest-electron/runner',
|
||||
testEnvironment: 'jest-electron/environment',
|
||||
preset: 'ts-jest',
|
||||
collectCoverage: false,
|
||||
collectCoverageFrom: ['src/**/*.{ts,js}', '!**/node_modules/**', '!**/vendor/**'],
|
||||
testRegex: '/tests/.*-spec\\.ts?$',
|
||||
moduleDirectories: ['node_modules', 'src'],
|
||||
moduleFileExtensions: ['js', 'ts', 'json'],
|
||||
moduleNameMapper: {
|
||||
'@g6/types': '<rootDir>/types',
|
||||
'@g6/(.*)': '<rootDir>/src/$1',
|
||||
},
|
||||
globals: {
|
||||
'ts-jest': {
|
||||
diagnostics: false,
|
||||
},
|
||||
},
|
||||
};
|
109
packages/core/package.json
Normal file
109
packages/core/package.json
Normal file
@ -0,0 +1,109 @@
|
||||
{
|
||||
"name": "@antv/g6-core",
|
||||
"version": "4.0.3",
|
||||
"description": "A Graph Visualization Framework in JavaScript",
|
||||
"keywords": [
|
||||
"antv",
|
||||
"g6",
|
||||
"graph",
|
||||
"graph analysis",
|
||||
"graph editor",
|
||||
"graph visualization",
|
||||
"relational data"
|
||||
],
|
||||
"homepage": "https://g6.antv.vision",
|
||||
"bugs": {
|
||||
"url": "https://github.com/antvis/g6/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/antvis/g6"
|
||||
},
|
||||
"license": "MIT",
|
||||
"author": "https://github.com/orgs/antvis/people",
|
||||
"files": [
|
||||
"package.json",
|
||||
"es",
|
||||
"lib",
|
||||
"dist",
|
||||
"LICENSE",
|
||||
"README.md"
|
||||
],
|
||||
"main": "lib/index.js",
|
||||
"module": "es/index.js",
|
||||
"browser": "dist/g6.min.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "npm run clean && father build && npm run build:umd",
|
||||
"build:umd": "webpack --config webpack.config.js --mode production --profile --json > stats.json",
|
||||
"ci": "npm run build && npm run coverage",
|
||||
"clean": "rimraf es esm lib dist",
|
||||
"coverage": "jest --coverage",
|
||||
"doc": "rimraf apis && typedoc",
|
||||
"lint": "eslint --ext .js,.jsx,.ts,.tsx --format=pretty \"./\"",
|
||||
"lint:src": "eslint --ext .ts --format=pretty \"./src\"",
|
||||
"prettier": "prettier -c --write \"**/*\"",
|
||||
"test": "jest",
|
||||
"test-live": "DEBUG_MODE=1 jest --watch ./tests/unit/graph/graph-watermarker-spec.ts",
|
||||
"lint-staged:js": "eslint --ext .js,.jsx,.ts,.tsx"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/*.{js,jsx,ts,tsx}": [
|
||||
"npm run lint-staged:js",
|
||||
"prettier --write"
|
||||
],
|
||||
"**/*.{less,md,json}": [
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@antv/dom-util": "^2.0.1",
|
||||
"@antv/event-emitter": "~0.1.0",
|
||||
"@antv/g-base": "^0.5.1",
|
||||
"@antv/g-canvas": "^0.5.2",
|
||||
"@antv/g-math": "^0.1.1",
|
||||
"@antv/g-svg": "^0.5.1",
|
||||
"@antv/g-webgpu": "^0.5.1",
|
||||
"@antv/hierarchy": "^0.6.2",
|
||||
"@antv/matrix-util": "^2.0.4",
|
||||
"@antv/path-util": "^2.0.3",
|
||||
"@antv/scale": "^0.3.1",
|
||||
"@antv/util": "~2.0.5",
|
||||
"dagre": "^0.8.5",
|
||||
"insert-css": "^2.0.0",
|
||||
"ml-matrix": "^6.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.7.7",
|
||||
"@babel/plugin-proposal-class-properties": "^7.1.0",
|
||||
"@babel/preset-react": "^7.7.4",
|
||||
"@turf/turf": "^5.1.6",
|
||||
"@types/jest": "^25.2.1",
|
||||
"@types/node": "13.11.1",
|
||||
"@typescript-eslint/eslint-plugin": "^3.9.0",
|
||||
"@umijs/fabric": "^2.0.0",
|
||||
"awesome-typescript-loader": "^5.2.1",
|
||||
"babel-loader": "^8.0.6",
|
||||
"eslint": "^7.7.0",
|
||||
"event-simulate": "~1.0.0",
|
||||
"father": "^2.29.1",
|
||||
"husky": "^4.2.5",
|
||||
"jest": "^26.0.1",
|
||||
"jest-electron": "^0.1.7",
|
||||
"jest-extended": "^0.11.2",
|
||||
"lint-staged": "^10.2.11",
|
||||
"pre-commit": "^1.2.2",
|
||||
"prettier": "^2.0.5",
|
||||
"rimraf": "^3.0.0",
|
||||
"ts-jest": "^24.1.0",
|
||||
"ts-loader": "^7.0.3",
|
||||
"typescript": "^3.5.3",
|
||||
"webpack": "^4.41.4",
|
||||
"webpack-cli": "^3.3.10"
|
||||
}
|
||||
}
|
39
packages/core/src/algorithm/adjacent-matrix.ts
Normal file
39
packages/core/src/algorithm/adjacent-matrix.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { IGraph } from '../interface/graph';
|
||||
import { Matrix } from '../types';
|
||||
|
||||
const adjMatrix = (graph: IGraph, directed?: boolean) => {
|
||||
const nodes = graph.getNodes();
|
||||
const edges = graph.getEdges();
|
||||
const matrix: Matrix[] = [];
|
||||
// map node with index in data.nodes
|
||||
const nodeMap: {
|
||||
[key: string]: number;
|
||||
} = {};
|
||||
|
||||
if (!nodes) {
|
||||
throw new Error('invalid nodes data!');
|
||||
}
|
||||
if (nodes) {
|
||||
nodes.forEach((node, i) => {
|
||||
nodeMap[node.getID()] = i;
|
||||
const row: number[] = [];
|
||||
matrix.push(row);
|
||||
});
|
||||
}
|
||||
|
||||
if (edges) {
|
||||
edges.forEach((e) => {
|
||||
const model = e.getModel();
|
||||
const { source, target } = model;
|
||||
const sIndex = nodeMap[source as string];
|
||||
const tIndex = nodeMap[target as string];
|
||||
matrix[sIndex][tIndex] = 1;
|
||||
if (!directed) {
|
||||
matrix[tIndex][sIndex] = 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
return matrix;
|
||||
};
|
||||
|
||||
export default adjMatrix;
|
83
packages/core/src/algorithm/comparator.ts
Normal file
83
packages/core/src/algorithm/comparator.ts
Normal file
@ -0,0 +1,83 @@
|
||||
export default class Comparator {
|
||||
private compare;
|
||||
|
||||
/**
|
||||
* @param {function(a: *, b: *)} [compareFunction] - It may be custom compare function that, let's
|
||||
* say may compare custom objects together.
|
||||
*/
|
||||
constructor(compareFunction) {
|
||||
this.compare = compareFunction || Comparator.defaultCompareFunction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default comparison function. It just assumes that "a" and "b" are strings or numbers.
|
||||
* @param {(string|number)} a
|
||||
* @param {(string|number)} b
|
||||
* @returns {number}
|
||||
*/
|
||||
static defaultCompareFunction(a, b) {
|
||||
if (a === b) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return a < b ? -1 : 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if two variables are equal.
|
||||
* @param {*} a
|
||||
* @param {*} b
|
||||
* @return {boolean}
|
||||
*/
|
||||
equal(a, b) {
|
||||
return this.compare(a, b) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if variable "a" is less than "b".
|
||||
* @param {*} a
|
||||
* @param {*} b
|
||||
* @return {boolean}
|
||||
*/
|
||||
lessThan(a, b) {
|
||||
return this.compare(a, b) < 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if variable "a" is greater than "b".
|
||||
* @param {*} a
|
||||
* @param {*} b
|
||||
* @return {boolean}
|
||||
*/
|
||||
greaterThan(a, b) {
|
||||
return this.compare(a, b) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if variable "a" is less than or equal to "b".
|
||||
* @param {*} a
|
||||
* @param {*} b
|
||||
* @return {boolean}
|
||||
*/
|
||||
lessThanOrEqual(a, b) {
|
||||
return this.lessThan(a, b) || this.equal(a, b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if variable "a" is greater than or equal to "b".
|
||||
* @param {*} a
|
||||
* @param {*} b
|
||||
* @return {boolean}
|
||||
*/
|
||||
greaterThanOrEqual(a, b) {
|
||||
return this.greaterThan(a, b) || this.equal(a, b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverses the comparison order.
|
||||
*/
|
||||
reverse() {
|
||||
const compareOriginal = this.compare;
|
||||
this.compare = (a, b) => compareOriginal(b, a);
|
||||
}
|
||||
}
|
24
packages/core/src/algorithm/degree.ts
Normal file
24
packages/core/src/algorithm/degree.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { IGraph } from '../interface/graph';
|
||||
|
||||
const degree = (graph: IGraph) => {
|
||||
const degrees = {};
|
||||
|
||||
graph.getNodes().forEach((node) => {
|
||||
degrees[node.getID()] = {
|
||||
degree: 0,
|
||||
inDegree: 0,
|
||||
outDegree: 0,
|
||||
};
|
||||
});
|
||||
|
||||
graph.getEdges().forEach((edge) => {
|
||||
degrees[edge.getSource().getID()].degree++;
|
||||
degrees[edge.getSource().getID()].outDegree++;
|
||||
degrees[edge.getTarget().getID()].degree++;
|
||||
degrees[edge.getTarget().getID()].inDegree++;
|
||||
});
|
||||
|
||||
return degrees;
|
||||
};
|
||||
|
||||
export default degree;
|
38
packages/core/src/algorithm/floydWarshall.ts
Normal file
38
packages/core/src/algorithm/floydWarshall.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { IGraph } from '../interface/graph';
|
||||
import adjMatrix from './adjacent-matrix';
|
||||
import { Matrix } from '../types';
|
||||
|
||||
const floydWarshall = (graph: IGraph, directed?: boolean) => {
|
||||
let adjacentMatrix = graph.get('adjMatrix');
|
||||
if (!adjacentMatrix) {
|
||||
adjacentMatrix = adjMatrix(graph, directed);
|
||||
}
|
||||
|
||||
const dist: Matrix[] = [];
|
||||
const size = adjacentMatrix.length;
|
||||
for (let i = 0; i < size; i += 1) {
|
||||
dist[i] = [];
|
||||
for (let j = 0; j < size; j += 1) {
|
||||
if (i === j) {
|
||||
dist[i][j] = 0;
|
||||
} else if (adjacentMatrix[i][j] === 0 || !adjacentMatrix[i][j]) {
|
||||
dist[i][j] = Infinity;
|
||||
} else {
|
||||
dist[i][j] = adjacentMatrix[i][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
// floyd
|
||||
for (let k = 0; k < size; k += 1) {
|
||||
for (let i = 0; i < size; i += 1) {
|
||||
for (let j = 0; j < size; j += 1) {
|
||||
if (dist[i][j] > dist[i][k] + dist[k][j]) {
|
||||
dist[i][j] = dist[i][k] + dist[k][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return dist;
|
||||
};
|
||||
|
||||
export default floydWarshall;
|
3
packages/core/src/algorithm/index.ts
Normal file
3
packages/core/src/algorithm/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { default as degree } from './degree';
|
||||
export { default as adjMatrix } from './adjacent-matrix';
|
||||
export { default as floydWarshall } from './floydWarshall';
|
239
packages/core/src/algorithm/structs/linked-list/index.ts
Normal file
239
packages/core/src/algorithm/structs/linked-list/index.ts
Normal file
@ -0,0 +1,239 @@
|
||||
import Comparator from '../../comparator';
|
||||
|
||||
/**
|
||||
* 链表中单个元素节点
|
||||
*/
|
||||
export class LinkedListNode {
|
||||
public value;
|
||||
|
||||
public next: LinkedListNode;
|
||||
|
||||
constructor(value, next: LinkedListNode = null) {
|
||||
this.value = value;
|
||||
this.next = next;
|
||||
}
|
||||
|
||||
toString(callback) {
|
||||
return callback ? callback(this.value) : `${this.value}`;
|
||||
}
|
||||
}
|
||||
|
||||
export default class LinkedList {
|
||||
public head: LinkedListNode;
|
||||
|
||||
public tail: LinkedListNode;
|
||||
|
||||
public compare: Comparator;
|
||||
|
||||
constructor(comparatorFunction = null) {
|
||||
this.head = null;
|
||||
this.tail = null;
|
||||
this.compare = new Comparator(comparatorFunction);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将指定元素添加到链表头部
|
||||
* @param value
|
||||
*/
|
||||
prepend(value) {
|
||||
// 在头部添加一个节点
|
||||
const newNode = new LinkedListNode(value, this.head);
|
||||
this.head = newNode;
|
||||
|
||||
if (!this.tail) {
|
||||
this.tail = newNode;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将指定元素添加到链表中
|
||||
* @param value
|
||||
*/
|
||||
append(value) {
|
||||
const newNode = new LinkedListNode(value);
|
||||
|
||||
// 如果不存在头节点,则将创建的新节点作为头节点
|
||||
if (!this.head) {
|
||||
this.head = newNode;
|
||||
this.tail = newNode;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
// 将新节点附加到链表末尾
|
||||
this.tail.next = newNode;
|
||||
this.tail = newNode;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指定元素
|
||||
* @param value 要删除的元素
|
||||
*/
|
||||
delete(value): LinkedListNode {
|
||||
if (!this.head) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let deleteNode = null;
|
||||
|
||||
// 如果删除的是头部元素,则将next作为头元素
|
||||
while (this.head && this.compare.equal(this.head.value, value)) {
|
||||
deleteNode = this.head;
|
||||
this.head = this.head.next;
|
||||
}
|
||||
|
||||
let currentNode = this.head;
|
||||
|
||||
if (currentNode !== null) {
|
||||
// 如果删除了节点以后,将next节点前移
|
||||
while (currentNode.next) {
|
||||
if (this.compare.equal(currentNode.next.value, value)) {
|
||||
deleteNode = currentNode.next;
|
||||
currentNode.next = currentNode.next.next;
|
||||
} else {
|
||||
currentNode = currentNode.next;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查尾部节点是否被删除
|
||||
if (this.compare.equal(this.tail.value, value)) {
|
||||
this.tail = currentNode;
|
||||
}
|
||||
|
||||
return deleteNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找指定的元素
|
||||
* @param param0
|
||||
*/
|
||||
find({ value = undefined, callback = undefined }): LinkedListNode {
|
||||
if (!this.head) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let currentNode = this.head;
|
||||
|
||||
while (currentNode) {
|
||||
// 如果指定了 callback,则按指定的 callback 查找
|
||||
if (callback && callback(currentNode.value)) {
|
||||
return currentNode;
|
||||
}
|
||||
|
||||
// 如果指定了 value,则按 value 查找
|
||||
if (value !== undefined && this.compare.equal(currentNode.value, value)) {
|
||||
return currentNode;
|
||||
}
|
||||
|
||||
currentNode = currentNode.next;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除尾部节点
|
||||
*/
|
||||
deleteTail() {
|
||||
const deletedTail = this.tail;
|
||||
|
||||
if (this.head === this.tail) {
|
||||
// 链表中只有一个元素
|
||||
this.head = null;
|
||||
this.tail = null;
|
||||
return deletedTail;
|
||||
}
|
||||
|
||||
let currentNode = this.head;
|
||||
while (currentNode.next) {
|
||||
if (!currentNode.next.next) {
|
||||
currentNode.next = null;
|
||||
} else {
|
||||
currentNode = currentNode.next;
|
||||
}
|
||||
}
|
||||
|
||||
this.tail = currentNode;
|
||||
|
||||
return deletedTail;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除头部节点
|
||||
*/
|
||||
deleteHead() {
|
||||
if (!this.head) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const deletedHead = this.head;
|
||||
|
||||
if (this.head.next) {
|
||||
this.head = this.head.next;
|
||||
} else {
|
||||
this.head = null;
|
||||
this.tail = null;
|
||||
}
|
||||
|
||||
return deletedHead;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将一组元素转成链表中的节点
|
||||
* @param values 链表中的元素
|
||||
*/
|
||||
fromArray(values) {
|
||||
values.forEach((value) => this.append(value));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将链表中的节点转成数组元素
|
||||
*/
|
||||
toArray() {
|
||||
const nodes = [];
|
||||
|
||||
let currentNode = this.head;
|
||||
|
||||
while (currentNode) {
|
||||
nodes.push(currentNode);
|
||||
currentNode = currentNode.next;
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 反转链表中的元素节点
|
||||
*/
|
||||
reverse() {
|
||||
let currentNode = this.head;
|
||||
let prevNode = null;
|
||||
let nextNode = null;
|
||||
while (currentNode) {
|
||||
// 存储下一个元素节点
|
||||
nextNode = currentNode.next;
|
||||
|
||||
// 更改当前节点的下一个节点,以便将它连接到上一个节点上
|
||||
currentNode.next = prevNode;
|
||||
|
||||
// 将 prevNode 和 currentNode 向前移动一步
|
||||
prevNode = currentNode;
|
||||
currentNode = nextNode;
|
||||
}
|
||||
|
||||
this.tail = this.head;
|
||||
this.head = prevNode;
|
||||
}
|
||||
|
||||
toString(callback = undefined) {
|
||||
return this.toArray()
|
||||
.map((node) => node.toString(callback))
|
||||
.toString();
|
||||
}
|
||||
}
|
65
packages/core/src/algorithm/structs/stack/index.ts
Normal file
65
packages/core/src/algorithm/structs/stack/index.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import LinkedList from '../linked-list';
|
||||
|
||||
export default class Stack {
|
||||
|
||||
private linkedList: LinkedList;
|
||||
|
||||
private maxStep: number;
|
||||
|
||||
constructor(maxStep: number = 10) {
|
||||
this.linkedList = new LinkedList();
|
||||
this.maxStep = maxStep;
|
||||
}
|
||||
|
||||
get length() {
|
||||
return this.linkedList.toArray().length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断栈是否为空,如果链表中没有头部元素,则栈为空
|
||||
*/
|
||||
isEmpty() {
|
||||
return !this.linkedList.head;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否到定义的栈的最大长度,如果达到最大长度后,不再允许入栈
|
||||
*/
|
||||
isMaxStack() {
|
||||
return this.toArray().length >= this.maxStep;
|
||||
}
|
||||
|
||||
/**
|
||||
* 访问顶端元素
|
||||
*/
|
||||
peek() {
|
||||
if (this.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 返回头部元素,不删除元素
|
||||
return this.linkedList.head.value;
|
||||
}
|
||||
|
||||
push(value) {
|
||||
this.linkedList.prepend(value);
|
||||
if (this.length > this.maxStep) {
|
||||
this.linkedList.deleteTail();
|
||||
}
|
||||
}
|
||||
|
||||
pop() {
|
||||
const removeHead = this.linkedList.deleteHead();
|
||||
return removeHead ? removeHead.value : null;
|
||||
}
|
||||
|
||||
toArray() {
|
||||
return this.linkedList.toArray().map((node) => node.value);
|
||||
}
|
||||
|
||||
clear() {
|
||||
while (!this.isEmpty()) {
|
||||
this.pop();
|
||||
}
|
||||
}
|
||||
}
|
51
packages/core/src/behavior/behavior.ts
Normal file
51
packages/core/src/behavior/behavior.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { clone, each, wrapBehavior } from '@antv/util';
|
||||
import { BehaviorOption } from '../types';
|
||||
import behaviorOption from './behaviorOption';
|
||||
|
||||
export default class Behavior {
|
||||
// 所有自定义的 Behavior 的实例
|
||||
private static types = {};
|
||||
|
||||
/**
|
||||
* 自定义 Behavior
|
||||
* @param type Behavior 名称
|
||||
* @param behavior Behavior 定义的方法集合
|
||||
*/
|
||||
public static registerBehavior(type: string, behavior: BehaviorOption) {
|
||||
if (!behavior) {
|
||||
throw new Error(`please specify handler for this behavior: ${type}`);
|
||||
}
|
||||
|
||||
const prototype = clone(behaviorOption);
|
||||
|
||||
Object.assign(prototype, behavior);
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
const base = function (cfg: object) {
|
||||
Object.assign(this, this.getDefaultCfg(), cfg);
|
||||
|
||||
const events = this.getEvents();
|
||||
this.events = null;
|
||||
|
||||
const eventsToBind = {};
|
||||
|
||||
if (events) {
|
||||
each(events, (handle, event) => {
|
||||
eventsToBind[event] = wrapBehavior(this, handle);
|
||||
});
|
||||
this.events = eventsToBind;
|
||||
}
|
||||
};
|
||||
|
||||
base.prototype = prototype;
|
||||
Behavior.types[type] = base;
|
||||
}
|
||||
|
||||
public static hasBehavior(type: string) {
|
||||
return !!Behavior.types[type];
|
||||
}
|
||||
|
||||
public static getBehavior(type: string) {
|
||||
return Behavior.types[type];
|
||||
}
|
||||
}
|
79
packages/core/src/behavior/behaviorOption.ts
Normal file
79
packages/core/src/behavior/behaviorOption.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import { each } from '@antv/util';
|
||||
import { IGraph } from '../interface/graph';
|
||||
import { G6Event } from '../types';
|
||||
|
||||
// 自定义 Behavior 时候共有的方法
|
||||
export default {
|
||||
getDefaultCfg() {
|
||||
return {};
|
||||
},
|
||||
|
||||
/**
|
||||
* register event handler, behavior will auto bind events
|
||||
* for example:
|
||||
* return {
|
||||
* click: 'onClick'
|
||||
* }
|
||||
*/
|
||||
getEvents() {
|
||||
return {};
|
||||
},
|
||||
|
||||
shouldBegin() {
|
||||
return true;
|
||||
},
|
||||
|
||||
shouldUpdate() {
|
||||
return true;
|
||||
},
|
||||
|
||||
shouldEnd() {
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* auto bind events when register behavior
|
||||
* @param graph Graph instance
|
||||
*/
|
||||
bind(graph: IGraph) {
|
||||
const { events } = this;
|
||||
this.graph = graph;
|
||||
if (
|
||||
this.type === 'drag-canvas' ||
|
||||
this.type === 'brush-select' ||
|
||||
this.type === 'lasso-select'
|
||||
) {
|
||||
graph.get('canvas').set('draggable', true);
|
||||
}
|
||||
each(events, (handler: () => void, event: G6Event) => {
|
||||
graph.on(event, handler);
|
||||
});
|
||||
// To avoid the tabs switching makes the keydown related behaviors disable
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
this.keydown = false;
|
||||
});
|
||||
},
|
||||
|
||||
unbind(graph: IGraph) {
|
||||
const { events } = this;
|
||||
if (
|
||||
this.type === 'drag-canvas' ||
|
||||
this.type === 'brush-select' ||
|
||||
this.type === 'lasso-select'
|
||||
) {
|
||||
graph.get('canvas').set('draggable', false);
|
||||
}
|
||||
each(events, (handler: () => void, event: G6Event) => {
|
||||
graph.off(event, handler);
|
||||
});
|
||||
},
|
||||
|
||||
get(val: string) {
|
||||
return (this as any)[val];
|
||||
},
|
||||
|
||||
set(key: string, val: any) {
|
||||
(this as any)[key] = val;
|
||||
return this;
|
||||
},
|
||||
};
|
3
packages/core/src/behavior/index.ts
Normal file
3
packages/core/src/behavior/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import Behavior from "./behavior";
|
||||
|
||||
export default Behavior;
|
288
packages/core/src/global.ts
Normal file
288
packages/core/src/global.ts
Normal file
@ -0,0 +1,288 @@
|
||||
const subjectColor = 'rgb(95, 149, 255)';
|
||||
const backColor = 'rgb(255, 255, 255)';
|
||||
const textColor = 'rgb(0, 0, 0)';
|
||||
|
||||
// const colorSet = {
|
||||
|
||||
// // for nodes
|
||||
// mainStroke: subjectColor,
|
||||
// mainFill: subjectColor01,
|
||||
|
||||
// activeStroke: subjectColor,
|
||||
// activeFill: subjectColor005,
|
||||
|
||||
// inactiveStroke: subjectColor04,
|
||||
// inactiveFill: subjectColor005,
|
||||
|
||||
// selectedStroke: subjectColor,
|
||||
// selectedFill: backColor,
|
||||
|
||||
// highlightStroke: deeperSubject,
|
||||
// highlightFill: subjectColor02,
|
||||
|
||||
// disableStroke: disableColor03,
|
||||
// disableFill: disableColor005,
|
||||
|
||||
// // for edges
|
||||
// edgeMainStroke: disableColor03,
|
||||
// edgeActiveStroke: subjectColor,
|
||||
// edgeInactiveStroke: disableColor02,
|
||||
// edgeSelectedStroke: subjectColor,
|
||||
// edgeHighlightStroke: subjectColor,
|
||||
// edgeDisableStroke: disableColor01,
|
||||
|
||||
// // for combos
|
||||
// comboMainStroke: disableColor03,
|
||||
// comboMainFill: disableColor002,
|
||||
|
||||
// comboActiveStroke: subjectColor,
|
||||
// comboActiveFill: subjectColor005,
|
||||
|
||||
// comboInactiveStroke: disableColor03,
|
||||
// comboInactiveFill: disableColor002,
|
||||
|
||||
// comboSelectedStroke: subjectColor,
|
||||
// comboSelectedFill: disableColor002,
|
||||
|
||||
// comboHighlightStroke: deeperSubject, // 'rgb(53, 119, 222)', // TODO: how to generate it ???
|
||||
// comboHighlightFill: disableColor002,
|
||||
|
||||
// comboDisableStroke: disableColor02,
|
||||
// comboDisableFill: disableColor005,
|
||||
// }
|
||||
|
||||
const colorSet = {
|
||||
// for nodes
|
||||
mainStroke: subjectColor,
|
||||
mainFill: backColor,
|
||||
|
||||
activeStroke: subjectColor,
|
||||
activeFill: backColor,
|
||||
|
||||
inactiveStroke: backColor,
|
||||
inactiveFill: backColor,
|
||||
|
||||
selectedStroke: subjectColor,
|
||||
selectedFill: backColor,
|
||||
|
||||
highlightStroke: backColor,
|
||||
highlightFill: backColor,
|
||||
|
||||
disableStroke: backColor,
|
||||
disableFill: backColor,
|
||||
|
||||
// for edges
|
||||
edgeMainStroke: backColor,
|
||||
edgeActiveStroke: subjectColor,
|
||||
edgeInactiveStroke: backColor,
|
||||
edgeSelectedStroke: subjectColor,
|
||||
edgeHighlightStroke: subjectColor,
|
||||
edgeDisableStroke: backColor,
|
||||
|
||||
// for combos
|
||||
comboMainStroke: backColor,
|
||||
comboMainFill: backColor,
|
||||
|
||||
comboActiveStroke: subjectColor,
|
||||
comboActiveFill: backColor,
|
||||
|
||||
comboInactiveStroke: backColor,
|
||||
comboInactiveFill: backColor,
|
||||
|
||||
comboSelectedStroke: subjectColor,
|
||||
comboSelectedFill: backColor,
|
||||
|
||||
comboHighlightStroke: backColor, // 'rgb(53, 119, 222)', // TODO: how to generate it ???
|
||||
comboHighlightFill: backColor,
|
||||
|
||||
comboDisableStroke: backColor,
|
||||
comboDisableFill: backColor,
|
||||
};
|
||||
|
||||
export default {
|
||||
version: '4.0.3',
|
||||
rootContainerClassName: 'root-container',
|
||||
nodeContainerClassName: 'node-container',
|
||||
edgeContainerClassName: 'edge-container',
|
||||
comboContainerClassName: 'combo-container',
|
||||
delegateContainerClassName: 'delegate-container',
|
||||
defaultLoopPosition: 'top',
|
||||
nodeLabel: {
|
||||
style: {
|
||||
fill: '#000',
|
||||
fontSize: 12,
|
||||
textAlign: 'center',
|
||||
textBaseline: 'middle',
|
||||
},
|
||||
offset: 4, // 节点的默认文本不居中时的偏移量
|
||||
},
|
||||
defaultNode: {
|
||||
type: 'circle',
|
||||
style: {
|
||||
lineWidth: 1,
|
||||
stroke: colorSet.mainStroke,
|
||||
fill: colorSet.mainFill,
|
||||
},
|
||||
size: 20,
|
||||
color: colorSet.mainStroke,
|
||||
linkPoints: {
|
||||
size: 8,
|
||||
lineWidth: 1,
|
||||
fill: colorSet.activeFill,
|
||||
stroke: colorSet.activeStroke,
|
||||
},
|
||||
},
|
||||
// 节点应用状态后的样式,默认仅提供 active、selected、highlight、inactive、disable,用户可以自己扩展
|
||||
nodeStateStyles: {
|
||||
active: {
|
||||
fill: colorSet.activeFill,
|
||||
stroke: colorSet.activeStroke,
|
||||
lineWidth: 2,
|
||||
shadowColor: colorSet.mainStroke,
|
||||
shadowBlur: 10,
|
||||
},
|
||||
selected: {
|
||||
fill: colorSet.selectedFill,
|
||||
stroke: colorSet.selectedStroke,
|
||||
lineWidth: 4,
|
||||
shadowColor: colorSet.selectedStroke,
|
||||
shadowBlur: 10,
|
||||
'text-shape': {
|
||||
fontWeight: 500,
|
||||
},
|
||||
},
|
||||
highlight: {
|
||||
fill: colorSet.highlightFill,
|
||||
stroke: colorSet.highlightStroke,
|
||||
lineWidth: 2,
|
||||
'text-shape': {
|
||||
fontWeight: 500,
|
||||
},
|
||||
},
|
||||
inactive: {
|
||||
fill: colorSet.inactiveFill,
|
||||
stroke: colorSet.inactiveStroke,
|
||||
lineWidth: 1,
|
||||
},
|
||||
disable: {
|
||||
fill: colorSet.disableFill,
|
||||
stroke: colorSet.disableStroke,
|
||||
lineWidth: 1,
|
||||
},
|
||||
},
|
||||
edgeLabel: {
|
||||
style: {
|
||||
fill: textColor,
|
||||
textAlign: 'center',
|
||||
textBaseline: 'middle',
|
||||
fontSize: 12,
|
||||
},
|
||||
},
|
||||
defaultEdge: {
|
||||
type: 'line',
|
||||
size: 1,
|
||||
style: {
|
||||
stroke: colorSet.edgeMainStroke,
|
||||
lineAppendWidth: 2,
|
||||
},
|
||||
color: colorSet.edgeMainStroke,
|
||||
},
|
||||
// 边应用状态后的样式,默认仅提供 active、selected、highlight、inactive、disable,用户可以自己扩展
|
||||
edgeStateStyles: {
|
||||
active: {
|
||||
stroke: colorSet.edgeActiveStroke,
|
||||
lineWidth: 1,
|
||||
},
|
||||
selected: {
|
||||
stroke: colorSet.edgeSelectedStroke,
|
||||
lineWidth: 2,
|
||||
shadowColor: colorSet.edgeSelectedStroke,
|
||||
shadowBlur: 10,
|
||||
'text-shape': {
|
||||
fontWeight: 500,
|
||||
},
|
||||
},
|
||||
highlight: {
|
||||
stroke: colorSet.edgeHighlightStroke,
|
||||
lineWidth: 2,
|
||||
'text-shape': {
|
||||
fontWeight: 500,
|
||||
},
|
||||
},
|
||||
inactive: {
|
||||
stroke: colorSet.edgeInactiveStroke,
|
||||
lineWidth: 1,
|
||||
},
|
||||
disable: {
|
||||
stroke: colorSet.edgeDisableStroke,
|
||||
lineWidth: 1,
|
||||
},
|
||||
},
|
||||
comboLabel: {
|
||||
style: {
|
||||
fill: textColor,
|
||||
// textAlign: 'center',
|
||||
textBaseline: 'middle',
|
||||
fontSize: 12,
|
||||
},
|
||||
refY: 10, // Combo 的默认文本不居中时的偏移量
|
||||
refX: 10, // Combo 的默认文本不居中时的偏移量
|
||||
},
|
||||
defaultCombo: {
|
||||
type: 'circle',
|
||||
style: {
|
||||
fill: colorSet.comboMainFill,
|
||||
lineWidth: 1,
|
||||
stroke: colorSet.comboMainStroke,
|
||||
r: 5,
|
||||
width: 20,
|
||||
height: 10,
|
||||
},
|
||||
size: [20, 5],
|
||||
color: colorSet.comboMainStroke,
|
||||
padding: [25, 20, 15, 20],
|
||||
},
|
||||
// combo 应用状态后的样式,默认仅提供 active、selected、highlight、inactive、disable,用户可以自己扩展
|
||||
comboStateStyles: {
|
||||
active: {
|
||||
stroke: colorSet.comboActiveStroke,
|
||||
lineWidth: 1,
|
||||
fill: colorSet.comboActiveFill,
|
||||
},
|
||||
selected: {
|
||||
stroke: colorSet.comboSelectedStroke,
|
||||
lineWidth: 2,
|
||||
fill: colorSet.comboSelectedFill,
|
||||
shadowColor: colorSet.comboSelectedStroke,
|
||||
shadowBlur: 10,
|
||||
'text-shape': {
|
||||
fontWeight: 500,
|
||||
},
|
||||
},
|
||||
highlight: {
|
||||
stroke: colorSet.comboHighlightStroke,
|
||||
lineWidth: 2,
|
||||
fill: colorSet.comboHighlightFill,
|
||||
'text-shape': {
|
||||
fontWeight: 500,
|
||||
},
|
||||
},
|
||||
inactive: {
|
||||
stroke: colorSet.comboInactiveStroke,
|
||||
fill: colorSet.comboInactiveFill,
|
||||
lineWidth: 1,
|
||||
},
|
||||
disable: {
|
||||
stroke: colorSet.comboDisableStroke,
|
||||
fill: colorSet.comboDisableFill,
|
||||
lineWidth: 1,
|
||||
},
|
||||
},
|
||||
delegateStyle: {
|
||||
fill: '#F3F9FF',
|
||||
fillOpacity: 0.5,
|
||||
stroke: '#1890FF',
|
||||
strokeOpacity: 0.9,
|
||||
lineDash: [5, 5],
|
||||
},
|
||||
};
|
264
packages/core/src/graph/controller/event.ts
Normal file
264
packages/core/src/graph/controller/event.ts
Normal file
@ -0,0 +1,264 @@
|
||||
import { addEventListener } from '@antv/dom-util';
|
||||
import Canvas from '@antv/g-base/lib/abstract/canvas';
|
||||
import { Group, Shape } from '@antv/g-canvas';
|
||||
import { each, isNil, wrapBehavior } from '@antv/util';
|
||||
import Graph from '../graph';
|
||||
import { IG6GraphEvent, Matrix, Item } from '../../types';
|
||||
import { cloneEvent, isViewportChanged } from '../../util/base';
|
||||
|
||||
type ShapeBase = Shape.Base;
|
||||
type Fun = () => void;
|
||||
|
||||
// const EVENTS = [
|
||||
// 'click',
|
||||
// 'mousedown',
|
||||
// 'mouseup',
|
||||
// 'dblclick',
|
||||
// 'contextmenu',
|
||||
// 'mouseenter',
|
||||
// 'mouseout',
|
||||
// 'mouseover',
|
||||
// 'mousemove',
|
||||
// 'mouseleave',
|
||||
// 'dragstart',
|
||||
// 'dragend',
|
||||
// 'drag',
|
||||
// 'dragenter',
|
||||
// 'dragleave',
|
||||
// 'dragover',
|
||||
// 'dragout', // no this event
|
||||
// 'drop',
|
||||
// 'touchstart',
|
||||
// 'touchmove',
|
||||
// 'touchend',
|
||||
// ];
|
||||
|
||||
export default class EventController {
|
||||
private graph: Graph;
|
||||
|
||||
private extendEvents: any[];
|
||||
|
||||
private canvasHandler!: Fun;
|
||||
|
||||
private dragging: boolean;
|
||||
|
||||
private preItem: Item | null = null;
|
||||
|
||||
public destroyed: boolean;
|
||||
|
||||
constructor(graph: Graph) {
|
||||
this.graph = graph;
|
||||
this.extendEvents = [];
|
||||
this.dragging = false;
|
||||
this.destroyed = false;
|
||||
this.initEvents();
|
||||
}
|
||||
|
||||
// 初始化 G6 中的事件
|
||||
private initEvents() {
|
||||
const { graph, extendEvents } = this;
|
||||
|
||||
const canvas: Canvas = graph.get('canvas');
|
||||
// canvas.set('draggable', true);
|
||||
const el = canvas.get('el');
|
||||
|
||||
const canvasHandler: Fun = wrapBehavior(this, 'onCanvasEvents') as Fun;
|
||||
const originHandler = wrapBehavior(this, 'onExtendEvents');
|
||||
const wheelHandler = wrapBehavior(this, 'onWheelEvent');
|
||||
|
||||
// each(EVENTS, event => {
|
||||
// canvas.on(event, canvasHandler);
|
||||
// });
|
||||
|
||||
canvas.on('*', canvasHandler);
|
||||
|
||||
this.canvasHandler = canvasHandler;
|
||||
extendEvents.push(addEventListener(el, 'DOMMouseScroll', wheelHandler));
|
||||
extendEvents.push(addEventListener(el, 'mousewheel', wheelHandler));
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
extendEvents.push(addEventListener(window as any, 'keydown', originHandler));
|
||||
extendEvents.push(addEventListener(window as any, 'keyup', originHandler));
|
||||
extendEvents.push(addEventListener(window as any, 'focus', originHandler));
|
||||
}
|
||||
}
|
||||
|
||||
// 获取 shape 的 item 对象
|
||||
private static getItemRoot<T extends ShapeBase>(shape: any): T {
|
||||
while (shape && !shape.get('item')) {
|
||||
shape = shape.get('parent');
|
||||
}
|
||||
return shape;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 canvas 事件
|
||||
* @param evt 事件句柄
|
||||
*/
|
||||
protected onCanvasEvents(evt: IG6GraphEvent) {
|
||||
const { graph } = this;
|
||||
|
||||
const canvas = graph.get('canvas');
|
||||
const { target } = evt;
|
||||
const eventType = evt.type;
|
||||
/**
|
||||
* (clientX, clientY): 相对于页面的坐标;
|
||||
* (canvasX, canvasY): 相对于 <canvas> 左上角的坐标;
|
||||
* (x, y): 相对于整个画布的坐标, 与 model 的 x, y 是同一维度的。
|
||||
*/
|
||||
evt.canvasX = evt.x;
|
||||
evt.canvasY = evt.y;
|
||||
let point = { x: evt.canvasX, y: evt.canvasY };
|
||||
|
||||
const group: Group = graph.get('group');
|
||||
let matrix: Matrix = group.getMatrix();
|
||||
|
||||
if (!matrix) {
|
||||
matrix = [1, 0, 0, 0, 1, 0, 0, 0, 1];
|
||||
}
|
||||
|
||||
if (isViewportChanged(matrix)) {
|
||||
point = graph.getPointByClient(evt.clientX, evt.clientY);
|
||||
}
|
||||
|
||||
evt.x = point.x;
|
||||
evt.y = point.y;
|
||||
|
||||
evt.currentTarget = graph;
|
||||
|
||||
if (target === canvas) {
|
||||
if (eventType === 'mousemove') {
|
||||
this.handleMouseMove(evt, 'canvas');
|
||||
}
|
||||
evt.target = canvas;
|
||||
evt.item = null;
|
||||
|
||||
graph.emit(eventType, evt);
|
||||
graph.emit(`canvas:${eventType}`, evt);
|
||||
return;
|
||||
}
|
||||
|
||||
const itemShape: ShapeBase = EventController.getItemRoot(target);
|
||||
if (!itemShape) {
|
||||
graph.emit(eventType, evt);
|
||||
return;
|
||||
}
|
||||
|
||||
const item = itemShape.get('item');
|
||||
if (item.destroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const type = item.getType();
|
||||
|
||||
// 事件target是触发事件的Shape实例,item是触发事件的item实例
|
||||
evt.target = target;
|
||||
evt.item = item;
|
||||
if (evt.canvasX === evt.x && evt.canvasY === evt.y) {
|
||||
const canvasPoint = graph.getCanvasByPoint(evt.x, evt.y);
|
||||
evt.canvasX = canvasPoint.x;
|
||||
evt.canvasY = canvasPoint.y;
|
||||
}
|
||||
|
||||
// emit('click', evt);
|
||||
graph.emit(eventType, evt);
|
||||
|
||||
if (evt.name && !evt.name.includes(':')) graph.emit(`${type}:${eventType}`, evt);
|
||||
// emit('node:click', evt)
|
||||
else graph.emit(evt.name, evt); // emit('text-shape:click', evt)
|
||||
|
||||
if (eventType === 'dragstart') {
|
||||
this.dragging = true;
|
||||
}
|
||||
if (eventType === 'dragend') {
|
||||
this.dragging = false;
|
||||
}
|
||||
if (eventType === 'mousemove') {
|
||||
this.handleMouseMove(evt, type);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理扩展事件
|
||||
* @param evt 事件句柄
|
||||
*/
|
||||
protected onExtendEvents(evt: IG6GraphEvent) {
|
||||
this.graph.emit(evt.type, evt);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理滚轮事件
|
||||
* @param evt 事件句柄
|
||||
*/
|
||||
protected onWheelEvent(evt: IG6GraphEvent) {
|
||||
if (isNil(evt.wheelDelta)) {
|
||||
evt.wheelDelta = -evt.detail;
|
||||
}
|
||||
this.graph.emit('wheel', evt);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理鼠标移动的事件
|
||||
* @param evt 事件句柄
|
||||
* @param type item 类型
|
||||
*/
|
||||
private handleMouseMove(evt: IG6GraphEvent, type: string) {
|
||||
const { graph, preItem } = this;
|
||||
const canvas: Canvas = graph.get('canvas');
|
||||
const item = (evt.target as any) === canvas ? null : evt.item;
|
||||
|
||||
evt = cloneEvent(evt) as IG6GraphEvent;
|
||||
|
||||
// 从前一个item直接移动到当前item,触发前一个item的leave事件
|
||||
if (preItem && preItem !== item && !preItem.destroyed) {
|
||||
evt.item = preItem;
|
||||
this.emitCustomEvent(preItem.getType(), 'mouseleave', evt);
|
||||
if (this.dragging) {
|
||||
this.emitCustomEvent(preItem.getType(), 'dragleave', evt);
|
||||
}
|
||||
}
|
||||
|
||||
// 从一个item或canvas移动到当前item,触发当前item的enter事件
|
||||
if (item && preItem !== item) {
|
||||
evt.item = item;
|
||||
this.emitCustomEvent(type, 'mouseenter', evt);
|
||||
if (this.dragging) {
|
||||
this.emitCustomEvent(type, 'dragenter', evt);
|
||||
}
|
||||
}
|
||||
|
||||
this.preItem = item;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 graph 上面 emit 事件
|
||||
* @param itemType item 类型
|
||||
* @param eventType 事件类型
|
||||
* @param evt 事件句柄
|
||||
*/
|
||||
private emitCustomEvent(itemType: string, eventType: string, evt: IG6GraphEvent) {
|
||||
evt.type = eventType;
|
||||
this.graph.emit(`${itemType}:${eventType}`, evt);
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
const { graph, canvasHandler, extendEvents } = this;
|
||||
const canvas: Canvas = graph.get('canvas');
|
||||
|
||||
// each(EVENTS, event => {
|
||||
// canvas.off(event, canvasHandler);
|
||||
// });
|
||||
|
||||
canvas.off('*', canvasHandler);
|
||||
|
||||
each(extendEvents, (event) => {
|
||||
event.remove();
|
||||
});
|
||||
|
||||
this.dragging = false;
|
||||
this.preItem = null;
|
||||
this.extendEvents.length = 0;
|
||||
(this.canvasHandler as Fun | null) = null;
|
||||
this.destroyed = true;
|
||||
}
|
||||
}
|
6
packages/core/src/graph/controller/index.ts
Normal file
6
packages/core/src/graph/controller/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export { default as ModeController } from './mode';
|
||||
export { default as ViewController } from './view';
|
||||
export { default as EventController } from './event';
|
||||
export { default as ItemController } from './item';
|
||||
export { default as LayoutController } from './layout';
|
||||
export { default as StateController } from './state';
|
624
packages/core/src/graph/controller/item.ts
Normal file
624
packages/core/src/graph/controller/item.ts
Normal file
@ -0,0 +1,624 @@
|
||||
import { Group } from '@antv/g-canvas';
|
||||
import { clone, deepMix, each, isArray, isObject, isString, upperFirst } from '@antv/util';
|
||||
import Edge from '../../item/edge';
|
||||
import Node from '../../item/node';
|
||||
import Combo from '../../item/combo';
|
||||
import {
|
||||
EdgeConfig,
|
||||
Item,
|
||||
ITEM_TYPE,
|
||||
ModelConfig,
|
||||
NodeConfig,
|
||||
NodeMap,
|
||||
ComboTree,
|
||||
ComboConfig,
|
||||
} from '../../types';
|
||||
import Graph from '../graph';
|
||||
|
||||
import { IEdge, INode, ICombo } from '../../interface/item';
|
||||
import { traverseTreeUp, traverseTree, getComboBBox } from '../../util/graphic';
|
||||
|
||||
const NODE = 'node';
|
||||
const EDGE = 'edge';
|
||||
const VEDGE = 'vedge';
|
||||
const COMBO = 'combo';
|
||||
const CFG_PREFIX = 'default';
|
||||
const MAPPER_SUFFIX = 'Mapper';
|
||||
const STATE_SUFFIX = 'stateStyles';
|
||||
const { hasOwnProperty } = Object;
|
||||
|
||||
type Id = string | Item | undefined;
|
||||
|
||||
export default class ItemController {
|
||||
private graph: Graph;
|
||||
|
||||
public destroyed: boolean;
|
||||
|
||||
constructor(graph: Graph) {
|
||||
this.graph = graph;
|
||||
this.destroyed = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加 Item 实例
|
||||
*
|
||||
* @param {ITEM_TYPE} type 实例类型,node 或 edge
|
||||
* @param {(NodeConfig & EdgeConfig)} model 数据模型
|
||||
* @returns {(Item)}
|
||||
* @memberof ItemController
|
||||
*/
|
||||
public addItem<T extends Item>(type: ITEM_TYPE, model: ModelConfig) {
|
||||
const { graph } = this;
|
||||
const vType = type === VEDGE ? EDGE : type;
|
||||
const parent: Group = graph.get(`${vType}Group`) || graph.get('group');
|
||||
const upperType = upperFirst(vType);
|
||||
|
||||
let item: Item | null = null;
|
||||
// 获取 this.get('styles') 中的值
|
||||
let styles = graph.get(vType + upperFirst(STATE_SUFFIX)) || {};
|
||||
const defaultModel = graph.get(CFG_PREFIX + upperType);
|
||||
|
||||
if (model[STATE_SUFFIX]) {
|
||||
// 设置 this.get('styles') 中的值
|
||||
styles = model[STATE_SUFFIX];
|
||||
}
|
||||
|
||||
const mapper = graph.get(vType + MAPPER_SUFFIX);
|
||||
if (mapper) {
|
||||
const mappedModel = mapper(model);
|
||||
if (mappedModel[STATE_SUFFIX]) {
|
||||
// 设置 this.get('styles') 中的值
|
||||
styles = mappedModel[STATE_SUFFIX];
|
||||
delete mappedModel[STATE_SUFFIX];
|
||||
}
|
||||
|
||||
// 如果配置了 defaultEdge 或 defaultNode,则将默认配置的数据也合并进去
|
||||
model = deepMix({}, defaultModel, model, mappedModel);
|
||||
} else if (defaultModel) {
|
||||
// 很多布局会直接修改原数据模型,所以不能用 merge 的形式,逐个写入原 model 中
|
||||
each(defaultModel, (val, cfg) => {
|
||||
if (!hasOwnProperty.call(model, cfg)) {
|
||||
if (isObject(val)) {
|
||||
model[cfg] = clone(val);
|
||||
} else {
|
||||
model[cfg] = defaultModel[cfg];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
graph.emit('beforeadditem', { type, model });
|
||||
|
||||
if (type === EDGE || type === VEDGE) {
|
||||
let source: Id;
|
||||
let target: Id;
|
||||
source = (model as EdgeConfig).source; // eslint-disable-line prefer-destructuring
|
||||
target = (model as EdgeConfig).target; // eslint-disable-line prefer-destructuring
|
||||
|
||||
if (source && isString(source)) {
|
||||
source = graph.findById(source);
|
||||
}
|
||||
if (target && isString(target)) {
|
||||
target = graph.findById(target);
|
||||
}
|
||||
|
||||
if (!source || !target) {
|
||||
console.warn(`The source or target node of edge ${model.id} does not exist!`);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((source as Item).getType && (source as Item).getType() === 'combo') {
|
||||
model.isComboEdge = true;
|
||||
graph.updateCombo(source as ICombo);
|
||||
}
|
||||
if ((target as Item).getType && (target as Item).getType() === 'combo') {
|
||||
model.isComboEdge = true;
|
||||
graph.updateCombo(target as ICombo);
|
||||
}
|
||||
|
||||
item = new Edge({
|
||||
model,
|
||||
source,
|
||||
target,
|
||||
styles,
|
||||
linkCenter: graph.get('linkCenter'),
|
||||
group: parent.addGroup(),
|
||||
});
|
||||
} else if (type === NODE) {
|
||||
item = new Node({
|
||||
model,
|
||||
styles,
|
||||
group: parent.addGroup(),
|
||||
});
|
||||
} else if (type === COMBO) {
|
||||
const children: ComboTree[] = (model as ComboConfig).children;
|
||||
|
||||
const comboBBox = getComboBBox(children, graph);
|
||||
model.x = comboBBox.x || model.x || Math.random() * 100;
|
||||
model.y = comboBBox.y || model.y || Math.random() * 100;
|
||||
|
||||
const comboGroup = parent.addGroup();
|
||||
comboGroup.setZIndex((model as ComboConfig).depth as number);
|
||||
item = new Combo({
|
||||
model,
|
||||
styles,
|
||||
bbox: comboBBox,
|
||||
group: comboGroup,
|
||||
});
|
||||
|
||||
const comboModel = item.getModel();
|
||||
|
||||
(children || []).forEach((child) => {
|
||||
const childItem = graph.findById(child.id) as ICombo | INode;
|
||||
(item as ICombo).addChild(childItem);
|
||||
child.depth = (comboModel.depth as number) + 2;
|
||||
});
|
||||
|
||||
// collapse the combo if the collapsed is true in the model
|
||||
if (model.collapsed) {
|
||||
setTimeout(() => {
|
||||
graph.collapseCombo(item as ICombo);
|
||||
this.updateCombo(item as ICombo, []);
|
||||
}, 250);
|
||||
}
|
||||
}
|
||||
|
||||
if (item) {
|
||||
graph.get(`${type}s`).push(item);
|
||||
graph.get('itemMap')[item.get('id')] = item;
|
||||
graph.emit('afteradditem', { item, model });
|
||||
// eslint-disable-next-line consistent-return
|
||||
return item as T;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新节点或边
|
||||
*
|
||||
* @param {Item} item ID 或 实例
|
||||
* @param {(EdgeConfig | Partial<NodeConfig>)} cfg 数据模型
|
||||
* @returns
|
||||
* @memberof ItemController
|
||||
*/
|
||||
public updateItem(item: Item | string, cfg: EdgeConfig | Partial<NodeConfig>) {
|
||||
const { graph } = this;
|
||||
|
||||
if (isString(item)) {
|
||||
item = graph.findById(item) as Item;
|
||||
}
|
||||
|
||||
if (!item || item.destroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新的 item 的类型
|
||||
let type = '';
|
||||
if (item.getType) type = item.getType();
|
||||
|
||||
const mapper = graph.get(type + MAPPER_SUFFIX);
|
||||
const model = item.getModel();
|
||||
|
||||
const isOnlyMove = item.isOnlyMove(cfg);
|
||||
|
||||
if (mapper) {
|
||||
const result: ModelConfig = deepMix({}, model, cfg);
|
||||
const mappedModel: ModelConfig = mapper(result);
|
||||
// 将 update 时候用户传入的参数与mapperModel做deepMix,以便复用之前设置的参数值
|
||||
const newModel: ModelConfig = deepMix({}, model, mappedModel, cfg);
|
||||
|
||||
if (mappedModel[STATE_SUFFIX]) {
|
||||
item.set('styles', newModel[STATE_SUFFIX]);
|
||||
delete newModel[STATE_SUFFIX];
|
||||
}
|
||||
|
||||
each(newModel, (val, key) => {
|
||||
cfg[key] = val;
|
||||
});
|
||||
} else {
|
||||
// merge update传进来的对象参数,model中没有的数据不做处理,对象和字符串值也不做处理,直接替换原来的
|
||||
each(cfg, (val, key) => {
|
||||
if (model[key]) {
|
||||
if (isObject(val) && !isArray(val)) {
|
||||
cfg[key] = { ...(model[key] as Object), ...(cfg[key] as Object) };
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// emit beforeupdateitem 事件
|
||||
graph.emit('beforeupdateitem', { item, cfg });
|
||||
|
||||
if (type === EDGE) {
|
||||
// 若是边要更新source || target, 为了不影响示例内部model,并且重新计算startPoint和endPoint,手动设置
|
||||
if (cfg.source) {
|
||||
let source: INode = cfg.source as INode;
|
||||
if (isString(source)) {
|
||||
source = graph.findById(source) as INode;
|
||||
}
|
||||
(item as IEdge).setSource(source);
|
||||
}
|
||||
if (cfg.target) {
|
||||
let target: INode = cfg.target as INode;
|
||||
if (isString(target)) {
|
||||
target = graph.findById(target) as INode;
|
||||
}
|
||||
(item as IEdge).setTarget(target);
|
||||
}
|
||||
item.update(cfg);
|
||||
}
|
||||
|
||||
// item.update(cfg);
|
||||
|
||||
if (type === NODE || type === COMBO) {
|
||||
item.update(cfg, isOnlyMove);
|
||||
const edges: IEdge[] = (item as INode).getEdges();
|
||||
each(edges, (edge: IEdge) => {
|
||||
edge.refresh();
|
||||
});
|
||||
}
|
||||
graph.emit('afterupdateitem', { item, cfg });
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 combo 的子元素更新 combo 的位置及大小
|
||||
*
|
||||
* @param {ICombo} combo ID 或 实例
|
||||
* @returns
|
||||
* @memberof ItemController
|
||||
*/
|
||||
public updateCombo(combo: ICombo | string, children: ComboTree[]) {
|
||||
const { graph } = this;
|
||||
|
||||
if (isString(combo)) {
|
||||
combo = graph.findById(combo) as ICombo;
|
||||
}
|
||||
|
||||
if (!combo || combo.destroyed) {
|
||||
return;
|
||||
}
|
||||
const comboBBox = getComboBBox(children, graph);
|
||||
|
||||
combo.set('bbox', comboBBox);
|
||||
combo.update({
|
||||
x: comboBBox.x,
|
||||
y: comboBBox.y,
|
||||
});
|
||||
const combEdges = combo.getEdges() || [];
|
||||
const length = combEdges.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
const edge = combEdges[i];
|
||||
edge?.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 收起 combo,隐藏相关元素
|
||||
*/
|
||||
public collapseCombo(combo: ICombo | string) {
|
||||
const graph = this.graph;
|
||||
if (isString(combo)) {
|
||||
combo = graph.findById(combo) as ICombo;
|
||||
}
|
||||
const children = (combo as ICombo).getChildren();
|
||||
children.nodes.forEach((node) => {
|
||||
graph.hideItem(node);
|
||||
});
|
||||
children.combos.forEach((c) => {
|
||||
graph.hideItem(c);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 展开 combo,相关元素出现
|
||||
* 若子 combo 原先是收起状态,则保持它的收起状态
|
||||
*/
|
||||
public expandCombo(combo: ICombo | string) {
|
||||
const graph = this.graph;
|
||||
if (isString(combo)) {
|
||||
combo = graph.findById(combo) as ICombo;
|
||||
}
|
||||
const children = (combo as ICombo).getChildren();
|
||||
children.nodes.forEach((node) => {
|
||||
graph.showItem(node);
|
||||
});
|
||||
children.combos.forEach((c) => {
|
||||
if (c.getModel().collapsed) {
|
||||
c.show();
|
||||
} else {
|
||||
graph.showItem(c);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指定的节点或边
|
||||
*
|
||||
* @param {Item} item item ID 或实例
|
||||
* @returns {void}
|
||||
* @memberof ItemController
|
||||
*/
|
||||
public removeItem(item: Item | string): void {
|
||||
const { graph } = this;
|
||||
if (isString(item)) {
|
||||
item = graph.findById(item);
|
||||
}
|
||||
|
||||
if (!item || item.destroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const itemModel = clone(item.getModel());
|
||||
graph.emit('beforeremoveitem', { item: itemModel });
|
||||
|
||||
let type = '';
|
||||
if (item.getType) type = item.getType();
|
||||
const items = graph.get(`${type}s`);
|
||||
const index = items.indexOf(item);
|
||||
if (index > -1) items.splice(index, 1);
|
||||
if (type === EDGE) {
|
||||
const vitems = graph.get(`v${type}s`);
|
||||
const vindex = vitems.indexOf(item);
|
||||
if (vindex > -1) vitems.splice(vindex, 1);
|
||||
}
|
||||
|
||||
const itemId: string = item.get('id');
|
||||
const itemMap: NodeMap = graph.get('itemMap');
|
||||
delete itemMap[itemId];
|
||||
|
||||
const comboTrees = graph.get('comboTrees');
|
||||
const id = item.get('id');
|
||||
if (type === NODE) {
|
||||
const comboId = item.getModel().comboId as string;
|
||||
if (comboTrees) {
|
||||
let brothers = comboTrees;
|
||||
let found = false; // the flag to terminate the forEach circulation
|
||||
// remove the node from the children array of its parent fromt he tree
|
||||
comboTrees.forEach((ctree) => {
|
||||
if (found) return;
|
||||
traverseTree<ComboTree>(ctree, (combo) => {
|
||||
if (combo.id === id && brothers) {
|
||||
const bidx = brothers.indexOf(combo);
|
||||
brothers.splice(bidx, 1);
|
||||
found = true;
|
||||
return false; // terminate the traverse
|
||||
}
|
||||
brothers = combo.children;
|
||||
return true;
|
||||
});
|
||||
});
|
||||
}
|
||||
// 若移除的是节点,需要将与之相连的边一同删除
|
||||
const edges = (item as INode).getEdges();
|
||||
for (let i = edges.length - 1; i >= 0; i--) {
|
||||
graph.removeItem(edges[i], false);
|
||||
}
|
||||
if (comboId) graph.updateCombo(comboId);
|
||||
} else if (type === COMBO) {
|
||||
const parentId = item.getModel().parentId as string;
|
||||
let comboInTree;
|
||||
// find the subtree rooted at the item to be removed
|
||||
let found = false; // the flag to terminate the forEach circulation
|
||||
(comboTrees || []).forEach((ctree) => {
|
||||
if (found) return;
|
||||
traverseTree<ComboTree>(ctree, (combo) => {
|
||||
if (combo.id === id) {
|
||||
comboInTree = combo;
|
||||
found = true;
|
||||
return false; // terminate the traverse
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
comboInTree.removed = true;
|
||||
if (comboInTree && comboInTree.children) {
|
||||
comboInTree.children.forEach((child) => {
|
||||
this.removeItem(child.id);
|
||||
});
|
||||
}
|
||||
// 若移除的是 combo,需要将与之相连的边一同删除
|
||||
const edges = (item as ICombo).getEdges();
|
||||
for (let i = edges.length; i >= 0; i--) {
|
||||
graph.removeItem(edges[i], false);
|
||||
}
|
||||
if (parentId) graph.updateCombo(parentId);
|
||||
}
|
||||
|
||||
item.destroy();
|
||||
graph.emit('afterremoveitem', { item: itemModel });
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 item 状态
|
||||
*
|
||||
* @param {Item} item Item 实例
|
||||
* @param {string} state 状态名称
|
||||
* @param {boolean} value 是否启用状态或状态值
|
||||
* @returns {void}
|
||||
* @memberof ItemController
|
||||
*/
|
||||
public setItemState(item: Item, state: string, value: string | boolean): void {
|
||||
const { graph } = this;
|
||||
|
||||
let stateName = state;
|
||||
if (isString(value)) {
|
||||
stateName = `${state}:${value}`;
|
||||
}
|
||||
|
||||
// 已经存在要设置的 state,或不存在 state 的样式为 undefined
|
||||
if (item.hasState(stateName) === value || (isString(value) && item.hasState(stateName))) {
|
||||
return;
|
||||
}
|
||||
|
||||
graph.emit('beforeitemstatechange', { item, state: stateName, enabled: value });
|
||||
|
||||
item.setState(state, value);
|
||||
|
||||
graph.autoPaint();
|
||||
graph.emit('afteritemstatechange', { item, state: stateName, enabled: value });
|
||||
}
|
||||
|
||||
/**
|
||||
* 将指定状态的优先级提升为最高优先级
|
||||
* @param {Item} item 元素id或元素实例
|
||||
* @param state 状态名称
|
||||
*/
|
||||
public priorityState(item: Item | string, state: string): void {
|
||||
const { graph } = this;
|
||||
|
||||
let currentItem = item;
|
||||
if (isString(item)) {
|
||||
currentItem = graph.findById(item);
|
||||
}
|
||||
// 先取消已有的 state
|
||||
this.setItemState(currentItem as Item, state, false);
|
||||
|
||||
// 再设置state,则此时该优先级为最高
|
||||
this.setItemState(currentItem as Item, state, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有指定的状态
|
||||
*
|
||||
* @param {Item} item Item 实例
|
||||
* @param {string[]} states 状态名称集合
|
||||
* @memberof ItemController
|
||||
*/
|
||||
public clearItemStates(item: Item | string, states?: string | string[]): void {
|
||||
const { graph } = this;
|
||||
|
||||
if (isString(item)) {
|
||||
item = graph.findById(item);
|
||||
}
|
||||
|
||||
graph.emit('beforeitemstatesclear', { item, states });
|
||||
|
||||
item.clearStates(states);
|
||||
|
||||
graph.emit('afteritemstatesclear', { item, states });
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新指定的 Item
|
||||
*
|
||||
* @param {Item} item Item ID 或 实例
|
||||
* @memberof ItemController
|
||||
*/
|
||||
public refreshItem(item: Item | string): void {
|
||||
const { graph } = this;
|
||||
|
||||
if (isString(item)) {
|
||||
item = graph.findById(item);
|
||||
}
|
||||
|
||||
graph.emit('beforeitemrefresh', { item });
|
||||
|
||||
// 调用 Item 的 refresh 方法,实现刷新功能
|
||||
item.refresh();
|
||||
|
||||
graph.emit('afteritemrefresh', { item });
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 graph 上用 combos 数据生成的 comboTree 来增加所有 combos
|
||||
*
|
||||
* @param {ComboTree[]} comboTrees graph 上用 combos 数据生成的 comboTree
|
||||
* @param {ComboConfig[]} comboModels combos 数据
|
||||
* @memberof ItemController
|
||||
*/
|
||||
public addCombos(comboTrees: ComboTree[], comboModels: ComboConfig[]) {
|
||||
const { graph } = this;
|
||||
(comboTrees || []).forEach((ctree: ComboTree) => {
|
||||
traverseTreeUp<ComboTree>(ctree, (child) => {
|
||||
let comboModel;
|
||||
comboModels.forEach((model) => {
|
||||
if (model.id === child.id) {
|
||||
model.children = child.children;
|
||||
model.depth = child.depth;
|
||||
comboModel = model;
|
||||
}
|
||||
});
|
||||
if (comboModel) {
|
||||
this.addItem('combo', comboModel);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
const comboGroup = graph.get('comboGroup');
|
||||
if (comboGroup) comboGroup.sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* 改变Item的显示状态
|
||||
*
|
||||
* @param {Item} item Item ID 或 实例
|
||||
* @param {boolean} visible 是否显示
|
||||
* @memberof ItemController
|
||||
*/
|
||||
public changeItemVisibility(item: Item | string, visible: boolean): Item {
|
||||
const { graph } = this;
|
||||
|
||||
if (isString(item)) {
|
||||
item = graph.findById(item);
|
||||
}
|
||||
|
||||
if (!item) {
|
||||
console.warn('The item to be shown or hidden does not exist!');
|
||||
return;
|
||||
}
|
||||
|
||||
graph.emit('beforeitemvisibilitychange', { item, visible });
|
||||
|
||||
item.changeVisibility(visible);
|
||||
|
||||
if (item.getType && item.getType() === NODE) {
|
||||
const edges = (item as INode).getEdges();
|
||||
each(edges, (edge: IEdge) => {
|
||||
// 若隐藏节点,则将与之关联的边也隐藏
|
||||
// 若显示节点,则将与之关联的边也显示,但是需要判断边两端的节点都是可见的
|
||||
if (visible && !(edge.get('source').isVisible() && edge.get('target').isVisible())) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.changeItemVisibility(edge, visible);
|
||||
});
|
||||
} else if (item.getType && item.getType() === COMBO) {
|
||||
const comboTrees = graph.get('comboTrees');
|
||||
const id = item.get('id');
|
||||
let children = [];
|
||||
let found = false; // flag the terminate the forEach
|
||||
(comboTrees || []).forEach((ctree) => {
|
||||
if (found) return;
|
||||
if (!ctree.children || ctree.children.length === 0) return;
|
||||
traverseTree<ComboTree>(ctree, (combo) => {
|
||||
if (combo.id === id) {
|
||||
children = combo.children;
|
||||
found = true;
|
||||
return false; // terminate the traverse
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
children.forEach((child) => {
|
||||
const childItem = graph.findById(child.id);
|
||||
this.changeItemVisibility(childItem, visible);
|
||||
});
|
||||
|
||||
const edges = (item as INode).getEdges();
|
||||
each(edges, (edge: IEdge) => {
|
||||
// 若隐藏 combo,则将与 combo 本身关联的边也隐藏
|
||||
// 若显示 combo,则将与 combo 本身关联的边也显示,但是需要判断边两端的节点都是可见的
|
||||
if (visible && !(edge.get('source').isVisible() && edge.get('target').isVisible())) {
|
||||
return;
|
||||
}
|
||||
this.changeItemVisibility(edge, visible);
|
||||
});
|
||||
}
|
||||
graph.emit('afteritemvisibilitychange', { item, visible });
|
||||
return item;
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
(this.graph as Graph | null) = null;
|
||||
this.destroyed = true;
|
||||
}
|
||||
}
|
227
packages/core/src/graph/controller/layout.ts
Normal file
227
packages/core/src/graph/controller/layout.ts
Normal file
@ -0,0 +1,227 @@
|
||||
import { isNaN } from '../../util/base';
|
||||
import { GraphData } from '../../types';
|
||||
|
||||
import { IGraph } from '../../interface/graph';
|
||||
|
||||
export default abstract class LayoutController {
|
||||
public graph: IGraph;
|
||||
|
||||
public destroyed: boolean;
|
||||
|
||||
private layoutCfg;
|
||||
|
||||
private layoutType: string;
|
||||
|
||||
private layoutMethod;
|
||||
|
||||
private data;
|
||||
|
||||
constructor(graph: IGraph) {
|
||||
this.graph = graph;
|
||||
this.layoutCfg = graph.get('layout') || {};
|
||||
this.layoutType = this.layoutCfg.type;
|
||||
this.initLayout();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
private initLayout() {
|
||||
// no data before rendering
|
||||
}
|
||||
|
||||
public getLayoutType(): string {
|
||||
return this.layoutCfg.type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {function} success callback
|
||||
* @return {boolean} 是否使用web worker布局
|
||||
*/
|
||||
public abstract layout(success?: () => void): boolean;
|
||||
|
||||
// 绘制
|
||||
public refreshLayout() {
|
||||
const { graph } = this;
|
||||
if (graph.get('animate')) {
|
||||
graph.positionsAnimate();
|
||||
} else {
|
||||
graph.refreshPositions();
|
||||
}
|
||||
}
|
||||
|
||||
// 更新布局参数
|
||||
public abstract updateLayoutCfg(cfg);
|
||||
|
||||
// 更换布局
|
||||
public changeLayout(layoutType: string) {
|
||||
const { graph, layoutMethod } = this;
|
||||
|
||||
this.layoutType = layoutType;
|
||||
|
||||
this.layoutCfg = graph.get('layout') || {};
|
||||
this.layoutCfg.type = layoutType;
|
||||
|
||||
if (layoutMethod) {
|
||||
layoutMethod.destroy();
|
||||
}
|
||||
this.layout();
|
||||
}
|
||||
|
||||
// 更换数据
|
||||
public changeData() {
|
||||
const { layoutMethod } = this;
|
||||
|
||||
if (layoutMethod) {
|
||||
layoutMethod.destroy();
|
||||
}
|
||||
this.layout();
|
||||
}
|
||||
|
||||
// 销毁布局,不能使用 this.destroy,因为 controller 还需要被使用,只是把布局算法销毁
|
||||
public destroyLayout() {
|
||||
const { layoutMethod, graph } = this;
|
||||
if (layoutMethod) {
|
||||
layoutMethod.destroy();
|
||||
}
|
||||
graph.set('layout', undefined);
|
||||
this.layoutCfg = undefined;
|
||||
this.layoutType = undefined;
|
||||
this.layoutMethod = undefined;
|
||||
}
|
||||
|
||||
// 从 this.graph 获取数据
|
||||
public setDataFromGraph(): GraphData {
|
||||
const nodes = [];
|
||||
const edges = [];
|
||||
const combos = [];
|
||||
const nodeItems = this.graph.getNodes();
|
||||
const edgeItems = this.graph.getEdges();
|
||||
const comboItems = this.graph.getCombos();
|
||||
const nodeLength = nodeItems.length;
|
||||
for (let i = 0; i < nodeLength; i++) {
|
||||
const nodeItem = nodeItems[i];
|
||||
if (!nodeItem.isVisible()) continue;
|
||||
const model = nodeItem.getModel();
|
||||
nodes.push(model);
|
||||
}
|
||||
|
||||
const edgeLength = edgeItems.length;
|
||||
for (let i = 0; i < edgeLength; i++) {
|
||||
const edgeItem = edgeItems[i];
|
||||
if (edgeItem.destroyed || !edgeItem.isVisible()) continue;
|
||||
const model = edgeItem.getModel();
|
||||
if (!model.isComboEdge) edges.push(model);
|
||||
}
|
||||
|
||||
const comboLength = comboItems.length;
|
||||
for (let i = 0; i < comboLength; i++) {
|
||||
const comboItem = comboItems[i];
|
||||
if (comboItem.destroyed || !comboItem.isVisible()) continue;
|
||||
const model = comboItem.getModel();
|
||||
combos.push(model);
|
||||
}
|
||||
return { nodes, edges, combos } as GraphData;
|
||||
}
|
||||
|
||||
// 重新布局
|
||||
public relayout(reloadData?: boolean) {
|
||||
const { graph, layoutMethod, layoutCfg } = this;
|
||||
|
||||
if (reloadData) {
|
||||
this.data = this.setDataFromGraph();
|
||||
const { nodes } = this.data;
|
||||
if (!nodes) {
|
||||
return false;
|
||||
}
|
||||
this.initPositions(layoutCfg.center, nodes);
|
||||
layoutMethod.init(this.data);
|
||||
}
|
||||
|
||||
if (this.layoutType === 'force') {
|
||||
layoutMethod.ticking = false;
|
||||
layoutMethod.forceSimulation.stop();
|
||||
}
|
||||
graph.emit('beforelayout');
|
||||
layoutMethod.execute(reloadData);
|
||||
if (this.layoutType !== 'force' && !layoutMethod.enableTick) {
|
||||
graph.emit('afterlayout');
|
||||
}
|
||||
this.refreshLayout();
|
||||
}
|
||||
|
||||
// 控制布局动画
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
public layoutAnimate() {}
|
||||
|
||||
// 将当前节点的平均中心移动到原点
|
||||
public moveToZero() {
|
||||
const { graph } = this;
|
||||
|
||||
const data = graph.get('data');
|
||||
const { nodes } = data;
|
||||
if (nodes[0].x === undefined || nodes[0].x === null || isNaN(nodes[0].x)) {
|
||||
return;
|
||||
}
|
||||
const meanCenter = [0, 0];
|
||||
const nodeLength = nodes.length;
|
||||
for (let i = 0; i < nodeLength; i++) {
|
||||
const node = nodes[i];
|
||||
meanCenter[0] += node.x;
|
||||
meanCenter[1] += node.y;
|
||||
}
|
||||
|
||||
meanCenter[0] /= nodes.length;
|
||||
meanCenter[1] /= nodes.length;
|
||||
|
||||
for (let i = 0; i < nodeLength; i++) {
|
||||
const node = nodes[i];
|
||||
node.x -= meanCenter[0];
|
||||
node.y -= meanCenter[1];
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化节点到 center 附近
|
||||
public initPositions(center, nodes): boolean {
|
||||
const { graph } = this;
|
||||
if (!nodes) {
|
||||
return false;
|
||||
}
|
||||
let allHavePos = true;
|
||||
const width = graph.get('width') * 0.85;
|
||||
const height = graph.get('height') * 0.85;
|
||||
const nodeNum = nodes.length;
|
||||
const horiNum = Math.ceil(Math.sqrt(nodeNum) * (width / height));
|
||||
const vertiNum = Math.ceil(nodeNum / horiNum);
|
||||
let horiGap = width / (horiNum - 1);
|
||||
let vertiGap = height / (vertiNum - 1);
|
||||
if (!isFinite(horiGap) || !horiGap) horiGap = 0;
|
||||
if (!isFinite(vertiGap) || !horiGap) vertiGap = 0;
|
||||
const beginX = center[0] - width / 2;
|
||||
const beginY = center[1] - height / 2;
|
||||
const nodeLength = nodes.length;
|
||||
for (let i = 0; i < nodeLength; i++) {
|
||||
const node = nodes[i];
|
||||
if (isNaN(node.x)) {
|
||||
allHavePos = false;
|
||||
node.x = (i % horiNum) * horiGap + beginX;
|
||||
}
|
||||
if (isNaN(node.y)) {
|
||||
allHavePos = false;
|
||||
node.y = Math.floor(i / horiNum) * vertiGap + beginY;
|
||||
}
|
||||
}
|
||||
return allHavePos;
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
const { layoutMethod } = this;
|
||||
|
||||
this.graph = null;
|
||||
|
||||
if (layoutMethod) {
|
||||
layoutMethod.destroy();
|
||||
layoutMethod.destroyed = true;
|
||||
}
|
||||
|
||||
this.destroyed = true;
|
||||
}
|
||||
}
|
209
packages/core/src/graph/controller/mode.ts
Normal file
209
packages/core/src/graph/controller/mode.ts
Normal file
@ -0,0 +1,209 @@
|
||||
import { each, isArray, isString } from '@antv/util';
|
||||
import Behavior from '../../behavior/behavior';
|
||||
import { IBehavior } from '../../interface/behavior';
|
||||
import { IGraph } from '../../interface/graph';
|
||||
import { ModeType, Modes } from '../../types';
|
||||
import Graph from '../graph';
|
||||
|
||||
export default class ModeController {
|
||||
private graph: Graph;
|
||||
|
||||
public destroyed: boolean;
|
||||
|
||||
/**
|
||||
* modes = {
|
||||
* default: [ 'drag-node', 'zoom-canvas' ],
|
||||
* edit: [ 'drag-canvas', {
|
||||
* type: 'brush-select',
|
||||
* trigger: 'ctrl'
|
||||
* }]
|
||||
* }
|
||||
*
|
||||
* @private
|
||||
* @type {Modes}
|
||||
* @memberof Mode
|
||||
*/
|
||||
public modes: Modes;
|
||||
|
||||
/**
|
||||
* mode = 'drag-node'
|
||||
*
|
||||
* @private
|
||||
* @type {string}
|
||||
* @memberof Mode
|
||||
*/
|
||||
public mode: string;
|
||||
|
||||
private currentBehaves: IBehavior[];
|
||||
|
||||
constructor(graph: Graph) {
|
||||
this.graph = graph;
|
||||
this.destroyed = false;
|
||||
this.modes = graph.get('modes') || {
|
||||
default: [],
|
||||
};
|
||||
this.formatModes();
|
||||
|
||||
this.mode = graph.get('defaultMode') || 'default';
|
||||
this.currentBehaves = [];
|
||||
|
||||
this.setMode(this.mode);
|
||||
}
|
||||
|
||||
private formatModes() {
|
||||
const { modes } = this;
|
||||
each(modes, (mode) => {
|
||||
each(mode, (behavior, i) => {
|
||||
if (isString(behavior)) {
|
||||
mode[i] = { type: behavior };
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private setBehaviors(mode: string) {
|
||||
const { graph } = this;
|
||||
const behaviors = this.modes[mode];
|
||||
const behaves: IBehavior[] = [];
|
||||
let behave: IBehavior;
|
||||
each(behaviors || [], (behavior) => {
|
||||
const BehaviorInstance = Behavior.getBehavior(behavior.type);
|
||||
if (!BehaviorInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
behave = new BehaviorInstance(behavior);
|
||||
if (behave) {
|
||||
behave.bind(graph as IGraph);
|
||||
behaves.push(behave);
|
||||
}
|
||||
});
|
||||
this.currentBehaves = behaves;
|
||||
}
|
||||
|
||||
private static mergeBehaviors(modeBehaviors: ModeType[], behaviors: ModeType[]): ModeType[] {
|
||||
each(behaviors, (behavior) => {
|
||||
if (modeBehaviors.indexOf(behavior) < 0) {
|
||||
if (isString(behavior)) {
|
||||
behavior = { type: behavior };
|
||||
}
|
||||
modeBehaviors.push(behavior);
|
||||
}
|
||||
});
|
||||
return modeBehaviors;
|
||||
}
|
||||
|
||||
private static filterBehaviors(modeBehaviors: ModeType[], behaviors: ModeType[]): ModeType[] {
|
||||
const result: ModeType[] = [];
|
||||
modeBehaviors.forEach((behavior) => {
|
||||
let type: string = '';
|
||||
if (isString(behavior)) {
|
||||
type = behavior;
|
||||
} else {
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
type = behavior.type;
|
||||
}
|
||||
if (behaviors.indexOf(type) < 0) {
|
||||
result.push(behavior);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
public setMode(mode: string) {
|
||||
const { modes, graph } = this;
|
||||
|
||||
const current = mode;
|
||||
|
||||
const behaviors = modes[current];
|
||||
if (!behaviors) {
|
||||
return;
|
||||
}
|
||||
graph.emit('beforemodechange', { mode });
|
||||
|
||||
each(this.currentBehaves, (behave) => {
|
||||
behave.unbind(graph);
|
||||
});
|
||||
|
||||
this.setBehaviors(current);
|
||||
|
||||
graph.emit('aftermodechange', { mode });
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
public getMode(): string {
|
||||
return this.mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态增加或删除 Behavior
|
||||
*
|
||||
* @param {ModeType[]} behaviors
|
||||
* @param {(ModeType[] | ModeType)} modes
|
||||
* @param {boolean} isAdd
|
||||
* @returns {Mode}
|
||||
* @memberof Mode
|
||||
*/
|
||||
public manipulateBehaviors(
|
||||
behaviors: ModeType[] | ModeType,
|
||||
modes: string[] | string,
|
||||
isAdd: boolean,
|
||||
): ModeController {
|
||||
let behaves: ModeType[];
|
||||
if (!isArray(behaviors)) {
|
||||
behaves = [behaviors];
|
||||
} else {
|
||||
behaves = behaviors;
|
||||
}
|
||||
|
||||
if (isArray(modes)) {
|
||||
each(modes, (mode) => {
|
||||
if (!this.modes[mode]) {
|
||||
if (isAdd) {
|
||||
this.modes[mode] = behaves;
|
||||
}
|
||||
} else if (isAdd) {
|
||||
this.modes[mode] = ModeController.mergeBehaviors(this.modes[mode] || [], behaves);
|
||||
} else {
|
||||
this.modes[mode] = ModeController.filterBehaviors(this.modes[mode] || [], behaves);
|
||||
}
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
let currentMode = modes;
|
||||
if (!modes) {
|
||||
currentMode = this.mode; // isString(this.mode) ? this.mode : this.mode.type
|
||||
}
|
||||
|
||||
if (!this.modes[currentMode]) {
|
||||
if (isAdd) {
|
||||
this.modes[currentMode] = behaves;
|
||||
}
|
||||
}
|
||||
|
||||
if (isAdd) {
|
||||
this.modes[currentMode] = ModeController.mergeBehaviors(
|
||||
this.modes[currentMode] || [],
|
||||
behaves,
|
||||
);
|
||||
} else {
|
||||
this.modes[currentMode] = ModeController.filterBehaviors(
|
||||
this.modes[currentMode] || [],
|
||||
behaves,
|
||||
);
|
||||
}
|
||||
|
||||
this.setMode(this.mode);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
(this.graph as Graph | null) = null;
|
||||
(this.modes as Modes | null) = null;
|
||||
(this.currentBehaves as IBehavior[] | null) = null;
|
||||
this.destroyed = true;
|
||||
}
|
||||
}
|
181
packages/core/src/graph/controller/state.ts
Normal file
181
packages/core/src/graph/controller/state.ts
Normal file
@ -0,0 +1,181 @@
|
||||
import { each, isString } from '@antv/util';
|
||||
import { Item, States } from '../../types';
|
||||
import Graph from '../graph';
|
||||
import { INode } from '../../interface/item';
|
||||
|
||||
interface CachedStates {
|
||||
enabled: States;
|
||||
disabled: States;
|
||||
}
|
||||
|
||||
let timer: any = null;
|
||||
|
||||
export default class StateController {
|
||||
private graph: Graph;
|
||||
|
||||
private cachedStates: CachedStates;
|
||||
|
||||
public destroyed: boolean;
|
||||
|
||||
constructor(graph: Graph) {
|
||||
this.graph = graph;
|
||||
/**
|
||||
* this.cachedStates = {
|
||||
* enabled: {
|
||||
* hover: [Node]
|
||||
* },
|
||||
* disabled: {}
|
||||
* }
|
||||
*/
|
||||
this.cachedStates = {
|
||||
enabled: {},
|
||||
disabled: {},
|
||||
};
|
||||
this.destroyed = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 cache 的可用性
|
||||
*
|
||||
* @private
|
||||
* @param {Item} item
|
||||
* @param {string} state
|
||||
* @param {object} cache
|
||||
* @returns
|
||||
* @memberof State
|
||||
*/
|
||||
private static checkCache(item: Item, state: string, cache: { [key: string]: any }) {
|
||||
if (!cache[state]) {
|
||||
return;
|
||||
}
|
||||
const index = cache[state].indexOf(item);
|
||||
if (index >= 0) {
|
||||
cache[state].splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存 state
|
||||
*
|
||||
* @private
|
||||
* @param {Item} item Item 实例
|
||||
* @param {string} state 状态名称
|
||||
* @param {object} states
|
||||
* @memberof State
|
||||
*/
|
||||
private static cacheState(item: Item, state: string, states: States) {
|
||||
if (!states[state]) {
|
||||
states[state] = [];
|
||||
}
|
||||
states[state].push(item as INode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 Item 的状态
|
||||
*
|
||||
* @param {Item} item Item实例
|
||||
* @param {string} state 状态名称
|
||||
* @param {boolean} enabled 状态是否可用
|
||||
* @memberof State
|
||||
*/
|
||||
public updateState(item: Item, state: string, enabled: boolean) {
|
||||
const { checkCache, cacheState } = StateController;
|
||||
if (item.destroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { cachedStates } = this;
|
||||
|
||||
const enabledStates = cachedStates.enabled;
|
||||
const disabledStates = cachedStates.disabled;
|
||||
|
||||
if (enabled) {
|
||||
checkCache(item, state, disabledStates);
|
||||
cacheState(item, state, enabledStates);
|
||||
} else {
|
||||
checkCache(item, state, enabledStates);
|
||||
cacheState(item, state, disabledStates);
|
||||
}
|
||||
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
|
||||
timer = setTimeout(() => {
|
||||
timer = null;
|
||||
this.updateGraphStates();
|
||||
}, 16);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新 states,兼容 updateState,支持更新一个 state
|
||||
*
|
||||
* @param {Item} item
|
||||
* @param {(string | string[])} states
|
||||
* @param {boolean} enabled
|
||||
* @memberof State
|
||||
*/
|
||||
public updateStates(item: Item, states: string | string[], enabled: boolean) {
|
||||
if (isString(states)) {
|
||||
this.updateState(item, states, enabled);
|
||||
} else {
|
||||
states.forEach((state) => {
|
||||
this.updateState(item, state, enabled);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 states
|
||||
*
|
||||
* @memberof State
|
||||
*/
|
||||
public updateGraphStates() {
|
||||
const states = this.graph.get('states') as States;
|
||||
const { cachedStates } = this;
|
||||
|
||||
each(cachedStates.disabled, (val, key) => {
|
||||
if (states[key]) {
|
||||
states[key] = states[key].filter((item) => val.indexOf(item) < 0 && !val.destroyed);
|
||||
}
|
||||
});
|
||||
|
||||
each(cachedStates.enabled, (val: INode[], key) => {
|
||||
if (!states[key]) {
|
||||
states[key] = val;
|
||||
} else {
|
||||
const map: { [key: string]: boolean } = {};
|
||||
states[key].forEach((item) => {
|
||||
if (!item.destroyed) {
|
||||
map[item.get('id')] = true;
|
||||
}
|
||||
});
|
||||
val.forEach((item: Item) => {
|
||||
if (!item.destroyed) {
|
||||
const id = item.get('id');
|
||||
if (!map[id]) {
|
||||
map[id] = true;
|
||||
states[key].push(item as INode);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.graph.emit('graphstatechange', { states });
|
||||
this.cachedStates = {
|
||||
enabled: {},
|
||||
disabled: {},
|
||||
};
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
(this.graph as Graph | null) = null;
|
||||
(this.cachedStates as CachedStates | null) = null;
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
timer = null;
|
||||
this.destroyed = true;
|
||||
}
|
||||
}
|
230
packages/core/src/graph/controller/view.ts
Normal file
230
packages/core/src/graph/controller/view.ts
Normal file
@ -0,0 +1,230 @@
|
||||
import Canvas from '@antv/g-base/lib/abstract/canvas';
|
||||
import { Point } from '@antv/g-base/lib/types';
|
||||
import { Group } from '@antv/g-canvas';
|
||||
import { isNumber, isString } from '@antv/util';
|
||||
import { Item, Matrix, Padding, GraphAnimateConfig } from '../../types';
|
||||
import { formatPadding } from '../../util/base';
|
||||
import { applyMatrix, invertMatrix } from '../../util/math';
|
||||
import Graph from '../graph';
|
||||
import { modifyCSS } from '@antv/dom-util';
|
||||
|
||||
export default class ViewController {
|
||||
private graph: Graph;
|
||||
|
||||
public destroyed: boolean = false;
|
||||
|
||||
constructor(graph: Graph) {
|
||||
this.graph = graph;
|
||||
this.destroyed = false;
|
||||
}
|
||||
|
||||
// get view center coordinate
|
||||
private getViewCenter(): Point {
|
||||
const padding = this.getFormatPadding();
|
||||
const { graph } = this;
|
||||
const width: number = this.graph.get('width');
|
||||
const height: number = graph.get('height');
|
||||
return {
|
||||
x: (width - padding[1] - padding[3]) / 2 + padding[3],
|
||||
y: (height - padding[0] - padding[2]) / 2 + padding[0],
|
||||
};
|
||||
}
|
||||
|
||||
public fitCenter() {
|
||||
const { graph } = this;
|
||||
const group: Group = graph.get('group');
|
||||
group.resetMatrix();
|
||||
const bbox = group.getCanvasBBox();
|
||||
if (bbox.width === 0 || bbox.height === 0) return;
|
||||
const viewCenter = this.getViewCenter();
|
||||
const groupCenter: Point = {
|
||||
x: bbox.x + bbox.width / 2,
|
||||
y: bbox.y + bbox.height / 2,
|
||||
};
|
||||
|
||||
graph.translate(viewCenter.x - groupCenter.x, viewCenter.y - groupCenter.y);
|
||||
}
|
||||
|
||||
// fit view graph
|
||||
public fitView() {
|
||||
const { graph } = this;
|
||||
const padding = this.getFormatPadding();
|
||||
const width: number = graph.get('width');
|
||||
const height: number = graph.get('height');
|
||||
const group: Group = graph.get('group');
|
||||
group.resetMatrix();
|
||||
const bbox = group.getCanvasBBox();
|
||||
|
||||
if (bbox.width === 0 || bbox.height === 0) return;
|
||||
const viewCenter = this.getViewCenter();
|
||||
|
||||
const groupCenter: Point = {
|
||||
x: bbox.x + bbox.width / 2,
|
||||
y: bbox.y + bbox.height / 2,
|
||||
};
|
||||
|
||||
graph.translate(viewCenter.x - groupCenter.x, viewCenter.y - groupCenter.y);
|
||||
const w = (width - padding[1] - padding[3]) / bbox.width;
|
||||
const h = (height - padding[0] - padding[2]) / bbox.height;
|
||||
let ratio = w;
|
||||
if (w > h) {
|
||||
ratio = h;
|
||||
}
|
||||
graph.zoom(ratio, viewCenter);
|
||||
}
|
||||
|
||||
public getFormatPadding(): number[] {
|
||||
const padding = this.graph.get('fitViewPadding') as Padding;
|
||||
return formatPadding(padding);
|
||||
}
|
||||
|
||||
public focusPoint(point: Point, animate?: boolean, animateCfg?: GraphAnimateConfig) {
|
||||
const viewCenter = this.getViewCenter();
|
||||
const modelCenter = this.getPointByCanvas(viewCenter.x, viewCenter.y);
|
||||
let viewportMatrix: Matrix = this.graph.get('group').getMatrix();
|
||||
if (!viewportMatrix) viewportMatrix = [1, 0, 0, 0, 1, 0, 0, 0, 1];
|
||||
if (animate) {
|
||||
const dx = (modelCenter.x - point.x) * viewportMatrix[0];
|
||||
const dy = (modelCenter.y - point.y) * viewportMatrix[4];
|
||||
let lastX = 0;
|
||||
let lastY = 0;
|
||||
let newX = 0;
|
||||
let newY = 0;
|
||||
// 动画每次平移一点,直到目标位置
|
||||
this.graph.get('canvas').animate(
|
||||
(ratio) => {
|
||||
newX = dx * ratio;
|
||||
newY = dy * ratio;
|
||||
this.graph.translate(newX - lastX, newY - lastY);
|
||||
lastX = newX;
|
||||
lastY = newY;
|
||||
},
|
||||
{
|
||||
...animateCfg,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
this.graph.translate(
|
||||
(modelCenter.x - point.x) * viewportMatrix[0],
|
||||
(modelCenter.y - point.y) * viewportMatrix[4],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Canvas 坐标转成视口坐标
|
||||
* @param canvasX canvas x 坐标
|
||||
* @param canvasY canvas y 坐标
|
||||
*/
|
||||
public getPointByCanvas(canvasX: number, canvasY: number): Point {
|
||||
let viewportMatrix: Matrix = this.graph.get('group').getMatrix();
|
||||
if (!viewportMatrix) {
|
||||
viewportMatrix = [1, 0, 0, 0, 1, 0, 0, 0, 1];
|
||||
}
|
||||
const point = invertMatrix({ x: canvasX, y: canvasY }, viewportMatrix);
|
||||
return point;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将页面坐标转成视口坐标
|
||||
* @param clientX 页面 x 坐标
|
||||
* @param clientY 页面 y 坐标
|
||||
*/
|
||||
public getPointByClient(clientX: number, clientY: number): Point {
|
||||
const canvas: Canvas = this.graph.get('canvas');
|
||||
const canvasPoint: Point = canvas.getPointByClient(clientX, clientY);
|
||||
return this.getPointByCanvas(canvasPoint.x, canvasPoint.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将视口坐标转成页面坐标
|
||||
* @param x 视口 x 坐标
|
||||
* @param y 视口 y 坐标
|
||||
*/
|
||||
public getClientByPoint(x: number, y: number): Point {
|
||||
const canvas: Canvas = this.graph.get('canvas');
|
||||
const canvasPoint = this.getCanvasByPoint(x, y);
|
||||
const point = canvas.getClientByPoint(canvasPoint.x, canvasPoint.y);
|
||||
// return { x: point.clientX, y: point.clientY };
|
||||
return { x: point.x, y: point.y };
|
||||
}
|
||||
|
||||
/**
|
||||
* 将视口坐标转成 Canvas 坐标
|
||||
* @param x 视口 x 坐标
|
||||
* @param y 视口 y 坐标
|
||||
*/
|
||||
public getCanvasByPoint(x: number, y: number): Point {
|
||||
let viewportMatrix: Matrix = this.graph.get('group').getMatrix();
|
||||
if (!viewportMatrix) {
|
||||
viewportMatrix = [1, 0, 0, 0, 1, 0, 0, 0, 1];
|
||||
}
|
||||
return applyMatrix({ x, y }, viewportMatrix);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将元素移动到画布中心
|
||||
* @param item Item 实例或 id
|
||||
* @param {boolean} animate 是否带有动画地移动
|
||||
* @param {GraphAnimateConfig} animateCfg 若带有动画,动画的配置项
|
||||
*/
|
||||
public focus(item: string | Item, animate?: boolean, animateCfg?: GraphAnimateConfig) {
|
||||
if (isString(item)) {
|
||||
item = this.graph.findById(item);
|
||||
}
|
||||
const group: Group = item.get('group');
|
||||
let matrix: Matrix = group.getMatrix();
|
||||
if (!matrix) matrix = [1, 0, 0, 0, 1, 0, 0, 0, 1];
|
||||
|
||||
if (item) {
|
||||
// 用实际位置而不是model中的x,y,防止由于拖拽等的交互导致model的x,y并不是当前的x,y
|
||||
this.focusPoint(
|
||||
{
|
||||
x: matrix[6],
|
||||
y: matrix[7],
|
||||
},
|
||||
animate,
|
||||
animateCfg,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 改变 canvas 画布的宽度和高度
|
||||
* @param width canvas 宽度
|
||||
* @param height canvas 高度
|
||||
*/
|
||||
public changeSize(width: number, height: number) {
|
||||
const { graph } = this;
|
||||
if (!isNumber(width) || !isNumber(height)) {
|
||||
throw Error('invalid canvas width & height, please make sure width & height type is number');
|
||||
}
|
||||
|
||||
graph.set({ width, height });
|
||||
const canvas: Canvas = graph.get('canvas');
|
||||
canvas.changeSize(width, height);
|
||||
|
||||
// change the size of grid plugin if it exists on graph
|
||||
const plugins = graph.get('plugins');
|
||||
plugins.forEach((plugin) => {
|
||||
if (plugin.get('gridContainer')) {
|
||||
const minZoom = graph.get('minZoom');
|
||||
modifyCSS(plugin.get('container'), {
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
});
|
||||
modifyCSS(plugin.get('gridContainer'), {
|
||||
width: `${width / minZoom}px`,
|
||||
height: `${height / minZoom}px`,
|
||||
left: 0,
|
||||
top: 0,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
(this.graph as Graph | null) = null;
|
||||
this.destroyed = false;
|
||||
}
|
||||
}
|
2855
packages/core/src/graph/graph.ts
Normal file
2855
packages/core/src/graph/graph.ts
Normal file
File diff suppressed because it is too large
Load Diff
591
packages/core/src/graph/tree-graph.ts
Normal file
591
packages/core/src/graph/tree-graph.ts
Normal file
@ -0,0 +1,591 @@
|
||||
import { Point } from '@antv/g-base/lib/types';
|
||||
import Hierarchy from '@antv/hierarchy';
|
||||
import { each, isString } from '@antv/util';
|
||||
import { ITreeGraph } from '../interface/graph';
|
||||
import { GraphData, Item, NodeConfig, ShapeStyle, TreeGraphData, GraphOptions } from '../types';
|
||||
import { radialLayout, traverseTree } from '../util/graphic';
|
||||
import { ViewController } from './controller';
|
||||
import Graph, { PrivateGraphOption } from './graph';
|
||||
|
||||
export default abstract class TreeGraph extends Graph implements ITreeGraph {
|
||||
private layoutAnimating: boolean;
|
||||
|
||||
constructor(cfg: GraphOptions) {
|
||||
super(cfg);
|
||||
this.layoutAnimating = false;
|
||||
|
||||
// 用于缓存动画结束后需要删除的节点
|
||||
this.set('removeList', []);
|
||||
this.set('layoutMethod', this.getLayout());
|
||||
}
|
||||
|
||||
/**
|
||||
* initCanvas 需要在 PC 和 Mobile 分别实现
|
||||
*/
|
||||
public abstract initCanvas(): void;
|
||||
|
||||
/**
|
||||
* 通过 Layout 配置获取布局配置
|
||||
*/
|
||||
private getLayout() {
|
||||
const layout = this.get('layout');
|
||||
if (!layout) {
|
||||
return null;
|
||||
}
|
||||
if (typeof layout === 'function') {
|
||||
return layout;
|
||||
}
|
||||
if (!layout.type) {
|
||||
layout.type = 'dendrogram';
|
||||
}
|
||||
if (!layout.direction) {
|
||||
layout.direction = 'TB';
|
||||
}
|
||||
if (layout.radial) {
|
||||
return (data: any) => {
|
||||
const layoutData = Hierarchy[layout.type](data, layout);
|
||||
radialLayout(layoutData);
|
||||
return layoutData;
|
||||
};
|
||||
}
|
||||
return (data: any) => Hierarchy[layout.type](data, layout);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回指定节点在树图数据中的索引
|
||||
* @param children 树图数据
|
||||
* @param child 树图中某一个 Item 的数据
|
||||
*/
|
||||
private static indexOfChild(children: TreeGraphData[], id: string): number {
|
||||
let index = -1;
|
||||
// eslint-disable-next-line consistent-return
|
||||
each(children, (former, i) => {
|
||||
if (id === former.id) {
|
||||
index = i;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return index;
|
||||
}
|
||||
|
||||
public getDefaultCfg(): Partial<PrivateGraphOption> {
|
||||
const cfg = super.getDefaultCfg();
|
||||
// 树图默认打开动画
|
||||
cfg.animate = true;
|
||||
return cfg;
|
||||
}
|
||||
|
||||
/**
|
||||
* 向🌲树中添加数据
|
||||
* @param treeData 树图数据
|
||||
* @param parent 父节点实例
|
||||
* @param animate 是否开启动画
|
||||
*/
|
||||
private innerAddChild(treeData: TreeGraphData, parent: Item | undefined, animate: boolean): Item {
|
||||
const self = this;
|
||||
const model = treeData.data;
|
||||
|
||||
if (model) {
|
||||
// model 中应存储真实的数据,特别是真实的 children
|
||||
model.x = treeData.x;
|
||||
model.y = treeData.y;
|
||||
model.depth = treeData.depth;
|
||||
}
|
||||
|
||||
const node = self.addItem('node', model!, false);
|
||||
if (parent) {
|
||||
node.set('parent', parent);
|
||||
if (animate) {
|
||||
const origin = parent.get('originAttrs');
|
||||
if (origin) {
|
||||
node.set('originAttrs', origin);
|
||||
} else {
|
||||
const parentModel = parent.getModel();
|
||||
node.set('originAttrs', {
|
||||
x: parentModel.x,
|
||||
y: parentModel.y,
|
||||
});
|
||||
}
|
||||
}
|
||||
const childrenList = parent.get('children');
|
||||
if (!childrenList) {
|
||||
parent.set('children', [node]);
|
||||
} else {
|
||||
childrenList.push(node);
|
||||
}
|
||||
self.addItem(
|
||||
'edge',
|
||||
{
|
||||
source: parent,
|
||||
target: node,
|
||||
id: `${parent.get('id')}:${node.get('id')}`,
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
// 渲染到视图上应参考布局的children, 避免多绘制了收起的节点
|
||||
each(treeData.children || [], child => {
|
||||
self.innerAddChild(child, node, animate);
|
||||
});
|
||||
self.emit('afteraddchild', { item: node, parent });
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将数据上的变更转换到视图上
|
||||
* @param data
|
||||
* @param parent
|
||||
* @param animate
|
||||
*/
|
||||
private innerUpdateChild(data: TreeGraphData, parent: Item | undefined, animate: boolean) {
|
||||
const self = this;
|
||||
const current = self.findById(data.id);
|
||||
|
||||
// 若子树不存在,整体添加即可
|
||||
if (!current) {
|
||||
self.innerAddChild(data, parent, animate);
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新新节点下所有子节点
|
||||
each(data.children || [], (child: TreeGraphData) => {
|
||||
self.innerUpdateChild(child, current, animate);
|
||||
});
|
||||
|
||||
// 用现在节点的children实例来删除移除的子节点
|
||||
const children = current.get('children');
|
||||
if (children) {
|
||||
const len = children.length;
|
||||
if (len > 0) {
|
||||
for (let i = children.length - 1; i >= 0; i--) {
|
||||
const child = children[i].getModel();
|
||||
|
||||
if (TreeGraph.indexOfChild(data.children || [], child.id) === -1) {
|
||||
self.innerRemoveChild(
|
||||
child.id,
|
||||
{
|
||||
x: data.x!,
|
||||
y: data.y!,
|
||||
},
|
||||
animate,
|
||||
);
|
||||
|
||||
// 更新父节点下缓存的子节点 item 实例列表
|
||||
children.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let oriX: number;
|
||||
let oriY: number;
|
||||
if (current.get('originAttrs')) {
|
||||
oriX = current.get('originAttrs').x;
|
||||
oriY = current.get('originAttrs').y;
|
||||
}
|
||||
const model = current.getModel();
|
||||
if (animate) {
|
||||
// 如果有动画,先缓存节点运动再更新节点
|
||||
current.set('originAttrs', {
|
||||
x: model.x,
|
||||
y: model.y,
|
||||
});
|
||||
}
|
||||
current.set('model', data.data);
|
||||
if (oriX !== data.x || oriY !== data.y) {
|
||||
current.updatePosition({ x: data.x, y: data.y });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除子节点Item对象
|
||||
* @param id
|
||||
* @param to
|
||||
* @param animate
|
||||
*/
|
||||
private innerRemoveChild(id: string, to: Point, animate: boolean) {
|
||||
const self = this;
|
||||
const node: Item = self.findById(id);
|
||||
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
each(node.get('children'), child => {
|
||||
self.innerRemoveChild(child.getModel().id, to, animate);
|
||||
});
|
||||
|
||||
if (animate) {
|
||||
const model = node.getModel();
|
||||
node.set('to', to);
|
||||
node.set('originAttrs', { x: model.x, y: model.y });
|
||||
self.get('removeList').push(node);
|
||||
} else {
|
||||
self.removeItem(node);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新数据模型,差量更新并重新渲染
|
||||
* @param {object} data 数据模型
|
||||
*/
|
||||
public changeData(data?: GraphData | TreeGraphData): any {
|
||||
const self = this;
|
||||
|
||||
// 更改数据源后,取消所有状态
|
||||
this.getNodes().map(node => self.clearItemStates(node));
|
||||
this.getEdges().map(edge => self.clearItemStates(edge));
|
||||
|
||||
if (data) {
|
||||
self.data(data);
|
||||
self.render();
|
||||
} else {
|
||||
self.layout(this.get('fitView'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 已更名为 updateLayout,为保持兼容暂且保留。
|
||||
* 更改并应用树布局算法
|
||||
* @param {object} layout 布局算法
|
||||
*/
|
||||
public changeLayout(layout: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
'Please call updateLayout instead of changeLayout. changeLayout will be discarded soon',
|
||||
);
|
||||
const self = this;
|
||||
self.updateLayout(layout);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更改并应用树布局算法
|
||||
* @param {object} layout 布局算法
|
||||
*/
|
||||
public updateLayout(layout: any) {
|
||||
const self = this;
|
||||
if (!layout) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('layout cannot be null');
|
||||
return;
|
||||
}
|
||||
self.set('layout', layout);
|
||||
self.set('layoutMethod', self.getLayout());
|
||||
self.layout();
|
||||
}
|
||||
|
||||
/**
|
||||
* 已更名为 layout,为保持兼容暂且保留。
|
||||
* 根据目前的 data 刷新布局,更新到画布上。用于变更数据之后刷新视图。
|
||||
* @param {boolean} fitView 更新布局时是否需要适应窗口
|
||||
*/
|
||||
public refreshLayout(fitView?: boolean) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
'Please call layout instead of refreshLayout. refreshLayout will be discarded soon',
|
||||
);
|
||||
const self = this;
|
||||
self.layout(fitView);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据目前的 data 刷新布局,更新到画布上。用于变更数据之后刷新视图。
|
||||
* @param {boolean} fitView 更新布局时是否需要适应窗口
|
||||
*/
|
||||
public layout(fitView?: boolean) {
|
||||
const self = this;
|
||||
const data: TreeGraphData = self.get('data');
|
||||
const layoutMethod = self.get('layoutMethod');
|
||||
const layoutData = layoutMethod(data, self.get('layout'));
|
||||
|
||||
const animate: boolean = self.get('animate');
|
||||
|
||||
self.emit('beforerefreshlayout', { data, layoutData });
|
||||
self.emit('beforelayout');
|
||||
|
||||
self.innerUpdateChild(layoutData, undefined, animate);
|
||||
|
||||
if (fitView) {
|
||||
const viewController: ViewController = self.get('viewController');
|
||||
viewController.fitView();
|
||||
}
|
||||
|
||||
if (!animate) {
|
||||
// 如果没有动画,目前仅更新了节点的位置,刷新一下边的样式
|
||||
self.refresh();
|
||||
self.paint();
|
||||
} else {
|
||||
self.layoutAnimate(layoutData);
|
||||
}
|
||||
self.emit('afterrefreshlayout', { data, layoutData });
|
||||
self.emit('afterlayout');
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加子树到对应 id 的节点
|
||||
* @param {TreeGraphData} data 子树数据模型
|
||||
* @param {string} parent 子树的父节点id
|
||||
*/
|
||||
public addChild(data: TreeGraphData, parent: string | Item): void {
|
||||
const self = this;
|
||||
self.emit('beforeaddchild', { model: data, parent });
|
||||
// 将数据添加到源数据中,走changeData方法
|
||||
if (!isString(parent)) {
|
||||
parent = parent.get('id') as string;
|
||||
}
|
||||
|
||||
const parentData = self.findDataById(parent);
|
||||
|
||||
if (parentData) {
|
||||
if (!parentData.children) {
|
||||
parentData.children = [];
|
||||
}
|
||||
parentData.children.push(data);
|
||||
self.changeData();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新某个节点下的所有子节点
|
||||
* @param {TreeGraphData[]} data 子树数据模型集合
|
||||
* @param {string} parent 子树的父节点id
|
||||
*/
|
||||
public updateChildren(data: TreeGraphData[], parentId: string): void {
|
||||
const self = this;
|
||||
|
||||
// 如果没有父节点或找不到该节点,是全量的更新,直接重置data
|
||||
if (!parentId || !self.findById(parentId)) {
|
||||
console.warn(`Update children failed! There is no node with id '${parentId}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
const parentModel = self.findDataById(parentId) as NodeConfig;
|
||||
|
||||
parentModel.children = data;
|
||||
|
||||
self.changeData();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新源数据,差量更新子树
|
||||
* @param {TreeGraphData} data 子树数据模型
|
||||
* @param {string} parentId 子树的父节点id
|
||||
*/
|
||||
public updateChild(data: TreeGraphData, parentId?: string): void {
|
||||
const self = this;
|
||||
|
||||
// 如果没有父节点或找不到该节点,是全量的更新,直接重置data
|
||||
if (!parentId || !self.findById(parentId)) {
|
||||
self.changeData(data);
|
||||
return;
|
||||
}
|
||||
|
||||
const parentModel = self.findDataById(parentId) as NodeConfig;
|
||||
|
||||
const current = self.findById(data.id);
|
||||
|
||||
if (!parentModel.children) {
|
||||
// 当 current 不存在时,children 为空数组
|
||||
parentModel.children = [];
|
||||
}
|
||||
|
||||
// 如果不存在该节点,则添加
|
||||
if (!current) {
|
||||
parentModel.children.push(data);
|
||||
} else {
|
||||
const index = TreeGraph.indexOfChild(parentModel.children, data.id);
|
||||
parentModel.children[index] = data;
|
||||
}
|
||||
self.changeData();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除子树
|
||||
* @param {string} id 子树根节点id
|
||||
*/
|
||||
public removeChild(id: string): void {
|
||||
const self = this;
|
||||
const node = self.findById(id);
|
||||
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parent = node.get('parent');
|
||||
if (parent && !parent.destroyed) {
|
||||
const parentNode = self.findDataById(parent.get('id'));
|
||||
const siblings = (parentNode && parentNode.children) || [];
|
||||
const model: NodeConfig = node.getModel() as NodeConfig;
|
||||
|
||||
const index = TreeGraph.indexOfChild(siblings, model.id);
|
||||
siblings.splice(index, 1);
|
||||
}
|
||||
self.changeData();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据id获取对应的源数据
|
||||
* @param {string} id 元素id
|
||||
* @param {TreeGraphData | undefined} parent 从哪个节点开始寻找,为空时从根节点开始查找
|
||||
* @return {TreeGraphData} 对应源数据
|
||||
*/
|
||||
public findDataById(id: string, parent?: TreeGraphData | undefined): TreeGraphData | null {
|
||||
const self = this;
|
||||
|
||||
if (!parent) {
|
||||
parent = self.get('data') as TreeGraphData;
|
||||
}
|
||||
|
||||
if (id === parent.id) {
|
||||
return parent;
|
||||
}
|
||||
|
||||
let result: TreeGraphData | null = null;
|
||||
// eslint-disable-next-line consistent-return
|
||||
each(parent.children || [], child => {
|
||||
if (child.id === id) {
|
||||
result = child;
|
||||
return false;
|
||||
}
|
||||
result = self.findDataById(id, child);
|
||||
if (result) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 布局动画接口,用于数据更新时做节点位置更新的动画
|
||||
* @param {TreeGraphData} data 更新的数据
|
||||
* @param {function} onFrame 定义节点位置更新时如何移动
|
||||
*/
|
||||
public layoutAnimate(
|
||||
data: TreeGraphData,
|
||||
onFrame?: (
|
||||
item: Item,
|
||||
ratio: number,
|
||||
originAttrs?: ShapeStyle,
|
||||
data?: TreeGraphData,
|
||||
) => unknown,
|
||||
): void {
|
||||
const self = this;
|
||||
const animateCfg = this.get('animateCfg');
|
||||
self.emit('beforeanimate', { data });
|
||||
// 如果边中没有指定锚点,但是本身有锚点控制,在动画过程中保持锚点不变
|
||||
self.getEdges().forEach(edge => {
|
||||
const model = edge.get('model');
|
||||
if (!model.sourceAnchor) {
|
||||
model.sourceAnchor = edge.get('sourceAnchorIndex');
|
||||
}
|
||||
});
|
||||
|
||||
this.get('canvas').animate(
|
||||
(ratio: number) => {
|
||||
traverseTree<TreeGraphData>(data, child => {
|
||||
const node = self.findById(child.id);
|
||||
|
||||
// 只有当存在node的时候才执行
|
||||
if (node) {
|
||||
let origin = node.get('originAttrs');
|
||||
const model = node.get('model');
|
||||
|
||||
if (!origin) {
|
||||
origin = {
|
||||
x: model.x,
|
||||
y: model.y,
|
||||
};
|
||||
node.set('originAttrs', origin);
|
||||
}
|
||||
|
||||
if (onFrame) {
|
||||
const attrs = onFrame(node, ratio, origin, data);
|
||||
node.set('model', Object.assign(model, attrs));
|
||||
} else {
|
||||
model.x = origin.x + (child.x! - origin.x) * ratio;
|
||||
model.y = origin.y + (child.y! - origin.y) * ratio;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
each(self.get('removeList'), node => {
|
||||
const model = node.getModel();
|
||||
const from = node.get('originAttrs');
|
||||
const to = node.get('to');
|
||||
model.x = from.x + (to.x - from.x) * ratio;
|
||||
model.y = from.y + (to.y - from.y) * ratio;
|
||||
});
|
||||
|
||||
self.refreshPositions();
|
||||
},
|
||||
{
|
||||
duration: animateCfg.duration,
|
||||
easing: animateCfg.ease,
|
||||
callback: () => {
|
||||
each(self.getNodes(), node => {
|
||||
node.set('originAttrs', null);
|
||||
});
|
||||
|
||||
each(self.get('removeList'), node => {
|
||||
self.removeItem(node);
|
||||
});
|
||||
|
||||
self.set('removeList', []);
|
||||
|
||||
if (animateCfg.callback) {
|
||||
animateCfg.callback();
|
||||
}
|
||||
|
||||
self.emit('afteranimate', { data });
|
||||
},
|
||||
delay: animateCfg.delay,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 立即停止布局动画
|
||||
*/
|
||||
public stopLayoutAnimate(): void {
|
||||
this.get('canvas').stopAnimate();
|
||||
this.emit('layoutanimateend', { data: this.get('data') });
|
||||
this.layoutAnimating = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否在布局动画
|
||||
* @return {boolean} 是否有布局动画
|
||||
*/
|
||||
public isLayoutAnimating(): boolean {
|
||||
return this.layoutAnimating;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据data接口的数据渲染视图
|
||||
*/
|
||||
public render(): void {
|
||||
const self = this;
|
||||
const data: TreeGraphData = self.get('data');
|
||||
|
||||
if (!data) {
|
||||
throw new Error('data must be defined first');
|
||||
}
|
||||
|
||||
self.clear();
|
||||
|
||||
self.emit('beforerender');
|
||||
|
||||
self.layout(this.get('fitView'));
|
||||
|
||||
self.emit('afterrender');
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出图数据
|
||||
* @return {object} data
|
||||
*/
|
||||
public save(): TreeGraphData | GraphData {
|
||||
return this.get('data');
|
||||
}
|
||||
}
|
34
packages/core/src/index.ts
Normal file
34
packages/core/src/index.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import Behaviors from "./behavior";
|
||||
import Graph from "./graph/graph";
|
||||
import TreeGraph from "./graph/tree-graph";
|
||||
import Shape, { Arrow, Marker } from "./shape";
|
||||
import Global from "./global";
|
||||
import Util from "./util";
|
||||
|
||||
const registerNode = Shape.registerNode;
|
||||
const registerEdge = Shape.registerEdge;
|
||||
const registerCombo = Shape.registerCombo;
|
||||
const registerBehavior = Behaviors.registerBehavior;
|
||||
|
||||
export {
|
||||
registerNode,
|
||||
registerCombo,
|
||||
Graph,
|
||||
TreeGraph,
|
||||
Util,
|
||||
registerEdge,
|
||||
registerBehavior
|
||||
};
|
||||
|
||||
export default {
|
||||
version: Global.version,
|
||||
Graph,
|
||||
TreeGraph,
|
||||
Util,
|
||||
registerNode: Shape.registerNode,
|
||||
registerEdge: Shape.registerEdge,
|
||||
registerCombo: Shape.registerCombo,
|
||||
registerBehavior: Behaviors.registerBehavior,
|
||||
Arrow,
|
||||
Marker
|
||||
};
|
43
packages/core/src/interface/behavior.ts
Normal file
43
packages/core/src/interface/behavior.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { Event as GraphEvent } from '@antv/g-base';
|
||||
import { Canvas } from '@antv/g-canvas';
|
||||
import { G6Event, IG6GraphEvent, IShapeBase, Item } from '../types';
|
||||
import { IGraph } from './graph';
|
||||
|
||||
export interface IBehavior {
|
||||
getEvents(): {
|
||||
[key in G6Event]?: string;
|
||||
};
|
||||
getDefaultCfg?(): object;
|
||||
shouldBegin?(e?: IG6GraphEvent): boolean;
|
||||
shouldUpdate?(e?: IG6GraphEvent): boolean;
|
||||
shouldEnd?(e?: IG6GraphEvent): boolean;
|
||||
bind?(e: IGraph): void;
|
||||
unbind?(e: IGraph): void;
|
||||
}
|
||||
|
||||
export class G6GraphEvent extends GraphEvent implements IG6GraphEvent {
|
||||
public item: Item;
|
||||
|
||||
public canvasX: number;
|
||||
|
||||
public canvasY: number;
|
||||
|
||||
public clientX: number;
|
||||
|
||||
public clientY: number;
|
||||
|
||||
public wheelDelta: number;
|
||||
|
||||
public detail: number;
|
||||
|
||||
public target!: IShapeBase & Canvas;
|
||||
|
||||
constructor(type: string, event: IG6GraphEvent) {
|
||||
super(type, event);
|
||||
this.item = event.item;
|
||||
this.canvasX = event.canvasX;
|
||||
this.canvasY = event.canvasY;
|
||||
this.wheelDelta = event.wheelDelta;
|
||||
this.detail = event.detail;
|
||||
}
|
||||
}
|
642
packages/core/src/interface/graph.ts
Normal file
642
packages/core/src/interface/graph.ts
Normal file
@ -0,0 +1,642 @@
|
||||
import EventEmitter from '@antv/event-emitter';
|
||||
import { Point } from '@antv/g-base/lib/types';
|
||||
import { IGroup } from '@antv/g-base/lib/interfaces';
|
||||
import Graph from '../graph/graph';
|
||||
import {
|
||||
EdgeConfig,
|
||||
GraphData,
|
||||
Item,
|
||||
ITEM_TYPE,
|
||||
ModelConfig,
|
||||
NodeConfig,
|
||||
Padding,
|
||||
ShapeStyle,
|
||||
TreeGraphData,
|
||||
LayoutConfig,
|
||||
GraphOptions,
|
||||
ModeOption,
|
||||
ModeType,
|
||||
ComboConfig,
|
||||
GraphAnimateConfig,
|
||||
StackData,
|
||||
HullCfg,
|
||||
WaterMarkerConfig,
|
||||
IG6GraphEvent,
|
||||
} from '../types';
|
||||
import { IEdge, INode, ICombo } from './item';
|
||||
import Hull from '../item/hull';
|
||||
import Stack from '../algorithm/structs/stack';
|
||||
|
||||
export interface IGraph extends EventEmitter {
|
||||
getDefaultCfg(): Partial<GraphOptions>;
|
||||
get<T = any>(key: string): T;
|
||||
set<T = any>(key: string | object, value?: T): Graph;
|
||||
findById(id: string): Item;
|
||||
translate(dx: number, dy: number): void;
|
||||
zoom(ratio: number, center?: Point): void;
|
||||
|
||||
/**
|
||||
* 获取 graph 的根图形分组
|
||||
* @return 根 group
|
||||
*/
|
||||
getGroup(): IGroup;
|
||||
|
||||
/**
|
||||
* 获取 graph 的 DOM 容器
|
||||
* @return DOM 容器
|
||||
*/
|
||||
getContainer(): HTMLElement;
|
||||
|
||||
/**
|
||||
* 获取 graph 的最小缩放比例
|
||||
* @return minZoom
|
||||
*/
|
||||
getMinZoom(): number;
|
||||
|
||||
/**
|
||||
* 设置 graph 的最小缩放比例
|
||||
* @return minZoom
|
||||
*/
|
||||
setMinZoom(ratio: number);
|
||||
|
||||
/**
|
||||
* 获取 graph 的最大缩放比例
|
||||
* @param maxZoom
|
||||
*/
|
||||
getMaxZoom(): number;
|
||||
|
||||
/**
|
||||
* 设置 graph 的最大缩放比例
|
||||
* @param maxZoom
|
||||
*/
|
||||
setMaxZoom(ratio: number);
|
||||
|
||||
/**
|
||||
* 获取 graph 的宽度
|
||||
* @return width
|
||||
*/
|
||||
getWidth(): number;
|
||||
|
||||
/**
|
||||
* 获取 graph 的高度
|
||||
* @return height
|
||||
*/
|
||||
getHeight(): number;
|
||||
|
||||
/**
|
||||
* 将屏幕坐标转换为视口坐标
|
||||
* @param {number} clientX 屏幕 x 坐标
|
||||
* @param {number} clientY 屏幕 y 坐标
|
||||
* @return {Point} 视口坐标
|
||||
*/
|
||||
getPointByClient(clientX: number, clientY: number): Point;
|
||||
|
||||
/**
|
||||
* 将视口坐标转换为屏幕坐标
|
||||
* @param {number} x 视口x坐标
|
||||
* @param {number} y 视口y坐标
|
||||
* @return {object} 视口坐标
|
||||
*/
|
||||
getClientByPoint(x: number, y: number): Point;
|
||||
|
||||
/**
|
||||
* 将画布坐标转换为视口坐标
|
||||
* @param {number} canvasX 画布 x 坐标
|
||||
* @param {number} canvasY 画布 y 坐标
|
||||
* @return {Point} 视口坐标
|
||||
*/
|
||||
getPointByCanvas(canvasX: number, canvasY: number): Point;
|
||||
|
||||
/**
|
||||
* 将视口坐标转换为画布坐标
|
||||
* @param {number} x 视口 x 坐标
|
||||
* @param {number} y 视口 y 坐标
|
||||
* @return {Point} 画布坐标
|
||||
*/
|
||||
getCanvasByPoint(x: number, y: number): Point;
|
||||
|
||||
/**
|
||||
* 设置是否在更新/刷新后自动重绘
|
||||
* @param {boolean} auto 自动重绘
|
||||
*/
|
||||
setAutoPaint(auto: boolean): void;
|
||||
|
||||
/**
|
||||
* 显示元素
|
||||
* @param {Item} item 指定元素
|
||||
* @param {boolean} stack 本次操作是否入栈,默认为 true
|
||||
*/
|
||||
showItem(item: Item | string, stack?: boolean): void;
|
||||
|
||||
/**
|
||||
* 隐藏元素
|
||||
* @param {Item} item 指定元素
|
||||
* @param {boolean} stack 本次操作是否入栈,默认为 true
|
||||
*/
|
||||
hideItem(item: Item | string, stack?: boolean): void;
|
||||
|
||||
/**
|
||||
* 仅画布重新绘制
|
||||
*/
|
||||
paint(): void;
|
||||
|
||||
/**
|
||||
* 自动重绘
|
||||
*/
|
||||
autoPaint(): void;
|
||||
|
||||
/**
|
||||
* 刷新元素
|
||||
* @param {Item} item 元素id或元素实例
|
||||
*/
|
||||
refreshItem(item: Item | string): void;
|
||||
|
||||
/**
|
||||
* 将元素移动到视口中心
|
||||
* @param {Item} item 指定元素
|
||||
* @param {boolean} animate 是否带有动画地移动
|
||||
* @param {GraphAnimateConfig} animateCfg 若带有动画,动画的配置项
|
||||
*/
|
||||
focusItem(item: Item | string, animate?: boolean, animateCfg?: GraphAnimateConfig): void;
|
||||
|
||||
/**
|
||||
* 调整视口适应视图
|
||||
* @param {Padding} padding 四周围边距
|
||||
*/
|
||||
fitView(padding?: Padding): void;
|
||||
|
||||
/**
|
||||
* 调整视口适应视图,不缩放,仅将图 bbox 中心对齐到画布中心
|
||||
*/
|
||||
fitCenter(): void;
|
||||
|
||||
/**
|
||||
* 伸缩视口到一固定比例
|
||||
* @param {number} toRatio 伸缩比例
|
||||
* @param {Point} center 以center的x, y坐标为中心缩放
|
||||
*/
|
||||
zoomTo(toRatio: number, center?: Point): void;
|
||||
|
||||
/**
|
||||
* 删除元素
|
||||
* @param {Item} item 元素id或元素实例
|
||||
* @param {boolean} stack 本次操作是否入栈,默认为 true
|
||||
*/
|
||||
removeItem(item: Item | string, stack?: boolean): void;
|
||||
|
||||
/**
|
||||
* 删除元素
|
||||
* @param {Item} item 元素id或元素实例
|
||||
* @param {boolean} stack 本次操作是否入栈,默认为 true
|
||||
*/
|
||||
remove(item: Item | string, stack?: boolean): void;
|
||||
|
||||
/**
|
||||
* 新增元素
|
||||
* @param {string} type 元素类型(node | edge)
|
||||
* @param {ModelConfig} model 元素数据模型
|
||||
* @param {boolean} stack 本次操作是否入栈,默认为 true
|
||||
* @return {Item} 元素实例
|
||||
*/
|
||||
addItem(type: ITEM_TYPE, model: ModelConfig, stack?: boolean): Item;
|
||||
|
||||
add(type: ITEM_TYPE, model: ModelConfig, stack?: boolean): Item;
|
||||
|
||||
/**
|
||||
* 更新元素
|
||||
* @param {Item} item 元素id或元素实例
|
||||
* @param {EdgeConfig | NodeConfig} cfg 需要更新的数据
|
||||
* @param {boolean} stack 本次操作是否入栈,默认为 true
|
||||
*/
|
||||
updateItem(item: Item | string, cfg: Partial<NodeConfig> | EdgeConfig, stack?: boolean): void;
|
||||
|
||||
update(item: Item | string, cfg: Partial<NodeConfig> | EdgeConfig, stack?: boolean): void;
|
||||
|
||||
/**
|
||||
* 更新 Combo 结构,例如移动子树等
|
||||
* @param {string | INode | ICombo} item 需要被更新的 Combo 或 节点 id
|
||||
* @param {string | undefined} parentId 新的父 combo id,undefined 代表没有父 combo
|
||||
*/
|
||||
updateComboTree(
|
||||
item: string | INode | ICombo,
|
||||
parentId?: string | undefined,
|
||||
stack?: boolean,
|
||||
): void;
|
||||
|
||||
/**
|
||||
* 解散 combo
|
||||
* @param {String | ICombo} item 需要被解散的 Combo item 或 id
|
||||
*/
|
||||
uncombo(item: string | ICombo): void;
|
||||
|
||||
/**
|
||||
* 根据已经存在的节点或 combo 创建新的 combo
|
||||
* @param combo combo ID 或 Combo 配置
|
||||
* @param elements 添加到 Combo 中的元素,包括节点和 combo
|
||||
*/
|
||||
createCombo(combo: string | ComboConfig, elements: string[]): void;
|
||||
|
||||
/**
|
||||
* 设置元素状态
|
||||
* @param {Item} item 元素id或元素实例
|
||||
* @param {string} state 状态名称
|
||||
* @param {boolean} value 是否启用状态或状态值
|
||||
*/
|
||||
setItemState(item: Item | string, state: string, value: string | boolean): void;
|
||||
|
||||
/**
|
||||
* 将指定状态的优先级提升为最高优先级
|
||||
* @param {Item} item 元素id或元素实例
|
||||
* @param state 状态名称
|
||||
*/
|
||||
priorityState(item: Item | string, state: string): void;
|
||||
|
||||
/**
|
||||
* 设置视图初始化数据
|
||||
* @param {GraphData} data 初始化数据
|
||||
*/
|
||||
data(data?: GraphData | TreeGraphData): void;
|
||||
|
||||
/**
|
||||
* 当源数据在外部发生变更时,根据新数据刷新视图。但是不刷新节点位置
|
||||
*/
|
||||
refresh(): void;
|
||||
|
||||
/**
|
||||
* 根据 graph 上的 animateCfg 进行视图中节点位置动画接口
|
||||
*/
|
||||
positionsAnimate(): void;
|
||||
|
||||
/**
|
||||
* 当节点位置在外部发生改变时,刷新所有节点位置,重计算边
|
||||
*/
|
||||
refreshPositions(): void;
|
||||
|
||||
/**
|
||||
* 根据data接口的数据渲染视图
|
||||
*/
|
||||
render(): void;
|
||||
|
||||
/**
|
||||
* 获取当前图中所有节点的item实例
|
||||
*/
|
||||
getNodes(): INode[];
|
||||
|
||||
/**
|
||||
* 获取当前图中所有边的item实例
|
||||
*/
|
||||
getEdges(): IEdge[];
|
||||
|
||||
/**
|
||||
* 获取当前图中所有 combo 的实例
|
||||
*/
|
||||
getCombos(): ICombo[];
|
||||
|
||||
/**
|
||||
* 获取节点所有的邻居节点,有向图中效果同无向图
|
||||
*
|
||||
* @param {(string | INode)} node 节点 ID 或实例
|
||||
* @returns {INode[]}
|
||||
* @memberof IGraph
|
||||
*/
|
||||
getNeighbors(node: string | INode, type?: 'source' | 'target' | undefined): INode[];
|
||||
|
||||
/**
|
||||
* 获取 node 的度数
|
||||
*
|
||||
* @param {(string | INode)} node 节点 ID 或实例
|
||||
* @param {('in' | 'out' | 'total' | 'all' | undefined)} 度数类型,in 入度,out 出度,total 总度数,all 返回三种类型度数的对象
|
||||
* @returns {Number | Object} 该节点的度数
|
||||
* @memberof IGraph
|
||||
*/
|
||||
getNodeDegree(
|
||||
node: string | INode,
|
||||
type?: 'in' | 'out' | 'total' | 'all' | undefined,
|
||||
): Number | Object;
|
||||
/**
|
||||
* 获取指定 combo 中所有的节点
|
||||
* @param comboId Combo ID 或 combo 实例
|
||||
*/
|
||||
getComboChildren(combo: string | ICombo): { nodes: INode[]; combos: ICombo[] };
|
||||
|
||||
/**
|
||||
* 获取当前视口伸缩比例
|
||||
* @return {number} 比例
|
||||
*/
|
||||
getZoom(): number;
|
||||
|
||||
/**
|
||||
* 获取当前的行为模式
|
||||
*/
|
||||
getCurrentMode(): string;
|
||||
|
||||
/**
|
||||
* 切换行为模式
|
||||
* @param {string} mode 指定模式
|
||||
*/
|
||||
setMode(mode: string): Graph;
|
||||
|
||||
isAnimating(): boolean;
|
||||
|
||||
stopAnimate(): void;
|
||||
|
||||
/**
|
||||
* 新增行为
|
||||
* @param {string | ModeOption | ModeType[]} behaviors 添加的行为
|
||||
* @param {string | string[]} modes 添加到对应的模式
|
||||
* @return {Graph} Graph
|
||||
*/
|
||||
addBehaviors(behaviors: string | ModeOption | ModeType[], modes: string | string[]): Graph;
|
||||
|
||||
/**
|
||||
* 移除行为
|
||||
* @param {string | ModeOption | ModeType[]} behaviors 移除的行为
|
||||
* @param {string | string[]} modes 从指定的模式中移除
|
||||
* @return {Graph} Graph
|
||||
*/
|
||||
removeBehaviors(behaviors: string | ModeOption | ModeType[], modes: string | string[]): Graph;
|
||||
|
||||
/**
|
||||
* 清除画布元素
|
||||
*/
|
||||
clear(): Graph;
|
||||
|
||||
/**
|
||||
* 接收数据进行渲染
|
||||
* @Param {GraphData} data 初始化数据
|
||||
*/
|
||||
read(data: GraphData): void;
|
||||
|
||||
/**
|
||||
* 更改源数据,根据新数据重新渲染视图
|
||||
* @param {GraphData | TreeGraphData} data 源数据
|
||||
* @param {boolean} 是否入栈,默认为true
|
||||
* @return {object} this
|
||||
*/
|
||||
changeData(data?: GraphData | TreeGraphData, stack?: boolean): Graph;
|
||||
|
||||
/**
|
||||
* 导出图数据
|
||||
* @return {GraphData} data
|
||||
*/
|
||||
save(): TreeGraphData | GraphData;
|
||||
|
||||
/**
|
||||
* 改变画布大小
|
||||
* @param {number} width 画布宽度
|
||||
* @param {number} height 画布高度
|
||||
* @return {Graph} this
|
||||
*/
|
||||
changeSize(width: number, height: number): Graph;
|
||||
|
||||
/**
|
||||
* 清理元素多个状态
|
||||
* @param {string|Item} item 元素id或元素实例
|
||||
* @param {string | string[]} states 状态
|
||||
*/
|
||||
clearItemStates(item: Item | string, states?: string | string[]): void;
|
||||
|
||||
/**
|
||||
* 设置各个节点样式,以及在各种状态下节点 keyShape 的样式。
|
||||
* 若是自定义节点切在各种状态下
|
||||
* graph.node(node => {
|
||||
* return {
|
||||
* {
|
||||
* type: 'rect',
|
||||
* label: node.id,
|
||||
* style: { fill: '#666' },
|
||||
* stateStyles: {
|
||||
* selected: { fill: 'blue' },
|
||||
* custom: { fill: 'green' }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* });
|
||||
* @param {function} nodeFn 指定每个节点样式
|
||||
*/
|
||||
node(nodeFn: (config: NodeConfig) => Partial<NodeConfig>): void;
|
||||
|
||||
/**
|
||||
* 设置各个边样式
|
||||
* @param {function} edgeFn 指定每个边的样式,用法同 node
|
||||
*/
|
||||
edge(edgeFn: (config: EdgeConfig) => Partial<EdgeConfig>): void;
|
||||
|
||||
/**
|
||||
* 设置每个 combo 的配置
|
||||
* @param comboFn 指定每个 combo 的配置
|
||||
*/
|
||||
combo(comboFn: (config: ComboConfig) => Partial<ComboConfig>): void;
|
||||
|
||||
/**
|
||||
* 平移画布到某点
|
||||
* @param {number} x 水平坐标
|
||||
* @param {number} y 垂直坐标
|
||||
*/
|
||||
moveTo(x: number, y: number): void;
|
||||
|
||||
/**
|
||||
* 根据对应规则查找单个元素
|
||||
* @param {ITEM_TYPE} type 元素类型(node | edge | group)
|
||||
* @param {(item: T, index: number) => T} fn 指定规则
|
||||
* @return {T} 元素实例
|
||||
*/
|
||||
find<T extends Item>(type: ITEM_TYPE, fn: (item: T, index?: number) => boolean): T | undefined;
|
||||
|
||||
/**
|
||||
* 查找所有满足规则的元素
|
||||
* @param {string} type 元素类型(node|edge)
|
||||
* @param {string} fn 指定规则
|
||||
* @return {array} 元素实例
|
||||
*/
|
||||
findAll<T extends Item>(type: ITEM_TYPE, fn: (item: T, index?: number) => boolean): T[];
|
||||
|
||||
/**
|
||||
* 查找所有处于指定状态的元素
|
||||
* @param {string} type 元素类型(node|edge)
|
||||
* @param {string} state z状态
|
||||
* @return {object} 元素实例
|
||||
*/
|
||||
findAllByState<T extends Item>(type: ITEM_TYPE, state: string): T[];
|
||||
|
||||
/**
|
||||
* 更换布局配置项
|
||||
* @param {object} cfg 新布局配置项
|
||||
* 若 cfg 含有 type 字段或为 String 类型,且与现有布局方法不同,则更换布局
|
||||
* 若 cfg 不包括 type ,则保持原有布局方法,仅更新布局配置项
|
||||
*/
|
||||
updateLayout(cfg: LayoutConfig): void;
|
||||
|
||||
/**
|
||||
* 重新以当前示例中配置的属性进行一次布局
|
||||
*/
|
||||
layout(): void;
|
||||
|
||||
/**
|
||||
* 收起指定的 Combo
|
||||
* @param comboId combo ID 或 combo 实例
|
||||
*/
|
||||
collapseCombo(combo: string | ICombo): void;
|
||||
|
||||
/**
|
||||
* 展开指定的 Combo
|
||||
* @param combo combo ID 或 combo 实例
|
||||
*/
|
||||
expandCombo(combo: string | ICombo): void;
|
||||
|
||||
/**
|
||||
* 展开或收缩指定的 Combo
|
||||
* @param comboId combo ID 或 combo 实例
|
||||
*/
|
||||
collapseExpandCombo(combo: string | ICombo): void;
|
||||
|
||||
/**
|
||||
* 根据节点的 bbox 更新所有 combos 的绘制,包括 combos 的位置和范围
|
||||
*/
|
||||
updateCombos(): void;
|
||||
|
||||
/**
|
||||
* 获取 undo stack
|
||||
*/
|
||||
getUndoStack(): Stack;
|
||||
|
||||
/**
|
||||
* 获取 redo stack
|
||||
*/
|
||||
getRedoStack(): Stack;
|
||||
|
||||
/**
|
||||
* 获取 undo 和 redo 栈的数据
|
||||
*/
|
||||
getStackData(): {
|
||||
undoStack: StackData[];
|
||||
redoStack: StackData[];
|
||||
};
|
||||
|
||||
/**
|
||||
* 清空 undo stack & redo stack
|
||||
*/
|
||||
clearStack(): void;
|
||||
|
||||
/**
|
||||
* 将操作类型和操作数据入栈
|
||||
* @param action 操作类型
|
||||
* @param data 入栈的数据
|
||||
* @param stackType 入栈的类型
|
||||
*/
|
||||
pushStack(action?: string, data?: unknown, stackType?: 'redo' | 'undo'): void;
|
||||
|
||||
/**
|
||||
* 根据节点的 bbox 更新 combo 及其祖先 combos 的绘制,包括 combos 的位置和范围
|
||||
* @param combo 需要更新的 combo
|
||||
*/
|
||||
updateCombo(combo: string | ICombo): void;
|
||||
|
||||
/**
|
||||
* 获取邻接矩阵
|
||||
*
|
||||
* @param {boolean} cache 是否使用缓存的
|
||||
* @param {boolean} directed 是否是有向图,默认取 graph.directed
|
||||
* @returns {Matrix} 邻接矩阵
|
||||
* @memberof IGraph
|
||||
*/
|
||||
getAdjMatrix(cache: boolean, directed?: boolean): Number | Object;
|
||||
|
||||
/**
|
||||
* 获取最短路径矩阵
|
||||
*
|
||||
* @param {boolean} cache 是否使用缓存的
|
||||
* @param {boolean} directed 是否是有向图,默认取 graph.directed
|
||||
* @returns {Matrix} 最短路径矩阵
|
||||
* @memberof IGraph
|
||||
*/
|
||||
getShortestPathMatrix(cache: boolean, directed?: boolean): Number | Object;
|
||||
|
||||
/**
|
||||
* 创建凸包或凹包轮廓
|
||||
* @param cfg HullCfg 轮廓配置项
|
||||
*/
|
||||
createHull(cfg: HullCfg): void;
|
||||
|
||||
/**
|
||||
* 获取当前 graph 中存在的包裹轮廓
|
||||
* @return {[key: string]: Hull} hullId 对应的 hull 实例
|
||||
*/
|
||||
getHulls(): Hull[];
|
||||
|
||||
/**
|
||||
* 根据 hullId 获取对应的 hull
|
||||
* @return Hull
|
||||
*/
|
||||
getHullById(hullId: string): Hull;
|
||||
|
||||
/**
|
||||
* 根据 hullId 删除 Hull
|
||||
*/
|
||||
removeHull(hull: Hull | string);
|
||||
|
||||
/**
|
||||
* 重新定义监听函数,复写参数类型
|
||||
*/
|
||||
on(eventName: string, callback: (e: IG6GraphEvent) => void, once?: boolean): this;
|
||||
/**
|
||||
* 销毁画布
|
||||
*/
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
export interface ITreeGraph extends IGraph {
|
||||
/**
|
||||
* 添加子树到对应 id 的节点
|
||||
* @param {TreeGraphData} data 子树数据模型
|
||||
* @param {string | Item} parent 子树的父节点id
|
||||
*/
|
||||
addChild(data: TreeGraphData, parent: string | Item): void;
|
||||
|
||||
/**
|
||||
* 更新源数据,差量更新子树
|
||||
* @param {TreeGraphData} data 子树数据模型
|
||||
* @param {string} parent 子树的父节点id
|
||||
*/
|
||||
updateChild(data: TreeGraphData, parent?: string): void;
|
||||
|
||||
/**
|
||||
* 删除子树
|
||||
* @param {string} id 子树根节点id
|
||||
*/
|
||||
removeChild(id: string): void;
|
||||
|
||||
/**
|
||||
* 根据id获取对应的源数据
|
||||
* @param {string} id 元素id
|
||||
* @param {TreeGraphData | undefined} parent 从哪个节点开始寻找,为空时从根节点开始查找
|
||||
* @return {TreeGraphData} 对应源数据
|
||||
*/
|
||||
findDataById(id: string, parent?: TreeGraphData | undefined): TreeGraphData | null;
|
||||
|
||||
/**
|
||||
* 布局动画接口,用于数据更新时做节点位置更新的动画
|
||||
* @param {TreeGraphData} data 更新的数据
|
||||
* @param {function} onFrame 定义节点位置更新时如何移动
|
||||
*/
|
||||
layoutAnimate(
|
||||
data: TreeGraphData,
|
||||
onFrame?: (
|
||||
item: Item,
|
||||
ratio: number,
|
||||
originAttrs?: ShapeStyle,
|
||||
data?: TreeGraphData,
|
||||
) => unknown,
|
||||
): void;
|
||||
|
||||
/**
|
||||
* 立即停止布局动画
|
||||
*/
|
||||
stopLayoutAnimate(): void;
|
||||
|
||||
/**
|
||||
* 是否在布局动画
|
||||
* @return {boolean} 是否有布局动画
|
||||
*/
|
||||
isLayoutAnimating(): boolean;
|
||||
}
|
367
packages/core/src/interface/item.ts
Normal file
367
packages/core/src/interface/item.ts
Normal file
@ -0,0 +1,367 @@
|
||||
/* eslint @typescript-eslint/no-use-before-define: 0 */
|
||||
import { IGroup } from '@antv/g-base/lib/interfaces';
|
||||
import { Point } from '@antv/g-base/lib/types';
|
||||
import { Group } from '@antv/g-canvas';
|
||||
import {
|
||||
IBBox,
|
||||
IPoint,
|
||||
IShapeBase,
|
||||
Item,
|
||||
ModelConfig,
|
||||
NodeConfig,
|
||||
EdgeConfig,
|
||||
ModelStyle,
|
||||
ShapeStyle,
|
||||
Indexable,
|
||||
ComboConfig,
|
||||
ITEM_TYPE,
|
||||
TreeGraphData
|
||||
} from '../types';
|
||||
|
||||
// item 的配置项
|
||||
export type IItemBaseConfig = Partial<{
|
||||
/**
|
||||
* id
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
type: 'item' | 'node' | 'edge' | 'combo' | 'vedge';
|
||||
|
||||
/**
|
||||
* data model
|
||||
*/
|
||||
model: ModelConfig;
|
||||
|
||||
/**
|
||||
* G Group
|
||||
*/
|
||||
group: IGroup;
|
||||
|
||||
/**
|
||||
* is open animate
|
||||
*/
|
||||
animate: boolean;
|
||||
|
||||
/**
|
||||
* visible - not group visible
|
||||
*/
|
||||
visible: boolean;
|
||||
|
||||
/**
|
||||
* locked - lock node
|
||||
*/
|
||||
locked: boolean;
|
||||
/**
|
||||
* capture event
|
||||
*/
|
||||
event: boolean;
|
||||
/**
|
||||
* key shape to calculate item's bbox
|
||||
*/
|
||||
keyShape: IShapeBase;
|
||||
/**
|
||||
* item's states, such as selected or active
|
||||
* @type Array
|
||||
*/
|
||||
states: string[];
|
||||
|
||||
/**
|
||||
* Item 的样式
|
||||
*/
|
||||
styles: ModelStyle;
|
||||
|
||||
source: string | Item;
|
||||
target: string | Item;
|
||||
|
||||
linkCenter: boolean;
|
||||
}> &
|
||||
Indexable<any>;
|
||||
|
||||
export interface IItemBase {
|
||||
_cfg: IItemBaseConfig | null;
|
||||
|
||||
destroyed: boolean;
|
||||
|
||||
isItem(): boolean;
|
||||
|
||||
getShapeStyleByName(name?: string): ShapeStyle;
|
||||
|
||||
/**
|
||||
* 获取当前元素的所有状态
|
||||
* @return {Array} 元素的所有状态
|
||||
*/
|
||||
getStates(): string[];
|
||||
|
||||
/**
|
||||
* 当前元素是否处于某状态
|
||||
* @param {String} state 状态名
|
||||
* @return {Boolean} 是否处于某状态
|
||||
*/
|
||||
hasState(state: string): boolean;
|
||||
|
||||
getStateStyle(state: string): ShapeStyle;
|
||||
|
||||
getOriginStyle(): ShapeStyle;
|
||||
|
||||
getCurrentStatesStyle(): ShapeStyle;
|
||||
|
||||
/**
|
||||
* 更改元素状态, visible 不属于这个范畴
|
||||
* @internal 仅提供内部类 graph 使用
|
||||
* @param {String} state 状态名
|
||||
* @param {Boolean} value 节点状态值
|
||||
*/
|
||||
setState(state: string, value: string | boolean): void;
|
||||
|
||||
clearStates(states?: string | string[]): void;
|
||||
|
||||
/**
|
||||
* 节点的图形容器
|
||||
* @return {G.Group} 图形容器
|
||||
*/
|
||||
getContainer(): Group;
|
||||
|
||||
/**
|
||||
* 节点的关键形状,用于计算节点大小,连线截距等
|
||||
* @return {IShapeBase} 关键形状
|
||||
*/
|
||||
getKeyShape(): IShapeBase;
|
||||
|
||||
/**
|
||||
* 节点 / 边 / Combo 的数据模型
|
||||
* @return {Object} 数据模型
|
||||
*/
|
||||
getModel(): NodeConfig | EdgeConfig | ComboConfig | TreeGraphData;
|
||||
|
||||
/**
|
||||
* 节点类型
|
||||
* @return {string} 节点的类型
|
||||
*/
|
||||
getType(): ITEM_TYPE;
|
||||
|
||||
/**
|
||||
* 获取 Item 的ID
|
||||
*/
|
||||
getID(): string;
|
||||
|
||||
getShapeCfg(model: ModelConfig): ModelConfig;
|
||||
|
||||
/**
|
||||
* 刷新一般用于处理几种情况
|
||||
* 1. item model 在外部被改变
|
||||
* 2. 边的节点位置发生改变,需要重新计算边
|
||||
*
|
||||
* 因为数据从外部被修改无法判断一些属性是否被修改,直接走位置和 shape 的更新
|
||||
*/
|
||||
refresh(): void;
|
||||
|
||||
/**
|
||||
* 将更新应用到 model 上,刷新属性
|
||||
* @internal 仅提供给 Graph 使用,外部直接调用 graph.update 接口
|
||||
* @param {Object} cfg 配置项,可以是增量信息
|
||||
* @param {boolean} onlyMove 是否仅移动,只有 node 和 combo 可能是 true
|
||||
*/
|
||||
update(cfg: ModelConfig, onlyMove?: boolean): void;
|
||||
|
||||
/**
|
||||
* 更新元素内容,样式
|
||||
*/
|
||||
updateShape(): void;
|
||||
|
||||
/**
|
||||
* 更新位置,避免整体重绘
|
||||
* @param {object} cfg 待更新数据
|
||||
*/
|
||||
updatePosition(cfg: Point): void;
|
||||
|
||||
/**
|
||||
* 绘制元素
|
||||
*/
|
||||
draw(): void;
|
||||
|
||||
/**
|
||||
* 获取 item 的包围盒,这个包围盒是相对于 item 自己,不会将 matrix 计算在内
|
||||
*/
|
||||
getBBox(): IBBox;
|
||||
|
||||
/**
|
||||
* 获取 item 相对于画布的包围盒,会将从顶层到当前元素的 matrix 都计算在内
|
||||
*/
|
||||
getCanvasBBox(): IBBox;
|
||||
|
||||
/**
|
||||
* 将元素放到最前面
|
||||
*/
|
||||
toFront(): void;
|
||||
|
||||
/**
|
||||
* 将元素放到最后面
|
||||
*/
|
||||
toBack(): void;
|
||||
|
||||
/**
|
||||
* 显示元素
|
||||
*/
|
||||
show(): void;
|
||||
|
||||
/**
|
||||
* 隐藏元素
|
||||
*/
|
||||
hide(): void;
|
||||
|
||||
/**
|
||||
* 更改是否显示
|
||||
* @param {Boolean} visible 是否显示
|
||||
*/
|
||||
changeVisibility(visible: boolean): void;
|
||||
|
||||
/**
|
||||
* 是否捕获及触发该元素的交互事件
|
||||
* @param {Boolean} enable 标识位
|
||||
*/
|
||||
enableCapture(enable: boolean): void;
|
||||
|
||||
isVisible(): boolean;
|
||||
|
||||
isOnlyMove(cfg: ModelConfig): boolean;
|
||||
|
||||
get<T = any>(key: string): T;
|
||||
set<T = any>(key: string, value: T): void;
|
||||
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
export interface IEdge extends IItemBase {
|
||||
setSource(source: INode | ICombo): void;
|
||||
setTarget(target: INode | ICombo): void;
|
||||
getSource(): INode | ICombo;
|
||||
getTarget(): INode | ICombo;
|
||||
}
|
||||
|
||||
export interface INode extends IItemBase {
|
||||
/**
|
||||
* 获取从节点关联的所有边
|
||||
* @return {Array} 边的集合
|
||||
*/
|
||||
getEdges(): IEdge[];
|
||||
|
||||
/**
|
||||
* 获取引入节点的边 target == this
|
||||
* @return {Array} 边的集合
|
||||
*/
|
||||
getInEdges(): IEdge[];
|
||||
|
||||
/**
|
||||
* 获取从节点引出的边 source == this
|
||||
* @return {Array} 边的集合
|
||||
*/
|
||||
getOutEdges(): IEdge[];
|
||||
|
||||
/**
|
||||
* 根据锚点的索引获取连接点
|
||||
* @param {Number} index 索引
|
||||
* @return {Object} 连接点 {x,y}
|
||||
*/
|
||||
getLinkPointByAnchor(index: number): IPoint;
|
||||
|
||||
/**
|
||||
* 获取连接点
|
||||
* @param {Object} point 节点外面的一个点,用于计算交点、最近的锚点
|
||||
* @return {Object} 连接点 {x,y}
|
||||
*/
|
||||
getLinkPoint(point: IPoint): IPoint | null;
|
||||
|
||||
/**
|
||||
* 添加边
|
||||
* @param {Edge} edge 边
|
||||
*/
|
||||
addEdge(edge: IEdge): void;
|
||||
|
||||
/**
|
||||
* 移除边
|
||||
* @param {Edge} edge 边
|
||||
*/
|
||||
removeEdge(edge: IEdge): void;
|
||||
|
||||
/**
|
||||
* 获取锚点的定义
|
||||
* @return {array} anchorPoints, {x,y,...cfg}
|
||||
*/
|
||||
getAnchorPoints(): IPoint[] | number[][];
|
||||
|
||||
hasLocked(): boolean;
|
||||
|
||||
lock(): void;
|
||||
|
||||
unlock(): void;
|
||||
|
||||
/**
|
||||
* 获取节点所有的邻居节点
|
||||
*
|
||||
* @returns {INode[]}
|
||||
* @memberof INode
|
||||
*/
|
||||
getNeighbors(type?: 'source' | 'target' | undefined): INode[];
|
||||
}
|
||||
|
||||
export interface ICombo extends INode {
|
||||
/**
|
||||
* 获取 Combo 中所有的子元素,包括 Combo、Node 及 Edge
|
||||
*/
|
||||
getChildren: () => { nodes: INode[]; combos: ICombo[] };
|
||||
|
||||
/**
|
||||
* 获取 Combo 中所有节点
|
||||
*/
|
||||
getNodes: () => INode[];
|
||||
|
||||
/**
|
||||
* 获取 Combo 中所有子 combo
|
||||
*/
|
||||
getCombos: () => INode[];
|
||||
|
||||
/**
|
||||
* 向 Combo 中增加 combo
|
||||
* @param item 节点或 combo 的 Item 实例
|
||||
* @return boolean 添加成功返回 true,否则返回 false
|
||||
*/
|
||||
addChild: (item: INode | ICombo) => boolean;
|
||||
|
||||
/**
|
||||
* 向 Combo 中增加 combo
|
||||
* @param combo Combo 实例
|
||||
* @return boolean 添加成功返回 true,否则返回 false
|
||||
*/
|
||||
addCombo: (combo: ICombo) => boolean;
|
||||
|
||||
/**
|
||||
* 向 Combo 中添加节点
|
||||
* @param node 节点实例
|
||||
* @return boolean 添加成功返回 true,否则返回 false
|
||||
*/
|
||||
addNode: (node: string | INode) => boolean;
|
||||
|
||||
/**
|
||||
* 从 Combo 中移除子元素
|
||||
* @param item Combo 或 Node 实例
|
||||
* @return boolean 添加成功返回 true,否则返回 false
|
||||
*/
|
||||
removeChild: (item: ICombo | INode) => boolean;
|
||||
|
||||
/**
|
||||
* 从 Combo 中移除指定的 combo
|
||||
* @param combo Combo 实例
|
||||
* @return boolean 移除成功返回 true,否则返回 false
|
||||
*/
|
||||
removeCombo: (combo: ICombo) => boolean;
|
||||
|
||||
/**
|
||||
* 向 Combo 中移除指定的节点
|
||||
* @param node 节点实例
|
||||
* @return boolean 移除成功返回 true,否则返回 false
|
||||
*/
|
||||
removeNode: (node: string | INode) => boolean;
|
||||
}
|
91
packages/core/src/interface/shape.ts
Normal file
91
packages/core/src/interface/shape.ts
Normal file
@ -0,0 +1,91 @@
|
||||
import { Group as GGroup } from '@antv/g-canvas';
|
||||
import { IShape } from '@antv/g-canvas/lib/interfaces';
|
||||
import { IPoint, Item, LabelStyle, ModelConfig, ShapeStyle } from '../types';
|
||||
|
||||
export type ILabelConfig = Partial<{
|
||||
position: string;
|
||||
offset: number;
|
||||
refX: number;
|
||||
refY: number;
|
||||
autoRotate: boolean;
|
||||
style: LabelStyle;
|
||||
}>;
|
||||
|
||||
export type ShapeDefine = string | ((cfg: ModelConfig) => string);
|
||||
|
||||
export type ShapeOptions = Partial<{
|
||||
options: ModelConfig;
|
||||
/**
|
||||
* 形状的类型,例如 circle,ellipse,polyline...
|
||||
*/
|
||||
type: string;
|
||||
|
||||
itemType: string;
|
||||
shapeType: string;
|
||||
labelPosition: string;
|
||||
labelAutoRotate: boolean;
|
||||
|
||||
jsx: ShapeDefine;
|
||||
[key: string]: any;
|
||||
|
||||
/**
|
||||
* 绘制
|
||||
*/
|
||||
draw(cfg?: ModelConfig, group?: GGroup): IShape;
|
||||
|
||||
getCustomConfig(cfg: ModelConfig): ModelConfig;
|
||||
drawShape(cfg?: ModelConfig, group?: GGroup): IShape;
|
||||
drawLabel(cfg: ModelConfig, group: GGroup): IShape;
|
||||
getLabelStyleByPosition(cfg: ModelConfig, labelCfg: ILabelConfig, group?: GGroup): LabelStyle;
|
||||
getLabelStyle(cfg: ModelConfig, labelCfg: ILabelConfig, group: GGroup): LabelStyle;
|
||||
|
||||
/**
|
||||
* 绘制完成后的操作,便于用户继承现有的节点、边
|
||||
*/
|
||||
afterDraw(cfg?: ModelConfig, group?: GGroup, rst?: IShape): void;
|
||||
|
||||
afterUpdate(cfg?: ModelConfig, item?: Item): void;
|
||||
|
||||
/**
|
||||
* 设置节点、边状态
|
||||
*/
|
||||
setState(name?: string, value?: string | boolean, item?: Item): void;
|
||||
|
||||
/**
|
||||
* 获取控制点
|
||||
* @param {Object} cfg 节点、边的配置项
|
||||
* @return {Array|null} 控制点的数组,如果为 null,则没有控制点
|
||||
*/
|
||||
getControlPoints(cfg: ModelConfig): IPoint[] | undefined;
|
||||
|
||||
/**
|
||||
* 获取控制点
|
||||
* @param {Object} cfg 节点、边的配置项
|
||||
* @return {Array|null} 控制点的数组,如果为 null,则没有控制点
|
||||
*/
|
||||
getAnchorPoints(cfg?: ModelConfig): number[][] | undefined;
|
||||
|
||||
// 如果没定义 update 方法,每次都调用 draw 方法
|
||||
update(cfg: ModelConfig, item: Item): void;
|
||||
|
||||
// 获取节点的大小,只对节点起效
|
||||
getSize: (cfg: ModelConfig) => number[];
|
||||
|
||||
// 获取路径
|
||||
// getPath: (cfg?: ModelConfig) => Array<Array<string | number>>
|
||||
// getPath: (points?: Point[]) => Array<Array<string | number>>
|
||||
// getPath: (points: Point[], routeCfg) => Array<Array<string | number>> | string
|
||||
// getPath: (...args: Point[] | object[] | ModelConfig[]) => Array<Array<string | number>> | string
|
||||
|
||||
// 获取文本对齐方式,只对边起效
|
||||
_getTextAlign: (labelPosition: string, angle: number) => string;
|
||||
|
||||
/**
|
||||
* @internal 处理需要重计算点和边的情况
|
||||
* @param {Object} cfg 边的配置项
|
||||
* @return {Object} 边的配置项
|
||||
*/
|
||||
getPathPoints: (cfg: ModelConfig) => ModelConfig;
|
||||
|
||||
// [key: string]: (...args: string[] | number[] | object[] | ModelConfig[]) => unknown
|
||||
}>;
|
247
packages/core/src/item/combo.ts
Normal file
247
packages/core/src/item/combo.ts
Normal file
@ -0,0 +1,247 @@
|
||||
import { ICombo, INode, IItemBaseConfig } from '../interface/item';
|
||||
import { Group } from '@antv/g-canvas';
|
||||
import Node from './node';
|
||||
import { ComboConfig, IBBox, IShapeBase } from '../types';
|
||||
import Global from '../global';
|
||||
import { getBBox } from '../util/graphic';
|
||||
import { isNumber } from '@antv/util';
|
||||
|
||||
const CACHE_BBOX = 'bboxCache';
|
||||
const CACHE_CANVAS_BBOX = 'bboxCanvasCache';
|
||||
const CACHE_SIZE = 'sizeCache';
|
||||
const CACHE_ANCHOR_POINTS = 'anchorPointsCache';
|
||||
|
||||
export default class Combo extends Node implements ICombo {
|
||||
public getDefaultCfg() {
|
||||
return {
|
||||
type: 'combo',
|
||||
nodes: [],
|
||||
edges: [],
|
||||
combos: [],
|
||||
};
|
||||
}
|
||||
|
||||
public getShapeCfg(model: ComboConfig): ComboConfig {
|
||||
const styles = this.get('styles');
|
||||
const bbox = this.get('bbox');
|
||||
if (styles && bbox) {
|
||||
// merge graph的item样式与数据模型中的样式
|
||||
const newModel = model;
|
||||
const size = {
|
||||
r: Math.hypot(bbox.height, bbox.width) / 2 || Global.defaultCombo.size[0] / 2,
|
||||
width: bbox.width || Global.defaultCombo.size[0],
|
||||
height: bbox.height || Global.defaultCombo.size[1],
|
||||
};
|
||||
newModel.style = { ...styles, ...model.style, ...size };
|
||||
const padding = model.padding || Global.defaultCombo.padding;
|
||||
if (isNumber(padding)) {
|
||||
size.r += padding;
|
||||
size.width += padding * 2;
|
||||
size.height += padding * 2;
|
||||
} else {
|
||||
size.r += padding[0];
|
||||
size.width += padding[1] + padding[3] || padding[1] * 2;
|
||||
size.height += padding[0] + padding[2] || padding[0] * 2;
|
||||
}
|
||||
this.set(CACHE_SIZE, size);
|
||||
return newModel;
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 keyshape 计算包围盒
|
||||
*/
|
||||
public calculateCanvasBBox(): IBBox {
|
||||
const keyShape: IShapeBase = this.get('keyShape');
|
||||
const group: Group = this.get('group');
|
||||
// 因为 group 可能会移动,所以必须通过父元素计算才能计算出正确的包围盒
|
||||
const bbox = getBBox(keyShape, group);
|
||||
bbox.x = bbox.minX;
|
||||
bbox.y = bbox.minY;
|
||||
bbox.centerX = (bbox.minX + bbox.maxX) / 2;
|
||||
bbox.centerY = (bbox.minY + bbox.maxY) / 2;
|
||||
const cacheSize = this.get(CACHE_SIZE);
|
||||
if (cacheSize) {
|
||||
const type: string = keyShape.get('type');
|
||||
if (type === 'circle') {
|
||||
bbox.width = cacheSize.r * 2;
|
||||
bbox.height = cacheSize.r * 2;
|
||||
} else {
|
||||
bbox.width = cacheSize.width;
|
||||
bbox.height = cacheSize.height;
|
||||
}
|
||||
bbox.minX = bbox.centerX - bbox.width / 2;
|
||||
bbox.minY = bbox.centerY - bbox.height / 2;
|
||||
bbox.maxX = bbox.centerX + bbox.width / 2;
|
||||
bbox.maxY = bbox.centerY + bbox.height / 2;
|
||||
} else {
|
||||
bbox.width = bbox.maxX - bbox.minX;
|
||||
bbox.height = bbox.maxY - bbox.minY;
|
||||
bbox.centerX = (bbox.minX + bbox.maxX) / 2;
|
||||
bbox.centerY = (bbox.minY + bbox.maxY) / 2;
|
||||
}
|
||||
return bbox;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Combo 中所有的子元素,包括 Combo、Node 及 Edge
|
||||
*/
|
||||
public getChildren(): { nodes: INode[]; combos: ICombo[] } {
|
||||
const self = this;
|
||||
return {
|
||||
nodes: self.getNodes(),
|
||||
combos: self.getCombos(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Combo 中所有子节点
|
||||
*/
|
||||
getNodes(): INode[] {
|
||||
const self = this;
|
||||
return self.get('nodes');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Combo 中所有子 combo
|
||||
*/
|
||||
getCombos(): ICombo[] {
|
||||
const self = this;
|
||||
return self.get('combos');
|
||||
}
|
||||
|
||||
/**
|
||||
* 向 Combo 中增加子 combo 或 node
|
||||
* @param item Combo 或节点实例
|
||||
* @return boolean 添加成功返回 true,否则返回 false
|
||||
*/
|
||||
addChild(item: ICombo | INode): boolean {
|
||||
const self = this;
|
||||
const itemType = item.getType();
|
||||
switch (itemType) {
|
||||
case 'node':
|
||||
self.addNode(item);
|
||||
break;
|
||||
case 'combo':
|
||||
self.addCombo(item as ICombo);
|
||||
break;
|
||||
default:
|
||||
console.warn('Only node or combo items are allowed to be added into a combo');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 向 Combo 中增加 combo
|
||||
* @param combo Combo 实例
|
||||
* @return boolean 添加成功返回 true,否则返回 false
|
||||
*/
|
||||
addCombo(combo: ICombo): boolean {
|
||||
const self = this;
|
||||
self.get('combos').push(combo);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 向 Combo 中添加节点
|
||||
* @param node 节点实例
|
||||
* @return boolean 添加成功返回 true,否则返回 false
|
||||
*/
|
||||
addNode(node: string | INode): boolean {
|
||||
const self = this;
|
||||
self.get('nodes').push(node);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 向 Combo 中增加子 combo 或 node
|
||||
* @param item Combo 或节点实例
|
||||
* @return boolean 添加成功返回 true,否则返回 false
|
||||
*/
|
||||
removeChild(item: ICombo | INode): boolean {
|
||||
const self = this;
|
||||
const itemType = item.getType();
|
||||
switch (itemType) {
|
||||
case 'node':
|
||||
self.removeNode(item);
|
||||
break;
|
||||
case 'combo':
|
||||
self.removeCombo(item as ICombo);
|
||||
break;
|
||||
default:
|
||||
console.warn('Only node or combo items are allowed to be added into a combo');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Combo 中移除指定的 combo
|
||||
* @param combo Combo 实例
|
||||
* @return boolean 移除成功返回 true,否则返回 false
|
||||
*/
|
||||
removeCombo(combo: ICombo): boolean {
|
||||
if (!combo) return;
|
||||
const combos = this.getCombos();
|
||||
const index = combos.indexOf(combo);
|
||||
if (index > -1) {
|
||||
combos.splice(index, 1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 向 Combo 中移除指定的节点
|
||||
* @param node 节点实例
|
||||
* @return boolean 移除成功返回 true,否则返回 false
|
||||
*/
|
||||
removeNode(node: INode): boolean {
|
||||
if (!node) return;
|
||||
const nodes = this.getNodes();
|
||||
const index = nodes.indexOf(node);
|
||||
if (index > -1) {
|
||||
nodes.splice(index, 1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public isOnlyMove(cfg?: any): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 item 的包围盒,这个包围盒是相对于 item 自己,不会将 matrix 计算在内
|
||||
* @return {Object} 包含 x,y,width,height, centerX, centerY
|
||||
*/
|
||||
public getBBox(): IBBox {
|
||||
this.set(CACHE_CANVAS_BBOX, null);
|
||||
const bbox: IBBox = this.calculateCanvasBBox();
|
||||
return bbox;
|
||||
}
|
||||
|
||||
public clearCache() {
|
||||
this.set(CACHE_BBOX, null); // 清理缓存的 bbox
|
||||
this.set(CACHE_CANVAS_BBOX, null);
|
||||
this.set(CACHE_ANCHOR_POINTS, null);
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
if (!this.destroyed) {
|
||||
const animate = this.get('animate');
|
||||
const group: Group = this.get('group');
|
||||
if (animate) {
|
||||
group.stopAnimate();
|
||||
}
|
||||
this.clearCache();
|
||||
this.set(CACHE_SIZE, null);
|
||||
this.set('bbox', null);
|
||||
group.remove();
|
||||
(this._cfg as IItemBaseConfig | null) = null;
|
||||
this.destroyed = true;
|
||||
}
|
||||
}
|
||||
}
|
242
packages/core/src/item/edge.ts
Normal file
242
packages/core/src/item/edge.ts
Normal file
@ -0,0 +1,242 @@
|
||||
import { isString, isPlainObject, isNil, mix } from '@antv/util';
|
||||
import { IEdge, INode, ICombo } from '../interface/item';
|
||||
import { EdgeConfig, IPoint, NodeConfig, SourceTarget, Indexable } from '../types';
|
||||
import Item from './item';
|
||||
import Node from './node';
|
||||
|
||||
const END_MAP: Indexable<string> = { source: 'start', target: 'end' };
|
||||
const ITEM_NAME_SUFFIX = 'Node'; // 端点的后缀,如 sourceNode, targetNode
|
||||
const POINT_NAME_SUFFIX = 'Point'; // 起点或者结束点的后缀,如 startPoint, endPoint
|
||||
const ANCHOR_NAME_SUFFIX = 'Anchor';
|
||||
|
||||
export default class Edge extends Item implements IEdge {
|
||||
protected getDefaultCfg() {
|
||||
return {
|
||||
type: 'edge',
|
||||
sourceNode: null,
|
||||
targetNode: null,
|
||||
startPoint: null,
|
||||
endPoint: null,
|
||||
linkCenter: false,
|
||||
};
|
||||
}
|
||||
|
||||
private setEnd(name: SourceTarget, value: INode) {
|
||||
const pointName = END_MAP[name] + POINT_NAME_SUFFIX;
|
||||
const itemName = name + ITEM_NAME_SUFFIX;
|
||||
const preItem = this.get(itemName);
|
||||
if (preItem && !preItem.destroyed) {
|
||||
// 如果之前存在节点,则移除掉边
|
||||
preItem.removeEdge(this);
|
||||
}
|
||||
|
||||
if (isPlainObject(value)) {
|
||||
// 如果设置成具体的点,则清理节点
|
||||
this.set(pointName, value);
|
||||
this.set(itemName, null);
|
||||
} else {
|
||||
value!.addEdge(this);
|
||||
this.set(itemName, value);
|
||||
this.set(pointName, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接点的坐标
|
||||
* @param name source | target
|
||||
* @param model 边的数据模型
|
||||
* @param controlPoints 控制点
|
||||
*/
|
||||
private getLinkPoint(name: SourceTarget, model: EdgeConfig, controlPoints: IPoint[]): IPoint {
|
||||
const pointName = END_MAP[name] + POINT_NAME_SUFFIX;
|
||||
const itemName = name + ITEM_NAME_SUFFIX;
|
||||
let point = this.get(pointName);
|
||||
if (!point) {
|
||||
const item = this.get(itemName);
|
||||
const anchorName = name + ANCHOR_NAME_SUFFIX;
|
||||
const prePoint = this.getPrePoint(name, controlPoints);
|
||||
const anchorIndex = model[anchorName];
|
||||
if (!isNil(anchorIndex)) {
|
||||
// 如果有锚点,则使用锚点索引获取连接点
|
||||
point = item.getLinkPointByAnchor(anchorIndex);
|
||||
}
|
||||
// 如果锚点没有对应的点或者没有锚点,则直接计算连接点
|
||||
point = point || item.getLinkPoint(prePoint);
|
||||
if (!isNil(point.index)) {
|
||||
this.set(`${name}AnchorIndex`, point.index);
|
||||
}
|
||||
}
|
||||
return point;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取同端点进行连接的点,计算交汇点
|
||||
* @param name
|
||||
* @param controlPoints
|
||||
*/
|
||||
private getPrePoint(name: SourceTarget, controlPoints: IPoint[]): NodeConfig | IPoint {
|
||||
if (controlPoints && controlPoints.length) {
|
||||
const index = name === 'source' ? 0 : controlPoints.length - 1;
|
||||
return controlPoints[index];
|
||||
}
|
||||
const oppositeName = name === 'source' ? 'target' : 'source'; // 取另一个节点的位置
|
||||
return this.getEndPoint(oppositeName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取端点的位置
|
||||
* @param name
|
||||
*/
|
||||
private getEndPoint(name: SourceTarget): NodeConfig | IPoint {
|
||||
const itemName = name + ITEM_NAME_SUFFIX;
|
||||
const pointName = END_MAP[name] + POINT_NAME_SUFFIX;
|
||||
const item = this.get(itemName);
|
||||
// 如果有端点,直接使用 model
|
||||
if (item) {
|
||||
return item.get('model');
|
||||
} // 否则直接使用点
|
||||
return this.get(pointName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过端点的中心获取控制点
|
||||
* @param model
|
||||
*/
|
||||
private getControlPointsByCenter(model: EdgeConfig) {
|
||||
const sourcePoint = this.getEndPoint('source');
|
||||
const targetPoint = this.getEndPoint('target');
|
||||
const shapeFactory = this.get('shapeFactory');
|
||||
const type = model.type;
|
||||
return shapeFactory.getControlPoints(type, {
|
||||
startPoint: sourcePoint,
|
||||
endPoint: targetPoint,
|
||||
});
|
||||
}
|
||||
|
||||
private getEndCenter(name: SourceTarget): IPoint {
|
||||
const itemName = name + ITEM_NAME_SUFFIX;
|
||||
const pointName = END_MAP[name] + POINT_NAME_SUFFIX;
|
||||
const item = this.get(itemName);
|
||||
// 如果有端点,直接使用 model
|
||||
if (item) {
|
||||
const bbox = item.getBBox();
|
||||
return {
|
||||
x: bbox.centerX,
|
||||
y: bbox.centerY,
|
||||
};
|
||||
} // 否则直接使用点
|
||||
return this.get(pointName);
|
||||
}
|
||||
|
||||
protected init() {
|
||||
super.init();
|
||||
// 初始化两个端点
|
||||
this.setSource(this.get('source'));
|
||||
this.setTarget(this.get('target'));
|
||||
}
|
||||
|
||||
public getShapeCfg(model: EdgeConfig): EdgeConfig {
|
||||
const self = this;
|
||||
const linkCenter: boolean = self.get('linkCenter'); // 如果连接到中心,忽视锚点、忽视控制点
|
||||
const cfg = super.getShapeCfg(model) as EdgeConfig;
|
||||
|
||||
if (linkCenter) {
|
||||
cfg.startPoint = self.getEndCenter('source');
|
||||
cfg.endPoint = self.getEndCenter('target');
|
||||
} else {
|
||||
const controlPoints = cfg.controlPoints || self.getControlPointsByCenter(cfg);
|
||||
cfg.startPoint = self.getLinkPoint('source', model, controlPoints);
|
||||
cfg.endPoint = self.getLinkPoint('target', model, controlPoints);
|
||||
}
|
||||
cfg.sourceNode = self.get('sourceNode');
|
||||
cfg.targetNode = self.get('targetNode');
|
||||
return cfg;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取边的数据模型
|
||||
*/
|
||||
public getModel(): EdgeConfig {
|
||||
const out: EdgeConfig = this.get('model');
|
||||
const sourceItem = this.get(`source${ITEM_NAME_SUFFIX}`);
|
||||
const targetItem = this.get(`target${ITEM_NAME_SUFFIX}`);
|
||||
if (sourceItem) {
|
||||
delete out[`source${ITEM_NAME_SUFFIX}`];
|
||||
} else {
|
||||
out.source = this.get(`start${POINT_NAME_SUFFIX}`);
|
||||
}
|
||||
|
||||
if (targetItem) {
|
||||
delete out[`target${ITEM_NAME_SUFFIX}`];
|
||||
} else {
|
||||
out.target = this.get(`end${POINT_NAME_SUFFIX}`);
|
||||
}
|
||||
|
||||
if (!isString(out.source) && !isPlainObject(out.source)) {
|
||||
out.source = (out.source as any).getID();
|
||||
}
|
||||
|
||||
if (!isString(out.target) && !isPlainObject(out.target)) {
|
||||
out.target = (out.target as any).getID();
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
public setSource(source: INode | ICombo) {
|
||||
this.setEnd('source', source);
|
||||
this.set('source', source);
|
||||
}
|
||||
|
||||
public setTarget(target: INode | ICombo) {
|
||||
this.setEnd('target', target);
|
||||
this.set('target', target);
|
||||
}
|
||||
|
||||
public getSource(): INode | ICombo {
|
||||
return this.get('source');
|
||||
}
|
||||
|
||||
public getTarget(): INode | ICombo {
|
||||
return this.get('target');
|
||||
}
|
||||
|
||||
public updatePosition() { }
|
||||
|
||||
/**
|
||||
* 边不需要重计算容器位置,直接重新计算 path 位置
|
||||
* @param {object} cfg 待更新数据
|
||||
*/
|
||||
public update(cfg: EdgeConfig, onlyMove: boolean = false) {
|
||||
const model: EdgeConfig = this.get('model');
|
||||
const oriVisible = model.visible;
|
||||
const cfgVisible = cfg.visible;
|
||||
if (oriVisible !== cfgVisible && cfgVisible !== undefined) this.changeVisibility(cfgVisible);
|
||||
|
||||
const styles = this.get('styles');
|
||||
if (cfg.stateStyles) {
|
||||
// 更新 item 时更新 this.get('styles') 中的值
|
||||
const { stateStyles } = cfg;
|
||||
mix(styles, stateStyles);
|
||||
delete cfg.stateStyles;
|
||||
}
|
||||
|
||||
Object.assign(model, cfg);
|
||||
this.updateShape();
|
||||
this.afterUpdate();
|
||||
this.clearCache();
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
const sourceItem: Node = this.get(`source${ITEM_NAME_SUFFIX}`);
|
||||
const targetItem: Node = this.get(`target${ITEM_NAME_SUFFIX}`);
|
||||
if (sourceItem && !sourceItem.destroyed) {
|
||||
sourceItem.removeEdge(this);
|
||||
}
|
||||
|
||||
if (targetItem && !targetItem.destroyed) {
|
||||
targetItem.removeEdge(this);
|
||||
}
|
||||
super.destroy();
|
||||
}
|
||||
}
|
277
packages/core/src/item/hull.ts
Normal file
277
packages/core/src/item/hull.ts
Normal file
@ -0,0 +1,277 @@
|
||||
import { IGroup } from '@antv/g-base';
|
||||
import { deepMix, isString } from '@antv/util';
|
||||
import { parsePathString } from '@antv/path-util';
|
||||
import { Item, BubblesetCfg, HullCfg } from '../types';
|
||||
import { pathToPoints, getClosedSpline, roundedHull, paddedHull } from '../util/path';
|
||||
|
||||
import { isPolygonsIntersect } from '../util/math';
|
||||
import { IGraph } from '../interface/graph';
|
||||
|
||||
import { genConvexHull } from '../shape/hull/convexHull';
|
||||
import { genBubbleSet } from '../shape/hull/bubbleset';
|
||||
|
||||
/**
|
||||
* 用于包裹内部的成员的轮廓。
|
||||
* convex hull(凸包):http://geomalgorithms.com/a10-_hull-1.html#Monotone%20Chain
|
||||
* bubble: 使用 bubbleset算法,refer: http://vialab.science.uoit.ca/wp-content/papercite-data/pdf/col2009c.pdf
|
||||
* 通过配置 padding 可以调节包裹轮廓对节点的松紧程度
|
||||
*/
|
||||
export default class Hull {
|
||||
id: string;
|
||||
|
||||
graph: IGraph;
|
||||
|
||||
cfg: any;
|
||||
|
||||
path: any[][];
|
||||
|
||||
group: IGroup;
|
||||
|
||||
members: Item[];
|
||||
|
||||
nonMembers: Item[];
|
||||
|
||||
padding: number;
|
||||
|
||||
bubbleCfg: Partial<BubblesetCfg>;
|
||||
|
||||
type: string;
|
||||
|
||||
constructor(graph: IGraph, cfg: HullCfg) {
|
||||
this.cfg = deepMix(this.getDefaultCfg(), cfg);
|
||||
this.graph = graph;
|
||||
this.id = this.cfg.id;
|
||||
this.group = this.cfg.group;
|
||||
this.members = this.cfg.members.map((item) => (isString(item) ? graph.findById(item) : item));
|
||||
this.nonMembers = this.cfg.nonMembers.map((item) =>
|
||||
isString(item) ? graph.findById(item) : item,
|
||||
);
|
||||
this.setPadding();
|
||||
this.setType();
|
||||
|
||||
this.path = this.calcPath(this.members, this.nonMembers);
|
||||
this.render();
|
||||
}
|
||||
|
||||
public getDefaultCfg(): HullCfg {
|
||||
return {
|
||||
id: 'g6-hull',
|
||||
type: 'round-convex', // 'round-convex' /'smooth-convex' / 'bubble'
|
||||
members: [],
|
||||
nonMembers: [],
|
||||
style: {
|
||||
fill: 'lightblue',
|
||||
stroke: 'blue',
|
||||
opacity: 0.2,
|
||||
},
|
||||
padding: 10,
|
||||
};
|
||||
}
|
||||
|
||||
setPadding() {
|
||||
const nodeSize = this.members.length && this.members[0].getKeyShape().getCanvasBBox().width / 2;
|
||||
this.padding = this.cfg.padding > 0 ? this.cfg.padding + nodeSize : 10 + nodeSize;
|
||||
this.cfg.bubbleCfg = {
|
||||
nodeR0: this.padding - nodeSize,
|
||||
nodeR1: this.padding - nodeSize,
|
||||
morphBuffer: this.padding - nodeSize,
|
||||
};
|
||||
}
|
||||
|
||||
setType() {
|
||||
this.type = this.cfg.type;
|
||||
if (this.members.length < 3) {
|
||||
this.type = 'round-convex';
|
||||
}
|
||||
if (this.type !== 'round-convex' && this.type !== 'smooth-convex' && this.type !== 'bubble') {
|
||||
console.warn(
|
||||
'The hull type should be either round-convex, smooth-convex or bubble, round-convex is used by default.',
|
||||
);
|
||||
this.type = 'round-convex';
|
||||
}
|
||||
}
|
||||
|
||||
calcPath(members: Item[], nonMembers: Item[]) {
|
||||
let contour, path, hull;
|
||||
switch (this.type) {
|
||||
case 'round-convex':
|
||||
contour = genConvexHull(members);
|
||||
hull = roundedHull(
|
||||
contour.map((p) => [p.x, p.y]),
|
||||
this.padding,
|
||||
);
|
||||
path = parsePathString(hull);
|
||||
break;
|
||||
case 'smooth-convex':
|
||||
contour = genConvexHull(members);
|
||||
hull = paddedHull(
|
||||
contour.map((p) => [p.x, p.y]),
|
||||
this.padding,
|
||||
);
|
||||
path = contour.length >= 2 && getClosedSpline(hull);
|
||||
break;
|
||||
case 'bubble':
|
||||
contour = genBubbleSet(members, nonMembers, this.cfg.bubbleCfg);
|
||||
path = contour.length >= 2 && getClosedSpline(contour);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
render() {
|
||||
this.group.addShape('path', {
|
||||
attrs: {
|
||||
path: this.path,
|
||||
...this.cfg.style,
|
||||
},
|
||||
id: this.id,
|
||||
name: this.cfg.id,
|
||||
});
|
||||
this.group.toBack();
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加hull的成员,同时如果该成员原先在nonMembers中,则从nonMembers中去掉
|
||||
* @param item 节点实例
|
||||
* @return boolean 添加成功返回 true,否则返回 false
|
||||
*/
|
||||
public addMember(item: Item | string): boolean {
|
||||
if (!item) return;
|
||||
if (isString(item)) item = this.graph.findById(item);
|
||||
this.members.push(item);
|
||||
const index = this.nonMembers.indexOf(item);
|
||||
if (index > -1) {
|
||||
this.nonMembers.splice(index, 1);
|
||||
}
|
||||
this.updateData(this.members, this.nonMembers);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加hull需要排除的节点,同时如果该成员原先在members中,则从members中去掉
|
||||
* @param item 节点实例
|
||||
* @return boolean 添加成功返回 true,否则返回 false
|
||||
*/
|
||||
public addNonMember(item: Item | string): boolean {
|
||||
if (!item) return;
|
||||
if (isString(item)) item = this.graph.findById(item);
|
||||
this.nonMembers.push(item);
|
||||
const index = this.members.indexOf(item);
|
||||
if (index > -1) {
|
||||
this.members.splice(index, 1);
|
||||
}
|
||||
this.updateData(this.members, this.nonMembers);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除hull中的成员
|
||||
* @param node 节点实例
|
||||
* @return boolean 移除成功返回 true,否则返回 false
|
||||
*/
|
||||
public removeMember(item: Item | string): boolean {
|
||||
if (!item) return;
|
||||
if (isString(item)) item = this.graph.findById(item);
|
||||
const index = this.members.indexOf(item);
|
||||
if (index > -1) {
|
||||
this.members.splice(index, 1);
|
||||
this.updateData(this.members, this.nonMembers);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param node 节点实例
|
||||
* @return boolean 移除成功返回 true,否则返回 false
|
||||
*/
|
||||
public removeNonMember(item: Item | string): boolean {
|
||||
if (!item) return;
|
||||
if (isString(item)) item = this.graph.findById(item);
|
||||
const index = this.nonMembers.indexOf(item);
|
||||
if (index > -1) {
|
||||
this.nonMembers.splice(index, 1);
|
||||
this.updateData(this.members, this.nonMembers);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public updateData(members: Item[] | string[], nonMembers: string[] | Item[]) {
|
||||
this.group.findById(this.id).remove();
|
||||
if (members)
|
||||
this.members = (members as any[]).map((item) =>
|
||||
isString(item) ? this.graph.findById(item) : item,
|
||||
);
|
||||
if (nonMembers)
|
||||
this.nonMembers = (nonMembers as any[]).map((item) =>
|
||||
isString(item) ? this.graph.findById(item) : item,
|
||||
);
|
||||
this.path = this.calcPath(this.members, this.nonMembers);
|
||||
this.render();
|
||||
}
|
||||
|
||||
public updateStyle(cfg: HullCfg['style']) {
|
||||
const path = this.group.findById(this.id);
|
||||
path.attr({
|
||||
...cfg,
|
||||
});
|
||||
}
|
||||
|
||||
public updateCfg(cfg: Partial<HullCfg>) {
|
||||
this.cfg = deepMix(this.cfg, cfg);
|
||||
this.id = this.cfg.id;
|
||||
this.group = this.cfg.group;
|
||||
if (cfg.members) {
|
||||
this.members = this.cfg.members.map((item) =>
|
||||
isString(item) ? this.graph.findById(item) : item,
|
||||
);
|
||||
}
|
||||
if (cfg.nonMembers) {
|
||||
this.nonMembers = this.cfg.nonMembers.map((item) =>
|
||||
isString(item) ? this.graph.findById(item) : item,
|
||||
);
|
||||
}
|
||||
this.setPadding();
|
||||
this.setType();
|
||||
this.path = this.calcPath(this.members, this.nonMembers);
|
||||
this.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否在hull内部
|
||||
* @param item
|
||||
*/
|
||||
public contain(item: Item | string): boolean {
|
||||
let nodeItem: Item;
|
||||
if (isString(item)) {
|
||||
nodeItem = this.graph.findById(item);
|
||||
} else {
|
||||
nodeItem = item;
|
||||
}
|
||||
let shapePoints;
|
||||
const shape = nodeItem.getKeyShape();
|
||||
if (nodeItem.get('type') === 'path') {
|
||||
shapePoints = pathToPoints(shape.attr('path'));
|
||||
} else {
|
||||
const shapeBBox = shape.getCanvasBBox();
|
||||
shapePoints = [
|
||||
[shapeBBox.minX, shapeBBox.minY],
|
||||
[shapeBBox.maxX, shapeBBox.minY],
|
||||
[shapeBBox.maxX, shapeBBox.maxY],
|
||||
[shapeBBox.minX, shapeBBox.maxY],
|
||||
];
|
||||
}
|
||||
shapePoints = shapePoints.map((canvasPoint) => {
|
||||
const point = this.graph.getPointByCanvas(canvasPoint[0], canvasPoint[1]);
|
||||
return [point.x, point.y];
|
||||
});
|
||||
return isPolygonsIntersect(shapePoints, pathToPoints(this.path));
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.group.remove();
|
||||
this.cfg = null;
|
||||
}
|
||||
}
|
790
packages/core/src/item/item.ts
Normal file
790
packages/core/src/item/item.ts
Normal file
@ -0,0 +1,790 @@
|
||||
import { Group } from '@antv/g-canvas';
|
||||
import {
|
||||
each,
|
||||
isNil,
|
||||
isPlainObject,
|
||||
isString,
|
||||
isBoolean,
|
||||
uniqueId,
|
||||
mix,
|
||||
deepMix,
|
||||
isArray,
|
||||
} from '@antv/util';
|
||||
import { IItemBase, IItemBaseConfig } from '../interface/item';
|
||||
import Shape from '../shape/shape';
|
||||
import {
|
||||
IBBox,
|
||||
IPoint,
|
||||
IShapeBase,
|
||||
ModelConfig,
|
||||
ShapeStyle,
|
||||
Indexable,
|
||||
NodeConfig,
|
||||
EdgeConfig,
|
||||
ComboConfig,
|
||||
ITEM_TYPE,
|
||||
} from '../types';
|
||||
import { getBBox } from '../util/graphic';
|
||||
import { translate } from '../util/math';
|
||||
import { IGroup } from '@antv/g-base/lib/interfaces';
|
||||
import Global from '../global';
|
||||
|
||||
const CACHE_BBOX = 'bboxCache';
|
||||
const CACHE_CANVAS_BBOX = 'bboxCanvasCache';
|
||||
const ARROWS = ['startArrow', 'endArrow'];
|
||||
|
||||
export default class ItemBase implements IItemBase {
|
||||
public _cfg: IItemBaseConfig & {
|
||||
[key: string]: unknown;
|
||||
} = {};
|
||||
|
||||
public destroyed: boolean = false;
|
||||
|
||||
constructor(cfg: IItemBaseConfig) {
|
||||
const defaultCfg: IItemBaseConfig = {
|
||||
/**
|
||||
* id
|
||||
* @type {string}
|
||||
*/
|
||||
id: undefined,
|
||||
|
||||
/**
|
||||
* 类型
|
||||
* @type {string}
|
||||
*/
|
||||
type: 'item',
|
||||
|
||||
/**
|
||||
* data model
|
||||
* @type {object}
|
||||
*/
|
||||
model: {} as ModelConfig,
|
||||
|
||||
/**
|
||||
* g group
|
||||
* @type {G.Group}
|
||||
*/
|
||||
group: undefined,
|
||||
|
||||
/**
|
||||
* is open animate
|
||||
* @type {boolean}
|
||||
*/
|
||||
animate: false,
|
||||
|
||||
/**
|
||||
* visible - not group visible
|
||||
* @type {boolean}
|
||||
*/
|
||||
visible: true,
|
||||
|
||||
/**
|
||||
* locked - lock node
|
||||
* @type {boolean}
|
||||
*/
|
||||
locked: false,
|
||||
/**
|
||||
* capture event
|
||||
* @type {boolean}
|
||||
*/
|
||||
event: true,
|
||||
/**
|
||||
* key shape to calculate item's bbox
|
||||
* @type object
|
||||
*/
|
||||
keyShape: undefined,
|
||||
/**
|
||||
* item's states, such as selected or active
|
||||
* @type Array
|
||||
*/
|
||||
states: [],
|
||||
};
|
||||
this._cfg = Object.assign(defaultCfg, this.getDefaultCfg(), cfg);
|
||||
|
||||
const model = this.get('model');
|
||||
let { id } = model;
|
||||
|
||||
const itemType = this.get('type');
|
||||
|
||||
if (!id) {
|
||||
id = uniqueId(itemType);
|
||||
this.get('model').id = id;
|
||||
}
|
||||
|
||||
this.set('id', id);
|
||||
|
||||
const { group } = cfg;
|
||||
if (group) {
|
||||
group.set('item', this);
|
||||
group.set('id', id);
|
||||
}
|
||||
|
||||
this.init();
|
||||
this.draw();
|
||||
|
||||
const shapeType = (model.shape as string) || (model.type as string) || (itemType === 'edge' ? 'line' : 'circle');
|
||||
const shapeFactory = this.get('shapeFactory');
|
||||
if (shapeFactory && shapeFactory[shapeType]) {
|
||||
const { options } = shapeFactory[shapeType];
|
||||
// merge the stateStyles from item and shape
|
||||
if (options && options.stateStyles) {
|
||||
let styles = this.get('styles') || model.stateStyles;
|
||||
styles = deepMix({}, options.stateStyles, styles);
|
||||
this.set('styles', styles);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 keyshape 计算包围盒
|
||||
*/
|
||||
private calculateBBox(): IBBox {
|
||||
const keyShape: IShapeBase = this.get('keyShape');
|
||||
const group: Group = this.get('group');
|
||||
// 因为 group 可能会移动,所以必须通过父元素计算才能计算出正确的包围盒
|
||||
const bbox = getBBox(keyShape, group);
|
||||
bbox.x = bbox.minX;
|
||||
bbox.y = bbox.minY;
|
||||
bbox.width = bbox.maxX - bbox.minX;
|
||||
bbox.height = bbox.maxY - bbox.minY;
|
||||
bbox.centerX = (bbox.minX + bbox.maxX) / 2;
|
||||
bbox.centerY = (bbox.minY + bbox.maxY) / 2;
|
||||
return bbox;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 keyshape 计算包围盒
|
||||
*/
|
||||
public calculateCanvasBBox(): IBBox {
|
||||
const keyShape: IShapeBase = this.get('keyShape');
|
||||
const group: Group = this.get('group');
|
||||
// 因为 group 可能会移动,所以必须通过父元素计算才能计算出正确的包围盒
|
||||
const bbox = getBBox(keyShape, group);
|
||||
bbox.x = bbox.minX;
|
||||
bbox.y = bbox.minY;
|
||||
bbox.width = bbox.maxX - bbox.minX;
|
||||
bbox.height = bbox.maxY - bbox.minY;
|
||||
bbox.centerX = (bbox.minX + bbox.maxX) / 2;
|
||||
bbox.centerY = (bbox.minY + bbox.maxY) / 2;
|
||||
return bbox;
|
||||
}
|
||||
|
||||
/**
|
||||
* draw shape
|
||||
*/
|
||||
private drawInner() {
|
||||
const self = this;
|
||||
const shapeFactory = self.get('shapeFactory');
|
||||
const group: Group = self.get('group');
|
||||
const model: ModelConfig = self.get('model');
|
||||
group.clear();
|
||||
const visible = model.visible;
|
||||
if (visible !== undefined && !visible) self.changeVisibility(visible);
|
||||
|
||||
if (!shapeFactory) {
|
||||
return;
|
||||
}
|
||||
self.updatePosition(model);
|
||||
const cfg = self.getShapeCfg(model); // 可能会附加额外信息
|
||||
const shapeType = (cfg.type as string);
|
||||
|
||||
const keyShape: IShapeBase = shapeFactory.draw(shapeType, cfg, group);
|
||||
|
||||
if (keyShape) {
|
||||
self.set('keyShape', keyShape);
|
||||
keyShape.set('isKeyShape', true);
|
||||
keyShape.set('draggable', true);
|
||||
}
|
||||
|
||||
this.setOriginStyle();
|
||||
|
||||
// 防止由于用户外部修改 model 中的 shape 导致 shape 不更新
|
||||
this.set('currentShape', shapeType);
|
||||
this.restoreStates(shapeFactory, shapeType!);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置图元素原始样式
|
||||
* @param keyShape 图元素 keyShape
|
||||
* @param group Group 容器
|
||||
*/
|
||||
private setOriginStyle(cfg?: ModelConfig) {
|
||||
const originStyles = {};
|
||||
const group: Group = this.get('group');
|
||||
const children = group.get('children');
|
||||
const keyShape: IShapeBase = this.getKeyShape();
|
||||
const self = this;
|
||||
const keyShapeName = keyShape.get('name');
|
||||
|
||||
if (!this.get('originStyle')) {
|
||||
each(children, (child) => {
|
||||
const name = child.get('name');
|
||||
if (name && name !== keyShapeName) {
|
||||
originStyles[name] = self.getShapeStyleByName(name);
|
||||
} else {
|
||||
const keyShapeStyle: ShapeStyle = self.getShapeStyleByName();
|
||||
if (keyShapeStyle.path) delete keyShapeStyle.path;
|
||||
if (keyShapeStyle.matrix) delete keyShapeStyle.matrix;
|
||||
if (!keyShapeName) {
|
||||
Object.assign(originStyles, keyShapeStyle);
|
||||
} else {
|
||||
originStyles[keyShapeName] = keyShapeStyle;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const itemType = this.get('type');
|
||||
const model = this.getModel();
|
||||
let shapeType = model.type;
|
||||
if (!shapeType) {
|
||||
switch (itemType) {
|
||||
case 'edge':
|
||||
shapeType = 'line';
|
||||
break;
|
||||
default:
|
||||
shapeType = 'circle';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let shapeFactory = Shape.getFactory(itemType)[shapeType];
|
||||
if (!shapeFactory) shapeFactory = Shape.getFactory(itemType).getShape();
|
||||
const shapeOptions = shapeFactory.getOptions ? shapeFactory.getOptions(model) : {};
|
||||
const defaultStyle = shapeOptions.style || {};
|
||||
const size = shapeOptions.size;
|
||||
if (itemType === 'edge') {
|
||||
if (!defaultStyle.lineWidth) defaultStyle.lineWidth = size || Global.defaultEdge.size;
|
||||
} else {
|
||||
if (!defaultStyle.r) defaultStyle.r = size / 2 || Global.defaultNode.size / 2;
|
||||
if (!defaultStyle.width) defaultStyle.width = (isArray(size) ? size[0] : size) || Global.defaultNode.size / 2;
|
||||
if (!defaultStyle.height) defaultStyle.height = (isArray(size) ? size[1] : size) || Global.defaultNode.size / 2;
|
||||
}
|
||||
if (!keyShapeName) {
|
||||
Object.assign(originStyles, defaultStyle);
|
||||
} else {
|
||||
const styles: ShapeStyle = {};
|
||||
for (const key in defaultStyle) {
|
||||
const style = defaultStyle[key];
|
||||
if (!isPlainObject(style) || ARROWS.includes(key)) styles[key] = style;
|
||||
}
|
||||
if (styles.path) delete styles.path;
|
||||
if (styles.matrix) delete styles.matrix;
|
||||
if (!originStyles[keyShapeName]) originStyles[keyShapeName] = styles;
|
||||
else originStyles[keyShapeName] = Object.assign(styles, originStyles[keyShapeName]);
|
||||
}
|
||||
|
||||
const drawOriginStyle = this.getOriginStyle();
|
||||
let styles: ShapeStyle = {};
|
||||
if (cfg) {
|
||||
styles = deepMix({}, drawOriginStyle, originStyles, cfg.style, {
|
||||
labelCfg: cfg.labelCfg,
|
||||
});
|
||||
} else {
|
||||
styles = deepMix({}, drawOriginStyle, originStyles);
|
||||
}
|
||||
|
||||
if (styles.path) delete styles.path;
|
||||
if (styles.matrix) delete styles.matrix;
|
||||
self.set('originStyle', styles);
|
||||
}
|
||||
|
||||
/**
|
||||
* restore shape states
|
||||
* @param shapeFactory
|
||||
* @param shapeType
|
||||
*/
|
||||
private restoreStates(shapeFactory: any, shapeType: string) {
|
||||
const self = this;
|
||||
const states: string[] = self.get('states');
|
||||
each(states, (state) => {
|
||||
shapeFactory.setState(shapeType, state, true, self);
|
||||
});
|
||||
}
|
||||
|
||||
protected init() {
|
||||
const shapeFactory = Shape.getFactory(this.get('type'));
|
||||
this.set('shapeFactory', shapeFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取属性
|
||||
* @internal 仅内部类使用
|
||||
* @param {String} key 属性名
|
||||
* @return {object | string | number} 属性值
|
||||
*/
|
||||
public get<T = any>(key: string): T {
|
||||
return this._cfg[key] as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置属性
|
||||
* @internal 仅内部类使用
|
||||
* @param {String|Object} key 属性名,也可以是对象
|
||||
* @param {object | string | number} val 属性值
|
||||
*/
|
||||
public set(key: string | object, val?: unknown): void {
|
||||
if (isPlainObject(key)) {
|
||||
this._cfg = { ...this._cfg, ...key };
|
||||
} else {
|
||||
this._cfg[key] = val;
|
||||
}
|
||||
}
|
||||
|
||||
protected getDefaultCfg() {
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新/刷新等操作后,清除 cache
|
||||
*/
|
||||
protected clearCache() {
|
||||
this.set(CACHE_BBOX, null);
|
||||
this.set(CACHE_CANVAS_BBOX, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染前的逻辑,提供给子类复写
|
||||
*/
|
||||
protected beforeDraw() { }
|
||||
|
||||
/**
|
||||
* 渲染后的逻辑,提供给子类复写
|
||||
*/
|
||||
protected afterDraw() { }
|
||||
|
||||
/**
|
||||
* 更新后做一些工作
|
||||
*/
|
||||
protected afterUpdate() { }
|
||||
|
||||
/**
|
||||
* draw shape
|
||||
*/
|
||||
public draw() {
|
||||
this.beforeDraw();
|
||||
this.drawInner();
|
||||
this.afterDraw();
|
||||
}
|
||||
|
||||
public getShapeStyleByName(name?: string): ShapeStyle {
|
||||
const group: Group = this.get('group');
|
||||
let currentShape: IShapeBase = this.getKeyShape();
|
||||
|
||||
if (name) {
|
||||
currentShape = group.find((element) => element.get('name') === name) as IShapeBase;
|
||||
}
|
||||
|
||||
if (currentShape) {
|
||||
const styles: ShapeStyle & Indexable<any> = {};
|
||||
|
||||
each(currentShape.attr(), (val, key) => {
|
||||
// 修改 img 通过 updateItem 实现
|
||||
if (key !== 'img') {
|
||||
styles[key] = val;
|
||||
}
|
||||
});
|
||||
return styles;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
public getShapeCfg(model: ModelConfig): ModelConfig {
|
||||
const styles = this.get('styles');
|
||||
if (styles) {
|
||||
// merge graph的item样式与数据模型中的样式
|
||||
const newModel = model;
|
||||
newModel.style = { ...styles, ...model.style };
|
||||
return newModel;
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定状态的样式,去除了全局样式
|
||||
* @param state 状态名称
|
||||
*/
|
||||
public getStateStyle(state: string) {
|
||||
const styles = this.get('styles');
|
||||
const stateStyle = styles && styles[state];
|
||||
return stateStyle;
|
||||
}
|
||||
|
||||
/**
|
||||
* get keyshape style
|
||||
*/
|
||||
public getOriginStyle(): ShapeStyle {
|
||||
return this.get('originStyle');
|
||||
}
|
||||
|
||||
public getCurrentStatesStyle(): ShapeStyle {
|
||||
const self = this;
|
||||
let styles = {};
|
||||
const states = self.getStates();
|
||||
if (!states || !states.length) {
|
||||
return this.getOriginStyle();
|
||||
}
|
||||
each(self.getStates(), (state) => {
|
||||
styles = Object.assign(styles, self.getStateStyle(state));
|
||||
});
|
||||
return styles;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更改元素状态, visible 不属于这个范畴
|
||||
* @internal 仅提供内部类 graph 使用
|
||||
* @param {String} state 状态名
|
||||
* @param {Boolean} value 节点状态值
|
||||
*/
|
||||
public setState(state: string, value: string | boolean) {
|
||||
const states: string[] = this.get('states');
|
||||
const shapeFactory = this.get('shapeFactory');
|
||||
let stateName = state;
|
||||
let filterStateName = state;
|
||||
if (isString(value)) {
|
||||
stateName = `${state}:${value}`;
|
||||
filterStateName = `${state}:`;
|
||||
}
|
||||
|
||||
let newStates = states;
|
||||
|
||||
if (isBoolean(value)) {
|
||||
const index = states.indexOf(filterStateName);
|
||||
if (value) {
|
||||
if (index > -1) {
|
||||
return;
|
||||
}
|
||||
states.push(stateName);
|
||||
} else if (index > -1) {
|
||||
states.splice(index, 1);
|
||||
}
|
||||
} else if (isString(value)) {
|
||||
// 过滤掉 states 中 filterStateName 相关的状态
|
||||
const filterStates = states.filter((name) => name.includes(filterStateName));
|
||||
|
||||
if (filterStates.length > 0) {
|
||||
this.clearStates(filterStates);
|
||||
}
|
||||
newStates = newStates.filter((name) => !name.includes(filterStateName));
|
||||
|
||||
newStates.push(stateName);
|
||||
this.set('states', newStates);
|
||||
}
|
||||
|
||||
if (shapeFactory) {
|
||||
const model: ModelConfig = this.get('model');
|
||||
const type = model.type;
|
||||
|
||||
// 调用 shape/shape.ts 中的 setState
|
||||
shapeFactory.setState(type, state, value, this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除指定的状态,如果参数为空,则不做任务处理
|
||||
* @param states 状态名称
|
||||
*/
|
||||
public clearStates(states?: string | string[]) {
|
||||
const self = this;
|
||||
const originStates = self.getStates();
|
||||
const shapeFactory = self.get('shapeFactory');
|
||||
const model: ModelConfig = self.get('model');
|
||||
const shape = model.type;
|
||||
if (!states) {
|
||||
states = originStates;
|
||||
}
|
||||
|
||||
if (isString(states)) {
|
||||
states = [states];
|
||||
}
|
||||
|
||||
const newStates = originStates.filter((state) => states.indexOf(state) === -1);
|
||||
self.set('states', newStates);
|
||||
|
||||
states.forEach((state) => {
|
||||
shapeFactory.setState(shape, state, false, self);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点的图形容器
|
||||
* @return {G.Group} 图形容器
|
||||
*/
|
||||
public getContainer(): Group {
|
||||
return this.get('group');
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点的关键形状,用于计算节点大小,连线截距等
|
||||
* @return {IShapeBase} 关键形状
|
||||
*/
|
||||
public getKeyShape(): IShapeBase {
|
||||
return this.get('keyShape');
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点数据模型
|
||||
* @return {Object} 数据模型
|
||||
*/
|
||||
public getModel(): NodeConfig | EdgeConfig | ComboConfig {
|
||||
return this.get('model');
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点类型
|
||||
* @return {string} 节点的类型
|
||||
*/
|
||||
public getType(): ITEM_TYPE {
|
||||
return this.get('type');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Item 的ID
|
||||
*/
|
||||
public getID(): string {
|
||||
return this.get('id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否是 Item 对象,悬空边情况下进行判定
|
||||
*/
|
||||
public isItem(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前元素的所有状态
|
||||
* @return {Array} 元素的所有状态
|
||||
*/
|
||||
public getStates(): string[] {
|
||||
return this.get('states');
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前元素是否处于某状态
|
||||
* @param {String} state 状态名
|
||||
* @return {Boolean} 是否处于某状态
|
||||
*/
|
||||
public hasState(state: string): boolean {
|
||||
const states = this.getStates();
|
||||
return states.indexOf(state) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新一般用于处理几种情况
|
||||
* 1. item model 在外部被改变
|
||||
* 2. 边的节点位置发生改变,需要重新计算边
|
||||
*
|
||||
* 因为数据从外部被修改无法判断一些属性是否被修改,直接走位置和 shape 的更新
|
||||
*/
|
||||
public refresh() {
|
||||
const model: ModelConfig = this.get('model');
|
||||
// 更新元素位置
|
||||
this.updatePosition(model);
|
||||
// 更新元素内容,样式
|
||||
this.updateShape();
|
||||
// 做一些更新之后的操作
|
||||
this.afterUpdate();
|
||||
// 清除缓存
|
||||
this.clearCache();
|
||||
}
|
||||
|
||||
public isOnlyMove(cfg?: ModelConfig): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将更新应用到 model 上,刷新属性
|
||||
* @internal 仅提供给 Graph 使用,外部直接调用 graph.update 接口
|
||||
* @param {Object} cfg 配置项,可以是增量信息
|
||||
*/
|
||||
public update(cfg: ModelConfig, onlyMove: boolean = false) {
|
||||
const model: ModelConfig = this.get('model');
|
||||
const oriVisible = model.visible;
|
||||
const cfgVisible = cfg.visible;
|
||||
if (oriVisible !== cfgVisible && cfgVisible !== undefined) this.changeVisibility(cfgVisible);
|
||||
const originPosition: IPoint = { x: model.x!, y: model.y! };
|
||||
cfg.x = isNaN(cfg.x) ? model.x : cfg.x;
|
||||
cfg.y = isNaN(cfg.y) ? model.y : cfg.y;
|
||||
|
||||
const styles = this.get('styles');
|
||||
if (cfg.stateStyles) {
|
||||
// 更新 item 时更新 this.get('styles') 中的值
|
||||
const { stateStyles } = cfg;
|
||||
mix(styles, stateStyles);
|
||||
delete cfg.stateStyles;
|
||||
}
|
||||
|
||||
// 直接将更新合到原数据模型上,可以保证用户在外部修改源数据然后刷新时的样式符合期待。
|
||||
Object.assign(model, cfg);
|
||||
|
||||
// isOnlyMove 仅用于node
|
||||
// const onlyMove = this.isOnlyMove(cfg);
|
||||
|
||||
// 仅仅移动位置时,既不更新,也不重绘
|
||||
if (onlyMove) {
|
||||
this.updatePosition(cfg);
|
||||
} else {
|
||||
// 如果 x,y 有变化,先重置位置
|
||||
if (originPosition.x !== cfg.x || originPosition.y !== cfg.y) {
|
||||
this.updatePosition(cfg);
|
||||
}
|
||||
this.updateShape();
|
||||
}
|
||||
this.afterUpdate();
|
||||
this.clearCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新元素内容,样式
|
||||
*/
|
||||
public updateShape() {
|
||||
const shapeFactory = this.get('shapeFactory');
|
||||
const model = this.get('model');
|
||||
const shape = model.type;
|
||||
// 判定是否允许更新
|
||||
// 1. 注册的节点允许更新
|
||||
// 2. 更新后的 shape 等于原先的 shape
|
||||
if (shapeFactory.shouldUpdate(shape) && shape === this.get('currentShape')) {
|
||||
const updateCfg = this.getShapeCfg(model);
|
||||
shapeFactory.baseUpdate(shape, updateCfg, this);
|
||||
} else {
|
||||
// 如果不满足上面两种状态,重新绘制
|
||||
this.draw();
|
||||
}
|
||||
|
||||
// 更新完以后重新设置原始样式
|
||||
this.setOriginStyle(model);
|
||||
|
||||
// 更新后重置节点状态
|
||||
this.restoreStates(shapeFactory, shape);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新位置,避免整体重绘
|
||||
* @param {object} cfg 待更新数据
|
||||
*/
|
||||
public updatePosition(cfg: ModelConfig) {
|
||||
const model: ModelConfig = this.get('model');
|
||||
|
||||
const x = isNil(cfg.x) ? model.x : cfg.x;
|
||||
const y = isNil(cfg.y) ? model.y : cfg.y;
|
||||
|
||||
const group: Group = this.get('group');
|
||||
|
||||
if (isNil(x) || isNil(y)) {
|
||||
return;
|
||||
}
|
||||
group.resetMatrix();
|
||||
// G 4.0 element 中移除了矩阵相关方法,详见https://www.yuque.com/antv/blog/kxzk9g#4rMMV
|
||||
translate(group, { x: x!, y: y! });
|
||||
model.x = x;
|
||||
model.y = y;
|
||||
this.clearCache(); // 位置更新后需要清除缓存
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 item 的包围盒,这个包围盒是相对于 item 自己,不会将 matrix 计算在内
|
||||
* @return {Object} 包含 x,y,width,height, centerX, centerY
|
||||
*/
|
||||
public getBBox(): IBBox {
|
||||
// 计算 bbox 开销有些大,缓存
|
||||
let bbox: IBBox = this.get(CACHE_BBOX);
|
||||
if (!bbox) {
|
||||
bbox = this.calculateBBox();
|
||||
this.set(CACHE_BBOX, bbox);
|
||||
}
|
||||
return bbox;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 item 相对于画布的包围盒,会将从顶层到当前元素的 matrix 都计算在内
|
||||
* @return {Object} 包含 x,y,width,height, centerX, centerY
|
||||
*/
|
||||
public getCanvasBBox(): IBBox {
|
||||
// 计算 bbox 开销有些大,缓存
|
||||
let bbox: IBBox = this.get(CACHE_CANVAS_BBOX);
|
||||
if (!bbox) {
|
||||
bbox = this.calculateCanvasBBox();
|
||||
this.set(CACHE_CANVAS_BBOX, bbox);
|
||||
}
|
||||
return bbox;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将元素放到最前面
|
||||
*/
|
||||
public toFront() {
|
||||
const group: IGroup = this.get('group');
|
||||
group.toFront();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将元素放到最后面
|
||||
*/
|
||||
public toBack() {
|
||||
const group: IGroup = this.get('group');
|
||||
group.toBack();
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示元素
|
||||
*/
|
||||
public show() {
|
||||
this.changeVisibility(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏元素
|
||||
*/
|
||||
public hide() {
|
||||
this.changeVisibility(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更改是否显示
|
||||
* @param {Boolean} visible 是否显示
|
||||
*/
|
||||
public changeVisibility(visible: boolean) {
|
||||
const group: Group = this.get('group');
|
||||
if (visible) {
|
||||
group.show();
|
||||
} else {
|
||||
group.hide();
|
||||
}
|
||||
this.set('visible', visible);
|
||||
}
|
||||
|
||||
/**
|
||||
* 元素是否可见
|
||||
* @return {Boolean} 返回该元素是否可见
|
||||
*/
|
||||
public isVisible(): boolean {
|
||||
return this.get('visible');
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否拾取及出发该元素的交互事件
|
||||
* @param {Boolean} enable 标识位
|
||||
*/
|
||||
public enableCapture(enable: boolean) {
|
||||
const group: Group = this.get('group');
|
||||
if (group) {
|
||||
group.set('capture', enable);
|
||||
}
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
if (!this.destroyed) {
|
||||
const animate = this.get('animate');
|
||||
const group: Group = this.get('group');
|
||||
if (animate) {
|
||||
group.stopAnimate();
|
||||
}
|
||||
this.clearCache();
|
||||
group.remove();
|
||||
(this._cfg as IItemBaseConfig | null) = null;
|
||||
this.destroyed = true;
|
||||
}
|
||||
}
|
||||
}
|
258
packages/core/src/item/node.ts
Normal file
258
packages/core/src/item/node.ts
Normal file
@ -0,0 +1,258 @@
|
||||
import { each, isNil, mix } from '@antv/util';
|
||||
import { IEdge, INode } from '../interface/item';
|
||||
import { IPoint, IShapeBase, NodeConfig } from '../types';
|
||||
import {
|
||||
distance,
|
||||
getCircleIntersectByPoint,
|
||||
getEllipseIntersectByPoint,
|
||||
getRectIntersectByPoint,
|
||||
} from '../util/math';
|
||||
import Item from './item';
|
||||
|
||||
const CACHE_ANCHOR_POINTS = 'anchorPointsCache';
|
||||
const CACHE_BBOX = 'bboxCache';
|
||||
|
||||
export default class Node extends Item implements INode {
|
||||
public getNearestPoint(points: IPoint[], curPoint: IPoint): IPoint {
|
||||
let index = 0;
|
||||
let nearestPoint = points[0];
|
||||
let minDistance = distance(points[0], curPoint);
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
const point = points[i];
|
||||
const dis = distance(point, curPoint);
|
||||
if (dis < minDistance) {
|
||||
nearestPoint = point;
|
||||
minDistance = dis;
|
||||
index = i;
|
||||
}
|
||||
}
|
||||
nearestPoint.anchorIndex = index;
|
||||
return nearestPoint;
|
||||
}
|
||||
|
||||
public getDefaultCfg() {
|
||||
return {
|
||||
type: 'node',
|
||||
edges: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取从节点关联的所有边
|
||||
*/
|
||||
public getEdges(): IEdge[] {
|
||||
return this.get('edges');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有的入边
|
||||
*/
|
||||
public getInEdges(): IEdge[] {
|
||||
const self = this;
|
||||
return this.get('edges').filter((edge: IEdge) => edge.get('target') === self);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有的出边
|
||||
*/
|
||||
public getOutEdges(): IEdge[] {
|
||||
const self = this;
|
||||
return this.get('edges').filter((edge: IEdge) => edge.get('source') === self);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取节点的邻居节点
|
||||
*
|
||||
* @returns {INode[]}
|
||||
* @memberof Node
|
||||
*/
|
||||
public getNeighbors(type?: 'target' | 'source' | undefined): INode[] {
|
||||
const edges = this.get('edges') as IEdge[];
|
||||
|
||||
if (type === 'target') {
|
||||
// 当前节点为 source,它所指向的目标节点
|
||||
const neighhborsConverter = (edge: IEdge) => {
|
||||
return edge.getSource() === this;
|
||||
};
|
||||
return edges.filter(neighhborsConverter).map((edge) => edge.getTarget());
|
||||
}
|
||||
if (type === 'source') {
|
||||
// 当前节点为 target,它所指向的源节点
|
||||
const neighhborsConverter = (edge: IEdge) => {
|
||||
return edge.getTarget() === this;
|
||||
};
|
||||
return edges.filter(neighhborsConverter).map((edge) => edge.getSource());
|
||||
}
|
||||
|
||||
// 若未指定 type ,则返回所有邻居
|
||||
const neighhborsConverter = (edge: IEdge) => {
|
||||
return edge.getSource() === this ? edge.getTarget() : edge.getSource();
|
||||
};
|
||||
return edges.map(neighhborsConverter);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据锚点的索引获取连接点
|
||||
* @param {Number} index 索引
|
||||
*/
|
||||
public getLinkPointByAnchor(index: number): IPoint {
|
||||
const anchorPoints = this.getAnchorPoints();
|
||||
return anchorPoints[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接点
|
||||
* @param point
|
||||
*/
|
||||
public getLinkPoint(point: IPoint): IPoint | null {
|
||||
const keyShape: IShapeBase = this.get('keyShape');
|
||||
const type: string = keyShape.get('type');
|
||||
const itemType: string = this.get('type');
|
||||
let centerX;
|
||||
let centerY;
|
||||
const bbox = this.getBBox();
|
||||
if (itemType === 'combo') {
|
||||
centerX = (bbox.maxX + bbox.minX) / 2;
|
||||
centerY = (bbox.maxY + bbox.minY) / 2;
|
||||
} else {
|
||||
centerX = bbox.centerX;
|
||||
centerY = bbox.centerY;
|
||||
}
|
||||
const anchorPoints = this.getAnchorPoints();
|
||||
let intersectPoint: IPoint | null;
|
||||
switch (type) {
|
||||
case 'circle':
|
||||
intersectPoint = getCircleIntersectByPoint(
|
||||
{
|
||||
x: centerX!,
|
||||
y: centerY!,
|
||||
r: bbox.width / 2,
|
||||
},
|
||||
point,
|
||||
);
|
||||
break;
|
||||
case 'ellipse':
|
||||
intersectPoint = getEllipseIntersectByPoint(
|
||||
{
|
||||
x: centerX!,
|
||||
y: centerY!,
|
||||
rx: bbox.width / 2,
|
||||
ry: bbox.height / 2,
|
||||
},
|
||||
point,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
intersectPoint = getRectIntersectByPoint(bbox, point);
|
||||
}
|
||||
let linkPoint = intersectPoint;
|
||||
// 如果存在锚点,则使用交点计算最近的锚点
|
||||
if (anchorPoints.length) {
|
||||
if (!linkPoint) {
|
||||
// 如果计算不出交点
|
||||
linkPoint = point;
|
||||
}
|
||||
linkPoint = this.getNearestPoint(anchorPoints, linkPoint);
|
||||
}
|
||||
if (!linkPoint) {
|
||||
// 如果最终依然没法找到锚点和连接点,直接返回中心点
|
||||
linkPoint = { x: centerX, y: centerY } as IPoint;
|
||||
}
|
||||
return linkPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取锚点的定义
|
||||
* @return {array} anchorPoints
|
||||
*/
|
||||
public getAnchorPoints(): IPoint[] {
|
||||
let anchorPoints: IPoint[] = this.get(CACHE_ANCHOR_POINTS);
|
||||
if (!anchorPoints) {
|
||||
anchorPoints = [];
|
||||
const shapeFactory = this.get('shapeFactory');
|
||||
const bbox = this.getBBox();
|
||||
const model: NodeConfig = this.get('model');
|
||||
const shapeCfg = this.getShapeCfg(model);
|
||||
const type = model.type;
|
||||
const points = shapeFactory.getAnchorPoints(type, shapeCfg) || [];
|
||||
|
||||
each(points, (pointArr, index) => {
|
||||
const point = mix(
|
||||
{
|
||||
x: bbox.minX + pointArr[0] * bbox.width,
|
||||
y: bbox.minY + pointArr[1] * bbox.height,
|
||||
},
|
||||
pointArr[2],
|
||||
{
|
||||
index,
|
||||
},
|
||||
);
|
||||
anchorPoints.push(point);
|
||||
});
|
||||
this.set(CACHE_ANCHOR_POINTS, anchorPoints);
|
||||
}
|
||||
return anchorPoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* add edge
|
||||
* @param edge Edge instance
|
||||
*/
|
||||
public addEdge(edge: IEdge) {
|
||||
this.get('edges').push(edge);
|
||||
}
|
||||
|
||||
/**
|
||||
* 锁定节点
|
||||
*/
|
||||
public lock() {
|
||||
this.set('locked', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解锁锁定的节点
|
||||
*/
|
||||
public unlock() {
|
||||
this.set('locked', false);
|
||||
}
|
||||
|
||||
public hasLocked(): boolean {
|
||||
return this.get('locked');
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除边
|
||||
* @param {Edge} edge 边
|
||||
*/
|
||||
public removeEdge(edge: IEdge) {
|
||||
const edges = this.getEdges();
|
||||
const index = edges.indexOf(edge);
|
||||
if (index > -1) {
|
||||
edges.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
public clearCache() {
|
||||
this.set(CACHE_BBOX, null); // 清理缓存的 bbox
|
||||
this.set(CACHE_ANCHOR_POINTS, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否仅仅移动节点,其他属性没变化
|
||||
* @param cfg 节点数据模型
|
||||
*/
|
||||
public isOnlyMove(cfg: NodeConfig): boolean {
|
||||
if (!cfg) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const existX = !isNil(cfg.x);
|
||||
const existY = !isNil(cfg.y);
|
||||
|
||||
const keys = Object.keys(cfg);
|
||||
|
||||
// 仅有一个字段,包含 x 或者 包含 y
|
||||
// 两个字段,同时有 x,同时有 y
|
||||
return (keys.length === 1 && (existX || existY)) || (keys.length === 2 && existX && existY);
|
||||
}
|
||||
}
|
58
packages/core/src/shape/arrow.ts
Normal file
58
packages/core/src/shape/arrow.ts
Normal file
@ -0,0 +1,58 @@
|
||||
export default {
|
||||
triangle: (width: number = 10, length: number = 15, d: number = 0) => {
|
||||
const begin = d * 2;
|
||||
const path = `M ${begin},0 L ${begin + length},-${width / 2} L ${begin + length},${
|
||||
width / 2
|
||||
} Z`;
|
||||
return path;
|
||||
},
|
||||
vee: (width: number = 15, length: number = 20, d: number = 0) => {
|
||||
const begin = d * 2;
|
||||
const path = `M ${begin},0 L ${begin + length},-${width / 2}
|
||||
L ${begin + (2 * length) / 3},0 L ${begin + length},${width / 2} Z`;
|
||||
return path;
|
||||
},
|
||||
circle: (r: number = 5, d: number = 0) => {
|
||||
const begin = d * 2;
|
||||
const path = `M ${begin}, 0
|
||||
a ${r},${r} 0 1,0 ${r * 2},0
|
||||
a ${r},${r} 0 1,0 ${-r * 2},0`;
|
||||
return path;
|
||||
},
|
||||
rect: (width: number = 10, length: number = 10, d: number = 0) => {
|
||||
const begin = d * 2;
|
||||
const path = `M ${begin},${-width / 2}
|
||||
L ${begin + length},${-width / 2}
|
||||
L ${begin + length},${width / 2}
|
||||
L ${begin},${width / 2} Z`;
|
||||
return path;
|
||||
},
|
||||
diamond: (width: number = 15, length: number = 15, d: number = 0) => {
|
||||
const begin = d * 2;
|
||||
const path = `M ${begin},0
|
||||
L ${begin + length / 2},${-width / 2}
|
||||
L ${begin + length},0
|
||||
L ${begin + length / 2},${width / 2} Z`;
|
||||
return path;
|
||||
},
|
||||
triangleRect: (
|
||||
tWidth: number = 15,
|
||||
tLength: number = 15,
|
||||
rWidth: number = 15,
|
||||
rLength: number = 3,
|
||||
gap: number = 5,
|
||||
d: number = 0,
|
||||
) => {
|
||||
const begin = d * 2;
|
||||
const rectBegin = begin + tLength + gap;
|
||||
const path = `M ${begin},0 L ${begin + tLength},-${tWidth / 2} L ${begin + tLength},${
|
||||
tWidth / 2
|
||||
} Z
|
||||
M ${rectBegin}, -${rWidth / 2}
|
||||
L ${rectBegin + rLength} -${rWidth / 2}
|
||||
L ${rectBegin + rLength} ${rWidth / 2}
|
||||
L ${rectBegin} ${rWidth / 2}
|
||||
Z`;
|
||||
return path;
|
||||
},
|
||||
};
|
163
packages/core/src/shape/combo.ts
Normal file
163
packages/core/src/shape/combo.ts
Normal file
@ -0,0 +1,163 @@
|
||||
/**
|
||||
* @fileOverview common combo shape
|
||||
* @author shiwu.wyy@antfin.com
|
||||
*/
|
||||
import { Group as GGroup } from '@antv/g-canvas';
|
||||
import { IShape } from '@antv/g-canvas/lib/interfaces';
|
||||
import { isArray, isNil, clone } from '@antv/util';
|
||||
import { ILabelConfig, ShapeOptions } from '../interface/shape';
|
||||
import { Item, LabelStyle, NodeConfig, ModelConfig, ShapeStyle } from '../types';
|
||||
import Global from '../global';
|
||||
import Shape from './shape';
|
||||
import { shapeBase } from './shapeBase';
|
||||
|
||||
const singleCombo: ShapeOptions = {
|
||||
itemType: 'combo',
|
||||
// 单个图形的类型
|
||||
shapeType: 'single-combo',
|
||||
/**
|
||||
* Combo 标题文本相对图形的位置,默认为 top
|
||||
* 位置包括: top, bottom, left, right, center
|
||||
* @type {String}
|
||||
*/
|
||||
labelPosition: 'top',
|
||||
/**
|
||||
* 标题文本相对偏移,当 labelPosition 不为 center 时有效
|
||||
* @type {Number}
|
||||
*/
|
||||
refX: Global.comboLabel.refX,
|
||||
refY: Global.comboLabel.refY,
|
||||
|
||||
options: {
|
||||
style: {
|
||||
stroke: Global.defaultCombo.style.stroke,
|
||||
fill: Global.defaultCombo.style.fill,
|
||||
lineWidth: Global.defaultCombo.style.lineWidth
|
||||
},
|
||||
labelCfg: {
|
||||
style: {
|
||||
fill: Global.comboLabel.style.fill,
|
||||
fontSize: Global.comboLabel.style.fontSize,
|
||||
},
|
||||
},
|
||||
stateStyles: {
|
||||
...Global.comboStateStyles
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 获取 Combo 宽高
|
||||
* @internal 返回 Combo 的大小,以 [width, height] 的方式维护
|
||||
* @param {Object} cfg Combo 的配置项
|
||||
* @return {Array} 宽高
|
||||
*/
|
||||
getSize(cfg: ModelConfig): number[] {
|
||||
let size: number | number[] = clone(cfg.size || this.options!.size || Global.defaultCombo.size);
|
||||
|
||||
// size 是数组,若长度为 1,则补长度为 2
|
||||
if (isArray(size) && size.length === 1) {
|
||||
size = [size[0], size[0]];
|
||||
}
|
||||
|
||||
// size 为数字,则转换为数组
|
||||
if (!isArray(size)) {
|
||||
size = [size, size];
|
||||
}
|
||||
return size;
|
||||
},
|
||||
// 私有方法,不希望扩展的 Combo 复写这个方法
|
||||
getLabelStyleByPosition(cfg: NodeConfig, labelCfg: ILabelConfig): LabelStyle {
|
||||
const labelPosition = labelCfg.position || this.labelPosition;
|
||||
const { style: cfgStyle } = cfg;
|
||||
let padding: number | number[] = cfg.padding || this.options.padding;
|
||||
if (isArray(padding)) padding = padding[0];
|
||||
|
||||
let { refX, refY } = labelCfg;
|
||||
// 考虑 refX 和 refY = 0 的场景,不用用 labelCfg.refX || Global.nodeLabel.refX
|
||||
if (isNil(refX)) {
|
||||
refX = this.refX as number; // 不居中时的偏移量
|
||||
}
|
||||
if (isNil(refY)) {
|
||||
refY = this.refY as number; // 不居中时的偏移量
|
||||
}
|
||||
|
||||
const size = this.getSize!(cfg as ModelConfig);
|
||||
|
||||
const r = Math.max(cfgStyle.r, size[0] / 2) || size[0] / 2;
|
||||
|
||||
const dis = r + padding;
|
||||
|
||||
let style: any;
|
||||
switch (labelPosition) {
|
||||
case 'top':
|
||||
style = {
|
||||
x: 0,
|
||||
y: -dis - (refY as number),
|
||||
textBaseline: 'bottom', // 文本在图形的上方
|
||||
textAlign: 'center',
|
||||
};
|
||||
break;
|
||||
case 'bottom':
|
||||
style = {
|
||||
x: 0,
|
||||
y: dis + (refY as number),
|
||||
textBaseline: 'bottom',
|
||||
textAlign: 'center',
|
||||
};
|
||||
break;
|
||||
case 'left':
|
||||
style = {
|
||||
x: -dis + (refX as number),
|
||||
y: 0,
|
||||
textAlign: 'left',
|
||||
};
|
||||
break;
|
||||
case 'center':
|
||||
style = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
text: cfg!.label,
|
||||
textAlign: 'center',
|
||||
};
|
||||
break;
|
||||
default:
|
||||
style = {
|
||||
x: dis + (refX as number),
|
||||
y: 0,
|
||||
textAlign: 'right',
|
||||
};
|
||||
break;
|
||||
}
|
||||
style.text = cfg.label;
|
||||
return style;
|
||||
},
|
||||
drawShape(cfg: NodeConfig, group: GGroup): IShape {
|
||||
const { shapeType } = this; // || this.type,都已经加了 shapeType
|
||||
const style = this.getShapeStyle!(cfg);
|
||||
const shape = group.addShape(shapeType, {
|
||||
attrs: style,
|
||||
draggable: true,
|
||||
name: 'combo-shape',
|
||||
});
|
||||
return shape;
|
||||
},
|
||||
updateShape(cfg: NodeConfig, item: Item, keyShapeStyle: ShapeStyle) {
|
||||
const keyShape = item.get('keyShape');
|
||||
const animate = cfg.animate === undefined ? this.options.animate : cfg.animate;
|
||||
if (animate && keyShape.animate) {
|
||||
keyShape.animate(keyShapeStyle, {
|
||||
duration: 200,
|
||||
easing: 'easeLinear',
|
||||
});
|
||||
} else {
|
||||
keyShape.attr({
|
||||
...keyShapeStyle,
|
||||
});
|
||||
}
|
||||
|
||||
(this as any).updateLabel(cfg, item);
|
||||
// special for some types of nodes
|
||||
},
|
||||
};
|
||||
|
||||
const singleComboDef = { ...shapeBase, ...singleCombo };
|
||||
Shape.registerCombo('single-combo', singleComboDef);
|
112
packages/core/src/shape/combos/circle.ts
Normal file
112
packages/core/src/shape/combos/circle.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import { Group as GGroup } from '@antv/g-canvas';
|
||||
import { IShape } from '@antv/g-canvas/lib/interfaces';
|
||||
import { Item, ComboConfig, ShapeStyle } from '../../types';
|
||||
import Global from '../../global';
|
||||
import Shape from '../shape';
|
||||
import { ShapeOptions } from '../../interface/shape';
|
||||
import { isNumber, clone, mix, isArray } from '@antv/util';
|
||||
|
||||
// 圆形 Combo
|
||||
Shape.registerCombo(
|
||||
'circle',
|
||||
{
|
||||
// 自定义节点时的配置
|
||||
options: {
|
||||
size: [Global.defaultCombo.size[0], Global.defaultCombo.size[0]],
|
||||
padding: Global.defaultCombo.padding[0],
|
||||
animate: true,
|
||||
style: {
|
||||
stroke: Global.defaultCombo.style.stroke,
|
||||
fill: Global.defaultCombo.style.fill,
|
||||
lineWidth: Global.defaultCombo.style.lineWidth,
|
||||
},
|
||||
labelCfg: {
|
||||
style: {
|
||||
fill: Global.comboLabel.style.fill,
|
||||
fontSize: Global.comboLabel.style.fontSize,
|
||||
},
|
||||
refX: 0,
|
||||
refY: 0,
|
||||
},
|
||||
stateStyles: {
|
||||
...Global.comboStateStyles
|
||||
}
|
||||
},
|
||||
shapeType: 'circle',
|
||||
// 文本位置
|
||||
labelPosition: 'top',
|
||||
drawShape(cfg: ComboConfig, group: GGroup): IShape {
|
||||
const style = this.getShapeStyle!(cfg);
|
||||
delete style.height;
|
||||
delete style.width;
|
||||
const keyShape: IShape = group.addShape('circle', {
|
||||
attrs: style,
|
||||
className: 'circle-combo',
|
||||
name: 'circle-combo',
|
||||
draggable: true,
|
||||
});
|
||||
|
||||
return keyShape;
|
||||
},
|
||||
/**
|
||||
* 获取 Combo 的样式,供基于该 Combo 自定义时使用
|
||||
* @param {Object} cfg Combo 数据模型
|
||||
* @return {Object} Combo 的样式
|
||||
*/
|
||||
getShapeStyle(cfg: ComboConfig): ShapeStyle {
|
||||
const { style: defaultStyle } = this.options as ComboConfig;
|
||||
let padding: number | number[] = cfg.padding || this.options.padding;
|
||||
if (isArray(padding)) padding = padding[0];
|
||||
const strokeStyle: ShapeStyle = {
|
||||
stroke: cfg.color,
|
||||
};
|
||||
|
||||
// 如果设置了color,则覆盖默认的stroke属性
|
||||
const style = mix({}, defaultStyle, strokeStyle, cfg.style);
|
||||
const size = (this as ShapeOptions).getSize!(cfg);
|
||||
let r: number;
|
||||
if (!isNumber(style.r) || isNaN(style.r)) r = size[0] / 2 || Global.defaultCombo.style.r;
|
||||
else r = Math.max(style.r, size[0] / 2) || size[0] / 2;
|
||||
style.r = r + padding;
|
||||
const styles = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
...style,
|
||||
};
|
||||
if (cfg.style) cfg.style.r = r;
|
||||
else {
|
||||
cfg.style = { r };
|
||||
}
|
||||
return styles;
|
||||
},
|
||||
update(cfg: ComboConfig, item: Item) {
|
||||
const size = (this as ShapeOptions).getSize!(cfg);
|
||||
let padding: number | number[] = cfg.padding || this.options.padding;
|
||||
if (isArray(padding)) padding = padding[0];
|
||||
const cfgStyle = clone(cfg.style);
|
||||
const r = Math.max(cfgStyle.r, size[0] / 2) || size[0] / 2;
|
||||
cfgStyle.r = r + padding;
|
||||
|
||||
const itemCacheSize = item.get('sizeCache');
|
||||
if (itemCacheSize) {
|
||||
itemCacheSize.r = cfgStyle.r;
|
||||
}
|
||||
|
||||
// 下面这些属性需要覆盖默认样式与目前样式,但若在 cfg 中有指定则应该被 cfg 的相应配置覆盖。
|
||||
const strokeStyle = {
|
||||
stroke: cfg.color,
|
||||
};
|
||||
// 与 getShapeStyle 不同在于,update 时需要获取到当前的 style 进行融合。即新传入的配置项中没有涉及的属性,保留当前的配置。
|
||||
const keyShape = item.get('keyShape');
|
||||
const style = mix({}, keyShape.attr(), strokeStyle, cfgStyle);
|
||||
|
||||
if (cfg.style) cfg.style.r = r;
|
||||
else {
|
||||
cfg.style = { r };
|
||||
}
|
||||
|
||||
(this as any).updateShape(cfg, item, style, true);
|
||||
},
|
||||
},
|
||||
'single-combo',
|
||||
);
|
2
packages/core/src/shape/combos/index.ts
Normal file
2
packages/core/src/shape/combos/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
import './circle';
|
||||
import './rect';
|
215
packages/core/src/shape/combos/rect.ts
Normal file
215
packages/core/src/shape/combos/rect.ts
Normal file
@ -0,0 +1,215 @@
|
||||
import { Group as GGroup } from '@antv/g-canvas';
|
||||
import { IShape } from '@antv/g-canvas/lib/interfaces';
|
||||
import { mix, isNumber, clone, isNil } from '@antv/util';
|
||||
import { LabelStyle, Item, ComboConfig, ShapeStyle } from '../../types';
|
||||
import Global from '../../global';
|
||||
import Shape from '../shape';
|
||||
import { ILabelConfig, ShapeOptions } from '../../interface/shape';
|
||||
|
||||
Shape.registerCombo(
|
||||
'rect',
|
||||
{
|
||||
// 自定义 Combo 时的配置
|
||||
options: {
|
||||
size: [40, 5],
|
||||
padding: [25, 20, 15, 20],
|
||||
animate: true,
|
||||
style: {
|
||||
radius: 0,
|
||||
stroke: Global.defaultCombo.style.stroke,
|
||||
fill: Global.defaultCombo.style.fill,
|
||||
lineWidth: Global.defaultCombo.style.lineWidth
|
||||
},
|
||||
// 文本样式配置
|
||||
labelCfg: {
|
||||
style: {
|
||||
fill: Global.comboLabel.style.fill,
|
||||
fontSize: Global.comboLabel.style.fontSize,
|
||||
},
|
||||
},
|
||||
// 连接点,默认为左右
|
||||
anchorPoints: [
|
||||
[0, 0.5],
|
||||
[1, 0.5],
|
||||
],
|
||||
stateStyles: {
|
||||
...Global.comboStateStyles
|
||||
}
|
||||
},
|
||||
shapeType: 'rect',
|
||||
labelPosition: 'top',
|
||||
drawShape(cfg: ComboConfig, group: GGroup): IShape {
|
||||
const style = this.getShapeStyle!(cfg);
|
||||
const keyShape = group.addShape('rect', {
|
||||
attrs: style,
|
||||
className: 'rect-combo',
|
||||
name: 'rect-combo',
|
||||
draggable: true,
|
||||
});
|
||||
return keyShape;
|
||||
},
|
||||
// 私有方法,不希望扩展的 Combo 复写这个方法
|
||||
getLabelStyleByPosition(cfg: ComboConfig, labelCfg: ILabelConfig): LabelStyle {
|
||||
const labelPosition = labelCfg.position || this.labelPosition;
|
||||
const { style: cfgStyle } = cfg;
|
||||
let padding = cfg.padding || this.options.padding;
|
||||
if (isNumber(padding)) padding = [padding, padding, padding, padding];
|
||||
|
||||
let { refX, refY } = labelCfg;
|
||||
// 考虑 refX 和 refY = 0 的场景,不用用 labelCfg.refX || Global.nodeLabel.refY
|
||||
if (isNil(refX)) {
|
||||
refX = this.refX as number; // 不居中时的偏移量
|
||||
}
|
||||
if (isNil(refY)) {
|
||||
refY = this.refY as number; // 不居中时的偏移量
|
||||
}
|
||||
|
||||
const leftDis = cfgStyle.width / 2 + padding[3];
|
||||
const topDis = cfgStyle.height / 2 + padding[0];
|
||||
|
||||
let style: any;
|
||||
switch (labelPosition) {
|
||||
case 'top':
|
||||
style = {
|
||||
x: 0 - leftDis + refX,
|
||||
y: 0 - topDis + refY,
|
||||
textBaseline: 'top', // 文本在图形的上方
|
||||
textAlign: 'left',
|
||||
};
|
||||
break;
|
||||
case 'bottom':
|
||||
style = {
|
||||
x: 0,
|
||||
y: topDis + refY,
|
||||
textBaseline: 'bottom',
|
||||
textAlign: 'center',
|
||||
};
|
||||
break;
|
||||
case 'left':
|
||||
style = {
|
||||
x: 0 - leftDis + refY,
|
||||
y: 0,
|
||||
textAlign: 'left',
|
||||
};
|
||||
break;
|
||||
case 'center':
|
||||
style = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
text: cfg!.label,
|
||||
textAlign: 'center',
|
||||
};
|
||||
break;
|
||||
default:
|
||||
style = {
|
||||
x: leftDis + refX,
|
||||
y: 0,
|
||||
textAlign: 'right',
|
||||
};
|
||||
break;
|
||||
}
|
||||
style.text = cfg.label;
|
||||
return style;
|
||||
},
|
||||
/**
|
||||
* 获取节点的样式,供基于该节点自定义时使用
|
||||
* @param {Object} cfg 节点数据模型
|
||||
* @return {Object} 节点的样式
|
||||
*/
|
||||
getShapeStyle(cfg: ComboConfig) {
|
||||
const { style: defaultStyle } = this.options as ComboConfig;
|
||||
let padding: number | number[] = cfg.padding || this.options.padding;
|
||||
if (isNumber(padding)) padding = [padding, padding, padding, padding];
|
||||
const strokeStyle: ShapeStyle = {
|
||||
stroke: cfg.color,
|
||||
};
|
||||
|
||||
// 如果设置了color,则覆盖默认的stroke属性
|
||||
const style = mix({}, defaultStyle, strokeStyle, cfg.style);
|
||||
const size = (this as ShapeOptions).getSize!(cfg);
|
||||
let width: number;
|
||||
let height: number;
|
||||
if (!isNumber(style.width) || isNaN(style.width))
|
||||
width = size[0] || Global.defaultCombo.style.width;
|
||||
else width = Math.max(style.width, size[0]) || size[0];
|
||||
if (!isNumber(style.height) || isNaN(style.height))
|
||||
height = size[1] || Global.defaultCombo.style.height;
|
||||
else height = Math.max(style.height, size[1]) || size[1];
|
||||
|
||||
const x = -width / 2 - padding[3];
|
||||
const y = -height / 2 - padding[0];
|
||||
|
||||
style.width = width + padding[1] + padding[3];
|
||||
style.height = height + padding[0] + padding[2];
|
||||
|
||||
const styles = {
|
||||
x,
|
||||
y,
|
||||
...style,
|
||||
};
|
||||
if (!cfg.style) {
|
||||
cfg.style = {
|
||||
width,
|
||||
height,
|
||||
};
|
||||
} else {
|
||||
cfg.style.width = width;
|
||||
cfg.style.height = height;
|
||||
}
|
||||
return styles;
|
||||
},
|
||||
update(cfg: ComboConfig, item: Item) {
|
||||
const size = (this as ShapeOptions).getSize!(cfg);
|
||||
let padding: number | number[] = cfg.padding || this.options.padding;
|
||||
if (isNumber(padding)) padding = [padding, padding, padding, padding];
|
||||
const cfgStyle = clone(cfg.style);
|
||||
const width = Math.max(cfgStyle.width, size[0]) || size[0];
|
||||
const height = Math.max(cfgStyle.height, size[1]) || size[1];
|
||||
|
||||
cfgStyle.width = width + padding[1] + padding[3];
|
||||
cfgStyle.height = height + padding[0] + padding[2];
|
||||
|
||||
const itemCacheSize = item.get('sizeCache');
|
||||
if (itemCacheSize) {
|
||||
itemCacheSize.width = cfgStyle.width;
|
||||
itemCacheSize.height = cfgStyle.height;
|
||||
}
|
||||
|
||||
cfgStyle.x = -width / 2 - padding[3];
|
||||
cfgStyle.y = -height / 2 - padding[0];
|
||||
// 下面这些属性需要覆盖默认样式与目前样式,但若在 cfg 中有指定则应该被 cfg 的相应配置覆盖。
|
||||
const strokeStyle = {
|
||||
stroke: cfg.color,
|
||||
};
|
||||
// 与 getShapeStyle 不同在于,update 时需要获取到当前的 style 进行融合。即新传入的配置项中没有涉及的属性,保留当前的配置。
|
||||
const keyShape = item.get('keyShape');
|
||||
const style = mix({}, keyShape.attr(), strokeStyle, cfgStyle);
|
||||
|
||||
if (cfg.style) {
|
||||
cfg.style.width = width;
|
||||
cfg.style.height = height;
|
||||
} else {
|
||||
cfg.style = { width, height };
|
||||
}
|
||||
|
||||
(this as any).updateShape(cfg, item, style, false);
|
||||
},
|
||||
updateShape(cfg: ComboConfig, item: Item, keyShapeStyle: object) {
|
||||
const keyShape = item.get('keyShape');
|
||||
const animate = cfg.animate === undefined ? this.options.animate : cfg.animate;
|
||||
if (animate && keyShape.animate) {
|
||||
keyShape.animate(keyShapeStyle, {
|
||||
duration: 200,
|
||||
easing: 'easeLinear',
|
||||
});
|
||||
} else {
|
||||
keyShape.attr({
|
||||
...keyShapeStyle,
|
||||
});
|
||||
}
|
||||
|
||||
(this as any).updateLabel(cfg, item);
|
||||
},
|
||||
},
|
||||
'single-combo',
|
||||
);
|
653
packages/core/src/shape/edge.ts
Normal file
653
packages/core/src/shape/edge.ts
Normal file
@ -0,0 +1,653 @@
|
||||
/**
|
||||
* @fileOverview 自定义边
|
||||
* @description 自定义边中有大量逻辑同自定义节点重复,虽然可以提取成为 mixin ,但是考虑到代码的可读性,还是单独实现。
|
||||
* @author dxq613@gmail.com
|
||||
*/
|
||||
|
||||
import { Point } from '@antv/g-base/lib/types';
|
||||
import { Group as GGroup } from '@antv/g-canvas';
|
||||
import { IShape, IElement } from '@antv/g-canvas/lib/interfaces';
|
||||
import { deepMix, mix, each, isNil, isNumber, isArray } from '@antv/util';
|
||||
import { ILabelConfig, ShapeOptions } from '../interface/shape';
|
||||
import { EdgeConfig, EdgeData, IPoint, LabelStyle, ShapeStyle, Item, ModelConfig } from '../types';
|
||||
import { getLabelPosition, getLoopCfgs } from '../util/graphic';
|
||||
import { distance, getCircleCenterByPoints } from '../util/math';
|
||||
import { getControlPoint, getSpline } from '../util/path';
|
||||
import Global from '../global';
|
||||
import Shape from './shape';
|
||||
import { shapeBase, CLS_LABEL_BG_SUFFIX } from './shapeBase';
|
||||
import { Path } from '@antv/g-canvas/lib/shape';
|
||||
|
||||
const CLS_SHAPE = 'edge-shape';
|
||||
|
||||
// start,end 倒置,center 不变
|
||||
function revertAlign(labelPosition: string): string {
|
||||
let textAlign = labelPosition;
|
||||
if (labelPosition === 'start') {
|
||||
textAlign = 'end';
|
||||
} else if (labelPosition === 'end') {
|
||||
textAlign = 'start';
|
||||
}
|
||||
return textAlign;
|
||||
}
|
||||
|
||||
const singleEdge: ShapeOptions = {
|
||||
itemType: 'edge',
|
||||
/**
|
||||
* 文本的位置
|
||||
* @type {String}
|
||||
*/
|
||||
labelPosition: 'center', // start, end, center
|
||||
/**
|
||||
* 文本的 x 偏移
|
||||
* @type {Number}
|
||||
*/
|
||||
refX: 0,
|
||||
/**
|
||||
* 文本的 y 偏移
|
||||
* @type {Number}
|
||||
*/
|
||||
refY: 0,
|
||||
/**
|
||||
* 文本是否跟着线自动旋转,默认 false
|
||||
* @type {Boolean}
|
||||
*/
|
||||
labelAutoRotate: false,
|
||||
|
||||
// 自定义边时的配置
|
||||
options: {
|
||||
size: Global.defaultEdge.size,
|
||||
style: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
stroke: Global.defaultEdge.style.stroke,
|
||||
lineAppendWidth: Global.defaultEdge.style.lineAppendWidth
|
||||
},
|
||||
labelCfg: {
|
||||
style: {
|
||||
fill: Global.edgeLabel.style.fill,
|
||||
fontSize: Global.edgeLabel.style.fontSize,
|
||||
},
|
||||
},
|
||||
stateStyles: {
|
||||
...Global.edgeStateStyles
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 获取边的 path
|
||||
* @internal 供扩展的边覆盖
|
||||
* @param {Array} points 构成边的点的集合
|
||||
* @return {Array} 构成 path 的数组
|
||||
*/
|
||||
getPath(points: Point[]): Array<Array<string | number>> {
|
||||
const path: Array<Array<string | number>> = [];
|
||||
each(points, (point: Point, index: number) => {
|
||||
if (index === 0) {
|
||||
path.push(['M', point.x, point.y]);
|
||||
} else {
|
||||
path.push(['L', point.x, point.y]);
|
||||
}
|
||||
});
|
||||
return path;
|
||||
},
|
||||
getShapeStyle(cfg: EdgeConfig): ShapeStyle {
|
||||
const { style: defaultStyle } = this.options as ModelConfig;
|
||||
const strokeStyle: ShapeStyle = {
|
||||
stroke: cfg.color,
|
||||
};
|
||||
// 如果设置了color,则覆盖默认的stroke属性
|
||||
const style: ShapeStyle = mix({}, defaultStyle, strokeStyle, cfg.style);
|
||||
|
||||
const size = cfg.size || Global.defaultEdge.size;
|
||||
cfg = this.getPathPoints!(cfg);
|
||||
const { startPoint, endPoint } = cfg;
|
||||
|
||||
const controlPoints = this.getControlPoints!(cfg);
|
||||
let points = [startPoint]; // 添加起始点
|
||||
// 添加控制点
|
||||
if (controlPoints) {
|
||||
points = points.concat(controlPoints);
|
||||
}
|
||||
// 添加结束点
|
||||
points.push(endPoint);
|
||||
const path = (this as any).getPath(points);
|
||||
const styles = mix(
|
||||
{},
|
||||
Global.defaultEdge.style as ShapeStyle,
|
||||
{
|
||||
stroke: Global.defaultEdge.color,
|
||||
lineWidth: size,
|
||||
path,
|
||||
} as ShapeStyle,
|
||||
style,
|
||||
);
|
||||
return styles;
|
||||
},
|
||||
updateShapeStyle(cfg: EdgeConfig, item: Item) {
|
||||
const group = item.getContainer();
|
||||
const strokeStyle: ShapeStyle = {
|
||||
stroke: cfg.color,
|
||||
};
|
||||
const shape =
|
||||
group.find((element) => element.get('className') === 'edge-shape') || item.getKeyShape();
|
||||
|
||||
const { size } = cfg;
|
||||
cfg = this.getPathPoints!(cfg);
|
||||
|
||||
const { startPoint, endPoint } = cfg;
|
||||
|
||||
const controlPoints = this.getControlPoints!(cfg); // || cfg.controlPoints;
|
||||
let points = [startPoint]; // 添加起始点
|
||||
// 添加控制点
|
||||
if (controlPoints) {
|
||||
points = points.concat(controlPoints);
|
||||
}
|
||||
// 添加结束点
|
||||
points.push(endPoint);
|
||||
|
||||
const currentAttr = shape.attr();
|
||||
const previousStyle = mix({}, strokeStyle, currentAttr, cfg.style);
|
||||
const source = cfg.sourceNode;
|
||||
const target = cfg.targetNode;
|
||||
let routeCfg: { [key: string]: unknown } = { radius: previousStyle.radius };
|
||||
if (!controlPoints) {
|
||||
routeCfg = { source, target, offset: previousStyle.offset, radius: previousStyle.radius };
|
||||
}
|
||||
if (currentAttr.endArrow && previousStyle.endArrow === false) {
|
||||
cfg.style.endArrow = {
|
||||
path: ''
|
||||
};
|
||||
}
|
||||
if (currentAttr.startArrow && previousStyle.startArrow === false) {
|
||||
cfg.style.startArrow = {
|
||||
path: ''
|
||||
};
|
||||
}
|
||||
const path = (this as any).getPath(points, routeCfg);
|
||||
const style = mix(
|
||||
strokeStyle,
|
||||
shape.attr(),
|
||||
{
|
||||
lineWidth: size,
|
||||
path
|
||||
},
|
||||
cfg.style
|
||||
);
|
||||
|
||||
if (shape) {
|
||||
shape.attr(style);
|
||||
}
|
||||
},
|
||||
getLabelStyleByPosition(cfg: EdgeConfig, labelCfg: ILabelConfig, group?: GGroup): LabelStyle {
|
||||
const labelPosition = labelCfg.position || this.labelPosition; // 文本的位置用户可以传入
|
||||
const style: LabelStyle = {};
|
||||
|
||||
const pathShape =
|
||||
group && (group.find((element) => element.get('className') === CLS_SHAPE) as Path);
|
||||
|
||||
// 不对 pathShape 进行判空,如果线不存在,说明有问题了
|
||||
let pointPercent;
|
||||
if (labelPosition === 'start') {
|
||||
pointPercent = 0;
|
||||
} else if (labelPosition === 'end') {
|
||||
pointPercent = 1;
|
||||
} else {
|
||||
pointPercent = 0.5;
|
||||
}
|
||||
// 偏移量
|
||||
const offsetX = labelCfg.refX || (this.refX as number);
|
||||
const offsetY = labelCfg.refY || (this.refY as number);
|
||||
// 如果两个节点重叠,线就变成了一个点,这时候label的位置,就是这个点 + 绝对偏移
|
||||
if (cfg.startPoint!.x === cfg.endPoint!.x && cfg.startPoint!.y === cfg.endPoint!.y) {
|
||||
style.x = cfg.startPoint!.x + offsetX;
|
||||
style.y = cfg.startPoint!.y + offsetY;
|
||||
style.text = cfg.label as string;
|
||||
return style;
|
||||
}
|
||||
|
||||
const autoRotate = isNil(labelCfg.autoRotate) ? this.labelAutoRotate : labelCfg.autoRotate;
|
||||
const offsetStyle = getLabelPosition(
|
||||
pathShape as Path,
|
||||
pointPercent,
|
||||
offsetX,
|
||||
offsetY,
|
||||
autoRotate as boolean,
|
||||
);
|
||||
style.x = offsetStyle.x;
|
||||
style.y = offsetStyle.y;
|
||||
style.rotate = offsetStyle.rotate;
|
||||
style.textAlign = this._getTextAlign!(labelPosition as string, offsetStyle.angle as number);
|
||||
style.text = cfg.label as string;
|
||||
return style;
|
||||
},
|
||||
getLabelBgStyleByPosition(
|
||||
label: IElement,
|
||||
cfg: EdgeConfig,
|
||||
labelCfg?: ILabelConfig,
|
||||
group?: GGroup,
|
||||
) {
|
||||
if (!label) {
|
||||
return {};
|
||||
}
|
||||
const bbox = label.getBBox();
|
||||
const backgroundStyle = labelCfg.style && labelCfg.style.background;
|
||||
if (!backgroundStyle) {
|
||||
return {};
|
||||
}
|
||||
const { padding } = backgroundStyle;
|
||||
const backgroundWidth = bbox.width + padding[1] + padding[3];
|
||||
const backgroundHeight = bbox.height + padding[0] + padding[2];
|
||||
const labelPosition = labelCfg.position || this.labelPosition;
|
||||
const style = {
|
||||
...backgroundStyle,
|
||||
width: backgroundWidth,
|
||||
height: backgroundHeight,
|
||||
x: bbox.minX - padding[2],
|
||||
y: bbox.minY - padding[0],
|
||||
rotate: 0,
|
||||
};
|
||||
const autoRotate = isNil(labelCfg.autoRotate) ? this.labelAutoRotate : labelCfg.autoRotate;
|
||||
|
||||
const pathShape =
|
||||
group && (group.find((element) => element.get('className') === CLS_SHAPE) as Path);
|
||||
|
||||
// 不对 pathShape 进行判空,如果线不存在,说明有问题了
|
||||
let pointPercent;
|
||||
if (labelPosition === 'start') {
|
||||
pointPercent = 0;
|
||||
} else if (labelPosition === 'end') {
|
||||
pointPercent = 1;
|
||||
} else {
|
||||
pointPercent = 0.5;
|
||||
}
|
||||
// 偏移量
|
||||
const offsetX = labelCfg.refX || (this.refX as number);
|
||||
const offsetY = labelCfg.refY || (this.refY as number);
|
||||
// // 如果两个节点重叠,线就变成了一个点,这时候label的位置,就是这个点 + 绝对偏移
|
||||
if (cfg.startPoint!.x === cfg.endPoint!.x && cfg.startPoint!.y === cfg.endPoint!.y) {
|
||||
style.x = cfg.startPoint!.x + offsetX;
|
||||
style.y = cfg.startPoint!.y + offsetY;
|
||||
return style;
|
||||
}
|
||||
|
||||
let offsetStyle = getLabelPosition(
|
||||
pathShape,
|
||||
pointPercent,
|
||||
offsetX - backgroundWidth / 2,
|
||||
offsetY + backgroundHeight / 2,
|
||||
autoRotate,
|
||||
);
|
||||
|
||||
const rad = offsetStyle.angle;
|
||||
|
||||
if (rad > (1 / 2) * Math.PI && rad < ((3 * 1) / 2) * Math.PI) {
|
||||
offsetStyle = getLabelPosition(
|
||||
pathShape,
|
||||
pointPercent,
|
||||
offsetX + backgroundWidth / 2,
|
||||
offsetY + backgroundHeight / 2,
|
||||
autoRotate,
|
||||
);
|
||||
}
|
||||
|
||||
if (autoRotate) {
|
||||
style.x = offsetStyle.x;
|
||||
style.y = offsetStyle.y;
|
||||
}
|
||||
style.rotate = offsetStyle.rotate;
|
||||
return style;
|
||||
},
|
||||
// 获取文本对齐方式
|
||||
_getTextAlign(labelPosition: string, angle: number): string {
|
||||
let textAlign = 'center';
|
||||
if (!angle) {
|
||||
return labelPosition;
|
||||
}
|
||||
angle = angle % (Math.PI * 2); // 取模
|
||||
if (labelPosition !== 'center') {
|
||||
if (
|
||||
(angle >= 0 && angle <= Math.PI / 2) ||
|
||||
(angle >= (3 / 2) * Math.PI && angle < 2 * Math.PI)
|
||||
) {
|
||||
textAlign = labelPosition;
|
||||
} else {
|
||||
textAlign = revertAlign(labelPosition);
|
||||
}
|
||||
}
|
||||
return textAlign;
|
||||
},
|
||||
/**
|
||||
* @internal 获取边的控制点
|
||||
* @param {Object} cfg 边的配置项
|
||||
* @return {Array} 控制点的数组
|
||||
*/
|
||||
getControlPoints(cfg: EdgeConfig): IPoint[] | undefined {
|
||||
return cfg.controlPoints;
|
||||
},
|
||||
/**
|
||||
* @internal 处理需要重计算点和边的情况
|
||||
* @param {Object} cfg 边的配置项
|
||||
* @return {Object} 边的配置项
|
||||
*/
|
||||
getPathPoints(cfg: EdgeConfig): EdgeConfig {
|
||||
return cfg;
|
||||
},
|
||||
/**
|
||||
* 绘制边
|
||||
* @override
|
||||
* @param {Object} cfg 边的配置项
|
||||
* @param {G.Group} group 边的容器
|
||||
* @return {IShape} 图形
|
||||
*/
|
||||
drawShape(cfg: EdgeConfig, group: GGroup): IShape {
|
||||
const shapeStyle = this.getShapeStyle!(cfg);
|
||||
const shape = group.addShape('path', {
|
||||
className: CLS_SHAPE,
|
||||
name: CLS_SHAPE,
|
||||
attrs: shapeStyle,
|
||||
});
|
||||
return shape;
|
||||
},
|
||||
drawLabel(cfg: EdgeConfig, group: GGroup): IShape {
|
||||
const { labelCfg: defaultLabelCfg } = this.options as ModelConfig;
|
||||
const labelCfg = deepMix({
|
||||
fontFamily: typeof window !== 'undefined' ? window.getComputedStyle(document.body, null).getPropertyValue("font-family") || 'Arial, sans-serif' : 'Arial, sans-serif',
|
||||
}, defaultLabelCfg, cfg.labelCfg);
|
||||
const labelStyle = this.getLabelStyle!(cfg, labelCfg, group);
|
||||
const rotate = labelStyle.rotate;
|
||||
delete labelStyle.rotate;
|
||||
const label = group.addShape('text', {
|
||||
attrs: labelStyle,
|
||||
name: 'text-shape',
|
||||
});
|
||||
if (rotate) {
|
||||
label.rotateAtStart(rotate);
|
||||
}
|
||||
|
||||
if (labelStyle.background) {
|
||||
const rect = this.drawLabelBg(cfg, group, label);
|
||||
const labelBgClassname = this.itemType + CLS_LABEL_BG_SUFFIX;
|
||||
rect.set('classname', labelBgClassname);
|
||||
label.toFront();
|
||||
}
|
||||
return label;
|
||||
},
|
||||
drawLabelBg(cfg: ModelConfig, group: GGroup, label: IElement) {
|
||||
const { labelCfg: defaultLabelCfg } = this.options as ModelConfig;
|
||||
const labelCfg = deepMix({}, defaultLabelCfg, cfg.labelCfg);
|
||||
const labelStyle = this.getLabelStyle!(cfg, labelCfg, group);
|
||||
const rotate = labelStyle.rotate;
|
||||
|
||||
const style = this.getLabelBgStyleByPosition(label, cfg, labelCfg, group);
|
||||
delete style.rotate;
|
||||
const rect = group.addShape('rect', { name: 'text-bg-shape', attrs: style });
|
||||
if (rotate) rect.rotateAtStart(rotate);
|
||||
return rect;
|
||||
},
|
||||
};
|
||||
|
||||
const singleEdgeDef = { ...shapeBase, ...singleEdge };
|
||||
Shape.registerEdge('single-edge', singleEdgeDef);
|
||||
|
||||
// 直线, 不支持控制点
|
||||
Shape.registerEdge(
|
||||
'line',
|
||||
{
|
||||
// 控制点不生效
|
||||
getControlPoints() {
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
'single-edge',
|
||||
);
|
||||
|
||||
// 直线
|
||||
Shape.registerEdge(
|
||||
'spline',
|
||||
{
|
||||
getPath(points: Point[]): Array<Array<string | number>> {
|
||||
const path = getSpline(points);
|
||||
return path;
|
||||
},
|
||||
},
|
||||
'single-edge',
|
||||
);
|
||||
|
||||
Shape.registerEdge(
|
||||
'arc',
|
||||
{
|
||||
curveOffset: 20,
|
||||
clockwise: 1,
|
||||
getControlPoints(cfg: EdgeConfig): IPoint[] {
|
||||
const { startPoint, endPoint } = cfg;
|
||||
|
||||
const midPoint = {
|
||||
x: (startPoint.x + endPoint.x) / 2,
|
||||
y: (startPoint.y + endPoint.y) / 2,
|
||||
};
|
||||
let center;
|
||||
let arcPoint;
|
||||
// 根据给定点计算圆弧
|
||||
if (cfg.controlPoints !== undefined) {
|
||||
arcPoint = cfg.controlPoints[0];
|
||||
center = getCircleCenterByPoints(startPoint, arcPoint, endPoint);
|
||||
// 根据控制点和直线关系决定 clockwise值
|
||||
if (startPoint.x <= endPoint.x && startPoint.y > endPoint.y) {
|
||||
this.clockwise = center.x > arcPoint.x ? 0 : 1;
|
||||
} else if (startPoint.x <= endPoint.x && startPoint.y < endPoint.y) {
|
||||
this.clockwise = center.x > arcPoint.x ? 1 : 0;
|
||||
} else if (startPoint.x > endPoint.x && startPoint.y <= endPoint.y) {
|
||||
this.clockwise = center.y < arcPoint.y ? 0 : 1;
|
||||
} else {
|
||||
this.clockwise = center.y < arcPoint.y ? 1 : 0;
|
||||
}
|
||||
// 若给定点和两端点共线,无法生成圆弧,绘制直线
|
||||
if (
|
||||
(arcPoint.x - startPoint.x) / (arcPoint.y - startPoint.y) ===
|
||||
(endPoint.x - startPoint.x) / (endPoint.y - startPoint.y)
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
} else {
|
||||
// 根据直线连线中点的的偏移计算圆弧
|
||||
// 若用户给定偏移量则根据其计算,否则按照默认偏移值计算
|
||||
if (cfg.curveOffset === undefined) {
|
||||
cfg.curveOffset = this.curveOffset;
|
||||
}
|
||||
if (isArray(cfg.curveOffset)) {
|
||||
cfg.curveOffset = cfg.curveOffset[0];
|
||||
}
|
||||
if (cfg.curveOffset < 0) {
|
||||
this.clockwise = 0;
|
||||
} else {
|
||||
this.clockwise = 1;
|
||||
}
|
||||
const vec = {
|
||||
x: endPoint.x - startPoint.x,
|
||||
y: endPoint.y - startPoint.y,
|
||||
};
|
||||
const edgeAngle = Math.atan2(vec.y, vec.x);
|
||||
arcPoint = {
|
||||
x: cfg.curveOffset * Math.cos(-Math.PI / 2 + edgeAngle) + midPoint.x,
|
||||
y: cfg.curveOffset * Math.sin(-Math.PI / 2 + edgeAngle) + midPoint.y,
|
||||
};
|
||||
center = getCircleCenterByPoints(startPoint, arcPoint, endPoint);
|
||||
}
|
||||
const radius = distance(startPoint, center);
|
||||
const controlPoints = [{ x: radius, y: radius }];
|
||||
|
||||
return controlPoints;
|
||||
},
|
||||
getPath(points: Point[]): Array<Array<string | number>> {
|
||||
const path: Array<Array<string | number>> = [];
|
||||
path.push(['M', points[0].x, points[0].y]);
|
||||
// 控制点与端点共线
|
||||
if (points.length === 2) {
|
||||
path.push(['L', points[1].x, points[1].y]);
|
||||
} else {
|
||||
path.push([
|
||||
'A',
|
||||
points[1].x,
|
||||
points[1].y,
|
||||
0,
|
||||
0,
|
||||
this.clockwise as number,
|
||||
points[2].x,
|
||||
points[2].y,
|
||||
]);
|
||||
}
|
||||
return path;
|
||||
},
|
||||
},
|
||||
'single-edge',
|
||||
);
|
||||
|
||||
Shape.registerEdge(
|
||||
'quadratic',
|
||||
{
|
||||
curvePosition: 0.5, // 弯曲的默认位置
|
||||
curveOffset: -20, // 弯曲度,沿着 startPoint, endPoint 的垂直向量(顺时针)方向,距离线的距离,距离越大越弯曲
|
||||
getControlPoints(cfg: EdgeConfig): IPoint[] {
|
||||
let { controlPoints } = cfg; // 指定controlPoints
|
||||
if (!controlPoints || !controlPoints.length) {
|
||||
const { startPoint, endPoint } = cfg;
|
||||
if (cfg.curveOffset === undefined) cfg.curveOffset = this.curveOffset;
|
||||
if (cfg.curvePosition === undefined) cfg.curvePosition = this.curvePosition;
|
||||
if (isArray(this.curveOffset)) cfg.curveOffset = cfg.curveOffset[0];
|
||||
if (isArray(this.curvePosition)) cfg.curvePosition = cfg.curveOffset[0];
|
||||
const innerPoint = getControlPoint(
|
||||
startPoint as Point,
|
||||
endPoint as Point,
|
||||
cfg.curvePosition as number,
|
||||
cfg.curveOffset as number,
|
||||
);
|
||||
controlPoints = [innerPoint];
|
||||
}
|
||||
return controlPoints;
|
||||
},
|
||||
getPath(points: Point[]): Array<Array<string | number>> {
|
||||
const path = [];
|
||||
path.push(['M', points[0].x, points[0].y]);
|
||||
path.push(['Q', points[1].x, points[1].y, points[2].x, points[2].y]);
|
||||
return path;
|
||||
},
|
||||
},
|
||||
'single-edge',
|
||||
);
|
||||
|
||||
Shape.registerEdge(
|
||||
'cubic',
|
||||
{
|
||||
curvePosition: [1 / 2, 1 / 2],
|
||||
curveOffset: [-20, 20],
|
||||
getControlPoints(cfg: EdgeConfig): IPoint[] {
|
||||
let { controlPoints } = cfg; // 指定controlPoints
|
||||
if (cfg.curveOffset === undefined) cfg.curveOffset = this.curveOffset;
|
||||
if (cfg.curvePosition === undefined) cfg.curvePosition = this.curvePosition;
|
||||
if (isNumber(cfg.curveOffset)) cfg.curveOffset = [cfg.curveOffset, -cfg.curveOffset];
|
||||
if (isNumber(cfg.curvePosition))
|
||||
cfg.curvePosition = [cfg.curvePosition, 1 - cfg.curvePosition];
|
||||
if (!controlPoints || !controlPoints.length || controlPoints.length < 2) {
|
||||
const { startPoint, endPoint } = cfg;
|
||||
const innerPoint1 = getControlPoint(
|
||||
startPoint as Point,
|
||||
endPoint as Point,
|
||||
cfg.curvePosition[0],
|
||||
cfg.curveOffset[0],
|
||||
);
|
||||
const innerPoint2 = getControlPoint(
|
||||
startPoint as Point,
|
||||
endPoint as Point,
|
||||
cfg.curvePosition[1],
|
||||
cfg.curveOffset[1],
|
||||
);
|
||||
controlPoints = [innerPoint1, innerPoint2];
|
||||
}
|
||||
return controlPoints;
|
||||
},
|
||||
getPath(points: Point[]): Array<Array<string | number>> {
|
||||
const path = [];
|
||||
path.push(['M', points[0].x, points[0].y]);
|
||||
path.push([
|
||||
'C',
|
||||
points[1].x,
|
||||
points[1].y,
|
||||
points[2].x,
|
||||
points[2].y,
|
||||
points[3].x,
|
||||
points[3].y,
|
||||
]);
|
||||
return path;
|
||||
},
|
||||
},
|
||||
'single-edge',
|
||||
);
|
||||
|
||||
// 垂直方向的三阶贝塞尔曲线,不再考虑用户外部传入的控制点
|
||||
Shape.registerEdge(
|
||||
'cubic-vertical',
|
||||
{
|
||||
curvePosition: [1 / 2, 1 / 2],
|
||||
getControlPoints(cfg: EdgeConfig): IPoint[] {
|
||||
const { startPoint, endPoint } = cfg;
|
||||
if (cfg.curvePosition !== undefined) this.curvePosition = cfg.curvePosition;
|
||||
if (isNumber(this.curvePosition))
|
||||
this.curvePosition = [this.curvePosition, 1 - this.curvePosition];
|
||||
const innerPoint1 = {
|
||||
x: startPoint!.x,
|
||||
y: (endPoint!.y - startPoint!.y) * (this as any).curvePosition[0] + startPoint!.y,
|
||||
};
|
||||
const innerPoint2 = {
|
||||
x: endPoint!.x,
|
||||
y: (endPoint!.y - startPoint!.y) * (this as any).curvePosition[1] + startPoint!.y,
|
||||
};
|
||||
const controlPoints = [innerPoint1, innerPoint2];
|
||||
return controlPoints;
|
||||
},
|
||||
},
|
||||
'cubic',
|
||||
);
|
||||
|
||||
// 水平方向的三阶贝塞尔曲线,不再考虑用户外部传入的控制点
|
||||
Shape.registerEdge(
|
||||
'cubic-horizontal',
|
||||
{
|
||||
curvePosition: [1 / 2, 1 / 2],
|
||||
getControlPoints(cfg: EdgeConfig): IPoint[] {
|
||||
const { startPoint, endPoint } = cfg;
|
||||
if (cfg.curvePosition !== undefined) this.curvePosition = cfg.curvePosition;
|
||||
if (isNumber(this.curvePosition))
|
||||
this.curvePosition = [this.curvePosition, 1 - this.curvePosition];
|
||||
const innerPoint1 = {
|
||||
x: (endPoint!.x - startPoint!.x) * (this as any).curvePosition[0] + startPoint!.x,
|
||||
y: startPoint!.y,
|
||||
};
|
||||
const innerPoint2 = {
|
||||
x: (endPoint!.x - startPoint!.x) * (this as any).curvePosition[1] + startPoint!.x,
|
||||
y: endPoint!.y,
|
||||
};
|
||||
const controlPoints = [innerPoint1, innerPoint2];
|
||||
return controlPoints;
|
||||
},
|
||||
},
|
||||
'cubic',
|
||||
);
|
||||
|
||||
Shape.registerEdge(
|
||||
'loop',
|
||||
{
|
||||
getPathPoints(cfg: ModelConfig): EdgeData {
|
||||
return getLoopCfgs(cfg as EdgeData);
|
||||
},
|
||||
getControlPoints(cfg: EdgeConfig): IPoint[] | undefined {
|
||||
return cfg.controlPoints;
|
||||
},
|
||||
afterDraw(cfg: EdgeConfig) {
|
||||
cfg.controlPoints = undefined;
|
||||
},
|
||||
afterUpdate(cfg: EdgeConfig) {
|
||||
cfg.controlPoints = undefined;
|
||||
},
|
||||
},
|
||||
'cubic',
|
||||
);
|
1
packages/core/src/shape/edges/index.ts
Normal file
1
packages/core/src/shape/edges/index.ts
Normal file
@ -0,0 +1 @@
|
||||
import './polyline';
|
538
packages/core/src/shape/edges/polyline-util.ts
Normal file
538
packages/core/src/shape/edges/polyline-util.ts
Normal file
@ -0,0 +1,538 @@
|
||||
import { each } from '@antv/util';
|
||||
import { INode, ICombo } from '../../interface/item';
|
||||
import { Point } from '@antv/g-base/lib/types';
|
||||
|
||||
export interface PolyPoint {
|
||||
x: number;
|
||||
y: number;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export type PBBox = Partial<{
|
||||
x: number;
|
||||
y: number;
|
||||
minX: number;
|
||||
minY: number;
|
||||
maxX: number;
|
||||
maxY: number;
|
||||
height: number;
|
||||
width: number;
|
||||
centerX: number;
|
||||
centerY: number;
|
||||
}>;
|
||||
|
||||
export const getBBoxFromPoint = (point: PolyPoint): PBBox => {
|
||||
const { x, y } = point;
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
centerX: x,
|
||||
centerY: y,
|
||||
minX: x,
|
||||
minY: y,
|
||||
maxX: x,
|
||||
maxY: y,
|
||||
height: 0,
|
||||
width: 0,
|
||||
};
|
||||
};
|
||||
export const getBBoxFromPoints = (points: PolyPoint[] = []): PBBox => {
|
||||
const xs: number[] = [];
|
||||
const ys: number[] = [];
|
||||
points.forEach((p) => {
|
||||
xs.push(p.x);
|
||||
ys.push(p.y);
|
||||
});
|
||||
const minX = Math.min.apply(Math, xs);
|
||||
const maxX = Math.max.apply(Math, xs);
|
||||
const minY = Math.min.apply(Math, ys);
|
||||
const maxY = Math.max.apply(Math, ys);
|
||||
return {
|
||||
centerX: (minX + maxX) / 2,
|
||||
centerY: (minY + maxY) / 2,
|
||||
maxX,
|
||||
maxY,
|
||||
minX,
|
||||
minY,
|
||||
height: maxY - minY,
|
||||
width: maxX - minX,
|
||||
};
|
||||
};
|
||||
export const isBBoxesOverlapping = (b1: PBBox, b2: PBBox) =>
|
||||
Math.abs(b1.centerX - b2.centerX) * 2 < b1.width + b2.width &&
|
||||
Math.abs(b1.centerY - b2.centerY) * 2 < b1.height + b2.height;
|
||||
|
||||
export const filterConnectPoints = (points: PolyPoint[]): PolyPoint[] => {
|
||||
// pre-process: remove duplicated points
|
||||
const result: any[] = [];
|
||||
const pointsMap: any = {};
|
||||
points.forEach((p) => {
|
||||
const id = `${p.x}-${p.y}`;
|
||||
p.id = id;
|
||||
pointsMap[id] = p;
|
||||
});
|
||||
each(pointsMap, (p) => {
|
||||
result.push(p);
|
||||
});
|
||||
return result;
|
||||
};
|
||||
export const simplifyPolyline = (points: PolyPoint[]): PolyPoint[] => {
|
||||
points = filterConnectPoints(points);
|
||||
return points;
|
||||
};
|
||||
export const getSimplePolyline = (sPoint: PolyPoint, tPoint: PolyPoint): PolyPoint[] => [
|
||||
sPoint,
|
||||
{ x: sPoint.x, y: tPoint.y },
|
||||
tPoint,
|
||||
];
|
||||
|
||||
export const getExpandedBBox = (bbox: any, offset: number): PBBox => {
|
||||
if (bbox.width === 0 && bbox.height === 0) {
|
||||
// when it is a point
|
||||
return bbox;
|
||||
}
|
||||
return {
|
||||
centerX: bbox.centerX,
|
||||
centerY: bbox.centerY,
|
||||
minX: bbox.minX - offset,
|
||||
minY: bbox.minY - offset,
|
||||
maxX: bbox.maxX + offset,
|
||||
maxY: bbox.maxY + offset,
|
||||
height: bbox.height + 2 * offset,
|
||||
width: bbox.width + 2 * offset,
|
||||
};
|
||||
};
|
||||
export const isHorizontalPort = (port: PolyPoint, bbox: PBBox): boolean => {
|
||||
const dx = Math.abs(port.x - bbox.centerX);
|
||||
const dy = Math.abs(port.y - bbox.centerY);
|
||||
return dx / bbox.width > dy / bbox.height;
|
||||
};
|
||||
export const getExpandedBBoxPoint = (bbox: any, point: PolyPoint): PolyPoint => {
|
||||
const isHorizontal = isHorizontalPort(point, bbox);
|
||||
if (isHorizontal) {
|
||||
return {
|
||||
x: point.x > bbox.centerX ? bbox.maxX : bbox.minX,
|
||||
y: point.y,
|
||||
};
|
||||
}
|
||||
return {
|
||||
x: point.x,
|
||||
y: point.y > bbox.centerY ? bbox.maxY : bbox.minY,
|
||||
};
|
||||
};
|
||||
/**
|
||||
*
|
||||
* @param b1
|
||||
* @param b2
|
||||
*/
|
||||
export const mergeBBox = (b1: PBBox, b2: PBBox): PBBox => {
|
||||
const minX = Math.min(b1.minX, b2.minX);
|
||||
const minY = Math.min(b1.minY, b2.minY);
|
||||
const maxX = Math.max(b1.maxX, b2.maxX);
|
||||
const maxY = Math.max(b1.maxY, b2.maxY);
|
||||
return {
|
||||
centerX: (minX + maxX) / 2,
|
||||
centerY: (minY + maxY) / 2,
|
||||
minX,
|
||||
minY,
|
||||
maxX,
|
||||
maxY,
|
||||
height: maxY - minY,
|
||||
width: maxX - minX,
|
||||
};
|
||||
};
|
||||
export const getPointsFromBBox = (bbox: PBBox): PolyPoint[] => {
|
||||
// anticlockwise
|
||||
const { minX, minY, maxX, maxY } = bbox;
|
||||
return [
|
||||
{
|
||||
x: minX,
|
||||
y: minY,
|
||||
},
|
||||
{
|
||||
x: maxX,
|
||||
y: minY,
|
||||
},
|
||||
{
|
||||
x: maxX,
|
||||
y: maxY,
|
||||
},
|
||||
{
|
||||
x: minX,
|
||||
y: maxY,
|
||||
},
|
||||
];
|
||||
};
|
||||
export const isPointOutsideBBox = (point: PolyPoint, bbox: PBBox): boolean => {
|
||||
const { x, y } = point;
|
||||
return x < bbox.minX || x > bbox.maxX || y < bbox.minY || y > bbox.maxY;
|
||||
};
|
||||
export const getBBoxXCrossPoints = (bbox: PBBox, x: number): PolyPoint[] => {
|
||||
if (x < bbox.minX || x > bbox.maxX) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
x,
|
||||
y: bbox.minY,
|
||||
},
|
||||
{
|
||||
x,
|
||||
y: bbox.maxY,
|
||||
},
|
||||
];
|
||||
};
|
||||
export const getBBoxYCrossPoints = (bbox: PBBox, y: number): PolyPoint[] => {
|
||||
if (y < bbox.minY || y > bbox.maxY) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
x: bbox.minX,
|
||||
y,
|
||||
},
|
||||
{
|
||||
x: bbox.maxX,
|
||||
y,
|
||||
},
|
||||
];
|
||||
};
|
||||
export const getBBoxCrossPointsByPoint = (bbox: PBBox, point: PolyPoint): PolyPoint[] =>
|
||||
getBBoxXCrossPoints(bbox, point.x).concat(getBBoxYCrossPoints(bbox, point.y));
|
||||
|
||||
/**
|
||||
* 曼哈顿距离
|
||||
*/
|
||||
export const distance = (p1: PolyPoint, p2: PolyPoint): number =>
|
||||
Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y);
|
||||
|
||||
/**
|
||||
* 如果 points 中的一个节点 x 与 p 相等,则消耗 -2。y 同
|
||||
* 即优先选择和 points 在同一水平线 / 垂直线上的点
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export const _costByPoints = (p: PolyPoint, points: PolyPoint[]): number => {
|
||||
const offset = -2;
|
||||
let result = 0;
|
||||
points.forEach((point) => {
|
||||
if (point) {
|
||||
if (p.x === point.x) {
|
||||
result += offset;
|
||||
}
|
||||
if (p.y === point.y) {
|
||||
result += offset;
|
||||
}
|
||||
}
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* ps 经过 p 到 pt 的距离,减去其他路过节点造成的消耗
|
||||
*/
|
||||
export const heuristicCostEstimate = (
|
||||
p: PolyPoint,
|
||||
ps: PolyPoint,
|
||||
pt: PolyPoint,
|
||||
source?: PolyPoint,
|
||||
target?: PolyPoint,
|
||||
): number => distance(p, ps) + distance(p, pt) + _costByPoints(p, [ps, pt, source!, target!]);
|
||||
|
||||
export const reconstructPath = (
|
||||
pathPoints: PolyPoint[],
|
||||
pointById: any,
|
||||
cameFrom: any,
|
||||
currentId: string,
|
||||
iterator: number = 0,
|
||||
) => {
|
||||
pathPoints.unshift(pointById[currentId]);
|
||||
if (cameFrom[currentId] && cameFrom[currentId] !== currentId && iterator <= 100) {
|
||||
reconstructPath(pathPoints, pointById, cameFrom, cameFrom[currentId], iterator + 1);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 从 arr 中删去 item
|
||||
*/
|
||||
export const removeFrom = (arr: PolyPoint[], item: PolyPoint) => {
|
||||
const index = arr.indexOf(item);
|
||||
if (index > -1) {
|
||||
arr.splice(index, 1);
|
||||
}
|
||||
};
|
||||
export const isSegmentsIntersected = (
|
||||
p0: PolyPoint,
|
||||
p1: PolyPoint,
|
||||
p2: PolyPoint,
|
||||
p3: PolyPoint,
|
||||
): boolean => {
|
||||
const s1X = p1.x - p0.x;
|
||||
const s1Y = p1.y - p0.y;
|
||||
const s2X = p3.x - p2.x;
|
||||
const s2Y = p3.y - p2.y;
|
||||
|
||||
const s = (-s1Y * (p0.x - p2.x) + s1X * (p0.y - p2.y)) / (-s2X * s1Y + s1X * s2Y);
|
||||
const t = (s2X * (p0.y - p2.y) - s2Y * (p0.x - p2.x)) / (-s2X * s1Y + s1X * s2Y);
|
||||
|
||||
return s >= 0 && s <= 1 && t >= 0 && t <= 1;
|
||||
};
|
||||
export const isSegmentCrossingBBox = (p1: PolyPoint, p2: PolyPoint, bbox: PBBox): boolean => {
|
||||
if (bbox.width === 0 && bbox.height === 0) {
|
||||
return false;
|
||||
}
|
||||
const [pa, pb, pc, pd] = getPointsFromBBox(bbox);
|
||||
return (
|
||||
isSegmentsIntersected(p1, p2, pa, pb) ||
|
||||
isSegmentsIntersected(p1, p2, pa, pd) ||
|
||||
isSegmentsIntersected(p1, p2, pb, pc) ||
|
||||
isSegmentsIntersected(p1, p2, pc, pd)
|
||||
);
|
||||
};
|
||||
/**
|
||||
* 在 points 中找到满足 x 或 y 和 point 的 x 或 y 相等,且与 point 连线不经过 bbox1 与 bbox2 的点
|
||||
*/
|
||||
export const getNeighborPoints = (
|
||||
points: PolyPoint[],
|
||||
point: PolyPoint,
|
||||
bbox1: PBBox,
|
||||
bbox2: PBBox,
|
||||
): PolyPoint[] => {
|
||||
const neighbors: Point[] = [];
|
||||
points.forEach((p) => {
|
||||
if (p !== point) {
|
||||
if (p.x === point.x || p.y === point.y) {
|
||||
if (!isSegmentCrossingBBox(p, point, bbox1) && !isSegmentCrossingBBox(p, point, bbox2)) {
|
||||
neighbors.push(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return filterConnectPoints(neighbors);
|
||||
};
|
||||
export const pathFinder = (
|
||||
points: PolyPoint[],
|
||||
start: PolyPoint,
|
||||
goal: any,
|
||||
sBBox: PBBox,
|
||||
tBBox: PBBox,
|
||||
os: any,
|
||||
ot: any,
|
||||
): PolyPoint[] => {
|
||||
// A-Star Algorithm
|
||||
const closedSet: any = [];
|
||||
const openSet = [start];
|
||||
const cameFrom: {
|
||||
[key: string]: any;
|
||||
} = {};
|
||||
|
||||
const gScore: {
|
||||
[key: string]: number;
|
||||
} = {}; // all default values are Infinity
|
||||
|
||||
const fScore: {
|
||||
[key: string]: number;
|
||||
} = {}; // all default values are Infinity
|
||||
|
||||
gScore[start.id] = 0;
|
||||
fScore[start.id] = heuristicCostEstimate(start, goal, start);
|
||||
|
||||
const pointById: {
|
||||
[key: string]: PolyPoint;
|
||||
} = {};
|
||||
|
||||
points.forEach((p) => {
|
||||
pointById[p.id] = p;
|
||||
});
|
||||
|
||||
while (openSet.length) {
|
||||
let current: any;
|
||||
let lowestFScore = Infinity;
|
||||
// 找到 openSet 中 fScore 最小的点
|
||||
openSet.forEach((p: any) => {
|
||||
if (fScore[p.id] < lowestFScore) {
|
||||
lowestFScore = fScore[p.id];
|
||||
current = p;
|
||||
}
|
||||
});
|
||||
|
||||
// 若 openSet 中 fScore 最小的点就是终点
|
||||
if (current === goal) {
|
||||
// ending condition
|
||||
const pathPoints: any = [];
|
||||
reconstructPath(pathPoints, pointById, cameFrom, goal.id);
|
||||
return pathPoints;
|
||||
}
|
||||
|
||||
removeFrom(openSet, current);
|
||||
closedSet.push(current);
|
||||
|
||||
getNeighborPoints(points, current, sBBox, tBBox).forEach((neighbor) => {
|
||||
if (closedSet.indexOf(neighbor) !== -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (openSet.indexOf(neighbor) === -1) {
|
||||
openSet.push(neighbor);
|
||||
}
|
||||
|
||||
const tentativeGScore = fScore[current.id] + distance(current, neighbor); // + distance(neighbor, goal);
|
||||
if (gScore[neighbor.id] && tentativeGScore >= gScore[neighbor.id]) {
|
||||
return;
|
||||
}
|
||||
|
||||
cameFrom[neighbor.id] = current.id;
|
||||
gScore[neighbor.id] = tentativeGScore;
|
||||
fScore[neighbor.id] =
|
||||
gScore[neighbor.id] + heuristicCostEstimate(neighbor, goal, start, os, ot);
|
||||
});
|
||||
}
|
||||
// throw new Error('Cannot find path');
|
||||
return [start, goal];
|
||||
};
|
||||
export const isBending = (p0: PolyPoint, p1: PolyPoint, p2: PolyPoint): boolean =>
|
||||
!((p0.x === p1.x && p1.x === p2.x) || (p0.y === p1.y && p1.y === p2.y));
|
||||
|
||||
export const getBorderRadiusPoints = (
|
||||
p0: PolyPoint,
|
||||
p1: PolyPoint,
|
||||
p2: PolyPoint,
|
||||
r: number,
|
||||
): PolyPoint[] => {
|
||||
const d0 = distance(p0, p1);
|
||||
const d1 = distance(p2, p1);
|
||||
if (d0 < r) {
|
||||
r = d0;
|
||||
}
|
||||
if (d1 < r) {
|
||||
r = d1;
|
||||
}
|
||||
const ps = {
|
||||
x: p1.x - (r / d0) * (p1.x - p0.x),
|
||||
y: p1.y - (r / d0) * (p1.y - p0.y),
|
||||
};
|
||||
const pt = {
|
||||
x: p1.x - (r / d1) * (p1.x - p2.x),
|
||||
y: p1.y - (r / d1) * (p1.y - p2.y),
|
||||
};
|
||||
return [ps, pt];
|
||||
};
|
||||
export const getPathWithBorderRadiusByPolyline = (
|
||||
points: PolyPoint[],
|
||||
borderRadius: number,
|
||||
): string => {
|
||||
// TODO
|
||||
const pathSegments: string[] = [];
|
||||
const startPoint = points[0];
|
||||
pathSegments.push(`M${startPoint.x} ${startPoint.y}`);
|
||||
points.forEach((p, i) => {
|
||||
const p1 = points[i + 1];
|
||||
const p2 = points[i + 2];
|
||||
if (p1 && p2) {
|
||||
if (isBending(p, p1, p2)) {
|
||||
const [ps, pt] = getBorderRadiusPoints(p, p1, p2, borderRadius);
|
||||
pathSegments.push(`L${ps.x} ${ps.y}`);
|
||||
pathSegments.push(`Q${p1.x} ${p1.y} ${pt.x} ${pt.y}`);
|
||||
pathSegments.push(`L${pt.x} ${pt.y}`);
|
||||
} else {
|
||||
pathSegments.push(`L${p1.x} ${p1.y}`);
|
||||
}
|
||||
} else if (p1) {
|
||||
pathSegments.push(`L${p1.x} ${p1.y}`);
|
||||
}
|
||||
});
|
||||
return pathSegments.join('');
|
||||
};
|
||||
export const getPolylinePoints = (
|
||||
start: PolyPoint,
|
||||
end: PolyPoint,
|
||||
sNode: INode | ICombo,
|
||||
tNode: INode | ICombo,
|
||||
offset: number,
|
||||
): PolyPoint[] => {
|
||||
let sBBox: PBBox, tBBox: PBBox;
|
||||
|
||||
if (!sNode || !sNode.getType()) {
|
||||
sBBox = getBBoxFromPoint(start);
|
||||
} else if (sNode.getType() === 'combo') {
|
||||
const sNodeKeyShape = sNode.getKeyShape();
|
||||
sBBox = sNodeKeyShape.getCanvasBBox() || (getBBoxFromPoint(start) as PBBox);
|
||||
sBBox.centerX = (sBBox.minX + sBBox.maxX) / 2;
|
||||
sBBox.centerY = (sBBox.minY + sBBox.maxY) / 2;
|
||||
} else {
|
||||
sBBox = sNode.getBBox();
|
||||
}
|
||||
|
||||
if (!tNode || !tNode.getType()) {
|
||||
tBBox = getBBoxFromPoint(end);
|
||||
} else if (tNode.getType() === 'combo') {
|
||||
const tNodeKeyShape = tNode.getKeyShape();
|
||||
tBBox = tNodeKeyShape.getCanvasBBox() || (getBBoxFromPoint(end) as PBBox);
|
||||
tBBox.centerX = (tBBox.minX + tBBox.maxX) / 2;
|
||||
tBBox.centerY = (tBBox.minY + tBBox.maxY) / 2;
|
||||
} else {
|
||||
tBBox = tNode && tNode.getBBox();
|
||||
}
|
||||
|
||||
if (isBBoxesOverlapping(sBBox, tBBox)) {
|
||||
// source and target nodes are overlapping
|
||||
return simplifyPolyline(getSimplePolyline(start, end));
|
||||
}
|
||||
const sxBBox = getExpandedBBox(sBBox, offset);
|
||||
const txBBox = getExpandedBBox(tBBox, offset);
|
||||
if (isBBoxesOverlapping(sxBBox, txBBox)) {
|
||||
// the expanded bounding boxes of source and target nodes are overlapping
|
||||
return simplifyPolyline(getSimplePolyline(start, end));
|
||||
}
|
||||
const sPoint = getExpandedBBoxPoint(sxBBox, start);
|
||||
const tPoint = getExpandedBBoxPoint(txBBox, end);
|
||||
const lineBBox = getBBoxFromPoints([sPoint, tPoint]);
|
||||
const outerBBox = mergeBBox(sxBBox, txBBox);
|
||||
const sMixBBox = mergeBBox(sxBBox, lineBBox);
|
||||
const tMixBBox = mergeBBox(txBBox, lineBBox);
|
||||
let connectPoints: any = [];
|
||||
connectPoints = connectPoints.concat(
|
||||
getPointsFromBBox(sMixBBox), // .filter(p => !isPointIntersectBBox(p, txBBox))
|
||||
);
|
||||
connectPoints = connectPoints.concat(
|
||||
getPointsFromBBox(tMixBBox), // .filter(p => !isPointIntersectBBox(p, sxBBox))
|
||||
);
|
||||
const centerPoint = {
|
||||
x: (start.x + end.x) / 2,
|
||||
y: (start.y + end.y) / 2,
|
||||
};
|
||||
[lineBBox, sMixBBox, tMixBBox].forEach((bbox: PBBox) => {
|
||||
connectPoints = connectPoints.concat(
|
||||
getBBoxCrossPointsByPoint(bbox, centerPoint).filter(
|
||||
(p) => isPointOutsideBBox(p, sxBBox) && isPointOutsideBBox(p, txBBox),
|
||||
),
|
||||
);
|
||||
});
|
||||
[
|
||||
{
|
||||
x: sPoint.x,
|
||||
y: tPoint.y,
|
||||
},
|
||||
{
|
||||
x: tPoint.x,
|
||||
y: sPoint.y,
|
||||
},
|
||||
].forEach((p) => {
|
||||
// impossible!!
|
||||
if (
|
||||
isPointOutsideBBox(p, sxBBox) &&
|
||||
isPointOutsideBBox(p, txBBox) // &&
|
||||
// isPointInsideBBox(p, sMixBBox) && isPointInsideBBox(p, tMixBBox)
|
||||
) {
|
||||
connectPoints.push(p);
|
||||
}
|
||||
});
|
||||
connectPoints.unshift(sPoint);
|
||||
connectPoints.push(tPoint);
|
||||
// filter out dulplicated points in connectPoints
|
||||
connectPoints = filterConnectPoints(connectPoints); // , sxBBox, txBBox, outerBBox
|
||||
const pathPoints = pathFinder(connectPoints, sPoint, tPoint, sBBox, tBBox, start, end);
|
||||
pathPoints.unshift(start);
|
||||
pathPoints.push(end);
|
||||
|
||||
return simplifyPolyline(pathPoints);
|
||||
};
|
198
packages/core/src/shape/edges/polyline.ts
Normal file
198
packages/core/src/shape/edges/polyline.ts
Normal file
@ -0,0 +1,198 @@
|
||||
import { Point } from '@antv/g-base/lib/types';
|
||||
import { Group } from '@antv/g-canvas';
|
||||
import { mix, each, isArray, isString } from '@antv/util';
|
||||
import { ShapeStyle, EdgeConfig, Item } from '../../types';
|
||||
import { pointsToPolygon } from '../../util/path';
|
||||
import Global from '../../global';
|
||||
import Shape from '../shape';
|
||||
import { getPathWithBorderRadiusByPolyline } from './polyline-util';
|
||||
import { RouterCfg, pathFinder } from './router';
|
||||
import { INode } from '../../interface/item';
|
||||
|
||||
// 折线
|
||||
Shape.registerEdge(
|
||||
'polyline',
|
||||
{
|
||||
options: {
|
||||
color: Global.defaultEdge.color,
|
||||
size: Global.defaultEdge.size,
|
||||
style: {
|
||||
radius: 0,
|
||||
offset: 15,
|
||||
x: 0,
|
||||
y: 0,
|
||||
stroke: Global.defaultEdge.style.stroke,
|
||||
lineAppendWidth: Global.defaultEdge.style.lineAppendWidth
|
||||
},
|
||||
// 文本样式配置
|
||||
labelCfg: {
|
||||
style: {
|
||||
fill: Global.edgeLabel.style.fill,
|
||||
fontSize: Global.edgeLabel.style.fontSize,
|
||||
},
|
||||
},
|
||||
routeCfg: {
|
||||
obstacles: [], // 希望边绕过的障碍节点
|
||||
maxAllowedDirectionChange: 90, // 允许的最大转角
|
||||
maximumLoops: 1000,
|
||||
gridSize: 10, // 指定精度
|
||||
},
|
||||
stateStyles: {
|
||||
...Global.edgeStateStyles
|
||||
}
|
||||
},
|
||||
shapeType: 'polyline',
|
||||
// 文本位置
|
||||
labelPosition: 'center',
|
||||
drawShape(cfg: EdgeConfig, group: Group) {
|
||||
const shapeStyle = (this as any).getShapeStyle(cfg);
|
||||
if (shapeStyle.radius === 0) delete shapeStyle.radius;
|
||||
const keyShape = group.addShape('path', {
|
||||
className: 'edge-shape',
|
||||
name: 'edge-shape',
|
||||
attrs: shapeStyle,
|
||||
});
|
||||
return keyShape;
|
||||
},
|
||||
getShapeStyle(cfg: EdgeConfig): ShapeStyle {
|
||||
const { style: defaultStyle } = this.options;
|
||||
|
||||
const strokeStyle: ShapeStyle = {
|
||||
stroke: cfg.color,
|
||||
};
|
||||
|
||||
const style: ShapeStyle = mix({}, defaultStyle, strokeStyle, cfg.style);
|
||||
cfg = (this as any).getPathPoints(cfg);
|
||||
|
||||
this.radius = style.radius;
|
||||
this.offset = style.offset;
|
||||
|
||||
const { startPoint, endPoint } = cfg;
|
||||
const controlPoints = (this as any).getControlPoints(cfg);
|
||||
let points = [startPoint]; // 添加起始点
|
||||
// 添加控制点
|
||||
if (controlPoints) {
|
||||
points = points.concat(controlPoints);
|
||||
}
|
||||
// 添加结束点
|
||||
points.push(endPoint);
|
||||
const source = cfg.sourceNode;
|
||||
const target = cfg.targetNode;
|
||||
const radius = style.radius;
|
||||
const { routeCfg } = this.options;
|
||||
routeCfg.offset = style.offset;
|
||||
|
||||
let path = (this as any).getPath(points, source, target, radius, routeCfg);
|
||||
if ((isArray(path) && path.length <= 1) || (isString(path) && path.indexOf('L') === -1)) {
|
||||
path = 'M0 0, L0 0';
|
||||
}
|
||||
if (isNaN(startPoint.x) || isNaN(startPoint.y) || isNaN(endPoint.x) || isNaN(endPoint.y)) {
|
||||
path = 'M0 0, L0 0';
|
||||
}
|
||||
|
||||
const attrs: ShapeStyle = mix({}, Global.defaultEdge.style as ShapeStyle, style, {
|
||||
lineWidth: cfg.size,
|
||||
path,
|
||||
} as ShapeStyle);
|
||||
return attrs;
|
||||
},
|
||||
updateShapeStyle(cfg: EdgeConfig, item: Item) {
|
||||
const group = item.getContainer();
|
||||
const strokeStyle: ShapeStyle = {
|
||||
stroke: cfg.color,
|
||||
};
|
||||
const shape =
|
||||
group.find((element) => element.get('className') === 'edge-shape') || item.getKeyShape();
|
||||
|
||||
const { size } = cfg;
|
||||
cfg = this.getPathPoints!(cfg);
|
||||
|
||||
const { startPoint, endPoint } = cfg;
|
||||
|
||||
const controlPoints = this.getControlPoints!(cfg); // || cfg.controlPoints;
|
||||
let points = [startPoint]; // 添加起始点
|
||||
// 添加控制点
|
||||
if (controlPoints) {
|
||||
points = points.concat(controlPoints);
|
||||
}
|
||||
// 添加结束点
|
||||
points.push(endPoint);
|
||||
|
||||
const currentAttr = shape.attr();
|
||||
const previousStyle = mix({}, strokeStyle, currentAttr, cfg.style);
|
||||
const source = cfg.sourceNode;
|
||||
const target = cfg.targetNode;
|
||||
const radius = previousStyle.radius;
|
||||
const { routeCfg } = this.options;
|
||||
routeCfg.offset = previousStyle.offset;
|
||||
let path = (this as any).getPath(points, source, target, radius, routeCfg);
|
||||
if ((isArray(path) && path.length <= 1) || (isString(path) && path.indexOf('L') === -1)) {
|
||||
path = 'M0 0, L0 0';
|
||||
}
|
||||
if (isNaN(startPoint.x) || isNaN(startPoint.y) || isNaN(endPoint.x) || isNaN(endPoint.y)) {
|
||||
path = 'M0 0, L0 0';
|
||||
}
|
||||
if (currentAttr.endArrow && previousStyle.endArrow === false) {
|
||||
cfg.style.endArrow = {
|
||||
path: ''
|
||||
};
|
||||
}
|
||||
if (currentAttr.startArrow && previousStyle.startArrow === false) {
|
||||
cfg.style.startArrow = {
|
||||
path: ''
|
||||
};
|
||||
}
|
||||
const style = mix(
|
||||
strokeStyle,
|
||||
shape.attr(),
|
||||
{
|
||||
lineWidth: size,
|
||||
path
|
||||
},
|
||||
cfg.style,
|
||||
);
|
||||
|
||||
if (shape) {
|
||||
shape.attr(style);
|
||||
}
|
||||
},
|
||||
getPath(
|
||||
points: Point[],
|
||||
source: INode,
|
||||
target: INode,
|
||||
radius: number,
|
||||
routeCfg?: RouterCfg,
|
||||
): Array<Array<string | number>> | string {
|
||||
const { offset } = routeCfg;
|
||||
// 指定了控制点
|
||||
if (!offset || points.length > 2) {
|
||||
if (radius) {
|
||||
return getPathWithBorderRadiusByPolyline(points, radius);
|
||||
}
|
||||
|
||||
const pathArray: Array<Array<string | number>> = [];
|
||||
each(points, (point, index) => {
|
||||
if (index === 0) {
|
||||
pathArray.push(['M', point.x, point.y]);
|
||||
} else {
|
||||
pathArray.push(['L', point.x, point.y]);
|
||||
}
|
||||
});
|
||||
return pathArray;
|
||||
}
|
||||
|
||||
// 未指定控制点
|
||||
let polylinePoints: any;
|
||||
if (radius) {
|
||||
polylinePoints = pathFinder(points[0], points[points.length - 1], source, target, routeCfg);
|
||||
const res = getPathWithBorderRadiusByPolyline(polylinePoints, radius);
|
||||
return res;
|
||||
}
|
||||
|
||||
polylinePoints = pathFinder(points[0], points[points.length - 1], source, target, routeCfg);
|
||||
const res = pointsToPolygon(polylinePoints);
|
||||
return res;
|
||||
},
|
||||
},
|
||||
'single-edge',
|
||||
);
|
429
packages/core/src/shape/edges/router.ts
Normal file
429
packages/core/src/shape/edges/router.ts
Normal file
@ -0,0 +1,429 @@
|
||||
/**
|
||||
* 通过配置不同的 costFunc, distFunc, constraints 可以得到不同效果的 router
|
||||
* generalRouter: 不限制搜索时的移动方向,避开障碍即可
|
||||
* orthogonal: 线必须沿着竖直或水平方向(4个方向)
|
||||
* octolinearRouter: 线沿着竖直、水平、对角线方向(8个方向)
|
||||
*/
|
||||
import { INode } from '../../interface/item';
|
||||
import { Item } from '../../types';
|
||||
import { deepMix } from '@antv/util';
|
||||
import { getLineIntersect } from '../../util/math';
|
||||
import {
|
||||
getExpandedBBox,
|
||||
getExpandedBBoxPoint,
|
||||
PolyPoint,
|
||||
getPolylinePoints,
|
||||
simplifyPolyline,
|
||||
isSegmentCrossingBBox,
|
||||
} from './polyline-util';
|
||||
|
||||
export interface RouterCfg {
|
||||
offset?: number; // 连线和点的间距
|
||||
gridSize?: number;
|
||||
obstacles?: Item[];
|
||||
maxAllowedDirectionChange?: number; // 允许的最大转角
|
||||
directions?: any[]; // 允许的边的方向
|
||||
startDirections?: string[]; // 边从点出发的方向(e.g. 从上拐 / 从下拐)
|
||||
penalties?: {}; // 附加的分数
|
||||
distFunc?: (p1: PolyPoint, p2: PolyPoint) => number;
|
||||
fallbackRoute?: (
|
||||
p1: PolyPoint,
|
||||
p2: PolyPoint,
|
||||
startNode?: INode,
|
||||
endNode?: INode,
|
||||
cfg?: RouterCfg,
|
||||
) => PolyPoint[]; // 若找不到符合要求的路径时的退化路径方案
|
||||
maximumLoops?: number;
|
||||
}
|
||||
|
||||
const manhattanDist = (p1: PolyPoint, p2: PolyPoint): number =>
|
||||
Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y);
|
||||
|
||||
const eucliDist = (p1: PolyPoint, p2: PolyPoint): number =>
|
||||
Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2);
|
||||
|
||||
const straightPath = (start: PolyPoint, end: PolyPoint): PolyPoint[] => {
|
||||
// console.warn('fallbackRoute: straight path');
|
||||
return [start, end];
|
||||
};
|
||||
|
||||
const simplePolyline = (
|
||||
start: PolyPoint,
|
||||
end: PolyPoint,
|
||||
startNode: INode,
|
||||
endNode: INode,
|
||||
cfg: RouterCfg,
|
||||
) => {
|
||||
// console.warn('fallbackRoute: simple polyline path');
|
||||
return simplifyPolyline(getPolylinePoints(start, end, startNode, endNode, cfg.offset));
|
||||
};
|
||||
// getPolylinePoints
|
||||
|
||||
const defaultCfg: RouterCfg = {
|
||||
offset: 20,
|
||||
maxAllowedDirectionChange: 90,
|
||||
maximumLoops: 2000,
|
||||
gridSize: 10,
|
||||
directions: [
|
||||
{ stepX: 1, stepY: 0 }, // right
|
||||
{ stepX: -1, stepY: 0 }, // left
|
||||
{ stepX: 0, stepY: 1 }, // bottom
|
||||
{ stepX: 0, stepY: -1 }, // top
|
||||
],
|
||||
get penalties() {
|
||||
return {
|
||||
0: 0,
|
||||
45: this.gridSize / 2,
|
||||
90: this.gridSize / 2,
|
||||
};
|
||||
},
|
||||
distFunc: manhattanDist,
|
||||
fallbackRoute: simplePolyline,
|
||||
};
|
||||
|
||||
export const octolinearCfg: RouterCfg = {
|
||||
maxAllowedDirectionChange: 45,
|
||||
// 8 个方向: 上下左右 + 45度斜线方向
|
||||
directions: [
|
||||
{ stepX: 1, stepY: 0 },
|
||||
{ stepX: 1, stepY: 1 },
|
||||
{ stepX: 0, stepY: 1 },
|
||||
{ stepX: -1, stepY: 1 },
|
||||
{ stepX: -1, stepY: 0 },
|
||||
{ stepX: -1, stepY: -1 },
|
||||
{ stepX: 0, stepY: -1 },
|
||||
{ stepX: 1, stepY: -1 },
|
||||
],
|
||||
distFunc: eucliDist,
|
||||
fallbackRoute: straightPath,
|
||||
};
|
||||
|
||||
const pos2GridIx = (pos: number, gridSize: number) => {
|
||||
const gridIx = Math.floor(pos / gridSize);
|
||||
return gridIx < 0 ? 0 : gridIx;
|
||||
};
|
||||
|
||||
const getObstacleMap = (items: Item[], gridSize: number, offset: number) => {
|
||||
const map = {};
|
||||
items.forEach((item: Item) => {
|
||||
// create-edge 时,当边类型为 polyline 时 endNode 为 null
|
||||
if (item) {
|
||||
const bbox = getExpandedBBox(item.getBBox(), offset);
|
||||
for (let x = pos2GridIx(bbox.minX, gridSize); x <= pos2GridIx(bbox.maxX, gridSize); x += 1) {
|
||||
for (let y = pos2GridIx(bbox.minY, gridSize); y <= pos2GridIx(bbox.maxY, gridSize); y += 1) {
|
||||
const gridKey = `${x}-${y}`;
|
||||
map[gridKey] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return map;
|
||||
};
|
||||
|
||||
/**
|
||||
* 方向角:计算从 p1 到 p2 的射线与水平线形成的夹角度数(顺时针从右侧0°转到该射线的角度)
|
||||
* @param p1 PolyPoint
|
||||
* @param p2 PolyPoint
|
||||
*/
|
||||
const getDirectionAngle = (p1: PolyPoint, p2: PolyPoint) => {
|
||||
const deltaX = p2.x - p1.x;
|
||||
const deltaY = p2.y - p1.y;
|
||||
if (!deltaX && !deltaY) return 0;
|
||||
const angle = (360 + (Math.atan2(deltaY, deltaX) * 180) / Math.PI) % 360;
|
||||
return angle;
|
||||
};
|
||||
|
||||
/**
|
||||
* 方向角的改变,取小于180度角
|
||||
* @param angle1
|
||||
* @param angle2
|
||||
*/
|
||||
const getAngleDiff = (angle1: number, angle2: number) => {
|
||||
const directionChange = Math.abs(angle1 - angle2);
|
||||
return directionChange > 180 ? 360 - directionChange : directionChange;
|
||||
};
|
||||
|
||||
// Path finder //
|
||||
const estimateCost = (from: PolyPoint, endPoints: PolyPoint[], distFunc) => {
|
||||
let min = Infinity;
|
||||
for (let i = 0, len = endPoints.length; i < len; i++) {
|
||||
const cost = distFunc(from, endPoints[i]);
|
||||
if (cost < min) {
|
||||
min = cost;
|
||||
}
|
||||
}
|
||||
return min;
|
||||
};
|
||||
|
||||
// 计算考虑 offset 后的 BBox 上的连接点
|
||||
const getBoxPoints = (point: PolyPoint, node: INode, cfg: RouterCfg): PolyPoint[] => {
|
||||
const points = [];
|
||||
// create-edge 生成边的过程中,endNode 为 null
|
||||
if (!node) {
|
||||
return points
|
||||
}
|
||||
|
||||
const { directions, offset } = cfg;
|
||||
const bbox = node.getBBox();
|
||||
const expandBBox = getExpandedBBox(node.getBBox(), offset);
|
||||
for (const i in expandBBox) {
|
||||
expandBBox[i] = pos2GridIx(expandBBox[i], cfg.gridSize);
|
||||
}
|
||||
const isInside =
|
||||
point.x > pos2GridIx(bbox.minX, cfg.gridSize) &&
|
||||
point.x < pos2GridIx(bbox.maxX, cfg.gridSize) &&
|
||||
point.y > pos2GridIx(bbox.minY, cfg.gridSize) &&
|
||||
point.y < pos2GridIx(bbox.maxY, cfg.gridSize);
|
||||
|
||||
if (isInside) {
|
||||
// 如果 anchorPoint 在节点内部,允许第一段线穿过节点
|
||||
for (const dir of directions) {
|
||||
const bounds = [
|
||||
[
|
||||
{
|
||||
x: expandBBox.minX,
|
||||
y: expandBBox.minY,
|
||||
},
|
||||
{
|
||||
x: expandBBox.maxX,
|
||||
y: expandBBox.minY,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
x: expandBBox.minX,
|
||||
y: expandBBox.minY,
|
||||
},
|
||||
{
|
||||
x: expandBBox.minX,
|
||||
y: expandBBox.maxY,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
x: expandBBox.maxX,
|
||||
y: expandBBox.minY,
|
||||
},
|
||||
{
|
||||
x: expandBBox.maxX,
|
||||
y: expandBBox.maxY,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
x: expandBBox.minX,
|
||||
y: expandBBox.maxY,
|
||||
},
|
||||
{
|
||||
x: expandBBox.maxX,
|
||||
y: expandBBox.maxY,
|
||||
},
|
||||
],
|
||||
];
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const boundLine = bounds[i];
|
||||
const insterctP = getLineIntersect(
|
||||
point,
|
||||
{ x: point.x + dir.stepX * expandBBox.width, y: point.y + dir.stepY * expandBBox.height },
|
||||
boundLine[0],
|
||||
boundLine[1],
|
||||
) as PolyPoint;
|
||||
if (insterctP && !isSegmentCrossingBBox(point, insterctP, node.getBBox())) {
|
||||
insterctP.id = `${insterctP.x}-${insterctP.y}`;
|
||||
points.push(insterctP);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果 anchorPoint 在节点上,只有一个可选方向
|
||||
const insterctP = getExpandedBBoxPoint(expandBBox, point);
|
||||
insterctP.id = `${insterctP.x}-${insterctP.y}`;
|
||||
points.push(insterctP);
|
||||
}
|
||||
|
||||
return points;
|
||||
};
|
||||
|
||||
export const pathFinder = (
|
||||
startPoint: PolyPoint,
|
||||
endPoint: PolyPoint,
|
||||
startNode: INode,
|
||||
endNode: INode,
|
||||
routerCfg?: RouterCfg,
|
||||
): PolyPoint[] => {
|
||||
const cfg: RouterCfg = deepMix(defaultCfg, routerCfg);
|
||||
cfg.obstacles = cfg.obstacles || [];
|
||||
const map = getObstacleMap(cfg.obstacles.concat([startNode, endNode]), cfg.gridSize, cfg.offset);
|
||||
|
||||
const scaleStartPoint = {
|
||||
x: pos2GridIx(startPoint.x, cfg.gridSize),
|
||||
y: pos2GridIx(startPoint.y, cfg.gridSize),
|
||||
};
|
||||
const scaleEndPoint = {
|
||||
x: pos2GridIx(endPoint.x, cfg.gridSize),
|
||||
y: pos2GridIx(endPoint.y, cfg.gridSize),
|
||||
};
|
||||
|
||||
startPoint.id = `${scaleStartPoint.x}-${scaleStartPoint.y}`;
|
||||
endPoint.id = `${scaleEndPoint.x}-${scaleEndPoint.y}`;
|
||||
const startPoints = getBoxPoints(scaleStartPoint, startNode, cfg);
|
||||
const endPoints = getBoxPoints(scaleEndPoint, endNode, cfg);
|
||||
startPoints.forEach((point) => {
|
||||
delete map[point.id];
|
||||
});
|
||||
endPoints.forEach((point) => {
|
||||
delete map[point.id];
|
||||
});
|
||||
|
||||
const openSet = {};
|
||||
const closedSet = {};
|
||||
const cameFrom: {
|
||||
[key: string]: string;
|
||||
} = {};
|
||||
|
||||
// 从起点到当前点已产生的 cost, default: Infinity
|
||||
const gScore: {
|
||||
[key: string]: number;
|
||||
} = {};
|
||||
|
||||
// 起点经过当前点到达终点预估的 cost, default: Infinity
|
||||
const fScore: {
|
||||
[key: string]: number;
|
||||
} = {};
|
||||
|
||||
// initialize
|
||||
for (let i = 0; i < startPoints.length; i++) {
|
||||
const firstStep = startPoints[i];
|
||||
openSet[firstStep.id] = firstStep;
|
||||
// cameFrom[firstStep.id] = startPoint.id;
|
||||
gScore[firstStep.id] = 0;
|
||||
fScore[firstStep.id] = estimateCost(firstStep, endPoints, cfg.distFunc);
|
||||
}
|
||||
|
||||
const getDirectionChange = (current: PolyPoint, neighbor: PolyPoint): number => {
|
||||
const directionAngle = getDirectionAngle(current, neighbor);
|
||||
let directionChange: number;
|
||||
if (!cameFrom[current.id]) {
|
||||
const startAngle = getDirectionAngle(scaleStartPoint, current);
|
||||
directionChange = getAngleDiff(startAngle, directionAngle);
|
||||
} else {
|
||||
const prevDirectionAngle = getDirectionAngle(
|
||||
{
|
||||
x: parseFloat(cameFrom[current.id].split('-')[0]),
|
||||
y: parseFloat(cameFrom[current.id].split('-')[1]),
|
||||
},
|
||||
current,
|
||||
);
|
||||
directionChange = getAngleDiff(prevDirectionAngle, directionAngle);
|
||||
}
|
||||
return directionChange;
|
||||
};
|
||||
|
||||
const getControlPoints = (currentId: string) => {
|
||||
const controlPoints = [endPoint];
|
||||
const lastPoint = {
|
||||
x: parseFloat(currentId.split('-')[0]),
|
||||
y: parseFloat(currentId.split('-')[1]),
|
||||
id: currentId,
|
||||
};
|
||||
if (getDirectionChange(lastPoint, scaleEndPoint)) {
|
||||
controlPoints.unshift({
|
||||
x: lastPoint.x === scaleEndPoint.x ? endPoint.x : lastPoint.x * cfg.gridSize,
|
||||
y: lastPoint.y === scaleEndPoint.y ? endPoint.y : lastPoint.y * cfg.gridSize,
|
||||
});
|
||||
}
|
||||
while (cameFrom[currentId] && cameFrom[currentId] !== currentId) {
|
||||
const point = {
|
||||
x: parseFloat(currentId.split('-')[0]),
|
||||
y: parseFloat(currentId.split('-')[1]),
|
||||
id: currentId,
|
||||
};
|
||||
const preId = cameFrom[currentId];
|
||||
const prePoint = {
|
||||
x: parseFloat(preId.split('-')[0]),
|
||||
y: parseFloat(preId.split('-')[1]),
|
||||
id: preId,
|
||||
};
|
||||
const directionChange = getDirectionChange(prePoint, point);
|
||||
if (directionChange) {
|
||||
controlPoints.unshift({
|
||||
x: prePoint.x === point.x ? controlPoints[0].x : prePoint.x * cfg.gridSize,
|
||||
y: prePoint.y === point.y ? controlPoints[0].y : prePoint.y * cfg.gridSize,
|
||||
});
|
||||
}
|
||||
|
||||
currentId = preId;
|
||||
}
|
||||
|
||||
// 和startNode对齐
|
||||
const firstPoint = {
|
||||
x: parseFloat(currentId.split('-')[0]),
|
||||
y: parseFloat(currentId.split('-')[1]),
|
||||
id: currentId,
|
||||
};
|
||||
controlPoints[0].x = firstPoint.x === scaleStartPoint.x ? startPoint.x : controlPoints[0].x;
|
||||
controlPoints[0].y = firstPoint.y === scaleStartPoint.y ? startPoint.y : controlPoints[0].y;
|
||||
controlPoints.unshift(startPoint);
|
||||
return controlPoints;
|
||||
};
|
||||
|
||||
let remainLoops = cfg.maximumLoops;
|
||||
while (Object.keys(openSet).length > 0 && remainLoops > 0) {
|
||||
let current;
|
||||
let curCost = Infinity;
|
||||
|
||||
// 找到 openSet 中 fScore 最小的点
|
||||
for (const id in openSet) {
|
||||
if (fScore[id] <= curCost) {
|
||||
curCost = fScore[id];
|
||||
current = openSet[id];
|
||||
}
|
||||
}
|
||||
if (!current) break;
|
||||
|
||||
// 如果 fScore 最小的点就是终点
|
||||
if (endPoints.findIndex((point) => point.x === current.x && point.y === current.y) > -1) {
|
||||
const controlPoints = getControlPoints(current.id);
|
||||
return controlPoints;
|
||||
}
|
||||
|
||||
delete openSet[current.id];
|
||||
closedSet[current.id] = true;
|
||||
|
||||
// 获取符合条件的下一步的候选连接点
|
||||
// 沿候选方向走一步
|
||||
for (let i = 0; i < cfg.directions.length; i++) {
|
||||
const direction = cfg.directions[i];
|
||||
const neighbor = {
|
||||
x: current.x + direction.stepX,
|
||||
y: current.y + direction.stepY,
|
||||
id: `${current.x + direction.stepX}-${current.y + direction.stepY}`,
|
||||
};
|
||||
|
||||
if (closedSet[neighbor.id]) continue;
|
||||
const directionChange = getDirectionChange(current, neighbor);
|
||||
if (directionChange > cfg.maxAllowedDirectionChange) continue;
|
||||
if (map[neighbor.id]) continue; // 如果交叉则跳过
|
||||
|
||||
// 将候选点加入 openSet, 并计算每个候选点的 cost
|
||||
if (!openSet[neighbor.id]) {
|
||||
openSet[neighbor.id] = neighbor;
|
||||
}
|
||||
const neighborCost =
|
||||
cfg.distFunc(current, neighbor) +
|
||||
(isNaN(cfg.penalties[directionChange]) ? cfg.gridSize : cfg.penalties[directionChange]);
|
||||
const costFromStart = gScore[current.id] + neighborCost;
|
||||
if (gScore[neighbor.id] && costFromStart >= gScore[neighbor.id]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cameFrom[neighbor.id] = current.id;
|
||||
gScore[neighbor.id] = costFromStart;
|
||||
fScore[neighbor.id] = costFromStart + estimateCost(neighbor, endPoints, cfg.distFunc);
|
||||
}
|
||||
|
||||
remainLoops -= 1;
|
||||
}
|
||||
|
||||
return cfg.fallbackRoute(startPoint, endPoint, startNode, endNode, cfg);
|
||||
};
|
653
packages/core/src/shape/hull/bubbleset.ts
Normal file
653
packages/core/src/shape/hull/bubbleset.ts
Normal file
@ -0,0 +1,653 @@
|
||||
import { IPoint, IBBox, Item, BubblesetCfg } from '../../types';
|
||||
import {
|
||||
squareDist,
|
||||
pointLineSquareDist,
|
||||
itemIntersectByLine,
|
||||
getPointsCenter,
|
||||
fractionToLine,
|
||||
isPointsOverlap,
|
||||
pointRectSquareDist,
|
||||
Line,
|
||||
isPointInPolygon,
|
||||
} from '../../util/math';
|
||||
|
||||
const defaultOps = {
|
||||
maxRoutingIterations: 100, // number of times to run the algorithm to refine the path finding in difficult areas
|
||||
maxMarchingIterations: 100, // number of times to refine the boundary
|
||||
pixelGroupSize: 2, // the resolution of the algorithm in square pixels
|
||||
edgeR0: 10, // the distance from edges at which energy is 1 (full influence)
|
||||
edgeR1: 10, // the distance from edges at which energy is 0 (no influence)
|
||||
nodeR0: 5, // the distance from nodes which energy is 1 (full influence)
|
||||
nodeR1: 10, // the distance from nodes at which energy is 0 (no influence)
|
||||
morphBuffer: 5, // DEFAULT_NODE_R0; the amount of space to move the virtual edge when wrapping around obstacles
|
||||
threshold: 0.001,
|
||||
skip: 16,
|
||||
nodeInfluenceFactor: 1,
|
||||
edgeInfluenceFactor: 1,
|
||||
negativeNodeInfluenceFactor: -0.5,
|
||||
};
|
||||
|
||||
/**
|
||||
* Marching square algorithm for traching the contour of a pixel group
|
||||
* https://www.emanueleferonato.com/2013/03/01/using-marching-squares-algorithm-to-trace-the-contour-of-an-image/
|
||||
* @param potentialArea
|
||||
* @param threshold
|
||||
*/
|
||||
function MarchingSquares(contour, potentialArea, threshold) {
|
||||
let marched = false;
|
||||
const getVal = (x: number, y: number) => {
|
||||
return potentialArea.cells[x + y * potentialArea.width];
|
||||
};
|
||||
|
||||
const getState = (x: number, y: number) => {
|
||||
let squareVal = 0;
|
||||
if (getVal(x - 1, y - 1) >= threshold) {
|
||||
squareVal += 1;
|
||||
}
|
||||
if (getVal(x, y - 1) > threshold) {
|
||||
squareVal += 2;
|
||||
}
|
||||
if (getVal(x - 1, y) > threshold) {
|
||||
squareVal += 4;
|
||||
}
|
||||
if (getVal(x, y) > threshold) {
|
||||
squareVal += 8;
|
||||
}
|
||||
return squareVal;
|
||||
};
|
||||
|
||||
const doMarch = (xPos: number, yPos: number) => {
|
||||
let x = xPos;
|
||||
let y = yPos;
|
||||
let prevX;
|
||||
let prevY;
|
||||
|
||||
for (let i = 0; i < potentialArea.width * potentialArea.height; i++) {
|
||||
prevX = x;
|
||||
prevY = y;
|
||||
if (contour.findIndex((item) => item.x === x && item.y === y) > -1) {
|
||||
if (contour[0].x !== x || contour[0].y !== y) {
|
||||
// encountered a loop but haven't returned to start: change direction using conditionals and continue back to start
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
contour.push({ x, y });
|
||||
}
|
||||
|
||||
const state = getState(x, y);
|
||||
// assign the move direction according to state of the square
|
||||
switch (state) {
|
||||
case -1:
|
||||
console.warn('Marched out of bounds');
|
||||
return true;
|
||||
case 0:
|
||||
case 3:
|
||||
case 2:
|
||||
case 7:
|
||||
x++; // go right
|
||||
break;
|
||||
case 12:
|
||||
case 14:
|
||||
case 4:
|
||||
x--; // go left
|
||||
break;
|
||||
case 6: // go left if come from up else go right
|
||||
if (prevX === 0) {
|
||||
if (prevY === -1) {
|
||||
x -= 1;
|
||||
} else {
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
case 13:
|
||||
case 5:
|
||||
y--; // go up
|
||||
break;
|
||||
case 9: // go up if come from right else go down
|
||||
if (prevX === 1) {
|
||||
if (prevY === 0) {
|
||||
y -= 1;
|
||||
} else {
|
||||
y += 1;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 10:
|
||||
case 8:
|
||||
case 11:
|
||||
y++; // go down
|
||||
break;
|
||||
default:
|
||||
console.warn(`Marching squares invalid state: ${state}`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.march = () => {
|
||||
for (let x = 0; x < potentialArea.width && !marched; x += 1) {
|
||||
for (let y = 0; y < potentialArea.height && !marched; y += 1) {
|
||||
if (getVal(x, y) > threshold && getState(x, y) !== 15) {
|
||||
marched = doMarch(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
return marched;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Space partition & assign value to each cell
|
||||
* @param points
|
||||
*/
|
||||
const initGridCells = (width: number, height: number, pixelGroupSize: number) => {
|
||||
const scaleWidth = Math.ceil(width / pixelGroupSize);
|
||||
const scaleHeight = Math.ceil(height / pixelGroupSize);
|
||||
const gridCells = new Float32Array(Math.max(0, scaleWidth * scaleHeight)).fill(0);
|
||||
return {
|
||||
cells: gridCells,
|
||||
width: scaleWidth,
|
||||
height: scaleHeight,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Find the optimal already visited member to item;
|
||||
Optimal: minimize cost(j) = distance(i,j) ∗ countObstacles(i,j)
|
||||
* @param item
|
||||
* @param visited
|
||||
*/
|
||||
const pickBestNeighbor = (item: Item, visited: Item[], nonMembers: Item[]): Item | null => {
|
||||
let closestNeighbour = null;
|
||||
let minCost = Number.POSITIVE_INFINITY;
|
||||
|
||||
visited.forEach((neighbourItem) => {
|
||||
const itemP = { x: item.getModel().x, y: item.getModel().y };
|
||||
const neighbourItemP = { x: neighbourItem.getModel().x, y: neighbourItem.getModel().y };
|
||||
const dist = squareDist(itemP, neighbourItemP);
|
||||
const directLine = new Line(itemP.x, itemP.y, neighbourItemP.x, neighbourItemP.y);
|
||||
const numberObstacles = nonMembers.reduce((count, _item) => {
|
||||
if (fractionToLine(_item, directLine) > 0) {
|
||||
return count + 1;
|
||||
}
|
||||
return count;
|
||||
}, 0);
|
||||
if (dist * (numberObstacles + 1) ** 2 < minCost) {
|
||||
closestNeighbour = neighbourItem;
|
||||
minCost = dist * (numberObstacles + 1) ** 2;
|
||||
}
|
||||
});
|
||||
return closestNeighbour;
|
||||
};
|
||||
|
||||
/**
|
||||
* 返回和线相交的item中,离边的起点最近的item
|
||||
* @param items
|
||||
* @param line
|
||||
*/
|
||||
const getIntersectItem = (items: Item[], line: Line): Item | null => {
|
||||
let minDistance = Number.POSITIVE_INFINITY;
|
||||
let closestItem = null;
|
||||
|
||||
items.forEach((item) => {
|
||||
const distance = fractionToLine(item, line);
|
||||
// find closest intersection
|
||||
if (distance >= 0 && distance < minDistance) {
|
||||
closestItem = item;
|
||||
minDistance = distance;
|
||||
}
|
||||
});
|
||||
return closestItem;
|
||||
};
|
||||
|
||||
/**
|
||||
* Modify the directLine and Route virtual edges around obstacles
|
||||
*/
|
||||
const computeRoute = (
|
||||
directLine: Line,
|
||||
nonMembers: Item[],
|
||||
maxRoutingIterations: number,
|
||||
morphBuffer: number,
|
||||
): Line[] => {
|
||||
const checkedLines: Line[] = [];
|
||||
const linesToCheck: Line[] = [];
|
||||
linesToCheck.push(directLine);
|
||||
|
||||
let hasIntersection = true;
|
||||
let iterations = 0;
|
||||
|
||||
const pointExists = (point: IPoint, lines: Line[]) => {
|
||||
let flag = false;
|
||||
lines.forEach((line) => {
|
||||
if (flag) return;
|
||||
if (
|
||||
isPointsOverlap(point, { x: line.x1, y: line.y1 }) ||
|
||||
isPointsOverlap(point, { x: line.x2, y: line.y2 })
|
||||
) {
|
||||
flag = true;
|
||||
}
|
||||
});
|
||||
return flag;
|
||||
};
|
||||
const isPointInNonMembers = (point: IPoint, _nonMembers: Item[]) => {
|
||||
for (const item of _nonMembers) {
|
||||
const bbox = item.getBBox();
|
||||
const itemContour = [
|
||||
[bbox.x, bbox.y],
|
||||
[bbox.x + bbox.width, bbox.y],
|
||||
[bbox.x, bbox.y + bbox.height],
|
||||
[bbox.x + bbox.width, bbox.y + bbox.height],
|
||||
];
|
||||
if (isPointInPolygon(itemContour, point.x, point.y)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// outer loop end when no more intersections or out of iterations
|
||||
while (hasIntersection && iterations < maxRoutingIterations) {
|
||||
hasIntersection = false;
|
||||
// inner loop end when out of lines or found an intersection
|
||||
while (!hasIntersection && linesToCheck.length) {
|
||||
const line = linesToCheck.pop();
|
||||
const closestItem = getIntersectItem(nonMembers, line);
|
||||
if (closestItem) {
|
||||
const [intersections, countIntersections] = itemIntersectByLine(closestItem, line);
|
||||
// if line passes through item
|
||||
if (countIntersections === 2) {
|
||||
const testReroute = (isFirst: boolean) => {
|
||||
let tempMorphBuffer = morphBuffer;
|
||||
let virtualNode = rerouteLine(closestItem, tempMorphBuffer, intersections, isFirst);
|
||||
// test the virtualNode already exists
|
||||
let exist =
|
||||
pointExists(virtualNode, linesToCheck) || pointExists(virtualNode, checkedLines);
|
||||
let pointInside = isPointInNonMembers(virtualNode, nonMembers);
|
||||
|
||||
while (!exist && pointInside && tempMorphBuffer >= 1) {
|
||||
// try a smaller buffer
|
||||
tempMorphBuffer /= 1.5;
|
||||
virtualNode = rerouteLine(closestItem, tempMorphBuffer, intersections, isFirst);
|
||||
exist =
|
||||
pointExists(virtualNode, linesToCheck) || pointExists(virtualNode, checkedLines);
|
||||
pointInside = isPointInNonMembers(virtualNode, nonMembers);
|
||||
}
|
||||
|
||||
// 第二次route时不要求pointInside
|
||||
if (virtualNode && !exist && (!isFirst || !pointInside)) {
|
||||
// add 2 rerouted lines to check
|
||||
linesToCheck.push(new Line(line.x1, line.y1, virtualNode.x, virtualNode.y));
|
||||
linesToCheck.push(new Line(virtualNode.x, virtualNode.y, line.x2, line.y2));
|
||||
hasIntersection = true;
|
||||
}
|
||||
};
|
||||
|
||||
testReroute(true);
|
||||
if (!hasIntersection) {
|
||||
// if we didn't find a valid point around the first corner, try the second
|
||||
testReroute(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// no intersection found, mark this line as completed
|
||||
if (!hasIntersection) {
|
||||
checkedLines.push(line);
|
||||
}
|
||||
iterations += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 加入剩余的线
|
||||
while (linesToCheck.length) {
|
||||
checkedLines.push(linesToCheck.pop());
|
||||
}
|
||||
return checkedLines;
|
||||
};
|
||||
|
||||
/**
|
||||
* Connect item with visited members using direct line or virtual edges
|
||||
*/
|
||||
function getRoute(
|
||||
item: Item,
|
||||
nonMembers: Item[],
|
||||
visited: Item[],
|
||||
maxRoutingIterations: number,
|
||||
morphBuffer: number,
|
||||
) {
|
||||
const optimalNeighbor = pickBestNeighbor(item, visited, nonMembers);
|
||||
if (optimalNeighbor === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// merge the consecutive lines
|
||||
const mergeLines = (checkedLines: Line[]): Line[] => {
|
||||
const finalRoute: Line[] = [];
|
||||
while (checkedLines.length > 0) {
|
||||
const line1 = checkedLines.pop()!;
|
||||
if (checkedLines.length === 0) {
|
||||
finalRoute.push(line1);
|
||||
break;
|
||||
}
|
||||
const line2 = checkedLines.pop()!;
|
||||
const mergeLine = new Line(line1.x1, line1.y1, line2.x2, line2.y2);
|
||||
const closestItem = getIntersectItem(nonMembers, mergeLine);
|
||||
// merge most recent line and previous line
|
||||
if (!closestItem) {
|
||||
checkedLines.push(mergeLine);
|
||||
} else {
|
||||
finalRoute.push(line1);
|
||||
checkedLines.push(line2);
|
||||
}
|
||||
}
|
||||
return finalRoute;
|
||||
};
|
||||
const directLine = new Line(
|
||||
item.getModel().x,
|
||||
item.getModel().y,
|
||||
optimalNeighbor.getModel().x,
|
||||
optimalNeighbor.getModel().y,
|
||||
);
|
||||
const checkedLines = computeRoute(directLine, nonMembers, maxRoutingIterations, morphBuffer);
|
||||
const finalRoute = mergeLines(checkedLines);
|
||||
return finalRoute;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the countor that includes the selected items and exclues the non-selected items
|
||||
* @param graph
|
||||
* @param members
|
||||
* @param nonMembers
|
||||
* @param options
|
||||
*/
|
||||
export const genBubbleSet = (members: Item[], nonMembers: Item[], ops?: BubblesetCfg) => {
|
||||
// eslint-disable-next-line no-redeclare
|
||||
const options = Object.assign(defaultOps, ops);
|
||||
const centroid = getPointsCenter(
|
||||
members.map((item) => ({ x: item.getModel().x, y: item.getModel().y })),
|
||||
);
|
||||
// 按照到中心距离远近排序
|
||||
members = members.sort(
|
||||
(a, b) =>
|
||||
squareDist({ x: a.getModel().x, y: a.getModel().y }, centroid) -
|
||||
squareDist({ x: b.getModel().x, y: b.getModel().y }, centroid),
|
||||
);
|
||||
const visited: Item[] = [];
|
||||
const virtualEdges: Line[] = [];
|
||||
members.forEach((item) => {
|
||||
const lines = getRoute(
|
||||
item,
|
||||
nonMembers,
|
||||
visited,
|
||||
options.maxRoutingIterations,
|
||||
options.morphBuffer,
|
||||
);
|
||||
lines.forEach((l) => {
|
||||
virtualEdges.push(l);
|
||||
});
|
||||
visited.push(item);
|
||||
});
|
||||
// 由于edge也可以作为member和nonMember传入,暂时不考虑把edges作为参数传入genBubbleSet
|
||||
// edges && edges.forEach(e => {
|
||||
// virtualEdges.push(new Line(e.getSource().getModel().x, e.getSource().getModel().y, e.getTarget().getModel().x, e.getTarget().getModel().y));
|
||||
// });
|
||||
|
||||
const activeRegion = getActiveRregion(members, virtualEdges, options.nodeR0);
|
||||
const potentialArea = initGridCells(
|
||||
activeRegion.width,
|
||||
activeRegion.height,
|
||||
options.pixelGroupSize,
|
||||
);
|
||||
|
||||
// Use march squares to generate contour
|
||||
let contour = [];
|
||||
let hull = [];
|
||||
for (let iterations = 0; iterations < options.maxMarchingIterations; iterations++) {
|
||||
fillPotentialArea(members, nonMembers, virtualEdges, activeRegion, potentialArea, options);
|
||||
contour = [];
|
||||
hull = [];
|
||||
if (!new MarchingSquares(contour, potentialArea, options.threshold).march()) continue;
|
||||
const marchedPath = contour.map((point) => ({
|
||||
x: Math.round(point.x * options.pixelGroupSize + activeRegion.minX),
|
||||
y: Math.round(point.y * options.pixelGroupSize + activeRegion.minY),
|
||||
}));
|
||||
// const marchedPath = marchingSquares(potentialArea, options.threshold).map(point => ({ x: Math.round(point.x * options.pixelGroupSize + activeRegion.minX), y: Math.round(point.y * options.pixelGroupSize + activeRegion.minY) }))
|
||||
if (marchedPath) {
|
||||
let size = marchedPath.length;
|
||||
if (options.skip > 1) {
|
||||
size = Math.floor(marchedPath.length / options.skip);
|
||||
// if we reduced too much (fewer than three points in reduced surface) reduce skip and try again
|
||||
while (size < 3 && options.skip > 1) {
|
||||
options.skip -= 1;
|
||||
size = Math.floor(marchedPath.length / options.skip);
|
||||
}
|
||||
}
|
||||
// copy hull values
|
||||
for (let i = 0, j = 0; j < size; j += 1, i += options.skip) {
|
||||
hull.push({ x: marchedPath[i].x, y: marchedPath[i].y });
|
||||
}
|
||||
}
|
||||
|
||||
const isContourValid = () => {
|
||||
for (const item of members) {
|
||||
const hullPoints = hull.map((point) => [point.x, point.y]);
|
||||
if (!isPointInPolygon(hullPoints, item.getBBox().centerX, item.getBBox().centerY))
|
||||
return false;
|
||||
}
|
||||
// 不强制要求所有nonMembers都没有包含在内
|
||||
// for (const item of nonMembers) {
|
||||
// if (isPointInPolygon({ x: item.getBBox().centerX, y: item.getBBox().centerY }, contour)) return false
|
||||
// }
|
||||
return true;
|
||||
};
|
||||
|
||||
if (hull && isContourValid()) {
|
||||
return hull;
|
||||
}
|
||||
|
||||
// update parameters for next iteraction
|
||||
options.threshold *= 0.9;
|
||||
if (iterations <= options.maxMarchingIterations * 0.5) {
|
||||
options.memberInfluenceFactor *= 1.2;
|
||||
options.edgeInfluenceFactor *= 1.2;
|
||||
} else if (options.nonMemberInfluenceFactor !== 0 && nonMembers.length > 0) {
|
||||
// after half the iterations, start increasing positive energy and lowering the threshold
|
||||
options.nonMemberInfluenceFactor *= 0.8;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return hull;
|
||||
};
|
||||
|
||||
/**
|
||||
* unionboundingbox
|
||||
* @param members
|
||||
* @param edges
|
||||
*/
|
||||
function getActiveRregion(members: Item[], edges: Line[], offset: number): IBBox {
|
||||
const activeRegion = {
|
||||
minX: Number.POSITIVE_INFINITY,
|
||||
minY: Number.POSITIVE_INFINITY,
|
||||
maxX: Number.NEGATIVE_INFINITY,
|
||||
maxY: Number.NEGATIVE_INFINITY,
|
||||
width: 0,
|
||||
height: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
const bboxes = [];
|
||||
|
||||
members.forEach((item) => {
|
||||
bboxes.push(item.getBBox());
|
||||
});
|
||||
edges.forEach((l) => {
|
||||
bboxes.push(l.getBBox());
|
||||
});
|
||||
|
||||
for (const bbox of bboxes) {
|
||||
activeRegion.minX = (bbox.minX < activeRegion.minX ? bbox.minX : activeRegion.minX) - offset;
|
||||
activeRegion.minY = (bbox.minY < activeRegion.minY ? bbox.minY : activeRegion.minY) - offset;
|
||||
activeRegion.maxX = (bbox.maxX > activeRegion.maxX ? bbox.maxX : activeRegion.maxX) + offset;
|
||||
activeRegion.maxY = (bbox.maxY > activeRegion.maxY ? bbox.maxY : activeRegion.maxY) + offset;
|
||||
}
|
||||
activeRegion.width = activeRegion.maxX - activeRegion.minX;
|
||||
activeRegion.height = activeRegion.maxY - activeRegion.minY;
|
||||
activeRegion.x = activeRegion.minX;
|
||||
activeRegion.y = activeRegion.minY;
|
||||
return activeRegion;
|
||||
}
|
||||
|
||||
function fillPotentialArea(
|
||||
members: Item[],
|
||||
nonMembers: Item[],
|
||||
edges: Line[],
|
||||
activeRegion: IBBox,
|
||||
potentialArea,
|
||||
options: BubblesetCfg,
|
||||
) {
|
||||
function pos2GridIx(x, offset) {
|
||||
const gridIx = Math.floor((x - offset) / options.pixelGroupSize);
|
||||
return gridIx < 0 ? 0 : gridIx;
|
||||
}
|
||||
|
||||
function gridIx2Pos(x, offset) {
|
||||
return x * options.pixelGroupSize + offset;
|
||||
}
|
||||
|
||||
// using inverse a for numerical stability
|
||||
const nodeInfA = (options.nodeR0 - options.nodeR1) * (options.nodeR0 - options.nodeR1);
|
||||
const edgeInfA = (options.edgeR0 - options.edgeR1) * (options.edgeR0 - options.edgeR1);
|
||||
|
||||
const getAffectedRegion = (bbox, thresholdR) => {
|
||||
const startX = Math.min(
|
||||
pos2GridIx(bbox.minX, thresholdR + activeRegion.minX),
|
||||
potentialArea.width,
|
||||
);
|
||||
const startY = Math.min(
|
||||
pos2GridIx(bbox.minY, thresholdR + activeRegion.minY),
|
||||
potentialArea.height,
|
||||
);
|
||||
const endX = Math.min(
|
||||
pos2GridIx(bbox.maxX, -thresholdR + activeRegion.minX),
|
||||
potentialArea.width,
|
||||
);
|
||||
const endY = Math.min(
|
||||
pos2GridIx(bbox.maxY, -thresholdR + activeRegion.minY),
|
||||
potentialArea.height,
|
||||
);
|
||||
return [startX, startY, endX, endY];
|
||||
};
|
||||
const addItemInfluence = (item: Item, influenceFactor: number) => {
|
||||
const bbox = item.getBBox();
|
||||
const [startX, startY, endX, endY] = getAffectedRegion(bbox, options.nodeR1);
|
||||
// calculate item influence for each cell
|
||||
for (let y = startY; y < endY; y += 1) {
|
||||
for (let x = startX; x < endX; x += 1) {
|
||||
if (influenceFactor < 0 && potentialArea[x + y * potentialArea.width] <= 0) {
|
||||
continue;
|
||||
}
|
||||
const tempX = gridIx2Pos(x, activeRegion.minX);
|
||||
const tempY = gridIx2Pos(y, activeRegion.minY);
|
||||
const distanceSq = pointRectSquareDist(
|
||||
{ x: tempX, y: tempY },
|
||||
{ x: bbox.minX, y: bbox.minY, width: bbox.width, height: bbox.height }
|
||||
);
|
||||
if (distanceSq < options.nodeR1 ** 2) {
|
||||
const dr = Math.sqrt(distanceSq) - options.nodeR1;
|
||||
potentialArea.cells[x + y * potentialArea.width] += influenceFactor * dr * dr;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const addEdgeInfluence = (line: Line, influenceFactor: number) => {
|
||||
const bbox = line.getBBox();
|
||||
const [startX, startY, endX, endY] = getAffectedRegion(bbox, options.edgeR1);
|
||||
// for every point in active part of potentialArea, calculate distance to nearest point on line and add influence
|
||||
for (let y = startY; y < endY; y += 1) {
|
||||
for (let x = startX; x < endX; x += 1) {
|
||||
if (influenceFactor < 0 && potentialArea.cells[x + y * potentialArea.width] <= 0) {
|
||||
continue;
|
||||
}
|
||||
const tempX = gridIx2Pos(x, activeRegion.minX);
|
||||
const tempY = gridIx2Pos(y, activeRegion.minY);
|
||||
const minDistanceSq = pointLineSquareDist({ x: tempX, y: tempY }, line);
|
||||
// only influence if less than r1
|
||||
if (minDistanceSq < options.edgeR1 ** 2) {
|
||||
const mdr = Math.sqrt(minDistanceSq) - options.edgeR1;
|
||||
potentialArea.cells[x + y * potentialArea.width] += influenceFactor * mdr * mdr;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (options.nodeInfluenceFactor) {
|
||||
members.forEach((item) => {
|
||||
addItemInfluence(item, options.nodeInfluenceFactor / nodeInfA);
|
||||
});
|
||||
}
|
||||
|
||||
if (options.edgeInfluenceFactor) {
|
||||
edges.forEach((edge) => {
|
||||
addEdgeInfluence(edge, options.edgeInfluenceFactor / edgeInfA);
|
||||
});
|
||||
}
|
||||
if (options.negativeNodeInfluenceFactor) {
|
||||
nonMembers.forEach((item) => {
|
||||
addItemInfluence(item, options.negativeNodeInfluenceFactor / nodeInfA);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function rerouteLine(item, buffer: number, intersections: IPoint[], wrapNormal: boolean): IPoint {
|
||||
const bbox = item.getBBox();
|
||||
const [topIntersect, leftIntersect, bottomIntersect, rightIntersect] = intersections;
|
||||
const cornerPos = {
|
||||
topLeft: { x: bbox.minX - buffer, y: bbox.minY - buffer },
|
||||
topRight: { x: bbox.maxX + buffer, y: bbox.minY - buffer },
|
||||
bottomLeft: { x: bbox.minX - buffer, y: bbox.maxY + buffer },
|
||||
bottomRight: { x: bbox.maxX + buffer, y: bbox.maxY + buffer },
|
||||
};
|
||||
|
||||
const totalArea = bbox.height * bbox.width;
|
||||
function calcHalfArea(intersect1, intersect2) {
|
||||
return bbox.width * ((intersect1.y - bbox.minY + (intersect2.y - bbox.minY)) * 0.5);
|
||||
}
|
||||
|
||||
// 根据线和boundingbox相交的情况,确定control point的位置
|
||||
if (leftIntersect) {
|
||||
// 相交区域有三角形
|
||||
if (topIntersect) return wrapNormal ? cornerPos.topLeft : cornerPos.bottomRight;
|
||||
if (bottomIntersect) return wrapNormal ? cornerPos.bottomLeft : cornerPos.topRight;
|
||||
// 相交区域分成上下两个梯形,比较面积
|
||||
const topArea = calcHalfArea(leftIntersect, rightIntersect);
|
||||
if (topArea < totalArea * 0.5) {
|
||||
if (leftIntersect.y > rightIntersect.y)
|
||||
return wrapNormal ? cornerPos.topLeft : cornerPos.bottomRight;
|
||||
return wrapNormal ? cornerPos.topRight : cornerPos.bottomLeft;
|
||||
}
|
||||
if (leftIntersect.y < rightIntersect.y)
|
||||
return wrapNormal ? cornerPos.bottomLeft : cornerPos.topRight;
|
||||
return wrapNormal ? cornerPos.bottomRight : cornerPos.topLeft;
|
||||
}
|
||||
|
||||
if (rightIntersect) {
|
||||
if (topIntersect) return wrapNormal ? cornerPos.topRight : cornerPos.bottomLeft;
|
||||
if (bottomIntersect) return wrapNormal ? cornerPos.bottomRight : cornerPos.topLeft;
|
||||
}
|
||||
|
||||
// 相交区域分成左右两个梯形
|
||||
const leftArea = calcHalfArea(topIntersect, bottomIntersect);
|
||||
if (leftArea < totalArea * 0.5) {
|
||||
if (topIntersect.x > bottomIntersect.x)
|
||||
return wrapNormal ? cornerPos.topLeft : cornerPos.bottomRight;
|
||||
return wrapNormal ? cornerPos.bottomLeft : cornerPos.topRight;
|
||||
}
|
||||
if (topIntersect.x < bottomIntersect.x)
|
||||
return wrapNormal ? cornerPos.topRight : cornerPos.bottomLeft;
|
||||
return wrapNormal ? cornerPos.bottomRight : cornerPos.topLeft;
|
||||
}
|
50
packages/core/src/shape/hull/convexHull.ts
Normal file
50
packages/core/src/shape/hull/convexHull.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { IPoint, Item } from '../../types';
|
||||
|
||||
/**
|
||||
* Use cross product to judge the direction of the turn.
|
||||
* Returns a positive value, if OAB makes a clockwise turn,
|
||||
* negative for counter-clockwise turn, and zero if the points are collinear.
|
||||
*/
|
||||
export const cross = (a: IPoint, b: IPoint, o: IPoint) => {
|
||||
return (a.y - o.y) * (b.x - o.x) - (a.x - o.x) * (b.y - o.y);
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a convex hull of given points. Andrew's monotone chain algorithm.
|
||||
* @param points An array of [x, y] representing the coordinates of points.
|
||||
* @return a list of vertices of the convex hull in counter-clockwise order,
|
||||
*/
|
||||
export const genConvexHull = (items: Item[]) => {
|
||||
const points: IPoint[] = items.map((item) => ({ x: item.getModel().x, y: item.getModel().y }));
|
||||
points.sort((a, b) => {
|
||||
return a.x === b.x ? a.y - b.y : a.x - b.x;
|
||||
});
|
||||
|
||||
// build the lower hull
|
||||
const lower = [];
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
while (
|
||||
lower.length >= 2 &&
|
||||
cross(lower[lower.length - 2], lower[lower.length - 1], points[i]) <= 0
|
||||
) {
|
||||
lower.pop();
|
||||
}
|
||||
lower.push(points[i]);
|
||||
}
|
||||
|
||||
// build the upper hull
|
||||
const upper = [];
|
||||
for (let i = points.length - 1; i >= 0; i--) {
|
||||
while (
|
||||
upper.length >= 2 &&
|
||||
cross(upper[upper.length - 2], upper[upper.length - 1], points[i]) <= 0
|
||||
) {
|
||||
upper.pop();
|
||||
}
|
||||
upper.push(points[i]);
|
||||
}
|
||||
upper.pop();
|
||||
lower.pop();
|
||||
const strictHull = lower.concat(upper);
|
||||
return strictHull;
|
||||
};
|
13
packages/core/src/shape/index.ts
Normal file
13
packages/core/src/shape/index.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import Shape from './shape';
|
||||
|
||||
import './node';
|
||||
import './edge';
|
||||
import './combo';
|
||||
import './combos';
|
||||
import './nodes';
|
||||
import './edges';
|
||||
|
||||
export { default as Arrow } from './arrow';
|
||||
export { default as Marker } from './marker';
|
||||
|
||||
export default Shape;
|
32
packages/core/src/shape/marker.ts
Normal file
32
packages/core/src/shape/marker.ts
Normal file
@ -0,0 +1,32 @@
|
||||
export default {
|
||||
collapse: (x, y, r) => {
|
||||
return [
|
||||
['M', x - r, y],
|
||||
['a', r, r, 0, 1, 0, r * 2, 0],
|
||||
['a', r, r, 0, 1, 0, -r * 2, 0],
|
||||
['M', x - r + 4, y],
|
||||
['L', x + r - 4, y],
|
||||
];
|
||||
},
|
||||
expand: (x, y, r) => {
|
||||
return [
|
||||
['M', x - r, y],
|
||||
['a', r, r, 0, 1, 0, r * 2, 0],
|
||||
['a', r, r, 0, 1, 0, -r * 2, 0],
|
||||
['M', x - r + 4, y],
|
||||
['L', x - r + 2 * r - 4, y],
|
||||
['M', x - r + r, y - r + 4],
|
||||
['L', x, y + r - 4],
|
||||
];
|
||||
},
|
||||
upTriangle: (x, y, r) => {
|
||||
const l1 = r * Math.cos(Math.PI / 6);
|
||||
const l2 = r * Math.sin(Math.PI / 6);
|
||||
return [['M', x - l1, y + l2], ['L', x + l1, y + l2], ['L', x, y - r], ['Z']];
|
||||
},
|
||||
downTriangle: (x, y, r) => {
|
||||
const l1 = r * Math.cos(Math.PI / 6);
|
||||
const l2 = r * Math.sin(Math.PI / 6);
|
||||
return [['M', x - l1, y - l2], ['L', x + l1, y - l2], ['L', x, y + r], ['Z']];
|
||||
},
|
||||
};
|
347
packages/core/src/shape/node.ts
Normal file
347
packages/core/src/shape/node.ts
Normal file
@ -0,0 +1,347 @@
|
||||
/**
|
||||
* @fileOverview common node shape
|
||||
* @author huangtonger@aliyun.com
|
||||
*/
|
||||
import { Group as GGroup } from '@antv/g-canvas';
|
||||
import { IShape, IElement } from '@antv/g-canvas/lib/interfaces';
|
||||
import { isArray, isNil, mix } from '@antv/util';
|
||||
import { ILabelConfig, ShapeOptions } from '../interface/shape';
|
||||
import { Item, LabelStyle, NodeConfig, ModelConfig } from '../types';
|
||||
import { formatPadding } from '../util/base';
|
||||
import Global from '../global';
|
||||
import Shape from './shape';
|
||||
import { shapeBase } from './shapeBase';
|
||||
|
||||
const singleNode: ShapeOptions = {
|
||||
itemType: 'node',
|
||||
// 单个图形的类型
|
||||
shapeType: 'single-node',
|
||||
/**
|
||||
* 文本相对图形的位置,默认以中心点
|
||||
* 位置包括: top, bottom, left, right, center
|
||||
* @type {String}
|
||||
*/
|
||||
labelPosition: 'center',
|
||||
/**
|
||||
* 文本相对偏移,当 labelPosition 不为 center 时有效
|
||||
* @type {Number}
|
||||
*/
|
||||
offset: Global.nodeLabel.offset,
|
||||
/**
|
||||
* 获取节点宽高
|
||||
* @internal 返回节点的大小,以 [width, height] 的方式维护
|
||||
* @param {Object} cfg 节点的配置项
|
||||
* @return {Array} 宽高
|
||||
*/
|
||||
getSize(cfg: ModelConfig): number[] {
|
||||
let size: number | number[] = cfg.size || this.getOptions({})!.size || Global.defaultNode.size;
|
||||
|
||||
// size 是数组,但长度为1,则补长度为2
|
||||
if (isArray(size) && size.length === 1) {
|
||||
size = [size[0], size[0]];
|
||||
}
|
||||
|
||||
// size 为数字,则转换为数组
|
||||
if (!isArray(size)) {
|
||||
size = [size, size];
|
||||
}
|
||||
return size;
|
||||
},
|
||||
// 私有方法,不希望扩展的节点复写这个方法
|
||||
getLabelStyleByPosition(cfg: NodeConfig, labelCfg: ILabelConfig): LabelStyle {
|
||||
const labelPosition = labelCfg.position || this.labelPosition;
|
||||
|
||||
// 默认的位置(最可能的情形),所以放在最上面
|
||||
if (labelPosition === 'center') {
|
||||
return { x: 0, y: 0, text: cfg!.label as string };
|
||||
}
|
||||
|
||||
let { offset } = labelCfg;
|
||||
if (isNil(offset)) {
|
||||
// 考虑 offset = 0 的场景,不用用 labelCfg.offset || Global.nodeLabel.offset
|
||||
offset = this.offset as number; // 不居中时的偏移量
|
||||
}
|
||||
|
||||
const size = this.getSize!(cfg as ModelConfig);
|
||||
|
||||
const width = size[0];
|
||||
const height = size[1];
|
||||
|
||||
let style: any;
|
||||
switch (labelPosition) {
|
||||
case 'top':
|
||||
style = {
|
||||
x: 0,
|
||||
y: 0 - height / 2 - (offset as number),
|
||||
textBaseline: 'bottom', // 文本在图形的上面
|
||||
};
|
||||
break;
|
||||
case 'bottom':
|
||||
style = {
|
||||
x: 0,
|
||||
y: height / 2 + (offset as number),
|
||||
textBaseline: 'top',
|
||||
};
|
||||
break;
|
||||
case 'left':
|
||||
style = {
|
||||
x: 0 - width / 2 - (offset as number),
|
||||
y: 0,
|
||||
textAlign: 'right',
|
||||
};
|
||||
break;
|
||||
default:
|
||||
style = {
|
||||
x: width / 2 + (offset as number),
|
||||
y: 0,
|
||||
textAlign: 'left',
|
||||
};
|
||||
break;
|
||||
}
|
||||
style.text = cfg.label;
|
||||
return style;
|
||||
},
|
||||
getLabelBgStyleByPosition(
|
||||
label: IElement,
|
||||
cfg: ModelConfig,
|
||||
labelCfg?: ILabelConfig,
|
||||
group?: GGroup,
|
||||
) {
|
||||
if (!label) {
|
||||
return {};
|
||||
}
|
||||
const bbox = label.getBBox();
|
||||
const backgroundStyle = labelCfg.style && labelCfg.style.background;
|
||||
if (!backgroundStyle) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const padding = formatPadding(backgroundStyle.padding);
|
||||
const backgroundWidth = bbox.width + padding[1] + padding[3];
|
||||
const backgroundHeight = bbox.height + padding[0] + padding[2];
|
||||
|
||||
let { offset } = labelCfg;
|
||||
if (isNil(offset)) {
|
||||
// 考虑 offset = 0 的场景,不用用 labelCfg.offset || Global.nodeLabel.offset
|
||||
offset = this.offset as number; // 不居中时的偏移量
|
||||
}
|
||||
|
||||
let style: any;
|
||||
style = {
|
||||
x: bbox.minX - padding[3],
|
||||
y: bbox.minY - padding[0]
|
||||
}
|
||||
|
||||
style = {
|
||||
...style,
|
||||
...backgroundStyle,
|
||||
width: backgroundWidth,
|
||||
height: backgroundHeight,
|
||||
};
|
||||
|
||||
return style;
|
||||
},
|
||||
drawShape(cfg: NodeConfig, group: GGroup): IShape {
|
||||
const { shapeType } = this; // || this.type,都已经加了 shapeType
|
||||
const style = this.getShapeStyle!(cfg);
|
||||
const shape = group.addShape(shapeType, {
|
||||
attrs: style,
|
||||
draggable: true,
|
||||
name: 'node-shape',
|
||||
});
|
||||
return shape;
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新linkPoints
|
||||
* @param {Object} cfg 节点数据配置项
|
||||
* @param {Group} group Item所在的group
|
||||
*/
|
||||
updateLinkPoints(cfg: NodeConfig, group: GGroup) {
|
||||
const { linkPoints: defaultLinkPoints } = this.getOptions(cfg) as ModelConfig;
|
||||
|
||||
const markLeft = group.find((element) => element.get('className') === 'link-point-left');
|
||||
const markRight = group.find((element) => element.get('className') === 'link-point-right');
|
||||
const markTop = group.find((element) => element.get('className') === 'link-point-top');
|
||||
const markBottom = group.find((element) => element.get('className') === 'link-point-bottom');
|
||||
|
||||
let currentLinkPoints;
|
||||
if (markLeft) {
|
||||
currentLinkPoints = markLeft.attr();
|
||||
}
|
||||
if (markRight && !currentLinkPoints) {
|
||||
currentLinkPoints = markRight.attr();
|
||||
}
|
||||
if (markTop && !currentLinkPoints) {
|
||||
currentLinkPoints = markTop.attr();
|
||||
}
|
||||
if (markBottom && !currentLinkPoints) {
|
||||
currentLinkPoints = markBottom.attr();
|
||||
}
|
||||
if (!currentLinkPoints) currentLinkPoints = defaultLinkPoints;
|
||||
|
||||
const linkPoints = mix({}, currentLinkPoints, cfg.linkPoints);
|
||||
|
||||
const { fill: markFill, stroke: markStroke, lineWidth: borderWidth } = linkPoints;
|
||||
let markSize = linkPoints.size / 2;
|
||||
if (!markSize) markSize = linkPoints.r;
|
||||
const { left, right, top, bottom } = cfg.linkPoints
|
||||
? cfg.linkPoints
|
||||
: { left: undefined, right: undefined, top: undefined, bottom: undefined };
|
||||
|
||||
const size = this.getSize!(cfg);
|
||||
const width = size[0];
|
||||
const height = size[1];
|
||||
const styles = {
|
||||
r: markSize,
|
||||
fill: markFill,
|
||||
stroke: markStroke,
|
||||
lineWidth: borderWidth,
|
||||
};
|
||||
|
||||
if (markLeft) {
|
||||
if (!left && left !== undefined) {
|
||||
markLeft.remove();
|
||||
} else {
|
||||
markLeft.attr({
|
||||
...styles,
|
||||
x: -width / 2,
|
||||
y: 0,
|
||||
});
|
||||
}
|
||||
} else if (left) {
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...styles,
|
||||
x: -width / 2,
|
||||
y: 0,
|
||||
},
|
||||
className: 'link-point-left',
|
||||
name: 'link-point-left',
|
||||
isAnchorPoint: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (markRight) {
|
||||
if (!right && right !== undefined) {
|
||||
markRight.remove();
|
||||
}
|
||||
markRight.attr({
|
||||
...styles,
|
||||
x: width / 2,
|
||||
y: 0,
|
||||
});
|
||||
} else if (right) {
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...styles,
|
||||
x: width / 2,
|
||||
y: 0,
|
||||
},
|
||||
className: 'link-point-right',
|
||||
name: 'link-point-right',
|
||||
isAnchorPoint: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (markTop) {
|
||||
if (!top && top !== undefined) {
|
||||
markTop.remove();
|
||||
}
|
||||
markTop.attr({
|
||||
...styles,
|
||||
x: 0,
|
||||
y: -height / 2,
|
||||
});
|
||||
} else if (top) {
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...styles,
|
||||
x: 0,
|
||||
y: -height / 2,
|
||||
},
|
||||
className: 'link-point-top',
|
||||
name: 'link-point-top',
|
||||
isAnchorPoint: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (markBottom) {
|
||||
if (!bottom && bottom !== undefined) {
|
||||
markBottom.remove();
|
||||
} else {
|
||||
markBottom.attr({
|
||||
...styles,
|
||||
x: 0,
|
||||
y: height / 2,
|
||||
});
|
||||
}
|
||||
} else if (bottom) {
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...styles,
|
||||
x: 0,
|
||||
y: height / 2,
|
||||
},
|
||||
className: 'link-point-bottom',
|
||||
name: 'link-point-bottom',
|
||||
isAnchorPoint: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
updateShape(cfg: NodeConfig, item: Item, keyShapeStyle: object, hasIcon: boolean) {
|
||||
const keyShape = item.get('keyShape');
|
||||
keyShape.attr({
|
||||
...keyShapeStyle,
|
||||
});
|
||||
|
||||
(this as any).updateLabel(cfg, item);
|
||||
// special for some types of nodes
|
||||
|
||||
if (hasIcon) {
|
||||
(this as any).updateIcon(cfg, item);
|
||||
}
|
||||
},
|
||||
updateIcon(cfg: NodeConfig, item: Item) {
|
||||
const group = item.getContainer();
|
||||
const { icon } = this.getOptions(cfg) as NodeConfig;
|
||||
const { show } = cfg.icon ? cfg.icon : { show: undefined };
|
||||
const iconShape = group.find((element) => element.get('className') === `${this.type}-icon`);
|
||||
if (iconShape) {
|
||||
// 若原先存在 icon
|
||||
if (show || show === undefined) {
|
||||
// 若传入 show: true, 或没有设置,则更新原有的 icon 样式
|
||||
const iconConfig = mix({}, iconShape.attr(), icon);
|
||||
const { width: w, height: h } = iconConfig;
|
||||
iconShape.attr({
|
||||
...iconConfig,
|
||||
x: -w / 2,
|
||||
y: -h / 2,
|
||||
});
|
||||
} else {
|
||||
// 若传入了 show: false 则删除原先的 icon
|
||||
iconShape.remove();
|
||||
}
|
||||
} else if (show) {
|
||||
// 如果原先不存在 icon,但传入了 show: true,则新增 icon
|
||||
const { width: w, height: h } = icon;
|
||||
group.addShape('image', {
|
||||
attrs: {
|
||||
...icon,
|
||||
x: -w! / 2,
|
||||
y: -h! / 2,
|
||||
},
|
||||
className: `${this.type}-icon`,
|
||||
name: `${this.type}-icon`,
|
||||
});
|
||||
// to ensure the label is on the top of all the shapes
|
||||
const labelShape = group.find((element) => element.get('className') === `node-label`);
|
||||
if (labelShape) {
|
||||
labelShape.toFront();
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const singleNodeDef = { ...shapeBase, ...singleNode };
|
||||
Shape.registerNode('single-node', singleNodeDef);
|
196
packages/core/src/shape/nodes/circle.ts
Normal file
196
packages/core/src/shape/nodes/circle.ts
Normal file
@ -0,0 +1,196 @@
|
||||
import { Group as GGroup } from '@antv/g-canvas';
|
||||
import { IShape } from '@antv/g-canvas/lib/interfaces';
|
||||
import { deepMix } from '@antv/util';
|
||||
import { Item, NodeConfig, ShapeStyle } from '../../types';
|
||||
import Global from '../../global';
|
||||
import Shape from '../shape';
|
||||
import { ShapeOptions } from '../../interface/shape';
|
||||
|
||||
// 带有图标的圆,可用于拓扑图中
|
||||
Shape.registerNode(
|
||||
'circle',
|
||||
{
|
||||
// 自定义节点时的配置
|
||||
options: {
|
||||
size: Global.defaultNode.size,
|
||||
style: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
stroke: Global.defaultNode.style.stroke,
|
||||
fill: Global.defaultNode.style.fill,
|
||||
lineWidth: Global.defaultNode.style.lineWidth
|
||||
},
|
||||
labelCfg: {
|
||||
style: {
|
||||
fill: Global.nodeLabel.style.fill,
|
||||
fontSize: Global.nodeLabel.style.fontSize,
|
||||
},
|
||||
},
|
||||
// 节点上左右上下四个方向上的链接circle配置
|
||||
linkPoints: {
|
||||
top: false,
|
||||
right: false,
|
||||
bottom: false,
|
||||
left: false,
|
||||
// circle的大小
|
||||
size: Global.defaultNode.linkPoints.size,
|
||||
lineWidth: Global.defaultNode.linkPoints.lineWidth,
|
||||
fill: Global.defaultNode.linkPoints.fill,
|
||||
stroke: Global.defaultNode.linkPoints.stroke,
|
||||
},
|
||||
// 节点中icon配置
|
||||
icon: {
|
||||
// 是否显示icon,值为 false 则不渲染icon
|
||||
show: false,
|
||||
// icon的地址,字符串类型
|
||||
img:
|
||||
'https://gw.alipayobjects.com/zos/bmw-prod/5d015065-8505-4e7a-baec-976f81e3c41d.svg',
|
||||
width: 20,
|
||||
height: 20,
|
||||
},
|
||||
stateStyles: {
|
||||
...Global.nodeStateStyles
|
||||
}
|
||||
},
|
||||
shapeType: 'circle',
|
||||
// 文本位置
|
||||
labelPosition: 'center',
|
||||
drawShape(cfg: NodeConfig, group: GGroup): IShape {
|
||||
const { icon: defaultIcon = {} } = this.getOptions(cfg) as NodeConfig;
|
||||
const style = this.getShapeStyle!(cfg);
|
||||
const icon = deepMix({}, defaultIcon, cfg.icon);
|
||||
const keyShape: IShape = group.addShape('circle', {
|
||||
attrs: style,
|
||||
className: `${this.type}-keyShape`,
|
||||
draggable: true,
|
||||
});
|
||||
|
||||
const { width, height, show } = icon;
|
||||
if (show) {
|
||||
group.addShape('image', {
|
||||
attrs: {
|
||||
x: -width / 2,
|
||||
y: -height / 2,
|
||||
...icon,
|
||||
},
|
||||
className: `${this.type}-icon`,
|
||||
name: `${this.type}-icon`,
|
||||
draggable: true,
|
||||
});
|
||||
}
|
||||
|
||||
(this as any).drawLinkPoints(cfg, group);
|
||||
|
||||
return keyShape;
|
||||
},
|
||||
/**
|
||||
* 绘制节点上的LinkPoints
|
||||
* @param {Object} cfg data数据配置项
|
||||
* @param {Group} group Group实例
|
||||
*/
|
||||
drawLinkPoints(cfg: NodeConfig, group: GGroup) {
|
||||
const { linkPoints = {} } = this.getOptions(cfg) as NodeConfig;
|
||||
|
||||
const { top, left, right, bottom, size: markSize, r: markR, ...markStyle } = linkPoints;
|
||||
const size = this.getSize!(cfg);
|
||||
const r = size[0] / 2;
|
||||
if (left) {
|
||||
// left circle
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...markStyle,
|
||||
x: -r,
|
||||
y: 0,
|
||||
r: markSize / 2 || markR || 5,
|
||||
},
|
||||
className: 'link-point-left',
|
||||
name: 'link-point-left',
|
||||
isAnchorPoint: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (right) {
|
||||
// right circle
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...markStyle,
|
||||
x: r,
|
||||
y: 0,
|
||||
r: markSize / 2 || markR || 5,
|
||||
},
|
||||
className: 'link-point-right',
|
||||
name: 'link-point-right',
|
||||
isAnchorPoint: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (top) {
|
||||
// top circle
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...markStyle,
|
||||
x: 0,
|
||||
y: -r,
|
||||
r: markSize / 2 || markR || 5,
|
||||
},
|
||||
className: 'link-point-top',
|
||||
name: 'link-point-top',
|
||||
isAnchorPoint: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (bottom) {
|
||||
// bottom circle
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...markStyle,
|
||||
x: 0,
|
||||
y: r,
|
||||
r: markSize / 2 || markR || 5,
|
||||
},
|
||||
className: 'link-point-bottom',
|
||||
name: 'link-point-bottom',
|
||||
isAnchorPoint: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 获取节点的样式,供基于该节点自定义时使用
|
||||
* @param {Object} cfg 节点数据模型
|
||||
* @return {Object} 节点的样式
|
||||
*/
|
||||
getShapeStyle(cfg: NodeConfig): ShapeStyle {
|
||||
const { style: defaultStyle } = this.getOptions(cfg) as NodeConfig;
|
||||
const strokeStyle = {
|
||||
stroke: cfg.color,
|
||||
};
|
||||
// 如果设置了color,则覆盖默认的stroke属性
|
||||
const style = deepMix({}, defaultStyle, strokeStyle);
|
||||
const size = (this as ShapeOptions).getSize!(cfg);
|
||||
const r = size[0] / 2;
|
||||
const styles = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
r,
|
||||
...style,
|
||||
};
|
||||
return styles;
|
||||
},
|
||||
update(cfg: NodeConfig, item: Item) {
|
||||
const group = item.getContainer();
|
||||
const size = (this as ShapeOptions).getSize!(cfg);
|
||||
// 下面这些属性需要覆盖默认样式与目前样式,但若在 cfg 中有指定则应该被 cfg 的相应配置覆盖。
|
||||
const strokeStyle = {
|
||||
stroke: cfg.color,
|
||||
r: size[0] / 2,
|
||||
};
|
||||
// 与 getShapeStyle 不同在于,update 时需要获取到当前的 style 进行融合。即新传入的配置项中没有涉及的属性,保留当前的配置。
|
||||
const keyShape = item.get('keyShape');
|
||||
const style = deepMix({}, keyShape.attr(), strokeStyle, cfg.style);
|
||||
|
||||
(this as any).updateShape(cfg, item, style, true);
|
||||
(this as any).updateLinkPoints(cfg, group);
|
||||
},
|
||||
},
|
||||
'single-node',
|
||||
);
|
206
packages/core/src/shape/nodes/diamond.ts
Normal file
206
packages/core/src/shape/nodes/diamond.ts
Normal file
@ -0,0 +1,206 @@
|
||||
import { Group as GGroup } from '@antv/g-canvas';
|
||||
import { IShape } from '@antv/g-canvas/lib/interfaces';
|
||||
import { mix } from '@antv/util';
|
||||
import { Item, NodeConfig, ShapeStyle } from '../../types';
|
||||
import Global from '../../global';
|
||||
import Shape from '../shape';
|
||||
|
||||
// 菱形shape
|
||||
Shape.registerNode(
|
||||
'diamond',
|
||||
{
|
||||
// 自定义节点时的配置
|
||||
options: {
|
||||
size: [80, 80],
|
||||
style: {
|
||||
stroke: Global.defaultNode.style.stroke,
|
||||
fill: Global.defaultNode.style.fill,
|
||||
lineWidth: Global.defaultNode.style.lineWidth
|
||||
},
|
||||
// 文本样式配置
|
||||
labelCfg: {
|
||||
style: {
|
||||
fill: Global.nodeLabel.style.fill,
|
||||
fontSize: Global.nodeLabel.style.fontSize,
|
||||
},
|
||||
},
|
||||
// 节点上左右上下四个方向上的链接circle配置
|
||||
linkPoints: {
|
||||
top: false,
|
||||
right: false,
|
||||
bottom: false,
|
||||
left: false,
|
||||
// circle的大小
|
||||
size: Global.defaultNode.linkPoints.size,
|
||||
lineWidth: Global.defaultNode.linkPoints.lineWidth,
|
||||
fill: Global.defaultNode.linkPoints.fill,
|
||||
stroke: Global.defaultNode.linkPoints.stroke,
|
||||
},
|
||||
// 节点中icon配置
|
||||
icon: {
|
||||
// 是否显示icon,值为 false 则不渲染icon
|
||||
show: false,
|
||||
// icon的地址,字符串类型
|
||||
img:
|
||||
'https://gw.alipayobjects.com/zos/bmw-prod/5d015065-8505-4e7a-baec-976f81e3c41d.svg',
|
||||
width: 20,
|
||||
height: 20,
|
||||
},
|
||||
stateStyles: {
|
||||
...Global.nodeStateStyles
|
||||
}
|
||||
},
|
||||
shapeType: 'diamond',
|
||||
// 文本位置
|
||||
labelPosition: 'center',
|
||||
drawShape(cfg: NodeConfig, group: GGroup): IShape {
|
||||
const { icon = {} } = this.getOptions(cfg) as NodeConfig;
|
||||
const style = this.getShapeStyle!(cfg);
|
||||
|
||||
const keyShape = group.addShape('path', {
|
||||
attrs: style,
|
||||
className: `${this.type}-keyShape`,
|
||||
name: `${this.type}-keyShape`,
|
||||
draggable: true,
|
||||
});
|
||||
|
||||
const { width: w, height: h, show } = icon;
|
||||
if (show) {
|
||||
const image = group.addShape('image', {
|
||||
attrs: {
|
||||
x: -w! / 2,
|
||||
y: -h! / 2,
|
||||
...icon,
|
||||
},
|
||||
className: `${this.type}-icon`,
|
||||
name: `${this.type}-icon`,
|
||||
draggable: true,
|
||||
});
|
||||
}
|
||||
|
||||
(this as any).drawLinkPoints(cfg, group);
|
||||
|
||||
return keyShape;
|
||||
},
|
||||
/**
|
||||
* 绘制节点上的LinkPoints
|
||||
* @param {Object} cfg data数据配置项
|
||||
* @param {Group} group Group实例
|
||||
*/
|
||||
drawLinkPoints(cfg: NodeConfig, group: GGroup) {
|
||||
const { linkPoints = {} } = this.getOptions(cfg) as NodeConfig;
|
||||
|
||||
const { top, left, right, bottom, size: markSize, r: markR, ...markStyle } = linkPoints;
|
||||
const size = this.getSize!(cfg);
|
||||
const width = size[0];
|
||||
const height = size[1];
|
||||
if (left) {
|
||||
// left circle
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...markStyle,
|
||||
x: -width / 2,
|
||||
y: 0,
|
||||
r: markSize / 2 || markR || 5,
|
||||
},
|
||||
className: 'link-point-left',
|
||||
name: 'link-point-left',
|
||||
isAnchorPoint: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (right) {
|
||||
// right circle
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...markStyle,
|
||||
x: width / 2,
|
||||
y: 0,
|
||||
r: markSize / 2 || markR || 5,
|
||||
},
|
||||
className: 'link-point-right',
|
||||
name: 'link-point-right',
|
||||
isAnchorPoint: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (top) {
|
||||
// top circle
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...markStyle,
|
||||
x: 0,
|
||||
y: -height / 2,
|
||||
r: markSize / 2 || markR || 5,
|
||||
},
|
||||
className: 'link-point-top',
|
||||
name: 'link-point-top',
|
||||
isAnchorPoint: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (bottom) {
|
||||
// bottom circle
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...markStyle,
|
||||
x: 0,
|
||||
y: height / 2,
|
||||
r: markSize / 2 || markR || 5,
|
||||
},
|
||||
className: 'link-point-bottom',
|
||||
name: 'link-point-bottom',
|
||||
isAnchorPoint: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
getPath(cfg: NodeConfig): Array<Array<string | number>> {
|
||||
const size = this.getSize!(cfg);
|
||||
const width = size[0];
|
||||
const height = size[1];
|
||||
const path = [
|
||||
['M', 0, -height / 2], // 上部顶点
|
||||
['L', width / 2, 0], // 右侧点
|
||||
['L', 0, height / 2], // 下部
|
||||
['L', -width / 2, 0], // 左侧
|
||||
['Z'], // 封闭
|
||||
];
|
||||
return path;
|
||||
},
|
||||
/**
|
||||
* 获取节点的样式,供基于该节点自定义时使用
|
||||
* @param {Object} cfg 节点数据模型
|
||||
* @return {Object} 节点的样式
|
||||
*/
|
||||
getShapeStyle(cfg: NodeConfig): ShapeStyle {
|
||||
const { style: defaultStyle } = this.getOptions(cfg) as NodeConfig;
|
||||
const strokeStyle: ShapeStyle = {
|
||||
stroke: cfg.color,
|
||||
};
|
||||
// 如果设置了color,则覆盖默认的stroke属性
|
||||
const style = mix({}, defaultStyle, strokeStyle);
|
||||
const path = (this as any).getPath(cfg);
|
||||
const styles = { path, ...style };
|
||||
return styles;
|
||||
},
|
||||
update(cfg: NodeConfig, item: Item) {
|
||||
const group = item.getContainer();
|
||||
// 这里不传 cfg 参数是因为 cfg.style 需要最后覆盖样式
|
||||
const { style: defaultStyle } = this.getOptions({}) as NodeConfig;
|
||||
const path = (this as any).getPath(cfg);
|
||||
// 下面这些属性需要覆盖默认样式与目前样式,但若在 cfg 中有指定则应该被 cfg 的相应配置覆盖。
|
||||
const strokeStyle = {
|
||||
stroke: cfg.color,
|
||||
path,
|
||||
};
|
||||
// 与 getShapeStyle 不同在于,update 时需要获取到当前的 style 进行融合。即新传入的配置项中没有涉及的属性,保留当前的配置。
|
||||
const keyShape = item.get('keyShape');
|
||||
let style = mix({}, defaultStyle, keyShape.attr(), strokeStyle);
|
||||
style = mix(style, cfg.style);
|
||||
|
||||
(this as any).updateShape(cfg, item, style, true);
|
||||
(this as any).updateLinkPoints(cfg, group);
|
||||
},
|
||||
},
|
||||
'single-node',
|
||||
);
|
207
packages/core/src/shape/nodes/ellipse.ts
Normal file
207
packages/core/src/shape/nodes/ellipse.ts
Normal file
@ -0,0 +1,207 @@
|
||||
import { Group as GGroup } from '@antv/g-canvas';
|
||||
import { IShape } from '@antv/g-canvas/lib/interfaces';
|
||||
import { mix } from '@antv/util';
|
||||
import { Item, NodeConfig, ShapeStyle } from '../../types';
|
||||
import Global from '../../global';
|
||||
import Shape from '../shape';
|
||||
|
||||
/**
|
||||
* 基本的椭圆,可以添加文本,默认文本居中
|
||||
*/
|
||||
Shape.registerNode(
|
||||
'ellipse',
|
||||
{
|
||||
// 自定义节点时的配置
|
||||
options: {
|
||||
size: [80, 40],
|
||||
style: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
stroke: Global.defaultNode.style.stroke,
|
||||
fill: Global.defaultNode.style.fill,
|
||||
lineWidth: Global.defaultNode.style.lineWidth
|
||||
},
|
||||
// 文本样式配置
|
||||
labelCfg: {
|
||||
style: {
|
||||
fill: Global.nodeLabel.style.fill,
|
||||
fontSize: Global.nodeLabel.style.fontSize,
|
||||
},
|
||||
},
|
||||
// 节点上左右上下四个方向上的链接circle配置
|
||||
linkPoints: {
|
||||
top: false,
|
||||
right: false,
|
||||
bottom: false,
|
||||
left: false,
|
||||
// circle的大小
|
||||
size: Global.defaultNode.linkPoints.size,
|
||||
lineWidth: Global.defaultNode.linkPoints.lineWidth,
|
||||
fill: Global.defaultNode.linkPoints.fill,
|
||||
stroke: Global.defaultNode.linkPoints.stroke,
|
||||
},
|
||||
// 节点中icon配置
|
||||
icon: {
|
||||
// 是否显示icon,值为 false 则不渲染icon
|
||||
show: false,
|
||||
// icon的地址,字符串类型
|
||||
img:
|
||||
'https://gw.alipayobjects.com/zos/bmw-prod/5d015065-8505-4e7a-baec-976f81e3c41d.svg',
|
||||
width: 20,
|
||||
height: 20,
|
||||
},
|
||||
stateStyles: {
|
||||
...Global.nodeStateStyles
|
||||
}
|
||||
},
|
||||
shapeType: 'ellipse',
|
||||
// 文本位置
|
||||
labelPosition: 'center',
|
||||
drawShape(cfg: NodeConfig, group: GGroup): IShape {
|
||||
const { icon = {} } = this.getOptions(cfg) as NodeConfig;
|
||||
const style = this.getShapeStyle!(cfg);
|
||||
|
||||
const keyShape = group.addShape('ellipse', {
|
||||
attrs: style,
|
||||
className: 'ellipse-keyShape',
|
||||
name: 'ellipse-keyShape',
|
||||
draggable: true,
|
||||
});
|
||||
|
||||
const { width, height, show } = icon;
|
||||
if (show) {
|
||||
const image = group.addShape('image', {
|
||||
attrs: {
|
||||
x: -width! / 2,
|
||||
y: -height! / 2,
|
||||
...icon,
|
||||
},
|
||||
className: `${this.type}-icon`,
|
||||
name: `${this.type}-icon`,
|
||||
draggable: true,
|
||||
});
|
||||
}
|
||||
|
||||
(this as any).drawLinkPoints(cfg, group);
|
||||
|
||||
return keyShape;
|
||||
},
|
||||
/**
|
||||
* 绘制节点上的LinkPoints
|
||||
* @param {Object} cfg data数据配置项
|
||||
* @param {Group} group Group实例
|
||||
*/
|
||||
drawLinkPoints(cfg: NodeConfig, group: GGroup) {
|
||||
const { linkPoints = {} } = this.getOptions(cfg) as NodeConfig;
|
||||
|
||||
const { top, left, right, bottom, size: markSize, r: markR, ...markStyle } = linkPoints;
|
||||
const size = this.getSize!(cfg);
|
||||
const rx = size[0] / 2;
|
||||
const ry = size[1] / 2;
|
||||
|
||||
if (left) {
|
||||
// left circle
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...markStyle,
|
||||
x: -rx,
|
||||
y: 0,
|
||||
r: markSize / 2 || markR || 5,
|
||||
},
|
||||
className: 'link-point-left',
|
||||
name: 'link-point-left',
|
||||
isAnchorPoint: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (right) {
|
||||
// right circle
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...markStyle,
|
||||
x: rx,
|
||||
y: 0,
|
||||
r: markSize / 2 || markR || 5,
|
||||
},
|
||||
className: 'link-point-right',
|
||||
name: 'link-point-right',
|
||||
isAnchorPoint: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (top) {
|
||||
// top circle
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...markStyle,
|
||||
x: 0,
|
||||
y: -ry,
|
||||
r: markSize / 2 || markR || 5,
|
||||
},
|
||||
className: 'link-point-top',
|
||||
name: 'link-point-top',
|
||||
isAnchorPoint: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (bottom) {
|
||||
// bottom circle
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...markStyle,
|
||||
x: 0,
|
||||
y: ry,
|
||||
r: markSize / 2 || markR || 5,
|
||||
},
|
||||
className: 'link-point-bottom',
|
||||
name: 'link-point-bottom',
|
||||
isAnchorPoint: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 获取节点的样式,供基于该节点自定义时使用
|
||||
* @param {Object} cfg 节点数据模型
|
||||
* @return {Object} 节点的样式
|
||||
*/
|
||||
getShapeStyle(cfg: NodeConfig): ShapeStyle {
|
||||
const { style: defaultStyle } = this.getOptions(cfg) as NodeConfig;
|
||||
const strokeStyle: ShapeStyle = {
|
||||
stroke: cfg.color,
|
||||
};
|
||||
// 如果设置了color,则覆盖默认的stroke属性
|
||||
const style = mix({}, defaultStyle, strokeStyle);
|
||||
const size = this.getSize!(cfg);
|
||||
const rx = size[0] / 2;
|
||||
const ry = size[1] / 2;
|
||||
const styles = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
rx,
|
||||
ry,
|
||||
...style,
|
||||
};
|
||||
return styles;
|
||||
},
|
||||
update(cfg: NodeConfig, item: Item) {
|
||||
const group = item.getContainer();
|
||||
// 这里不传 cfg 参数是因为 cfg.style 需要最后覆盖样式
|
||||
const { style: defaultStyle } = this.getOptions({}) as NodeConfig;
|
||||
const size = this.getSize!(cfg);
|
||||
|
||||
const strokeStyle = {
|
||||
stroke: cfg.color,
|
||||
rx: size[0] / 2,
|
||||
ry: size[1] / 2,
|
||||
};
|
||||
// 与 getShapeStyle 不同在于,update 时需要获取到当前的 style 进行融合。即新传入的配置项中没有涉及的属性,保留当前的配置。
|
||||
const keyShape = item.get('keyShape');
|
||||
let style = mix({}, defaultStyle, keyShape.attr(), strokeStyle);
|
||||
style = mix(style, cfg.style);
|
||||
|
||||
(this as any).updateShape(cfg, item, style, true);
|
||||
(this as any).updateLinkPoints(cfg, group);
|
||||
},
|
||||
},
|
||||
'single-node',
|
||||
);
|
163
packages/core/src/shape/nodes/image.ts
Normal file
163
packages/core/src/shape/nodes/image.ts
Normal file
@ -0,0 +1,163 @@
|
||||
import Shape from '../shape';
|
||||
import { NodeConfig, Item } from '../../types';
|
||||
import { Group as GGroup } from '@antv/g-canvas';
|
||||
import { IShape } from '@antv/g-canvas/lib/interfaces';
|
||||
|
||||
/**
|
||||
* 基本的图片,可以添加文本,默认文本在图片的下面
|
||||
*/
|
||||
Shape.registerNode(
|
||||
'image',
|
||||
{
|
||||
options: {
|
||||
img: 'https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*eD7nT6tmYgAAAAAAAAAAAABkARQnAQ',
|
||||
size: 200,
|
||||
clipCfg: {
|
||||
show: false,
|
||||
type: 'circle',
|
||||
// circle
|
||||
r: 50,
|
||||
// ellipse
|
||||
rx: 50,
|
||||
ry: 35,
|
||||
// rect
|
||||
width: 50,
|
||||
height: 35,
|
||||
// polygon
|
||||
points: [
|
||||
[30, 12],
|
||||
[12, 30],
|
||||
[30, 48],
|
||||
[48, 30],
|
||||
],
|
||||
// path
|
||||
path: [
|
||||
['M', 25, 25],
|
||||
['L', 50, 25],
|
||||
['A', 12.5, 12.5, 0, 1, 1, 50, 50],
|
||||
['A', 12.5, 12.5, 0, 1, 0, 50, 50],
|
||||
['L', 25, 75],
|
||||
['Z'],
|
||||
],
|
||||
// 坐标
|
||||
x: 0,
|
||||
y: 0,
|
||||
// clip 的属性样式
|
||||
// style: {
|
||||
// lineWidth: 1
|
||||
// },
|
||||
},
|
||||
},
|
||||
shapeType: 'image',
|
||||
labelPosition: 'bottom',
|
||||
drawShape(cfg: NodeConfig, group: GGroup): IShape {
|
||||
const { shapeType } = this; // || this.type,都已经加了 shapeType
|
||||
const style = this.getShapeStyle!(cfg);
|
||||
delete style.fill;
|
||||
const shape = group.addShape(shapeType, {
|
||||
attrs: style,
|
||||
className: `${this.type}-keyShape`,
|
||||
name: `${this.type}-keyShape`,
|
||||
draggable: true,
|
||||
});
|
||||
(this as any).drawClip(cfg, shape);
|
||||
return shape;
|
||||
},
|
||||
drawClip(cfg: NodeConfig, shape: IShape) {
|
||||
const { clipCfg: clip } = this.getOptions(cfg);
|
||||
|
||||
if (!clip.show) {
|
||||
return;
|
||||
}
|
||||
// 支持 circle、rect、ellipse、Polygon 及自定义 path clip
|
||||
const { type, x, y, style } = clip;
|
||||
if (type === 'circle') {
|
||||
const { r } = clip;
|
||||
shape.setClip({
|
||||
type: 'circle',
|
||||
attrs: {
|
||||
r,
|
||||
x,
|
||||
y,
|
||||
...style,
|
||||
},
|
||||
});
|
||||
} else if (type === 'rect') {
|
||||
const { width, height } = clip;
|
||||
const rectX = x - width / 2;
|
||||
const rectY = y - height / 2;
|
||||
shape.setClip({
|
||||
type: 'rect',
|
||||
attrs: {
|
||||
x: rectX,
|
||||
y: rectY,
|
||||
width,
|
||||
height,
|
||||
...style,
|
||||
},
|
||||
});
|
||||
} else if (type === 'ellipse') {
|
||||
const { rx, ry } = clip;
|
||||
shape.setClip({
|
||||
type: 'ellipse',
|
||||
attrs: {
|
||||
x,
|
||||
y,
|
||||
rx,
|
||||
ry,
|
||||
...style,
|
||||
},
|
||||
});
|
||||
} else if (type === 'polygon') {
|
||||
const { points } = clip;
|
||||
shape.setClip({
|
||||
type: 'polygon',
|
||||
attrs: {
|
||||
points,
|
||||
...style,
|
||||
},
|
||||
});
|
||||
} else if (type === 'path') {
|
||||
const { path } = clip;
|
||||
shape.setClip({
|
||||
type: 'path',
|
||||
attrs: {
|
||||
path,
|
||||
...style,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
getShapeStyle(cfg: NodeConfig) {
|
||||
const { style: defaultStyle } = this.getOptions(cfg);
|
||||
const size = this.getSize!(cfg);
|
||||
const { img } = this.getOptions(cfg);
|
||||
let width = size[0];
|
||||
let height = size[1];
|
||||
if (defaultStyle) {
|
||||
width = defaultStyle.width || size[0];
|
||||
height = defaultStyle.height || size[1];
|
||||
}
|
||||
const style = {
|
||||
x: -width / 2, // 节点的位置在上层确定,所以这里仅使用相对位置即可
|
||||
y: -height / 2,
|
||||
width,
|
||||
height,
|
||||
img,
|
||||
...defaultStyle,
|
||||
};
|
||||
return style;
|
||||
},
|
||||
updateShapeStyle(cfg: NodeConfig, item: Item) {
|
||||
const group = item.getContainer();
|
||||
const shapeClassName = `${this.itemType}-shape`;
|
||||
const shape =
|
||||
group.find((element) => element.get('className') === shapeClassName) || item.getKeyShape();
|
||||
const shapeStyle = this.getShapeStyle!(cfg);
|
||||
if (shape) {
|
||||
shape.attr(shapeStyle);
|
||||
}
|
||||
},
|
||||
},
|
||||
'single-node',
|
||||
);
|
8
packages/core/src/shape/nodes/index.ts
Normal file
8
packages/core/src/shape/nodes/index.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import './circle';
|
||||
import './rect';
|
||||
import './ellipse';
|
||||
import './diamond';
|
||||
import './triangle';
|
||||
import './modelRect';
|
||||
import './star';
|
||||
import './image';
|
481
packages/core/src/shape/nodes/modelRect.ts
Normal file
481
packages/core/src/shape/nodes/modelRect.ts
Normal file
@ -0,0 +1,481 @@
|
||||
import { Group as GGroup } from '@antv/g-canvas';
|
||||
import { IShape } from '@antv/g-canvas/lib/interfaces';
|
||||
import { mix, isString } from '@antv/util';
|
||||
import { Item, NodeConfig, ShapeStyle } from '../../types';
|
||||
import Shape from '../shape';
|
||||
import Global from '../../global';
|
||||
import { ShapeOptions } from '../../interface/shape';
|
||||
|
||||
Shape.registerNode(
|
||||
'modelRect',
|
||||
{
|
||||
// 自定义节点时的配置
|
||||
options: {
|
||||
size: [185, 70],
|
||||
style: {
|
||||
radius: 5,
|
||||
stroke: '#69c0ff',
|
||||
fill: '#ffffff',
|
||||
lineWidth: Global.defaultNode.style.lineWidth,
|
||||
fillOpacity: 1,
|
||||
},
|
||||
// 文本样式配置
|
||||
labelCfg: {
|
||||
style: {
|
||||
fill: '#595959',
|
||||
fontSize: 14,
|
||||
},
|
||||
offset: 30, // 距离左侧的 offset,没有设置 y 轴上移动的配置
|
||||
},
|
||||
descriptionCfg: {
|
||||
style: {
|
||||
fontSize: 12,
|
||||
fill: '#bfbfbf',
|
||||
},
|
||||
paddingTop: 0,
|
||||
},
|
||||
preRect: {
|
||||
show: true,
|
||||
width: 4,
|
||||
fill: '#40a9ff',
|
||||
radius: 2,
|
||||
},
|
||||
// 节点上左右上下四个方向上的链接circle配置
|
||||
linkPoints: {
|
||||
top: false,
|
||||
right: false,
|
||||
bottom: false,
|
||||
left: false,
|
||||
// circle的大小
|
||||
size: 10,
|
||||
lineWidth: 1,
|
||||
fill: '#72CC4A',
|
||||
stroke: '#72CC4A',
|
||||
},
|
||||
// 节点中icon配置
|
||||
logoIcon: {
|
||||
// 是否显示icon,值为 false 则不渲染icon
|
||||
show: true,
|
||||
x: 0,
|
||||
y: 0,
|
||||
// icon的地址,字符串类型
|
||||
img:
|
||||
'https://gw.alipayobjects.com/zos/basement_prod/4f81893c-1806-4de4-aff3-9a6b266bc8a2.svg',
|
||||
width: 16,
|
||||
height: 16,
|
||||
// 用于调整图标的左右位置
|
||||
offset: 0,
|
||||
},
|
||||
// 节点中表示状态的icon配置
|
||||
stateIcon: {
|
||||
// 是否显示icon,值为 false 则不渲染icon
|
||||
show: true,
|
||||
x: 0,
|
||||
y: 0,
|
||||
// icon的地址,字符串类型
|
||||
img:
|
||||
'https://gw.alipayobjects.com/zos/basement_prod/300a2523-67e0-4cbf-9d4a-67c077b40395.svg',
|
||||
width: 16,
|
||||
height: 16,
|
||||
// 用于调整图标的左右位置
|
||||
offset: -5,
|
||||
},
|
||||
// 连接点,默认为左右
|
||||
// anchorPoints: [{ x: 0, y: 0.5 }, { x: 1, y: 0.5 }]
|
||||
anchorPoints: [
|
||||
[0, 0.5],
|
||||
[1, 0.5],
|
||||
],
|
||||
},
|
||||
shapeType: 'modelRect',
|
||||
drawShape(cfg: NodeConfig, group: GGroup): IShape {
|
||||
const { preRect = {} } = this.getOptions(cfg) as NodeConfig;
|
||||
const style = this.getShapeStyle!(cfg);
|
||||
const size = (this as ShapeOptions).getSize!(cfg);
|
||||
const width = size[0];
|
||||
const height = size[1];
|
||||
|
||||
const keyShape = group.addShape('rect', {
|
||||
attrs: style,
|
||||
className: `${this.type}-keyShape`,
|
||||
name: `${this.type}-keyShape`,
|
||||
draggable: true,
|
||||
});
|
||||
|
||||
const { show: preRectShow, ...preRectStyle } = preRect;
|
||||
if (preRectShow) {
|
||||
group.addShape('rect', {
|
||||
attrs: {
|
||||
x: -width / 2,
|
||||
y: -height / 2,
|
||||
height,
|
||||
...preRectStyle,
|
||||
},
|
||||
className: 'pre-rect',
|
||||
name: 'pre-rect',
|
||||
draggable: true,
|
||||
});
|
||||
}
|
||||
|
||||
(this as any).drawLogoIcon(cfg, group);
|
||||
|
||||
(this as any).drawStateIcon(cfg, group);
|
||||
|
||||
(this as any).drawLinkPoints(cfg, group);
|
||||
return keyShape;
|
||||
},
|
||||
/**
|
||||
* 绘制模型矩形左边的logo图标
|
||||
* @param {Object} cfg 数据配置项
|
||||
* @param {Group} group Group实例
|
||||
*/
|
||||
drawLogoIcon(cfg: NodeConfig, group: GGroup) {
|
||||
const { logoIcon = {} } = this.getOptions(cfg) as NodeConfig;
|
||||
const size = (this as ShapeOptions).getSize!(cfg);
|
||||
const width = size[0];
|
||||
|
||||
if (logoIcon.show) {
|
||||
const { width: w, height: h, x, y, offset, ...logoIconStyle } = logoIcon;
|
||||
group.addShape('image', {
|
||||
attrs: {
|
||||
...logoIconStyle,
|
||||
x: x || -width / 2 + (w as number) + (offset as number),
|
||||
y: y || -(h as number) / 2,
|
||||
width: w,
|
||||
height: h,
|
||||
},
|
||||
className: 'rect-logo-icon',
|
||||
name: 'rect-logo-icon',
|
||||
draggable: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 绘制模型矩形右边的状态图标
|
||||
* @param {Object} cfg 数据配置项
|
||||
* @param {Group} group Group实例
|
||||
*/
|
||||
drawStateIcon(cfg: NodeConfig, group: GGroup) {
|
||||
const { stateIcon = {} } = this.getOptions(cfg) as NodeConfig;
|
||||
const size = (this as ShapeOptions).getSize!(cfg);
|
||||
const width = size[0];
|
||||
|
||||
if (stateIcon.show) {
|
||||
const { width: w, height: h, x, y, offset, ...iconStyle } = stateIcon;
|
||||
group.addShape('image', {
|
||||
attrs: {
|
||||
...iconStyle,
|
||||
x: x || width / 2 - (w as number) + (offset as number),
|
||||
y: y || -(h as number) / 2,
|
||||
width: w,
|
||||
height: h,
|
||||
},
|
||||
className: 'rect-state-icon',
|
||||
name: 'rect-state-icon',
|
||||
draggable: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 绘制节点上的LinkPoints
|
||||
* @param {Object} cfg data数据配置项
|
||||
* @param {Group} group Group实例
|
||||
*/
|
||||
drawLinkPoints(cfg: NodeConfig, group: GGroup) {
|
||||
const { linkPoints = {} } = this.getOptions(cfg) as NodeConfig;
|
||||
|
||||
const { top, left, right, bottom, size: markSize, r: markR, ...markStyle } = linkPoints;
|
||||
const size = (this as ShapeOptions).getSize!(cfg);
|
||||
const width = size[0];
|
||||
const height = size[1];
|
||||
if (left) {
|
||||
// left circle
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...markStyle,
|
||||
x: -width / 2,
|
||||
y: 0,
|
||||
r: markSize / 2 || markR || 5,
|
||||
},
|
||||
className: 'link-point-left',
|
||||
name: 'link-point-left',
|
||||
isAnchorPoint: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (right) {
|
||||
// right circle
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...markStyle,
|
||||
x: width / 2,
|
||||
y: 0,
|
||||
r: markSize / 2 || markR || 5,
|
||||
},
|
||||
className: 'link-point-right',
|
||||
name: 'link-point-right',
|
||||
isAnchorPoint: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (top) {
|
||||
// top circle
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...markStyle,
|
||||
x: 0,
|
||||
y: -height / 2,
|
||||
r: markSize / 2 || markR || 5,
|
||||
},
|
||||
className: 'link-point-top',
|
||||
name: 'link-point-top',
|
||||
isAnchorPoint: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (bottom) {
|
||||
// bottom circle
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...markStyle,
|
||||
x: 0,
|
||||
y: height / 2,
|
||||
r: markSize / 2 || markR || 5,
|
||||
},
|
||||
className: 'link-point-bottom',
|
||||
name: 'link-point-bottom',
|
||||
isAnchorPoint: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
drawLabel(cfg: NodeConfig, group: GGroup): IShape {
|
||||
const { labelCfg = {}, logoIcon = {}, descriptionCfg = {} } = this.getOptions(cfg) as NodeConfig;
|
||||
|
||||
const size = (this as ShapeOptions).getSize!(cfg);
|
||||
const width = size[0];
|
||||
|
||||
let label = null;
|
||||
|
||||
const { show, width: w } = logoIcon;
|
||||
let offsetX = -width / 2 + labelCfg.offset;
|
||||
|
||||
if (show) {
|
||||
offsetX = -width / 2 + (w as number) + labelCfg.offset;
|
||||
}
|
||||
|
||||
const { style: fontStyle } = labelCfg;
|
||||
const { style: descriptionStyle, paddingTop: descriptionPaddingTop } = descriptionCfg;
|
||||
if (isString(cfg.description)) {
|
||||
label = group.addShape('text', {
|
||||
attrs: {
|
||||
...fontStyle,
|
||||
x: offsetX,
|
||||
y: -5,
|
||||
text: cfg.label,
|
||||
},
|
||||
className: 'text-shape',
|
||||
name: 'text-shape',
|
||||
draggable: true,
|
||||
});
|
||||
|
||||
group.addShape('text', {
|
||||
attrs: {
|
||||
...descriptionStyle,
|
||||
x: offsetX,
|
||||
y: 17 + (descriptionPaddingTop as any || 0),
|
||||
text: cfg.description,
|
||||
},
|
||||
className: 'rect-description',
|
||||
name: 'rect-description',
|
||||
draggable: true,
|
||||
});
|
||||
} else {
|
||||
label = group.addShape('text', {
|
||||
attrs: {
|
||||
...fontStyle,
|
||||
x: offsetX,
|
||||
y: 7,
|
||||
text: cfg.label,
|
||||
},
|
||||
draggable: true,
|
||||
});
|
||||
}
|
||||
return label;
|
||||
},
|
||||
/**
|
||||
* 获取节点的样式,供基于该节点自定义时使用
|
||||
* @param {Object} cfg 节点数据模型
|
||||
* @return {Object} 节点的样式
|
||||
*/
|
||||
getShapeStyle(cfg: NodeConfig) {
|
||||
const { style: defaultStyle } = this.getOptions(cfg) as NodeConfig;
|
||||
const strokeStyle: ShapeStyle = {
|
||||
stroke: cfg.color,
|
||||
};
|
||||
// 如果设置了color,则覆盖默认的stroke属性
|
||||
const style: ShapeStyle = mix({}, defaultStyle, strokeStyle);
|
||||
const size = (this as ShapeOptions).getSize!(cfg);
|
||||
const width = style.width || size[0];
|
||||
const height = style.height || size[1];
|
||||
const styles = {
|
||||
x: -width / 2,
|
||||
y: -height / 2,
|
||||
width,
|
||||
height,
|
||||
...style,
|
||||
};
|
||||
return styles;
|
||||
},
|
||||
update(cfg: NodeConfig, item: Item) {
|
||||
const { style = {}, labelCfg = {}, descriptionCfg = {} } = this.getOptions(cfg) as NodeConfig;
|
||||
const size = (this as ShapeOptions).getSize!(cfg);
|
||||
const width = size[0];
|
||||
const height = size[1];
|
||||
const keyShape = item.get('keyShape');
|
||||
keyShape.attr({
|
||||
...style,
|
||||
x: -width / 2,
|
||||
y: -height / 2,
|
||||
width,
|
||||
height,
|
||||
});
|
||||
|
||||
const group = item.getContainer();
|
||||
|
||||
const logoIconShape = group.find((element) => element.get('className') === 'rect-logo-icon');
|
||||
const currentLogoIconAttr = logoIconShape ? logoIconShape.attr() : {};
|
||||
|
||||
const logoIcon = mix({}, currentLogoIconAttr, cfg.logoIcon);
|
||||
|
||||
let { width: w } = logoIcon;
|
||||
if (w === undefined) {
|
||||
w = (this as any).options.logoIcon.width;
|
||||
}
|
||||
const show = cfg.logoIcon ? cfg.logoIcon.show : undefined;
|
||||
|
||||
const { offset } = labelCfg;
|
||||
let offsetX = -width / 2 + w + offset;
|
||||
|
||||
if (!show && show !== undefined) {
|
||||
offsetX = -width / 2 + offset;
|
||||
}
|
||||
|
||||
const label = group.find((element) => element.get('className') === 'node-label');
|
||||
const description = group.find((element) => element.get('className') === 'rect-description');
|
||||
if (cfg.label) {
|
||||
if (!label) {
|
||||
group.addShape('text', {
|
||||
attrs: {
|
||||
...labelCfg.style,
|
||||
x: offsetX,
|
||||
y: cfg.description ? -5 : 7,
|
||||
text: cfg.label,
|
||||
},
|
||||
className: 'node-label',
|
||||
name: 'node-label',
|
||||
draggable: true,
|
||||
});
|
||||
} else {
|
||||
const cfgStyle = cfg.labelCfg ? cfg.labelCfg.style : {};
|
||||
const labelStyle = mix({}, label.attr(), cfgStyle);
|
||||
if (cfg.label) labelStyle.text = cfg.label;
|
||||
labelStyle.x = offsetX;
|
||||
if (isString(cfg.description)) labelStyle.y = -5;
|
||||
if (description) {
|
||||
description.resetMatrix();
|
||||
description.attr({
|
||||
x: offsetX,
|
||||
});
|
||||
}
|
||||
label.resetMatrix();
|
||||
label.attr(labelStyle);
|
||||
}
|
||||
}
|
||||
if (isString(cfg.description)) {
|
||||
const { paddingTop } = descriptionCfg;
|
||||
if (!description) {
|
||||
group.addShape('text', {
|
||||
attrs: {
|
||||
...descriptionCfg.style,
|
||||
x: offsetX,
|
||||
y: 17 + (paddingTop as any || 0),
|
||||
text: cfg.description,
|
||||
},
|
||||
className: 'rect-description',
|
||||
name: 'rect-description',
|
||||
draggable: true,
|
||||
});
|
||||
} else {
|
||||
const cfgStyle = cfg.descriptionCfg ? cfg.descriptionCfg.style : {};
|
||||
const descriptionStyle = mix({}, description.attr(), cfgStyle);
|
||||
if (isString(cfg.description)) descriptionStyle.text = cfg.description;
|
||||
descriptionStyle.x = offsetX;
|
||||
description.resetMatrix();
|
||||
description.attr({
|
||||
...descriptionStyle,
|
||||
y: 17 + (paddingTop as any || 0),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const preRectShape = group.find((element) => element.get('className') === 'pre-rect');
|
||||
if (preRectShape) {
|
||||
const preRect = mix({}, preRectShape.attr(), cfg.preRect);
|
||||
preRectShape.attr({
|
||||
...preRect,
|
||||
x: -width / 2,
|
||||
y: -height / 2,
|
||||
height,
|
||||
});
|
||||
}
|
||||
|
||||
if (logoIconShape) {
|
||||
if (!show && show !== undefined) {
|
||||
logoIconShape.remove();
|
||||
} else {
|
||||
const { width: logoW, height: h, x, y, offset: logoOffset, ...logoIconStyle } = logoIcon;
|
||||
logoIconShape.attr({
|
||||
...logoIconStyle,
|
||||
x: x || -width / 2 + logoW + logoOffset,
|
||||
y: y || -h / 2,
|
||||
width: logoW,
|
||||
height: h,
|
||||
});
|
||||
}
|
||||
} else if (show) {
|
||||
(this as any).drawLogoIcon(cfg, group);
|
||||
}
|
||||
|
||||
const stateIconShape = group.find(
|
||||
(element) => element.get('className') === 'rect-state-icon',
|
||||
);
|
||||
const currentStateIconAttr = stateIconShape ? stateIconShape.attr() : {};
|
||||
const stateIcon = mix({}, currentStateIconAttr, cfg.stateIcon);
|
||||
if (stateIconShape) {
|
||||
if (!stateIcon.show && stateIcon.show !== undefined) {
|
||||
stateIconShape.remove();
|
||||
}
|
||||
const {
|
||||
width: stateW,
|
||||
height: h,
|
||||
x,
|
||||
y,
|
||||
offset: stateOffset,
|
||||
...stateIconStyle
|
||||
} = stateIcon;
|
||||
stateIconShape.attr({
|
||||
...stateIconStyle,
|
||||
x: x || width / 2 - stateW + stateOffset,
|
||||
y: y || -h / 2,
|
||||
width: stateW,
|
||||
height: h,
|
||||
});
|
||||
} else if (stateIcon.show) {
|
||||
(this as any).drawStateIcon(cfg, group);
|
||||
}
|
||||
|
||||
(this as any).updateLinkPoints(cfg, group);
|
||||
},
|
||||
},
|
||||
'single-node',
|
||||
);
|
199
packages/core/src/shape/nodes/rect.ts
Normal file
199
packages/core/src/shape/nodes/rect.ts
Normal file
@ -0,0 +1,199 @@
|
||||
import { Group as GGroup } from '@antv/g-canvas';
|
||||
import { IShape } from '@antv/g-canvas/lib/interfaces';
|
||||
import { mix } from '@antv/util';
|
||||
import { Item, NodeConfig, ShapeStyle } from '../../types';
|
||||
import Global from '../../global';
|
||||
import Shape from '../shape';
|
||||
import { ShapeOptions } from '../../interface/shape';
|
||||
|
||||
Shape.registerNode(
|
||||
'rect',
|
||||
{
|
||||
// 自定义节点时的配置
|
||||
options: {
|
||||
size: [100, 30],
|
||||
style: {
|
||||
radius: 0,
|
||||
stroke: Global.defaultNode.style.stroke,
|
||||
fill: Global.defaultNode.style.fill,
|
||||
lineWidth: Global.defaultNode.style.lineWidth
|
||||
},
|
||||
// 文本样式配置
|
||||
labelCfg: {
|
||||
style: {
|
||||
fill: Global.nodeLabel.style.fill,
|
||||
fontSize: Global.nodeLabel.style.fontSize,
|
||||
},
|
||||
},
|
||||
// 节点上左右上下四个方向上的链接circle配置
|
||||
linkPoints: {
|
||||
top: false,
|
||||
right: false,
|
||||
bottom: false,
|
||||
left: false,
|
||||
// circle的大小
|
||||
size: Global.defaultNode.linkPoints.size,
|
||||
lineWidth: Global.defaultNode.linkPoints.lineWidth,
|
||||
fill: Global.defaultNode.linkPoints.fill,
|
||||
stroke: Global.defaultNode.linkPoints.stroke,
|
||||
},
|
||||
// 节点中icon配置
|
||||
icon: {
|
||||
// 是否显示icon,值为 false 则不渲染icon
|
||||
show: false,
|
||||
// icon的地址,字符串类型
|
||||
img:
|
||||
'https://gw.alipayobjects.com/zos/bmw-prod/5d015065-8505-4e7a-baec-976f81e3c41d.svg',
|
||||
width: 20,
|
||||
height: 20,
|
||||
},
|
||||
// 连接点,默认为左右
|
||||
// anchorPoints: [{ x: 0, y: 0.5 }, { x: 1, y: 0.5 }]
|
||||
anchorPoints: [
|
||||
[0, 0.5],
|
||||
[1, 0.5],
|
||||
],
|
||||
stateStyles: {
|
||||
...Global.nodeStateStyles
|
||||
}
|
||||
},
|
||||
shapeType: 'rect',
|
||||
labelPosition: 'center',
|
||||
drawShape(cfg: NodeConfig, group: GGroup): IShape {
|
||||
const style = this.getShapeStyle!(cfg);
|
||||
|
||||
const keyShape = group.addShape('rect', {
|
||||
attrs: style,
|
||||
className: `${this.type}-keyShape`,
|
||||
name: `${this.type}-keyShape`,
|
||||
draggable: true,
|
||||
});
|
||||
|
||||
(this as any).drawLinkPoints(cfg, group);
|
||||
return keyShape;
|
||||
},
|
||||
/**
|
||||
* 绘制节点上的LinkPoints
|
||||
* @param {Object} cfg data数据配置项
|
||||
* @param {Group} group Group实例
|
||||
*/
|
||||
drawLinkPoints(cfg: NodeConfig, group: GGroup) {
|
||||
const { linkPoints = {} } = this.getOptions(cfg) as NodeConfig;
|
||||
|
||||
const { top, left, right, bottom, size: markSize, r: markR, ...markStyle } = linkPoints;
|
||||
const size = (this as ShapeOptions).getSize!(cfg);
|
||||
const width = size[0];
|
||||
const height = size[1];
|
||||
|
||||
if (left) {
|
||||
// left circle
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...markStyle,
|
||||
x: -width / 2,
|
||||
y: 0,
|
||||
r: markSize / 2 || markR || 5,
|
||||
},
|
||||
className: 'link-point-left',
|
||||
name: 'link-point-left',
|
||||
isAnchorPoint: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (right) {
|
||||
// right circle
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...markStyle,
|
||||
x: width / 2,
|
||||
y: 0,
|
||||
r: markSize / 2 || markR || 5,
|
||||
},
|
||||
className: 'link-point-right',
|
||||
name: 'link-point-right',
|
||||
isAnchorPoint: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (top) {
|
||||
// top circle
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...markStyle,
|
||||
x: 0,
|
||||
y: -height / 2,
|
||||
r: markSize / 2 || markR || 5,
|
||||
},
|
||||
className: 'link-point-top',
|
||||
name: 'link-point-top',
|
||||
isAnchorPoint: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (bottom) {
|
||||
// bottom circle
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...markStyle,
|
||||
x: 0,
|
||||
y: height / 2,
|
||||
r: markSize / 2 || markR || 5,
|
||||
},
|
||||
className: 'link-point-bottom',
|
||||
name: 'link-point-bottom',
|
||||
isAnchorPoint: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 获取节点的样式,供基于该节点自定义时使用
|
||||
* @param {Object} cfg 节点数据模型
|
||||
* @return {Object} 节点的样式
|
||||
*/
|
||||
getShapeStyle(cfg: NodeConfig) {
|
||||
const { style: defaultStyle } = this.getOptions(cfg) as NodeConfig;
|
||||
const strokeStyle: ShapeStyle = {
|
||||
stroke: cfg.color,
|
||||
};
|
||||
// 如果设置了color,则覆盖默认的stroke属性
|
||||
const style = mix({}, defaultStyle, strokeStyle);
|
||||
const size = (this as ShapeOptions).getSize!(cfg);
|
||||
const width = style.width || size[0];
|
||||
const height = style.height || size[1];
|
||||
const styles = {
|
||||
x: -width / 2,
|
||||
y: -height / 2,
|
||||
width,
|
||||
height,
|
||||
...style,
|
||||
};
|
||||
return styles;
|
||||
},
|
||||
update(cfg: NodeConfig, item: Item) {
|
||||
const group = item.getContainer();
|
||||
// 这里不传 cfg 参数是因为 cfg.style 需要最后覆盖样式
|
||||
const { style: defaultStyle } = this.getOptions({}) as NodeConfig;
|
||||
const size = (this as ShapeOptions).getSize!(cfg);
|
||||
const keyShape = item.get('keyShape');
|
||||
if (!cfg.size) {
|
||||
size[0] = keyShape.attr('width') || defaultStyle.width;
|
||||
size[1] = keyShape.attr('height') || defaultStyle.height;
|
||||
}
|
||||
// 下面这些属性需要覆盖默认样式与目前样式,但若在 cfg 中有指定则应该被 cfg 的相应配置覆盖。
|
||||
const strokeStyle = {
|
||||
stroke: cfg.color,
|
||||
x: -size[0] / 2,
|
||||
y: -size[1] / 2,
|
||||
width: size[0],
|
||||
height: size[1],
|
||||
};
|
||||
// 与 getShapeStyle 不同在于,update 时需要获取到当前的 style 进行融合。即新传入的配置项中没有涉及的属性,保留当前的配置。
|
||||
let style = mix({}, defaultStyle, keyShape.attr(), strokeStyle);
|
||||
style = mix(style, cfg.style);
|
||||
|
||||
(this as any).updateShape(cfg, item, style, false);
|
||||
(this as any).updateLinkPoints(cfg, group);
|
||||
},
|
||||
},
|
||||
'single-node',
|
||||
);
|
429
packages/core/src/shape/nodes/star.ts
Normal file
429
packages/core/src/shape/nodes/star.ts
Normal file
@ -0,0 +1,429 @@
|
||||
import { Group as GGroup } from '@antv/g-canvas';
|
||||
import { IShape } from '@antv/g-canvas/lib/interfaces';
|
||||
import { mix } from '@antv/util';
|
||||
import { Item, NodeConfig, ShapeStyle } from '../../types';
|
||||
import Global from '../../global';
|
||||
import Shape from '../shape';
|
||||
import { ShapeOptions } from '../../interface/shape';
|
||||
|
||||
// 五角星shape
|
||||
Shape.registerNode(
|
||||
'star',
|
||||
{
|
||||
// 自定义节点时的配置
|
||||
options: {
|
||||
size: 60,
|
||||
style: {
|
||||
stroke: Global.defaultNode.style.stroke,
|
||||
fill: Global.defaultNode.style.fill,
|
||||
lineWidth: Global.defaultNode.style.lineWidth
|
||||
},
|
||||
labelCfg: {
|
||||
style: {
|
||||
fill: Global.nodeLabel.style.fill,
|
||||
fontSize: Global.nodeLabel.style.fontSize,
|
||||
},
|
||||
},
|
||||
// 节点上左右上下四个方向上的链接circle配置
|
||||
linkPoints: {
|
||||
top: false,
|
||||
right: false,
|
||||
bottom: false,
|
||||
left: false,
|
||||
// circle的大小
|
||||
size: Global.defaultNode.linkPoints.size,
|
||||
lineWidth: Global.defaultNode.linkPoints.lineWidth,
|
||||
fill: Global.defaultNode.linkPoints.fill,
|
||||
stroke: Global.defaultNode.linkPoints.stroke,
|
||||
},
|
||||
// 节点中icon配置
|
||||
icon: {
|
||||
// 是否显示icon,值为 false 则不渲染icon
|
||||
show: false,
|
||||
// icon的地址,字符串类型
|
||||
img:
|
||||
'https://gw.alipayobjects.com/zos/bmw-prod/5d015065-8505-4e7a-baec-976f81e3c41d.svg',
|
||||
width: 20,
|
||||
height: 20,
|
||||
},
|
||||
stateStyles: {
|
||||
...Global.nodeStateStyles
|
||||
}
|
||||
},
|
||||
shapeType: 'star',
|
||||
// 文本位置
|
||||
labelPosition: 'center',
|
||||
drawShape(cfg: NodeConfig, group: GGroup): IShape {
|
||||
const { icon = {} } = this.getOptions(cfg) as NodeConfig;
|
||||
const style = this.getShapeStyle!(cfg);
|
||||
|
||||
const keyShape = group.addShape('path', {
|
||||
attrs: style,
|
||||
className: `${this.type}-keyShape`,
|
||||
name: `${this.type}-keyShape`,
|
||||
draggable: true,
|
||||
});
|
||||
|
||||
const { width: w, height: h, show } = icon;
|
||||
if (show) {
|
||||
const image = group.addShape('image', {
|
||||
attrs: {
|
||||
x: -w! / 2,
|
||||
y: -h! / 2,
|
||||
...icon,
|
||||
},
|
||||
className: `${this.type}-icon`,
|
||||
name: `${this.type}-icon`,
|
||||
draggable: true,
|
||||
});
|
||||
}
|
||||
|
||||
(this as any).drawLinkPoints(cfg, group);
|
||||
|
||||
return keyShape;
|
||||
},
|
||||
/**
|
||||
* 绘制节点上的LinkPoints
|
||||
* @param {Object} cfg data数据配置项
|
||||
* @param {Group} group Group实例
|
||||
*/
|
||||
drawLinkPoints(cfg: NodeConfig, group: GGroup) {
|
||||
const { linkPoints = {} } = this.getOptions(cfg) as NodeConfig;
|
||||
|
||||
const {
|
||||
top,
|
||||
left,
|
||||
right,
|
||||
leftBottom,
|
||||
rightBottom,
|
||||
size: markSize,
|
||||
r: markR,
|
||||
...markStyle
|
||||
} = linkPoints;
|
||||
const size = (this as ShapeOptions).getSize!(cfg);
|
||||
const outerR = size[0];
|
||||
|
||||
if (right) {
|
||||
// right circle
|
||||
// up down left right 四个方向的坐标均不相同
|
||||
const x1 = Math.cos(((18 + 72 * 0) / 180) * Math.PI) * outerR;
|
||||
const y1 = Math.sin(((18 + 72 * 0) / 180) * Math.PI) * outerR;
|
||||
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...markStyle,
|
||||
x: x1,
|
||||
y: -y1,
|
||||
r: markSize / 2 || markR || 5,
|
||||
},
|
||||
className: 'link-point-right',
|
||||
name: 'link-point-right',
|
||||
});
|
||||
}
|
||||
|
||||
if (top) {
|
||||
// up down left right 四个方向的坐标均不相同
|
||||
const x1 = Math.cos(((18 + 72 * 1) / 180) * Math.PI) * outerR;
|
||||
const y1 = Math.sin(((18 + 72 * 1) / 180) * Math.PI) * outerR;
|
||||
|
||||
// top circle
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...markStyle,
|
||||
x: x1,
|
||||
y: -y1,
|
||||
r: markSize / 2 || markR || 5,
|
||||
},
|
||||
className: 'link-point-top',
|
||||
name: 'link-point-top',
|
||||
});
|
||||
}
|
||||
|
||||
if (left) {
|
||||
// up down left right 四个方向的坐标均不相同
|
||||
const x1 = Math.cos(((18 + 72 * 2) / 180) * Math.PI) * outerR;
|
||||
const y1 = Math.sin(((18 + 72 * 2) / 180) * Math.PI) * outerR;
|
||||
|
||||
// left circle
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...markStyle,
|
||||
x: x1,
|
||||
y: -y1,
|
||||
r: markSize / 2 || markR || 5,
|
||||
},
|
||||
className: 'link-point-left',
|
||||
name: 'link-point-left',
|
||||
});
|
||||
}
|
||||
|
||||
if (leftBottom) {
|
||||
// up down left right 四个方向的坐标均不相同
|
||||
const x1 = Math.cos(((18 + 72 * 3) / 180) * Math.PI) * outerR;
|
||||
const y1 = Math.sin(((18 + 72 * 3) / 180) * Math.PI) * outerR;
|
||||
|
||||
// left bottom circle
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...markStyle,
|
||||
x: x1,
|
||||
y: -y1,
|
||||
r: markSize / 2 || markR || 5,
|
||||
},
|
||||
className: 'link-point-left-bottom',
|
||||
name: 'link-point-left-bottom',
|
||||
});
|
||||
}
|
||||
|
||||
if (rightBottom) {
|
||||
// up down left right 四个方向的坐标均不相同
|
||||
const x1 = Math.cos(((18 + 72 * 4) / 180) * Math.PI) * outerR;
|
||||
const y1 = Math.sin(((18 + 72 * 4) / 180) * Math.PI) * outerR;
|
||||
|
||||
// left bottom circle
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...markStyle,
|
||||
x: x1,
|
||||
y: -y1,
|
||||
r: markSize / 2 || markR || 5,
|
||||
},
|
||||
className: 'link-point-right-bottom',
|
||||
name: 'link-point-right-bottom',
|
||||
});
|
||||
}
|
||||
},
|
||||
getPath(cfg: NodeConfig) {
|
||||
const size = (this as ShapeOptions).getSize!(cfg);
|
||||
const outerR = size[0];
|
||||
const defaultInnerR = (outerR * 3) / 8;
|
||||
const innerR = cfg.innerR || defaultInnerR;
|
||||
const path = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const x1 = Math.cos(((18 + 72 * i) / 180) * Math.PI) * outerR;
|
||||
const y1 = Math.sin(((18 + 72 * i) / 180) * Math.PI) * outerR;
|
||||
const x2 = Math.cos(((54 + 72 * i) / 180) * Math.PI) * innerR;
|
||||
const y2 = Math.sin(((54 + 72 * i) / 180) * Math.PI) * innerR;
|
||||
|
||||
if (i === 0) {
|
||||
path.push(['M', x1, -y1]);
|
||||
} else {
|
||||
path.push(['L', x1, -y1]);
|
||||
}
|
||||
path.push(['L', x2, -y2]);
|
||||
}
|
||||
|
||||
path.push(['Z']);
|
||||
|
||||
return path;
|
||||
},
|
||||
/**
|
||||
* 获取节点的样式,供基于该节点自定义时使用
|
||||
* @param {Object} cfg 节点数据模型
|
||||
* @return {Object} 节点的样式
|
||||
*/
|
||||
getShapeStyle(cfg: NodeConfig): ShapeStyle {
|
||||
const { style: defaultStyle } = this.getOptions(cfg) as NodeConfig;
|
||||
const strokeStyle: ShapeStyle = {
|
||||
stroke: cfg.color,
|
||||
};
|
||||
// 如果设置了color,则覆盖原来默认的 stroke 属性。但 cfg 中但 stroke 属性优先级更高
|
||||
const style = mix({}, defaultStyle, strokeStyle);
|
||||
const path = (this as any).getPath(cfg);
|
||||
const styles = { path, ...style };
|
||||
return styles;
|
||||
},
|
||||
update(cfg: NodeConfig, item: Item) {
|
||||
const group = item.getContainer();
|
||||
// 这里不传 cfg 参数是因为 cfg.style 需要最后覆盖样式
|
||||
const { style: defaultStyle } = this.getOptions({}) as NodeConfig;
|
||||
const path = (this as any).getPath(cfg);
|
||||
// 下面这些属性需要覆盖默认样式与目前样式,但若在 cfg 中有指定则应该被 cfg 的相应配置覆盖。
|
||||
const strokeStyle = {
|
||||
stroke: cfg.color,
|
||||
path,
|
||||
};
|
||||
// 与 getShapeStyle 不同在于,update 时需要获取到当前的 style 进行融合。即新传入的配置项中没有涉及的属性,保留当前的配置。
|
||||
const keyShape = item.get('keyShape');
|
||||
let style = mix({}, defaultStyle, keyShape.attr(), strokeStyle);
|
||||
style = mix(style, cfg.style);
|
||||
|
||||
(this as any).updateShape(cfg, item, style, true);
|
||||
(this as any).updateLinkPoints(cfg, group);
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新linkPoints
|
||||
* @param {Object} cfg 节点数据配置项
|
||||
* @param {Group} group Item所在的group
|
||||
*/
|
||||
updateLinkPoints(cfg: NodeConfig, group: GGroup) {
|
||||
const { linkPoints: defaultLinkPoints } = this.getOptions({}) as NodeConfig;
|
||||
const markLeft = group.find((element) => element.get('className') === 'link-point-left');
|
||||
const markRight = group.find((element) => element.get('className') === 'link-point-right');
|
||||
const markTop = group.find((element) => element.get('className') === 'link-point-top');
|
||||
const markLeftBottom = group.find(
|
||||
(element) => element.get('className') === 'link-point-left-bottom',
|
||||
);
|
||||
const markRightBottom = group.find(
|
||||
(element) => element.get('className') === 'link-point-right-bottom',
|
||||
);
|
||||
|
||||
let currentLinkPoints = defaultLinkPoints;
|
||||
const existLinkPoint = markLeft || markRight || markTop || markLeftBottom || markRightBottom;
|
||||
if (existLinkPoint) {
|
||||
currentLinkPoints = existLinkPoint.attr();
|
||||
}
|
||||
|
||||
const linkPoints = mix({}, currentLinkPoints, cfg.linkPoints);
|
||||
|
||||
const { fill: markFill, stroke: markStroke, lineWidth: borderWidth } = linkPoints;
|
||||
let markSize = linkPoints.size / 2;
|
||||
if (!markSize) markSize = linkPoints.r;
|
||||
const { left, right, top, leftBottom, rightBottom } = cfg.linkPoints
|
||||
? cfg.linkPoints
|
||||
: {
|
||||
left: undefined,
|
||||
right: undefined,
|
||||
top: undefined,
|
||||
leftBottom: undefined,
|
||||
rightBottom: undefined,
|
||||
};
|
||||
|
||||
const size = (this as ShapeOptions).getSize!(cfg);
|
||||
const outerR = size[0];
|
||||
const styles = {
|
||||
r: markSize,
|
||||
fill: markFill,
|
||||
stroke: markStroke,
|
||||
lineWidth: borderWidth,
|
||||
};
|
||||
|
||||
let x = Math.cos(((18 + 72 * 0) / 180) * Math.PI) * outerR;
|
||||
let y = Math.sin(((18 + 72 * 0) / 180) * Math.PI) * outerR;
|
||||
if (markRight) {
|
||||
if (!right && right !== undefined) {
|
||||
markRight.remove();
|
||||
} else {
|
||||
markRight.attr({
|
||||
...styles,
|
||||
x,
|
||||
y: -y,
|
||||
});
|
||||
}
|
||||
} else if (right) {
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...styles,
|
||||
x,
|
||||
y: -y,
|
||||
},
|
||||
className: 'link-point-right',
|
||||
name: 'link-point-right',
|
||||
isAnchorPoint: true,
|
||||
});
|
||||
}
|
||||
|
||||
x = Math.cos(((18 + 72 * 1) / 180) * Math.PI) * outerR;
|
||||
y = Math.sin(((18 + 72 * 1) / 180) * Math.PI) * outerR;
|
||||
if (markTop) {
|
||||
if (!top && top !== undefined) {
|
||||
markTop.remove();
|
||||
} else {
|
||||
markTop.attr({
|
||||
...styles,
|
||||
x,
|
||||
y: -y,
|
||||
});
|
||||
}
|
||||
} else if (top) {
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...styles,
|
||||
x,
|
||||
y: -y,
|
||||
},
|
||||
className: 'link-point-top',
|
||||
name: 'link-point-top',
|
||||
isAnchorPoint: true,
|
||||
});
|
||||
}
|
||||
|
||||
x = Math.cos(((18 + 72 * 2) / 180) * Math.PI) * outerR;
|
||||
y = Math.sin(((18 + 72 * 2) / 180) * Math.PI) * outerR;
|
||||
if (markLeft) {
|
||||
if (!left && left !== undefined) {
|
||||
markLeft.remove();
|
||||
} else {
|
||||
markLeft.attr({
|
||||
...styles,
|
||||
x,
|
||||
y: -y,
|
||||
});
|
||||
}
|
||||
} else if (left) {
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...styles,
|
||||
x,
|
||||
y: -y,
|
||||
},
|
||||
className: 'link-point-left',
|
||||
name: 'link-point-left',
|
||||
isAnchorPoint: true,
|
||||
});
|
||||
}
|
||||
|
||||
x = Math.cos(((18 + 72 * 3) / 180) * Math.PI) * outerR;
|
||||
y = Math.sin(((18 + 72 * 3) / 180) * Math.PI) * outerR;
|
||||
if (markLeftBottom) {
|
||||
if (!leftBottom && leftBottom !== undefined) {
|
||||
markLeftBottom.remove();
|
||||
} else {
|
||||
markLeftBottom.attr({
|
||||
...styles,
|
||||
x,
|
||||
y: -y,
|
||||
});
|
||||
}
|
||||
} else if (leftBottom) {
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...styles,
|
||||
x,
|
||||
y: -y,
|
||||
},
|
||||
className: 'link-point-left-bottom',
|
||||
name: 'link-point-left-bottom',
|
||||
isAnchorPoint: true,
|
||||
});
|
||||
}
|
||||
|
||||
x = Math.cos(((18 + 72 * 4) / 180) * Math.PI) * outerR;
|
||||
y = Math.sin(((18 + 72 * 4) / 180) * Math.PI) * outerR;
|
||||
if (markRightBottom) {
|
||||
if (!rightBottom && rightBottom !== undefined) {
|
||||
markLeftBottom.remove();
|
||||
} else {
|
||||
markRightBottom.attr({
|
||||
...styles,
|
||||
x,
|
||||
y: -y,
|
||||
});
|
||||
}
|
||||
} else if (rightBottom) {
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...styles,
|
||||
x,
|
||||
y: -y,
|
||||
},
|
||||
className: 'link-point-right-bottom',
|
||||
name: 'link-point-right-bottom',
|
||||
isAnchorPoint: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
'single-node',
|
||||
);
|
476
packages/core/src/shape/nodes/triangle.ts
Normal file
476
packages/core/src/shape/nodes/triangle.ts
Normal file
@ -0,0 +1,476 @@
|
||||
import Shape from '../shape';
|
||||
import { mix } from '@antv/util';
|
||||
import Global from '../../global';
|
||||
import { Group as GGroup } from '@antv/g-canvas';
|
||||
import { IShape } from '@antv/g-canvas/lib/interfaces';
|
||||
import { ModelConfig, NodeConfig, Item, ShapeStyle } from '../../types';
|
||||
import { ShapeOptions } from '../../interface/shape';
|
||||
|
||||
// 菱形shape
|
||||
Shape.registerNode(
|
||||
'triangle',
|
||||
{
|
||||
// 自定义节点时的配置
|
||||
options: {
|
||||
size: 40,
|
||||
direction: 'up',
|
||||
style: {
|
||||
stroke: Global.defaultNode.style.stroke,
|
||||
fill: Global.defaultNode.style.fill,
|
||||
lineWidth: Global.defaultNode.style.lineWidth
|
||||
},
|
||||
labelCfg: {
|
||||
style: {
|
||||
fill: Global.nodeLabel.style.fill,
|
||||
fontSize: Global.nodeLabel.style.fontSize,
|
||||
},
|
||||
offset: 15
|
||||
},
|
||||
// 节点上左右上下四个方向上的链接circle配置
|
||||
linkPoints: {
|
||||
top: false,
|
||||
right: false,
|
||||
bottom: false,
|
||||
left: false,
|
||||
// circle的大小
|
||||
size: Global.defaultNode.linkPoints.size,
|
||||
lineWidth: Global.defaultNode.linkPoints.lineWidth,
|
||||
fill: Global.defaultNode.linkPoints.fill,
|
||||
stroke: Global.defaultNode.linkPoints.stroke,
|
||||
},
|
||||
// 节点中icon配置
|
||||
icon: {
|
||||
// 是否显示icon,值为 false 则不渲染icon
|
||||
show: false,
|
||||
// icon的地址,字符串类型
|
||||
img:
|
||||
'https://gw.alipayobjects.com/zos/bmw-prod/5d015065-8505-4e7a-baec-976f81e3c41d.svg',
|
||||
width: 20,
|
||||
height: 20,
|
||||
offset: 6
|
||||
},
|
||||
stateStyles: {
|
||||
...Global.nodeStateStyles
|
||||
}
|
||||
},
|
||||
shapeType: 'triangle',
|
||||
// 文本位置
|
||||
labelPosition: 'bottom',
|
||||
drawShape(cfg: NodeConfig, group: GGroup): IShape {
|
||||
const { icon = {}, direction: defaultDirection } = this.getOptions(cfg) as NodeConfig;
|
||||
const style = this.getShapeStyle!(cfg);
|
||||
const direction = cfg.direction || defaultDirection;
|
||||
|
||||
const keyShape = group.addShape('path', {
|
||||
attrs: style,
|
||||
className: `${this.type}-keyShape`,
|
||||
name: `${this.type}-keyShape`,
|
||||
draggable: true,
|
||||
});
|
||||
|
||||
const { width: w, height: h, show, offset } = icon;
|
||||
if (show) {
|
||||
let iconW = -w! / 2;
|
||||
let iconH = -h! / 2;
|
||||
if (direction === 'up' || direction === 'down') {
|
||||
iconH += offset;
|
||||
}
|
||||
if (direction === 'left' || direction === 'right') {
|
||||
iconW += offset;
|
||||
}
|
||||
group.addShape('image', {
|
||||
attrs: {
|
||||
x: iconW,
|
||||
y: iconH,
|
||||
...icon,
|
||||
},
|
||||
className: `${this.type}-icon`,
|
||||
name: `${this.type}-icon`,
|
||||
draggable: true,
|
||||
});
|
||||
}
|
||||
|
||||
(this as any).drawLinkPoints(cfg, group);
|
||||
|
||||
return keyShape;
|
||||
},
|
||||
/**
|
||||
* 绘制节点上的LinkPoints
|
||||
* @param {Object} cfg data数据配置项
|
||||
* @param {Group} group Group实例
|
||||
*/
|
||||
drawLinkPoints(cfg: NodeConfig, group: GGroup) {
|
||||
const { linkPoints = {}, direction: defaultDirection } = this.getOptions(cfg) as NodeConfig;
|
||||
|
||||
const direction = cfg.direction || defaultDirection;
|
||||
|
||||
const { top, left, right, bottom, size: markSize, r: markR, ...markStyle } = linkPoints;
|
||||
|
||||
const size = (this as ShapeOptions).getSize!(cfg);
|
||||
const len = size[0];
|
||||
|
||||
if (left) {
|
||||
// up down left right 四个方向的坐标均不相同
|
||||
let leftPos = null;
|
||||
const diffY = len * Math.sin((1 / 3) * Math.PI);
|
||||
const r = len * Math.sin((1 / 3) * Math.PI);
|
||||
if (direction === 'up') {
|
||||
leftPos = [-r, diffY];
|
||||
} else if (direction === 'down') {
|
||||
leftPos = [-r, -diffY];
|
||||
} else if (direction === 'left') {
|
||||
leftPos = [-r, r - diffY];
|
||||
}
|
||||
|
||||
if (leftPos) {
|
||||
// left circle
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...markStyle,
|
||||
x: leftPos[0],
|
||||
y: leftPos[1],
|
||||
r: markSize / 2 || markR || 5,
|
||||
},
|
||||
className: 'link-point-left',
|
||||
name: 'link-point-left',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (right) {
|
||||
// right circle
|
||||
// up down left right 四个方向的坐标均不相同
|
||||
let rightPos = null;
|
||||
const diffY = len * Math.sin((1 / 3) * Math.PI);
|
||||
const r = len * Math.sin((1 / 3) * Math.PI);
|
||||
if (direction === 'up') {
|
||||
rightPos = [r, diffY];
|
||||
} else if (direction === 'down') {
|
||||
rightPos = [r, -diffY];
|
||||
} else if (direction === 'right') {
|
||||
rightPos = [r, r - diffY];
|
||||
}
|
||||
|
||||
if (rightPos) {
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...markStyle,
|
||||
x: rightPos[0],
|
||||
y: rightPos[1],
|
||||
r: markSize / 2 || markR || 5,
|
||||
},
|
||||
className: 'link-point-right',
|
||||
name: 'link-point-right',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (top) {
|
||||
// up down left right 四个方向的坐标均不相同
|
||||
let topPos = null;
|
||||
const diffY = len * Math.sin((1 / 3) * Math.PI);
|
||||
const r = len * Math.sin((1 / 3) * Math.PI);
|
||||
if (direction === 'up') {
|
||||
topPos = [r - diffY, -diffY];
|
||||
} else if (direction === 'left') {
|
||||
topPos = [r, -diffY];
|
||||
} else if (direction === 'right') {
|
||||
topPos = [-r, -diffY];
|
||||
}
|
||||
|
||||
if (topPos) {
|
||||
// top circle
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...markStyle,
|
||||
x: topPos[0],
|
||||
y: topPos[1],
|
||||
r: markSize / 2 || markR || 5,
|
||||
},
|
||||
className: 'link-point-top',
|
||||
name: 'link-point-top',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (bottom) {
|
||||
// up down left right 四个方向的坐标均不相同
|
||||
let bottomPos = null;
|
||||
const diffY = len * Math.sin((1 / 3) * Math.PI);
|
||||
const r = len * Math.sin((1 / 3) * Math.PI);
|
||||
if (direction === 'down') {
|
||||
bottomPos = [-r + diffY, diffY];
|
||||
} else if (direction === 'left') {
|
||||
bottomPos = [r, diffY];
|
||||
} else if (direction === 'right') {
|
||||
bottomPos = [-r, diffY];
|
||||
}
|
||||
|
||||
if (bottomPos) {
|
||||
// bottom circle
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...markStyle,
|
||||
x: bottomPos[0],
|
||||
y: bottomPos[1],
|
||||
r: markSize / 2 || markR || 5,
|
||||
},
|
||||
className: 'link-point-bottom',
|
||||
name: 'link-point-bottom',
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
getPath(cfg: ModelConfig) {
|
||||
const { direction: defaultDirection } = this.getOptions(cfg) as NodeConfig;
|
||||
|
||||
const direction = cfg.direction || defaultDirection;
|
||||
const size = (this as ShapeOptions).getSize!(cfg);
|
||||
const len = size[0];
|
||||
|
||||
const diffY = len * Math.sin((1 / 3) * Math.PI);
|
||||
const r = len * Math.sin((1 / 3) * Math.PI);
|
||||
let path = [
|
||||
['M', -r, diffY],
|
||||
['L', 0, -diffY],
|
||||
['L', r, diffY],
|
||||
['Z'], // 封闭
|
||||
];
|
||||
|
||||
if (direction === 'down') {
|
||||
path = [
|
||||
['M', -r, -diffY],
|
||||
['L', r, -diffY],
|
||||
['L', 0, diffY],
|
||||
['Z'], // 封闭
|
||||
];
|
||||
} else if (direction === 'left') {
|
||||
path = [
|
||||
['M', -r, r - diffY],
|
||||
['L', r, -r],
|
||||
['L', r, r],
|
||||
['Z'], // 封闭
|
||||
];
|
||||
} else if (direction === 'right') {
|
||||
path = [
|
||||
['M', r, r - diffY],
|
||||
['L', -r, r],
|
||||
['L', -r, -r],
|
||||
['Z'], // 封闭
|
||||
];
|
||||
}
|
||||
return path;
|
||||
},
|
||||
/**
|
||||
* 获取节点的样式,供基于该节点自定义时使用
|
||||
* @param {Object} cfg 节点数据模型
|
||||
* @return {Object} 节点的样式
|
||||
*/
|
||||
getShapeStyle(cfg: NodeConfig) {
|
||||
const { style: defaultStyle } = this.getOptions(cfg) as NodeConfig;
|
||||
const strokeStyle: ShapeStyle = {
|
||||
stroke: cfg.color,
|
||||
};
|
||||
// 如果设置了color,则覆盖默认的stroke属性
|
||||
const style = mix({}, defaultStyle, strokeStyle);
|
||||
const path = (this as any).getPath(cfg);
|
||||
const styles = { path, ...style };
|
||||
return styles;
|
||||
},
|
||||
update(cfg: NodeConfig, item: Item) {
|
||||
const group = item.getContainer();
|
||||
// 这里不传 cfg 参数是因为 cfg.style 需要最后覆盖样式
|
||||
const { style: defaultStyle } = this.getOptions({}) as NodeConfig;
|
||||
const path = (this as any).getPath(cfg);
|
||||
// 下面这些属性需要覆盖默认样式与目前样式,但若在 cfg 中有指定则应该被 cfg 的相应配置覆盖。
|
||||
const strokeStyle = {
|
||||
stroke: cfg.color,
|
||||
path,
|
||||
};
|
||||
// 与 getShapeStyle 不同在于,update 时需要获取到当前的 style 进行融合。即新传入的配置项中没有涉及的属性,保留当前的配置。
|
||||
const keyShape = item.get('keyShape');
|
||||
let style = mix({}, defaultStyle, keyShape.attr(), strokeStyle);
|
||||
style = mix(style, cfg.style);
|
||||
|
||||
(this as any).updateShape(cfg, item, style, true);
|
||||
(this as any).updateLinkPoints(cfg, group);
|
||||
},
|
||||
/**
|
||||
* 更新linkPoints
|
||||
* @param {Object} cfg 节点数据配置项
|
||||
* @param {Group} group Item所在的group
|
||||
*/
|
||||
updateLinkPoints(cfg: NodeConfig, group: GGroup) {
|
||||
const { linkPoints: defaultLinkPoints, direction: defaultDirection } = this.getOptions(
|
||||
{},
|
||||
) as NodeConfig;
|
||||
|
||||
const direction = cfg.direction || defaultDirection;
|
||||
|
||||
const markLeft = group.find((element) => element.get('className') === 'link-point-left');
|
||||
const markRight = group.find((element) => element.get('className') === 'link-point-right');
|
||||
const markTop = group.find((element) => element.get('className') === 'link-point-top');
|
||||
const markBottom = group.find((element) => element.get('className') === 'link-point-bottom');
|
||||
|
||||
let currentLinkPoints = defaultLinkPoints;
|
||||
const existLinkPoint = markLeft || markRight || markTop || markBottom;
|
||||
if (existLinkPoint) {
|
||||
currentLinkPoints = existLinkPoint.attr();
|
||||
}
|
||||
|
||||
const linkPoints = mix({}, currentLinkPoints, cfg.linkPoints);
|
||||
|
||||
const { fill: markFill, stroke: markStroke, lineWidth: borderWidth } = linkPoints;
|
||||
let markSize = linkPoints.size / 2;
|
||||
if (!markSize) markSize = linkPoints.r;
|
||||
const { left, right, top, bottom } = cfg.linkPoints
|
||||
? cfg.linkPoints
|
||||
: { left: undefined, right: undefined, top: undefined, bottom: undefined };
|
||||
|
||||
const size = (this as ShapeOptions).getSize!(cfg);
|
||||
const len = size[0];
|
||||
const styles = {
|
||||
r: markSize,
|
||||
fill: markFill,
|
||||
stroke: markStroke,
|
||||
lineWidth: borderWidth,
|
||||
};
|
||||
|
||||
let leftPos = null;
|
||||
const diffY = len * Math.sin((1 / 3) * Math.PI);
|
||||
const r = len * Math.sin((1 / 3) * Math.PI);
|
||||
if (direction === 'up') {
|
||||
leftPos = [-r, diffY];
|
||||
} else if (direction === 'down') {
|
||||
leftPos = [-r, -diffY];
|
||||
} else if (direction === 'left') {
|
||||
leftPos = [-r, r - diffY];
|
||||
}
|
||||
if (leftPos) {
|
||||
if (markLeft) {
|
||||
if (!left && left !== undefined) {
|
||||
markLeft.remove();
|
||||
} else {
|
||||
markLeft.attr({
|
||||
...styles,
|
||||
x: leftPos[0],
|
||||
y: leftPos[1],
|
||||
});
|
||||
}
|
||||
} else if (left) {
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...styles,
|
||||
x: leftPos[0],
|
||||
y: leftPos[1],
|
||||
},
|
||||
className: 'link-point-left',
|
||||
name: 'link-point-left',
|
||||
isAnchorPoint: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let rightPos = null;
|
||||
if (direction === 'up') {
|
||||
rightPos = [r, diffY];
|
||||
} else if (direction === 'down') {
|
||||
rightPos = [r, -diffY];
|
||||
} else if (direction === 'right') {
|
||||
rightPos = [r, r - diffY];
|
||||
}
|
||||
if (rightPos) {
|
||||
if (markRight) {
|
||||
if (!right && right !== undefined) {
|
||||
markRight.remove();
|
||||
} else {
|
||||
markRight.attr({
|
||||
...styles,
|
||||
x: rightPos[0],
|
||||
y: rightPos[1],
|
||||
});
|
||||
}
|
||||
} else if (right) {
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...styles,
|
||||
x: rightPos[0],
|
||||
y: rightPos[1],
|
||||
},
|
||||
className: 'link-point-right',
|
||||
name: 'link-point-right',
|
||||
isAnchorPoint: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let topPos = null;
|
||||
if (direction === 'up') {
|
||||
topPos = [r - diffY, -diffY];
|
||||
} else if (direction === 'left') {
|
||||
topPos = [r, -diffY];
|
||||
} else if (direction === 'right') {
|
||||
topPos = [-r, -diffY];
|
||||
}
|
||||
if (topPos) {
|
||||
if (markTop) {
|
||||
if (!top && top !== undefined) {
|
||||
markTop.remove();
|
||||
} else {
|
||||
// top circle
|
||||
markTop.attr({
|
||||
...styles,
|
||||
x: topPos[0],
|
||||
y: topPos[1],
|
||||
});
|
||||
}
|
||||
} else if (top) {
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...styles,
|
||||
x: topPos[0],
|
||||
y: topPos[1],
|
||||
},
|
||||
className: 'link-point-top',
|
||||
name: 'link-point-top',
|
||||
isAnchorPoint: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let bottomPos = null;
|
||||
if (direction === 'down') {
|
||||
bottomPos = [-r + diffY, diffY];
|
||||
} else if (direction === 'left') {
|
||||
bottomPos = [r, diffY];
|
||||
} else if (direction === 'right') {
|
||||
bottomPos = [-r, diffY];
|
||||
}
|
||||
if (bottomPos) {
|
||||
if (markBottom) {
|
||||
if (!bottom && bottom !== undefined) {
|
||||
markBottom.remove();
|
||||
} else {
|
||||
markBottom.attr({
|
||||
...styles,
|
||||
x: bottomPos[0],
|
||||
y: bottomPos[1],
|
||||
});
|
||||
}
|
||||
} else if (bottom) {
|
||||
group.addShape('circle', {
|
||||
attrs: {
|
||||
...styles,
|
||||
x: bottomPos[0],
|
||||
y: bottomPos[1],
|
||||
},
|
||||
className: 'link-point-bottom',
|
||||
name: 'link-point-bottom',
|
||||
isAnchorPoint: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
'single-node',
|
||||
);
|
265
packages/core/src/shape/shape.ts
Normal file
265
packages/core/src/shape/shape.ts
Normal file
@ -0,0 +1,265 @@
|
||||
/**
|
||||
* @fileOverview 自定义 Shape 的基类
|
||||
* @author dxq613@gmail.com
|
||||
*/
|
||||
|
||||
import { Group as GGroup } from '@antv/g-canvas';
|
||||
import { IShape } from '@antv/g-canvas/lib/interfaces';
|
||||
import { upperFirst } from '@antv/util';
|
||||
import { ShapeOptions, ShapeDefine } from '../interface/shape';
|
||||
import { IPoint, Item, ModelConfig, NodeConfig, EdgeConfig } from '../types';
|
||||
import { createNodeFromXML } from './xml';
|
||||
|
||||
const cache: {
|
||||
[key: string]: string;
|
||||
} = {}; // ucfirst 开销过大,进行缓存
|
||||
|
||||
// 首字母大写
|
||||
function ucfirst(str: string) {
|
||||
if (!cache[str]) {
|
||||
cache[str] = upperFirst(str);
|
||||
}
|
||||
return cache[str];
|
||||
}
|
||||
|
||||
/**
|
||||
* 工厂方法的基类
|
||||
* @type Shape.FactoryBase
|
||||
*/
|
||||
const ShapeFactoryBase = {
|
||||
/**
|
||||
* 默认的形状,当没有指定/匹配 shapeType 时,使用默认的
|
||||
* @type {String}
|
||||
*/
|
||||
defaultShapeType: 'defaultType',
|
||||
/**
|
||||
* 形状的 className,用于搜索
|
||||
* @type {String}
|
||||
*/
|
||||
className: null,
|
||||
/**
|
||||
* 获取绘制 Shape 的工具类,无状态
|
||||
* @param {String} type 类型
|
||||
* @return {Shape} 工具类
|
||||
*/
|
||||
getShape(type?: string): ShapeOptions {
|
||||
const self = this as any;
|
||||
const shape = self[type!] || self[self.defaultShapeType];
|
||||
return shape;
|
||||
},
|
||||
/**
|
||||
* 绘制图形
|
||||
* @param {String} type 类型
|
||||
* @param {Object} cfg 配置项
|
||||
* @param {G.Group} group 图形的分组
|
||||
* @return {IShape} 图形对象
|
||||
*/
|
||||
draw(type: string, cfg: ModelConfig, group: GGroup): IShape {
|
||||
const shape = this.getShape(type);
|
||||
const rst = shape.draw!(cfg, group);
|
||||
if (shape.afterDraw) {
|
||||
shape.afterDraw(cfg, group, rst);
|
||||
}
|
||||
return rst;
|
||||
},
|
||||
/**
|
||||
* 更新
|
||||
* @param {String} type 类型
|
||||
* @param {Object} cfg 配置项
|
||||
* @param {G6.Item} item 节点、边、分组等
|
||||
*/
|
||||
baseUpdate(type: string, cfg: ModelConfig, item: Item) {
|
||||
const shape = this.getShape(type);
|
||||
if (shape.update) {
|
||||
// 防止没定义 update 函数
|
||||
shape.update(cfg, item);
|
||||
}
|
||||
|
||||
if (shape.afterUpdate) {
|
||||
shape.afterUpdate(cfg, item);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 设置状态
|
||||
* @param {String} type 类型
|
||||
* @param {String} name 状态名
|
||||
* @param {String | Boolean} value 状态值
|
||||
* @param {G6.Item} item 节点、边、分组等
|
||||
*/
|
||||
setState(type: string, name: string, value: string | boolean, item: Item) {
|
||||
const shape = this.getShape(type);
|
||||
|
||||
// 调用 shape/shapeBase.ts 中的 setState 方法
|
||||
shape.setState!(name, value, item);
|
||||
},
|
||||
/**
|
||||
* 是否允许更新,不重新绘制图形
|
||||
* @param {String} type 类型
|
||||
* @return {Boolean} 是否允许使用更新
|
||||
*/
|
||||
shouldUpdate(type: string): boolean {
|
||||
const shape = this.getShape(type);
|
||||
return !!shape.update;
|
||||
},
|
||||
getControlPoints(type: string, cfg: ModelConfig): IPoint[] | undefined {
|
||||
const shape = this.getShape(type);
|
||||
return shape.getControlPoints!(cfg);
|
||||
},
|
||||
/**
|
||||
* 获取控制点
|
||||
* @param {String} type 节点、边类型
|
||||
* @param {Object} cfg 节点、边的配置项
|
||||
* @return {Array|null} 控制点的数组,如果为 null,则没有控制点
|
||||
*/
|
||||
getAnchorPoints(type: string, cfg: ModelConfig): number[][] | undefined {
|
||||
const shape = this.getShape(type);
|
||||
return shape.getAnchorPoints!(cfg);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 元素的框架
|
||||
*/
|
||||
const ShapeFramework = {
|
||||
// 默认样式及配置
|
||||
options: {},
|
||||
/**
|
||||
* 绘制
|
||||
*/
|
||||
draw(cfg, group) {
|
||||
return this.drawShape(cfg, group);
|
||||
},
|
||||
/**
|
||||
* 绘制
|
||||
*/
|
||||
drawShape(/* cfg, group */) { },
|
||||
/**
|
||||
* 绘制完成后的操作,便于用户继承现有的节点、边
|
||||
*/
|
||||
afterDraw(/* cfg, group */) { },
|
||||
// update(cfg, item) // 默认不定义
|
||||
afterUpdate(/* cfg, item */) { },
|
||||
/**
|
||||
* 设置节点、边状态
|
||||
*/
|
||||
setState(/* name, value, item */) { },
|
||||
/**
|
||||
* 获取控制点
|
||||
* @param {Object} cfg 节点、边的配置项
|
||||
* @return {Array|null} 控制点的数组,如果为 null,则没有控制点
|
||||
*/
|
||||
getControlPoints(cfg: NodeConfig | EdgeConfig) {
|
||||
return cfg.controlPoints;
|
||||
},
|
||||
/**
|
||||
* 获取控制点
|
||||
* @param {Object} cfg 节点、边的配置项
|
||||
* @return {Array|null} 控制点的数组,如果为 null,则没有控制点
|
||||
*/
|
||||
getAnchorPoints(cfg: NodeConfig | EdgeConfig) {
|
||||
const { anchorPoints: defaultAnchorPoints } = this.options as any;
|
||||
const anchorPoints = cfg.anchorPoints || defaultAnchorPoints;
|
||||
return anchorPoints;
|
||||
},
|
||||
/* 如果没定义 update 方法,每次都调用 draw 方法
|
||||
update(cfg, item) {
|
||||
|
||||
}
|
||||
*/
|
||||
};
|
||||
|
||||
export default class Shape {
|
||||
public static Node: any;
|
||||
|
||||
public static Edge: any;
|
||||
|
||||
public static Combo: any;
|
||||
|
||||
public static registerFactory(factoryType: string, cfg: object): object {
|
||||
const className = ucfirst(factoryType);
|
||||
const factoryBase = ShapeFactoryBase;
|
||||
const shapeFactory = { ...factoryBase, ...cfg } as any;
|
||||
(Shape as any)[className] = shapeFactory;
|
||||
shapeFactory.className = className;
|
||||
return shapeFactory;
|
||||
}
|
||||
|
||||
public static getFactory(factoryType: string) {
|
||||
const className = ucfirst(factoryType);
|
||||
return (Shape as any)[className];
|
||||
}
|
||||
|
||||
public static registerNode(
|
||||
shapeType: string,
|
||||
nodeDefinition: ShapeOptions | ShapeDefine,
|
||||
extendShapeType?: string,
|
||||
) {
|
||||
const shapeFactory = Shape.Node;
|
||||
let shapeObj;
|
||||
|
||||
if (typeof nodeDefinition === 'string' || typeof nodeDefinition === 'function') {
|
||||
const autoNodeDefinition = createNodeFromXML(nodeDefinition);
|
||||
shapeObj = { ...shapeFactory.getShape('single-node'), ...autoNodeDefinition };
|
||||
} else if (nodeDefinition.jsx) {
|
||||
const { jsx } = nodeDefinition;
|
||||
const autoNodeDefinition = createNodeFromXML(jsx);
|
||||
shapeObj = {
|
||||
...shapeFactory.getShape('single-node'),
|
||||
...autoNodeDefinition,
|
||||
...nodeDefinition,
|
||||
};
|
||||
} else {
|
||||
const extendShape = extendShapeType ? shapeFactory.getShape(extendShapeType) : ShapeFramework;
|
||||
shapeObj = { ...extendShape, ...nodeDefinition };
|
||||
}
|
||||
|
||||
shapeObj.type = shapeType;
|
||||
shapeObj.itemType = 'node';
|
||||
shapeFactory[shapeType] = shapeObj;
|
||||
return shapeObj;
|
||||
}
|
||||
|
||||
public static registerEdge(
|
||||
shapeType: string,
|
||||
edgeDefinition: ShapeOptions,
|
||||
extendShapeType?: string,
|
||||
) {
|
||||
const shapeFactory = Shape.Edge;
|
||||
const extendShape = extendShapeType ? shapeFactory.getShape(extendShapeType) : ShapeFramework;
|
||||
const shapeObj = { ...extendShape, ...edgeDefinition };
|
||||
shapeObj.type = shapeType;
|
||||
shapeObj.itemType = 'edge';
|
||||
shapeFactory[shapeType] = shapeObj;
|
||||
return shapeObj;
|
||||
}
|
||||
|
||||
public static registerCombo(
|
||||
shapeType: string,
|
||||
comboDefinition: ShapeOptions,
|
||||
extendShapeType?: string,
|
||||
) {
|
||||
const shapeFactory = Shape.Combo;
|
||||
const extendShape = extendShapeType ? shapeFactory.getShape(extendShapeType) : ShapeFramework;
|
||||
|
||||
const shapeObj = { ...extendShape, ...comboDefinition };
|
||||
shapeObj.type = shapeType;
|
||||
shapeObj.itemType = 'combo';
|
||||
shapeFactory[shapeType] = shapeObj;
|
||||
return shapeObj;
|
||||
}
|
||||
}
|
||||
|
||||
// 注册 Node 的工厂方法
|
||||
Shape.registerFactory('node', {
|
||||
defaultShapeType: 'circle',
|
||||
});
|
||||
|
||||
// 注册 Edge 的工厂方法
|
||||
Shape.registerFactory('edge', {
|
||||
defaultShapeType: 'line',
|
||||
});
|
||||
|
||||
// 注册 Combo 的工厂方法
|
||||
Shape.registerFactory('combo', {
|
||||
defaultShapeType: 'circle',
|
||||
});
|
518
packages/core/src/shape/shapeBase.ts
Normal file
518
packages/core/src/shape/shapeBase.ts
Normal file
@ -0,0 +1,518 @@
|
||||
/**
|
||||
* @fileOverview 自定义节点和边的过程中,发现大量重复代码
|
||||
* @author dxq613@gmail.com
|
||||
*/
|
||||
import { Group as GGroup } from '@antv/g-canvas';
|
||||
import { IShape, IElement } from '@antv/g-canvas/lib/interfaces';
|
||||
import { ShapeOptions, ILabelConfig } from '../interface/shape';
|
||||
import { IPoint, Item, LabelStyle, ShapeStyle, ModelConfig, EdgeConfig } from '../types';
|
||||
import Global from '../global';
|
||||
import { transform } from '@antv/matrix-util';
|
||||
import { deepMix, each, mix, isBoolean, isPlainObject, clone, isString } from '@antv/util';
|
||||
|
||||
const CLS_SHAPE_SUFFIX = '-shape';
|
||||
const CLS_LABEL_SUFFIX = '-label';
|
||||
const ARROWS = ['startArrow', 'endArrow'];
|
||||
const SHAPE_DEFAULT_ATTRS = {
|
||||
lineWidth: 1,
|
||||
stroke: undefined,
|
||||
fill: undefined,
|
||||
lineAppendWidth: 1,
|
||||
opacity: undefined,
|
||||
strokeOpacity: undefined,
|
||||
fillOpacity: undefined,
|
||||
x: 0,
|
||||
y: 0,
|
||||
r: 10,
|
||||
width: 20,
|
||||
height: 20,
|
||||
shadowColor: undefined,
|
||||
shadowBlur: 0,
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 0
|
||||
}
|
||||
const PATH_SHAPE_DEFAULT_ATTRS = {
|
||||
lineWidth: 1,
|
||||
stroke: '#000',
|
||||
lineDash: undefined,
|
||||
startArrow: false,
|
||||
endArrow: false,
|
||||
opacity: undefined,
|
||||
strokeOpacity: undefined,
|
||||
fillOpacity: undefined,
|
||||
shadowColor: undefined,
|
||||
shadowBlur: 0,
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 0
|
||||
}
|
||||
const SHAPES_DEFAULT_ATTRS = {
|
||||
edge: PATH_SHAPE_DEFAULT_ATTRS,
|
||||
node: SHAPE_DEFAULT_ATTRS,
|
||||
combo: SHAPE_DEFAULT_ATTRS
|
||||
}
|
||||
|
||||
export const CLS_LABEL_BG_SUFFIX = '-label-bg';
|
||||
|
||||
// 单个 shape 带有一个 label,共用这段代码
|
||||
export const shapeBase: ShapeOptions = {
|
||||
// 默认样式及配置
|
||||
options: {},
|
||||
itemType: '', // node, edge, combo 等
|
||||
/**
|
||||
* 形状的类型,例如 circle,ellipse,polyline...
|
||||
*/
|
||||
type: '',
|
||||
getCustomConfig(cfg: ModelConfig): ModelConfig {
|
||||
return {};
|
||||
},
|
||||
getOptions(cfg: ModelConfig): ModelConfig {
|
||||
return deepMix({
|
||||
// 解决局部渲染导致的文字移动残影问题
|
||||
labelCfg: {
|
||||
style: {
|
||||
fontFamily: typeof window !== 'undefined' ? window.getComputedStyle(document.body, null).getPropertyValue("font-family") || 'Arial, sans-serif' : 'Arial, sans-serif',
|
||||
}
|
||||
},
|
||||
descriptionCfg: {
|
||||
style: {
|
||||
fontFamily: typeof window !== 'undefined' ? window.getComputedStyle(document.body, null).getPropertyValue("font-family") || 'Arial, sans-serif' : 'Arial, sans-serif',
|
||||
}
|
||||
}
|
||||
}, this.options, this.getCustomConfig(cfg) || {}, cfg);
|
||||
},
|
||||
/**
|
||||
* 绘制节点/边,包含文本
|
||||
* @override
|
||||
* @param {Object} cfg 节点的配置项
|
||||
* @param {G.Group} group 节点的容器
|
||||
* @return {IShape} 绘制的图形
|
||||
*/
|
||||
draw(cfg: ModelConfig, group: GGroup): IShape {
|
||||
const shape: IShape = this.drawShape!(cfg, group);
|
||||
shape.set('className', this.itemType + CLS_SHAPE_SUFFIX);
|
||||
if (cfg.label) {
|
||||
const label = this.drawLabel!(cfg, group);
|
||||
label.set('className', this.itemType + CLS_LABEL_SUFFIX);
|
||||
}
|
||||
return shape;
|
||||
},
|
||||
/**
|
||||
* 绘制完成后的操作,便于用户继承现有的节点、边
|
||||
* @param cfg
|
||||
* @param group
|
||||
* @param keyShape
|
||||
*/
|
||||
afterDraw(cfg?: ModelConfig, group?: GGroup, keyShape?: IShape) { },
|
||||
drawShape(cfg?: ModelConfig, group?: GGroup): IShape {
|
||||
return null as any;
|
||||
},
|
||||
drawLabel(cfg: ModelConfig, group: GGroup): IShape {
|
||||
const { labelCfg: defaultLabelCfg } = this.getOptions(cfg) as ModelConfig;
|
||||
// image的情况下有可能为null
|
||||
const labelCfg = (defaultLabelCfg || {}) as ILabelConfig;
|
||||
const labelStyle = this.getLabelStyle!(cfg, labelCfg, group);
|
||||
const rotate = labelStyle.rotate;
|
||||
delete labelStyle.rotate;
|
||||
const label = group.addShape('text', {
|
||||
attrs: labelStyle,
|
||||
draggable: true,
|
||||
className: 'text-shape',
|
||||
name: 'text-shape',
|
||||
});
|
||||
if (rotate) {
|
||||
const labelBBox = label.getBBox();
|
||||
let labelMatrix = label.getMatrix();
|
||||
if (!labelMatrix) {
|
||||
labelMatrix = [1, 0, 0, 0, 1, 0, 0, 0, 1];
|
||||
}
|
||||
if (labelStyle.rotateCenter) {
|
||||
switch (labelStyle.rotateCenter) {
|
||||
case 'center':
|
||||
labelMatrix = transform(labelMatrix, [
|
||||
['t', -labelBBox.width / 2, -labelBBox.height / 2],
|
||||
['r', rotate],
|
||||
['t', labelBBox.width / 2, labelBBox.height / 2],
|
||||
]);
|
||||
break;
|
||||
case 'lefttop':
|
||||
labelMatrix = transform(labelMatrix, [
|
||||
['t', -labelStyle.x!, -labelStyle.y!],
|
||||
['r', rotate],
|
||||
['t', labelStyle.x, labelStyle.y],
|
||||
]);
|
||||
break;
|
||||
case 'leftcenter':
|
||||
labelMatrix = transform(labelMatrix, [
|
||||
['t', -labelStyle.x!, -labelStyle.y! - labelBBox.height / 2],
|
||||
['r', rotate],
|
||||
['t', labelStyle.x, labelStyle.y! + labelBBox.height / 2],
|
||||
]);
|
||||
break;
|
||||
default:
|
||||
labelMatrix = transform(labelMatrix, [
|
||||
['t', -labelBBox.width / 2, -labelBBox.height / 2],
|
||||
['r', rotate],
|
||||
['t', labelBBox.width / 2, labelBBox.height / 2],
|
||||
]);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
labelMatrix = transform(labelMatrix, [
|
||||
['t', -labelStyle.x!, -labelStyle.y! - labelBBox.height / 2],
|
||||
['r', rotate],
|
||||
['t', labelStyle.x, labelStyle.y! + labelBBox.height / 2],
|
||||
]);
|
||||
}
|
||||
label.setMatrix(labelMatrix);
|
||||
}
|
||||
if (labelStyle.background) {
|
||||
const rect = this.drawLabelBg(cfg, group, label);
|
||||
const labelBgClassname = this.itemType + CLS_LABEL_BG_SUFFIX;
|
||||
rect.set('classname', labelBgClassname);
|
||||
label.toFront();
|
||||
}
|
||||
return label;
|
||||
},
|
||||
drawLabelBg(cfg: ModelConfig, group: GGroup, label: IElement) {
|
||||
const { labelCfg: defaultLabelCfg } = this.options as ModelConfig;
|
||||
const labelCfg = mix({}, defaultLabelCfg, cfg.labelCfg) as ILabelConfig;
|
||||
const style = this.getLabelBgStyleByPosition(label, cfg, labelCfg, group);
|
||||
const rect = group.addShape('rect', { name: 'text-bg-shape', attrs: style });
|
||||
return rect;
|
||||
},
|
||||
getLabelStyleByPosition(cfg: ModelConfig, labelCfg?: ILabelConfig, group?: GGroup): LabelStyle {
|
||||
return { text: cfg.label as string };
|
||||
},
|
||||
getLabelBgStyleByPosition(
|
||||
label: IElement,
|
||||
cfg: ModelConfig,
|
||||
labelCfg?: ILabelConfig,
|
||||
group?: GGroup,
|
||||
): LabelStyle {
|
||||
return {};
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取文本的配置项
|
||||
* @param cfg 节点的配置项
|
||||
* @param labelCfg 文本的配置项
|
||||
* @param group 父容器,label 的定位可能与图形相关
|
||||
*/
|
||||
getLabelStyle(cfg: ModelConfig, labelCfg: ILabelConfig, group: GGroup): LabelStyle {
|
||||
const calculateStyle = this.getLabelStyleByPosition!(cfg, labelCfg, group);
|
||||
const attrName = `${this.itemType}Label`; // 取 nodeLabel,edgeLabel 的配置项
|
||||
const defaultStyle = (Global as any)[attrName] ? (Global as any)[attrName].style : null;
|
||||
const labelStyle = { ...defaultStyle, ...calculateStyle, ...labelCfg.style };
|
||||
return labelStyle;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取图形的配置项
|
||||
* @param cfg
|
||||
*/
|
||||
getShapeStyle(cfg: ModelConfig): ShapeStyle {
|
||||
return cfg.style!;
|
||||
},
|
||||
/**
|
||||
* 更新节点,包含文本
|
||||
* @override
|
||||
* @param {Object} cfg 节点/边的配置项
|
||||
* @param {G6.Item} item 节点/边
|
||||
*/
|
||||
update(cfg: ModelConfig, item: Item) {
|
||||
(this as any).updateShapeStyle(cfg, item);
|
||||
(this as any).updateLabel(cfg, item);
|
||||
},
|
||||
updateShapeStyle(cfg: ModelConfig, item: Item) {
|
||||
const group = item.getContainer();
|
||||
const shape = item.getKeyShape();
|
||||
const shapeStyle = mix({}, shape.attr(), cfg.style);
|
||||
for (const key in shapeStyle) {
|
||||
const style = shapeStyle[key];
|
||||
if (isPlainObject(style)) {
|
||||
// 更新图元素样式,支持更新子元素
|
||||
const subShape = group.find((element) => element.get('name') === key);
|
||||
if (subShape) {
|
||||
subShape.attr(style);
|
||||
}
|
||||
} else {
|
||||
shape.attr({
|
||||
[key]: style,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
updateLabel(cfg: ModelConfig, item: Item) {
|
||||
const group = item.getContainer();
|
||||
const { labelCfg: defaultLabelCfg } = this.getOptions({}) as ModelConfig;
|
||||
const labelClassName = this.itemType + CLS_LABEL_SUFFIX;
|
||||
const label = group.find((element) => element.get('className') === labelClassName);
|
||||
const labelBgClassname = this.itemType + CLS_LABEL_BG_SUFFIX;
|
||||
let labelBg = group.find((element) => element.get('classname') === labelBgClassname);
|
||||
// 防止 cfg.label = "" 的情况
|
||||
if (cfg.label || cfg.label === '') {
|
||||
// 若传入的新配置中有 label,(用户没传入但原先有 label,label 也会有值)
|
||||
if (!label) {
|
||||
// 若原先不存在 label,则绘制一个新的 label
|
||||
const newLabel = this.drawLabel!(cfg, group);
|
||||
newLabel.set('className', labelClassName);
|
||||
} else {
|
||||
// 若原先存在 label,则更新样式。与 getLabelStyle 不同在于这里需要融合当前 label 的样式
|
||||
// 用于融合 style 以外的属性:position, offset, ...
|
||||
let currentLabelCfg = {} as any;
|
||||
if (item.getModel) {
|
||||
currentLabelCfg = item.getModel().labelCfg;
|
||||
}
|
||||
// 这里不能去掉
|
||||
const labelCfg = deepMix({}, defaultLabelCfg, currentLabelCfg, cfg.labelCfg);
|
||||
|
||||
// 获取位置信息
|
||||
const calculateStyle = (this as any).getLabelStyleByPosition(cfg, labelCfg, group);
|
||||
|
||||
// 取 nodeLabel,edgeLabel 的配置项
|
||||
const cfgStyle = cfg.labelCfg ? cfg.labelCfg.style : undefined;
|
||||
const cfgBgStyle = labelCfg.style && labelCfg.style.background;
|
||||
|
||||
// 需要融合当前 label 的样式 label.attr()。不再需要全局/默认样式,因为已经应用在当前的 label 上
|
||||
const labelStyle = { ...label.attr(), ...calculateStyle, ...cfgStyle };
|
||||
const rotate = labelStyle.rotate;
|
||||
delete labelStyle.rotate;
|
||||
|
||||
// 计算 label 的旋转矩阵
|
||||
if (rotate) {
|
||||
// if G 4.x define the rotateAtStart, use it directly instead of using the following codes
|
||||
let rotateMatrix = [1, 0, 0, 0, 1, 0, 0, 0, 1];
|
||||
rotateMatrix = transform(rotateMatrix, [
|
||||
['t', -labelStyle.x, -labelStyle.y],
|
||||
['r', rotate],
|
||||
['t', labelStyle.x, labelStyle.y],
|
||||
]);
|
||||
label.resetMatrix();
|
||||
label.attr({ ...labelStyle, matrix: rotateMatrix });
|
||||
} else {
|
||||
label.resetMatrix();
|
||||
label.attr(labelStyle);
|
||||
}
|
||||
|
||||
if (!labelBg) {
|
||||
if (labelStyle.background) {
|
||||
labelBg = this.drawLabelBg(cfg, group, label);
|
||||
labelBg.set('classname', labelBgClassname);
|
||||
label.toFront();
|
||||
}
|
||||
} else if (labelStyle.background) {
|
||||
const calculateBgStyle = (this as any).getLabelBgStyleByPosition(
|
||||
label,
|
||||
cfg,
|
||||
labelCfg,
|
||||
group,
|
||||
);
|
||||
// const labelBgStyle = Object.assign({}, labelBg.attr(), calculateBgStyle, cfgBgStyle);
|
||||
const labelBgStyle = { ...calculateBgStyle, ...cfgBgStyle };
|
||||
labelBg.resetMatrix();
|
||||
if (rotate) {
|
||||
labelBg.rotateAtStart(rotate);
|
||||
}
|
||||
labelBg.attr(labelBgStyle);
|
||||
} else {
|
||||
group.removeChild(labelBg);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// update(cfg, item) // 默认不定义
|
||||
afterUpdate(cfg?: ModelConfig, item?: Item) { },
|
||||
/**
|
||||
* 设置节点的状态,主要是交互状态,业务状态请在 draw 方法中实现
|
||||
* 单图形的节点仅考虑 selected、active 状态,有其他状态需求的用户自己复写这个方法
|
||||
* @override
|
||||
* @param {String} name 状态名称
|
||||
* @param {String | Boolean} value 状态值
|
||||
* @param {G6.Item} item 节点
|
||||
*/
|
||||
setState(name: string, value: string | boolean, item: Item) {
|
||||
const shape: IShape = item.get('keyShape');
|
||||
if (!shape) {
|
||||
return;
|
||||
}
|
||||
|
||||
const type = item.getType();
|
||||
|
||||
const stateName = isBoolean(value) ? name : `${name}:${value}`;
|
||||
const shapeStateStyle = this.getStateStyle(stateName, item);
|
||||
const itemStateStyle = item.getStateStyle(stateName);
|
||||
|
||||
// 不允许设置一个不存在的状态
|
||||
if (!itemStateStyle && !shapeStateStyle) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 要设置或取消的状态的样式
|
||||
// 当没有 state 状态时,默认使用 model.stateStyles 中的样式
|
||||
const styles = mix({}, itemStateStyle || shapeStateStyle);
|
||||
|
||||
const group = item.getContainer();
|
||||
|
||||
// 从图元素现有的样式中删除本次要取消的 states 中存在的属性值。使用对象检索更快
|
||||
const keptAttrs = { x: 1, y: 1, cx: 1, cy: 1 };
|
||||
|
||||
if (value) {
|
||||
// style 为要设置的状态的样式
|
||||
for (const key in styles) {
|
||||
const style = styles[key];
|
||||
if (isPlainObject(style) && !ARROWS.includes(key)) {
|
||||
const subShape = group.find((element) => element.get('name') === key);
|
||||
if (subShape) {
|
||||
subShape.attr(style);
|
||||
}
|
||||
} else {
|
||||
// 非纯对象,则认为是设置到 keyShape 上面的
|
||||
shape.attr({
|
||||
[key]: style,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 所有生效的 state 的样式
|
||||
const enableStatesStyle = clone(item.getCurrentStatesStyle());
|
||||
|
||||
const model = item.getModel();
|
||||
// 原始样式
|
||||
const tmp = item.getOriginStyle();
|
||||
const originStyle = mix({}, model.style, clone(item.getOriginStyle()));
|
||||
|
||||
const keyShapeName = shape.get('name');
|
||||
const keyShapeStyles = clone(shape.attr());
|
||||
|
||||
// 已有样式 - 要取消的状态的样式
|
||||
const filtetDisableStatesStyle = {};
|
||||
|
||||
// styles 为要取消的状态的样式
|
||||
for (const p in styles) {
|
||||
const style = styles[p];
|
||||
if (isPlainObject(style) && !ARROWS.includes(p)) {
|
||||
const subShape = group.find((element) => element.get('name') === p);
|
||||
if (subShape) {
|
||||
const subShapeStyles = clone(subShape.attr());
|
||||
each(style, (v, key) => {
|
||||
if (p === keyShapeName && keyShapeStyles[key] && !keptAttrs[key]) {
|
||||
delete keyShapeStyles[key];
|
||||
const value = originStyle[p][key] || SHAPES_DEFAULT_ATTRS[type][key];
|
||||
shape.attr(key, value);
|
||||
} else if (subShapeStyles[key] || subShapeStyles[key] === 0) {
|
||||
delete subShapeStyles[key];
|
||||
const value = originStyle[p][key] || SHAPES_DEFAULT_ATTRS[type][key];
|
||||
subShape.attr(key, value);
|
||||
}
|
||||
});
|
||||
filtetDisableStatesStyle[p] = subShapeStyles;
|
||||
}
|
||||
} else {
|
||||
if (keyShapeStyles[p] && !keptAttrs[p]) {
|
||||
delete keyShapeStyles[p];
|
||||
const value = originStyle[p] || (originStyle[keyShapeName] ? originStyle[keyShapeName][p] : undefined) || SHAPES_DEFAULT_ATTRS[type][p];
|
||||
shape.attr(p, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 从图元素现有的样式中删除本次要取消的 states 中存在的属性值后,
|
||||
// 如果 keyShape 有 name 属性,则 filtetDisableStatesStyle 的格式为 { keyShapeName: {} }
|
||||
// 否则为普通对象
|
||||
if (!keyShapeName) {
|
||||
mix(filtetDisableStatesStyle, keyShapeStyles);
|
||||
} else {
|
||||
filtetDisableStatesStyle[keyShapeName] = keyShapeStyles;
|
||||
}
|
||||
|
||||
for (const key in enableStatesStyle) {
|
||||
if (keptAttrs[key]) continue;
|
||||
const enableStyle = enableStatesStyle[key];
|
||||
if (!isPlainObject(enableStyle) || ARROWS.includes(key)) {
|
||||
// 把样式属性merge到keyShape中
|
||||
if (!keyShapeName) {
|
||||
mix(originStyle, {
|
||||
[key]: enableStyle,
|
||||
});
|
||||
} else {
|
||||
mix(originStyle[keyShapeName], {
|
||||
[key]: enableStyle,
|
||||
});
|
||||
delete originStyle[key];
|
||||
}
|
||||
delete enableStatesStyle[key];
|
||||
}
|
||||
}
|
||||
|
||||
const originstyles = {};
|
||||
deepMix(originstyles, originStyle, filtetDisableStatesStyle, enableStatesStyle);
|
||||
let keyShapeSetted = false;
|
||||
|
||||
for (const originKey in originstyles) {
|
||||
const style = originstyles[originKey];
|
||||
if (isPlainObject(style) && !ARROWS.includes(originKey)) {
|
||||
const subShape = group.find((element) => element.get('name') === originKey);
|
||||
if (subShape) {
|
||||
subShape.attr(style);
|
||||
if (originKey === keyShapeName) keyShapeSetted = true;
|
||||
}
|
||||
} else if (!keyShapeSetted) {
|
||||
const value = style || SHAPES_DEFAULT_ATTRS[type][originKey];
|
||||
// 当更新 combo 状态时,当不存在 keyShapeName 时候,则认为是设置到 keyShape 上面的
|
||||
if (type === 'combo') {
|
||||
if (!keyShapeName) {
|
||||
shape.attr({
|
||||
[originKey]: value,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
shape.attr({
|
||||
[originKey]: value,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取不同状态下的样式
|
||||
*
|
||||
* @param {string} name 状态名称
|
||||
* @param {Item} item Node或Edge的实例
|
||||
* @return {object} 样式
|
||||
*/
|
||||
getStateStyle(name: string, item: Item): ShapeStyle {
|
||||
const model = item.getModel();
|
||||
const type = item.getType();
|
||||
const { stateStyles, style = {} } = this.getOptions(model);
|
||||
|
||||
const modelStateStyle = model.stateStyles
|
||||
? model.stateStyles[name]
|
||||
: stateStyles && stateStyles[name];
|
||||
|
||||
if (type === 'combo') {
|
||||
return clone(modelStateStyle);
|
||||
}
|
||||
return mix({}, style, modelStateStyle);
|
||||
},
|
||||
/**
|
||||
* 获取控制点
|
||||
* @param {Object} cfg 节点、边的配置项
|
||||
* @return {Array|null} 控制点的数组,如果为 null,则没有控制点
|
||||
*/
|
||||
getControlPoints(cfg: EdgeConfig): IPoint[] | undefined {
|
||||
return cfg.controlPoints;
|
||||
},
|
||||
/**
|
||||
* 获取控制点
|
||||
* @param {Object} cfg 节点、边的配置项
|
||||
* @return {Array|null} 锚点的数组,如果为 null,则没有锚点
|
||||
*/
|
||||
getAnchorPoints(cfg: ModelConfig): number[][] | undefined {
|
||||
const { anchorPoints } = this.getOptions(cfg) as ModelConfig;
|
||||
return anchorPoints;
|
||||
},
|
||||
};
|
571
packages/core/src/shape/xml.ts
Normal file
571
packages/core/src/shape/xml.ts
Normal file
@ -0,0 +1,571 @@
|
||||
/**
|
||||
* @fileOverview 从xml建立自定义Node,包含update
|
||||
* @author xuzhi.mxz@antfin.com
|
||||
*/
|
||||
|
||||
import { get } from '@antv/util';
|
||||
import { getTextSize } from '../util/graphic';
|
||||
|
||||
/**
|
||||
* 一种更宽松的JSON 解析,如果遇到不符合规范的字段会直接转为字符串
|
||||
* @param text json 内容
|
||||
*/
|
||||
function looseJSONParse(text) {
|
||||
if (typeof text !== 'string') {
|
||||
return text;
|
||||
}
|
||||
const safeParse = (str) => {
|
||||
if (typeof str !== 'string') {
|
||||
return str;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(str.trim());
|
||||
} catch (e) {
|
||||
return str.trim();
|
||||
}
|
||||
};
|
||||
const firstAttempt = safeParse(text);
|
||||
if (typeof firstAttempt !== 'string') {
|
||||
return firstAttempt;
|
||||
}
|
||||
const tail = (arr) => arr[arr.length - 1];
|
||||
const str = text.trim();
|
||||
const objectStack = [];
|
||||
const syntaxStack = [];
|
||||
const isLastPair = (...syntaxes) => syntaxes.some((syntax) => tail(syntaxStack) === syntax);
|
||||
const getValueStore = () => tail(objectStack);
|
||||
let rst = null;
|
||||
let i = 0;
|
||||
let temp = '';
|
||||
|
||||
while (i < str.length) {
|
||||
const nowChar = str[i];
|
||||
const isInString = isLastPair('"', "'");
|
||||
|
||||
if (!isInString && !nowChar.trim()) {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const isLastTranslate = str[i - 1] === '\\';
|
||||
const isInObject = isLastPair('}');
|
||||
const isInArray = isLastPair(']');
|
||||
const isWaitingValue = isLastPair(',');
|
||||
const tempArr = getValueStore();
|
||||
|
||||
if (isInString) {
|
||||
if (tail(syntaxStack) === nowChar && !isLastTranslate) {
|
||||
syntaxStack.pop();
|
||||
const value = safeParse(temp);
|
||||
tempArr.push(value);
|
||||
rst = value;
|
||||
temp = '';
|
||||
} else {
|
||||
temp += nowChar;
|
||||
}
|
||||
} else if (isInArray && nowChar === ',') {
|
||||
if (temp) {
|
||||
tempArr.push(safeParse(temp));
|
||||
temp = '';
|
||||
}
|
||||
} else if (isInObject && nowChar === ':') {
|
||||
syntaxStack.push(',');
|
||||
if (temp) {
|
||||
tempArr.push(temp);
|
||||
temp = '';
|
||||
}
|
||||
} else if (isWaitingValue && nowChar === ',') {
|
||||
if (temp) {
|
||||
tempArr.push(safeParse(temp));
|
||||
temp = '';
|
||||
}
|
||||
syntaxStack.pop();
|
||||
} else if (nowChar === '}' && (isInObject || isWaitingValue)) {
|
||||
if (temp) {
|
||||
tempArr.push(safeParse(temp));
|
||||
temp = '';
|
||||
}
|
||||
if (isWaitingValue) {
|
||||
syntaxStack.pop();
|
||||
}
|
||||
const obj = {};
|
||||
for (let c = 1; c < tempArr.length; c += 2) {
|
||||
obj[tempArr[c - 1]] = tempArr[c];
|
||||
}
|
||||
objectStack.pop();
|
||||
if (objectStack.length) {
|
||||
tail(objectStack).push(obj);
|
||||
}
|
||||
syntaxStack.pop();
|
||||
rst = obj;
|
||||
} else if (nowChar === ']' && isInArray) {
|
||||
if (temp) {
|
||||
tempArr.push(safeParse(temp));
|
||||
temp = '';
|
||||
}
|
||||
objectStack.pop();
|
||||
if (objectStack.length) {
|
||||
tail(objectStack).push(tempArr);
|
||||
}
|
||||
syntaxStack.pop();
|
||||
rst = tempArr;
|
||||
} else if (nowChar === '{') {
|
||||
objectStack.push([]);
|
||||
syntaxStack.push('}');
|
||||
} else if (nowChar === '[') {
|
||||
objectStack.push([]);
|
||||
syntaxStack.push(']');
|
||||
} else if (nowChar === '"') {
|
||||
syntaxStack.push('"');
|
||||
} else if (nowChar === "'") {
|
||||
syntaxStack.push("'");
|
||||
} else {
|
||||
temp += nowChar;
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
return rst || temp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部用于最终实际渲染的结构
|
||||
*/
|
||||
interface NodeInstructure {
|
||||
type: string;
|
||||
attrs: { [key: string]: any };
|
||||
children: NodeInstructure[];
|
||||
bbox: {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
}
|
||||
|
||||
const keyConvert = (str) =>
|
||||
str.split('-').reduce((a, b) => a + b.charAt(0).toUpperCase() + b.slice(1));
|
||||
|
||||
/**
|
||||
* 简单的一个{{}}模板渲染,不包含任何复杂语法
|
||||
* @param xml
|
||||
*/
|
||||
export const xmlDataRenderer = (xml: string) => (data) => {
|
||||
const len = xml.length;
|
||||
const arr = [];
|
||||
let i = 0;
|
||||
let tmp = '';
|
||||
while (i < len) {
|
||||
if (xml[i] === '{' && xml[i + 1] === '{') {
|
||||
arr.push(tmp);
|
||||
tmp = '';
|
||||
i += 2;
|
||||
} else if (xml[i] === '}' && xml[i + 1] === '}') {
|
||||
if (arr.length) {
|
||||
const last = arr.pop();
|
||||
tmp = get(data, tmp, last.endsWith('=') ? `"{${tmp}}"` : tmp);
|
||||
arr.push(last + tmp);
|
||||
}
|
||||
i += 2;
|
||||
tmp = '';
|
||||
} else {
|
||||
tmp += xml[i];
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
arr.push(tmp);
|
||||
return arr
|
||||
.map((e, index) => (arr[index - 1] && arr[index - 1].endsWith('=') ? `"{${e}}"` : e))
|
||||
.join('');
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析XML,并转化为相应的JSON结构
|
||||
* @param xml xml解析后的节点
|
||||
*/
|
||||
export function parseXML(xml: HTMLElement, cfg) {
|
||||
let attrs = {} as { [key: string]: any };
|
||||
const keys = (xml.getAttributeNames && xml.getAttributeNames()) || ([] as string[]);
|
||||
const children =
|
||||
xml.children && Array.from(xml.children).map((e) => parseXML(e as HTMLElement, cfg));
|
||||
const rst = {} as { [key: string]: any } & NodeInstructure;
|
||||
const tagName = xml.tagName ? xml.tagName.toLowerCase() : 'group';
|
||||
|
||||
if (tagName === 'text') {
|
||||
attrs.text = xml.innerText;
|
||||
}
|
||||
|
||||
rst.type = tagName;
|
||||
|
||||
if (tagName === 'img') {
|
||||
rst.type = 'image';
|
||||
}
|
||||
|
||||
Array.from(keys).forEach((k) => {
|
||||
const key = keyConvert(k);
|
||||
const val = xml.getAttribute(k);
|
||||
|
||||
try {
|
||||
if (key === 'style' || key === 'attrs') {
|
||||
const style = looseJSONParse(val);
|
||||
attrs = {
|
||||
...attrs,
|
||||
...style,
|
||||
};
|
||||
} else {
|
||||
rst[key] = looseJSONParse(val);
|
||||
}
|
||||
} catch (e) {
|
||||
if (key === 'style') {
|
||||
throw e;
|
||||
}
|
||||
rst[key] = val;
|
||||
}
|
||||
});
|
||||
|
||||
rst.attrs = attrs;
|
||||
|
||||
if (cfg && cfg.style && rst.name && typeof cfg.style[rst.name] === 'object') {
|
||||
rst.attrs = {
|
||||
...rst.attrs,
|
||||
...cfg.style[rst.name],
|
||||
};
|
||||
}
|
||||
|
||||
if (cfg && cfg.style && rst.keyshape) {
|
||||
rst.attrs = {
|
||||
...rst.attrs,
|
||||
...cfg.style,
|
||||
};
|
||||
}
|
||||
|
||||
if (children.length) {
|
||||
rst.children = children;
|
||||
}
|
||||
|
||||
return rst;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据偏移量和内部节点最终的bounding box来得出该shape最终的bbox
|
||||
*/
|
||||
export function getBBox(
|
||||
node: NodeInstructure,
|
||||
offset: { x: number; y: number },
|
||||
chilrenBBox: { width: number; height: number },
|
||||
) {
|
||||
const { attrs = {} } = node;
|
||||
const bbox = {
|
||||
x: offset.x || 0,
|
||||
y: offset.y || 0,
|
||||
width: chilrenBBox.width || 0,
|
||||
height: chilrenBBox.height || 0,
|
||||
};
|
||||
|
||||
let shapeHeight, shapeWidth;
|
||||
switch (node.type) {
|
||||
case 'maker':
|
||||
case 'circle':
|
||||
if (attrs.r) {
|
||||
shapeWidth = 2 * attrs.r;
|
||||
shapeHeight = 2 * attrs.r;
|
||||
}
|
||||
break;
|
||||
case 'text':
|
||||
if (attrs.text) {
|
||||
shapeWidth = getTextSize(attrs.text, attrs.fontSize || 12)[0];
|
||||
shapeHeight = 16;
|
||||
bbox.y += shapeHeight;
|
||||
bbox.height = shapeHeight;
|
||||
bbox.width = shapeWidth;
|
||||
node.attrs = {
|
||||
fontSize: 12,
|
||||
fill: '#000',
|
||||
...attrs,
|
||||
};
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (attrs.width) {
|
||||
shapeWidth = attrs.width;
|
||||
}
|
||||
if (attrs.height) {
|
||||
shapeHeight = attrs.height;
|
||||
}
|
||||
}
|
||||
if (shapeHeight >= 0) {
|
||||
bbox.height = shapeHeight;
|
||||
}
|
||||
if (shapeWidth >= 0) {
|
||||
bbox.width = shapeWidth;
|
||||
}
|
||||
|
||||
if (attrs.marginTop) {
|
||||
bbox.y += attrs.marginTop;
|
||||
}
|
||||
|
||||
if (attrs.marginLeft) {
|
||||
bbox.x += attrs.marginLeft;
|
||||
}
|
||||
|
||||
return bbox;
|
||||
}
|
||||
|
||||
/**
|
||||
* 把从xml计算出的结构填上位置信息,补全attrs
|
||||
* @param target
|
||||
* @param lastOffset
|
||||
*/
|
||||
export function generateTarget(target: NodeInstructure, lastOffset = { x: 0, y: 0 }) {
|
||||
const defaultBbox = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
...lastOffset,
|
||||
};
|
||||
|
||||
if (target.children?.length) {
|
||||
const { attrs = {} } = target;
|
||||
const { marginTop } = attrs;
|
||||
const offset = { ...lastOffset };
|
||||
|
||||
if (marginTop) {
|
||||
offset.y += marginTop;
|
||||
}
|
||||
|
||||
for (let index = 0; index < target.children.length; index++) {
|
||||
target.children[index].attrs.key = `${attrs.key || 'root'} -${index} `;
|
||||
const node = generateTarget(target.children[index], offset);
|
||||
if (node.bbox) {
|
||||
const { bbox } = node;
|
||||
if (node.attrs.next === 'inline') {
|
||||
offset.x += node.bbox.width;
|
||||
} else {
|
||||
offset.y += node.bbox.height;
|
||||
}
|
||||
if (bbox.width + bbox.x > defaultBbox.width) {
|
||||
defaultBbox.width = bbox.width + bbox.x;
|
||||
}
|
||||
if (bbox.height + bbox.y > defaultBbox.height) {
|
||||
defaultBbox.height = bbox.height + bbox.y;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
target.bbox = getBBox(target, lastOffset, defaultBbox);
|
||||
|
||||
target.attrs = {
|
||||
...target.attrs,
|
||||
...target.bbox,
|
||||
};
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对比前后两个最终计算出来的node,并对比出最小改动,
|
||||
* 动作: 'add' 添加节点 | ’delete‘ 删除节点 | ’change‘ 改变节点attrs | 'restructure' 重构节点
|
||||
* @param nowTarget
|
||||
* @param formerTarget
|
||||
*/
|
||||
export function compareTwoTarget(nowTarget: NodeInstructure, formerTarget: NodeInstructure) {
|
||||
const { type } = nowTarget || {};
|
||||
const { key } = formerTarget?.attrs || {};
|
||||
|
||||
if (key && nowTarget) {
|
||||
nowTarget.attrs.key = key;
|
||||
}
|
||||
|
||||
if (!nowTarget && formerTarget) {
|
||||
return {
|
||||
action: 'delete',
|
||||
val: formerTarget,
|
||||
type,
|
||||
key,
|
||||
};
|
||||
}
|
||||
if (nowTarget && !formerTarget) {
|
||||
return {
|
||||
action: 'add',
|
||||
val: nowTarget,
|
||||
type,
|
||||
};
|
||||
}
|
||||
if (!nowTarget && !formerTarget) {
|
||||
return {
|
||||
action: 'same',
|
||||
type,
|
||||
};
|
||||
}
|
||||
const children = [];
|
||||
|
||||
if (nowTarget.children?.length > 0 || formerTarget.children?.length > 0) {
|
||||
const length = Math.max(nowTarget.children?.length, formerTarget.children?.length);
|
||||
const formerChilren = formerTarget.children || [];
|
||||
const nowChilren = nowTarget.children || [];
|
||||
|
||||
for (let index = 0; index < length; index += 1) {
|
||||
children.push(compareTwoTarget(nowChilren[index], formerChilren[index]));
|
||||
}
|
||||
}
|
||||
|
||||
const formerKeys = Object.keys(formerTarget.attrs);
|
||||
const nowKeys = Object.keys(nowTarget.attrs);
|
||||
|
||||
if (formerTarget.type !== nowTarget.type) {
|
||||
return {
|
||||
action: 'restructure',
|
||||
nowTarget,
|
||||
formerTarget,
|
||||
key,
|
||||
children,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
formerKeys
|
||||
.filter((e) => e !== 'children')
|
||||
.some((e) => nowTarget.attrs[e] !== formerTarget.attrs[e] || !nowKeys.includes(e))
|
||||
) {
|
||||
return {
|
||||
action: 'change',
|
||||
val: nowTarget,
|
||||
children,
|
||||
type,
|
||||
key,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
action: 'same',
|
||||
children,
|
||||
type,
|
||||
key,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据xml或者返回xml的函数构建自定义节点的结构
|
||||
* @param gen
|
||||
*/
|
||||
export function createNodeFromXML(gen: string | ((node: any) => string)) {
|
||||
const structures = {};
|
||||
const compileXML = (cfg) => {
|
||||
const rawStr = typeof gen === 'function' ? gen(cfg) : gen;
|
||||
const target = xmlDataRenderer(rawStr)(cfg);
|
||||
const xmlParser = document.createElement('div');
|
||||
xmlParser.innerHTML = target;
|
||||
const xml = xmlParser.children[0] as HTMLElement;
|
||||
const result = generateTarget(parseXML(xml, cfg));
|
||||
|
||||
xmlParser.remove();
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return {
|
||||
draw(cfg, group) {
|
||||
const resultTarget = compileXML(cfg);
|
||||
let keyshape = group;
|
||||
const renderTarget = (target) => {
|
||||
const { attrs = {}, bbox, type, children, ...rest } = target;
|
||||
if (target.type !== 'group') {
|
||||
const shape = group.addShape(target.type, {
|
||||
attrs,
|
||||
origin: {
|
||||
bbox,
|
||||
type,
|
||||
children,
|
||||
},
|
||||
...rest,
|
||||
});
|
||||
if (target.keyshape) {
|
||||
keyshape = shape;
|
||||
}
|
||||
}
|
||||
|
||||
if (target.children) {
|
||||
target.children.forEach((n) => renderTarget(n));
|
||||
}
|
||||
};
|
||||
|
||||
renderTarget(resultTarget);
|
||||
|
||||
structures[cfg.id] = [resultTarget];
|
||||
|
||||
return keyshape;
|
||||
},
|
||||
update(cfg, node) {
|
||||
if (!structures[cfg.id]) {
|
||||
structures[cfg.id] = [];
|
||||
}
|
||||
const container = node.getContainer();
|
||||
const children = container.get('children');
|
||||
const newTarget = compileXML(cfg);
|
||||
const lastTarget = structures[cfg.id].pop();
|
||||
const diffResult = compareTwoTarget(newTarget, lastTarget);
|
||||
const addShape = (shape) => {
|
||||
if (shape.type !== 'group') {
|
||||
container.addShape(shape.type, { attrs: shape.attrs });
|
||||
}
|
||||
if (shape.children?.length) {
|
||||
shape.children.map((e) => addShape(e));
|
||||
}
|
||||
};
|
||||
const delShape = (shape) => {
|
||||
const targetShape = children.find((e) => e.attrs.key === shape.attrs.key);
|
||||
if (targetShape) {
|
||||
container.removeChild(targetShape);
|
||||
}
|
||||
if (shape.children?.length) {
|
||||
shape.children.map((e) => delShape(e));
|
||||
}
|
||||
};
|
||||
const updateTarget = (target) => {
|
||||
const { key } = target;
|
||||
if (target.type !== 'group') {
|
||||
const targetShape = children.find((e) => e.attrs.key === key);
|
||||
switch (target.action) {
|
||||
case 'change':
|
||||
if (targetShape) {
|
||||
const originAttr = target.val.keyshape ? node.getOriginStyle() : {};
|
||||
targetShape.attr({ ...originAttr, ...target.val.attrs });
|
||||
}
|
||||
break;
|
||||
case 'add':
|
||||
addShape(target.val);
|
||||
break;
|
||||
case 'delete':
|
||||
delShape(target.val);
|
||||
break;
|
||||
case 'restructure':
|
||||
delShape(target.formerTarget);
|
||||
addShape(target.nowTarget);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (target.children) {
|
||||
target.children.forEach((n) => updateTarget(n));
|
||||
}
|
||||
};
|
||||
|
||||
updateTarget(diffResult);
|
||||
|
||||
structures[cfg.id].push(newTarget);
|
||||
},
|
||||
getAnchorPoints() {
|
||||
return [
|
||||
[0, 0.5],
|
||||
[1, 0.5],
|
||||
[0.5, 1],
|
||||
[0.5, 0],
|
||||
];
|
||||
},
|
||||
};
|
||||
}
|
804
packages/core/src/types/index.ts
Normal file
804
packages/core/src/types/index.ts
Normal file
@ -0,0 +1,804 @@
|
||||
/* eslint @typescript-eslint/no-use-before-define: 0 */
|
||||
import { IGroup, Event as GraphEvent } from '@antv/g-base';
|
||||
import { BBox, AnimateCfg } from '@antv/g-base/lib/types';
|
||||
import { Canvas, Shape } from '@antv/g-canvas';
|
||||
import Node from '../item/node';
|
||||
import { IGraph } from '../interface/graph';
|
||||
import { IEdge, INode, ICombo } from '../interface/item';
|
||||
import { ILabelConfig } from '../interface/shape';
|
||||
|
||||
type ShapeBase = Shape.Base;
|
||||
|
||||
// Node Edge Combo 实例
|
||||
export type Item = INode | IEdge | ICombo;
|
||||
|
||||
export interface IG6GraphEvent extends GraphEvent {
|
||||
item: Item | null;
|
||||
canvasX: number;
|
||||
canvasY: number;
|
||||
clientX: number;
|
||||
clientY: number;
|
||||
x: number;
|
||||
y: number;
|
||||
wheelDelta: number;
|
||||
detail: number;
|
||||
key?: string;
|
||||
target: IShapeBase & Canvas;
|
||||
}
|
||||
|
||||
// Math types
|
||||
export interface IPoint {
|
||||
x: number;
|
||||
y: number;
|
||||
// 获取连接点时使用
|
||||
anchorIndex?: number;
|
||||
[key: string]: number | undefined;
|
||||
}
|
||||
|
||||
export type IPointTuple = [number, number];
|
||||
|
||||
export type Matrix = number[];
|
||||
|
||||
export interface IBBox extends BBox {
|
||||
centerX?: number;
|
||||
centerY?: number;
|
||||
[key: string]: number | undefined;
|
||||
}
|
||||
|
||||
export type Padding = number | string | number[];
|
||||
|
||||
export interface ArrowConfig {
|
||||
d?: number;
|
||||
path?: string;
|
||||
stroke?: string;
|
||||
fill?: string;
|
||||
}
|
||||
|
||||
// Shape types
|
||||
export type ShapeStyle = Partial<{
|
||||
x: number;
|
||||
y: number;
|
||||
r: number;
|
||||
radius: number;
|
||||
width: number;
|
||||
height: number;
|
||||
offset: number | number[];
|
||||
stroke: string | null;
|
||||
strokeOpacity: number;
|
||||
fill: string | null;
|
||||
fillOpacity: number;
|
||||
lineWidth: number;
|
||||
lineAppendWidth: number;
|
||||
lineDash: number[];
|
||||
path: string | object[];
|
||||
points: object[];
|
||||
matrix: number[];
|
||||
opacity: number;
|
||||
size: number | number[];
|
||||
endArrow: boolean | ArrowConfig;
|
||||
startArrow: boolean | ArrowConfig;
|
||||
shadowColor: string;
|
||||
shadowBlur: number;
|
||||
shadowOffsetX: number;
|
||||
shadowOffsetY: number;
|
||||
cursor: string;
|
||||
position: string;
|
||||
fontSize: number;
|
||||
|
||||
keepVisualSize: boolean;
|
||||
}>;
|
||||
|
||||
export interface IShapeBase extends ShapeBase {
|
||||
isKeyShape: boolean;
|
||||
}
|
||||
|
||||
export interface IRect extends IPoint {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface ICircle extends IPoint {
|
||||
r: number;
|
||||
}
|
||||
|
||||
export interface IEllipse extends IPoint {
|
||||
rx: number;
|
||||
ry: number;
|
||||
}
|
||||
|
||||
export type SourceTarget = 'source' | 'target';
|
||||
|
||||
// 自环边配置
|
||||
export type LoopConfig = Partial<{
|
||||
dist: number;
|
||||
position: string;
|
||||
// 如果逆时针画,交换起点和终点
|
||||
clockwise: boolean;
|
||||
}>;
|
||||
|
||||
export interface LayoutConfig {
|
||||
type?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface ModeOption {
|
||||
type: string;
|
||||
delegate?: boolean;
|
||||
delegateStyle?: object;
|
||||
updateEdge?: boolean;
|
||||
trigger?: string;
|
||||
enableDelegate?: boolean;
|
||||
maxZoom?: number;
|
||||
minZoom?: number;
|
||||
enableOptimize?: boolean;
|
||||
optimizeZoom?: number;
|
||||
multiple?: boolean;
|
||||
activeState?: string;
|
||||
comboActiveState?: string;
|
||||
selectedState?: string;
|
||||
onlyChangeComboSize?: boolean;
|
||||
includeEdges?: boolean;
|
||||
direction?: 'x' | 'y';
|
||||
scalableRange?: number;
|
||||
offset?: number;
|
||||
fixSelectedItems?: Partial<{
|
||||
fixAll: boolean;
|
||||
fixLineWidth: boolean;
|
||||
fixLabel: boolean;
|
||||
fixState: string;
|
||||
}>;
|
||||
key?: string | undefined;
|
||||
edgeConfig?: EdgeConfig;
|
||||
functionName?: string;
|
||||
functionParams?: any[];
|
||||
shouldUpdate?: (e: IG6GraphEvent) => boolean;
|
||||
shouldBegin?: (e: IG6GraphEvent) => boolean;
|
||||
shouldEnd?: (e: IG6GraphEvent) => boolean;
|
||||
onChange?: (item?: Item, judge?: boolean) => unknown;
|
||||
onSelect?: (selectedNodes?: Item[], selectedEdges?: Item[]) => unknown;
|
||||
onDeselect?: (selectedNodes?: Item[], selectedEdges?: Item[]) => unknown;
|
||||
formatText?: (data: { [key: string]: unknown }) => string;
|
||||
}
|
||||
|
||||
export type ModeType = string | ModeOption;
|
||||
|
||||
export interface Modes {
|
||||
default?: ModeType[];
|
||||
[key: string]: ModeType[] | undefined;
|
||||
}
|
||||
|
||||
// Graph 配置项中 state 的类型
|
||||
export interface States {
|
||||
[key: string]: INode[];
|
||||
}
|
||||
|
||||
export interface StateStyles {
|
||||
[key: string]:
|
||||
| ShapeStyle
|
||||
| {
|
||||
[key: string]: ShapeStyle;
|
||||
};
|
||||
}
|
||||
|
||||
// model types (node edge group)
|
||||
export type ModelStyle = Partial<{
|
||||
[key: string]: unknown;
|
||||
style: ShapeStyle;
|
||||
stateStyles: StateStyles;
|
||||
}>;
|
||||
|
||||
export interface GraphOptions {
|
||||
/**
|
||||
* 图的 DOM 容器,可以传入该 DOM 的 id 或者直接传入容器的 HTML 节点对象
|
||||
*/
|
||||
container: string | HTMLElement;
|
||||
/**
|
||||
* 指定画布宽度,单位为 'px'
|
||||
*/
|
||||
width: number;
|
||||
/**
|
||||
* 指定画布高度,单位为 'px'
|
||||
*/
|
||||
height: number;
|
||||
/**
|
||||
* renderer canvas or svg
|
||||
*/
|
||||
renderer?: string;
|
||||
|
||||
fitView?: boolean;
|
||||
|
||||
fitCenter?: boolean;
|
||||
|
||||
layout?: LayoutConfig;
|
||||
|
||||
/**
|
||||
* 图适应画布时,指定四周的留白。
|
||||
* 可以是一个值, 例如:fitViewPadding: 20
|
||||
* 也可以是一个数组,例如:fitViewPadding: [20, 40, 50,20]
|
||||
* 当指定一个值时,四边的边距都相等,当指定数组时,数组内数值依次对应 上,右,下,左四边的边距。
|
||||
*/
|
||||
fitViewPadding?: Padding;
|
||||
/**
|
||||
* 各种元素是否在一个分组内,决定节点和边的层级问题,默认情况下所有的节点在一个分组中,所有的边在一个分组中,当这个参数为 false 时,节点和边的层级根据生成的顺序确定。
|
||||
* 默认值:true
|
||||
*/
|
||||
groupByTypes?: boolean;
|
||||
|
||||
// 是否有向图
|
||||
directed?: boolean;
|
||||
|
||||
groupStyle?: {
|
||||
style?: {
|
||||
[key: string]: ShapeStyle;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 当图中元素更新,或视口变换时,是否自动重绘。建议在批量操作节点时关闭,以提高性能,完成批量操作后再打开,参见后面的 setAutoPaint() 方法。
|
||||
* 默认值:true
|
||||
*/
|
||||
autoPaint?: boolean;
|
||||
|
||||
/**
|
||||
* 设置画布的模式。详情可见G6中的Mode文档。
|
||||
*/
|
||||
modes?: Modes;
|
||||
|
||||
/**
|
||||
* 默认状态下节点的配置,比如 type, size, color。会被写入的 data 覆盖。
|
||||
*/
|
||||
defaultNode?: Partial<{
|
||||
type: string;
|
||||
size: number | number[];
|
||||
color: string;
|
||||
}> &
|
||||
ModelStyle;
|
||||
|
||||
/**
|
||||
* 默认状态下边的配置,比如 type, size, color。会被写入的 data 覆盖。
|
||||
*/
|
||||
defaultEdge?: Partial<{
|
||||
type: string;
|
||||
size: number | number[];
|
||||
color: string;
|
||||
}> &
|
||||
ModelStyle;
|
||||
|
||||
/**
|
||||
* Combo 默认配置
|
||||
*/
|
||||
defaultCombo?: Partial<{
|
||||
type: string;
|
||||
size: number | number[];
|
||||
color: string;
|
||||
}> &
|
||||
ModelStyle;
|
||||
|
||||
nodeStateStyles?: StateStyles;
|
||||
|
||||
edgeStateStyles?: StateStyles;
|
||||
|
||||
// Combo 状态样式
|
||||
comboStateStyles?: StateStyles;
|
||||
|
||||
/**
|
||||
* 向 graph 注册插件。插件机制请见:plugin
|
||||
*/
|
||||
plugins?: any[];
|
||||
/**
|
||||
* 是否启用全局动画。
|
||||
*/
|
||||
animate?: boolean;
|
||||
|
||||
/**
|
||||
* 动画配置项,仅在animate为true时有效。
|
||||
*/
|
||||
animateCfg?: GraphAnimateConfig;
|
||||
/**
|
||||
* 最小缩放比例
|
||||
* 默认值 0.2
|
||||
*/
|
||||
minZoom?: number;
|
||||
/**
|
||||
* 最大缩放比例
|
||||
* 默认值 10
|
||||
*/
|
||||
maxZoom?: number;
|
||||
|
||||
groupType?: string;
|
||||
|
||||
/**
|
||||
* Edge 是否连接到节点中间
|
||||
*/
|
||||
linkCenter?: boolean;
|
||||
|
||||
/**
|
||||
* 是否启用 stack,即是否开启 redo & undo 功能
|
||||
*/
|
||||
enabledStack?: boolean;
|
||||
|
||||
/**
|
||||
* redo & undo 最大步数, 只有当 enabledStack 为 true 时才起作用
|
||||
*/
|
||||
maxStep?: number;
|
||||
|
||||
/**
|
||||
* 存储图上的 tooltip dom,方便销毁
|
||||
*/
|
||||
tooltips?: [];
|
||||
}
|
||||
|
||||
export type LabelStyle = Partial<{
|
||||
rotate: number;
|
||||
textAlign: string;
|
||||
angle: number;
|
||||
x: number;
|
||||
y: number;
|
||||
text: string;
|
||||
stroke: string | null;
|
||||
opacity: number;
|
||||
fontSize: number;
|
||||
fontStyle: string;
|
||||
fill: string | null;
|
||||
rotateCenter: string;
|
||||
lineWidth?: number;
|
||||
shadowColor?: string;
|
||||
shadowBlur?: number;
|
||||
shadowOffsetX?: number;
|
||||
shadowOffsetY?: number;
|
||||
position: string;
|
||||
textBaseline: string;
|
||||
offset: number;
|
||||
background?: {
|
||||
fill?: string;
|
||||
stroke?: string;
|
||||
lineWidth?: number;
|
||||
radius?: number[] | number;
|
||||
padding?: number[] | number;
|
||||
};
|
||||
}>;
|
||||
|
||||
export type Easeing =
|
||||
| 'easeLinear'
|
||||
| 'easePolyIn'
|
||||
| 'easePolyOut'
|
||||
| 'easePolyInOut'
|
||||
| 'easeQuad'
|
||||
| 'easeQuadIn'
|
||||
| 'easeQuadOut'
|
||||
| 'easeQuadInOut'
|
||||
| string;
|
||||
|
||||
export interface ModelConfig extends ModelStyle {
|
||||
// 节点或边的类型
|
||||
type?: string;
|
||||
label?: string | LabelStyle;
|
||||
labelCfg?: ILabelConfig;
|
||||
x?: number;
|
||||
y?: number;
|
||||
size?: number | number[];
|
||||
color?: string;
|
||||
anchorPoints?: number[][];
|
||||
startPoint?: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
endPoint?: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
visible?: boolean;
|
||||
}
|
||||
|
||||
export interface TreeGraphData {
|
||||
id: string;
|
||||
label?: string;
|
||||
x?: number;
|
||||
y?: number;
|
||||
children?: TreeGraphData[];
|
||||
data?: ModelConfig;
|
||||
side?: 'left' | 'right';
|
||||
depth?: number;
|
||||
collapsed?: boolean;
|
||||
style?:
|
||||
| ShapeStyle
|
||||
| {
|
||||
[key: string]: ShapeStyle;
|
||||
};
|
||||
stateStyles?: StateStyles;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface NodeConfig extends ModelConfig {
|
||||
id: string;
|
||||
groupId?: string;
|
||||
comboId?: string;
|
||||
children?: TreeGraphData[];
|
||||
description?: string;
|
||||
descriptionCfg?: {
|
||||
style?: object;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
img?: string;
|
||||
innerR?: number;
|
||||
direction?: string;
|
||||
preRect?: {
|
||||
show?: boolean;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
logoIcon?: {
|
||||
show?: boolean;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
stateIcon?: {
|
||||
show?: boolean;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
linkPoints?: {
|
||||
top?: boolean;
|
||||
right?: boolean;
|
||||
bottom?: boolean;
|
||||
left?: boolean;
|
||||
size?: number;
|
||||
lineWidth?: number;
|
||||
fill?: string;
|
||||
stroke?: string;
|
||||
r?: number;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
icon?: {
|
||||
show?: boolean;
|
||||
// icon的地址,字符串类型
|
||||
img?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
offset?: number;
|
||||
};
|
||||
clipCfg?: {
|
||||
show?: boolean;
|
||||
type?: string;
|
||||
// circle
|
||||
r?: number;
|
||||
// ellipse
|
||||
rx?: number;
|
||||
ry?: number;
|
||||
// rect
|
||||
width?: number;
|
||||
height?: number;
|
||||
// polygon
|
||||
points?: number[][];
|
||||
// path
|
||||
path?: Array<Array<string | number>>;
|
||||
// 坐标
|
||||
x?: number;
|
||||
y?: number;
|
||||
// clip 的属性样式
|
||||
// style?: ShapeStyle
|
||||
};
|
||||
}
|
||||
|
||||
export interface ComboTree {
|
||||
id: string;
|
||||
label?: string | LabelStyle;
|
||||
children?: ComboTree[];
|
||||
depth?: number;
|
||||
parentId?: string;
|
||||
removed?: boolean;
|
||||
itemType?: 'node' | 'combo';
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface ComboConfig extends ModelConfig {
|
||||
id: string;
|
||||
parentId?: string;
|
||||
children?: ComboTree[];
|
||||
depth?: number;
|
||||
padding?: number | number[];
|
||||
collapseIcon?: Partial<{
|
||||
show: boolean;
|
||||
collapseSymbol: any;
|
||||
expandSymbol: any;
|
||||
r: number;
|
||||
lineWidth: number;
|
||||
stroke: string;
|
||||
offsetX: number;
|
||||
offsetY: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface EdgeConfig extends ModelConfig {
|
||||
id?: string;
|
||||
source?: string;
|
||||
target?: string;
|
||||
sourceNode?: Node;
|
||||
targetNode?: Node;
|
||||
startPoint?: IPoint;
|
||||
endPoint?: IPoint;
|
||||
controlPoints?: IPoint[];
|
||||
curveOffset?: number | number[];
|
||||
// loop edge config
|
||||
loopCfg?: LoopConfig;
|
||||
labelCfg?: ILabelConfig;
|
||||
}
|
||||
|
||||
export type EdgeData = EdgeConfig & {
|
||||
sourceNode: Node;
|
||||
targetNode: Node;
|
||||
startPoint: IPoint;
|
||||
endPoint: IPoint;
|
||||
};
|
||||
|
||||
export interface NodeMap {
|
||||
[key: string]: INode;
|
||||
}
|
||||
|
||||
export interface NodeConfigMap {
|
||||
[key: string]: NodeConfig;
|
||||
}
|
||||
|
||||
export interface GraphData {
|
||||
nodes?: NodeConfig[];
|
||||
edges?: EdgeConfig[];
|
||||
combos?: ComboConfig[];
|
||||
}
|
||||
|
||||
export interface GraphAnimateConfig extends AnimateCfg {
|
||||
/**
|
||||
* 回调函数,用于自定义节点运动路径。
|
||||
*/
|
||||
onFrame?: (item: Item, ratio: number, data?: GraphData, originAttrs?: ShapeStyle) => unknown;
|
||||
}
|
||||
|
||||
export interface GroupNodeIds {
|
||||
[key: string]: string[];
|
||||
}
|
||||
|
||||
// Behavior type file
|
||||
export enum G6Event {
|
||||
// common events
|
||||
CLICK = 'click',
|
||||
DBLCLICK = 'dblclick',
|
||||
MOUSEDOWN = 'mousedown',
|
||||
MOUDEUP = 'mouseup',
|
||||
CONTEXTMENU = 'contextmenu',
|
||||
MOUSEENTER = 'mouseenter',
|
||||
MOUSEOUT = 'mouseout',
|
||||
MOUSEOVER = 'mouseover',
|
||||
MOUSEMOVE = 'mousemove',
|
||||
MOUSELEAVE = 'mouseleave',
|
||||
DRAGSTART = 'dragstart',
|
||||
DRAGEND = 'dragend',
|
||||
DRAG = 'drag',
|
||||
DRAGENTER = 'dragenter',
|
||||
DRAGLEAVE = 'dragleave',
|
||||
DRAGOVER = 'dragover',
|
||||
DRAGOUT = 'dragout',
|
||||
DDROP = 'drop',
|
||||
KEYUP = 'keyup',
|
||||
KEYDOWN = 'keydown',
|
||||
WHEEL = 'wheel',
|
||||
FOCUS = 'focus',
|
||||
BLUR = 'blur',
|
||||
|
||||
// touch events
|
||||
TOUCHSTART = 'touchstart',
|
||||
TOUCHMOVE = 'touchmove',
|
||||
TOUCHEND = 'touchend',
|
||||
|
||||
// node events
|
||||
NODE_CONTEXTMENU = 'node:contextmenu',
|
||||
NODE_CLICK = 'node:click',
|
||||
NODE_DBLCLICK = 'node:dblclick',
|
||||
NODE_MOUSEDOWN = 'node:mousedown',
|
||||
NODE_MOUSEUP = 'node:mouseup',
|
||||
NODE_MOUSEENTER = 'node:mouseenter',
|
||||
NODE_MOUSELEAVE = 'node:mouseleave',
|
||||
NODE_MOUSEMOVE = 'node:mousemove',
|
||||
NODE_MOUSEOUT = 'node:mouseout',
|
||||
NODE_MOUSEOVER = 'node:mouseover',
|
||||
NODE_DROP = 'node:drop',
|
||||
NODE_DRAGOVER = 'node:dragover',
|
||||
NODE_DRAGENTER = 'node:dragenter',
|
||||
NODE_DRAGLEAVE = 'node:dragleave',
|
||||
NODE_DRAGSTART = 'node:dragstart',
|
||||
NODE_DRAG = 'node:drag',
|
||||
NODE_DRAGEND = 'node:dragend',
|
||||
// combo, extends from nodes
|
||||
COMBO_CONTEXTMENU = 'combo:contextmenu',
|
||||
COMBO_CLICK = 'combo:click',
|
||||
COMBO_DBLCLICK = 'combo:dblclick',
|
||||
COMBO_MOUSEDOWN = 'combo:mousedown',
|
||||
COMBO_MOUSEUP = 'combo:mouseup',
|
||||
COMBO_MOUSEENTER = 'combo:mouseenter',
|
||||
COMBO_MOUSELEAVE = 'combo:mouseleave',
|
||||
COMBO_MOUSEMOVE = 'combo:mousemove',
|
||||
COMBO_MOUSEOUT = 'combo:mouseout',
|
||||
COMBO_MOUSEOVER = 'combo:mouseover',
|
||||
COMBO_DROP = 'combo:drop',
|
||||
COMBO_DRAGOVER = 'combo:dragover',
|
||||
COMBO_DRAGENTER = 'combo:dragenter',
|
||||
COMBO_DRAGLEAVE = 'combo:dragleave',
|
||||
COMBO_DRAGSTART = 'combo:dragstart',
|
||||
COMBO_DRAG = 'combo:drag',
|
||||
COMBO_DRAGEND = 'combo:dragend',
|
||||
|
||||
// edge events
|
||||
EDGE_CONTEXTMENU = 'edge:contextmenu',
|
||||
EDGE_CLICK = 'edge:click',
|
||||
EDGE_DBLCLICK = 'edge:dblclick',
|
||||
EDGE_MOUSEDOWN = 'edge:mousedown',
|
||||
EDGE_MOUSEUP = 'edge:mouseup',
|
||||
EDGE_MOUSEENTER = 'edge:mouseenter',
|
||||
EDGE_MOUSELEAVE = 'edge:mouseleave',
|
||||
EDGE_MOUSEMOVE = 'edge:mousemove',
|
||||
EDGE_MOUSEOUT = 'edge:mouseout',
|
||||
EDGE_MOUSEOVER = 'edge:mouseover',
|
||||
EDGE_DROP = 'edge:drop',
|
||||
EDGE_DRAGOVER = 'edge:dragover',
|
||||
EDGE_DRAGENTER = 'edge:dragenter',
|
||||
EDGE_DRAGLEAVE = 'edge:dragleave',
|
||||
|
||||
// canvas events
|
||||
CANVAS_CONTEXTMENU = 'canvas:contextmenu',
|
||||
CANVAS_CLICK = 'canvas:click',
|
||||
CANVAS_DBLCLICK = 'canvas:dblclick',
|
||||
CANVAS_MOUSEDOWN = 'canvas:mousedown',
|
||||
CANVAS_MOUSEUP = 'canvas:mouseup',
|
||||
CANVAS_MOUSEENTER = 'canvas:mouseenter',
|
||||
CANVAS_MOUSELEAVE = 'canvas:mouseleave',
|
||||
CANVAS_MOUSEMOVE = 'canvas:mousemove',
|
||||
CANVAS_MOUSEOUT = 'canvas:mouseout',
|
||||
CANVAS_MOUSEOVER = 'canvas:mouseover',
|
||||
CANVAS_DROP = 'canvas:drop',
|
||||
CANVAS_DRAGENTER = 'canvas:dragenter',
|
||||
CANVAS_DRAGLEAVE = 'canvas:dragleave',
|
||||
CANVAS_DRAGSTART = 'canvas:dragstart',
|
||||
CANVAS_DRAG = 'canvas:drag',
|
||||
CANVAS_DRAGEND = 'canvas:dragend',
|
||||
|
||||
// timing events
|
||||
BEFORERENDER = 'beforerender',
|
||||
AFTERRENDER = 'afterrender',
|
||||
BEFOREADDITEM = 'beforeadditem',
|
||||
AFTERADDITEM = 'afteradditem',
|
||||
BEFOREREMOVEITEM = 'beforeremoveitem',
|
||||
AFTERREMOVEITEM = 'afterremoveitem',
|
||||
BEFOREUPDATEITEM = 'beforeupdateitem',
|
||||
AFTERUPDATEITEM = 'afterupdateitem',
|
||||
BEFOREITEMVISIBILITYCHANGE = 'beforeitemvisibilitychange',
|
||||
AFTERITEMVISIBILITYCHANGE = 'afteritemvisibilitychange',
|
||||
BEFOREITEMSTATECHANGE = 'beforeitemstatechange',
|
||||
AFTERITEMSTATECHANGE = 'afteritemstatechange',
|
||||
BEFOREITEMREFRESH = 'beforeitemrefresh',
|
||||
AFTERITEMREFRESH = 'afteritemrefresh',
|
||||
BEFOREITEMSTATESCLEAR = 'beforeitemstatesclear',
|
||||
AFTERITEMSTATESCLEAR = 'afteritemstatesclear',
|
||||
BEFOREMODECHANGE = 'beforemodechange',
|
||||
AFTERMODECHANGE = 'aftermodechange',
|
||||
BEFORELAYOUT = 'beforelayout',
|
||||
AFTERLAYOUT = 'afterlayout',
|
||||
BEFORECREATEEDGE = 'beforecreateedge',
|
||||
AFTERCREATEEDGE = 'aftercreateedge',
|
||||
BEFOREGRAPHREFRESHPOSITION = 'beforegraphrefreshposition',
|
||||
AFTERGRAPHREFRESHPOSITION = 'aftergraphrefreshposition',
|
||||
BEFOREGRAPHREFRESH = 'beforegraphrefresh',
|
||||
AFTERGRAPHREFRESH = 'aftergraphrefresh',
|
||||
BEFOREANIMATE = 'beforeanimate',
|
||||
AFTERANIMATE = 'afteranimate',
|
||||
BEFOREPAINT = 'beforepaint',
|
||||
AFTERPAINT = 'afterpaint',
|
||||
|
||||
GRAPHSTATECHANGE = 'graphstatechange',
|
||||
AFTERACTIVATERELATIONS = 'afteractivaterelations',
|
||||
NODESELECTCHANGE = 'nodeselectchange',
|
||||
TOOLTIPCHANGE = 'tooltipchange',
|
||||
WHEELZOOM = 'wheelzoom',
|
||||
VIEWPORTCHANGE = 'viewportchange',
|
||||
DRAGNODEEND = 'dragnodeend',
|
||||
STACKCHANGE = 'stackchange'
|
||||
}
|
||||
|
||||
|
||||
export type DefaultBehaviorType = IG6GraphEvent | string | number | object;
|
||||
|
||||
export interface BehaviorOption {
|
||||
getEvents(): {
|
||||
[key in G6Event]?: string;
|
||||
};
|
||||
getDefaultCfg?(): object;
|
||||
shouldBegin?(e?: IG6GraphEvent): boolean;
|
||||
shouldUpdate?(e?: IG6GraphEvent): boolean;
|
||||
shouldEnd?(e?: IG6GraphEvent): boolean;
|
||||
bind?(e: IGraph): void;
|
||||
unbind?(e: IGraph): void;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export type IEvent = Record<G6Event, string>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export type ITEM_TYPE = 'node' | 'edge' | 'combo' | 'vedge';
|
||||
|
||||
export type NodeIdxMap = {
|
||||
[key: string]: number;
|
||||
};
|
||||
|
||||
// 触发 viewportchange 事件的参数
|
||||
export interface ViewPortEventParam {
|
||||
action: string;
|
||||
matrix: Matrix;
|
||||
}
|
||||
|
||||
export interface Indexable<T> {
|
||||
[key: string]: T;
|
||||
}
|
||||
|
||||
// 图算法回调方法接口定义
|
||||
export interface IAlgorithmCallbacks {
|
||||
enter?: (param: { current: INode; previous: INode }) => void;
|
||||
leave?: (param: { current: INode; previous?: INode }) => void;
|
||||
allowTraversal?: (param: { previous?: INode; current?: INode; next: INode }) => boolean;
|
||||
}
|
||||
|
||||
// 栈中数据格式
|
||||
export interface StackData {
|
||||
action: string;
|
||||
data: GraphData;
|
||||
}
|
||||
|
||||
export interface BubblesetCfg {
|
||||
morphBuffer?: number; // DEFAULT_NODE_R0; the amount of space to move the virtual edge when wrapping around obstacles
|
||||
pixelGroupSize?: number; // the resolution of the algorithm in square pixels, 4 by default
|
||||
maxMarchingIterations?: number; // number of times to refine the boundary, 100 by default
|
||||
maxRoutingIterations?: number; // number of times to run the algorithm to refine the path finding in difficult areas
|
||||
nodeR0?: number; // the distance from nodes which energy is 1 (full influence)
|
||||
nodeR1?: number; // the distance from nodes at which energy is 0 (no influence)
|
||||
edgeR0?: number; // the distance from edges at which energy is 1 (full influence)
|
||||
edgeR1?: number; // the distance from edges at which energy is 0 (no influence)
|
||||
nodeInfluenceFactor?: number; // node influence factor
|
||||
negativeNodeInfluenceFactor?: number; // negativeNode influence factor
|
||||
edgeInfluenceFactor?: number; // edge influence factor
|
||||
memberInfluenceFactor?: number; // member influence factor
|
||||
nonMemberInfluenceFactor?: number; // nonMember influence factor
|
||||
}
|
||||
|
||||
export type TimeBarType = 'trend' | 'simple' | 'tick'
|
||||
|
||||
export type WaterMarkerConfig = Partial<{
|
||||
// 水印 canvas 容器的宽高
|
||||
width: number;
|
||||
height: number;
|
||||
compatible: boolean;
|
||||
text: {
|
||||
x?: number;
|
||||
y?: number;
|
||||
lineHeight?: number;
|
||||
rotate?: number;
|
||||
fontSize?: number;
|
||||
fontFamily?: string;
|
||||
fill?: string;
|
||||
baseline?: string;
|
||||
},
|
||||
image: {
|
||||
x?: number;
|
||||
y?: number;
|
||||
// 图片的宽高
|
||||
width?: number;
|
||||
height: number;
|
||||
rotate?: number;
|
||||
}
|
||||
}>
|
||||
export interface HullCfg {
|
||||
id: string;
|
||||
members?: Item[] | string[]; // 节点实例或节点 Id 数组
|
||||
nonMembers?: Item[] | string[];
|
||||
group?: IGroup;
|
||||
type?: string; // 'round-convex'(圆角凸包) /'smooth-convex'(平滑凸包) / 'bubble'(平滑凹包),
|
||||
padding?: number; // 轮廓边缘和内部成员间距
|
||||
style?: {
|
||||
fill?: string;
|
||||
stroke?: string;
|
||||
opacity?: number;
|
||||
};
|
||||
bubbleCfg?: BubblesetCfg; // 用于更精细控制bubble的效果(点和边轮廓的松弛程度,轮廓粒度),一般不需要配置
|
||||
}
|
207
packages/core/src/util/base.ts
Normal file
207
packages/core/src/util/base.ts
Normal file
@ -0,0 +1,207 @@
|
||||
import { isString, isNumber, isNil, isArray } from '@antv/util';
|
||||
import { G6GraphEvent } from '../interface/behavior';
|
||||
import { IG6GraphEvent, Padding, Matrix, Item } from '../types';
|
||||
|
||||
/**
|
||||
* turn padding into [top, right, bottom, right]
|
||||
* @param {Number|Array} padding input padding
|
||||
* @return {array} output
|
||||
*/
|
||||
export const formatPadding = (padding: Padding): number[] => {
|
||||
let top = 0;
|
||||
let left = 0;
|
||||
let right = 0;
|
||||
let bottom = 0;
|
||||
|
||||
if (isNumber(padding)) {
|
||||
top = left = right = bottom = padding;
|
||||
} else if (isString(padding)) {
|
||||
const intPadding = parseInt(padding, 10);
|
||||
top = left = right = bottom = intPadding;
|
||||
} else if (isArray(padding)) {
|
||||
top = padding[0];
|
||||
right = !isNil(padding[1]) ? padding[1] : padding[0];
|
||||
bottom = !isNil(padding[2]) ? padding[2] : padding[0];
|
||||
left = !isNil(padding[3]) ? padding[3] : right;
|
||||
}
|
||||
return [top, right, bottom, left];
|
||||
};
|
||||
|
||||
/**
|
||||
* clone event
|
||||
* @param e
|
||||
*/
|
||||
export const cloneEvent = (e: IG6GraphEvent): IG6GraphEvent => {
|
||||
const event = new G6GraphEvent(e.type, e);
|
||||
event.clientX = e.clientX;
|
||||
event.clientY = e.clientY;
|
||||
event.x = e.x;
|
||||
event.y = e.y;
|
||||
event.target = e.target;
|
||||
event.currentTarget = e.currentTarget;
|
||||
event.bubbles = true;
|
||||
(event.item as Item | null) = e.item;
|
||||
return event;
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断 viewport 是否改变,通过和单位矩阵对比
|
||||
* @param matrix Viewport 的 Matrix
|
||||
*/
|
||||
export const isViewportChanged = (matrix: Matrix) => {
|
||||
// matrix 为 null, 则说明没有变化
|
||||
if (!matrix) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const MATRIX_LEN = 9;
|
||||
const ORIGIN_MATRIX = [1, 0, 0, 0, 1, 0, 0, 0, 1];
|
||||
|
||||
for (let i = 0; i < MATRIX_LEN; i++) {
|
||||
if (matrix[i] !== ORIGIN_MATRIX[i]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const isNaN = (input: any) => Number.isNaN(Number(input));
|
||||
|
||||
/**
|
||||
* 计算一组 Item 的 BBox
|
||||
* @param items 选中的一组Item,可以是 node 或 combo
|
||||
*/
|
||||
export const calculationItemsBBox = (items: Item[]) => {
|
||||
let minx = Infinity;
|
||||
let maxx = -Infinity;
|
||||
let miny = Infinity;
|
||||
let maxy = -Infinity;
|
||||
|
||||
// 获取已节点的所有最大最小x y值
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const element = items[i];
|
||||
const bbox = element.getBBox();
|
||||
const { minX, minY, maxX, maxY } = bbox;
|
||||
if (minX < minx) {
|
||||
minx = minX;
|
||||
}
|
||||
|
||||
if (minY < miny) {
|
||||
miny = minY;
|
||||
}
|
||||
|
||||
if (maxX > maxx) {
|
||||
maxx = maxX;
|
||||
}
|
||||
|
||||
if (maxY > maxy) {
|
||||
maxy = maxY;
|
||||
}
|
||||
}
|
||||
|
||||
const x = Math.floor(minx);
|
||||
const y = Math.floor(miny);
|
||||
const width = Math.ceil(maxx) - Math.floor(minx);
|
||||
const height = Math.ceil(maxy) - Math.floor(miny);
|
||||
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
minX: minx,
|
||||
minY: miny,
|
||||
maxX: maxx,
|
||||
maxY: maxy,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 若 edges 中存在两端点相同的边,使用 quadratic 边并自动计算 curveOffset 使它们不相互重叠
|
||||
* @param edges 边数据集合
|
||||
* @param offsetDiff 相邻两边的 offset 之差
|
||||
*/
|
||||
export const processParallelEdges = (
|
||||
edges,
|
||||
offsetDiff = 15,
|
||||
multiEdgeType: string = 'quadratic',
|
||||
singleEdgeType: string = 'line',
|
||||
) => {
|
||||
const len = edges.length;
|
||||
const cod = offsetDiff * 2;
|
||||
const loopPosition = [
|
||||
'top',
|
||||
'top-right',
|
||||
'right',
|
||||
'bottom-right',
|
||||
'bottom',
|
||||
'bottom-left',
|
||||
'left',
|
||||
'top-left',
|
||||
];
|
||||
const edgeMap = {};
|
||||
const tags = [];
|
||||
const reverses = {};
|
||||
for (let i = 0; i < len; i++) {
|
||||
const edge = edges[i];
|
||||
const { source, target } = edge;
|
||||
const sourceTarget = `${source}-${target}`;
|
||||
|
||||
if (tags[i]) continue;
|
||||
if (!edgeMap[sourceTarget]) {
|
||||
edgeMap[sourceTarget] = [];
|
||||
}
|
||||
tags[i] = true;
|
||||
edgeMap[sourceTarget].push(edge);
|
||||
for (let j = 0; j < len; j++) {
|
||||
if (i === j) continue;
|
||||
const sedge = edges[j];
|
||||
const src = sedge.source;
|
||||
const dst = sedge.target;
|
||||
|
||||
// 两个节点之间共同的边
|
||||
// 第一条的source = 第二条的target
|
||||
// 第一条的target = 第二条的source
|
||||
if (!tags[j]) {
|
||||
if (source === dst && target === src) {
|
||||
edgeMap[sourceTarget].push(sedge);
|
||||
tags[j] = true;
|
||||
reverses[`${src}|${dst}|${edgeMap[sourceTarget].length - 1}`] = true;
|
||||
} else if (source === src && target === dst) {
|
||||
edgeMap[sourceTarget].push(sedge);
|
||||
tags[j] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const key in edgeMap) {
|
||||
const arcEdges = edgeMap[key];
|
||||
const { length } = arcEdges;
|
||||
for (let k = 0; k < length; k++) {
|
||||
const current = arcEdges[k];
|
||||
if (current.source === current.target) {
|
||||
current.type = 'loop';
|
||||
// 超过8条自环边,则需要重新处理
|
||||
current.loopCfg = {
|
||||
position: loopPosition[k % 8],
|
||||
dist: Math.floor(k / 8) * 20 + 50,
|
||||
};
|
||||
continue;
|
||||
}
|
||||
if (length === 1) {
|
||||
current.type = singleEdgeType;
|
||||
continue;
|
||||
}
|
||||
current.type = multiEdgeType;
|
||||
const sign =
|
||||
(k % 2 === 0 ? 1 : -1) * (reverses[`${current.source}|${current.target}|${k}`] ? -1 : 1);
|
||||
if (length % 2 === 1) {
|
||||
current.curveOffset = sign * Math.ceil(k / 2) * cod;
|
||||
} else {
|
||||
current.curveOffset = sign * (Math.floor(k / 2) * cod + offsetDiff);
|
||||
}
|
||||
}
|
||||
}
|
||||
return edges;
|
||||
};
|
742
packages/core/src/util/graphic.ts
Normal file
742
packages/core/src/util/graphic.ts
Normal file
@ -0,0 +1,742 @@
|
||||
import { Group } from '@antv/g-canvas';
|
||||
import Path from '@antv/g-canvas/lib/shape/path';
|
||||
import { vec2 } from '@antv/matrix-util';
|
||||
import Global from '../global';
|
||||
import {
|
||||
EdgeData,
|
||||
IBBox,
|
||||
IPoint,
|
||||
IShapeBase,
|
||||
LabelStyle,
|
||||
TreeGraphData,
|
||||
NodeConfig,
|
||||
ComboTree,
|
||||
ComboConfig,
|
||||
} from '../types';
|
||||
import { applyMatrix } from './math';
|
||||
import letterAspectRatio from './letterAspectRatio';
|
||||
import { isString, clone } from '@antv/util';
|
||||
import { BBox } from '@antv/g-math/lib/types';
|
||||
import { IGraph } from '../interface/graph';
|
||||
|
||||
const { PI, sin, cos } = Math;
|
||||
|
||||
// 一共支持8个方向的自环,每个环占的角度是45度,在计算时再二分,为22.5度
|
||||
const SELF_LINK_SIN: number = sin(PI / 8);
|
||||
const SELF_LINK_COS: number = cos(PI / 8);
|
||||
|
||||
export const getBBox = (element: IShapeBase, group: Group): IBBox => {
|
||||
const bbox = element.getBBox();
|
||||
let leftTop: IPoint = {
|
||||
x: bbox.minX,
|
||||
y: bbox.minY,
|
||||
};
|
||||
let rightBottom: IPoint = {
|
||||
x: bbox.maxX,
|
||||
y: bbox.maxY,
|
||||
};
|
||||
// 根据父元素变换矩阵
|
||||
if (group) {
|
||||
let matrix = group.getMatrix();
|
||||
if (!matrix) {
|
||||
matrix = [1, 0, 0, 0, 1, 0, 0, 0, 1];
|
||||
}
|
||||
leftTop = applyMatrix(leftTop, matrix);
|
||||
rightBottom = applyMatrix(rightBottom, matrix);
|
||||
}
|
||||
|
||||
const { x: lx, y: ly } = leftTop;
|
||||
const { x: rx, y: ry } = rightBottom;
|
||||
|
||||
return {
|
||||
x: lx,
|
||||
y: ly,
|
||||
minX: lx,
|
||||
minY: ly,
|
||||
maxX: rx,
|
||||
maxY: ry,
|
||||
width: rx - lx,
|
||||
height: ry - ly,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* get loop edge config
|
||||
* @param cfg edge config
|
||||
*/
|
||||
export const getLoopCfgs = (cfg: EdgeData): EdgeData => {
|
||||
const item = cfg.sourceNode || cfg.targetNode;
|
||||
const container: Group = item.get('group');
|
||||
let containerMatrix = container.getMatrix();
|
||||
if (!containerMatrix) containerMatrix = [1, 0, 0, 0, 1, 0, 0, 0, 1];
|
||||
|
||||
const keyShape: IShapeBase = item.getKeyShape();
|
||||
const bbox: IBBox = keyShape.getBBox();
|
||||
|
||||
const loopCfg = cfg.loopCfg || {};
|
||||
// 距离keyShape边的最高距离
|
||||
const dist: number = loopCfg.dist || Math.max(bbox.width, bbox.height) * 2;
|
||||
// 自环边与keyShape的相对位置关系
|
||||
const position: string = loopCfg.position || Global.defaultLoopPosition;
|
||||
|
||||
// 中心取group上真实位置
|
||||
const center = [containerMatrix[6], containerMatrix[7]];
|
||||
let startPoint = [cfg.startPoint.x, cfg.startPoint.y];
|
||||
let endPoint = [cfg.endPoint.x, cfg.endPoint.y];
|
||||
|
||||
let rstart = bbox.height / 2;
|
||||
let rend = bbox.height / 2;
|
||||
let sinDeltaStart = rstart * SELF_LINK_SIN;
|
||||
let cosDeltaStart = rstart * SELF_LINK_COS;
|
||||
let sinDeltaEnd = rend * SELF_LINK_SIN;
|
||||
let cosDeltaEnd = rend * SELF_LINK_COS;
|
||||
|
||||
// 如果定义了锚点的,直接用锚点坐标,否则,根据自环的 cfg 计算
|
||||
if (startPoint[0] === endPoint[0] && startPoint[1] === endPoint[1]) {
|
||||
switch (position) {
|
||||
case 'top':
|
||||
startPoint = [center[0] - sinDeltaStart, center[1] - cosDeltaStart];
|
||||
endPoint = [center[0] + sinDeltaEnd, center[1] - cosDeltaEnd];
|
||||
break;
|
||||
case 'top-right':
|
||||
rstart = bbox.height / 2;
|
||||
rend = bbox.width / 2;
|
||||
sinDeltaStart = rstart * SELF_LINK_SIN;
|
||||
cosDeltaStart = rstart * SELF_LINK_COS;
|
||||
sinDeltaEnd = rend * SELF_LINK_SIN;
|
||||
cosDeltaEnd = rend * SELF_LINK_COS;
|
||||
startPoint = [center[0] + sinDeltaStart, center[1] - cosDeltaStart];
|
||||
endPoint = [center[0] + cosDeltaEnd, center[1] - sinDeltaEnd];
|
||||
break;
|
||||
case 'right':
|
||||
rstart = bbox.width / 2;
|
||||
rend = bbox.width / 2;
|
||||
sinDeltaStart = rstart * SELF_LINK_SIN;
|
||||
cosDeltaStart = rstart * SELF_LINK_COS;
|
||||
sinDeltaEnd = rend * SELF_LINK_SIN;
|
||||
cosDeltaEnd = rend * SELF_LINK_COS;
|
||||
startPoint = [center[0] + cosDeltaStart, center[1] - sinDeltaStart];
|
||||
endPoint = [center[0] + cosDeltaEnd, center[1] + sinDeltaEnd];
|
||||
break;
|
||||
case 'bottom-right':
|
||||
rstart = bbox.width / 2;
|
||||
rend = bbox.height / 2;
|
||||
sinDeltaStart = rstart * SELF_LINK_SIN;
|
||||
cosDeltaStart = rstart * SELF_LINK_COS;
|
||||
sinDeltaEnd = rend * SELF_LINK_SIN;
|
||||
cosDeltaEnd = rend * SELF_LINK_COS;
|
||||
startPoint = [center[0] + cosDeltaStart, center[1] + sinDeltaStart];
|
||||
endPoint = [center[0] + sinDeltaEnd, center[1] + cosDeltaEnd];
|
||||
break;
|
||||
case 'bottom':
|
||||
rstart = bbox.height / 2;
|
||||
rend = bbox.height / 2;
|
||||
sinDeltaStart = rstart * SELF_LINK_SIN;
|
||||
cosDeltaStart = rstart * SELF_LINK_COS;
|
||||
sinDeltaEnd = rend * SELF_LINK_SIN;
|
||||
cosDeltaEnd = rend * SELF_LINK_COS;
|
||||
startPoint = [center[0] + sinDeltaStart, center[1] + cosDeltaStart];
|
||||
endPoint = [center[0] - sinDeltaEnd, center[1] + cosDeltaEnd];
|
||||
break;
|
||||
case 'bottom-left':
|
||||
rstart = bbox.height / 2;
|
||||
rend = bbox.width / 2;
|
||||
sinDeltaStart = rstart * SELF_LINK_SIN;
|
||||
cosDeltaStart = rstart * SELF_LINK_COS;
|
||||
sinDeltaEnd = rend * SELF_LINK_SIN;
|
||||
cosDeltaEnd = rend * SELF_LINK_COS;
|
||||
startPoint = [center[0] - sinDeltaStart, center[1] + cosDeltaStart];
|
||||
endPoint = [center[0] - cosDeltaEnd, center[1] + sinDeltaEnd];
|
||||
break;
|
||||
case 'left':
|
||||
rstart = bbox.width / 2;
|
||||
rend = bbox.width / 2;
|
||||
sinDeltaStart = rstart * SELF_LINK_SIN;
|
||||
cosDeltaStart = rstart * SELF_LINK_COS;
|
||||
sinDeltaEnd = rend * SELF_LINK_SIN;
|
||||
cosDeltaEnd = rend * SELF_LINK_COS;
|
||||
startPoint = [center[0] - cosDeltaStart, center[1] + sinDeltaStart];
|
||||
endPoint = [center[0] - cosDeltaEnd, center[1] - sinDeltaEnd];
|
||||
break;
|
||||
case 'top-left':
|
||||
rstart = bbox.width / 2;
|
||||
rend = bbox.height / 2;
|
||||
sinDeltaStart = rstart * SELF_LINK_SIN;
|
||||
cosDeltaStart = rstart * SELF_LINK_COS;
|
||||
sinDeltaEnd = rend * SELF_LINK_SIN;
|
||||
cosDeltaEnd = rend * SELF_LINK_COS;
|
||||
startPoint = [center[0] - cosDeltaStart, center[1] - sinDeltaStart];
|
||||
endPoint = [center[0] - sinDeltaEnd, center[1] - cosDeltaEnd];
|
||||
break;
|
||||
default:
|
||||
rstart = bbox.width / 2;
|
||||
rend = bbox.width / 2;
|
||||
sinDeltaStart = rstart * SELF_LINK_SIN;
|
||||
cosDeltaStart = rstart * SELF_LINK_COS;
|
||||
sinDeltaEnd = rend * SELF_LINK_SIN;
|
||||
cosDeltaEnd = rend * SELF_LINK_COS;
|
||||
startPoint = [center[0] - sinDeltaStart, center[1] - cosDeltaStart];
|
||||
endPoint = [center[0] + sinDeltaEnd, center[1] - cosDeltaEnd];
|
||||
}
|
||||
// 如果逆时针画,交换起点和终点
|
||||
if (loopCfg.clockwise === false) {
|
||||
const swap = [startPoint[0], startPoint[1]];
|
||||
startPoint = [endPoint[0], endPoint[1]];
|
||||
endPoint = [swap[0], swap[1]];
|
||||
}
|
||||
}
|
||||
|
||||
const startVec = [startPoint[0] - center[0], startPoint[1] - center[1]];
|
||||
let scaleRateStart = (rstart + dist) / rstart;
|
||||
let scaleRateEnd = (rend + dist) / rend;
|
||||
if (loopCfg.clockwise === false) {
|
||||
scaleRateStart = (rend + dist) / rend;
|
||||
scaleRateEnd = (rstart + dist) / rstart;
|
||||
}
|
||||
|
||||
const startExtendVec = vec2.scale([], startVec, scaleRateStart);
|
||||
const controlPoint1 = [center[0] + startExtendVec[0], center[1] + startExtendVec[1]];
|
||||
const endVec = [endPoint[0] - center[0], endPoint[1] - center[1]];
|
||||
|
||||
const endExtendVec = vec2.scale([], endVec, scaleRateEnd);
|
||||
const controlPoint2 = [center[0] + endExtendVec[0], center[1] + endExtendVec[1]];
|
||||
|
||||
cfg.startPoint = { x: startPoint[0], y: startPoint[1] };
|
||||
cfg.endPoint = { x: endPoint[0], y: endPoint[1] };
|
||||
cfg.controlPoints = [
|
||||
{ x: controlPoint1[0], y: controlPoint1[1] },
|
||||
{ x: controlPoint2[0], y: controlPoint2[1] },
|
||||
];
|
||||
return cfg;
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据 label 所在线条的位置百分比,计算 label 坐标
|
||||
* @param {object} pathShape G 的 path 实例,一般是 Edge 实例的 keyShape
|
||||
* @param {number} percent 范围 0 - 1 的线条百分比
|
||||
* @param {number} refX x 轴正方向为基准的 label 偏移
|
||||
* @param {number} refY y 轴正方向为基准的 label 偏移
|
||||
* @param {boolean} rotate 是否根据线条斜率旋转文本
|
||||
* @return {object} 文本的 x, y, 文本的旋转角度
|
||||
*/
|
||||
export const getLabelPosition = (
|
||||
pathShape: Path,
|
||||
percent: number,
|
||||
refX: number,
|
||||
refY: number,
|
||||
rotate: boolean,
|
||||
): LabelStyle => {
|
||||
const TAN_OFFSET = 0.0001;
|
||||
let vector: number[][] = [];
|
||||
const point: IPoint = pathShape.getPoint(percent);
|
||||
if (point === null) {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
angle: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// 头尾最可能,放在最前面,使用 g path 上封装的方法
|
||||
if (percent < TAN_OFFSET) {
|
||||
vector = pathShape.getStartTangent().reverse();
|
||||
} else if (percent > 1 - TAN_OFFSET) {
|
||||
vector = pathShape.getEndTangent();
|
||||
} else {
|
||||
// 否则取指定位置的点,与少量偏移的点,做微分向量
|
||||
const offsetPoint: IPoint = pathShape.getPoint(percent + TAN_OFFSET);
|
||||
vector.push([point.x, point.y]);
|
||||
vector.push([offsetPoint.x, offsetPoint.y]);
|
||||
}
|
||||
|
||||
let rad: number = Math.atan2(vector[1][1] - vector[0][1], vector[1][0] - vector[0][0]);
|
||||
|
||||
if (rad < 0) {
|
||||
rad += PI * 2;
|
||||
}
|
||||
|
||||
if (refX) {
|
||||
point.x += cos(rad) * refX;
|
||||
point.y += sin(rad) * refX;
|
||||
}
|
||||
if (refY) {
|
||||
// 默认方向是 x 轴正方向,法线是 求出角度 - 90°
|
||||
let normal = rad - PI / 2;
|
||||
// 若法线角度在 y 轴负方向,切到正方向,保证 refY 相对于 y 轴正方向
|
||||
if (rad > (1 / 2) * PI && rad < ((3 * 1) / 2) * PI) {
|
||||
normal -= PI;
|
||||
}
|
||||
point.x += cos(normal) * refY;
|
||||
point.y += sin(normal) * refY;
|
||||
}
|
||||
|
||||
const result = {
|
||||
x: point.x,
|
||||
y: point.y,
|
||||
angle: rad,
|
||||
};
|
||||
|
||||
if (rotate) {
|
||||
if (rad > (1 / 2) * PI && rad < ((3 * 1) / 2) * PI) {
|
||||
rad -= PI;
|
||||
}
|
||||
return {
|
||||
rotate: rad,
|
||||
...result,
|
||||
};
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* depth first traverse, from root to leaves, children in inverse order
|
||||
* if the fn returns false, terminate the traverse
|
||||
*/
|
||||
const traverse = <T extends { children?: T[] }>(data: T, fn: (param: T) => boolean) => {
|
||||
if (fn(data) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (data && data.children) {
|
||||
for (let i = data.children.length - 1; i >= 0; i--) {
|
||||
if (!traverse(data.children[i], fn)) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* depth first traverse, from leaves to root, children in inverse order
|
||||
* if the fn returns false, terminate the traverse
|
||||
*/
|
||||
const traverseUp = <T extends { children?: T[] }>(data: T, fn: (param: T) => boolean) => {
|
||||
if (data && data.children) {
|
||||
for (let i = data.children.length - 1; i >= 0; i--) {
|
||||
if (!traverseUp(data.children[i], fn)) return;
|
||||
}
|
||||
}
|
||||
|
||||
if (fn(data) === false) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* depth first traverse, from root to leaves, children in inverse order
|
||||
* if the fn returns false, terminate the traverse
|
||||
*/
|
||||
export const traverseTree = <T extends { children?: T[] }>(data: T, fn: (param: T) => boolean) => {
|
||||
if (typeof fn !== 'function') {
|
||||
return;
|
||||
}
|
||||
traverse(data, fn);
|
||||
};
|
||||
|
||||
/**
|
||||
* depth first traverse, from leaves to root, children in inverse order
|
||||
* if the fn returns false, terminate the traverse
|
||||
*/
|
||||
export const traverseTreeUp = <T extends { children?: T[] }>(
|
||||
data: T,
|
||||
fn: (param: T) => boolean,
|
||||
) => {
|
||||
if (typeof fn !== 'function') {
|
||||
return;
|
||||
}
|
||||
traverseUp(data, fn);
|
||||
};
|
||||
|
||||
export type TreeGraphDataWithPosition = TreeGraphData & {
|
||||
x: number;
|
||||
y: number;
|
||||
children?: TreeGraphDataWithPosition[];
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param data Tree graph data
|
||||
* @param layout
|
||||
*/
|
||||
export const radialLayout = (
|
||||
data: TreeGraphDataWithPosition,
|
||||
layout?: string,
|
||||
): TreeGraphDataWithPosition => {
|
||||
// 布局方式有 H / V / LR / RL / TB / BT
|
||||
const VERTICAL_LAYOUTS: string[] = ['V', 'TB', 'BT'];
|
||||
const min: IPoint = {
|
||||
x: Infinity,
|
||||
y: Infinity,
|
||||
};
|
||||
|
||||
const max: IPoint = {
|
||||
x: -Infinity,
|
||||
y: -Infinity,
|
||||
};
|
||||
// 默认布局是垂直布局TB,此时x对应rad,y对应r
|
||||
let rScale: 'x' | 'y' = 'x';
|
||||
let radScale: 'x' | 'y' = 'y';
|
||||
if (layout && VERTICAL_LAYOUTS.indexOf(layout) >= 0) {
|
||||
// 若是水平布局,y对应rad,x对应r
|
||||
radScale = 'x';
|
||||
rScale = 'y';
|
||||
}
|
||||
let count = 0;
|
||||
traverseTree(data, (node: TreeGraphDataWithPosition) => {
|
||||
count++;
|
||||
if (node.x > max.x) {
|
||||
max.x = node.x;
|
||||
}
|
||||
if (node.x < min.x) {
|
||||
min.x = node.x;
|
||||
}
|
||||
if (node.y > max.y) {
|
||||
max.y = node.y;
|
||||
}
|
||||
if (node.y < min.y) {
|
||||
min.y = node.y;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
const avgRad = (PI * 2) / count;
|
||||
const radDiff = max[radScale] - min[radScale];
|
||||
if (radDiff === 0) {
|
||||
return data;
|
||||
}
|
||||
|
||||
traverseTree(data, (node) => {
|
||||
const radial = ((node[radScale] - min[radScale]) / radDiff) * (PI * 2 - avgRad) + avgRad;
|
||||
const r = Math.abs(rScale === 'x' ? node.x - data.x : node.y - data.y);
|
||||
node.x = r * Math.cos(radial);
|
||||
node.y = r * Math.sin(radial);
|
||||
return true;
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param letter the letter
|
||||
* @param fontSize
|
||||
* @return the letter's width
|
||||
*/
|
||||
export const getLetterWidth = (letter, fontSize) => {
|
||||
return fontSize * (letterAspectRatio[letter] || 1);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param text the text
|
||||
* @param fontSize
|
||||
* @return the text's size
|
||||
*/
|
||||
export const getTextSize = (text, fontSize) => {
|
||||
let width = 0;
|
||||
const pattern = new RegExp('[\u{4E00}-\u{9FA5}]+');
|
||||
text.split('').forEach((letter) => {
|
||||
if (pattern.test(letter)) {
|
||||
// 中文字符
|
||||
width += fontSize;
|
||||
} else {
|
||||
width += getLetterWidth(letter, fontSize);
|
||||
}
|
||||
});
|
||||
return [width, fontSize];
|
||||
};
|
||||
|
||||
/**
|
||||
* construct the trees from combos data
|
||||
* @param array the combos array
|
||||
* @param nodes the nodes array
|
||||
* @return the tree
|
||||
*/
|
||||
export const plainCombosToTrees = (array: ComboConfig[], nodes?: NodeConfig[]) => {
|
||||
const result: ComboTree[] = [];
|
||||
const addedMap = {};
|
||||
const modelMap = {};
|
||||
array.forEach((d) => {
|
||||
modelMap[d.id] = d;
|
||||
});
|
||||
|
||||
array.forEach((d, i) => {
|
||||
const cd = clone(d);
|
||||
cd.itemType = 'combo';
|
||||
cd.children = undefined;
|
||||
if (cd.parentId === cd.id) {
|
||||
console.warn(`The parentId for combo ${cd.id} can not be the same as the combo's id`);
|
||||
delete cd.parentId;
|
||||
} else if (cd.parentId && !modelMap[cd.parentId]) {
|
||||
console.warn(`The parent combo for combo ${cd.id} does not exist!`);
|
||||
delete cd.parentId;
|
||||
}
|
||||
let mappedObj = addedMap[cd.id];
|
||||
if (mappedObj) {
|
||||
cd.children = mappedObj.children;
|
||||
addedMap[cd.id] = cd;
|
||||
mappedObj = cd;
|
||||
if (!mappedObj.parentId) {
|
||||
result.push(mappedObj);
|
||||
return;
|
||||
}
|
||||
const mappedParent = addedMap[mappedObj.parentId];
|
||||
if (mappedParent) {
|
||||
if (mappedParent.children) mappedParent.children.push(cd);
|
||||
else mappedParent.children = [cd];
|
||||
} else {
|
||||
const parent = {
|
||||
id: mappedObj.parentId,
|
||||
children: [mappedObj],
|
||||
};
|
||||
addedMap[mappedObj.parentId] = parent;
|
||||
addedMap[cd.id] = cd;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (isString(d.parentId)) {
|
||||
const parent = addedMap[d.parentId];
|
||||
if (parent) {
|
||||
if (parent.children) parent.children.push(cd);
|
||||
else parent.children = [cd];
|
||||
addedMap[cd.id] = cd;
|
||||
} else {
|
||||
const pa = {
|
||||
id: d.parentId,
|
||||
children: [cd],
|
||||
};
|
||||
addedMap[pa.id] = pa;
|
||||
addedMap[cd.id] = cd;
|
||||
}
|
||||
} else {
|
||||
result.push(cd);
|
||||
addedMap[cd.id] = cd;
|
||||
}
|
||||
});
|
||||
|
||||
// proccess the nodes
|
||||
const nodeMap = {};
|
||||
(nodes || []).forEach((node) => {
|
||||
nodeMap[node.id] = node;
|
||||
const combo = addedMap[node.comboId as string];
|
||||
if (combo) {
|
||||
const cnode: NodeConfig = {
|
||||
id: node.id,
|
||||
comboId: node.comboId as string,
|
||||
};
|
||||
if (combo.children) combo.children.push(cnode);
|
||||
else combo.children = [cnode];
|
||||
cnode.itemType = 'node';
|
||||
addedMap[node.id] = cnode;
|
||||
}
|
||||
});
|
||||
|
||||
// assign the depth for each element
|
||||
let maxDepth = 0;
|
||||
result.forEach((tree: ComboTree) => {
|
||||
tree.depth = maxDepth + 10;
|
||||
traverse<ComboTree>(tree, (child) => {
|
||||
let parent;
|
||||
const itemType = addedMap[child.id].itemType;
|
||||
if (itemType === 'node') {
|
||||
parent = addedMap[child.comboId as string];
|
||||
} else {
|
||||
parent = addedMap[child.parentId];
|
||||
}
|
||||
if (parent) {
|
||||
if (itemType === 'node') child.depth = maxDepth + 1;
|
||||
else child.depth = maxDepth + 10;
|
||||
} else {
|
||||
child.depth = maxDepth + 10;
|
||||
}
|
||||
if (maxDepth < child.depth) maxDepth = child.depth;
|
||||
const oriNodeModel = nodeMap[child.id];
|
||||
if (oriNodeModel) {
|
||||
oriNodeModel.depth = child.depth;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
export const reconstructTree = (
|
||||
trees: ComboTree[],
|
||||
subtreeId?: string,
|
||||
newParentId?: string | undefined,
|
||||
): ComboTree[] => {
|
||||
let brothers = trees;
|
||||
let subtree;
|
||||
const comboChildsMap = {
|
||||
root: {
|
||||
children: trees,
|
||||
},
|
||||
};
|
||||
let foundSubTree = false;
|
||||
let oldParentId = 'root';
|
||||
(trees || []).forEach((tree) => {
|
||||
if (foundSubTree) return;
|
||||
if (tree.id === subtreeId) {
|
||||
subtree = tree;
|
||||
if (tree.itemType === 'combo') {
|
||||
subtree.parentId = newParentId;
|
||||
} else {
|
||||
subtree.comboId = newParentId;
|
||||
}
|
||||
foundSubTree = true;
|
||||
return;
|
||||
}
|
||||
traverseTree<ComboTree>(tree, (child: any) => {
|
||||
comboChildsMap[child.id] = {
|
||||
children: child.children,
|
||||
};
|
||||
// store the old parent id to delete the subtree from the old parent's children in next recursion
|
||||
brothers = comboChildsMap[child.parentId || child.comboId || 'root'].children;
|
||||
if (child && (child.removed || subtreeId === child.id) && brothers) {
|
||||
oldParentId = child.parentId || child.comboId || 'root';
|
||||
subtree = child;
|
||||
// re-assign the parentId or comboId for the moved subtree
|
||||
if (child.itemType === 'combo') {
|
||||
subtree.parentId = newParentId;
|
||||
} else {
|
||||
subtree.comboId = newParentId;
|
||||
}
|
||||
foundSubTree = true;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
brothers = comboChildsMap[oldParentId].children;
|
||||
const index = brothers ? brothers.indexOf(subtree) : -1;
|
||||
if (index > -1) brothers.splice(index, 1);
|
||||
|
||||
// 如果遍历完整棵树还没有找到,说明之前就不在树中
|
||||
if (!foundSubTree) {
|
||||
subtree = {
|
||||
id: subtreeId,
|
||||
itemType: 'node',
|
||||
comboId: newParentId,
|
||||
};
|
||||
|
||||
comboChildsMap[subtreeId] = {
|
||||
children: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// append to new parent
|
||||
if (subtreeId) {
|
||||
let found = false;
|
||||
// newParentId is undefined means the subtree will have no parent
|
||||
if (newParentId) {
|
||||
let newParentDepth = 0;
|
||||
(trees || []).forEach((tree) => {
|
||||
if (found) return; // terminate
|
||||
traverseTree<ComboTree>(tree, (child: any) => {
|
||||
// append subtree to the new parent ans assign the depth to the subtree
|
||||
if (newParentId === child.id) {
|
||||
found = true;
|
||||
if (child.children) child.children.push(subtree);
|
||||
else child.children = [subtree];
|
||||
newParentDepth = child.depth;
|
||||
if (subtree.itemType === 'node') subtree.depth = newParentDepth + 2;
|
||||
else subtree.depth = newParentDepth + 1;
|
||||
return false; // terminate
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
} else if ((!newParentId || !found) && subtree.itemType !== 'node') {
|
||||
// if the newParentId is undefined or it is not found in the tree, add the subTree to the root
|
||||
trees.push(subtree);
|
||||
}
|
||||
// update the depth of the subtree and its children from the subtree
|
||||
let currentDepth = subtree.depth;
|
||||
traverseTree<ComboTree>(subtree, (child: any) => {
|
||||
if (child.itemType === 'node') currentDepth += 2;
|
||||
else currentDepth += 1;
|
||||
child.depth = currentDepth;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
return trees;
|
||||
};
|
||||
|
||||
export const getComboBBox = (children: ComboTree[], graph: IGraph): BBox => {
|
||||
const comboBBox = {
|
||||
minX: Infinity,
|
||||
minY: Infinity,
|
||||
maxX: -Infinity,
|
||||
maxY: -Infinity,
|
||||
x: undefined,
|
||||
y: undefined,
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
centerX: undefined,
|
||||
centerY: undefined,
|
||||
};
|
||||
|
||||
if (!children || children.length === 0) {
|
||||
return comboBBox;
|
||||
}
|
||||
|
||||
children.forEach((child) => {
|
||||
const childItem = graph.findById(child.id);
|
||||
if (!childItem || !childItem.isVisible()) return; // ignore hidden children
|
||||
childItem.set('bboxCanvasCache', undefined);
|
||||
const childBBox = childItem.getCanvasBBox();
|
||||
if (childBBox.x && comboBBox.minX > childBBox.minX) comboBBox.minX = childBBox.minX;
|
||||
if (childBBox.y && comboBBox.minY > childBBox.minY) comboBBox.minY = childBBox.minY;
|
||||
if (childBBox.x && comboBBox.maxX < childBBox.maxX) comboBBox.maxX = childBBox.maxX;
|
||||
if (childBBox.y && comboBBox.maxY < childBBox.maxY) comboBBox.maxY = childBBox.maxY;
|
||||
});
|
||||
comboBBox.x = (comboBBox.minX + comboBBox.maxX) / 2;
|
||||
comboBBox.y = (comboBBox.minY + comboBBox.maxY) / 2;
|
||||
comboBBox.width = comboBBox.maxX - comboBBox.minX;
|
||||
comboBBox.height = comboBBox.maxY - comboBBox.minY;
|
||||
|
||||
comboBBox.centerX = (comboBBox.minX + comboBBox.maxX) / 2;
|
||||
comboBBox.centerY = (comboBBox.minY + comboBBox.maxY) / 2;
|
||||
|
||||
Object.keys(comboBBox).forEach((key) => {
|
||||
if (comboBBox[key] === Infinity || comboBBox[key] === -Infinity) {
|
||||
comboBBox[key] = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
return comboBBox;
|
||||
};
|
||||
|
||||
export const getChartRegion = (
|
||||
params: {
|
||||
group: Group,
|
||||
width: number,
|
||||
height: number,
|
||||
x: number,
|
||||
y: number
|
||||
}) => {
|
||||
const {
|
||||
group,
|
||||
height,
|
||||
width,
|
||||
x,
|
||||
y
|
||||
} = params;
|
||||
const canvas = group.get('canvas');
|
||||
const canvasWidth = canvas.get('width');
|
||||
const canvasHeight = canvas.get('height');
|
||||
const region = {
|
||||
start: {
|
||||
x: 0,
|
||||
y: 0
|
||||
},
|
||||
end: {
|
||||
x: 0,
|
||||
y: 0
|
||||
}
|
||||
}
|
||||
region.start.x = x / canvasWidth;
|
||||
region.start.y = y / canvasHeight;
|
||||
region.end.x = (x + width) / canvasWidth;
|
||||
region.end.y = (y + height) / canvasHeight;
|
||||
return region;
|
||||
}
|
19
packages/core/src/util/index.ts
Normal file
19
packages/core/src/util/index.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import * as MathUtil from './math';
|
||||
import * as GraphicUtil from './graphic';
|
||||
import * as PathUtil from './path';
|
||||
import * as BaseUtil from './base';
|
||||
import { mat3, transform } from '@antv/matrix-util';
|
||||
import { mix, isNumber, deepMix, isArray, uniqueId } from '@antv/util';
|
||||
|
||||
const Base = {
|
||||
mat3,
|
||||
mix,
|
||||
deepMix,
|
||||
transform,
|
||||
isArray,
|
||||
isNumber,
|
||||
uniqueId
|
||||
};
|
||||
|
||||
const Util = { ...Base, ...BaseUtil, ...GraphicUtil, ...PathUtil, ...MathUtil };
|
||||
export default Util;
|
95
packages/core/src/util/letterAspectRatio.ts
Normal file
95
packages/core/src/util/letterAspectRatio.ts
Normal file
@ -0,0 +1,95 @@
|
||||
export default {
|
||||
' ': 0.3329986572265625,
|
||||
a: 0.5589996337890625,
|
||||
A: 0.6569992065429687,
|
||||
b: 0.58599853515625,
|
||||
B: 0.6769989013671875,
|
||||
c: 0.5469985961914062,
|
||||
C: 0.7279998779296875,
|
||||
d: 0.58599853515625,
|
||||
D: 0.705999755859375,
|
||||
e: 0.554998779296875,
|
||||
E: 0.63699951171875,
|
||||
f: 0.37299957275390627,
|
||||
F: 0.5769989013671875,
|
||||
g: 0.5909988403320312,
|
||||
G: 0.7479995727539063,
|
||||
h: 0.555999755859375,
|
||||
H: 0.7199996948242188,
|
||||
i: 0.255999755859375,
|
||||
I: 0.23699951171875,
|
||||
j: 0.26699981689453123,
|
||||
J: 0.5169998168945312,
|
||||
k: 0.5289993286132812,
|
||||
K: 0.6899993896484375,
|
||||
l: 0.23499908447265624,
|
||||
L: 0.5879989624023437,
|
||||
m: 0.854998779296875,
|
||||
M: 0.8819992065429687,
|
||||
n: 0.5589996337890625,
|
||||
N: 0.7189987182617188,
|
||||
o: 0.58599853515625,
|
||||
O: 0.7669998168945312,
|
||||
p: 0.58599853515625,
|
||||
P: 0.6419998168945312,
|
||||
q: 0.58599853515625,
|
||||
Q: 0.7669998168945312,
|
||||
r: 0.3649993896484375,
|
||||
R: 0.6759994506835938,
|
||||
s: 0.504998779296875,
|
||||
S: 0.6319992065429687,
|
||||
t: 0.354998779296875,
|
||||
T: 0.6189987182617187,
|
||||
u: 0.5599990844726562,
|
||||
U: 0.7139999389648437,
|
||||
v: 0.48199920654296874,
|
||||
V: 0.6389999389648438,
|
||||
w: 0.754998779296875,
|
||||
W: 0.929998779296875,
|
||||
x: 0.5089996337890625,
|
||||
X: 0.63699951171875,
|
||||
y: 0.4959991455078125,
|
||||
Y: 0.66199951171875,
|
||||
z: 0.48699951171875,
|
||||
Z: 0.6239990234375,
|
||||
'0': 0.6,
|
||||
'1': 0.40099945068359377,
|
||||
'2': 0.6,
|
||||
'3': 0.6,
|
||||
'4': 0.6,
|
||||
'5': 0.6,
|
||||
'6': 0.6,
|
||||
'7': 0.5469985961914062,
|
||||
'8': 0.6,
|
||||
'9': 0.6,
|
||||
'[': 0.3329986572265625,
|
||||
']': 0.3329986572265625,
|
||||
',': 0.26399993896484375,
|
||||
'.': 0.26399993896484375,
|
||||
';': 0.26399993896484375,
|
||||
':': 0.26399993896484375,
|
||||
'{': 0.3329986572265625,
|
||||
'}': 0.3329986572265625,
|
||||
'\\': 0.5,
|
||||
'|': 0.19499969482421875,
|
||||
'=': 0.604998779296875,
|
||||
'+': 0.604998779296875,
|
||||
'-': 0.604998779296875,
|
||||
_: 0.5,
|
||||
'`': 0.3329986572265625,
|
||||
' ~': 0.8329986572265625,
|
||||
'!': 0.3329986572265625,
|
||||
'@': 0.8579986572265625,
|
||||
'#': 0.6,
|
||||
$: 0.6,
|
||||
'%': 0.9699996948242188,
|
||||
'^': 0.517999267578125,
|
||||
'&': 0.7259994506835937,
|
||||
'*': 0.505999755859375,
|
||||
'(': 0.3329986572265625,
|
||||
')': 0.3329986572265625,
|
||||
'<': 0.604998779296875,
|
||||
'>': 0.604998779296875,
|
||||
'/': 0.5,
|
||||
'?': 0.53699951171875,
|
||||
};
|
783
packages/core/src/util/math.ts
Normal file
783
packages/core/src/util/math.ts
Normal file
@ -0,0 +1,783 @@
|
||||
import { Point } from '@antv/g-base/lib/types';
|
||||
import { IGroup } from '@antv/g-canvas/lib/interfaces';
|
||||
import { mat3, transform, vec3 } from '@antv/matrix-util';
|
||||
import { isArray, each } from '@antv/util';
|
||||
import {
|
||||
GraphData,
|
||||
ICircle,
|
||||
IEllipse,
|
||||
IRect,
|
||||
Matrix,
|
||||
EdgeConfig,
|
||||
NodeIdxMap,
|
||||
IBBox,
|
||||
Item,
|
||||
IPoint,
|
||||
} from '../types';
|
||||
|
||||
/**
|
||||
* 对比对象,用于对象数组排序
|
||||
* @param {string} attributeName 排序依据的字段名称
|
||||
* @param {number} min 最小值
|
||||
* @param {number} max 最大值
|
||||
* @return {boolean} bool 布尔
|
||||
*/
|
||||
export const compare = (attributeName: string) => {
|
||||
return (m, n) => {
|
||||
return m[attributeName] - n[attributeName];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 是否在区间内
|
||||
* @param {number} value 值
|
||||
* @param {number} min 最小值
|
||||
* @param {number} max 最大值
|
||||
* @return {boolean} bool 布尔
|
||||
*/
|
||||
const isBetween = (value: number, min: number, max: number) => value >= min && value <= max;
|
||||
|
||||
/**
|
||||
* 获取两条线段的交点
|
||||
* @param {Point} p0 第一条线段起点
|
||||
* @param {Point} p1 第一条线段终点
|
||||
* @param {Point} p2 第二条线段起点
|
||||
* @param {Point} p3 第二条线段终点
|
||||
* @return {Point} 交点
|
||||
*/
|
||||
export const getLineIntersect = (p0: Point, p1: Point, p2: Point, p3: Point): Point | null => {
|
||||
const tolerance = 0.001;
|
||||
|
||||
const E: Point = {
|
||||
x: p2.x - p0.x,
|
||||
y: p2.y - p0.y,
|
||||
};
|
||||
const D0: Point = {
|
||||
x: p1.x - p0.x,
|
||||
y: p1.y - p0.y,
|
||||
};
|
||||
const D1: Point = {
|
||||
x: p3.x - p2.x,
|
||||
y: p3.y - p2.y,
|
||||
};
|
||||
const kross: number = D0.x * D1.y - D0.y * D1.x;
|
||||
const sqrKross: number = kross * kross;
|
||||
const sqrLen0: number = D0.x * D0.x + D0.y * D0.y;
|
||||
const sqrLen1: number = D1.x * D1.x + D1.y * D1.y;
|
||||
let point: Point | null = null;
|
||||
if (sqrKross > tolerance * sqrLen0 * sqrLen1) {
|
||||
const s = (E.x * D1.y - E.y * D1.x) / kross;
|
||||
const t = (E.x * D0.y - E.y * D0.x) / kross;
|
||||
if (isBetween(s, 0, 1) && isBetween(t, 0, 1)) {
|
||||
point = {
|
||||
x: p0.x + s * D0.x,
|
||||
y: p0.y + s * D0.y,
|
||||
};
|
||||
}
|
||||
}
|
||||
return point;
|
||||
};
|
||||
|
||||
/**
|
||||
* point and rectangular intersection point
|
||||
* @param {IRect} rect rect
|
||||
* @param {Point} point point
|
||||
* @return {PointPoint} rst;
|
||||
*/
|
||||
export const getRectIntersectByPoint = (rect: IRect, point: Point): Point | null => {
|
||||
const { x, y, width, height } = rect;
|
||||
const cx = x + width / 2;
|
||||
const cy = y + height / 2;
|
||||
const points: Point[] = [];
|
||||
const center: Point = {
|
||||
x: cx,
|
||||
y: cy,
|
||||
};
|
||||
points.push({
|
||||
x,
|
||||
y,
|
||||
});
|
||||
points.push({
|
||||
x: x + width,
|
||||
y,
|
||||
});
|
||||
points.push({
|
||||
x: x + width,
|
||||
y: y + height,
|
||||
});
|
||||
points.push({
|
||||
x,
|
||||
y: y + height,
|
||||
});
|
||||
points.push({
|
||||
x,
|
||||
y,
|
||||
});
|
||||
let rst: Point | null = null;
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
rst = getLineIntersect(points[i - 1], points[i], center, point);
|
||||
if (rst) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return rst;
|
||||
};
|
||||
|
||||
/**
|
||||
* get point and circle inIntersect
|
||||
* @param {ICircle} circle 圆点,x,y,r
|
||||
* @param {Point} point 点 x,y
|
||||
* @return {Point} applied point
|
||||
*/
|
||||
export const getCircleIntersectByPoint = (circle: ICircle, point: Point): Point | null => {
|
||||
const { x: cx, y: cy, r } = circle;
|
||||
const { x, y } = point;
|
||||
|
||||
const dx = x - cx;
|
||||
const dy = y - cy;
|
||||
const d = Math.sqrt(dx * dx + dy * dy);
|
||||
if (d < r) {
|
||||
return null;
|
||||
}
|
||||
const signX = Math.sign(dx);
|
||||
const signY = Math.sign(dy);
|
||||
const angle = Math.atan(dy / dx);
|
||||
return {
|
||||
x: cx + Math.abs(r * Math.cos(angle)) * signX,
|
||||
y: cy + Math.abs(r * Math.sin(angle)) * signY,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* get point and ellipse inIntersect
|
||||
* @param {Object} ellipse 椭圆 x,y,rx,ry
|
||||
* @param {Object} point 点 x,y
|
||||
* @return {object} applied point
|
||||
*/
|
||||
export const getEllipseIntersectByPoint = (ellipse: IEllipse, point: Point): Point => {
|
||||
const a = ellipse.rx;
|
||||
const b = ellipse.ry;
|
||||
const cx = ellipse.x;
|
||||
const cy = ellipse.y;
|
||||
|
||||
const dx = point.x - cx;
|
||||
const dy = point.y - cy;
|
||||
// 直接通过 x,y 求夹角,求出来的范围是 -PI, PI
|
||||
let angle = Math.atan2(dy / b, dx / a);
|
||||
|
||||
if (angle < 0) {
|
||||
angle += 2 * Math.PI; // 转换到 0,2PI
|
||||
}
|
||||
|
||||
return {
|
||||
x: cx + a * Math.cos(angle),
|
||||
y: cy + b * Math.sin(angle),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* coordinate matrix transformation
|
||||
* @param {number} point coordinate
|
||||
* @param {Matrix} matrix matrix
|
||||
* @param {number} tag could be 0 or 1
|
||||
* @return {Point} transformed point
|
||||
*/
|
||||
export const applyMatrix = (point: Point, matrix: Matrix, tag: 0 | 1 = 1): Point => {
|
||||
const vector = [point.x, point.y, tag];
|
||||
if (!matrix || isNaN(matrix[0])) {
|
||||
matrix = [1, 0, 0, 0, 1, 0, 0, 0, 1];
|
||||
}
|
||||
|
||||
vec3.transformMat3(vector, vector, matrix);
|
||||
|
||||
return {
|
||||
x: vector[0],
|
||||
y: vector[1],
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* coordinate matrix invert transformation
|
||||
* @param {number} point coordinate
|
||||
* @param {number} matrix matrix
|
||||
* @param {number} tag could be 0 or 1
|
||||
* @return {object} transformed point
|
||||
*/
|
||||
export const invertMatrix = (point: Point, matrix: Matrix, tag: 0 | 1 = 1): Point => {
|
||||
if (!matrix || isNaN(matrix[0])) {
|
||||
matrix = [1, 0, 0, 0, 1, 0, 0, 0, 1];
|
||||
}
|
||||
|
||||
let inversedMatrix = mat3.invert([], matrix);
|
||||
if (!inversedMatrix) {
|
||||
inversedMatrix = [1, 0, 0, 0, 1, 0, 0, 0, 1];
|
||||
}
|
||||
const vector = [point.x, point.y, tag];
|
||||
vec3.transformMat3(vector, vector, inversedMatrix);
|
||||
|
||||
return {
|
||||
x: vector[0],
|
||||
y: vector[1],
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param p1 First coordinate
|
||||
* @param p2 second coordinate
|
||||
* @param p3 three coordinate
|
||||
*/
|
||||
export const getCircleCenterByPoints = (p1: Point, p2: Point, p3: Point): Point => {
|
||||
const a = p1.x - p2.x;
|
||||
const b = p1.y - p2.y;
|
||||
const c = p1.x - p3.x;
|
||||
const d = p1.y - p3.y;
|
||||
const e = (p1.x * p1.x - p2.x * p2.x - p2.y * p2.y + p1.y * p1.y) / 2;
|
||||
const f = (p1.x * p1.x - p3.x * p3.x - p3.y * p3.y + p1.y * p1.y) / 2;
|
||||
const denominator = b * c - a * d;
|
||||
return {
|
||||
x: -(d * e - b * f) / denominator,
|
||||
y: -(a * f - c * e) / denominator,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* get distance by two points
|
||||
* @param p1 first point
|
||||
* @param p2 second point
|
||||
*/
|
||||
export const distance = (p1: Point, p2: Point): number => {
|
||||
const vx = p1.x - p2.x;
|
||||
const vy = p1.y - p2.y;
|
||||
return Math.sqrt(vx * vx + vy * vy);
|
||||
};
|
||||
|
||||
/**
|
||||
* scale matrix
|
||||
* @param matrix [ [], [], [] ]
|
||||
* @param ratio
|
||||
*/
|
||||
export const scaleMatrix = (matrix: Matrix[], ratio: number) => {
|
||||
const result: Matrix[] = [];
|
||||
matrix.forEach((row) => {
|
||||
const newRow: number[] = [];
|
||||
row.forEach((v) => {
|
||||
newRow.push(v * ratio);
|
||||
});
|
||||
result.push(newRow);
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Floyd Warshall algorithm for shortest path distances matrix
|
||||
* @param {array} adjMatrix adjacency matrix
|
||||
* @return {array} distances shortest path distances matrix
|
||||
*/
|
||||
export const floydWarshall = (adjMatrix: Matrix[]): Matrix[] => {
|
||||
// initialize
|
||||
const dist: Matrix[] = [];
|
||||
const size = adjMatrix.length;
|
||||
for (let i = 0; i < size; i += 1) {
|
||||
dist[i] = [];
|
||||
for (let j = 0; j < size; j += 1) {
|
||||
if (i === j) {
|
||||
dist[i][j] = 0;
|
||||
} else if (adjMatrix[i][j] === 0 || !adjMatrix[i][j]) {
|
||||
dist[i][j] = Infinity;
|
||||
} else {
|
||||
dist[i][j] = adjMatrix[i][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
// floyd
|
||||
for (let k = 0; k < size; k += 1) {
|
||||
for (let i = 0; i < size; i += 1) {
|
||||
for (let j = 0; j < size; j += 1) {
|
||||
if (dist[i][j] > dist[i][k] + dist[k][j]) {
|
||||
dist[i][j] = dist[i][k] + dist[k][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return dist;
|
||||
};
|
||||
|
||||
/**
|
||||
* get adjacency matrix
|
||||
* @param data graph data
|
||||
* @param directed whether it's a directed graph
|
||||
*/
|
||||
export const getAdjMatrix = (data: GraphData, directed: boolean): Matrix[] => {
|
||||
const { nodes, edges } = data;
|
||||
const matrix: Matrix[] = [];
|
||||
// map node with index in data.nodes
|
||||
const nodeMap: {
|
||||
[key: string]: number;
|
||||
} = {};
|
||||
|
||||
if (!nodes) {
|
||||
throw new Error('invalid nodes data!');
|
||||
}
|
||||
if (nodes) {
|
||||
nodes.forEach((node, i) => {
|
||||
nodeMap[node.id] = i;
|
||||
const row: number[] = [];
|
||||
matrix.push(row);
|
||||
});
|
||||
}
|
||||
|
||||
if (edges) {
|
||||
edges.forEach((e) => {
|
||||
const { source, target } = e;
|
||||
const sIndex = nodeMap[source as string];
|
||||
const tIndex = nodeMap[target as string];
|
||||
matrix[sIndex][tIndex] = 1;
|
||||
if (!directed) {
|
||||
matrix[tIndex][sIndex] = 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return matrix;
|
||||
};
|
||||
|
||||
/**
|
||||
* 平移group
|
||||
* @param group Group 实例
|
||||
* @param vec 移动向量
|
||||
*/
|
||||
export const translate = (group: IGroup, vec: Point) => {
|
||||
group.translate(vec.x, vec.y);
|
||||
};
|
||||
|
||||
/**
|
||||
* 移动到指定坐标点
|
||||
* @param group Group 实例
|
||||
* @param point 移动到的坐标点
|
||||
*/
|
||||
export const move = (group: IGroup, point: Point) => {
|
||||
let matrix: Matrix = group.getMatrix();
|
||||
if (!matrix) {
|
||||
matrix = [1, 0, 0, 0, 1, 0, 0, 0, 1];
|
||||
}
|
||||
const bbox = group.getCanvasBBox();
|
||||
const vx = point.x - bbox.minX;
|
||||
const vy = point.y - bbox.minY;
|
||||
|
||||
const movedMatrix = transform(matrix, [['t', vx, vy]]);
|
||||
group.setMatrix(movedMatrix);
|
||||
};
|
||||
|
||||
/**
|
||||
* 缩放 group
|
||||
* @param group Group 实例
|
||||
* @param point 在x 和 y 方向上的缩放比例
|
||||
*/
|
||||
export const scale = (group: IGroup, ratio: number | number[]) => {
|
||||
let matrix: Matrix = group.getMatrix();
|
||||
if (!matrix) {
|
||||
matrix = [1, 0, 0, 0, 1, 0, 0, 0, 1];
|
||||
}
|
||||
|
||||
let scaleXY = ratio;
|
||||
if (!isArray(ratio)) {
|
||||
scaleXY = [ratio, ratio];
|
||||
}
|
||||
|
||||
if (isArray(ratio) && ratio.length === 1) {
|
||||
scaleXY = [ratio[0], ratio[0]];
|
||||
}
|
||||
|
||||
matrix = transform(matrix, [['s', (scaleXY as number[])[0], (scaleXY as number[])[1]]]);
|
||||
|
||||
group.setMatrix(matrix);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param group Group 实例
|
||||
* @param ratio 选择角度
|
||||
*/
|
||||
export const rotate = (group: IGroup, angle: number) => {
|
||||
let matrix: Matrix = group.getMatrix();
|
||||
if (!matrix) {
|
||||
matrix = [1, 0, 0, 0, 1, 0, 0, 0, 1];
|
||||
}
|
||||
matrix = transform(matrix, [['r', angle]]);
|
||||
|
||||
group.setMatrix(matrix);
|
||||
};
|
||||
|
||||
export const getDegree = (n: number, nodeIdxMap: NodeIdxMap, edges: EdgeConfig[]): number[] => {
|
||||
const degrees: number[] = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
degrees[i] = 0;
|
||||
}
|
||||
edges.forEach((e) => {
|
||||
if (e.source) {
|
||||
degrees[nodeIdxMap[e.source]] += 1;
|
||||
}
|
||||
if (e.target) {
|
||||
degrees[nodeIdxMap[e.target]] += 1;
|
||||
}
|
||||
});
|
||||
return degrees;
|
||||
};
|
||||
|
||||
// 判断点Q是否在p1和p2的线段上
|
||||
function onSegment(p1, p2, q) {
|
||||
if (
|
||||
(q[0] - p1[0]) * (p2[1] - p1[1]) === (p2[0] - p1[0]) * (q[1] - p1[1]) &&
|
||||
Math.min(p1[0], p2[0]) <= q[0] &&
|
||||
q[0] <= Math.max(p1[0], p2[0]) &&
|
||||
Math.min(p1[1], p2[1]) <= q[1] &&
|
||||
q[1] <= Math.max(p1[1], p2[1])
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断点P在多边形内-射线法. Borrow from https://github.com/antvis/util/blob/master/packages/path-util/src/point-in-polygon.ts
|
||||
* @param points
|
||||
* @param x
|
||||
* @param y
|
||||
*/
|
||||
export const isPointInPolygon = (points: number[][], x: number, y: number) => {
|
||||
let isHit = false;
|
||||
const n = points.length;
|
||||
// 判断两个double在eps精度下的大小关系
|
||||
const tolerance = 1e-6;
|
||||
function dcmp(xValue) {
|
||||
if (Math.abs(xValue) < tolerance) {
|
||||
return 0;
|
||||
}
|
||||
return xValue < 0 ? -1 : 1;
|
||||
}
|
||||
if (n <= 2) {
|
||||
// svg 中点小于 3 个时,不显示,也无法被拾取
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < n; i++) {
|
||||
const p1 = points[i];
|
||||
const p2 = points[(i + 1) % n];
|
||||
if (onSegment(p1, p2, [x, y])) {
|
||||
// 点在多边形一条边上
|
||||
return true;
|
||||
}
|
||||
// 前一个判断min(p1[1],p2[1])<P.y<=max(p1[1],p2[1])
|
||||
// 后一个判断被测点 在 射线与边交点 的左边
|
||||
if (
|
||||
dcmp(p1[1] - y) > 0 !== dcmp(p2[1] - y) > 0 &&
|
||||
dcmp(x - ((y - p1[1]) * (p1[0] - p2[0])) / (p1[1] - p2[1]) - p1[0]) < 0
|
||||
) {
|
||||
isHit = !isHit;
|
||||
}
|
||||
}
|
||||
return isHit;
|
||||
};
|
||||
|
||||
// 判断两个BBox是否相交
|
||||
export const intersectBBox = (box1: Partial<IBBox>, box2: Partial<IBBox>) => {
|
||||
return !(
|
||||
box2.minX > box1.maxX ||
|
||||
box2.maxX < box1.minX ||
|
||||
box2.minY > box1.maxY ||
|
||||
box2.maxY < box1.minY
|
||||
);
|
||||
};
|
||||
|
||||
const lineIntersectPolygon = (lines, line) => {
|
||||
let isIntersect = false;
|
||||
each(lines, (l) => {
|
||||
if (getLineIntersect(l.from, l.to, line.from, line.to)) {
|
||||
isIntersect = true;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return isIntersect;
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断两个polygon是否相交。
|
||||
* borrow from @antv/path-util
|
||||
* @param points1 polygon1的顶点数组
|
||||
* @param points2 polygon2的顶点数组
|
||||
*/
|
||||
export const isPolygonsIntersect = (points1: number[][], points2: number[][]): boolean => {
|
||||
type BBox = Partial<IBBox>;
|
||||
const getBBox = (points): BBox => {
|
||||
const xArr = points.map((p) => p[0]);
|
||||
const yArr = points.map((p) => p[1]);
|
||||
return {
|
||||
minX: Math.min.apply(null, xArr),
|
||||
maxX: Math.max.apply(null, xArr),
|
||||
minY: Math.min.apply(null, yArr),
|
||||
maxY: Math.max.apply(null, yArr),
|
||||
};
|
||||
};
|
||||
|
||||
const parseToLines = (points: number[][]) => {
|
||||
const lines = [];
|
||||
const count = points.length;
|
||||
for (let i = 0; i < count - 1; i++) {
|
||||
const point = points[i];
|
||||
const next = points[i + 1];
|
||||
lines.push({
|
||||
from: {
|
||||
x: point[0],
|
||||
y: point[1],
|
||||
},
|
||||
to: {
|
||||
x: next[0],
|
||||
y: next[1],
|
||||
},
|
||||
});
|
||||
}
|
||||
if (lines.length > 1) {
|
||||
const first = points[0];
|
||||
const last = points[count - 1];
|
||||
lines.push({
|
||||
from: {
|
||||
x: last[0],
|
||||
y: last[1],
|
||||
},
|
||||
to: {
|
||||
x: first[0],
|
||||
y: first[1],
|
||||
},
|
||||
});
|
||||
}
|
||||
return lines;
|
||||
};
|
||||
|
||||
// 空数组,或者一个点返回 false
|
||||
if (points1.length < 2 || points2.length < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bbox1 = getBBox(points1);
|
||||
const bbox2 = getBBox(points2);
|
||||
// 判定包围盒是否相交,比判定点是否在多边形内要快的多,可以筛选掉大多数情况
|
||||
if (!intersectBBox(bbox1, bbox2)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let isIn = false;
|
||||
// 判定点是否在多边形内部,一旦有一个点在另一个多边形内,则返回
|
||||
each(points2, (point) => {
|
||||
if (isPointInPolygon(points1, point[0], point[1])) {
|
||||
isIn = true;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if (isIn) {
|
||||
return true;
|
||||
}
|
||||
each(points1, (point) => {
|
||||
if (isPointInPolygon(points2, point[0], point[1])) {
|
||||
isIn = true;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if (isIn) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const lines1 = parseToLines(points1);
|
||||
const lines2 = parseToLines(points2);
|
||||
let isIntersect = false;
|
||||
each(lines2, (line) => {
|
||||
if (lineIntersectPolygon(lines1, line)) {
|
||||
isIntersect = true;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return isIntersect;
|
||||
};
|
||||
|
||||
export class Line {
|
||||
public x1: number;
|
||||
|
||||
public y1: number;
|
||||
|
||||
public x2: number;
|
||||
|
||||
public y2: number;
|
||||
|
||||
constructor(x1: number, y1: number, x2: number, y2: number) {
|
||||
this.x1 = x1;
|
||||
this.y1 = y1;
|
||||
this.x2 = x2;
|
||||
this.y2 = y2;
|
||||
}
|
||||
|
||||
public getBBox() {
|
||||
const minX = Math.min(this.x1, this.x2);
|
||||
const minY = Math.min(this.y1, this.y2);
|
||||
const maxX = Math.max(this.x1, this.x2);
|
||||
const maxY = Math.max(this.y1, this.y2);
|
||||
const res = {
|
||||
x: minX,
|
||||
y: minY,
|
||||
minX,
|
||||
minY,
|
||||
maxX,
|
||||
maxY,
|
||||
width: maxX - minX,
|
||||
height: maxY - minY,
|
||||
};
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
export const getBBoxBoundLine = (bbox: IBBox, direction: string) => {
|
||||
const bounds = {
|
||||
top: [bbox.minX, bbox.minY, bbox.maxX, bbox.minY],
|
||||
left: [bbox.minX, bbox.minY, bbox.minX, bbox.maxY],
|
||||
bottom: [bbox.minX, bbox.maxY, bbox.maxX, bbox.maxY],
|
||||
right: [bbox.maxX, bbox.minY, bbox.maxX, bbox.maxY],
|
||||
};
|
||||
return bounds[direction];
|
||||
};
|
||||
|
||||
/**
|
||||
* 计算两条线段相交时,相交点对第一条线段上的分割比例
|
||||
*/
|
||||
const fractionAlongLineA = (la: Line, lb: Line) => {
|
||||
const uaT = (lb.x2 - lb.x1) * (la.y1 - lb.y1) - (lb.y2 - lb.y1) * (la.x1 - lb.x1);
|
||||
const ubT = (la.x2 - la.x1) * (la.y1 - lb.y1) - (la.y2 - la.y1) * (la.x1 - lb.x1);
|
||||
const uB = (lb.y2 - lb.y1) * (la.x2 - la.x1) - (lb.x2 - lb.x1) * (la.y2 - la.y1);
|
||||
if (uB) {
|
||||
const ua = uaT / uB;
|
||||
const ub = ubT / uB;
|
||||
if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1) {
|
||||
return ua;
|
||||
}
|
||||
}
|
||||
return Number.POSITIVE_INFINITY;
|
||||
};
|
||||
|
||||
export const itemIntersectByLine = (item: Item, line: Line): [IPoint[], number] => {
|
||||
const directions = ['top', 'left', 'bottom', 'right'];
|
||||
const bbox = item.getBBox();
|
||||
let countIntersections = 0;
|
||||
const intersections = [];
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const [x1, y1, x2, y2] = getBBoxBoundLine(bbox, directions[i]);
|
||||
intersections[i] = getLineIntersect(
|
||||
{ x: line.x1, y: line.y1 },
|
||||
{ x: line.x2, y: line.y2 },
|
||||
{ x: x1, y: y1 },
|
||||
{ x: x2, y: y2 },
|
||||
);
|
||||
if (intersections[i]) {
|
||||
countIntersections += 1;
|
||||
}
|
||||
}
|
||||
return [intersections, countIntersections];
|
||||
};
|
||||
|
||||
export const fractionToLine = (item: Item, line: Line) => {
|
||||
const directions = ['top', 'left', 'bottom', 'right'];
|
||||
const bbox = item.getBBox();
|
||||
let minDistance = Number.POSITIVE_INFINITY;
|
||||
let countIntersections = 0;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const [x1, y1, x2, y2] = getBBoxBoundLine(bbox, directions[i]);
|
||||
let testDistance = fractionAlongLineA(line, new Line(x1, y1, x2, y2));
|
||||
testDistance = Math.abs(testDistance - 0.5);
|
||||
if (testDistance >= 0 && testDistance <= 1) {
|
||||
countIntersections += 1;
|
||||
minDistance = testDistance < minDistance ? testDistance : minDistance;
|
||||
}
|
||||
}
|
||||
|
||||
if (countIntersections === 0) return -1;
|
||||
return minDistance;
|
||||
};
|
||||
|
||||
export const getPointsCenter = (points: IPoint[]): IPoint => {
|
||||
let centerX = 0;
|
||||
let centerY = 0;
|
||||
if (points.length > 0) {
|
||||
for (const point of points) {
|
||||
centerX += point.x;
|
||||
centerY += point.y;
|
||||
}
|
||||
centerX /= points.length;
|
||||
centerY /= points.length;
|
||||
}
|
||||
return { x: centerX, y: centerY };
|
||||
};
|
||||
|
||||
export const squareDist = (a: IPoint, b: IPoint): number => {
|
||||
return (a.x - b.x) ** 2 + (a.y - b.y) ** 2;
|
||||
};
|
||||
|
||||
export const pointLineSquareDist = (point: IPoint, line: Line) => {
|
||||
const x1 = line.x1;
|
||||
const y1 = line.y1;
|
||||
const x2 = line.x2 - x1;
|
||||
const y2 = line.y2 - y1;
|
||||
let px = point.x - x1;
|
||||
let py = point.y - y1;
|
||||
let dotprod = px * x2 + py * y2;
|
||||
let projlenSq;
|
||||
if (dotprod <= 0) {
|
||||
projlenSq = 0;
|
||||
} else {
|
||||
px = x2 - px;
|
||||
py = y2 - py;
|
||||
dotprod = px * x2 + py * y2;
|
||||
if (dotprod <= 0) {
|
||||
projlenSq = 0;
|
||||
} else {
|
||||
projlenSq = (dotprod * dotprod) / (x2 * x2 + y2 * y2);
|
||||
}
|
||||
}
|
||||
let lenSq = px * px + py * py - projlenSq;
|
||||
if (lenSq < 0) {
|
||||
lenSq = 0;
|
||||
}
|
||||
return lenSq;
|
||||
};
|
||||
|
||||
export const isPointsOverlap = (p1, p2, e = 1e-3) => {
|
||||
return (p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2 < e ** 2;
|
||||
};
|
||||
|
||||
/**
|
||||
* 点到矩形的距离的平方:矩形内部点视作距离为0,外部的点若投影落在矩形边上则为点到矩形边的最近的垂直距离,否则为点到矩形顶点的距离,
|
||||
* @param point IPoint
|
||||
* @param rect IRect
|
||||
*/
|
||||
export const pointRectSquareDist = (point: Point, rect: IRect) => {
|
||||
const isLeft = point.x < rect.x
|
||||
const isRight = point.x > rect.x + rect.width
|
||||
const isTop = point.y > rect.y + rect.height
|
||||
const isBottom = point.y < rect.y
|
||||
|
||||
const isPointOutside = isLeft || isRight || isTop || isBottom
|
||||
if (!isPointOutside) {
|
||||
return 0
|
||||
}
|
||||
if (isTop && !isLeft && !isRight) {
|
||||
return (rect.y + rect.height - point.y) ** 2;
|
||||
}
|
||||
if (isBottom && !isLeft && !isRight) {
|
||||
return (point.y - rect.y) ** 2;
|
||||
}
|
||||
if (isLeft && !isTop && !isBottom) {
|
||||
return (rect.x - point.x) ** 2;
|
||||
}
|
||||
if (isRight && !isTop && !isBottom) {
|
||||
return (rect.x + rect.width - point.x) ** 2;
|
||||
}
|
||||
const dx = Math.min(Math.abs(rect.x - point.x), Math.abs(rect.x + rect.width - point.x))
|
||||
const dy = Math.min(Math.abs(rect.y - point.y), Math.abs(rect.y + rect.height - point.y))
|
||||
return dx * dx + dy * dy;
|
||||
};
|
319
packages/core/src/util/path.ts
Normal file
319
packages/core/src/util/path.ts
Normal file
@ -0,0 +1,319 @@
|
||||
import { vec2 } from '@antv/matrix-util';
|
||||
import { catmullRom2Bezier } from '@antv/path-util';
|
||||
import { IPoint } from '../types';
|
||||
|
||||
/**
|
||||
* 替换字符串中的字段
|
||||
* @param {String} str 模版字符串
|
||||
* @param {Object} o json data
|
||||
*/
|
||||
const substitute = (str: string, o: any): string => {
|
||||
if (!str || !o) {
|
||||
return str;
|
||||
}
|
||||
|
||||
return str.replace(/\\?\{([^{}]+)\}/g, (match: string, name: string) => {
|
||||
if (match.charAt(0) === '\\') {
|
||||
return match.slice(1);
|
||||
}
|
||||
let res = o[name];
|
||||
if (res === 0) res = '0';
|
||||
return res || '';
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 给定坐标获取三次贝塞尔曲线的 M 及 C 值
|
||||
* @param points coordinate set
|
||||
*/
|
||||
export const getSpline = (points: IPoint[]) => {
|
||||
const data: number[] = [];
|
||||
|
||||
if (points.length < 2) {
|
||||
throw new Error(`point length must largn than 2, now it's ${points.length}`);
|
||||
}
|
||||
for (const point of points) {
|
||||
const { x, y } = point;
|
||||
data.push(x);
|
||||
data.push(y);
|
||||
}
|
||||
|
||||
const spliePath = catmullRom2Bezier(data);
|
||||
spliePath.unshift(['M', points[0].x, points[0].y]);
|
||||
return spliePath;
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据起始点、相对位置、偏移量计算控制点
|
||||
* @param {IPoint} startPoint 起始点,包含 x,y
|
||||
* @param {IPoint} endPoint 结束点, 包含 x,y
|
||||
* @param {Number} percent 相对位置,范围 0-1
|
||||
* @param {Number} offset 偏移量
|
||||
* @return {IPoint} 控制点,包含 x,y
|
||||
*/
|
||||
export const getControlPoint = (
|
||||
startPoint: IPoint,
|
||||
endPoint: IPoint,
|
||||
percent: number = 0,
|
||||
offset: number = 0,
|
||||
): IPoint => {
|
||||
const point: IPoint = {
|
||||
x: (1 - percent) * startPoint.x + percent * endPoint.x,
|
||||
y: (1 - percent) * startPoint.y + percent * endPoint.y,
|
||||
};
|
||||
|
||||
let tangent: number[] = [];
|
||||
vec2.normalize(tangent, [endPoint.x - startPoint.x, endPoint.y - startPoint.y]);
|
||||
|
||||
if (tangent.length === 0) {
|
||||
tangent = [0, 0];
|
||||
}
|
||||
const perpendicular = [-tangent[1] * offset, tangent[0] * offset]; // 垂直向量
|
||||
point.x += perpendicular[0];
|
||||
point.y += perpendicular[1];
|
||||
return point;
|
||||
};
|
||||
|
||||
/**
|
||||
* 点集转化为Path多边形
|
||||
* @param {Array} points 点集
|
||||
* @param {Boolen} z 是否封闭
|
||||
* @return {Array} Path
|
||||
*/
|
||||
export const pointsToPolygon = (points: IPoint[], z?: boolean): string => {
|
||||
const { length } = points;
|
||||
if (!length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let path = '';
|
||||
let str = '';
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const item = points[i];
|
||||
if (i === 0) {
|
||||
str = 'M{x} {y}';
|
||||
} else {
|
||||
str = 'L{x} {y}';
|
||||
}
|
||||
path += substitute(str, item);
|
||||
}
|
||||
|
||||
if (z) {
|
||||
path += 'Z';
|
||||
}
|
||||
return path;
|
||||
};
|
||||
|
||||
export const pathToPoints = (path: any[]) => {
|
||||
const points = [];
|
||||
path.forEach((seg) => {
|
||||
const command = seg[0];
|
||||
if (command !== 'A') {
|
||||
for (let i = 1; i < seg.length; i = i + 2) {
|
||||
points.push([seg[i], seg[i + 1]]);
|
||||
}
|
||||
} else {
|
||||
const length = seg.length;
|
||||
points.push([seg[length - 2], seg[length - 1]]);
|
||||
}
|
||||
});
|
||||
return points;
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成平滑的闭合曲线
|
||||
* @param points
|
||||
*/
|
||||
export const getClosedSpline = (points: IPoint[]) => {
|
||||
if (points.length < 2) {
|
||||
throw new Error(`point length must largn than 2, now it's ${points.length}`);
|
||||
}
|
||||
const first = points[0];
|
||||
const second = points[1];
|
||||
const last = points[points.length - 1];
|
||||
const lastSecond = points[points.length - 2];
|
||||
|
||||
points.unshift(last);
|
||||
points.unshift(lastSecond);
|
||||
points.push(first);
|
||||
points.push(second);
|
||||
|
||||
const closedPath = [];
|
||||
for (let i = 1; i < points.length - 2; i += 1) {
|
||||
const x0 = points[i - 1].x;
|
||||
const y0 = points[i - 1].y;
|
||||
const x1 = points[i].x;
|
||||
const y1 = points[i].y;
|
||||
const x2 = points[i + 1].x;
|
||||
const y2 = points[i + 1].y;
|
||||
const x3 = i !== points.length - 2 ? points[i + 2].x : x2;
|
||||
const y3 = i !== points.length - 2 ? points[i + 2].y : y2;
|
||||
|
||||
const cp1x = x1 + (x2 - x0) / 6;
|
||||
const cp1y = y1 + (y2 - y0) / 6;
|
||||
const cp2x = x2 - (x3 - x1) / 6;
|
||||
const cp2y = y2 - (y3 - y1) / 6;
|
||||
closedPath.push(['C', cp1x, cp1y, cp2x, cp2y, x2, y2]);
|
||||
}
|
||||
closedPath.unshift(['M', last.x, last.y]);
|
||||
return closedPath;
|
||||
};
|
||||
|
||||
const vecScaleTo = (v: number[], length: number) => {
|
||||
// Vector with direction of v with specified length
|
||||
return vec2.scale([], vec2.normalize([], v), length);
|
||||
};
|
||||
|
||||
const unitNormal = (p0: number[], p1: number[]) => {
|
||||
// Returns the unit normal to the line segment from p0 to p1.
|
||||
const n = [p0[1] - p1[1], p1[0] - p0[0]];
|
||||
const nLength = Math.sqrt(n[0] * n[0] + n[1] * n[1]);
|
||||
if (nLength === 0) {
|
||||
throw new Error('p0 should not be equal to p1');
|
||||
}
|
||||
return [n[0] / nLength, n[1] / nLength];
|
||||
};
|
||||
|
||||
const vecFrom = (p0: number[], p1: number[]) => {
|
||||
// Vector from p0 to p1
|
||||
return [p1[0] - p0[0], p1[1] - p0[1]];
|
||||
};
|
||||
|
||||
/**
|
||||
* 传入的节点作为多边形顶点,生成有圆角的多边形
|
||||
* @param polyPoints 多边形顶点
|
||||
* @param padding 在原多边形基础上增加最终轮廓和原多边形的空白间隔
|
||||
*/
|
||||
export function roundedHull(polyPoints: number[][], padding: number) {
|
||||
// The rounded hull path around a single point
|
||||
const roundedHull1 = (points: number[][]) => {
|
||||
const p1 = [points[0][0], points[0][1] - padding];
|
||||
const p2 = [points[0][0], points[0][1] + padding];
|
||||
|
||||
return `M ${p1} A ${padding},${padding},0,0,0,${p2} A ${padding},${padding},0,0,0,${p1}`;
|
||||
};
|
||||
|
||||
// The rounded hull path around two points
|
||||
const roundedHull2 = (points: number[][]) => {
|
||||
const offsetVector = vec2.scale([], unitNormal(points[0], points[1]), padding);
|
||||
const invOffsetVector = vec2.scale([], offsetVector, -1);
|
||||
|
||||
const p0 = vec2.add([], points[0], offsetVector);
|
||||
const p1 = vec2.add([], points[1], offsetVector);
|
||||
const p2 = vec2.add([], points[1], invOffsetVector);
|
||||
const p3 = vec2.add([], points[0], invOffsetVector);
|
||||
|
||||
return `M ${p0} L ${p1} A ${[padding, padding, '0,0,0', p2].join(',')} L ${p3} A ${[
|
||||
padding,
|
||||
padding,
|
||||
'0,0,0',
|
||||
p0,
|
||||
].join(',')}`;
|
||||
};
|
||||
|
||||
// 特殊情况处理:节点数小于等于2
|
||||
if (!polyPoints || polyPoints.length < 1) return '';
|
||||
if (polyPoints.length === 1) return roundedHull1(polyPoints);
|
||||
if (polyPoints.length === 2) return roundedHull2(polyPoints);
|
||||
|
||||
let segments = new Array(polyPoints.length);
|
||||
|
||||
// Calculate each offset (outwards) segment of the convex hull.
|
||||
for (let segmentIndex = 0; segmentIndex < segments.length; ++segmentIndex) {
|
||||
const p0 =
|
||||
segmentIndex === 0 ? polyPoints[polyPoints.length - 1] : polyPoints[segmentIndex - 1];
|
||||
const p1 = polyPoints[segmentIndex];
|
||||
|
||||
// Compute the offset vector for the line segment, with length = padding.
|
||||
const offset = vec2.scale([], unitNormal(p0, p1), padding);
|
||||
|
||||
segments[segmentIndex] = [vec2.add([], p0, offset), vec2.add([], p1, offset)];
|
||||
}
|
||||
|
||||
const arcData = `A ${[padding, padding, '0,0,0,'].join(',')}`;
|
||||
|
||||
segments = segments.map((segment, index) => {
|
||||
let pathFragment = '';
|
||||
if (index === 0) {
|
||||
pathFragment = `M ${segments[segments.length - 1][1]} `;
|
||||
}
|
||||
pathFragment += `${arcData + segment[0]} L ${segment[1]}`;
|
||||
return pathFragment;
|
||||
});
|
||||
|
||||
return segments.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* 传入的节点作为多边形顶点,生成平滑的闭合多边形
|
||||
* @param polyPoints
|
||||
* @param padding
|
||||
*/
|
||||
export function paddedHull(polyPoints: number[][], padding: number) {
|
||||
const pointCount = polyPoints.length;
|
||||
|
||||
const smoothHull1 = (points) => {
|
||||
// Returns the path for a circular hull around a single point.
|
||||
|
||||
const p1 = [points[0][0], points[0][1] - padding];
|
||||
const p2 = [points[0][0], points[0][1] + padding];
|
||||
|
||||
return `M ${p1} A ${[padding, padding, '0,0,0', p2].join(',')} A ${[
|
||||
padding,
|
||||
padding,
|
||||
'0,0,0',
|
||||
p1,
|
||||
].join(',')}`;
|
||||
};
|
||||
|
||||
// Returns the path for a rounded hull around two points.
|
||||
const smoothHull2 = (points) => {
|
||||
const v = vecFrom(points[0], points[1]);
|
||||
const extensionVec = vecScaleTo(v, padding);
|
||||
|
||||
const extension0 = vec2.add([], points[0], vec2.scale([], extensionVec, -1));
|
||||
const extension1 = vec2.add([], points[1], extensionVec);
|
||||
|
||||
const tangentHalfLength = 1.2 * padding;
|
||||
const controlDelta = vecScaleTo(vec2.normalize([], v), tangentHalfLength);
|
||||
const invControlDelta = vec2.scale([], controlDelta, -1);
|
||||
|
||||
const control0 = vec2.add([], extension0, invControlDelta);
|
||||
const control1 = vec2.add([], extension1, invControlDelta);
|
||||
const control3 = vec2.add([], extension0, controlDelta);
|
||||
|
||||
return `M ${extension0} C ${[control0, control1, extension1].join(',')} S ${[
|
||||
control3,
|
||||
extension0,
|
||||
].join(',')} Z`;
|
||||
};
|
||||
|
||||
// Handle special cases
|
||||
if (!polyPoints || pointCount < 1) return '';
|
||||
if (pointCount === 1) return smoothHull1(polyPoints);
|
||||
if (pointCount === 2) return smoothHull2(polyPoints);
|
||||
|
||||
const hullPoints = polyPoints.map((point, index) => {
|
||||
const pNext = polyPoints[(index + 1) % pointCount];
|
||||
return {
|
||||
p: point,
|
||||
v: vec2.normalize([], vecFrom(point, pNext)),
|
||||
};
|
||||
});
|
||||
|
||||
// Compute the expanded hull points, and the nearest prior control point for each.
|
||||
for (let i = 0; i < hullPoints.length; ++i) {
|
||||
const priorIndex = i > 0 ? i - 1 : pointCount - 1;
|
||||
const extensionVec = vec2.normalize(
|
||||
[],
|
||||
vec2.add([], hullPoints[priorIndex].v, vec2.scale([], hullPoints[i].v, -1)),
|
||||
);
|
||||
hullPoints[i].p = vec2.add([], hullPoints[i].p, vec2.scale([], extensionVec, padding));
|
||||
}
|
||||
|
||||
return hullPoints.map((obj) => {
|
||||
const point = obj.p;
|
||||
return { x: point[0], y: point[1] };
|
||||
});
|
||||
}
|
95446
packages/core/stats.json
Normal file
95446
packages/core/stats.json
Normal file
File diff suppressed because one or more lines are too long
232
packages/core/tests/unit/algorithm/adjacent-matrix-spec.ts
Normal file
232
packages/core/tests/unit/algorithm/adjacent-matrix-spec.ts
Normal file
@ -0,0 +1,232 @@
|
||||
import G6, { Algorithm } from '../../../src';
|
||||
const { adjMatrix } = Algorithm;
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.id = 'container';
|
||||
document.body.appendChild(div);
|
||||
|
||||
const data = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'A',
|
||||
label: '0',
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
label: '1',
|
||||
},
|
||||
{
|
||||
id: 'C',
|
||||
label: '2',
|
||||
},
|
||||
{
|
||||
id: 'D',
|
||||
label: '3',
|
||||
},
|
||||
{
|
||||
id: 'E',
|
||||
label: '4',
|
||||
},
|
||||
{
|
||||
id: 'F',
|
||||
label: '5',
|
||||
},
|
||||
{
|
||||
id: 'G',
|
||||
label: '6',
|
||||
},
|
||||
{
|
||||
id: 'H',
|
||||
label: '7',
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: 'A',
|
||||
target: 'B',
|
||||
},
|
||||
{
|
||||
source: 'B',
|
||||
target: 'C',
|
||||
},
|
||||
{
|
||||
source: 'C',
|
||||
target: 'G',
|
||||
},
|
||||
{
|
||||
source: 'A',
|
||||
target: 'D',
|
||||
},
|
||||
{
|
||||
source: 'A',
|
||||
target: 'E',
|
||||
},
|
||||
{
|
||||
source: 'E',
|
||||
target: 'F',
|
||||
},
|
||||
{
|
||||
source: 'F',
|
||||
target: 'D',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('Adjacency Matrix on graph', () => {
|
||||
const graph = new G6.Graph({
|
||||
container: 'container',
|
||||
width: 500,
|
||||
height: 500,
|
||||
defaultEdge: {
|
||||
style: {
|
||||
endArrow: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
it('get graph adjacency matrix', () => {
|
||||
const matrix = graph.getAdjMatrix();
|
||||
expect(Object.keys(matrix).length).toBe(8);
|
||||
const node0Adj = matrix[0];
|
||||
expect(node0Adj.length).toBe(5);
|
||||
expect(node0Adj[0]).toBe(undefined);
|
||||
expect(node0Adj[1]).toBe(1);
|
||||
expect(node0Adj[2]).toBe(undefined);
|
||||
expect(node0Adj[3]).toBe(1);
|
||||
expect(node0Adj[4]).toBe(1);
|
||||
});
|
||||
|
||||
it('add items and cache', () => {
|
||||
graph.addItem('node', {
|
||||
id: 'I',
|
||||
label: '8',
|
||||
});
|
||||
graph.addItem('edge', {
|
||||
source: 'I',
|
||||
target: 'A',
|
||||
});
|
||||
graph.addItem('edge', {
|
||||
source: 'C',
|
||||
target: 'A',
|
||||
});
|
||||
// use the cache
|
||||
const cachedMatrix = graph.getAdjMatrix();
|
||||
expect(Object.keys(cachedMatrix).length).toBe(8);
|
||||
const cachedNode0Adj = cachedMatrix[0];
|
||||
expect(cachedNode0Adj.length).toBe(5);
|
||||
expect(cachedNode0Adj[0]).toBe(undefined);
|
||||
expect(cachedNode0Adj[1]).toBe(1);
|
||||
expect(cachedNode0Adj[2]).toBe(undefined);
|
||||
expect(cachedNode0Adj[3]).toBe(1);
|
||||
expect(cachedNode0Adj[4]).toBe(1);
|
||||
|
||||
// do not use the cache
|
||||
const matrix = graph.getAdjMatrix(false);
|
||||
expect(Object.keys(matrix).length).toBe(9);
|
||||
const node0Adj = matrix[0];
|
||||
expect(node0Adj.length).toBe(9);
|
||||
expect(node0Adj[0]).toBe(undefined);
|
||||
expect(node0Adj[1]).toBe(1);
|
||||
expect(node0Adj[2]).toBe(1);
|
||||
expect(node0Adj[3]).toBe(1);
|
||||
expect(node0Adj[4]).toBe(1);
|
||||
expect(node0Adj[5]).toBe(undefined);
|
||||
expect(node0Adj[6]).toBe(undefined);
|
||||
expect(node0Adj[7]).toBe(undefined);
|
||||
expect(node0Adj[8]).toBe(1);
|
||||
});
|
||||
|
||||
it('directed', () => {
|
||||
// do not use the cache and directed
|
||||
const matrix = graph.getAdjMatrix(false, true);
|
||||
expect(Object.keys(matrix).length).toBe(9);
|
||||
const node0Adj = matrix[0];
|
||||
expect(node0Adj.length).toBe(5);
|
||||
expect(node0Adj[0]).toBe(undefined);
|
||||
expect(node0Adj[1]).toBe(1);
|
||||
expect(node0Adj[2]).toBe(undefined);
|
||||
expect(node0Adj[3]).toBe(1);
|
||||
expect(node0Adj[4]).toBe(1);
|
||||
const node8Adj = matrix[8];
|
||||
expect(node8Adj.length).toBe(1);
|
||||
expect(node8Adj[0]).toBe(1);
|
||||
graph.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Adjacency Matrix by Algorithm', () => {
|
||||
const graph = new G6.Graph({
|
||||
container: 'container',
|
||||
width: 500,
|
||||
height: 500,
|
||||
defaultEdge: {
|
||||
style: {
|
||||
endArrow: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
it('get graph adjacency matrix', () => {
|
||||
const matrix = adjMatrix(graph);
|
||||
expect(Object.keys(matrix).length).toBe(8);
|
||||
const node0Adj = matrix[0];
|
||||
expect(node0Adj.length).toBe(5);
|
||||
expect(node0Adj[0]).toBe(undefined);
|
||||
expect(node0Adj[1]).toBe(1);
|
||||
expect(node0Adj[2]).toBe(undefined);
|
||||
expect(node0Adj[3]).toBe(1);
|
||||
expect(node0Adj[4]).toBe(1);
|
||||
});
|
||||
|
||||
it('add items and cache', () => {
|
||||
graph.addItem('node', {
|
||||
id: 'I',
|
||||
label: '8',
|
||||
});
|
||||
graph.addItem('edge', {
|
||||
source: 'I',
|
||||
target: 'A',
|
||||
});
|
||||
graph.addItem('edge', {
|
||||
source: 'C',
|
||||
target: 'A',
|
||||
});
|
||||
|
||||
const matrix = adjMatrix(graph);
|
||||
expect(Object.keys(matrix).length).toBe(9);
|
||||
const node0Adj = matrix[0];
|
||||
expect(node0Adj.length).toBe(9);
|
||||
expect(node0Adj[0]).toBe(undefined);
|
||||
expect(node0Adj[1]).toBe(1);
|
||||
expect(node0Adj[2]).toBe(1);
|
||||
expect(node0Adj[3]).toBe(1);
|
||||
expect(node0Adj[4]).toBe(1);
|
||||
expect(node0Adj[5]).toBe(undefined);
|
||||
expect(node0Adj[6]).toBe(undefined);
|
||||
expect(node0Adj[7]).toBe(undefined);
|
||||
expect(node0Adj[8]).toBe(1);
|
||||
});
|
||||
|
||||
it('directed', () => {
|
||||
// directed
|
||||
const matrix = adjMatrix(graph, true);
|
||||
expect(Object.keys(matrix).length).toBe(9);
|
||||
const node0Adj = matrix[0];
|
||||
expect(node0Adj.length).toBe(5);
|
||||
expect(node0Adj[0]).toBe(undefined);
|
||||
expect(node0Adj[1]).toBe(1);
|
||||
expect(node0Adj[2]).toBe(undefined);
|
||||
expect(node0Adj[3]).toBe(1);
|
||||
expect(node0Adj[4]).toBe(1);
|
||||
const node8Adj = matrix[8];
|
||||
expect(node8Adj.length).toBe(1);
|
||||
expect(node8Adj[0]).toBe(1);
|
||||
graph.destroy();
|
||||
});
|
||||
});
|
201
packages/core/tests/unit/algorithm/bfs-spec.ts
Normal file
201
packages/core/tests/unit/algorithm/bfs-spec.ts
Normal file
@ -0,0 +1,201 @@
|
||||
import G6, { Algorithm } from '../../../src';
|
||||
const { breadthFirstSearch } = Algorithm;
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.id = 'container';
|
||||
document.body.appendChild(div);
|
||||
|
||||
const data = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'A',
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
},
|
||||
{
|
||||
id: 'C',
|
||||
},
|
||||
{
|
||||
id: 'D',
|
||||
},
|
||||
{
|
||||
id: 'E',
|
||||
},
|
||||
{
|
||||
id: 'F',
|
||||
},
|
||||
{
|
||||
id: 'G',
|
||||
},
|
||||
{
|
||||
id: 'H',
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: 'A',
|
||||
target: 'B',
|
||||
},
|
||||
{
|
||||
source: 'B',
|
||||
target: 'C',
|
||||
},
|
||||
{
|
||||
source: 'C',
|
||||
target: 'G',
|
||||
},
|
||||
{
|
||||
source: 'A',
|
||||
target: 'D',
|
||||
},
|
||||
{
|
||||
source: 'A',
|
||||
target: 'E',
|
||||
},
|
||||
{
|
||||
source: 'E',
|
||||
target: 'F',
|
||||
},
|
||||
{
|
||||
source: 'F',
|
||||
target: 'D',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('breadthFirstSearch', () => {
|
||||
it('should perform BFS operation on graph', () => {
|
||||
const enterNodeCallback = jest.fn();
|
||||
const leaveNodeCallback = jest.fn();
|
||||
|
||||
const graph = new G6.Graph({
|
||||
container: 'container',
|
||||
width: 500,
|
||||
height: 500,
|
||||
// layout
|
||||
});
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
// Traverse graphs without callbacks first.
|
||||
breadthFirstSearch(graph, 'A');
|
||||
|
||||
// Traverse graph with enterNode and leaveNode callbacks.
|
||||
breadthFirstSearch(graph, 'A', {
|
||||
enter: enterNodeCallback,
|
||||
leave: leaveNodeCallback,
|
||||
});
|
||||
|
||||
expect(enterNodeCallback).toHaveBeenCalledTimes(7);
|
||||
expect(leaveNodeCallback).toHaveBeenCalledTimes(7);
|
||||
|
||||
const nodeA = graph.findById('A');
|
||||
const nodeB = graph.findById('B');
|
||||
const nodeC = graph.findById('C');
|
||||
const nodeD = graph.findById('D');
|
||||
const nodeE = graph.findById('E');
|
||||
const nodeF = graph.findById('F');
|
||||
const nodeG = graph.findById('G');
|
||||
|
||||
const enterNodeParamsMap = [
|
||||
{ currentNode: nodeA, previousNode: null },
|
||||
{ currentNode: nodeB, previousNode: nodeA },
|
||||
{ currentNode: nodeD, previousNode: nodeB },
|
||||
{ currentNode: nodeE, previousNode: nodeD },
|
||||
{ currentNode: nodeC, previousNode: nodeE },
|
||||
{ currentNode: nodeF, previousNode: nodeC },
|
||||
{ currentNode: nodeG, previousNode: nodeF },
|
||||
];
|
||||
|
||||
for (let callIndex = 0; callIndex < 6; callIndex += 1) {
|
||||
const params = enterNodeCallback.mock.calls[callIndex][0];
|
||||
expect(params.current.get('id')).toEqual(enterNodeParamsMap[callIndex].currentNode.get('id'));
|
||||
expect(params.previous && params.previous.get('id')).toEqual(
|
||||
enterNodeParamsMap[callIndex].previousNode &&
|
||||
enterNodeParamsMap[callIndex].previousNode.get('id'),
|
||||
);
|
||||
}
|
||||
|
||||
const leaveNodeParamsMap = [
|
||||
{ currentNode: nodeA, previousNode: null },
|
||||
{ currentNode: nodeB, previousNode: nodeA },
|
||||
{ currentNode: nodeD, previousNode: nodeB },
|
||||
{ currentNode: nodeE, previousNode: nodeD },
|
||||
{ currentNode: nodeC, previousNode: nodeE },
|
||||
{ currentNode: nodeF, previousNode: nodeC },
|
||||
{ currentNode: nodeG, previousNode: nodeF },
|
||||
];
|
||||
|
||||
for (let callIndex = 0; callIndex < 6; callIndex += 1) {
|
||||
const params = leaveNodeCallback.mock.calls[callIndex][0];
|
||||
expect(params.current.get('id')).toEqual(leaveNodeParamsMap[callIndex].currentNode.get('id'));
|
||||
expect(params.previous && params.previous.get('id')).toEqual(
|
||||
leaveNodeParamsMap[callIndex].previousNode &&
|
||||
leaveNodeParamsMap[callIndex].previousNode.get('id'),
|
||||
);
|
||||
}
|
||||
|
||||
graph.destroy();
|
||||
});
|
||||
|
||||
it('should allow to create custom node visiting logic', () => {
|
||||
const graph = new G6.Graph({
|
||||
container: 'container',
|
||||
width: 500,
|
||||
height: 500,
|
||||
// layout
|
||||
});
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
const enterNodeCallback = jest.fn();
|
||||
const leaveNodeCallback = jest.fn();
|
||||
|
||||
// Traverse graph with enterNode and leaveNode callbacks.
|
||||
breadthFirstSearch(graph, 'A', {
|
||||
enter: enterNodeCallback,
|
||||
leave: leaveNodeCallback,
|
||||
allowTraversal: ({ current, next }) => {
|
||||
return !(current.get('id') === 'A' && next.get('id') === 'B');
|
||||
},
|
||||
});
|
||||
|
||||
expect(enterNodeCallback).toHaveBeenCalledTimes(5);
|
||||
expect(leaveNodeCallback).toHaveBeenCalledTimes(5);
|
||||
|
||||
const enterNodeParamsMap = [
|
||||
{ currentNode: 'A', previousNode: null },
|
||||
{ currentNode: 'D', previousNode: 'A' },
|
||||
{ currentNode: 'E', previousNode: 'D' },
|
||||
{ currentNode: 'F', previousNode: 'E' },
|
||||
{ currentNode: 'D', previousNode: 'F' },
|
||||
];
|
||||
|
||||
for (let callIndex = 0; callIndex < 5; callIndex += 1) {
|
||||
const params = enterNodeCallback.mock.calls[callIndex][0];
|
||||
expect(params.current.get('id')).toEqual(enterNodeParamsMap[callIndex].currentNode);
|
||||
expect(params.previous && params.previous.get('id')).toEqual(
|
||||
enterNodeParamsMap[callIndex].previousNode,
|
||||
);
|
||||
}
|
||||
|
||||
const leaveNodeParamsMap = [
|
||||
{ currentNode: 'A', previousNode: null },
|
||||
{ currentNode: 'D', previousNode: 'A' },
|
||||
{ currentNode: 'E', previousNode: 'D' },
|
||||
{ currentNode: 'F', previousNode: 'E' },
|
||||
{ currentNode: 'D', previousNode: 'F' },
|
||||
];
|
||||
|
||||
for (let callIndex = 0; callIndex < 5; callIndex += 1) {
|
||||
const params = leaveNodeCallback.mock.calls[callIndex][0];
|
||||
expect(params.current.get('id')).toEqual(leaveNodeParamsMap[callIndex].currentNode);
|
||||
expect(params.previous && params.previous.get('id')).toEqual(
|
||||
leaveNodeParamsMap[callIndex].previousNode,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
140
packages/core/tests/unit/algorithm/connected-component-spec.ts
Normal file
140
packages/core/tests/unit/algorithm/connected-component-spec.ts
Normal file
@ -0,0 +1,140 @@
|
||||
import G6, { Algorithm } from '../../../src';
|
||||
const { getConnectedComponents } = Algorithm;
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.id = 'container';
|
||||
document.body.appendChild(div);
|
||||
|
||||
const data = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'A',
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
},
|
||||
{
|
||||
id: 'C',
|
||||
},
|
||||
{
|
||||
id: 'D',
|
||||
},
|
||||
{
|
||||
id: 'E',
|
||||
},
|
||||
{
|
||||
id: 'F',
|
||||
},
|
||||
{
|
||||
id: 'G',
|
||||
},
|
||||
{
|
||||
id: 'H',
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: 'A',
|
||||
target: 'B',
|
||||
},
|
||||
{
|
||||
source: 'B',
|
||||
target: 'C',
|
||||
},
|
||||
{
|
||||
source: 'A',
|
||||
target: 'C',
|
||||
},
|
||||
{
|
||||
source: 'D',
|
||||
target: 'A',
|
||||
},
|
||||
{
|
||||
source: 'D',
|
||||
target: 'E',
|
||||
},
|
||||
{
|
||||
source: 'E',
|
||||
target: 'F',
|
||||
},
|
||||
{
|
||||
source: 'F',
|
||||
target: 'D',
|
||||
},
|
||||
{
|
||||
source: 'G',
|
||||
target: 'H',
|
||||
},
|
||||
{
|
||||
source: 'H',
|
||||
target: 'G',
|
||||
},
|
||||
],
|
||||
};
|
||||
data.nodes.forEach((d) => {
|
||||
d['label'] = d.id;
|
||||
});
|
||||
describe('find connected components', () => {
|
||||
const graph = new G6.Graph({
|
||||
container: 'container',
|
||||
width: 500,
|
||||
height: 400,
|
||||
layout: {
|
||||
type: 'force',
|
||||
},
|
||||
modes: {
|
||||
default: ['zoom-canvas', 'drag-canvas', 'drag-node'],
|
||||
},
|
||||
defaultNode: {
|
||||
type: 'node',
|
||||
labelCfg: {
|
||||
style: {
|
||||
fill: '#fff',
|
||||
fontSize: 14,
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultEdge: {
|
||||
style: {
|
||||
endArrow: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
it('detect strongly connected components in undirected graph', () => {
|
||||
let result = getConnectedComponents(graph, false);
|
||||
expect(result.length).toEqual(2);
|
||||
expect(result[0].map((node) => node.get('id')).sort()).toEqual(['A', 'B', 'C', 'D', 'E', 'F']);
|
||||
expect(result[1].map((node) => node.get('id')).sort()).toEqual(['G', 'H']);
|
||||
});
|
||||
it('detect strongly connected components in directed graph', () => {
|
||||
let result = getConnectedComponents(graph, true);
|
||||
expect(result.length).toEqual(5);
|
||||
expect(result[3].map((node) => node.get('id')).sort()).toEqual(['D', 'E', 'F']);
|
||||
expect(result[4].map((node) => node.get('id')).sort()).toEqual(['G', 'H']);
|
||||
});
|
||||
it('test connected components detection performance using large graph', () => {
|
||||
fetch('https://gw.alipayobjects.com/os/basement_prod/da5a1b47-37d6-44d7-8d10-f3e046dabf82.json')
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
data.nodes.forEach((node) => {
|
||||
node.label = node.olabel;
|
||||
node.degree = 0;
|
||||
data.edges.forEach((edge) => {
|
||||
if (edge.source === node.id || edge.target === node.id) {
|
||||
node.degree++;
|
||||
}
|
||||
});
|
||||
});
|
||||
graph.changeData(data);
|
||||
let directedComps = getConnectedComponents(graph, true);
|
||||
let undirectedComps = getConnectedComponents(graph, false);
|
||||
expect(directedComps.length).toEqual(1589);
|
||||
expect(undirectedComps.length).toEqual(396);
|
||||
graph.destroy();
|
||||
});
|
||||
});
|
||||
});
|
550
packages/core/tests/unit/algorithm/detect-cycle-spec.ts
Normal file
550
packages/core/tests/unit/algorithm/detect-cycle-spec.ts
Normal file
@ -0,0 +1,550 @@
|
||||
import G6, { Algorithm } from '../../../src';
|
||||
const { detectDirectedCycle, detectAllCycles } = Algorithm;
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.id = 'container';
|
||||
document.body.appendChild(div);
|
||||
|
||||
const data = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'A',
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
},
|
||||
{
|
||||
id: 'C',
|
||||
},
|
||||
{
|
||||
id: 'D',
|
||||
},
|
||||
{
|
||||
id: 'E',
|
||||
},
|
||||
{
|
||||
id: 'F',
|
||||
},
|
||||
{
|
||||
id: 'G',
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: 'A',
|
||||
target: 'B',
|
||||
},
|
||||
{
|
||||
source: 'B',
|
||||
target: 'C',
|
||||
},
|
||||
{
|
||||
source: 'A',
|
||||
target: 'C',
|
||||
},
|
||||
{
|
||||
source: 'D',
|
||||
target: 'A',
|
||||
},
|
||||
{
|
||||
source: 'D',
|
||||
target: 'E',
|
||||
},
|
||||
{
|
||||
source: 'E',
|
||||
target: 'F',
|
||||
},
|
||||
],
|
||||
};
|
||||
data.nodes.forEach((d) => {
|
||||
d['label'] = d.id;
|
||||
});
|
||||
describe('detectDirectedCycle', () => {
|
||||
const graph = new G6.Graph({
|
||||
container: 'container',
|
||||
width: 500,
|
||||
height: 500,
|
||||
defaultNode: {
|
||||
type: 'node',
|
||||
labelCfg: {
|
||||
style: {
|
||||
fill: '#333',
|
||||
fontSize: 12,
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultEdge: {
|
||||
style: {
|
||||
endArrow: true,
|
||||
},
|
||||
},
|
||||
modes: { default: ['drag-node', 'drag-canvas', 'zoom-canvas', 'lasso-select'] },
|
||||
});
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
it('should detect directed cycle', () => {
|
||||
let result = detectDirectedCycle(graph);
|
||||
// debugger
|
||||
expect(result).toBeNull();
|
||||
|
||||
data.edges.push({
|
||||
source: 'F',
|
||||
target: 'D',
|
||||
});
|
||||
|
||||
graph.changeData(data);
|
||||
// 返回格式:
|
||||
// { currentNodeId: prevNode }
|
||||
result = detectDirectedCycle(graph);
|
||||
|
||||
const nodeF = graph.findById('F');
|
||||
const nodeD = graph.findById('D');
|
||||
const nodeE = graph.findById('E');
|
||||
expect(result).toEqual({
|
||||
D: nodeF,
|
||||
F: nodeE,
|
||||
E: nodeD,
|
||||
});
|
||||
});
|
||||
it('detect all cycles in directed graph', () => {
|
||||
data.edges.push({
|
||||
source: 'C',
|
||||
target: 'D',
|
||||
});
|
||||
graph.changeData(data);
|
||||
graph.render();
|
||||
const result = detectAllCycles(graph, true);
|
||||
expect(result.length).toEqual(3);
|
||||
|
||||
const nodeA = graph.findById('A');
|
||||
const nodeB = graph.findById('B');
|
||||
const nodeC = graph.findById('C');
|
||||
const nodeD = graph.findById('D');
|
||||
|
||||
const result2 = detectAllCycles(graph, true, ['B']);
|
||||
expect(result2.length).toEqual(1);
|
||||
expect(result2[0]).toEqual({
|
||||
A: nodeB,
|
||||
B: nodeC,
|
||||
C: nodeD,
|
||||
D: nodeA,
|
||||
});
|
||||
});
|
||||
it('detect cycle in undirected graph', () => {
|
||||
const result = detectAllCycles(graph);
|
||||
expect(result.length).toEqual(3);
|
||||
const result2 = detectAllCycles(graph, false, ['B'], false);
|
||||
expect(Object.keys(result2[0]).sort()).toEqual(['D', 'E', 'F']);
|
||||
});
|
||||
it('test another graph', () => {
|
||||
const data = {
|
||||
nodes: [
|
||||
{
|
||||
id: '0',
|
||||
label: '0',
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
label: '1',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
label: '2',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
label: '3',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
label: '4',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
label: '5',
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
label: '6',
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
label: '7',
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
label: '8',
|
||||
},
|
||||
{
|
||||
id: '9',
|
||||
label: '9',
|
||||
},
|
||||
{
|
||||
id: '10',
|
||||
label: '10',
|
||||
},
|
||||
{
|
||||
id: '11',
|
||||
label: '11',
|
||||
},
|
||||
{
|
||||
id: '12',
|
||||
label: '12',
|
||||
},
|
||||
{
|
||||
id: '13',
|
||||
label: '13',
|
||||
},
|
||||
{
|
||||
id: '14',
|
||||
label: '14',
|
||||
},
|
||||
{
|
||||
id: '15',
|
||||
label: '15',
|
||||
},
|
||||
{
|
||||
id: '16',
|
||||
label: '16',
|
||||
},
|
||||
{
|
||||
id: '17',
|
||||
label: '17',
|
||||
},
|
||||
{
|
||||
id: '18',
|
||||
label: '18',
|
||||
},
|
||||
{
|
||||
id: '19',
|
||||
label: '19',
|
||||
},
|
||||
{
|
||||
id: '20',
|
||||
label: '20',
|
||||
},
|
||||
{
|
||||
id: '21',
|
||||
label: '21',
|
||||
},
|
||||
{
|
||||
id: '22',
|
||||
label: '22',
|
||||
},
|
||||
{
|
||||
id: '23',
|
||||
label: '23',
|
||||
},
|
||||
{
|
||||
id: '24',
|
||||
label: '24',
|
||||
},
|
||||
{
|
||||
id: '25',
|
||||
label: '25',
|
||||
},
|
||||
{
|
||||
id: '26',
|
||||
label: '26',
|
||||
},
|
||||
{
|
||||
id: '27',
|
||||
label: '27',
|
||||
},
|
||||
{
|
||||
id: '28',
|
||||
label: '28',
|
||||
},
|
||||
{
|
||||
id: '29',
|
||||
label: '29',
|
||||
},
|
||||
{
|
||||
id: '30',
|
||||
label: '30',
|
||||
},
|
||||
{
|
||||
id: '31',
|
||||
label: '31',
|
||||
},
|
||||
{
|
||||
id: '32',
|
||||
label: '32',
|
||||
},
|
||||
{
|
||||
id: '33',
|
||||
label: '33',
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: '0',
|
||||
target: '1',
|
||||
},
|
||||
{
|
||||
source: '0',
|
||||
target: '2',
|
||||
},
|
||||
{
|
||||
source: '3',
|
||||
target: '0',
|
||||
},
|
||||
{
|
||||
source: '0',
|
||||
target: '4',
|
||||
},
|
||||
{
|
||||
source: '5',
|
||||
target: '0',
|
||||
},
|
||||
{
|
||||
source: '0',
|
||||
target: '7',
|
||||
},
|
||||
{
|
||||
source: '0',
|
||||
target: '8',
|
||||
},
|
||||
{
|
||||
source: '0',
|
||||
target: '9',
|
||||
},
|
||||
{
|
||||
source: '0',
|
||||
target: '10',
|
||||
},
|
||||
{
|
||||
source: '0',
|
||||
target: '11',
|
||||
},
|
||||
{
|
||||
source: '0',
|
||||
target: '13',
|
||||
},
|
||||
{
|
||||
source: '14',
|
||||
target: '0',
|
||||
},
|
||||
{
|
||||
source: '0',
|
||||
target: '15',
|
||||
},
|
||||
{
|
||||
source: '0',
|
||||
target: '16',
|
||||
},
|
||||
{
|
||||
source: '2',
|
||||
target: '3',
|
||||
},
|
||||
{
|
||||
source: '4',
|
||||
target: '5',
|
||||
},
|
||||
{
|
||||
source: '4',
|
||||
target: '6',
|
||||
},
|
||||
{
|
||||
source: '5',
|
||||
target: '6',
|
||||
},
|
||||
{
|
||||
source: '7',
|
||||
target: '13',
|
||||
},
|
||||
{
|
||||
source: '8',
|
||||
target: '14',
|
||||
},
|
||||
{
|
||||
source: '9',
|
||||
target: '10',
|
||||
},
|
||||
{
|
||||
source: '10',
|
||||
target: '22',
|
||||
},
|
||||
{
|
||||
source: '10',
|
||||
target: '14',
|
||||
},
|
||||
{
|
||||
source: '10',
|
||||
target: '12',
|
||||
},
|
||||
{
|
||||
source: '10',
|
||||
target: '24',
|
||||
},
|
||||
{
|
||||
source: '10',
|
||||
target: '21',
|
||||
},
|
||||
{
|
||||
source: '10',
|
||||
target: '20',
|
||||
},
|
||||
{
|
||||
source: '11',
|
||||
target: '24',
|
||||
},
|
||||
{
|
||||
source: '11',
|
||||
target: '22',
|
||||
},
|
||||
{
|
||||
source: '11',
|
||||
target: '14',
|
||||
},
|
||||
{
|
||||
source: '12',
|
||||
target: '13',
|
||||
},
|
||||
{
|
||||
source: '16',
|
||||
target: '17',
|
||||
},
|
||||
{
|
||||
source: '16',
|
||||
target: '18',
|
||||
},
|
||||
{
|
||||
source: '16',
|
||||
target: '21',
|
||||
},
|
||||
{
|
||||
source: '16',
|
||||
target: '22',
|
||||
},
|
||||
{
|
||||
source: '17',
|
||||
target: '18',
|
||||
},
|
||||
{
|
||||
source: '17',
|
||||
target: '20',
|
||||
},
|
||||
{
|
||||
source: '18',
|
||||
target: '19',
|
||||
},
|
||||
{
|
||||
source: '19',
|
||||
target: '20',
|
||||
},
|
||||
{
|
||||
source: '19',
|
||||
target: '33',
|
||||
},
|
||||
{
|
||||
source: '19',
|
||||
target: '22',
|
||||
},
|
||||
{
|
||||
source: '19',
|
||||
target: '23',
|
||||
},
|
||||
{
|
||||
source: '20',
|
||||
target: '21',
|
||||
},
|
||||
{
|
||||
source: '21',
|
||||
target: '22',
|
||||
},
|
||||
{
|
||||
source: '22',
|
||||
target: '24',
|
||||
},
|
||||
{
|
||||
source: '22',
|
||||
target: '25',
|
||||
},
|
||||
{
|
||||
source: '22',
|
||||
target: '26',
|
||||
},
|
||||
{
|
||||
source: '22',
|
||||
target: '23',
|
||||
},
|
||||
{
|
||||
source: '22',
|
||||
target: '28',
|
||||
},
|
||||
{
|
||||
source: '22',
|
||||
target: '30',
|
||||
},
|
||||
{
|
||||
source: '22',
|
||||
target: '31',
|
||||
},
|
||||
{
|
||||
source: '22',
|
||||
target: '32',
|
||||
},
|
||||
{
|
||||
source: '22',
|
||||
target: '33',
|
||||
},
|
||||
{
|
||||
source: '23',
|
||||
target: '28',
|
||||
},
|
||||
{
|
||||
source: '23',
|
||||
target: '27',
|
||||
},
|
||||
{
|
||||
source: '23',
|
||||
target: '29',
|
||||
},
|
||||
{
|
||||
source: '23',
|
||||
target: '30',
|
||||
},
|
||||
{
|
||||
source: '23',
|
||||
target: '31',
|
||||
},
|
||||
{
|
||||
source: '23',
|
||||
target: '33',
|
||||
},
|
||||
{
|
||||
source: '32',
|
||||
target: '33',
|
||||
},
|
||||
],
|
||||
};
|
||||
graph.changeData(data);
|
||||
const result = detectAllCycles(graph, true, ['14']);
|
||||
const result2 = detectAllCycles(graph);
|
||||
expect(result.length).toEqual(4);
|
||||
expect(result2.length).toEqual(27);
|
||||
});
|
||||
it('test a large graph', () => {
|
||||
fetch('https://gw.alipayobjects.com/os/basement_prod/da5a1b47-37d6-44d7-8d10-f3e046dabf82.json')
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
data.nodes.forEach((node) => {
|
||||
node.label = node.olabel;
|
||||
node.degree = 0;
|
||||
data.edges.forEach((edge) => {
|
||||
if (edge.source === node.id || edge.target === node.id) {
|
||||
node.degree++;
|
||||
}
|
||||
});
|
||||
});
|
||||
graph.changeData(data);
|
||||
const directedCycles = detectAllCycles(graph, true);
|
||||
expect(directedCycles.length).toEqual(0);
|
||||
const undirectedCycles = detectAllCycles(graph, false, ['1084'], false);
|
||||
expect(undirectedCycles.length).toEqual(1548);
|
||||
});
|
||||
});
|
||||
});
|
197
packages/core/tests/unit/algorithm/dfs-spec.ts
Normal file
197
packages/core/tests/unit/algorithm/dfs-spec.ts
Normal file
@ -0,0 +1,197 @@
|
||||
import G6, { Algorithm } from '../../../src';
|
||||
const { depthFirstSearch } = Algorithm;
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.id = 'container';
|
||||
document.body.appendChild(div);
|
||||
|
||||
const data = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'A',
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
},
|
||||
{
|
||||
id: 'C',
|
||||
},
|
||||
{
|
||||
id: 'D',
|
||||
},
|
||||
{
|
||||
id: 'E',
|
||||
},
|
||||
{
|
||||
id: 'F',
|
||||
},
|
||||
{
|
||||
id: 'G',
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: 'A',
|
||||
target: 'B',
|
||||
},
|
||||
{
|
||||
source: 'B',
|
||||
target: 'C',
|
||||
},
|
||||
{
|
||||
source: 'C',
|
||||
target: 'G',
|
||||
},
|
||||
{
|
||||
source: 'A',
|
||||
target: 'D',
|
||||
},
|
||||
{
|
||||
source: 'A',
|
||||
target: 'E',
|
||||
},
|
||||
{
|
||||
source: 'E',
|
||||
target: 'F',
|
||||
},
|
||||
{
|
||||
source: 'F',
|
||||
target: 'D',
|
||||
},
|
||||
{
|
||||
source: 'D',
|
||||
target: 'G',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('depthFirstSearch', () => {
|
||||
it('should perform DFS operation on graph', () => {
|
||||
const graph = new G6.Graph({
|
||||
container: 'container',
|
||||
width: 500,
|
||||
height: 500,
|
||||
// layout
|
||||
});
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
const enterNodeCallback = jest.fn();
|
||||
const leaveNodeCallback = jest.fn();
|
||||
|
||||
// Traverse graphs without callbacks first to check default ones.
|
||||
depthFirstSearch(graph, 'A');
|
||||
|
||||
// Traverse graph with enterNode and leaveNode callbacks.
|
||||
depthFirstSearch(graph, 'A', {
|
||||
enter: enterNodeCallback,
|
||||
leave: leaveNodeCallback,
|
||||
});
|
||||
|
||||
expect(enterNodeCallback).toHaveBeenCalledTimes(graph.getNodes().length);
|
||||
expect(leaveNodeCallback).toHaveBeenCalledTimes(graph.getNodes().length);
|
||||
|
||||
const enterNodeParamsMap = [
|
||||
{ currentNode: 'A', previousNode: null },
|
||||
{ currentNode: 'B', previousNode: 'A' },
|
||||
{ currentNode: 'C', previousNode: 'B' },
|
||||
{ currentNode: 'G', previousNode: 'C' },
|
||||
{ currentNode: 'D', previousNode: 'A' },
|
||||
{ currentNode: 'E', previousNode: 'A' },
|
||||
{ currentNode: 'F', previousNode: 'E' },
|
||||
];
|
||||
|
||||
for (let callIndex = 0; callIndex < graph.getNodes().length; callIndex += 1) {
|
||||
const params = enterNodeCallback.mock.calls[callIndex][0];
|
||||
expect(params.current.get('id')).toEqual(enterNodeParamsMap[callIndex].currentNode);
|
||||
expect(params.previous && params.previous.get('id')).toEqual(
|
||||
enterNodeParamsMap[callIndex].previousNode,
|
||||
);
|
||||
}
|
||||
|
||||
const leaveNodeParamsMap = [
|
||||
{ currentNode: 'G', previousNode: 'C' },
|
||||
{ currentNode: 'C', previousNode: 'B' },
|
||||
{ currentNode: 'B', previousNode: 'A' },
|
||||
{ currentNode: 'D', previousNode: 'A' },
|
||||
{ currentNode: 'F', previousNode: 'E' },
|
||||
{ currentNode: 'E', previousNode: 'A' },
|
||||
{ currentNode: 'A', previousNode: null },
|
||||
];
|
||||
|
||||
for (let callIndex = 0; callIndex < graph.getNodes().length; callIndex += 1) {
|
||||
const params = leaveNodeCallback.mock.calls[callIndex][0];
|
||||
expect(params.current.get('id')).toEqual(leaveNodeParamsMap[callIndex].currentNode);
|
||||
expect(params.previous && params.previous.get('id')).toEqual(
|
||||
leaveNodeParamsMap[callIndex].previousNode,
|
||||
);
|
||||
}
|
||||
|
||||
graph.destroy();
|
||||
});
|
||||
|
||||
it('allow users to redefine node visiting logic', () => {
|
||||
const graph = new G6.Graph({
|
||||
container: 'container',
|
||||
width: 500,
|
||||
height: 500,
|
||||
// layout
|
||||
});
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
const enterNodeCallback = jest.fn();
|
||||
const leaveNodeCallback = jest.fn();
|
||||
|
||||
depthFirstSearch(graph, 'A', {
|
||||
enter: enterNodeCallback,
|
||||
leave: leaveNodeCallback,
|
||||
allowTraversal: ({ current: currentNode, next: nextNode }) => {
|
||||
return !(currentNode.get('id') === 'A' && nextNode.get('id') === 'B');
|
||||
},
|
||||
});
|
||||
|
||||
expect(enterNodeCallback).toHaveBeenCalledTimes(7);
|
||||
expect(leaveNodeCallback).toHaveBeenCalledTimes(7);
|
||||
|
||||
const enterNodeParamsMap = [
|
||||
{ currentNode: 'A', previousNode: null },
|
||||
{ currentNode: 'D', previousNode: 'A' },
|
||||
{ currentNode: 'G', previousNode: 'D' },
|
||||
{ currentNode: 'E', previousNode: 'A' },
|
||||
{ currentNode: 'F', previousNode: 'E' },
|
||||
{ currentNode: 'D', previousNode: 'F' },
|
||||
{ currentNode: 'G', previousNode: 'D' },
|
||||
];
|
||||
|
||||
for (let callIndex = 0; callIndex < graph.getNodes().length; callIndex += 1) {
|
||||
const params = enterNodeCallback.mock.calls[callIndex][0];
|
||||
expect(params.current.get('id')).toEqual(enterNodeParamsMap[callIndex].currentNode);
|
||||
expect(params.previous && params.previous.get('id')).toEqual(
|
||||
enterNodeParamsMap[callIndex].previousNode,
|
||||
);
|
||||
}
|
||||
|
||||
const leaveNodeParamsMap = [
|
||||
{ currentNode: 'G', previousNode: 'D' },
|
||||
{ currentNode: 'D', previousNode: 'A' },
|
||||
{ currentNode: 'G', previousNode: 'D' },
|
||||
{ currentNode: 'D', previousNode: 'F' },
|
||||
{ currentNode: 'F', previousNode: 'E' },
|
||||
{ currentNode: 'E', previousNode: 'A' },
|
||||
{ currentNode: 'A', previousNode: null },
|
||||
];
|
||||
|
||||
for (let callIndex = 0; callIndex < graph.getNodes().length; callIndex += 1) {
|
||||
const params = leaveNodeCallback.mock.calls[callIndex][0];
|
||||
expect(params.current.get('id')).toEqual(leaveNodeParamsMap[callIndex].currentNode);
|
||||
expect(params.previous && params.previous.get('id')).toEqual(
|
||||
leaveNodeParamsMap[callIndex].previousNode,
|
||||
);
|
||||
}
|
||||
|
||||
graph.destroy();
|
||||
});
|
||||
});
|
116
packages/core/tests/unit/algorithm/find-path-spec.ts
Normal file
116
packages/core/tests/unit/algorithm/find-path-spec.ts
Normal file
@ -0,0 +1,116 @@
|
||||
import G6, { Algorithm } from '../../../src';
|
||||
const { findAllPath, findShortestPath } = Algorithm;
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.id = 'container';
|
||||
document.body.appendChild(div);
|
||||
|
||||
const data = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'A',
|
||||
label: 'A',
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
label: 'B',
|
||||
},
|
||||
{
|
||||
id: 'C',
|
||||
label: 'C',
|
||||
},
|
||||
{
|
||||
id: 'D',
|
||||
label: 'D',
|
||||
},
|
||||
{
|
||||
id: 'E',
|
||||
label: 'E',
|
||||
},
|
||||
{
|
||||
id: 'F',
|
||||
label: 'F',
|
||||
},
|
||||
{
|
||||
id: 'G',
|
||||
label: 'G',
|
||||
},
|
||||
{
|
||||
id: 'H',
|
||||
label: 'H',
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: 'A',
|
||||
target: 'B',
|
||||
},
|
||||
{
|
||||
source: 'B',
|
||||
target: 'C',
|
||||
},
|
||||
{
|
||||
source: 'C',
|
||||
target: 'G',
|
||||
},
|
||||
{
|
||||
source: 'A',
|
||||
target: 'D',
|
||||
},
|
||||
{
|
||||
source: 'A',
|
||||
target: 'E',
|
||||
},
|
||||
{
|
||||
source: 'E',
|
||||
target: 'F',
|
||||
},
|
||||
{
|
||||
source: 'F',
|
||||
target: 'D',
|
||||
},
|
||||
{
|
||||
source: 'D',
|
||||
target: 'E',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('Shortest Path from source to target on graph', () => {
|
||||
const graph = new G6.Graph({
|
||||
container: 'container',
|
||||
width: 500,
|
||||
height: 500,
|
||||
defaultEdge: {
|
||||
style: {
|
||||
endArrow: true,
|
||||
},
|
||||
},
|
||||
modes: {
|
||||
default: ['zoom-canvas', 'drag-canvas', 'drag-node'],
|
||||
},
|
||||
});
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
it('find the shortest path', () => {
|
||||
const { length, path } = findShortestPath(graph, 'A', 'C');
|
||||
expect(length).toBe(2);
|
||||
expect(path).toStrictEqual(['A', 'B', 'C']);
|
||||
});
|
||||
|
||||
it('find all paths', () => {
|
||||
const allPaths = findAllPath(graph, 'A', 'E');
|
||||
expect(allPaths.length).toBe(3);
|
||||
expect(allPaths[0]).toStrictEqual(['A', 'D', 'F', 'E']);
|
||||
expect(allPaths[1]).toStrictEqual(['A', 'D', 'E']);
|
||||
expect(allPaths[2]).toStrictEqual(['A', 'E']);
|
||||
});
|
||||
|
||||
it('find all paths in directed graph', () => {
|
||||
const allPaths = findAllPath(graph, 'A', 'E', true);
|
||||
expect(allPaths.length).toStrictEqual(2);
|
||||
expect(allPaths[0]).toStrictEqual(['A', 'D', 'E']);
|
||||
expect(allPaths[1]).toStrictEqual(['A', 'E']);
|
||||
});
|
||||
});
|
220
packages/core/tests/unit/algorithm/label-propagation-spec.ts
Normal file
220
packages/core/tests/unit/algorithm/label-propagation-spec.ts
Normal file
@ -0,0 +1,220 @@
|
||||
import G6, { Algorithm } from '../../../src';
|
||||
import { GraphData } from '../../../src/types';
|
||||
const { labelPropagation } = Algorithm;
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.id = 'container';
|
||||
document.body.appendChild(div);
|
||||
|
||||
const data1: GraphData = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'A',
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
},
|
||||
{
|
||||
id: 'C',
|
||||
},
|
||||
{
|
||||
id: 'D',
|
||||
},
|
||||
{
|
||||
id: 'E',
|
||||
},
|
||||
{
|
||||
id: 'F',
|
||||
},
|
||||
{
|
||||
id: 'G',
|
||||
},
|
||||
{
|
||||
id: 'H',
|
||||
},
|
||||
{
|
||||
id: 'I',
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: 'A',
|
||||
target: 'B',
|
||||
},
|
||||
{
|
||||
source: 'A',
|
||||
target: 'G',
|
||||
},
|
||||
{
|
||||
source: 'B',
|
||||
target: 'C',
|
||||
},
|
||||
{
|
||||
source: 'C',
|
||||
target: 'G',
|
||||
},
|
||||
{
|
||||
source: 'A',
|
||||
target: 'D',
|
||||
},
|
||||
{
|
||||
source: 'A',
|
||||
target: 'E',
|
||||
},
|
||||
{
|
||||
source: 'E',
|
||||
target: 'F',
|
||||
},
|
||||
{
|
||||
source: 'F',
|
||||
target: 'D',
|
||||
},
|
||||
{
|
||||
source: 'H',
|
||||
target: 'I',
|
||||
},
|
||||
{
|
||||
source: 'A',
|
||||
target: 'I',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let colorMap = {
|
||||
g2: '#BDD2FD',
|
||||
g19: '#BDEFDB',
|
||||
g36: '#F6C3B7',
|
||||
g46: '#FFD8B8',
|
||||
g48: '#D3C6EA',
|
||||
g53: '#CCC',
|
||||
g57: '#a00',
|
||||
g67: '#0a0',
|
||||
g75: '#00a',
|
||||
g76: '#000',
|
||||
};
|
||||
|
||||
const colorArray = [
|
||||
'rgb(91, 143, 249)',
|
||||
'rgb(90, 216, 166)',
|
||||
'rgb(93, 112, 146)',
|
||||
'rgb(246, 189, 22)',
|
||||
'rgb(232, 104, 74)',
|
||||
'rgb(109, 200, 236)',
|
||||
'rgb(146, 112, 202)',
|
||||
'rgb(255, 157, 77)',
|
||||
'rgb(38, 154, 153)',
|
||||
'rgb(227, 137, 163)',
|
||||
];
|
||||
|
||||
describe('label propagation', () => {
|
||||
it('simple label propagation', () => {
|
||||
const data: GraphData = {
|
||||
nodes: [
|
||||
{ id: '0' }, { id: '1' }, { id: '2' }, { id: '3' }, { id: '4' },
|
||||
{ id: '5' }, { id: '6' }, { id: '7' }, { id: '8' }, { id: '9' },
|
||||
{ id: '10' }, { id: '11' }, { id: '12' }, { id: '13' }, { id: '14' },
|
||||
],
|
||||
edges: [
|
||||
{ source: '0', target: '1' }, { source: '0', target: '2' }, { source: '0', target: '3' }, { source: '0', target: '4' },
|
||||
{ source: '1', target: '2' }, { source: '1', target: '3' }, { source: '1', target: '4' },
|
||||
{ source: '2', target: '3' }, { source: '2', target: '4' },
|
||||
{ source: '3', target: '4' },
|
||||
{ source: '0', target: '0' },
|
||||
{ source: '0', target: '0' },
|
||||
{ source: '0', target: '0' },
|
||||
|
||||
{ source: '5', target: '6', weight: 5 }, { source: '5', target: '7' }, { source: '5', target: '8' }, { source: '5', target: '9' },
|
||||
{ source: '6', target: '7' }, { source: '6', target: '8' }, { source: '6', target: '9' },
|
||||
{ source: '7', target: '8' }, { source: '7', target: '9' },
|
||||
{ source: '8', target: '9' },
|
||||
|
||||
{ source: '10', target: '11' }, { source: '10', target: '12' }, { source: '10', target: '13' }, { source: '10', target: '14' },
|
||||
{ source: '11', target: '12' }, { source: '11', target: '13' }, { source: '11', target: '14' },
|
||||
{ source: '12', target: '13' }, { source: '12', target: '14' },
|
||||
{ source: '13', target: '14', weight: 5 },
|
||||
|
||||
{ source: '0', target: '5' },
|
||||
{ source: '5', target: '10' },
|
||||
{ source: '10', target: '0' },
|
||||
{ source: '10', target: '0' },
|
||||
]
|
||||
}
|
||||
const clusteredData = labelPropagation(data, false, 'weight');
|
||||
expect(clusteredData.clusters.length).not.toBe(0);
|
||||
expect(clusteredData.clusterEdges.length).not.toBe(0);
|
||||
|
||||
// to show the graph
|
||||
const clusterMap = {};
|
||||
data.nodes.forEach(node => {
|
||||
clusterMap[node.clusterId as string] = Object.keys(clusterMap).length - 1;
|
||||
});
|
||||
|
||||
data.nodes.forEach(node => {
|
||||
node.label = `${node.id}`;
|
||||
node.style = {
|
||||
fill: colorArray[clusterMap[node.clusterId as string] % colorArray.length]
|
||||
}
|
||||
})
|
||||
|
||||
const graph = new G6.Graph({
|
||||
container: 'container',
|
||||
width: 500,
|
||||
height: 500,
|
||||
modes: {
|
||||
default: ['drag-node', 'drag-canvas']
|
||||
},
|
||||
layout: {
|
||||
type: 'force'
|
||||
}
|
||||
});
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
graph.destroy();
|
||||
});
|
||||
it('label propagation with large graph', () => { // https://gw.alipayobjects.com/os/antvdemo/assets/data/relations.json
|
||||
fetch('https://gw.alipayobjects.com/os/basement_prod/da5a1b47-37d6-44d7-8d10-f3e046dabf82.json')
|
||||
.then((res) => res.json())
|
||||
.then((data) => { // 1589 nodes, 2747 edges
|
||||
const t0 = performance.now();
|
||||
const clusteredData = labelPropagation(data, false, 'weight');
|
||||
const t1 = performance.now();
|
||||
// console.log(`Call to doSomething took ${t1 - t0} milliseconds.`);
|
||||
|
||||
// 9037.91999999521 ms
|
||||
|
||||
expect(clusteredData.clusters.length).toBe(472);
|
||||
expect(clusteredData.clusterEdges.length).toBe(444);
|
||||
|
||||
const clusterMap = {};
|
||||
data.nodes.forEach(node => {
|
||||
clusterMap[node.clusterId] = Object.keys(clusterMap).length - 1;
|
||||
});
|
||||
|
||||
data.nodes.forEach(node => {
|
||||
node.label = `${node.clusterId}`;
|
||||
node.style = {
|
||||
fill: colorArray[clusterMap[node.clusterId] % colorArray.length]
|
||||
}
|
||||
})
|
||||
|
||||
const graph = new G6.Graph({
|
||||
container: 'container',
|
||||
width: 1500,
|
||||
height: 1500,
|
||||
modes: {
|
||||
default: ['drag-node', 'drag-canvas']
|
||||
},
|
||||
defaultNode: {
|
||||
labelCfg: {
|
||||
position: 'right'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
graph.destroy();
|
||||
});
|
||||
});
|
||||
});
|
286
packages/core/tests/unit/algorithm/linkedList-spec.ts
Normal file
286
packages/core/tests/unit/algorithm/linkedList-spec.ts
Normal file
@ -0,0 +1,286 @@
|
||||
import LinkedList, { LinkedListNode } from '../../../src/algorithm/structs/linked-list';
|
||||
|
||||
describe('LinkedListNode', () => {
|
||||
it('create list node with value', () => {
|
||||
const node = new LinkedListNode(1 as any);
|
||||
expect(node.value).toBe(1);
|
||||
expect(node.next).toBeNull();
|
||||
});
|
||||
|
||||
it('crate list node with object', () => {
|
||||
const nodeValue = {
|
||||
value: 1,
|
||||
key: 'test',
|
||||
};
|
||||
|
||||
const node = new LinkedListNode(nodeValue as any);
|
||||
expect(node.value.value).toBe(1);
|
||||
expect(node.value.key).toBe('test');
|
||||
expect(node.next).toBeNull();
|
||||
});
|
||||
|
||||
it('should link nodes together', () => {
|
||||
const node2 = new LinkedListNode(2);
|
||||
const node1 = new LinkedListNode(1, node2);
|
||||
|
||||
expect(node1.next).toBeDefined();
|
||||
expect(node2.next).toBeNull();
|
||||
expect(node1.value).toBe(1);
|
||||
expect(node1.next.value).toBe(2);
|
||||
});
|
||||
|
||||
it('should convert node to string', () => {
|
||||
const node = new LinkedListNode(1);
|
||||
|
||||
expect(node.toString()).toBe('1');
|
||||
|
||||
node.value = 'string value';
|
||||
expect(node.toString()).toBe('string value');
|
||||
});
|
||||
|
||||
it('should convert node to string with custom stringifier', () => {
|
||||
const nodeValue = { value: 1, key: 'test' };
|
||||
const node = new LinkedListNode(nodeValue);
|
||||
const toStringCallback = (value) => `value: ${value.value}, key: ${value.key}`;
|
||||
|
||||
expect(node.toString(toStringCallback)).toBe('value: 1, key: test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('LinkedList', () => {
|
||||
it('should create empty linked list', () => {
|
||||
const linkedList = new LinkedList();
|
||||
expect(linkedList.toString()).toBe('');
|
||||
});
|
||||
|
||||
it('should append node to linked list', () => {
|
||||
const linkedList = new LinkedList();
|
||||
|
||||
expect(linkedList.head).toBeNull();
|
||||
expect(linkedList.tail).toBeNull();
|
||||
|
||||
linkedList.append(1);
|
||||
linkedList.append(2);
|
||||
|
||||
expect(linkedList.toString()).toBe('1,2');
|
||||
expect(linkedList.tail.next).toBeNull();
|
||||
});
|
||||
|
||||
it('should prepend node to linked list', () => {
|
||||
const linkedList = new LinkedList();
|
||||
|
||||
linkedList.prepend(2);
|
||||
expect(linkedList.head.toString()).toBe('2');
|
||||
expect(linkedList.tail.toString()).toBe('2');
|
||||
|
||||
linkedList.append(1);
|
||||
linkedList.prepend(3);
|
||||
|
||||
expect(linkedList.toString()).toBe('3,2,1');
|
||||
});
|
||||
|
||||
it('should delete node by value from linked list', () => {
|
||||
const linkedList = new LinkedList();
|
||||
|
||||
expect(linkedList.delete(5)).toBeNull();
|
||||
|
||||
linkedList.append(1);
|
||||
linkedList.append(1);
|
||||
linkedList.append(2);
|
||||
linkedList.append(3);
|
||||
linkedList.append(3);
|
||||
linkedList.append(3);
|
||||
linkedList.append(4);
|
||||
linkedList.append(5);
|
||||
|
||||
expect(linkedList.head.toString()).toBe('1');
|
||||
expect(linkedList.tail.toString()).toBe('5');
|
||||
|
||||
const deletedNode = linkedList.delete(3);
|
||||
expect(deletedNode.value).toBe(3);
|
||||
expect(linkedList.toString()).toBe('1,1,2,4,5');
|
||||
|
||||
linkedList.delete(3);
|
||||
expect(linkedList.toString()).toBe('1,1,2,4,5');
|
||||
|
||||
linkedList.delete(1);
|
||||
expect(linkedList.toString()).toBe('2,4,5');
|
||||
|
||||
expect(linkedList.head.toString()).toBe('2');
|
||||
expect(linkedList.tail.toString()).toBe('5');
|
||||
|
||||
linkedList.delete(5);
|
||||
expect(linkedList.toString()).toBe('2,4');
|
||||
|
||||
expect(linkedList.head.toString()).toBe('2');
|
||||
expect(linkedList.tail.toString()).toBe('4');
|
||||
|
||||
linkedList.delete(4);
|
||||
expect(linkedList.toString()).toBe('2');
|
||||
|
||||
expect(linkedList.head.toString()).toBe('2');
|
||||
expect(linkedList.tail.toString()).toBe('2');
|
||||
|
||||
linkedList.delete(2);
|
||||
expect(linkedList.toString()).toBe('');
|
||||
});
|
||||
|
||||
it('should delete linked list tail', () => {
|
||||
const linkedList = new LinkedList();
|
||||
|
||||
linkedList.append(1);
|
||||
linkedList.append(2);
|
||||
linkedList.append(3);
|
||||
|
||||
expect(linkedList.head.toString()).toBe('1');
|
||||
expect(linkedList.tail.toString()).toBe('3');
|
||||
|
||||
const deletedNode1 = linkedList.deleteTail();
|
||||
|
||||
expect(deletedNode1.value).toBe(3);
|
||||
expect(linkedList.toString()).toBe('1,2');
|
||||
expect(linkedList.head.toString()).toBe('1');
|
||||
expect(linkedList.tail.toString()).toBe('2');
|
||||
|
||||
const deletedNode2 = linkedList.deleteTail();
|
||||
|
||||
expect(deletedNode2.value).toBe(2);
|
||||
expect(linkedList.toString()).toBe('1');
|
||||
expect(linkedList.head.toString()).toBe('1');
|
||||
expect(linkedList.tail.toString()).toBe('1');
|
||||
|
||||
const deletedNode3 = linkedList.deleteTail();
|
||||
|
||||
expect(deletedNode3.value).toBe(1);
|
||||
expect(linkedList.toString()).toBe('');
|
||||
expect(linkedList.head).toBeNull();
|
||||
expect(linkedList.tail).toBeNull();
|
||||
});
|
||||
|
||||
it('should delete linked list head', () => {
|
||||
const linkedList = new LinkedList();
|
||||
|
||||
expect(linkedList.deleteHead()).toBeNull();
|
||||
|
||||
linkedList.append(1);
|
||||
linkedList.append(2);
|
||||
|
||||
expect(linkedList.head.toString()).toBe('1');
|
||||
expect(linkedList.tail.toString()).toBe('2');
|
||||
|
||||
const deletedNode1 = linkedList.deleteHead();
|
||||
|
||||
expect(deletedNode1.value).toBe(1);
|
||||
expect(linkedList.toString()).toBe('2');
|
||||
expect(linkedList.head.toString()).toBe('2');
|
||||
expect(linkedList.tail.toString()).toBe('2');
|
||||
|
||||
const deletedNode2 = linkedList.deleteHead();
|
||||
|
||||
expect(deletedNode2.value).toBe(2);
|
||||
expect(linkedList.toString()).toBe('');
|
||||
expect(linkedList.head).toBeNull();
|
||||
expect(linkedList.tail).toBeNull();
|
||||
});
|
||||
|
||||
it('should be possible to store objects in the list and to print them out', () => {
|
||||
const linkedList = new LinkedList();
|
||||
|
||||
const nodeValue1 = { value: 1, key: 'key1' };
|
||||
const nodeValue2 = { value: 2, key: 'key2' };
|
||||
|
||||
linkedList.append(nodeValue1).prepend(nodeValue2);
|
||||
|
||||
const nodeStringifier = (value) => `${value.key}:${value.value}`;
|
||||
|
||||
expect(linkedList.toString(nodeStringifier)).toBe('key2:2,key1:1');
|
||||
});
|
||||
|
||||
it('should find node by value', () => {
|
||||
const linkedList = new LinkedList();
|
||||
|
||||
expect(linkedList.find({ value: 5 })).toBeNull();
|
||||
|
||||
linkedList.append(1);
|
||||
expect(linkedList.find({ value: 1 })).toBeDefined();
|
||||
|
||||
linkedList.append(2).append(3);
|
||||
|
||||
const node = linkedList.find({ value: 2 });
|
||||
|
||||
expect(node.value).toBe(2);
|
||||
expect(linkedList.find({ value: 5 })).toBeNull();
|
||||
});
|
||||
|
||||
it('should find node by callback', () => {
|
||||
const linkedList = new LinkedList();
|
||||
|
||||
linkedList
|
||||
.append({ value: 1, key: 'test1' })
|
||||
.append({ value: 2, key: 'test2' })
|
||||
.append({ value: 3, key: 'test3' });
|
||||
|
||||
const node = linkedList.find({ callback: (value) => value.key === 'test2' });
|
||||
|
||||
expect(node).toBeDefined();
|
||||
expect(node.value.value).toBe(2);
|
||||
expect(node.value.key).toBe('test2');
|
||||
expect(linkedList.find({ callback: (value) => value.key === 'test5' })).toBeNull();
|
||||
});
|
||||
|
||||
it('should create linked list from array', () => {
|
||||
const linkedList = new LinkedList();
|
||||
linkedList.fromArray([1, 1, 2, 3, 3, 3, 4, 5]);
|
||||
|
||||
expect(linkedList.toString()).toBe('1,1,2,3,3,3,4,5');
|
||||
});
|
||||
|
||||
it('should find node by means of custom compare function', () => {
|
||||
const comparatorFunction = (a, b) => {
|
||||
if (a.customValue === b.customValue) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return a.customValue < b.customValue ? -1 : 1;
|
||||
};
|
||||
|
||||
const linkedList = new LinkedList(comparatorFunction);
|
||||
|
||||
linkedList
|
||||
.append({ value: 1, customValue: 'test1' })
|
||||
.append({ value: 2, customValue: 'test2' })
|
||||
.append({ value: 3, customValue: 'test3' });
|
||||
|
||||
const node = linkedList.find({
|
||||
value: { value: 2, customValue: 'test2' },
|
||||
});
|
||||
|
||||
expect(node).toBeDefined();
|
||||
expect(node.value.value).toBe(2);
|
||||
expect(node.value.customValue).toBe('test2');
|
||||
expect(linkedList.find({ value: 2, customValue: 'test5' })).toBeNull();
|
||||
});
|
||||
|
||||
it('should reverse linked list', () => {
|
||||
const linkedList = new LinkedList();
|
||||
|
||||
// Add test values to linked list.
|
||||
linkedList.append(1).append(2).append(3);
|
||||
|
||||
expect(linkedList.toString()).toBe('1,2,3');
|
||||
expect(linkedList.head.value).toBe(1);
|
||||
expect(linkedList.tail.value).toBe(3);
|
||||
|
||||
// Reverse linked list.
|
||||
linkedList.reverse();
|
||||
expect(linkedList.toString()).toBe('3,2,1');
|
||||
expect(linkedList.head.value).toBe(3);
|
||||
expect(linkedList.tail.value).toBe(1);
|
||||
|
||||
// Reverse linked list back to initial state.
|
||||
linkedList.reverse();
|
||||
expect(linkedList.toString()).toBe('1,2,3');
|
||||
expect(linkedList.head.value).toBe(1);
|
||||
expect(linkedList.tail.value).toBe(3);
|
||||
});
|
||||
});
|
221
packages/core/tests/unit/algorithm/louvain-spec.ts
Normal file
221
packages/core/tests/unit/algorithm/louvain-spec.ts
Normal file
@ -0,0 +1,221 @@
|
||||
import G6, { Algorithm } from '../../../src';
|
||||
import { GraphData } from '../../../src/types';
|
||||
const { louvain } = Algorithm;
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.id = 'container';
|
||||
document.body.appendChild(div);
|
||||
|
||||
const data1: GraphData = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'A',
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
},
|
||||
{
|
||||
id: 'C',
|
||||
},
|
||||
{
|
||||
id: 'D',
|
||||
},
|
||||
{
|
||||
id: 'E',
|
||||
},
|
||||
{
|
||||
id: 'F',
|
||||
},
|
||||
{
|
||||
id: 'G',
|
||||
},
|
||||
{
|
||||
id: 'H',
|
||||
},
|
||||
{
|
||||
id: 'I',
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: 'A',
|
||||
target: 'B',
|
||||
},
|
||||
{
|
||||
source: 'A',
|
||||
target: 'G',
|
||||
},
|
||||
{
|
||||
source: 'B',
|
||||
target: 'C',
|
||||
},
|
||||
{
|
||||
source: 'C',
|
||||
target: 'G',
|
||||
},
|
||||
{
|
||||
source: 'A',
|
||||
target: 'D',
|
||||
},
|
||||
{
|
||||
source: 'A',
|
||||
target: 'E',
|
||||
},
|
||||
{
|
||||
source: 'E',
|
||||
target: 'F',
|
||||
},
|
||||
{
|
||||
source: 'F',
|
||||
target: 'D',
|
||||
},
|
||||
{
|
||||
source: 'H',
|
||||
target: 'I',
|
||||
},
|
||||
{
|
||||
source: 'A',
|
||||
target: 'I',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let colorMap = {
|
||||
g2: '#BDD2FD',
|
||||
g19: '#BDEFDB',
|
||||
g36: '#F6C3B7',
|
||||
g46: '#FFD8B8',
|
||||
g48: '#D3C6EA',
|
||||
g53: '#CCC',
|
||||
g57: '#a00',
|
||||
g67: '#0a0',
|
||||
g75: '#00a',
|
||||
g76: '#000',
|
||||
};
|
||||
|
||||
const colorArray = [
|
||||
'rgb(91, 143, 249)',
|
||||
'rgb(90, 216, 166)',
|
||||
'rgb(93, 112, 146)',
|
||||
'rgb(246, 189, 22)',
|
||||
'rgb(232, 104, 74)',
|
||||
'rgb(109, 200, 236)',
|
||||
'rgb(146, 112, 202)',
|
||||
'rgb(255, 157, 77)',
|
||||
'rgb(38, 154, 153)',
|
||||
'rgb(227, 137, 163)',
|
||||
];
|
||||
|
||||
describe('louvain', () => {
|
||||
it('simple louvain', () => {
|
||||
const data: GraphData = {
|
||||
nodes: [
|
||||
{ id: '0' }, { id: '1' }, { id: '2' }, { id: '3' }, { id: '4' },
|
||||
{ id: '5' }, { id: '6' }, { id: '7' }, { id: '8' }, { id: '9' },
|
||||
{ id: '10' }, { id: '11' }, { id: '12' }, { id: '13' }, { id: '14' },
|
||||
],
|
||||
edges: [
|
||||
{ source: '0', target: '1' }, { source: '0', target: '2' }, { source: '0', target: '3' }, { source: '0', target: '4' },
|
||||
{ source: '1', target: '2' }, { source: '1', target: '3' }, { source: '1', target: '4' },
|
||||
{ source: '2', target: '3' }, { source: '2', target: '4' },
|
||||
{ source: '3', target: '4' },
|
||||
{ source: '0', target: '0' },
|
||||
{ source: '0', target: '0' },
|
||||
{ source: '0', target: '0' },
|
||||
|
||||
{ source: '5', target: '6', weight: 5 }, { source: '5', target: '7' }, { source: '5', target: '8' }, { source: '5', target: '9' },
|
||||
{ source: '6', target: '7' }, { source: '6', target: '8' }, { source: '6', target: '9' },
|
||||
{ source: '7', target: '8' }, { source: '7', target: '9' },
|
||||
{ source: '8', target: '9' },
|
||||
|
||||
{ source: '10', target: '11' }, { source: '10', target: '12' }, { source: '10', target: '13' }, { source: '10', target: '14' },
|
||||
{ source: '11', target: '12' }, { source: '11', target: '13' }, { source: '11', target: '14' },
|
||||
{ source: '12', target: '13' }, { source: '12', target: '14' },
|
||||
{ source: '13', target: '14', weight: 5 },
|
||||
|
||||
{ source: '0', target: '5' },
|
||||
{ source: '5', target: '10' },
|
||||
{ source: '10', target: '0' },
|
||||
{ source: '10', target: '0' },
|
||||
]
|
||||
}
|
||||
const clusteredData = louvain(data, false, 'weight');
|
||||
expect(clusteredData.clusters.length).toBe(3);
|
||||
expect(clusteredData.clusters[0].sumTot).toBe(3);
|
||||
expect(clusteredData.clusters[1].sumTot).toBe(2);
|
||||
expect(clusteredData.clusterEdges.length).toBe(6);
|
||||
expect(clusteredData.clusterEdges[0].count).toBe(13);
|
||||
expect(clusteredData.clusterEdges[1].count).toBe(10);
|
||||
expect(clusteredData.clusterEdges[1].weight).toBe(14);
|
||||
|
||||
// to show the graph
|
||||
const clusterMap = {};
|
||||
data.nodes.forEach(node => {
|
||||
clusterMap[node.clusterId as string] = Object.keys(clusterMap).length - 1;
|
||||
});
|
||||
|
||||
data.nodes.forEach(node => {
|
||||
node.label = `${node.id}`;
|
||||
node.style = {
|
||||
fill: colorArray[clusterMap[node.clusterId as string] % colorArray.length]
|
||||
}
|
||||
})
|
||||
|
||||
const graph = new G6.Graph({
|
||||
container: 'container',
|
||||
width: 500,
|
||||
height: 500,
|
||||
modes: {
|
||||
default: ['drag-node', 'drag-canvas']
|
||||
},
|
||||
layout: {
|
||||
type: 'force'
|
||||
}
|
||||
});
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
});
|
||||
it('louvain with large graph', () => { // https://gw.alipayobjects.com/os/antvdemo/assets/data/relations.json
|
||||
fetch('https://gw.alipayobjects.com/os/basement_prod/da5a1b47-37d6-44d7-8d10-f3e046dabf82.json')
|
||||
.then((res) => res.json())
|
||||
.then((data) => { // 1589 nodes, 2747 edges
|
||||
const t0 = performance.now();
|
||||
const clusteredData = louvain(data, false, 'weight');
|
||||
const t1 = performance.now();
|
||||
// console.log(`Call to doSomething took ${t1 - t0} milliseconds.`);
|
||||
|
||||
expect(clusteredData.clusters.length).toBe(495);
|
||||
expect(clusteredData.clusterEdges.length).toBe(505);
|
||||
|
||||
const clusterMap = {};
|
||||
data.nodes.forEach(node => {
|
||||
clusterMap[node.clusterId] = Object.keys(clusterMap).length - 1;
|
||||
});
|
||||
|
||||
data.nodes.forEach(node => {
|
||||
node.label = `${node.clusterId}`;
|
||||
node.style = {
|
||||
fill: colorArray[clusterMap[node.clusterId] % colorArray.length]
|
||||
}
|
||||
})
|
||||
|
||||
const graph = new G6.Graph({
|
||||
container: 'container',
|
||||
width: 1500,
|
||||
height: 1500,
|
||||
modes: {
|
||||
default: ['drag-node', 'drag-canvas']
|
||||
},
|
||||
defaultNode: {
|
||||
labelCfg: {
|
||||
position: 'right'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
});
|
||||
});
|
||||
});
|
129
packages/core/tests/unit/algorithm/mst-spec.ts
Normal file
129
packages/core/tests/unit/algorithm/mst-spec.ts
Normal file
@ -0,0 +1,129 @@
|
||||
import G6, { Algorithm } from '../../../src';
|
||||
const { minimumSpanningTree } = Algorithm;
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.id = 'container';
|
||||
document.body.appendChild(div);
|
||||
|
||||
const data = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'A',
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
},
|
||||
{
|
||||
id: 'C',
|
||||
},
|
||||
{
|
||||
id: 'D',
|
||||
},
|
||||
{
|
||||
id: 'E',
|
||||
},
|
||||
{
|
||||
id: 'F',
|
||||
},
|
||||
{
|
||||
id: 'G',
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: 'A',
|
||||
target: 'B',
|
||||
weight: 1,
|
||||
},
|
||||
{
|
||||
source: 'B',
|
||||
target: 'C',
|
||||
weight: 1,
|
||||
},
|
||||
{
|
||||
source: 'A',
|
||||
target: 'C',
|
||||
weight: 2,
|
||||
},
|
||||
{
|
||||
source: 'D',
|
||||
target: 'A',
|
||||
weight: 3,
|
||||
},
|
||||
{
|
||||
source: 'D',
|
||||
target: 'E',
|
||||
weight: 4,
|
||||
},
|
||||
{
|
||||
source: 'E',
|
||||
target: 'F',
|
||||
weight: 2,
|
||||
},
|
||||
{
|
||||
source: 'F',
|
||||
target: 'D',
|
||||
weight: 3,
|
||||
},
|
||||
],
|
||||
};
|
||||
data.nodes.forEach((node) => (node['label'] = node.id));
|
||||
data.edges.forEach((edge) => (edge['label'] = edge.weight));
|
||||
describe('minimumSpanningTree', () => {
|
||||
const graph = new G6.Graph({
|
||||
container: 'container',
|
||||
width: 500,
|
||||
height: 500,
|
||||
layout: {
|
||||
type: 'force',
|
||||
},
|
||||
modes: {
|
||||
default: ['drag-node'],
|
||||
},
|
||||
defaultNode: {
|
||||
labelCfg: {
|
||||
style: {
|
||||
fontSize: 12,
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultEdge: {
|
||||
style: {
|
||||
endArrow: true,
|
||||
},
|
||||
labelCfg: {
|
||||
style: {
|
||||
fontSize: 12,
|
||||
},
|
||||
},
|
||||
},
|
||||
edgeStateStyles: {
|
||||
mst: {
|
||||
stroke: 'red',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
it('test kruskal algorithm', () => {
|
||||
let result = minimumSpanningTree(graph, 'weight');
|
||||
let totalWeight = 0;
|
||||
for (let edge of result) {
|
||||
graph.setItemState(edge, 'mst', true);
|
||||
totalWeight += edge.getModel()['weight'];
|
||||
}
|
||||
expect(totalWeight).toEqual(10);
|
||||
});
|
||||
|
||||
it('test prim algorithm', () => {
|
||||
let result = minimumSpanningTree(graph, 'weight', 'prim');
|
||||
let totalWeight = 0;
|
||||
for (let edge of result) {
|
||||
graph.setItemState(edge, 'mst', true);
|
||||
totalWeight += edge.getModel()['weight'];
|
||||
}
|
||||
expect(totalWeight).toEqual(10);
|
||||
});
|
||||
});
|
157
packages/core/tests/unit/algorithm/pagerank-spec.ts
Normal file
157
packages/core/tests/unit/algorithm/pagerank-spec.ts
Normal file
@ -0,0 +1,157 @@
|
||||
import G6, { Algorithm } from '../../../src';
|
||||
const { getPageRank } = Algorithm;
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.id = 'container';
|
||||
document.body.appendChild(div);
|
||||
|
||||
const data = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'A',
|
||||
label: 'A',
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
label: 'B',
|
||||
},
|
||||
{
|
||||
id: 'C',
|
||||
label: 'C',
|
||||
},
|
||||
{
|
||||
id: 'D',
|
||||
label: 'D',
|
||||
},
|
||||
{
|
||||
id: 'E',
|
||||
label: 'E',
|
||||
},
|
||||
{
|
||||
id: 'F',
|
||||
label: 'F',
|
||||
},
|
||||
{
|
||||
id: 'G',
|
||||
label: 'G',
|
||||
},
|
||||
{
|
||||
id: 'H',
|
||||
label: 'H',
|
||||
},
|
||||
{
|
||||
id: 'I',
|
||||
label: 'I',
|
||||
},
|
||||
{
|
||||
id: 'J',
|
||||
label: 'J',
|
||||
},
|
||||
{
|
||||
id: 'K',
|
||||
label: 'K',
|
||||
}
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: 'D',
|
||||
target: 'A',
|
||||
},
|
||||
{
|
||||
source: 'D',
|
||||
target: 'B',
|
||||
},
|
||||
{
|
||||
source: 'B',
|
||||
target: 'C',
|
||||
},
|
||||
{
|
||||
source: 'C',
|
||||
target: 'B',
|
||||
},
|
||||
{
|
||||
source: 'F',
|
||||
target: 'B',
|
||||
},
|
||||
{
|
||||
source: 'F',
|
||||
target: 'E',
|
||||
},
|
||||
{
|
||||
source: 'E',
|
||||
target: 'F',
|
||||
},
|
||||
{
|
||||
source: 'E',
|
||||
target: 'D',
|
||||
},
|
||||
{
|
||||
source: 'E',
|
||||
target: 'B',
|
||||
},
|
||||
{
|
||||
source: 'K',
|
||||
target: 'E',
|
||||
},
|
||||
{
|
||||
source: 'J',
|
||||
target: 'E',
|
||||
},
|
||||
{
|
||||
source: 'I',
|
||||
target: 'E',
|
||||
},
|
||||
{
|
||||
source: 'H',
|
||||
target: 'E',
|
||||
},
|
||||
{
|
||||
source: 'G',
|
||||
target: 'E',
|
||||
},
|
||||
{
|
||||
source: 'G',
|
||||
target: 'B',
|
||||
},
|
||||
{
|
||||
source: 'H',
|
||||
target: 'B',
|
||||
},
|
||||
{
|
||||
source: 'I',
|
||||
target: 'B',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('Calculate pagerank of graph nodes', () => {
|
||||
const graph = new G6.Graph({
|
||||
container: 'container',
|
||||
width: 500,
|
||||
height: 500,
|
||||
defaultEdge: {
|
||||
style: {
|
||||
endArrow: true,
|
||||
},
|
||||
},
|
||||
modes: {
|
||||
default: ['zoom-canvas', 'drag-canvas', 'drag-node'],
|
||||
},
|
||||
});
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
it('calculate pagerank', () => {
|
||||
const result = getPageRank(graph);
|
||||
let maxNodeId;
|
||||
let maxVal = 0;
|
||||
for (let nodeId in result) {
|
||||
const val = result[nodeId];
|
||||
if (val >= maxVal) {
|
||||
maxNodeId = nodeId;
|
||||
maxVal = val
|
||||
}
|
||||
}
|
||||
expect(maxNodeId).toBe('B')
|
||||
});
|
||||
});
|
65
packages/core/tests/unit/algorithm/queue-spec.ts
Normal file
65
packages/core/tests/unit/algorithm/queue-spec.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import Queue from '../../../src/algorithm/structs/queue';
|
||||
|
||||
describe('Queue', () => {
|
||||
it('should create empty queue', () => {
|
||||
const queue = new Queue();
|
||||
expect(queue).not.toBeNull();
|
||||
expect(queue.linkedList).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should enqueue data to queue', () => {
|
||||
const queue = new Queue();
|
||||
|
||||
queue.enqueue(1);
|
||||
queue.enqueue(2);
|
||||
|
||||
expect(queue.toString()).toBe('1,2');
|
||||
});
|
||||
|
||||
it('should be possible to enqueue/dequeue objects', () => {
|
||||
const queue = new Queue();
|
||||
|
||||
queue.enqueue({ value: 'test1', key: 'key1' });
|
||||
queue.enqueue({ value: 'test2', key: 'key2' });
|
||||
|
||||
const stringifier = (value) => `${value.key}:${value.value}`;
|
||||
|
||||
expect(queue.toString(stringifier)).toBe('key1:test1,key2:test2');
|
||||
expect(queue.dequeue().value).toBe('test1');
|
||||
expect(queue.dequeue().value).toBe('test2');
|
||||
});
|
||||
|
||||
it('should peek data from queue', () => {
|
||||
const queue = new Queue();
|
||||
|
||||
expect(queue.peek()).toBeNull();
|
||||
|
||||
queue.enqueue(1);
|
||||
queue.enqueue(2);
|
||||
|
||||
expect(queue.peek()).toBe(1);
|
||||
expect(queue.peek()).toBe(1);
|
||||
});
|
||||
|
||||
it('should check if queue is empty', () => {
|
||||
const queue = new Queue();
|
||||
|
||||
expect(queue.isEmpty()).toBe(true);
|
||||
|
||||
queue.enqueue(1);
|
||||
|
||||
expect(queue.isEmpty()).toBe(false);
|
||||
});
|
||||
|
||||
it('should dequeue from queue in FIFO order', () => {
|
||||
const queue = new Queue();
|
||||
|
||||
queue.enqueue(1);
|
||||
queue.enqueue(2);
|
||||
|
||||
expect(queue.dequeue()).toBe(1);
|
||||
expect(queue.dequeue()).toBe(2);
|
||||
expect(queue.dequeue()).toBeNull();
|
||||
expect(queue.isEmpty()).toBe(true);
|
||||
});
|
||||
});
|
258
packages/core/tests/unit/algorithm/shortestpath-matrix-spec.ts
Normal file
258
packages/core/tests/unit/algorithm/shortestpath-matrix-spec.ts
Normal file
@ -0,0 +1,258 @@
|
||||
import G6, { Algorithm } from '../../../src';
|
||||
const { floydWarshall } = Algorithm;
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.id = 'container';
|
||||
document.body.appendChild(div);
|
||||
|
||||
const data = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'A',
|
||||
label: '0',
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
label: '1',
|
||||
},
|
||||
{
|
||||
id: 'C',
|
||||
label: '2',
|
||||
},
|
||||
{
|
||||
id: 'D',
|
||||
label: '3',
|
||||
},
|
||||
{
|
||||
id: 'E',
|
||||
label: '4',
|
||||
},
|
||||
{
|
||||
id: 'F',
|
||||
label: '5',
|
||||
},
|
||||
{
|
||||
id: 'G',
|
||||
label: '6',
|
||||
},
|
||||
{
|
||||
id: 'H',
|
||||
label: '7',
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: 'A',
|
||||
target: 'B',
|
||||
},
|
||||
{
|
||||
source: 'B',
|
||||
target: 'C',
|
||||
},
|
||||
{
|
||||
source: 'C',
|
||||
target: 'G',
|
||||
},
|
||||
{
|
||||
source: 'A',
|
||||
target: 'D',
|
||||
},
|
||||
{
|
||||
source: 'A',
|
||||
target: 'E',
|
||||
},
|
||||
{
|
||||
source: 'E',
|
||||
target: 'F',
|
||||
},
|
||||
{
|
||||
source: 'F',
|
||||
target: 'D',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('Shortest Path Matrix on graph', () => {
|
||||
const graph = new G6.Graph({
|
||||
container: 'container',
|
||||
width: 500,
|
||||
height: 500,
|
||||
defaultEdge: {
|
||||
style: {
|
||||
endArrow: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
it('get graph shortest path matrix', () => {
|
||||
const matrix = graph.getShortestPathMatrix();
|
||||
expect(Object.keys(matrix).length).toBe(8);
|
||||
const node0 = matrix[0];
|
||||
expect(node0.length).toBe(8);
|
||||
expect(node0[0]).toBe(0);
|
||||
expect(node0[1]).toBe(1);
|
||||
expect(node0[2]).toBe(2);
|
||||
expect(node0[3]).toBe(1);
|
||||
expect(node0[4]).toBe(1);
|
||||
expect(node0[5]).toBe(2);
|
||||
expect(node0[6]).toBe(3);
|
||||
expect(node0[7]).toBe(Infinity);
|
||||
expect(matrix[1][7]).toBe(Infinity);
|
||||
expect(matrix[2][7]).toBe(Infinity);
|
||||
expect(matrix[3][7]).toBe(Infinity);
|
||||
});
|
||||
|
||||
it('add items and cache', () => {
|
||||
graph.addItem('node', {
|
||||
id: 'I',
|
||||
label: '8',
|
||||
});
|
||||
graph.addItem('edge', {
|
||||
source: 'I',
|
||||
target: 'A',
|
||||
});
|
||||
graph.addItem('edge', {
|
||||
source: 'C',
|
||||
target: 'A',
|
||||
});
|
||||
// use the cache
|
||||
const cachedMatrix = graph.getShortestPathMatrix();
|
||||
expect(Object.keys(cachedMatrix).length).toBe(8);
|
||||
const cachedNode0 = cachedMatrix[0];
|
||||
expect(cachedNode0.length).toBe(8);
|
||||
expect(cachedNode0[0]).toBe(0);
|
||||
expect(cachedNode0[1]).toBe(1);
|
||||
expect(cachedNode0[2]).toBe(2);
|
||||
expect(cachedNode0[3]).toBe(1);
|
||||
expect(cachedNode0[4]).toBe(1);
|
||||
expect(cachedNode0[5]).toBe(2);
|
||||
expect(cachedNode0[6]).toBe(3);
|
||||
expect(cachedNode0[7]).toBe(Infinity);
|
||||
expect(cachedMatrix[1][7]).toBe(Infinity);
|
||||
expect(cachedMatrix[2][7]).toBe(Infinity);
|
||||
expect(cachedMatrix[3][7]).toBe(Infinity);
|
||||
|
||||
// do not use the cache
|
||||
const matrix = graph.getShortestPathMatrix(false);
|
||||
expect(Object.keys(matrix).length).toBe(9);
|
||||
const node0 = matrix[0];
|
||||
expect(node0.length).toBe(9);
|
||||
expect(node0[0]).toBe(0);
|
||||
expect(node0[1]).toBe(1);
|
||||
expect(node0[2]).toBe(1);
|
||||
expect(node0[3]).toBe(1);
|
||||
expect(node0[4]).toBe(1);
|
||||
expect(node0[5]).toBe(2);
|
||||
expect(node0[6]).toBe(2);
|
||||
expect(node0[7]).toBe(Infinity);
|
||||
expect(node0[8]).toBe(1);
|
||||
});
|
||||
|
||||
it('directed', () => {
|
||||
// do not use the cache and directed
|
||||
const matrix = graph.getShortestPathMatrix(false, true);
|
||||
expect(Object.keys(matrix).length).toBe(9);
|
||||
const node0 = matrix[0];
|
||||
expect(node0.length).toBe(9);
|
||||
expect(node0[0]).toBe(0);
|
||||
expect(node0[1]).toBe(1);
|
||||
expect(node0[2]).toBe(2);
|
||||
expect(node0[3]).toBe(1);
|
||||
expect(node0[4]).toBe(1);
|
||||
expect(node0[5]).toBe(2);
|
||||
expect(node0[6]).toBe(3);
|
||||
expect(node0[7]).toBe(Infinity);
|
||||
expect(node0[8]).toBe(Infinity);
|
||||
const node8 = matrix[8];
|
||||
expect(node8.length).toBe(9);
|
||||
expect(node8[0]).toBe(1);
|
||||
graph.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Adjacency Matrix by Algorithm', () => {
|
||||
const graph = new G6.Graph({
|
||||
container: 'container',
|
||||
width: 500,
|
||||
height: 500,
|
||||
defaultEdge: {
|
||||
style: {
|
||||
endArrow: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
it('get graph shortestpath matrix', () => {
|
||||
const matrix = floydWarshall(graph);
|
||||
expect(Object.keys(matrix).length).toBe(8);
|
||||
const node0 = matrix[0];
|
||||
expect(node0.length).toBe(8);
|
||||
expect(node0[0]).toBe(0);
|
||||
expect(node0[1]).toBe(1);
|
||||
expect(node0[2]).toBe(2);
|
||||
expect(node0[3]).toBe(1);
|
||||
expect(node0[4]).toBe(1);
|
||||
expect(node0[5]).toBe(2);
|
||||
expect(node0[6]).toBe(3);
|
||||
expect(node0[7]).toBe(Infinity);
|
||||
expect(matrix[1][7]).toBe(Infinity);
|
||||
expect(matrix[2][7]).toBe(Infinity);
|
||||
expect(matrix[3][7]).toBe(Infinity);
|
||||
});
|
||||
|
||||
it('add items and cache', () => {
|
||||
graph.addItem('node', {
|
||||
id: 'I',
|
||||
label: '8',
|
||||
});
|
||||
graph.addItem('edge', {
|
||||
source: 'I',
|
||||
target: 'A',
|
||||
});
|
||||
graph.addItem('edge', {
|
||||
source: 'C',
|
||||
target: 'A',
|
||||
});
|
||||
|
||||
const matrix = floydWarshall(graph);
|
||||
expect(Object.keys(matrix).length).toBe(9);
|
||||
const node0 = matrix[0];
|
||||
expect(node0.length).toBe(9);
|
||||
expect(node0[0]).toBe(0);
|
||||
expect(node0[1]).toBe(1);
|
||||
expect(node0[2]).toBe(1);
|
||||
expect(node0[3]).toBe(1);
|
||||
expect(node0[4]).toBe(1);
|
||||
expect(node0[5]).toBe(2);
|
||||
expect(node0[6]).toBe(2);
|
||||
expect(node0[7]).toBe(Infinity);
|
||||
expect(node0[8]).toBe(1);
|
||||
});
|
||||
|
||||
it('directed', () => {
|
||||
// directed
|
||||
const matrix = floydWarshall(graph, true);
|
||||
expect(Object.keys(matrix).length).toBe(9);
|
||||
const node0 = matrix[0];
|
||||
expect(node0.length).toBe(9);
|
||||
expect(node0[0]).toBe(0);
|
||||
expect(node0[1]).toBe(1);
|
||||
expect(node0[2]).toBe(2);
|
||||
expect(node0[3]).toBe(1);
|
||||
expect(node0[4]).toBe(1);
|
||||
expect(node0[5]).toBe(2);
|
||||
expect(node0[6]).toBe(3);
|
||||
expect(node0[7]).toBe(Infinity);
|
||||
expect(node0[8]).toBe(Infinity);
|
||||
const node8 = matrix[8];
|
||||
expect(node8.length).toBe(9);
|
||||
expect(node8[0]).toBe(1);
|
||||
graph.destroy();
|
||||
});
|
||||
});
|
68
packages/core/tests/unit/algorithm/stack-spec.ts
Normal file
68
packages/core/tests/unit/algorithm/stack-spec.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import Stack from '../../../src/algorithm/structs/stack';
|
||||
|
||||
describe('stack unit test', () => {
|
||||
it('init stack', () => {
|
||||
const stack = new Stack();
|
||||
for (let i = 0; i < 4; i++) {
|
||||
stack.push({
|
||||
nodes: [
|
||||
{
|
||||
id: `node${i}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const result = stack.pop();
|
||||
// console.log(stack.toArray());
|
||||
expect(result).toEqual({
|
||||
nodes: [
|
||||
{
|
||||
id: 'node3',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(stack.peek()).toEqual({
|
||||
nodes: [
|
||||
{
|
||||
id: 'node2',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(stack.isMaxStack()).toBe(false);
|
||||
expect(stack.isEmpty()).toBe(false);
|
||||
|
||||
stack.push({
|
||||
nodes: [
|
||||
{
|
||||
id: 'node5',
|
||||
},
|
||||
],
|
||||
});
|
||||
stack.push({
|
||||
nodes: [
|
||||
{
|
||||
id: 'node6',
|
||||
},
|
||||
],
|
||||
});
|
||||
// console.log(stack.toArray());
|
||||
expect(stack.isMaxStack()).toBe(false);
|
||||
});
|
||||
|
||||
it('init stack with maxStep', () => {
|
||||
const stack = new Stack(3);
|
||||
for (let i = 0; i < 5; i++) {
|
||||
stack.push({
|
||||
nodes: [
|
||||
{
|
||||
id: `node${i}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
expect(stack.length).toBe(3);
|
||||
});
|
||||
});
|
294
packages/core/tests/unit/behavior/activate-relations-spec.ts
Normal file
294
packages/core/tests/unit/behavior/activate-relations-spec.ts
Normal file
@ -0,0 +1,294 @@
|
||||
import '../../../src/behavior';
|
||||
import '../../../src/shape';
|
||||
import Graph from '../../../src/graph/graph';
|
||||
|
||||
describe('activate-relations', () => {
|
||||
const div = document.createElement('div');
|
||||
div.id = 'activate-relations-spec';
|
||||
document.body.appendChild(div);
|
||||
const graph = new Graph({
|
||||
container: div,
|
||||
width: 500,
|
||||
height: 500,
|
||||
modes: { default: [] },
|
||||
defaultNode: {
|
||||
style: {
|
||||
stroke: '#f00',
|
||||
},
|
||||
},
|
||||
nodeStateStyles: {
|
||||
active: {
|
||||
fillOpacity: 0.8,
|
||||
lineWidth: 3,
|
||||
},
|
||||
selected: {
|
||||
lineWidth: 5,
|
||||
},
|
||||
inactive: {
|
||||
lineWidth: 1,
|
||||
},
|
||||
},
|
||||
edgeStateStyles: {
|
||||
active: {
|
||||
strokeOpacity: 0.8,
|
||||
lineWidth: 3,
|
||||
},
|
||||
inactive: {
|
||||
lineWidth: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const node1 = graph.addItem('node', { id: 'node1', x: 100, y: 100, label: 'node1' });
|
||||
const node2 = graph.addItem('node', { id: 'node2', x: 200, y: 200, label: 'node2' });
|
||||
graph.addItem('node', { id: 'node3', x: 80, y: 250, label: 'node3' });
|
||||
graph.addItem('edge', { source: 'node1', target: 'node2' });
|
||||
graph.addItem('edge', { source: 'node1', target: 'node3' });
|
||||
it('default activate', (done) => {
|
||||
graph.on('afteractivaterelations', (e) => {
|
||||
const action = e.action;
|
||||
if (e.item === node1) {
|
||||
if (action === 'activate') {
|
||||
const nodes = graph.findAllByState('node', 'active');
|
||||
const edges = graph.findAllByState('edge', 'active');
|
||||
expect(nodes.length).toEqual(3);
|
||||
expect(edges.length).toEqual(2);
|
||||
expect(graph.findAllByState('node', 'inactive').length).toEqual(0);
|
||||
expect(graph.findAllByState('edge', 'inactive').length).toEqual(0);
|
||||
const keyShape = node2.getKeyShape();
|
||||
expect(keyShape.attr('fillOpacity')).toEqual(0.8);
|
||||
} else {
|
||||
const nodes = graph.findAllByState('node', 'active');
|
||||
const edges = graph.findAllByState('edge', 'active');
|
||||
expect(nodes.length).toEqual(0);
|
||||
expect(edges.length).toEqual(0);
|
||||
expect(graph.findAllByState('node', 'inactive').length).toEqual(0);
|
||||
expect(graph.findAllByState('edge', 'inactive').length).toEqual(0);
|
||||
}
|
||||
} else {
|
||||
if (action === 'activate') {
|
||||
const nodes = graph.findAllByState('node', 'active');
|
||||
const edges = graph.findAllByState('edge', 'active');
|
||||
expect(nodes.length).toEqual(2);
|
||||
expect(edges.length).toEqual(1);
|
||||
expect(graph.findAllByState('node', 'inactive').length).toEqual(1);
|
||||
expect(graph.findAllByState('edge', 'inactive').length).toEqual(1);
|
||||
const keyShape = node1.getKeyShape();
|
||||
expect(keyShape.attr('fillOpacity')).toEqual(0.8);
|
||||
} else {
|
||||
const nodes = graph.findAllByState('node', 'active');
|
||||
const edges = graph.findAllByState('edge', 'active');
|
||||
expect(nodes.length).toEqual(0);
|
||||
expect(edges.length).toEqual(0);
|
||||
expect(graph.findAllByState('node', 'inactive').length).toEqual(0);
|
||||
expect(graph.findAllByState('edge', 'inactive').length).toEqual(0);
|
||||
}
|
||||
}
|
||||
done();
|
||||
});
|
||||
graph.addBehaviors(['activate-relations'], 'default');
|
||||
graph.emit('node:mouseenter', { item: node1 });
|
||||
graph.emit('node:mouseleave', { item: node1 });
|
||||
graph.emit('node:mouseenter', { item: node2 });
|
||||
graph.emit('node:mouseleave', { item: node2 });
|
||||
graph.removeBehaviors(['activate-relations'], 'default');
|
||||
// graph.removeEvent();
|
||||
graph.off('afteractivaterelations');
|
||||
});
|
||||
it('click to activate', (done) => {
|
||||
graph.on('afteractivaterelations', (e) => {
|
||||
const action = e.action;
|
||||
if (e.item === node1) {
|
||||
if (action === 'activate') {
|
||||
const nodes = graph.findAllByState('node', 'active');
|
||||
const edges = graph.findAllByState('edge', 'active');
|
||||
expect(nodes.length).toEqual(3);
|
||||
expect(edges.length).toEqual(2);
|
||||
expect(graph.findAllByState('node', 'inactive').length).toEqual(0);
|
||||
expect(graph.findAllByState('edge', 'inactive').length).toEqual(0);
|
||||
const keyShape = node2.getKeyShape();
|
||||
expect(keyShape.attr('fillOpacity')).toEqual(0.8);
|
||||
} else {
|
||||
const nodes = graph.findAllByState('node', 'active');
|
||||
const edges = graph.findAllByState('edge', 'active');
|
||||
expect(nodes.length).toEqual(0);
|
||||
expect(edges.length).toEqual(0);
|
||||
expect(graph.findAllByState('node', 'inactive').length).toEqual(0);
|
||||
expect(graph.findAllByState('edge', 'inactive').length).toEqual(0);
|
||||
}
|
||||
} else {
|
||||
if (action === 'activate') {
|
||||
const nodes = graph.findAllByState('node', 'active');
|
||||
const edges = graph.findAllByState('edge', 'active');
|
||||
expect(nodes.length).toEqual(2);
|
||||
expect(edges.length).toEqual(1);
|
||||
expect(graph.findAllByState('node', 'inactive').length).toEqual(1);
|
||||
expect(graph.findAllByState('edge', 'inactive').length).toEqual(1);
|
||||
const keyShape = node1.getKeyShape();
|
||||
expect(keyShape.attr('fillOpacity')).toEqual(0.8);
|
||||
} else {
|
||||
const nodes = graph.findAllByState('node', 'active');
|
||||
const edges = graph.findAllByState('edge', 'active');
|
||||
expect(nodes.length).toEqual(0);
|
||||
expect(edges.length).toEqual(0);
|
||||
expect(graph.findAllByState('node', 'inactive').length).toEqual(0);
|
||||
expect(graph.findAllByState('edge', 'inactive').length).toEqual(0);
|
||||
done();
|
||||
}
|
||||
}
|
||||
});
|
||||
graph.addBehaviors(
|
||||
[
|
||||
{
|
||||
type: 'activate-relations',
|
||||
trigger: 'click',
|
||||
},
|
||||
],
|
||||
'default',
|
||||
);
|
||||
|
||||
graph.emit('node:click', { item: node1 });
|
||||
graph.emit('canvas:click', {});
|
||||
graph.emit('node:click', { item: node2 });
|
||||
graph.emit('canvas:click', {});
|
||||
graph.removeBehaviors(['activate-relations'], 'default');
|
||||
graph.off('afteractivaterelations');
|
||||
});
|
||||
it('custom state', (done) => {
|
||||
const graph2 = new Graph({
|
||||
container: div,
|
||||
width: 500,
|
||||
height: 500,
|
||||
modes: { default: [] },
|
||||
nodeStateStyles: {
|
||||
highlight: {},
|
||||
inactive: {},
|
||||
active: {},
|
||||
},
|
||||
edgeStateStyles: {
|
||||
highlight: {},
|
||||
inactive: {},
|
||||
active: {},
|
||||
},
|
||||
});
|
||||
const g2node1 = graph2.addItem('node', { id: 'node1', x: 100, y: 100, label: 'node1' });
|
||||
const g2node2 = graph2.addItem('node', { id: 'node2', x: 200, y: 200, label: 'node2' });
|
||||
graph2.addItem('node', { id: 'node3', x: 80, y: 150, label: 'node3' });
|
||||
graph2.addItem('edge', { source: 'node1', target: 'node2' });
|
||||
graph2.addItem('edge', { source: 'node1', target: 'node3' });
|
||||
graph2.on('afteractivaterelations', (e) => {
|
||||
const action = e.action;
|
||||
if (e.item === g2node1) {
|
||||
if (action === 'activate') {
|
||||
const nodes = graph2.findAllByState('node', 'highlight');
|
||||
const edges = graph2.findAllByState('edge', 'highlight');
|
||||
expect(nodes.length).toEqual(3);
|
||||
expect(edges.length).toEqual(2);
|
||||
expect(graph2.findAllByState('node', 'inactive').length).toEqual(0);
|
||||
expect(graph2.findAllByState('edge', 'inactive').length).toEqual(0);
|
||||
} else {
|
||||
const nodes = graph2.findAllByState('node', 'highlight');
|
||||
const edges = graph2.findAllByState('edge', 'highlight');
|
||||
expect(nodes.length).toEqual(0);
|
||||
expect(edges.length).toEqual(0);
|
||||
}
|
||||
} else {
|
||||
if (action === 'activate') {
|
||||
const nodes = graph2.findAllByState('node', 'highlight');
|
||||
const edges = graph2.findAllByState('edge', 'highlight');
|
||||
expect(nodes.length).toEqual(2);
|
||||
expect(edges.length).toEqual(1);
|
||||
expect(graph2.findAllByState('node', 'inactive').length).toEqual(0);
|
||||
expect(graph2.findAllByState('edge', 'inactive').length).toEqual(0);
|
||||
} else {
|
||||
const nodes = graph2.findAllByState('node', 'highlight');
|
||||
const edges = graph2.findAllByState('edge', 'highlight');
|
||||
expect(nodes.length).toEqual(0);
|
||||
expect(edges.length).toEqual(0);
|
||||
done();
|
||||
}
|
||||
}
|
||||
});
|
||||
graph2.addBehaviors(
|
||||
[
|
||||
{
|
||||
type: 'activate-relations',
|
||||
activeState: 'highlight',
|
||||
inactiveState: null,
|
||||
},
|
||||
],
|
||||
'default',
|
||||
);
|
||||
graph2.emit('node:mouseenter', { item: g2node1 });
|
||||
graph2.emit('node:mouseleave', { item: g2node1 });
|
||||
graph2.emit('node:mouseenter', { item: g2node2 });
|
||||
graph2.emit('node:mouseleave', { item: g2node2 });
|
||||
graph2.destroy();
|
||||
});
|
||||
it('should not update', () => {
|
||||
graph.addBehaviors(
|
||||
[
|
||||
{
|
||||
type: 'activate-relations',
|
||||
trigger: 'click',
|
||||
shouldUpdate() {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
],
|
||||
'default',
|
||||
);
|
||||
graph.emit('node:click', { item: node1 });
|
||||
let nodes = graph.findAllByState('node', 'active');
|
||||
let edges = graph.findAllByState('edge', 'active');
|
||||
expect(nodes.length).toEqual(0);
|
||||
expect(edges.length).toEqual(0);
|
||||
graph.emit('canvas:click', {});
|
||||
graph.emit('node:click', { item: node2 });
|
||||
nodes = graph.findAllByState('node', 'active');
|
||||
edges = graph.findAllByState('edge', 'active');
|
||||
expect(nodes.length).toEqual(0);
|
||||
expect(edges.length).toEqual(0);
|
||||
graph.emit('canvas:click', {});
|
||||
graph.removeBehaviors(['activate-relations'], 'default');
|
||||
graph.off('node:click');
|
||||
graph.off('canvas:click');
|
||||
});
|
||||
it('combine selected state', () => {
|
||||
graph.addBehaviors(
|
||||
[
|
||||
{
|
||||
type: 'activate-relations',
|
||||
trigger: 'mouseenter',
|
||||
resetSelected: true,
|
||||
},
|
||||
],
|
||||
'default',
|
||||
);
|
||||
graph.addBehaviors(
|
||||
[
|
||||
{
|
||||
type: 'click-select',
|
||||
},
|
||||
],
|
||||
'default',
|
||||
);
|
||||
|
||||
graph.emit('node:click', { item: node1 });
|
||||
let nodes = graph.findAllByState('node', 'selected');
|
||||
expect(nodes.length).toEqual(1);
|
||||
graph.emit('node:mouseenter', { item: node2 });
|
||||
nodes = graph.findAllByState('node', 'selected');
|
||||
expect(nodes.length).toEqual(0);
|
||||
nodes = graph.findAllByState('node', 'active');
|
||||
const edges = graph.findAllByState('edge', 'active');
|
||||
expect(nodes.length).toEqual(2);
|
||||
expect(edges.length).toEqual(1);
|
||||
graph.emit('node:click', { item: node1 });
|
||||
nodes = graph.findAllByState('node', 'selected');
|
||||
expect(nodes.length).toEqual(1);
|
||||
graph.emit('node:mouseleave', {});
|
||||
graph.removeBehaviors(['activate-relations'], 'default');
|
||||
graph.destroy();
|
||||
});
|
||||
});
|
179
packages/core/tests/unit/behavior/brush-select-spec.ts
Normal file
179
packages/core/tests/unit/behavior/brush-select-spec.ts
Normal file
@ -0,0 +1,179 @@
|
||||
import '../../../src/behavior';
|
||||
import '../../../src/shape';
|
||||
import Graph from '../../../src/graph/graph';
|
||||
|
||||
describe('brush-select', () => {
|
||||
const div = document.createElement('div');
|
||||
div.id = 'activate-relations-spec';
|
||||
document.body.appendChild(div);
|
||||
const graph = new Graph({
|
||||
container: div,
|
||||
width: 500,
|
||||
height: 500,
|
||||
modes: { default: ['zoom-canvas'] },
|
||||
nodeStateStyles: {
|
||||
selected: {},
|
||||
customState: {},
|
||||
},
|
||||
edgeStateStyles: {
|
||||
selected: {},
|
||||
customState: {},
|
||||
},
|
||||
});
|
||||
const node1 = graph.addItem('node', { id: 'node1', x: 100, y: 100, label: 'node1' });
|
||||
const node2 = graph.addItem('node', { id: 'node2', x: 200, y: 200, label: 'node2' });
|
||||
const node3 = graph.addItem('node', { id: 'node3', x: 80, y: 150, label: 'node3' });
|
||||
graph.addItem('edge', { source: 'node1', target: 'node2' });
|
||||
const edge1 = graph.addItem('edge', { source: 'node1', target: 'node3' });
|
||||
it('default configs', () => {
|
||||
graph.addBehaviors(['brush-select'], 'default');
|
||||
graph.emit('keydown', { canvasX: 20, canvasY: 20, key: 'shift' });
|
||||
graph.emit('dragstart', { canvasX: 20, canvasY: 20 });
|
||||
graph.emit('drag', { canvasX: 120, canvasY: 120 });
|
||||
// 只选中一个节点,没有边被选中
|
||||
graph.emit('dragend', { canvasX: 120, canvasY: 120, x: 120, y: 120 });
|
||||
let selectedNodes = graph.findAllByState('node', 'selected');
|
||||
expect(selectedNodes.length).toEqual(1);
|
||||
expect(selectedNodes[0] === node1).toBe(true);
|
||||
|
||||
let selectedEdges = graph.findAllByState('edge', 'selected');
|
||||
expect(selectedEdges.length).toEqual(0);
|
||||
|
||||
// 选中两个节点,一条边
|
||||
graph.emit('keydown', { canvasX: 20, canvasY: 20 });
|
||||
graph.emit('dragstart', { canvasX: 20, canvasY: 20 });
|
||||
graph.emit('dragend', { canvasX: 120, canvasY: 160, x: 120, y: 160 });
|
||||
|
||||
selectedNodes = graph.findAllByState('node', 'selected');
|
||||
expect(selectedNodes.length).toEqual(2);
|
||||
expect(selectedNodes[1] === node3).toBe(true);
|
||||
|
||||
selectedEdges = graph.findAllByState('edge', 'selected');
|
||||
expect(selectedEdges.length).toEqual(1);
|
||||
expect(selectedEdges[0] === edge1).toBe(true);
|
||||
|
||||
graph.emit('canvas:click');
|
||||
selectedNodes = graph.findAllByState('node', 'selected');
|
||||
expect(selectedNodes.length).toEqual(0);
|
||||
|
||||
selectedEdges = graph.findAllByState('edge', 'selected');
|
||||
expect(selectedEdges.length).toEqual(0);
|
||||
|
||||
graph.translate(200, 200);
|
||||
graph.emit('keydown', { canvasX: 20, canvasY: 20, key: 'abc' }); // invalid key
|
||||
graph.emit('dragstart', { canvasX: 20, canvasY: 20 });
|
||||
graph.emit('drag', { canvasX: 120, canvasY: 120 });
|
||||
graph.emit('keyup', { key: 'shift' });
|
||||
graph.emit('dragend', { canvasX: 120, canvasY: 120, x: -80, y: -80 });
|
||||
selectedNodes = graph.findAllByState('node', 'selected');
|
||||
expect(selectedNodes.length).toEqual(0);
|
||||
|
||||
graph.emit('keydown', { canvasX: 20, canvasY: 20, key: 'shift' });
|
||||
graph.emit('dragstart', { canvasX: 20, canvasY: 20 });
|
||||
graph.emit('drag', { canvasX: 120, canvasY: 120 });
|
||||
graph.emit('keyup', { key: 'shift' });
|
||||
selectedNodes = graph.findAllByState('node', 'selected');
|
||||
expect(selectedNodes.length).toEqual(0);
|
||||
|
||||
// translate 到原点,以防影响后续的测试
|
||||
graph.translate(-200, -200);
|
||||
graph.removeBehaviors(['brush-select'], 'default');
|
||||
});
|
||||
it('modify cfgs', () => {
|
||||
let triggered = false;
|
||||
graph.translate(-200, -200);
|
||||
graph.addBehaviors(
|
||||
[
|
||||
{
|
||||
type: 'brush-select',
|
||||
selectedState: 'customState',
|
||||
includeEdges: false,
|
||||
trigger: 'drag',
|
||||
onSelect() {
|
||||
triggered = true;
|
||||
},
|
||||
},
|
||||
],
|
||||
'default',
|
||||
);
|
||||
graph.emit('dragstart', { canvasX: -110, canvasY: -120 });
|
||||
graph.emit('drag', { canvasX: 300, canvasY: 300 });
|
||||
graph.emit('dragend', { canvasX: 300, canvasY: 300, x: 300, y: 300 });
|
||||
let selectedNodes = graph.findAllByState('node', 'customState');
|
||||
expect(selectedNodes.length).toEqual(2);
|
||||
expect(selectedNodes[0] === node1).toBe(true);
|
||||
expect(selectedNodes[1] === node2).toBe(true);
|
||||
|
||||
const selectedEdges = graph.findAllByState('edge', 'customState');
|
||||
expect(selectedEdges.length).toEqual(0);
|
||||
|
||||
graph.emit('canvas:click');
|
||||
selectedNodes = graph.findAllByState('node', 'customState');
|
||||
expect(selectedNodes.length).toEqual(0);
|
||||
expect(triggered).toBe(true);
|
||||
graph.destroy();
|
||||
});
|
||||
it('invalid trigger, multiple is false', () => {
|
||||
const graph = new Graph({
|
||||
container: div,
|
||||
width: 500,
|
||||
height: 500,
|
||||
modes: {
|
||||
default: [
|
||||
{
|
||||
type: 'brush-select',
|
||||
trigger: 'abc',
|
||||
multiple: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
graph.addItem('node', {
|
||||
color: '#666',
|
||||
x: 50,
|
||||
y: 50,
|
||||
size: 20,
|
||||
style: { lineWidth: 2, fill: '#666' },
|
||||
});
|
||||
graph.paint();
|
||||
expect(graph.get('modeController').currentBehaves[0].trigger).toEqual('shift');
|
||||
graph.destroy();
|
||||
});
|
||||
it('manipulate on a node', () => {
|
||||
const graph2 = new Graph({
|
||||
container: div,
|
||||
width: 500,
|
||||
height: 500,
|
||||
modes: {
|
||||
default: [
|
||||
{
|
||||
type: 'brush-select',
|
||||
trigger: 'drag',
|
||||
},
|
||||
],
|
||||
},
|
||||
nodeStateStyles: {
|
||||
selected: {
|
||||
stroke: '#ff0',
|
||||
},
|
||||
},
|
||||
});
|
||||
const node = graph2.addItem('node', {
|
||||
color: '#666',
|
||||
x: 50,
|
||||
y: 50,
|
||||
size: 20,
|
||||
style: { lineWidth: 2, fill: '#666' },
|
||||
});
|
||||
graph2.paint();
|
||||
|
||||
graph2.emit('keydown', { canvasX: 50, canvasY: 50, key: 'shift' });
|
||||
graph2.emit('dragstart', { canvasX: 50, canvasY: 50, item: node });
|
||||
graph2.emit('drag', { canvasX: 100, canvasY: 100 });
|
||||
graph2.emit('dragend', { canvasX: 100, canvasY: 100, x: 100, y: 100 });
|
||||
const selectedNodes = graph2.findAllByState('node', 'selected');
|
||||
expect(selectedNodes.length).toEqual(0);
|
||||
|
||||
graph2.destroy();
|
||||
});
|
||||
});
|
373
packages/core/tests/unit/behavior/collapse-expand-combo-spec.ts
Normal file
373
packages/core/tests/unit/behavior/collapse-expand-combo-spec.ts
Normal file
@ -0,0 +1,373 @@
|
||||
import '../../../src/behavior';
|
||||
import '../../../src/shape';
|
||||
import G6 from '../../../src';
|
||||
import { ICombo, INode } from '../../../src/interface/item';
|
||||
import { ComboConfig, NodeConfig } from '../../../src/types';
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.id = 'container';
|
||||
document.body.appendChild(div);
|
||||
|
||||
const data = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'node1',
|
||||
x: 150,
|
||||
y: 150,
|
||||
label: 'node1',
|
||||
comboId: 'A',
|
||||
},
|
||||
{
|
||||
id: 'node2',
|
||||
x: 200,
|
||||
y: 250,
|
||||
label: 'node2',
|
||||
comboId: 'A',
|
||||
},
|
||||
{
|
||||
id: 'node3',
|
||||
x: 100,
|
||||
y: 250,
|
||||
label: 'node3',
|
||||
},
|
||||
{
|
||||
id: 'node4',
|
||||
x: 200,
|
||||
y: 350,
|
||||
label: 'node4',
|
||||
comboId: 'B',
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: 'node1',
|
||||
target: 'node4',
|
||||
},
|
||||
{
|
||||
source: 'node1',
|
||||
target: 'node2',
|
||||
},
|
||||
{
|
||||
source: 'node2',
|
||||
target: 'node3',
|
||||
},
|
||||
],
|
||||
combos: [
|
||||
{
|
||||
id: 'A',
|
||||
parentId: 'C',
|
||||
label: 'gorup A',
|
||||
type: 'circle',
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
parentId: 'C',
|
||||
label: 'gorup B',
|
||||
type: 'circle',
|
||||
},
|
||||
{
|
||||
id: 'C',
|
||||
label: 'gorup C',
|
||||
// type: 'rect'
|
||||
},
|
||||
{
|
||||
id: 'F',
|
||||
label: 'gorup F',
|
||||
// type: 'rect'
|
||||
},
|
||||
{
|
||||
id: 'G',
|
||||
label: 'gorup G',
|
||||
// parentId: 'F'
|
||||
type: 'circle',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('collapse-expand-combo', () => {
|
||||
it('default collapse expand combo', (done) => {
|
||||
const graph = new G6.Graph({
|
||||
container: 'container',
|
||||
width: 1000,
|
||||
height: 800,
|
||||
modes: {
|
||||
default: ['collapse-expand-combo'],
|
||||
},
|
||||
defaultCombo: {
|
||||
type: 'circle',
|
||||
style: {
|
||||
fill: '#b5f5ec',
|
||||
},
|
||||
},
|
||||
nodeStateStyles: {
|
||||
selected: {
|
||||
fill: 'red',
|
||||
},
|
||||
},
|
||||
comboStateStyles: {
|
||||
active: {
|
||||
stroke: 'red',
|
||||
},
|
||||
selected: {
|
||||
'text-shape': {
|
||||
fill: '#f00',
|
||||
fontSize: 20,
|
||||
},
|
||||
fill: '#36cfc9',
|
||||
},
|
||||
state2: {
|
||||
stroke: '#0f0',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
const comboA = graph.findById('A') as ICombo;
|
||||
const comboC = graph.findById('C') as ICombo;
|
||||
// collapse A and collapse B
|
||||
graph.emit('combo:dblclick', { item: comboA });
|
||||
graph.emit('combo:dblclick', { item: comboC });
|
||||
expect(comboA.getModel().collapsed).toBe(true);
|
||||
expect(comboC.getModel().collapsed).toBe(true);
|
||||
comboA.getChildren().nodes.forEach((node) => {
|
||||
expect(node.isVisible()).toBe(false);
|
||||
});
|
||||
comboA.getChildren().combos.forEach((combo) => {
|
||||
expect(combo.isVisible()).toBe(false);
|
||||
});
|
||||
comboC.getChildren().nodes.forEach((node) => {
|
||||
expect(node.isVisible()).toBe(false);
|
||||
});
|
||||
comboC.getChildren().combos.forEach((combo) => {
|
||||
expect(combo.isVisible()).toBe(false);
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
// The console will warn: Fail to expand the combo since it's ancestor combo is collapsed.
|
||||
graph.emit('combo:dblclick', { item: comboA });
|
||||
expect(comboA.getModel().collapsed).toBe(true);
|
||||
comboA.getChildren().nodes.forEach((node) => {
|
||||
expect(node.isVisible()).toBe(false);
|
||||
});
|
||||
comboA.getChildren().combos.forEach((combo) => {
|
||||
expect(combo.isVisible()).toBe(false);
|
||||
});
|
||||
|
||||
graph.emit('combo:dblclick', { item: comboC });
|
||||
expect(comboC.getModel().collapsed).toBe(false);
|
||||
comboC.getChildren().nodes.forEach((node) => {
|
||||
expect(node.isVisible()).toBe(true);
|
||||
});
|
||||
comboC.getChildren().combos.forEach((combo) => {
|
||||
expect(combo.isVisible()).toBe(true);
|
||||
});
|
||||
// but A is still in collapsed state
|
||||
expect(comboA.getModel().collapsed).toBe(true);
|
||||
comboA.getChildren().nodes.forEach((node) => {
|
||||
expect(node.isVisible()).toBe(false);
|
||||
});
|
||||
comboA.getChildren().combos.forEach((combo) => {
|
||||
expect(combo.isVisible()).toBe(false);
|
||||
});
|
||||
graph.destroy();
|
||||
done();
|
||||
}, 250);
|
||||
});
|
||||
it('default collapsed set in data', (done) => {
|
||||
data.combos.forEach((combo: ComboConfig) => {
|
||||
combo.collapsed = true;
|
||||
});
|
||||
const graph = new G6.Graph({
|
||||
container: 'container',
|
||||
width: 1000,
|
||||
height: 800,
|
||||
});
|
||||
graph.read(data);
|
||||
setTimeout(() => {
|
||||
graph.getNodes().forEach((node: INode) => {
|
||||
if (node.get('id') !== 'node3') {
|
||||
expect(node.isVisible()).toBe(false);
|
||||
}
|
||||
});
|
||||
graph.destroy();
|
||||
done();
|
||||
}, 250);
|
||||
});
|
||||
it('collapse-expand-combo behavior with click trigger', (done) => {
|
||||
data.combos.forEach((combo: ComboConfig) => {
|
||||
delete combo.collapsed;
|
||||
});
|
||||
const graph = new G6.Graph({
|
||||
container: 'container',
|
||||
width: 1000,
|
||||
height: 800,
|
||||
modes: {
|
||||
default: [
|
||||
{
|
||||
type: 'collapse-expand-combo',
|
||||
trigger: 'click',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
graph.read(data);
|
||||
|
||||
const comboA = graph.findById('A') as ICombo;
|
||||
const comboC = graph.findById('C') as ICombo;
|
||||
// collapse A and collapse B
|
||||
graph.emit('combo:click', { item: comboA });
|
||||
graph.emit('combo:click', { item: comboC });
|
||||
expect(comboA.getModel().collapsed).toBe(true);
|
||||
expect(comboC.getModel().collapsed).toBe(true);
|
||||
comboA.getChildren().nodes.forEach((node) => {
|
||||
expect(node.isVisible()).toBe(false);
|
||||
});
|
||||
comboA.getChildren().combos.forEach((combo) => {
|
||||
expect(combo.isVisible()).toBe(false);
|
||||
});
|
||||
comboC.getChildren().nodes.forEach((node) => {
|
||||
expect(node.isVisible()).toBe(false);
|
||||
});
|
||||
comboC.getChildren().combos.forEach((combo) => {
|
||||
expect(combo.isVisible()).toBe(false);
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
// The console will warn: Fail to expand the combo since it's ancestor combo is collapsed.
|
||||
graph.emit('combo:click', { item: comboA });
|
||||
expect(comboA.getModel().collapsed).toBe(true);
|
||||
comboA.getChildren().nodes.forEach((node) => {
|
||||
expect(node.isVisible()).toBe(false);
|
||||
});
|
||||
comboA.getChildren().combos.forEach((combo) => {
|
||||
expect(combo.isVisible()).toBe(false);
|
||||
});
|
||||
|
||||
graph.emit('combo:click', { item: comboC });
|
||||
expect(comboC.getModel().collapsed).toBe(false);
|
||||
comboC.getChildren().nodes.forEach((node) => {
|
||||
expect(node.isVisible()).toBe(true);
|
||||
});
|
||||
comboC.getChildren().combos.forEach((combo) => {
|
||||
expect(combo.isVisible()).toBe(true);
|
||||
});
|
||||
// but A is still in collapsed state
|
||||
expect(comboA.getModel().collapsed).toBe(true);
|
||||
comboA.getChildren().nodes.forEach((node) => {
|
||||
expect(node.isVisible()).toBe(false);
|
||||
});
|
||||
comboA.getChildren().combos.forEach((combo) => {
|
||||
expect(combo.isVisible()).toBe(false);
|
||||
});
|
||||
graph.destroy();
|
||||
done();
|
||||
}, 250);
|
||||
});
|
||||
|
||||
it('collapse-expand-combo behavior with invalid trigger', (done) => {
|
||||
data.combos.forEach((combo: ComboConfig) => {
|
||||
delete combo.collapsed;
|
||||
});
|
||||
const graph = new G6.Graph({
|
||||
container: 'container',
|
||||
width: 1000,
|
||||
height: 800,
|
||||
modes: {
|
||||
default: [
|
||||
{
|
||||
type: 'collapse-expand-combo',
|
||||
trigger: 'mouseenter',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
graph.read(data);
|
||||
|
||||
const comboA = graph.findById('A') as ICombo;
|
||||
// collapse A and collapse B
|
||||
graph.emit('combo:mouseenter', { item: comboA });
|
||||
expect(comboA.getModel().collapsed).toBe(undefined);
|
||||
comboA.getChildren().nodes.forEach((node) => {
|
||||
expect(node.isVisible()).toBe(true);
|
||||
});
|
||||
comboA.getChildren().combos.forEach((combo) => {
|
||||
expect(combo.isVisible()).toBe(true);
|
||||
});
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
|
||||
it('collapse-expand-combo behavior with layout', (done) => {
|
||||
data.combos.forEach((combo: ComboConfig) => {
|
||||
delete combo.collapsed;
|
||||
});
|
||||
const graph = new G6.Graph({
|
||||
container: 'container',
|
||||
width: 1000,
|
||||
height: 800,
|
||||
modes: {
|
||||
default: [
|
||||
{
|
||||
type: 'collapse-expand-combo',
|
||||
},
|
||||
],
|
||||
},
|
||||
layout: {
|
||||
type: 'comboForce',
|
||||
},
|
||||
});
|
||||
graph.read(data);
|
||||
|
||||
const comboA = graph.findById('A') as ICombo;
|
||||
// collapse A and collapse B
|
||||
graph.emit('combo:dblclick', { item: comboA });
|
||||
expect(comboA.getModel().collapsed).toBe(true);
|
||||
comboA.getChildren().nodes.forEach((node) => {
|
||||
expect(node.isVisible()).toBe(false);
|
||||
});
|
||||
comboA.getChildren().combos.forEach((combo) => {
|
||||
expect(combo.isVisible()).toBe(false);
|
||||
});
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
|
||||
it('click on item which is not a combo', (done) => {
|
||||
data.combos.forEach((combo: ComboConfig) => {
|
||||
delete combo.collapsed;
|
||||
});
|
||||
const graph = new G6.Graph({
|
||||
container: 'container',
|
||||
width: 1000,
|
||||
height: 800,
|
||||
modes: {
|
||||
default: [
|
||||
{
|
||||
type: 'collapse-expand-combo',
|
||||
},
|
||||
'zoom-canvas',
|
||||
'drag-canvas',
|
||||
],
|
||||
},
|
||||
layout: {
|
||||
type: 'comboForce',
|
||||
},
|
||||
});
|
||||
graph.read(data);
|
||||
|
||||
const node = graph.getNodes()[0];
|
||||
const comboB = graph.findById('B') as ICombo;
|
||||
graph.emit('combo:dblclick', { item: node });
|
||||
expect(comboB.getModel().collapsed).toBe(undefined);
|
||||
comboB.getChildren().nodes.forEach((node) => {
|
||||
expect(node.isVisible()).toBe(true);
|
||||
});
|
||||
comboB.getChildren().combos.forEach((combo) => {
|
||||
expect(combo.isVisible()).toBe(true);
|
||||
});
|
||||
graph.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
230
packages/core/tests/unit/behavior/collapse-expand-spec.ts
Normal file
230
packages/core/tests/unit/behavior/collapse-expand-spec.ts
Normal file
@ -0,0 +1,230 @@
|
||||
import '../../../src/behavior';
|
||||
import '../../../src/shape';
|
||||
import TreeGraph from '../../../src/graph/tree-graph';
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.id = 'collapse-expand-spec';
|
||||
document.body.appendChild(div);
|
||||
|
||||
function approximateEqual(a, b) {
|
||||
return Math.abs(a - b) < 0.0001;
|
||||
}
|
||||
|
||||
describe('collapse expand tree graph', () => {
|
||||
const data = {
|
||||
isRoot: true,
|
||||
id: 'Root',
|
||||
style: {
|
||||
fill: 'red',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
id: 'SubTreeNode1',
|
||||
raw: {},
|
||||
children: [
|
||||
{
|
||||
id: 'SubTreeNode1.1',
|
||||
},
|
||||
{
|
||||
id: 'SubTreeNode1.2',
|
||||
label: 'SubTreeNode1.2',
|
||||
children: [
|
||||
{
|
||||
id: 'SubTreeNode1.2.1',
|
||||
},
|
||||
{
|
||||
id: 'SubTreeNode1.2.2',
|
||||
},
|
||||
{
|
||||
id: 'SubTreeNode1.2.3',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'SubTreeNode2',
|
||||
children: [
|
||||
{
|
||||
id: 'SubTreeNode2.1',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'SubTreeNode3',
|
||||
label: 'SubTreeNode3',
|
||||
children: [
|
||||
{
|
||||
id: 'SubTreeNode3.1',
|
||||
},
|
||||
{
|
||||
id: 'SubTreeNode3.2',
|
||||
},
|
||||
{
|
||||
id: 'SubTreeNode3.3',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'SubTreeNode4',
|
||||
},
|
||||
{
|
||||
id: 'SubTreeNode5',
|
||||
},
|
||||
{
|
||||
id: 'SubTreeNode6',
|
||||
},
|
||||
{
|
||||
id: 'SubTreeNode7',
|
||||
label: 'SubTreeNode7',
|
||||
},
|
||||
{
|
||||
id: 'SubTreeNode8',
|
||||
},
|
||||
{
|
||||
id: 'SubTreeNode9',
|
||||
},
|
||||
{
|
||||
id: 'SubTreeNode10',
|
||||
},
|
||||
{
|
||||
id: 'SubTreeNode11',
|
||||
},
|
||||
],
|
||||
};
|
||||
const graph = new TreeGraph({
|
||||
container: div,
|
||||
width: 500,
|
||||
height: 500,
|
||||
layout: {
|
||||
type: 'compactBox',
|
||||
},
|
||||
fitView: true,
|
||||
modes: {
|
||||
default: ['collapse-expand'],
|
||||
},
|
||||
defaultNode: {
|
||||
size: 10,
|
||||
},
|
||||
});
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
const parent = graph.findById('SubTreeNode1.2');
|
||||
it('collapse tree graph', () => {
|
||||
graph.once('afterrefreshlayout', () => {
|
||||
expect(parent.getModel().collapsed).toEqual(true);
|
||||
expect(parent.get('children').length).toEqual(0);
|
||||
});
|
||||
graph.emit('node:click', { item: parent });
|
||||
});
|
||||
it('expand tree graph', () => {
|
||||
graph.emit('node:click', { item: parent });
|
||||
graph.once('afterrefreshlayout', () => {
|
||||
expect(parent.getModel().collapsed).toEqual(false);
|
||||
expect(parent.get('children').length).toEqual(2);
|
||||
});
|
||||
});
|
||||
it('expand a leaf', () => {
|
||||
const leaf = graph.findById('SubTreeNode7');
|
||||
graph.once('afterrefreshlayout', () => {
|
||||
expect(leaf.getModel().collapsed).toEqual(false);
|
||||
});
|
||||
graph.emit('node:click', { item: leaf });
|
||||
graph.destroy();
|
||||
});
|
||||
it('collapse-expand with invalid trigger', () => {
|
||||
const graph2 = new TreeGraph({
|
||||
container: div,
|
||||
width: 500,
|
||||
height: 500,
|
||||
layout: {
|
||||
type: 'compactBox',
|
||||
},
|
||||
fitView: true,
|
||||
modes: {
|
||||
default: [
|
||||
{
|
||||
type: 'collapse-expand',
|
||||
trigger: 'testInvalid',
|
||||
},
|
||||
],
|
||||
},
|
||||
defaultNode: {
|
||||
size: 10,
|
||||
},
|
||||
});
|
||||
graph2.data(data);
|
||||
graph2.render();
|
||||
const parent2 = graph2.findById('SubTreeNode1.2');
|
||||
graph2.once('afterrefreshlayout', () => {
|
||||
expect(parent2.getModel().collapsed).toEqual(true);
|
||||
expect(parent2.get('children').length).toEqual(0);
|
||||
});
|
||||
graph2.emit('node:click', { item: parent2 });
|
||||
graph2.destroy();
|
||||
});
|
||||
it('prevent begin', () => {
|
||||
const graph2 = new TreeGraph({
|
||||
container: div,
|
||||
width: 500,
|
||||
height: 500,
|
||||
layout: {
|
||||
type: 'compactBox',
|
||||
},
|
||||
fitView: true,
|
||||
modes: {
|
||||
default: [
|
||||
{
|
||||
type: 'collapse-expand',
|
||||
shouldBegin: () => {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
defaultNode: {
|
||||
size: 10,
|
||||
},
|
||||
});
|
||||
graph2.data(data);
|
||||
graph2.render();
|
||||
const parent2 = graph2.findById('SubTreeNode3');
|
||||
graph2.once('afterrefreshlayout', () => {
|
||||
expect(parent2.getModel().collapsed).toEqual(false);
|
||||
});
|
||||
graph2.emit('node:click', { item: parent2 });
|
||||
graph2.destroy();
|
||||
});
|
||||
it('prevent update', () => {
|
||||
const graph2 = new TreeGraph({
|
||||
container: div,
|
||||
width: 500,
|
||||
height: 500,
|
||||
layout: {
|
||||
type: 'compactBox',
|
||||
},
|
||||
fitView: true,
|
||||
modes: {
|
||||
default: [
|
||||
{
|
||||
type: 'collapse-expand',
|
||||
shouldUpdate: () => {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
defaultNode: {
|
||||
size: 10,
|
||||
},
|
||||
});
|
||||
graph2.data(data);
|
||||
graph2.render();
|
||||
const parent2 = graph2.findById('SubTreeNode3');
|
||||
graph2.once('afterrefreshlayout', () => {
|
||||
expect(parent2.getModel().collapsed).toEqual(false);
|
||||
});
|
||||
graph2.emit('node:click', { item: parent2 });
|
||||
graph2.destroy();
|
||||
});
|
||||
});
|
473
packages/core/tests/unit/behavior/create-edge-spec.ts
Normal file
473
packages/core/tests/unit/behavior/create-edge-spec.ts
Normal file
@ -0,0 +1,473 @@
|
||||
import '../../../src/behavior';
|
||||
import '../../../src/shape';
|
||||
|
||||
import Graph from '../../../src/graph/graph';
|
||||
import G6 from '../../../src'
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.id = 'drag-spec';
|
||||
document.body.appendChild(div);
|
||||
|
||||
const data = {
|
||||
nodes: [
|
||||
{
|
||||
id: '0',
|
||||
x: 50,
|
||||
y: 50,
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
x: 150,
|
||||
y: 150,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('create-edge', () => {
|
||||
it('create edge default config', () => {
|
||||
const graph: Graph = new Graph({
|
||||
container: div,
|
||||
width: 500,
|
||||
height: 500,
|
||||
modes: {
|
||||
default: ['create-edge'],
|
||||
},
|
||||
defaultEdge: {
|
||||
style: {
|
||||
stroke: '#f00',
|
||||
lineWidth: 2
|
||||
}
|
||||
},
|
||||
});
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
const node0 = graph.getNodes()[0];
|
||||
const node1 = graph.getNodes()[1];
|
||||
graph.emit('node:click', { x: 100, y: 100, item: node0 });
|
||||
graph.emit('mousemove', { x: 110, y: 110 });
|
||||
expect(graph.getEdges().length).toEqual(1);
|
||||
const edge = graph.getEdges()[0];
|
||||
|
||||
// cancel
|
||||
graph.emit('edge:click', { x: 100, y: 100, item: edge });
|
||||
expect(graph.getEdges().length).toEqual(0);
|
||||
|
||||
// create
|
||||
graph.emit('node:click', { x: 100, y: 100, item: node0 });
|
||||
graph.emit('node:click', { x: 120, y: 120, item: node1 });
|
||||
expect(graph.getEdges().length).toEqual(1);
|
||||
|
||||
// loop
|
||||
graph.emit('node:click', { x: 100, y: 100, item: node0 });
|
||||
graph.emit('node:click', { x: 100, y: 100, item: node0 });
|
||||
expect(graph.getEdges().length).toEqual(2);
|
||||
const loop = graph.getEdges()[1];
|
||||
expect(loop.getModel().source).toEqual(loop.getModel().target);
|
||||
graph.destroy();
|
||||
});
|
||||
it('create edge width polyline edge', () => {
|
||||
const graph: Graph = new Graph({
|
||||
container: div,
|
||||
width: 500,
|
||||
height: 500,
|
||||
modes: {
|
||||
default: ['create-edge'],
|
||||
},
|
||||
defaultEdge: {
|
||||
type: 'polyline',
|
||||
style: {
|
||||
stroke: '#f00',
|
||||
lineWidth: 2
|
||||
}
|
||||
},
|
||||
});
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
const node0 = graph.getNodes()[0];
|
||||
const node1 = graph.getNodes()[1];
|
||||
graph.emit('node:click', { x: 100, y: 100, item: node0 });
|
||||
graph.emit('mousemove', { x: 110, y: 110 });
|
||||
expect(graph.getEdges().length).toEqual(1);
|
||||
const edge = graph.getEdges()[0];
|
||||
|
||||
// cancel
|
||||
graph.emit('edge:click', { x: 100, y: 100, item: edge });
|
||||
expect(graph.getEdges().length).toEqual(0);
|
||||
|
||||
// create
|
||||
graph.emit('node:click', { x: 100, y: 100, item: node0 });
|
||||
graph.emit('node:click', { x: 120, y: 120, item: node1 });
|
||||
expect(graph.getEdges().length).toEqual(1);
|
||||
|
||||
// loop
|
||||
graph.emit('node:click', { x: 100, y: 100, item: node0 });
|
||||
graph.emit('node:click', { x: 100, y: 100, item: node0 });
|
||||
expect(graph.getEdges().length).toEqual(2);
|
||||
const loop = graph.getEdges()[1];
|
||||
expect(loop.getModel().source).toEqual(loop.getModel().target);
|
||||
graph.destroy();
|
||||
});
|
||||
it('create edge with drag trigger', () => {
|
||||
const graph: Graph = new Graph({
|
||||
container: div,
|
||||
width: 500,
|
||||
height: 500,
|
||||
modes: {
|
||||
default: [{
|
||||
type: 'create-edge',
|
||||
trigger: 'drag'
|
||||
}],
|
||||
},
|
||||
});
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
const node0 = graph.getNodes()[0];
|
||||
const node1 = graph.getNodes()[1];
|
||||
graph.emit('node:dragstart', { x: 100, y: 100, item: node0 });
|
||||
graph.emit('drag', { x: 110, y: 110 });
|
||||
expect(graph.getEdges().length).toEqual(1);
|
||||
const edge = graph.getEdges()[0];
|
||||
|
||||
// cancel
|
||||
graph.emit('dragend', { x: 100, y: 100, item: edge });
|
||||
expect(graph.getEdges().length).toEqual(0);
|
||||
|
||||
// create
|
||||
graph.emit('node:dragstart', { x: 100, y: 100, item: node0 });
|
||||
graph.emit('node:drop', { x: 120, y: 120, item: node1 });
|
||||
expect(graph.getEdges().length).toEqual(1);
|
||||
|
||||
graph.destroy();
|
||||
});
|
||||
it('create edge with click and shift key', () => {
|
||||
const graph: Graph = new Graph({
|
||||
container: div,
|
||||
width: 500,
|
||||
height: 500,
|
||||
modes: {
|
||||
default: [{
|
||||
type: 'create-edge',
|
||||
key: 'shift'
|
||||
}],
|
||||
},
|
||||
});
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
const node0 = graph.getNodes()[0];
|
||||
const node1 = graph.getNodes()[1];
|
||||
|
||||
// without keydown
|
||||
graph.emit('node:click', { x: 100, y: 100, item: node0 });
|
||||
graph.emit('mousemove', { x: 110, y: 110 });
|
||||
expect(graph.getEdges().length).toEqual(0);
|
||||
|
||||
// keydown with wrong key
|
||||
graph.emit('keydown', { key: 'a' });
|
||||
graph.emit('node:click', { x: 100, y: 100, item: node0 });
|
||||
graph.emit('mousemove', { x: 110, y: 110 });
|
||||
expect(graph.getEdges().length).toEqual(0);
|
||||
|
||||
// keydown with correct key
|
||||
graph.emit('keydown', { key: 'shift' });
|
||||
graph.emit('node:click', { x: 100, y: 100, item: node0 });
|
||||
graph.emit('mousemove', { x: 110, y: 110 });
|
||||
expect(graph.getEdges().length).toEqual(1);
|
||||
|
||||
// cancel with keyup
|
||||
graph.emit('keyup', {});
|
||||
expect(graph.getEdges().length).toEqual(0);
|
||||
|
||||
// create
|
||||
graph.emit('keydown', { key: 'shift' });
|
||||
graph.emit('node:click', { x: 100, y: 100, item: node0 });
|
||||
graph.emit('node:click', { x: 100, y: 100, item: node1 });
|
||||
expect(graph.getEdges().length).toEqual(1);
|
||||
|
||||
graph.destroy();
|
||||
});
|
||||
it('shouldBegin, shouldEnd', () => {
|
||||
const graph: Graph = new Graph({
|
||||
container: div,
|
||||
width: 500,
|
||||
height: 500,
|
||||
modes: {
|
||||
default: [{
|
||||
type: 'create-edge',
|
||||
shouldBegin: e => {
|
||||
if (e.target && e.target.get('name') === 'text-shape') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
shouldEnd: e => {
|
||||
if (e.target && e.target.get('name') === 'text-shape') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}],
|
||||
},
|
||||
defaultEdge: {
|
||||
style: {
|
||||
stroke: '#f00',
|
||||
lineWidth: 2
|
||||
}
|
||||
},
|
||||
});
|
||||
graph.node(node => {
|
||||
return {
|
||||
label: node.id,
|
||||
labelCfg: {
|
||||
position: 'bottom'
|
||||
}
|
||||
}
|
||||
})
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
const node0 = graph.getNodes()[0];
|
||||
const node0text = node0.getContainer().find(e => e.get('name') === 'text-shape')
|
||||
const node1 = graph.getNodes()[1];
|
||||
const node1text = node0.getContainer().find(e => e.get('name') === 'text-shape')
|
||||
|
||||
// shouldBegin returns true
|
||||
graph.emit('node:click', { x: 100, y: 100, item: node0, target: node0.getKeyShape() });
|
||||
expect(graph.getEdges().length).toEqual(1);
|
||||
// cancel
|
||||
let edge = graph.getEdges()[0];
|
||||
graph.emit('edge:click', { x: 100, y: 100, item: edge });
|
||||
expect(graph.getEdges().length).toEqual(0);
|
||||
|
||||
// shouldBegin returns false
|
||||
graph.emit('node:click', { x: 100, y: 100, item: node0, target: node0text });
|
||||
expect(graph.getEdges().length).toEqual(0);
|
||||
// cancel
|
||||
edge = graph.getEdges()[0];
|
||||
graph.emit('edge:click', { x: 100, y: 100, item: edge });
|
||||
expect(graph.getEdges().length).toEqual(0);
|
||||
|
||||
// shouldEnd returns false
|
||||
graph.emit('node:click', { x: 100, y: 100, item: node0 });
|
||||
graph.emit('node:click', { x: 100, y: 100, item: node1, target: node1text });
|
||||
expect(graph.getEdges().length).toEqual(1);
|
||||
edge = graph.getEdges()[0];
|
||||
expect(edge.getModel().target).not.toBe(node1.getID());
|
||||
|
||||
graph.destroy();
|
||||
});
|
||||
it('create edge width stack', () => {
|
||||
const toolbar = new G6.ToolBar()
|
||||
const graph: Graph = new Graph({
|
||||
container: div,
|
||||
width: 500,
|
||||
height: 500,
|
||||
enabledStack: true,
|
||||
plugins: [toolbar],
|
||||
modes: {
|
||||
default: ['create-edge'],
|
||||
},
|
||||
defaultEdge: {
|
||||
style: {
|
||||
stroke: '#f00',
|
||||
lineWidth: 2
|
||||
}
|
||||
},
|
||||
});
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
const node0 = graph.getNodes()[0];
|
||||
const node1 = graph.getNodes()[1];
|
||||
graph.emit('node:click', { x: 100, y: 100, item: node0 });
|
||||
graph.emit('mousemove', { x: 110, y: 110 });
|
||||
expect(graph.getEdges().length).toEqual(1);
|
||||
const edge = graph.getEdges()[0];
|
||||
|
||||
let stackData = graph.getStackData()
|
||||
const { undoStack, redoStack } = stackData
|
||||
expect(undoStack.length).toBe(1)
|
||||
expect(redoStack.length).toBe(0)
|
||||
|
||||
// cancel
|
||||
graph.emit('edge:click', { x: 100, y: 100, item: edge });
|
||||
expect(graph.getEdges().length).toEqual(0);
|
||||
|
||||
// create
|
||||
graph.emit('node:click', { x: 100, y: 100, item: node0 });
|
||||
graph.emit('node:click', { x: 120, y: 120, item: node1 });
|
||||
expect(graph.getEdges().length).toEqual(1);
|
||||
stackData = graph.getStackData()
|
||||
expect(stackData.undoStack.length).toBe(2)
|
||||
expect(stackData.redoStack.length).toBe(0)
|
||||
|
||||
// loop
|
||||
graph.emit('node:click', { x: 100, y: 100, item: node0 });
|
||||
graph.emit('node:click', { x: 100, y: 100, item: node0 });
|
||||
stackData = graph.getStackData()
|
||||
expect(stackData.undoStack.length).toBe(3)
|
||||
expect(stackData.redoStack.length).toBe(0)
|
||||
expect(graph.getEdges().length).toEqual(2);
|
||||
const loop = graph.getEdges()[1];
|
||||
expect(loop.getModel().source).toEqual(loop.getModel().target);
|
||||
graph.destroy();
|
||||
});
|
||||
it('create edge width edge configures', () => {
|
||||
const graph: Graph = new Graph({
|
||||
container: div,
|
||||
width: 500,
|
||||
height: 500,
|
||||
enabledStack: true,
|
||||
modes: {
|
||||
default: [{
|
||||
type: 'create-edge',
|
||||
edgeConfig: {
|
||||
type: 'cubic',
|
||||
style: {
|
||||
stroke: '#f00'
|
||||
}
|
||||
}
|
||||
}],
|
||||
},
|
||||
defaultEdge: {
|
||||
style: {
|
||||
stroke: '#0f0',
|
||||
lineWidth: 5
|
||||
}
|
||||
},
|
||||
});
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
const node0 = graph.getNodes()[0];
|
||||
const node1 = graph.getNodes()[1];
|
||||
|
||||
graph.addItem('edge', {
|
||||
source: '0',
|
||||
target: '1'
|
||||
})
|
||||
|
||||
graph.emit('node:click', { x: 100, y: 100, item: node0 });
|
||||
graph.emit('mousemove', { x: 110, y: 110 });
|
||||
expect(graph.getEdges().length).toEqual(2);
|
||||
let edge = graph.getEdges()[1];
|
||||
expect(edge.getModel().type).toEqual('cubic');
|
||||
let keyShape = edge.getKeyShape();
|
||||
expect(keyShape.attr('stroke')).toEqual('#f00');
|
||||
expect(keyShape.attr('lineWidth')).toEqual(1);
|
||||
|
||||
// cancel
|
||||
graph.emit('edge:click', { x: 100, y: 100, item: edge });
|
||||
expect(graph.getEdges().length).toEqual(1);
|
||||
|
||||
// create
|
||||
graph.emit('node:click', { x: 100, y: 100, item: node0 });
|
||||
graph.emit('node:click', { x: 120, y: 120, item: node1 });
|
||||
expect(graph.getEdges().length).toEqual(2);
|
||||
edge = graph.getEdges()[1];
|
||||
keyShape = edge.getKeyShape();
|
||||
expect(keyShape.attr('stroke')).toEqual('#f00');
|
||||
expect(keyShape.attr('lineWidth')).toEqual(1);
|
||||
|
||||
graph.destroy();
|
||||
});
|
||||
|
||||
it('click first node and remove the source node', () => {
|
||||
const graph: Graph = new Graph({
|
||||
container: div,
|
||||
width: 500,
|
||||
height: 500,
|
||||
modes: {
|
||||
default: ['create-edge'],
|
||||
}
|
||||
});
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
graph.on('node:click', e => {
|
||||
graph.removeItem('0');
|
||||
});
|
||||
expect(graph.getEdges().length).toEqual(0);
|
||||
graph.destroy();
|
||||
});
|
||||
|
||||
it('create edge for combos ', () => {
|
||||
const dataWithCombos = {
|
||||
nodes: [
|
||||
{
|
||||
id: '0',
|
||||
x: 50,
|
||||
y: 50,
|
||||
comboId: 'combo0'
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
x: 150,
|
||||
y: 150,
|
||||
comboId: 'combo1'
|
||||
},
|
||||
],
|
||||
combos: [
|
||||
{
|
||||
id: "combo0"
|
||||
},
|
||||
{
|
||||
id: "combo1"
|
||||
}
|
||||
]
|
||||
}
|
||||
const graph: Graph = new Graph({
|
||||
container: div,
|
||||
width: 500,
|
||||
height: 500,
|
||||
modes: {
|
||||
default: ['drag-node', 'drag-combo', {
|
||||
type: 'create-edge',
|
||||
trigger: 'click'
|
||||
}],
|
||||
},
|
||||
groupByTypes: false
|
||||
});
|
||||
graph.data(dataWithCombos);
|
||||
graph.render();
|
||||
|
||||
const node0 = graph.getNodes()[0];
|
||||
const node1 = graph.getNodes()[1];
|
||||
const combo0 = graph.getCombos()[0];
|
||||
const combo1 = graph.getCombos()[1];
|
||||
|
||||
// start from a combo, end on a node
|
||||
graph.emit('node:click', { x: 100, y: 100, item: combo0 });
|
||||
graph.emit('mousemove', { x: 110, y: 110 });
|
||||
expect(graph.getEdges().length).toEqual(1);
|
||||
// cancel
|
||||
graph.emit('canvas:click', { x: 100, y: 100, target: graph.get('canvas') });
|
||||
expect(graph.getEdges().length).toEqual(0);
|
||||
// create
|
||||
graph.emit('node:click', { x: 100, y: 100, item: combo0 });
|
||||
graph.emit('node:click', { x: 120, y: 120, item: node1 });
|
||||
expect(graph.getEdges().length).toEqual(1);
|
||||
|
||||
// start from a node, end on a combo
|
||||
graph.emit('node:click', { x: 100, y: 100, item: node0 });
|
||||
graph.emit('mousemove', { x: 110, y: 110 });
|
||||
expect(graph.getEdges().length).toEqual(2);
|
||||
// cancel
|
||||
graph.emit('canvas:click', { x: 100, y: 100, target: graph.get('canvas') });
|
||||
expect(graph.getEdges().length).toEqual(1);
|
||||
// create
|
||||
graph.emit('node:click', { x: 100, y: 100, item: node0 });
|
||||
graph.emit('node:click', { x: 120, y: 120, item: combo0 });
|
||||
expect(graph.getEdges().length).toEqual(2);
|
||||
|
||||
|
||||
// start from a combo, end on a combo
|
||||
graph.emit('node:click', { x: 100, y: 100, item: combo0 });
|
||||
graph.emit('mousemove', { x: 110, y: 110 });
|
||||
expect(graph.getEdges().length).toEqual(3);
|
||||
// cancel
|
||||
graph.emit('canvas:click', { x: 100, y: 100, target: graph.get('canvas') });
|
||||
expect(graph.getEdges().length).toEqual(2);
|
||||
// create
|
||||
graph.emit('node:click', { x: 100, y: 100, item: combo0 });
|
||||
graph.emit('node:click', { x: 120, y: 120, item: combo1 });
|
||||
expect(graph.getEdges().length).toEqual(3);
|
||||
|
||||
graph.destroy();
|
||||
});
|
||||
});
|
493
packages/core/tests/unit/behavior/drag-canvas-spec.ts
Normal file
493
packages/core/tests/unit/behavior/drag-canvas-spec.ts
Normal file
@ -0,0 +1,493 @@
|
||||
import '../../../src/behavior';
|
||||
import '../../../src/shape';
|
||||
import Graph from '../../../src/graph/graph';
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.id = 'pan-spec';
|
||||
document.body.appendChild(div);
|
||||
|
||||
const data = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'node1',
|
||||
x: 50,
|
||||
y: 50,
|
||||
},
|
||||
{
|
||||
id: 'node2',
|
||||
x: 200,
|
||||
y: 50,
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: 'node1',
|
||||
target: 'node2',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('drag-canvas', () => {
|
||||
it('drag canvas', () => {
|
||||
const graph = new Graph({
|
||||
container: div,
|
||||
width: 500,
|
||||
height: 500,
|
||||
modes: {
|
||||
default: ['drag-canvas'],
|
||||
},
|
||||
});
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
let start = false;
|
||||
graph.on('canvas:dragstart', () => {
|
||||
start = true;
|
||||
});
|
||||
graph.on('canvas:dragend', () => {
|
||||
start = false;
|
||||
});
|
||||
graph.paint();
|
||||
graph.emit('dragstart', { clientX: 150, clientY: 150, target: graph.get('canvas') });
|
||||
graph.emit('drag', { clientX: 200, clientY: 200, target: graph.get('canvas') });
|
||||
expect(start).toBe(true);
|
||||
graph.emit('drag', { clientX: 250, clientY: 250, target: graph.get('canvas') });
|
||||
expect(start).toBe(true);
|
||||
const matrix = graph.get('group').getMatrix();
|
||||
expect(matrix[6]).toEqual(100);
|
||||
expect(matrix[7]).toEqual(100);
|
||||
graph.emit('dragend', {});
|
||||
expect(start).toBe(false);
|
||||
graph.destroy();
|
||||
});
|
||||
it('prevent default drag behavior', () => {
|
||||
const graph = new Graph({
|
||||
container: div,
|
||||
width: 500,
|
||||
height: 500,
|
||||
modes: {
|
||||
default: [
|
||||
{
|
||||
type: 'drag-canvas',
|
||||
shouldUpdate: () => {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
let start = false;
|
||||
graph.on('canvas:dragstart', () => {
|
||||
start = true;
|
||||
});
|
||||
graph.on('canvas:dragend', () => {
|
||||
start = false;
|
||||
});
|
||||
graph.paint();
|
||||
graph.emit('dragstart', { clientX: 150, clientY: 150, target: graph.get('canvas') });
|
||||
graph.emit('drag', { clientX: 200, clientY: 200, target: graph.get('canvas') });
|
||||
expect(start).toBe(true);
|
||||
graph.emit('drag', { clientX: 250, clientY: 250, target: graph.get('canvas') });
|
||||
expect(start).toBe(true);
|
||||
const matrix = graph.get('group').getMatrix();
|
||||
expect(matrix).toEqual(null);
|
||||
graph.emit('canvas:dragend', {});
|
||||
expect(start).toBe(false);
|
||||
graph.destroy();
|
||||
});
|
||||
it('unbind event', () => {
|
||||
const graph = new Graph({
|
||||
container: div,
|
||||
width: 500,
|
||||
height: 500,
|
||||
modes: {
|
||||
default: [
|
||||
{
|
||||
type: 'drag-canvas',
|
||||
},
|
||||
],
|
||||
custom: [],
|
||||
},
|
||||
});
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
let triggered = false;
|
||||
graph.setMode('custom');
|
||||
graph.on('canvas:dragstart', () => {
|
||||
triggered = true;
|
||||
});
|
||||
graph.on('canvas:dragend', () => {
|
||||
triggered = true;
|
||||
});
|
||||
graph.emit('dragstart', { clientX: 150, clientY: 150, target: graph.get('canvas') });
|
||||
graph.emit('drag', { clientX: 200, clientY: 200, target: graph.get('canvas') });
|
||||
expect(triggered).toBe(false);
|
||||
graph.emit('drag', { clientX: 250, clientY: 250, target: graph.get('canvas') });
|
||||
expect(triggered).toBe(false);
|
||||
graph.emit('dragend', { clientX: 250, clientY: 250 });
|
||||
expect(triggered).toBe(false);
|
||||
graph.destroy();
|
||||
});
|
||||
it('drag with x direction restrict', () => {
|
||||
const graph = new Graph({
|
||||
container: div,
|
||||
width: 500,
|
||||
height: 500,
|
||||
modes: {
|
||||
default: [
|
||||
{
|
||||
type: 'drag-canvas',
|
||||
direction: 'x',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
let start = false;
|
||||
graph.addItem('node', { x: 100, y: 100, color: '#666', type: 'rect', id: 'test' });
|
||||
graph.on('canvas:dragstart', () => {
|
||||
start = true;
|
||||
});
|
||||
graph.on('canvas:dragend', () => {
|
||||
start = false;
|
||||
});
|
||||
graph.paint();
|
||||
graph.emit('dragstart', { clientX: 150, clientY: 150, target: graph.get('canvas') });
|
||||
graph.emit('drag', { clientX: 200, clientY: 200, target: graph.get('canvas') });
|
||||
expect(start).toBe(true);
|
||||
graph.emit('drag', { clientX: 250, clientY: 250, target: graph.get('canvas') });
|
||||
expect(start).toBe(true);
|
||||
const matrix = graph.get('group').getMatrix();
|
||||
expect(matrix[6]).toEqual(100);
|
||||
expect(matrix[7]).toEqual(0);
|
||||
graph.emit('dragend', { clientX: 200, clientY: 200 });
|
||||
expect(start).toBe(false);
|
||||
graph.destroy();
|
||||
});
|
||||
it('drag with y direction restrict', () => {
|
||||
const graph = new Graph({
|
||||
container: div,
|
||||
width: 500,
|
||||
height: 500,
|
||||
modes: {
|
||||
default: [
|
||||
{
|
||||
type: 'drag-canvas',
|
||||
direction: 'y',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
let start = false;
|
||||
graph.addItem('node', { x: 100, y: 100, color: '#666', type: 'rect', id: 'test' });
|
||||
graph.paint();
|
||||
graph.emit('dragstart', { clientX: 150, clientY: 150, target: graph.get('canvas') });
|
||||
graph.emit('drag', { clientX: 250, clientY: 250, target: graph.get('canvas') });
|
||||
const matrix = graph.get('group').getMatrix();
|
||||
expect(matrix[6]).toEqual(0);
|
||||
expect(matrix[7]).toEqual(100);
|
||||
graph.emit('dragend', { clientX: 250, clientY: 250 });
|
||||
graph.destroy();
|
||||
});
|
||||
it('drag offset', () => {
|
||||
const graph = new Graph({
|
||||
container: div,
|
||||
width: 500,
|
||||
height: 500,
|
||||
modes: {
|
||||
default: [
|
||||
{
|
||||
type: 'drag-canvas',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
let triggered = false;
|
||||
let dragging = false;
|
||||
graph.on('canvas:dragstart', () => {
|
||||
triggered = true;
|
||||
});
|
||||
graph.on('canvas:drag', () => {
|
||||
dragging = true;
|
||||
});
|
||||
graph.emit('dragstart', { clientX: 150, clientY: 150, target: graph.get('canvas') });
|
||||
graph.emit('drag', { clientX: 150, clientY: 150, target: graph.get('canvas') });
|
||||
expect(triggered).toBe(false);
|
||||
expect(dragging).toBe(false);
|
||||
graph.emit('drag', { clientX: 160, clientY: 160, target: graph.get('canvas') });
|
||||
expect(triggered).toBe(true);
|
||||
graph.emit('drag', { clientX: 170, clientY: 180, target: graph.get('canvas') });
|
||||
expect(dragging).toBe(true);
|
||||
dragging = false;
|
||||
graph.emit('canvas:click', { clientX: 170, clientY: 170, target: graph.get('canvas') });
|
||||
graph.emit('drag', { clientX: 170, clientY: 170, target: graph.get('canvas') });
|
||||
expect(dragging).toBe(false);
|
||||
graph.destroy();
|
||||
});
|
||||
it('drag with keydown', () => {
|
||||
const graph = new Graph({
|
||||
container: div,
|
||||
width: 500,
|
||||
height: 500,
|
||||
modes: {
|
||||
default: [
|
||||
{
|
||||
type: 'drag-canvas',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
let triggered = false;
|
||||
let dragging = false;
|
||||
graph.on('canvas:dragstart', () => {
|
||||
triggered = true;
|
||||
});
|
||||
graph.on('canvas:drag', () => {
|
||||
dragging = true;
|
||||
});
|
||||
|
||||
// mouse down and up without moving
|
||||
graph.emit('dragstart', { clientX: 150, clientY: 150, target: graph.get('canvas') });
|
||||
graph.emit('dragend', { clientX: 150, clientY: 150, target: graph.get('canvas') });
|
||||
expect(triggered).toBe(false);
|
||||
expect(dragging).toBe(false);
|
||||
|
||||
graph.emit('keydown', { key: 'shift' });
|
||||
graph.emit('dragstart', { clientX: 150, clientY: 150, target: graph.get('canvas') });
|
||||
expect(triggered).toBe(false);
|
||||
expect(dragging).toBe(false);
|
||||
graph.emit('drag', { clientX: 160, clientY: 160, target: graph.get('canvas') });
|
||||
expect(triggered).toBe(false);
|
||||
graph.emit('dragend', { clientX: 160, clientY: 160 });
|
||||
expect(triggered).toBe(false);
|
||||
|
||||
graph.emit('keyup', { key: 'shift' });
|
||||
graph.emit('dragstart', { clientX: 150, clientY: 150, target: graph.get('canvas') });
|
||||
graph.emit('drag', { clientX: 150, clientY: 150, target: graph.get('canvas') });
|
||||
expect(triggered).toBe(false);
|
||||
expect(dragging).toBe(false);
|
||||
graph.emit('drag', { clientX: 160, clientY: 160, target: graph.get('canvas') });
|
||||
expect(triggered).toBe(true);
|
||||
graph.destroy();
|
||||
});
|
||||
it('drag with keydown code invalid', () => {
|
||||
const graph = new Graph({
|
||||
container: div,
|
||||
width: 500,
|
||||
height: 500,
|
||||
modes: {
|
||||
default: [
|
||||
{
|
||||
type: 'drag-canvas',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
let triggered = false;
|
||||
graph.on('canvas:dragstart', () => {
|
||||
triggered = true;
|
||||
});
|
||||
graph.emit('keydown', {}); // key undefined
|
||||
graph.emit('dragstart', { clientX: 150, clientY: 150, target: graph.get('canvas') });
|
||||
graph.emit('dragend', { clientX: 160, clientY: 160 });
|
||||
expect(triggered).toBe(false);
|
||||
graph.emit('keyup', {});
|
||||
|
||||
graph.emit('keydown', { key: 'abc' }); // key invalid
|
||||
graph.emit('dragstart', { clientX: 150, clientY: 150, target: graph.get('canvas') });
|
||||
graph.emit('dragend', { clientX: 160, clientY: 160 });
|
||||
expect(triggered).toBe(false);
|
||||
graph.emit('keyup', {});
|
||||
graph.destroy();
|
||||
});
|
||||
|
||||
it('drag out of canvas', () => {
|
||||
const graph = new Graph({
|
||||
container: div,
|
||||
width: 500,
|
||||
height: 500,
|
||||
modes: {
|
||||
default: [
|
||||
{
|
||||
type: 'drag-canvas',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
graph.emit('dragstart', { clientX: 150, clientY: 150, target: graph.get('canvas') });
|
||||
graph.emit('drag', { clientX: 550, clientY: 550, target: graph.get('canvas') });
|
||||
graph.emit('canvas:mouseleave', { target: graph.get('canvas').get('el') });
|
||||
|
||||
// dragend out of the canvas
|
||||
const event = document.createEvent('MouseEvents');
|
||||
event.initMouseEvent(
|
||||
'dragend',
|
||||
true,
|
||||
true,
|
||||
document.defaultView,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
550,
|
||||
550, // clientX = 550, clientY = 550
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
0,
|
||||
null,
|
||||
);
|
||||
document.body.dispatchEvent(event);
|
||||
const movedMatrix = graph.get('canvas').get('children')[0].getMatrix();
|
||||
expect(movedMatrix[6]).toEqual(400);
|
||||
expect(movedMatrix[7]).toEqual(400);
|
||||
});
|
||||
|
||||
it('drag out of canvas, but it is not dragging', () => {
|
||||
const graph = new Graph({
|
||||
container: div,
|
||||
width: 500,
|
||||
height: 500,
|
||||
modes: {
|
||||
default: [
|
||||
{
|
||||
type: 'drag-canvas',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
graph.emit('dragstart', { clientX: 150, clientY: 150, target: graph.get('canvas') });
|
||||
graph.emit('drag', { clientX: 350, clientY: 350, target: graph.get('canvas') });
|
||||
graph.emit('dragend', { clientX: 350, clientY: 350 });
|
||||
graph.emit('canvas:mouseleave', { target: graph.get('canvas').get('el') });
|
||||
|
||||
// dragend out of the canvas
|
||||
const event = document.createEvent('MouseEvents');
|
||||
event.initMouseEvent(
|
||||
'dragend',
|
||||
true,
|
||||
true,
|
||||
document.defaultView,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
550,
|
||||
550, // clientX = 550, clientY = 550
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
0,
|
||||
null,
|
||||
);
|
||||
document.body.dispatchEvent(event);
|
||||
const movedMatrix = graph.get('canvas').get('children')[0].getMatrix();
|
||||
expect(movedMatrix[6]).toEqual(200);
|
||||
expect(movedMatrix[7]).toEqual(200);
|
||||
});
|
||||
|
||||
it('enable optimize to hide the shapes while dragging', () => {
|
||||
const graph = new Graph({
|
||||
container: div,
|
||||
width: 500,
|
||||
height: 500,
|
||||
modes: {
|
||||
default: [
|
||||
{
|
||||
type: 'drag-canvas',
|
||||
enableOptimize: true
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
graph.emit('dragstart', { clientX: 150, clientY: 150, target: graph.get('canvas') });
|
||||
graph.emit('drag', { clientX: 350, clientY: 350, target: graph.get('canvas') });
|
||||
|
||||
let nodes = graph.getNodes();
|
||||
nodes.forEach(node => {
|
||||
const children = node.getContainer().get('children');
|
||||
children.forEach(child => {
|
||||
if (!child.get('isKeyShape')) expect(child.get('visible')).toEqual(false);
|
||||
else expect(child.get('visible')).toEqual(true);
|
||||
});
|
||||
})
|
||||
let edges = graph.getEdges();
|
||||
edges.forEach(edge => {
|
||||
const children = edge.getContainer().get('children');
|
||||
children.forEach(child => {
|
||||
expect(child.get('visible')).toEqual(false);
|
||||
});
|
||||
})
|
||||
|
||||
graph.emit('dragend', { clientX: 350, clientY: 350 });
|
||||
|
||||
nodes = graph.getNodes();
|
||||
nodes.forEach(node => {
|
||||
const children = node.getContainer().get('children');
|
||||
children.forEach(child => {
|
||||
if (!child.get('isKeyShape')) expect(child.get('visible')).toEqual(true);
|
||||
else expect(child.get('visible')).toEqual(true);
|
||||
});
|
||||
})
|
||||
edges = graph.getEdges();
|
||||
edges.forEach(edge => {
|
||||
const children = edge.getContainer().get('children');
|
||||
children.forEach(child => {
|
||||
expect(child.get('visible')).toEqual(true);
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
it('drag canvas with scalableRange', () => {
|
||||
const graph = new Graph({
|
||||
container: div,
|
||||
width: 500,
|
||||
height: 500,
|
||||
modes: {
|
||||
default: [{
|
||||
type: 'drag-canvas',
|
||||
scalableRange: -150
|
||||
}],
|
||||
},
|
||||
});
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
let start = false;
|
||||
graph.on('canvas:dragstart', () => {
|
||||
start = true;
|
||||
});
|
||||
graph.on('canvas:dragend', () => {
|
||||
start = false;
|
||||
});
|
||||
graph.paint();
|
||||
graph.emit('dragstart', { clientX: 150, clientY: 150, target: graph.get('canvas') });
|
||||
graph.emit('drag', { clientX: 200, clientY: 200, target: graph.get('canvas') });
|
||||
expect(start).toBe(true);
|
||||
graph.emit('drag', { clientX: 250, clientY: 250, target: graph.get('canvas') });
|
||||
expect(start).toBe(true);
|
||||
let matrix = graph.get('group').getMatrix();
|
||||
expect(matrix[6]).toEqual(100);
|
||||
expect(matrix[7]).toEqual(100);
|
||||
|
||||
graph.emit('drag', { clientX: 400, clientY: 350, target: graph.get('canvas') });
|
||||
matrix = graph.get('group').getMatrix();
|
||||
expect(matrix[6]).toEqual(250);
|
||||
expect(matrix[7]).toEqual(200);
|
||||
expect(start).toBe(true);
|
||||
|
||||
// 超过了设置的范围,则不再移动
|
||||
graph.emit('drag', { clientX: 550, clientY: 550, target: graph.get('canvas') });
|
||||
matrix = graph.get('group').getMatrix();
|
||||
expect(matrix[6]).toEqual(250);
|
||||
expect(matrix[7]).toEqual(200);
|
||||
expect(start).toBe(true);
|
||||
|
||||
graph.emit('dragend', {});
|
||||
expect(start).toBe(false);
|
||||
graph.destroy();
|
||||
});
|
||||
});
|
390
packages/core/tests/unit/behavior/drag-combo-spec.ts
Normal file
390
packages/core/tests/unit/behavior/drag-combo-spec.ts
Normal file
@ -0,0 +1,390 @@
|
||||
import '../../../src/behavior';
|
||||
import '../../../src/shape';
|
||||
import Graph from '../../../src/graph/graph';
|
||||
import G6 from '../../../src';
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.id = 'drag-combo-spec';
|
||||
document.body.appendChild(div);
|
||||
|
||||
G6.registerCombo(
|
||||
'custom-combo',
|
||||
{
|
||||
draw: (cfg, group) => {
|
||||
const style = cfg.style || {};
|
||||
const keyShape = group.addShape('circle', {
|
||||
attrs: style,
|
||||
className: 'circle-combo',
|
||||
name: 'circle-combo',
|
||||
draggable: true,
|
||||
});
|
||||
group.addShape('marker', {
|
||||
attrs: {
|
||||
x: 0, //keyShape.attr('r') + 5,
|
||||
y: 0,
|
||||
r: 5,
|
||||
stroke: 'blue',
|
||||
symbol: 'triangle-down',
|
||||
},
|
||||
name: 'marker-shape',
|
||||
});
|
||||
return keyShape;
|
||||
},
|
||||
update: (cfg, item) => {
|
||||
const group = item.get('group');
|
||||
if (cfg.markerStyle) {
|
||||
const marker = group.find((ele) => ele.get('name') === 'marker-shape');
|
||||
marker.attr(cfg.markerStyle);
|
||||
}
|
||||
const keyShape = group.get('children')[0];
|
||||
keyShape.attr(cfg.style);
|
||||
},
|
||||
},
|
||||
'circle-combo',
|
||||
);
|
||||
|
||||
describe('drag-combo', () => {
|
||||
it('drag combo', () => {
|
||||
const data = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'node1',
|
||||
x: 150,
|
||||
y: 150,
|
||||
label: 'node1',
|
||||
comboId: 'A',
|
||||
},
|
||||
{
|
||||
id: 'node2',
|
||||
x: 200,
|
||||
y: 250,
|
||||
label: 'node2',
|
||||
comboId: 'A',
|
||||
},
|
||||
{
|
||||
id: 'node3',
|
||||
x: 100,
|
||||
y: 250,
|
||||
label: 'node3',
|
||||
},
|
||||
{
|
||||
id: 'node4',
|
||||
x: 200,
|
||||
y: 350,
|
||||
label: 'node4',
|
||||
comboId: 'B',
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: 'node1',
|
||||
target: 'node4',
|
||||
},
|
||||
{
|
||||
source: 'node1',
|
||||
target: 'node2',
|
||||
},
|
||||
{
|
||||
source: 'node2',
|
||||
target: 'node3',
|
||||
},
|
||||
],
|
||||
combos: [
|
||||
{
|
||||
id: 'A',
|
||||
parentId: 'C',
|
||||
label: 'gorup A',
|
||||
type: 'circle',
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
parentId: 'C',
|
||||
label: 'gorup B',
|
||||
},
|
||||
{
|
||||
id: 'C',
|
||||
label: 'gorup C',
|
||||
// type: 'rect'
|
||||
},
|
||||
{
|
||||
id: 'F',
|
||||
label: 'gorup F',
|
||||
// type: 'rect'
|
||||
},
|
||||
{
|
||||
id: 'G',
|
||||
label: 'gorup G',
|
||||
// parentId: 'F'
|
||||
type: 'custom-combo',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const graph = new G6.Graph({
|
||||
container: 'drag-combo-spec',
|
||||
width: 1000,
|
||||
height: 800,
|
||||
modes: {
|
||||
default: ['drag-canvas', 'drag-combo'],
|
||||
},
|
||||
defaultCombo: {
|
||||
// size: [100, 100],
|
||||
// type: 'custom-combo',
|
||||
style: {
|
||||
fill: '#b5f5ec',
|
||||
},
|
||||
},
|
||||
comboStateStyles: {
|
||||
active: {
|
||||
stroke: 'red',
|
||||
},
|
||||
selected: {
|
||||
'text-shape': {
|
||||
fill: '#f00',
|
||||
fontSize: 20,
|
||||
},
|
||||
fill: '#36cfc9',
|
||||
},
|
||||
state2: {
|
||||
stroke: '#0f0',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
graph.on('node:click', (e) => {
|
||||
// graph.hideItem(e.item);
|
||||
// graph.render()
|
||||
});
|
||||
graph.on('combo:click', (e) => {
|
||||
// selected = !selected;
|
||||
graph.setItemState(e.item, 'selected', true);
|
||||
const combos = graph.findAllByState('combo', 'selected');
|
||||
// graph.setItemState(e.item, 'state2', selected);
|
||||
// graph.getNodes().forEach(node => {
|
||||
// node.hide();
|
||||
// });
|
||||
// graph.hideItem(e.item);
|
||||
// graph.updateItem(e.item, {
|
||||
// // type: 'rect',
|
||||
// style: {
|
||||
// fill: '#f00'
|
||||
// },
|
||||
// label: 'new Label',
|
||||
// labelCfg: {
|
||||
// position: 'bottom'
|
||||
// }
|
||||
// });
|
||||
// graph.uncombo(e.item);
|
||||
});
|
||||
|
||||
graph.on('canvas:click', (evt) => {
|
||||
const combos = graph.findAllByState('combo', 'selected');
|
||||
combos.map((combo) => {
|
||||
graph.clearItemStates(combo);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('combo example', () => {
|
||||
const data = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'node1',
|
||||
x: 150,
|
||||
y: 150,
|
||||
label: 'node1',
|
||||
comboId: 'A',
|
||||
},
|
||||
{
|
||||
id: 'node2',
|
||||
x: 200,
|
||||
y: 250,
|
||||
label: 'node2',
|
||||
comboId: 'B',
|
||||
},
|
||||
{
|
||||
id: 'node3',
|
||||
x: 100,
|
||||
y: 250,
|
||||
label: 'node3',
|
||||
},
|
||||
{
|
||||
id: 'node4',
|
||||
x: 50,
|
||||
y: 50,
|
||||
label: 'node4',
|
||||
comboId: 'D',
|
||||
},
|
||||
{
|
||||
id: 'node5',
|
||||
x: 100,
|
||||
y: 100,
|
||||
label: 'node5',
|
||||
comboId: 'E',
|
||||
},
|
||||
{
|
||||
id: 'node6',
|
||||
x: 500,
|
||||
y: 200,
|
||||
label: 'node6',
|
||||
},
|
||||
{
|
||||
id: 'node7',
|
||||
x: 600,
|
||||
y: 200,
|
||||
label: 'node7',
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: 'node1',
|
||||
target: 'node2',
|
||||
},
|
||||
{
|
||||
source: 'node2',
|
||||
target: 'node3',
|
||||
},
|
||||
{
|
||||
source: 'node3',
|
||||
target: 'node1',
|
||||
},
|
||||
{
|
||||
source: 'A',
|
||||
target: 'D',
|
||||
},
|
||||
],
|
||||
combos: [
|
||||
{
|
||||
id: 'A',
|
||||
parentId: 'B',
|
||||
label: 'gorup A',
|
||||
padding: [50, 30, 10, 10],
|
||||
type: 'rect',
|
||||
// style: {
|
||||
// stroke: 'red',
|
||||
// fill: 'green'
|
||||
// },
|
||||
// collapsed: true
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
label: 'gorup B',
|
||||
padding: [50, 10, 10, 50],
|
||||
// type: 'custom-combo'
|
||||
},
|
||||
{
|
||||
id: 'D',
|
||||
label: 'gorup D',
|
||||
parentId: 'E',
|
||||
},
|
||||
{
|
||||
id: 'E',
|
||||
// collapsed: true
|
||||
},
|
||||
{
|
||||
id: 'FF',
|
||||
label: '空分组',
|
||||
// type: 'custom-combo',
|
||||
// style: {
|
||||
// stroke: 'green',
|
||||
// lineWidth: 3
|
||||
// }
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const graph = new G6.Graph({
|
||||
container: 'drag-combo-spec',
|
||||
width: 1000,
|
||||
height: 800,
|
||||
groupByTypes: false,
|
||||
modes: {
|
||||
default: [
|
||||
'drag-canvas',
|
||||
{
|
||||
type: 'drag-combo',
|
||||
activeState: 'active',
|
||||
},
|
||||
{
|
||||
type: 'drag-node',
|
||||
comboActiveState: 'active',
|
||||
},
|
||||
'collapse-expand-combo',
|
||||
'click-select',
|
||||
],
|
||||
},
|
||||
defaultCombo: {
|
||||
// size: [100, 100],
|
||||
type: 'circle',
|
||||
style: {
|
||||
fill: '#f0f0f0',
|
||||
stroke: '#bfbfbf',
|
||||
},
|
||||
},
|
||||
comboStateStyles: {
|
||||
selected: {
|
||||
// 'text-shape': {
|
||||
// fill: '#f00',
|
||||
// fontSize: 20
|
||||
// },
|
||||
stroke: '#8c8c8c',
|
||||
},
|
||||
active: {
|
||||
stroke: '#722ed1',
|
||||
lineWidth: 2,
|
||||
},
|
||||
state2: {
|
||||
stroke: '#0f0',
|
||||
},
|
||||
},
|
||||
nodeStateStyles: {
|
||||
selected: {
|
||||
stroke: 'green',
|
||||
lineWidth: 2,
|
||||
},
|
||||
},
|
||||
defaultEdge: {
|
||||
style: {
|
||||
stroke: '#bfbfbf',
|
||||
lineWidth: 2,
|
||||
endArrow: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
graph.on('nodeselectchange', (evt) => {
|
||||
// console.log(evt);
|
||||
});
|
||||
|
||||
// 删错 combo
|
||||
// graph.on('combo:click', evt => {
|
||||
// const { item } = evt
|
||||
// graph.removeItem(item)
|
||||
// })
|
||||
|
||||
// graph.on('combo:click', evt => {
|
||||
// const { item } = evt
|
||||
// graph.uncombo(item)
|
||||
// })
|
||||
|
||||
// setTimeout(() => {
|
||||
// graph.updateComboTree('D')
|
||||
// }, 1000)
|
||||
|
||||
// graph.on('combo:mouseenter', evt => {
|
||||
// const { item } = evt
|
||||
// graph.setItemState(item, 'hover', true)
|
||||
// })
|
||||
|
||||
// graph.on('combo:mouseleave', evt => {
|
||||
// const { item } = evt
|
||||
// graph.setItemState(item, 'hover', false)
|
||||
// })
|
||||
});
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user