Merge branch 'master' of https://github.com/antvis/G6 into elint-fix-behavior

This commit is contained in:
xinhui.zxh 2020-02-07 14:22:50 +08:00
commit 98864ecf6c
36 changed files with 655 additions and 553 deletions

View File

@ -1,12 +1,12 @@
module.exports = {
extends: [require.resolve('@umijs/fabric/dist/eslint')],
globals: {
$: true,
_: true,
},
rules: {
'no-bitwise': 0,
'import/order': 0,
'no-plusplus': 0,
},
};
extends: [require.resolve('@umijs/fabric/dist/eslint')],
globals: {
$: true,
_: true,
},
rules: {
'no-bitwise': 0,
'import/order': 0,
'no-plusplus': 0,
},
};

View File

@ -1,3 +1,3 @@
// window.g6 = require('./src/index.ts'); // import the source for debugging
// window.g6 = require('./src/index.ts') // import the source for debugging
window.g6 = require('./dist/g6.min.js') // import the package for webworker
window.insertCss = require('insert-css');

View File

@ -1,6 +1,6 @@
{
"name": "@antv/g6",
"version": "3.3.0-beta.3",
"version": "3.3.0-beta.5",
"description": "A Graph Visualization Framework in JavaScript",
"keywords": [
"antv",
@ -49,7 +49,7 @@
"site:develop": "GATSBY=true gatsby develop --open",
"start": "npm run site:develop",
"test": "jest",
"test-live": "DEBUG_MODE=1 jest --watch ./tests/unit/behavior/brush-select-spec.ts",
"test-live": "DEBUG_MODE=1 jest --watch ./tests/unit/layout/web-worker-spec.ts",
"lint-staged:js": "eslint --ext .js,.jsx,.ts,.tsx",
"watch": "father build -w",
"cdn": "antv-bin upload -n @antv/g6"

View File

@ -1,5 +1,5 @@
export default {
version: '3.3.0-beta.3',
version: '3.3.0-beta.5',
rootContainerClassName: 'root-container',
nodeContainerClassName: 'node-container',
edgeContainerClassName: 'edge-container',

View File

@ -5,8 +5,8 @@ import ShapeBase from '@antv/g-canvas/lib/shape/base';
import each from '@antv/util/lib/each'
import isNil from '@antv/util/lib/is-nil';
import wrapBehavior from '@antv/util/lib/wrap-behavior';
import { IGraph } from '../../interface/graph';
import { IG6GraphEvent, Matrix } from '../../types';
import Graph from "../graph"
import { IG6GraphEvent, Matrix, Item } from '../../types';
import { cloneEvent, isViewportChanged } from '../../util/base';
type Fun = () => void
@ -35,14 +35,14 @@ const EVENTS = [
'touchend',
];
export default class EventController {
private graph: IGraph
private graph: Graph
private extendEvents: any[]
private canvasHandler: Fun;
private canvasHandler!: Fun;
private dragging: boolean
private preItem
private preItem: Item | null = null
public destroyed: boolean
constructor(graph: IGraph) {
constructor(graph: Graph) {
this.graph = graph
this.extendEvents = []
this.dragging = false
@ -53,7 +53,7 @@ export default class EventController {
// 初始化 G6 中的事件
private initEvents() {
const self = this
const graph: IGraph = this.graph;
const graph: Graph = this.graph;
const canvas: Canvas = graph.get('canvas');
const el = canvas.get('el');
const extendEvents = this.extendEvents;
@ -245,8 +245,8 @@ export default class EventController {
this.dragging = false
this.preItem = null
this.extendEvents.length = 0
this.canvasHandler = null
this.extendEvents.length = 0;
(this.canvasHandler as Fun | null) = null
this.destroyed = true
}
}

View File

@ -8,7 +8,7 @@ import isString from '@antv/util/lib/is-string'
import upperFirst from '@antv/util/lib/upper-first'
import Edge from '../../item/edge';
import Node from '../../item/node';
import { EdgeConfig, Item, ITEM_TYPE, ModelConfig, NodeConfig, NodeMapConfig } from '../../types';
import { EdgeConfig, Item, ITEM_TYPE, ModelConfig, NodeConfig, NodeMap } from '../../types';
import Graph from '../graph';
import { IEdge, INode } from '../../interface/item';
@ -36,12 +36,12 @@ export default class ItemController {
* @returns {(Item)}
* @memberof ItemController
*/
public addItem<T extends Item>(type: ITEM_TYPE, model: ModelConfig): T {
public addItem<T extends Item>(type: ITEM_TYPE, model: ModelConfig) {
const graph = this.graph;
const parent: Group = graph.get(type + 'Group') || graph.get('group');
const upperType = upperFirst(type);
let item;
let item: Item | null = null;
let styles = graph.get(type + upperFirst(STATE_SUFFIX)) || {};
const defaultModel = graph.get(CFG_PREFIX + upperType);
@ -79,8 +79,8 @@ export default class ItemController {
graph.emit('beforeadditem', { type, model });
if(type === EDGE) {
let source: string | Item = (model as EdgeConfig).source
let target: string | Item = (model as EdgeConfig).target
let source: string | Item | undefined = (model as EdgeConfig).source
let target: string | Item | undefined = (model as EdgeConfig).target
if (source && isString(source)) {
source = graph.findById(source);
@ -110,11 +110,13 @@ export default class ItemController {
})
}
graph.get(type + 's').push(item);
graph.get('itemMap')[item.get('id')] = item;
graph.autoPaint();
graph.emit('afteradditem', { item, model });
return item;
if (item) {
graph.get(type + 's').push(item);
graph.get('itemMap')[item.get('id')] = item;
graph.autoPaint();
graph.emit('afteradditem', { item, model });
return item as T;
}
}
/**
@ -230,7 +232,7 @@ export default class ItemController {
items.splice(index, 1);
const itemId: string = item.get('id')
const itemMap: NodeMapConfig = graph.get('itemMap')
const itemMap: NodeMap = graph.get('itemMap')
delete itemMap[itemId];
if (type === NODE) {
@ -276,7 +278,7 @@ export default class ItemController {
* @param {string[]} states
* @memberof ItemController
*/
public clearItemStates(item: Item | string, states: string | string[]): void {
public clearItemStates(item: Item | string, states?: string | string[]): void {
const graph = this.graph;
if (isString(item)) {
@ -354,7 +356,7 @@ export default class ItemController {
}
public destroy() {
this.graph = null;
(this.graph as Graph | null) = null;
this.destroyed = true;
}
}

View File

@ -4,9 +4,10 @@ import isString from '@antv/util/lib/is-string'
import Behavior from '../../behavior/behavior'
import { IBehavior } from '../../interface/behavior';
import { IGraph, IMode, IModeType } from '../../interface/graph';
import Graph from '../graph';
export default class ModeController {
private graph: IGraph
private graph: Graph
public destroyed: boolean
/**
* modes = {
@ -32,7 +33,7 @@ export default class ModeController {
*/
public mode: string
private currentBehaves: IBehavior[]
constructor(graph: IGraph) {
constructor(graph: Graph) {
this.graph = graph
this.destroyed = false
this.modes = graph.get('modes') || {
@ -62,7 +63,7 @@ export default class ModeController {
const behaviors = this.modes[mode];
const behaves: IBehavior[] = [];
let behave: IBehavior;
each(behaviors, behavior => {
each(behaviors || [], behavior => {
const BehaviorInstance = Behavior.getBehavior(behavior.type)
if (!BehaviorInstance) {
return;
@ -70,14 +71,14 @@ export default class ModeController {
behave = new BehaviorInstance(behavior);
if(behave) {
behave.bind(graph)
behave.bind(graph as IGraph)
behaves.push(behave);
}
});
this.currentBehaves = behaves;
}
private mergeBehaviors(modeBehaviors: IModeType[], behaviors): IModeType[] {
private mergeBehaviors(modeBehaviors: IModeType[], behaviors: IModeType[]): IModeType[] {
each(behaviors, behavior => {
if (modeBehaviors.indexOf(behavior) < 0) {
if (isString(behavior)) {
@ -89,7 +90,7 @@ export default class ModeController {
return modeBehaviors;
}
private filterBehaviors(modeBehaviors: IModeType[], behaviors): IModeType[] {
private filterBehaviors(modeBehaviors: IModeType[], behaviors: IModeType[]): IModeType[] {
const result: IModeType[] = [];
modeBehaviors.forEach(behavior => {
let type: string = ''
@ -105,7 +106,7 @@ export default class ModeController {
return result;
}
public setMode(mode: string): ModeController {
public setMode(mode: string) {
const modes = this.modes;
const graph = this.graph;
const current = mode
@ -143,22 +144,24 @@ export default class ModeController {
*/
public manipulateBehaviors(behaviors: IModeType[] | IModeType, modes: string[] | string, isAdd: boolean): ModeController {
const self = this
let behaves = behaviors
let behaves: IModeType[]
if(!isArray(behaviors)) {
behaves = [ behaviors ]
}else {
behaves = behaviors
}
if(isArray(modes)) {
each(modes, mode => {
if (!self.modes[mode]) {
if (isAdd) {
self.modes[mode] = [].concat(behaves);
self.modes[mode] = behaves
}
} else {
if (isAdd) {
self.modes[mode] = this.mergeBehaviors(self.modes[mode], behaves);
self.modes[mode] = this.mergeBehaviors(self.modes[mode] || [], behaves);
} else {
self.modes[mode] = this.filterBehaviors(self.modes[mode], behaves);
self.modes[mode] = this.filterBehaviors(self.modes[mode] || [], behaves);
}
}
})
@ -173,14 +176,14 @@ export default class ModeController {
if(!this.modes[currentMode]) {
if (isAdd) {
self.modes[currentMode] = [].concat(behaves);
self.modes[currentMode] = behaves
}
}
if (isAdd) {
self.modes[currentMode] = this.mergeBehaviors(self.modes[currentMode], behaves);
self.modes[currentMode] = this.mergeBehaviors(self.modes[currentMode] || [], behaves);
} else {
self.modes[currentMode] = this.filterBehaviors(self.modes[currentMode], behaves);
self.modes[currentMode] = this.filterBehaviors(self.modes[currentMode] || [], behaves);
}
self.setMode(this.mode)
@ -189,9 +192,9 @@ export default class ModeController {
}
public destroy() {
this.graph = null;
this.modes = null;
this.currentBehaves = null;
(this.graph as Graph | null) = null;
(this.modes as IMode | null) = null;
(this.currentBehaves as IBehavior[] | null) = null;
this.destroyed = true;
}
}

View File

@ -2,21 +2,25 @@ import each from '@antv/util/lib/each'
import isString from '@antv/util/lib/is-string'
import { IGraph, IStates } from '../../interface/graph';
import { Item } from '../../types';
import Graph from '../graph';
import { INode } from '../../interface/item';
interface ICachedStates {
enabled: IStates;
disabled: IStates;
}
let timer = null
let timer: number | null = null
const TIME_OUT = 16;
export default class StateController {
private graph: IGraph
private graph: Graph
private cachedStates: ICachedStates
public destroyed: boolean
constructor(graph: IGraph) {
constructor(graph: Graph) {
this.graph = graph
/**
* this.cachedStates = {
@ -43,7 +47,7 @@ export default class StateController {
* @returns
* @memberof State
*/
private checkCache(item: Item, state: string, cache: object) {
private checkCache(item: Item, state: string, cache: { [key: string]: any }) {
if (!cache[state]) {
return;
}
@ -62,11 +66,11 @@ export default class StateController {
* @param {object} states
* @memberof State
*/
private cacheState(item: Item, state: string, states: object) {
private cacheState(item: Item, state: string, states: IStates) {
if (!states[state]) {
states[state] = [];
}
states[state].push(item);
states[state].push(item as INode);
}
/**
@ -132,7 +136,7 @@ export default class StateController {
* @memberof State
*/
public updateGraphStates() {
const states = this.graph.get('states')
const states = this.graph.get('states') as IStates
const cachedStates = this.cachedStates;
each(cachedStates.disabled, (val, key) => {
@ -143,22 +147,22 @@ export default class StateController {
}
});
each(cachedStates.enabled, (val, key) => {
each(cachedStates.enabled, (val: INode[], key) => {
if (!states[key]) {
states[key] = val;
} else {
const map = {};
const map: { [key: string]: boolean } = {};
states[key].forEach(item => {
if (!item.destroyed) {
map[item.get('id')] = true;
}
});
val.forEach(item => {
val.forEach((item:Item) => {
if (!item.destroyed) {
const id = item.get('id');
if (!map[id]) {
map[id] = true;
states[key].push(item);
states[key].push(item as INode);
}
}
});
@ -173,8 +177,8 @@ export default class StateController {
}
public destroy() {
this.graph = null;
this.cachedStates = null;
(this.graph as Graph | null) = null;
(this.cachedStates as ICachedStates | null) = null;
if (timer) {
clearTimeout(timer);
}

View File

@ -7,11 +7,12 @@ import { IGraph } from "../../interface/graph";
import { Item, Matrix, Padding } from '../../types';
import { formatPadding } from '../../util/base'
import { applyMatrix, invertMatrix } from '../../util/math';
import Graph from '../graph';
export default class ViewController {
private graph: IGraph = null
private graph: Graph
public destroyed: boolean = false
constructor(graph: IGraph) {
constructor(graph: Graph) {
this.graph = graph
this.destroyed = false
}
@ -53,7 +54,7 @@ export default class ViewController {
}
public getFormatPadding(): number[] {
const padding = this.graph.get<Padding>('fitViewPadding')
const padding = this.graph.get('fitViewPadding') as Padding
return formatPadding(padding)
}
@ -91,7 +92,7 @@ export default class ViewController {
* @param x x
* @param y y
*/
public getClientByPoint(x, y): Point {
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);
@ -104,7 +105,7 @@ export default class ViewController {
* @param x x
* @param y y
*/
public getCanvasByPoint(x, y): Point {
public getCanvasByPoint(x: number, y: number): Point {
const viewportMatrix: Matrix = this.graph.get('group').getMatrix();
return applyMatrix({ x, y }, viewportMatrix);
}
@ -146,7 +147,7 @@ export default class ViewController {
}
public destroy() {
this.graph = null
(this.graph as Graph | null) = null
this.destroyed = false
}
}

View File

@ -10,7 +10,7 @@ import each from '@antv/util/lib/each';
import isPlainObject from '@antv/util/lib/is-plain-object';
import isString from '@antv/util/lib/is-string';
import { GraphAnimateConfig, GraphOptions, IGraph, IModeOption, IModeType, IStates } from '../interface/graph';
import { IEdge, INode } from '../interface/item';
import { IEdge, INode, IItemBase } from '../interface/item';
import {
EdgeConfig,
GraphData,
@ -20,7 +20,7 @@ import {
Matrix,
ModelConfig,
NodeConfig,
NodeMapConfig,
NodeMap,
Padding,
TreeGraphData,
} from '../types';
@ -37,6 +37,7 @@ import {
StateController,
ViewController,
} from './controller';
import PluginBase from "../plugins/base"
const NODE = 'node';
const EDGE = 'edge';
@ -57,13 +58,13 @@ export interface PrivateGraphOption extends GraphOptions {
groups: GroupConfig[];
itemMap: NodeMapConfig;
itemMap: NodeMap;
callback: () => void;
groupBBoxs: IGroupBBox;
groupNodes: NodeMapConfig;
groupNodes: NodeMap;
/**
*
@ -77,7 +78,7 @@ export interface PrivateGraphOption extends GraphOptions {
export default class Graph extends EventEmitter implements IGraph {
private animating: boolean;
private _cfg: GraphOptions;
private _cfg: GraphOptions & { [key: string]: any };
public destroyed: boolean;
constructor(cfg: GraphOptions) {
@ -114,7 +115,7 @@ export default class Graph extends EventEmitter implements IGraph {
}
private initCanvas() {
let container: string | HTMLElement = this.get('container');
let container: string | HTMLElement | null = this.get('container');
if (isString(container)) {
container = document.getElementById(container);
this.set('container', container);
@ -186,7 +187,7 @@ export default class Graph extends EventEmitter implements IGraph {
this.set('group', group);
}
public getDefaultCfg(): PrivateGraphOption {
public getDefaultCfg(): Partial<PrivateGraphOption> {
return {
/**
* Container could be dom object or dom id
@ -315,7 +316,7 @@ export default class Graph extends EventEmitter implements IGraph {
/**
* 线
*/
onFrame: null,
onFrame: undefined,
/**
* (ms)
*/
@ -325,7 +326,7 @@ export default class Graph extends EventEmitter implements IGraph {
*/
easing: 'easeLinear',
},
callback: null,
callback: undefined,
/**
* group类型
*/
@ -441,18 +442,18 @@ export default class Graph extends EventEmitter implements IGraph {
* @param {(item: T, index: number) => T} fn
* @return {T}
*/
public find<T extends Item>(type: ITEM_TYPE, fn: (item: T, index?: number) => boolean): T {
let result;
public find<T extends Item>(type: ITEM_TYPE, fn: (item: T, index?: number) => boolean): T | undefined {
let result: T | undefined;
const items = this.get(type + 's');
each(items, (item, i) => {
if (fn(item, i)) {
result = item;
return false;
return result
}
});
return result;
return result
}
/**
@ -462,7 +463,7 @@ export default class Graph extends EventEmitter implements IGraph {
* @return {array}
*/
public findAll<T extends Item>(type: ITEM_TYPE, fn: (item: T, index?: number) => boolean): T[] {
const result = [];
const result: T[] = [];
each(this.get(type + 's'), (item, i) => {
if (fn(item, i)) {
@ -739,7 +740,7 @@ export default class Graph extends EventEmitter implements IGraph {
* @param {ModelConfig} model
* @return {Item}
*/
public addItem(type: ITEM_TYPE, model: ModelConfig): Item {
public addItem(type: ITEM_TYPE, model: ModelConfig) {
if (type === 'group') {
const { groupId, nodes, type: groupType, zIndex, title } = model;
let groupTitle = title;
@ -813,23 +814,25 @@ export default class Graph extends EventEmitter implements IGraph {
throw new Error('data must be defined first');
}
const { nodes = [], edges = [] } = data;
this.clear();
this.emit('beforerender');
const autoPaint = this.get('autoPaint');
this.setAutoPaint(false);
each(data.nodes, (node: NodeConfig) => {
each(nodes, (node: NodeConfig) => {
self.add('node', node);
});
each(data.edges, (edge: EdgeConfig) => {
each(edges, (edge: EdgeConfig) => {
self.add('edge', edge);
});
if (!this.get('groupByTypes')) {
// 为提升性能,选择数量少的进行操作
if (data.nodes.length < data.edges.length) {
if (data.nodes && data.edges && data.nodes.length < data.edges.length) {
const nodes = this.getNodes();
// 遍历节点实例,将所有节点提前。
@ -885,10 +888,10 @@ export default class Graph extends EventEmitter implements IGraph {
}
// 比较item
private diffItems(type: ITEM_TYPE, items, models: NodeConfig[] | EdgeConfig[]) {
private diffItems(type: ITEM_TYPE, items: { nodes: INode[], edges: IEdge[]}, models: NodeConfig[] | EdgeConfig[]) {
const self = this;
let item;
const itemMap: NodeMapConfig = this.get('itemMap');
let item: INode;
const itemMap: NodeMap = this.get('itemMap');
each(models, (model) => {
item = itemMap[model.id];
@ -906,7 +909,7 @@ export default class Graph extends EventEmitter implements IGraph {
} else {
item = self.addItem(type, model);
}
items[type + 's'].push(item);
(items as { [key:string]: any[]})[type + 's'].push(item);
});
}
@ -928,19 +931,22 @@ export default class Graph extends EventEmitter implements IGraph {
}
const autoPaint: boolean = this.get('autoPaint');
const itemMap: NodeMapConfig = this.get('itemMap');
const itemMap: NodeMap = this.get('itemMap');
const items = {
const items: {
nodes: INode[],
edges: IEdge[]
} = {
nodes: [],
edges: [],
};
this.setAutoPaint(false);
this.diffItems('node', items, (data as GraphData).nodes);
this.diffItems('edge', items, (data as GraphData).edges);
this.diffItems('node', items, (data as GraphData).nodes!);
this.diffItems('edge', items, (data as GraphData).edges!);
each(itemMap, (item: INode, id: number) => {
each(itemMap, (item: INode & IEdge, id: number) => {
if (items.nodes.indexOf(item) < 0 && items.edges.indexOf(item) < 0) {
delete itemMap[id];
self.remove(item);
@ -969,7 +975,7 @@ export default class Graph extends EventEmitter implements IGraph {
* @param {string} groupType group类型
*/
public renderCustomGroup(data: GraphData, groupType: string) {
const { groups, nodes } = data;
const { groups, nodes = [] } = data;
// 第一种情况不存在groups则不存在嵌套群组
let groupIndex = 10;
@ -977,7 +983,7 @@ export default class Graph extends EventEmitter implements IGraph {
// 存在单个群组
// 获取所有有groupID的node
const nodeInGroup = nodes.filter((node) => node.groupId);
const groupsArr = [];
const groupsArr: GroupConfig[] = [];
// 根据groupID分组
const groupIds = groupBy(nodeInGroup, 'groupId');
// tslint:disable-next-line:forin
@ -1020,10 +1026,10 @@ export default class Graph extends EventEmitter implements IGraph {
* @return {object} data
*/
public save(): TreeGraphData | GraphData {
const nodes = [];
const edges = [];
const nodes: NodeConfig[] = [];
const edges: EdgeConfig[] = [];
each(this.get('nodes'), (node: INode) => {
nodes.push(node.getModel());
nodes.push(node.getModel() as NodeConfig);
});
each(this.get('edges'), (edge: IEdge) => {
@ -1123,7 +1129,7 @@ export default class Graph extends EventEmitter implements IGraph {
const canvas: GCanvas = self.get('canvas');
canvas.animate(
(ratio) => {
(ratio: number) => {
each(toNodes, (data) => {
const node: Item = self.findById(data.id);
@ -1188,7 +1194,7 @@ export default class Graph extends EventEmitter implements IGraph {
each(nodes, (node: INode) => {
model = node.getModel() as NodeConfig;
node.updatePosition({ x: model.x, y: model.y });
node.updatePosition({ x: model.x!, y: model.y! });
});
each(edges, (edge: IEdge) => {
@ -1280,7 +1286,12 @@ export default class Graph extends EventEmitter implements IGraph {
if (typeof window !== 'undefined') {
if (window.Blob && window.URL) {
const arr = dataURL.split(',');
const mime = arr[0].match(/:(.*?);/)[1];
let mime = ""
if (arr && arr.length > 0 ) {
const match = arr[0].match(/:(.*?);/);
if (match && match.length >= 2) mime = match[1]
}
const bstr = atob(arr[1]);
let n = bstr.length;
const u8arr = new Uint8Array(n);
@ -1315,7 +1326,7 @@ export default class Graph extends EventEmitter implements IGraph {
* cfg type String
* cfg type
*/
public updateLayout(cfg): void {
public updateLayout(cfg: any): void {
const layoutController = this.get('layoutController');
let newLayoutType;
@ -1388,7 +1399,7 @@ export default class Graph extends EventEmitter implements IGraph {
*
* @param {object} plugin
*/
public addPlugin(plugin): void {
public addPlugin(plugin: PluginBase): void {
const self = this;
if (plugin.destroyed) {
return;
@ -1401,7 +1412,7 @@ export default class Graph extends EventEmitter implements IGraph {
*
* @param {object} plugin
*/
public removePlugin(plugin): void {
public removePlugin(plugin: PluginBase): void {
const plugins = this.get('plugins');
const index = plugins.indexOf(plugin);
if (index >= 0) {
@ -1428,7 +1439,7 @@ export default class Graph extends EventEmitter implements IGraph {
this.get('layoutController').destroy();
this.get('customGroupControll').destroy();
this.get('canvas').destroy();
this._cfg = null;
(this._cfg as any) = null;
this.destroyed = true;
}
}

View File

@ -38,13 +38,13 @@ export default class TreeGraph extends Graph implements ITreeGraph {
layout.direction = 'TB';
}
if (layout.radial) {
return function(data) {
return (data: any) => {
const layoutData = Hierarchy[layout.type](data, layout);
radialLayout(layoutData);
return layoutData;
};
}
return function(data) {
return (data: any) => {
return Hierarchy[layout.type](data, layout);
};
}
@ -65,7 +65,7 @@ export default class TreeGraph extends Graph implements ITreeGraph {
return index;
}
public getDefaultCfg(): PrivateGraphOption {
public getDefaultCfg(): Partial<PrivateGraphOption> {
const cfg = super.getDefaultCfg();
// 树图默认打开动画
cfg.animate = true;
@ -78,14 +78,18 @@ export default class TreeGraph extends Graph implements ITreeGraph {
* @param parent
* @param animate
*/
private innerAddChild(treeData: TreeGraphData, parent: Item, animate: boolean): Item {
private innerAddChild(treeData: TreeGraphData, parent: Item | undefined, animate: boolean): Item {
const self = this;
const model = treeData.data;
// model 中应存储真实的数据,特别是真实的 children
model.x = treeData.x;
model.y = treeData.y;
model.depth = treeData.depth;
const node = self.addItem('node', model);
if (model) {
// model 中应存储真实的数据,特别是真实的 children
model.x = treeData.x;
model.y = treeData.y;
model.depth = treeData.depth;
}
const node = self.addItem('node', model!);
if (parent) {
node.set('parent', parent);
if (animate) {
@ -113,7 +117,7 @@ export default class TreeGraph extends Graph implements ITreeGraph {
});
}
// 渲染到视图上应参考布局的children, 避免多绘制了收起的节点
each(treeData.children, child => {
each(treeData.children || [], child => {
self.innerAddChild(child, node, animate);
});
return node;
@ -125,7 +129,7 @@ export default class TreeGraph extends Graph implements ITreeGraph {
* @param parent
* @param animate
*/
private innerUpdateChild(data: TreeGraphData, parent: Item, animate: boolean) {
private innerUpdateChild(data: TreeGraphData, parent: Item | undefined, animate: boolean) {
const self = this;
const current = self.findById(data.id);
@ -136,7 +140,7 @@ export default class TreeGraph extends Graph implements ITreeGraph {
}
// 更新新节点下所有子节点
each(data.children, (child: TreeGraphData) => {
each(data.children || [], (child: TreeGraphData) => {
self.innerUpdateChild(child, current, animate);
});
@ -148,10 +152,10 @@ export default class TreeGraph extends Graph implements ITreeGraph {
for (let i = children.length - 1; i >= 0; i--) {
const child = children[i].getModel();
if (self.indexOfChild(data.children, child.id) === -1) {
if (self.indexOfChild(data.children || [], child.id) === -1) {
self.innerRemoveChild(child.id, {
x: data.x,
y: data.y
x: data.x!,
y: data.y!
}, animate);
// 更新父节点下缓存的子节点 item 实例列表
@ -169,7 +173,7 @@ export default class TreeGraph extends Graph implements ITreeGraph {
});
}
current.set('model', data.data);
current.updatePosition({ x: data.x, y: data.y });
current.updatePosition({ x: data.x!, y: data.y! });
}
/**
@ -218,7 +222,7 @@ export default class TreeGraph extends Graph implements ITreeGraph {
*
* @param {object} layout
*/
public updateLayout(layout) {
public updateLayout(layout: any) {
const self = this;
if (!layout) {
console.warn('layout cannot be null');
@ -246,7 +250,7 @@ export default class TreeGraph extends Graph implements ITreeGraph {
self.setAutoPaint(false);
self.innerUpdateChild(layoutData, null, animate);
self.innerUpdateChild(layoutData, undefined, animate);
if (fitView) {
const viewController: ViewController = self.get('viewController')
@ -258,7 +262,7 @@ export default class TreeGraph extends Graph implements ITreeGraph {
self.refresh();
self.paint();
} else {
self.layoutAnimate(layoutData, null);
self.layoutAnimate(layoutData);
}
self.setAutoPaint(autoPaint);
self.emit('afterrefreshlayout', { data, layoutData });
@ -276,13 +280,15 @@ export default class TreeGraph extends Graph implements ITreeGraph {
parent = parent.get('id') as string;
}
const parentData = self.findDataById(parent);
const parentData = self.findDataById(parent)
if (!parentData.children) {
parentData.children = [];
if (parentData) {
if (!parentData.children) {
parentData.children = [];
}
parentData.children.push(data);
self.changeData();
}
parentData.children.push(data);
self.changeData();
}
/**
@ -302,12 +308,14 @@ export default class TreeGraph extends Graph implements ITreeGraph {
const parentModel = self.findById(parent).getModel();
const current = self.findById(data.id);
if (!parentModel.children) {
// 当 current 不存在时children 为空数组
parentModel.children = [];
}
// 如果不存在该节点,则添加
if (!current) {
if (!parentModel.children) {
// 当 current 不存在时children 为空数组
parentModel.children = [];
}
parentModel.children.push(data);
} else {
const index = self.indexOfChild(parentModel.children, data.id);
@ -330,7 +338,8 @@ export default class TreeGraph extends Graph implements ITreeGraph {
const parent = node.get('parent');
if (parent && !parent.destroyed) {
const siblings = self.findDataById(parent.get('id')).children;
const parentNode = self.findDataById(parent.get('id'));
const siblings = (parentNode && parentNode.children) || [];
const model: NodeConfig = node.getModel() as NodeConfig
const index = self.indexOfChild(siblings, model.id);
@ -345,19 +354,19 @@ export default class TreeGraph extends Graph implements ITreeGraph {
* @param {TreeGraphData | undefined} parent
* @return {TreeGraphData}
*/
public findDataById(id: string, parent?: TreeGraphData | undefined): TreeGraphData {
public findDataById(id: string, parent?: TreeGraphData | undefined): TreeGraphData | null {
const self = this;
if (!parent) {
parent = self.get('data');
parent = self.get('data') as TreeGraphData;
}
if (id === parent.id) {
return parent;
}
let result = null;
each(parent.children, child => {
let result: TreeGraphData | null = null;
each(parent.children || [], child => {
if (child.id === id) {
result = child;
return false;
@ -389,7 +398,7 @@ export default class TreeGraph extends Graph implements ITreeGraph {
}
});
this.get('canvas').animate(ratio => {
this.get('canvas').animate((ratio: number) => {
traverseTree<TreeGraphData>(data, child => {
const node = self.findById(child.id);
@ -410,8 +419,8 @@ export default class TreeGraph extends Graph implements ITreeGraph {
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;
model.x = origin.x + (child.x! - origin.x) * ratio;
model.y = origin.y + (child.y! - origin.y) * ratio;
}
}
return true

View File

@ -22,8 +22,8 @@ export class G6GraphEvent extends GraphEvent implements IG6GraphEvent {
public canvasY: number
public wheelDelta: number
public detail: number
public target: Item & Canvas;
constructor(type, event) {
public target!: Item & Canvas;
constructor(type: string, event: IG6GraphEvent) {
super(type, event)
this.item = event.item
this.canvasX = event.canvasX

View File

@ -1,8 +1,9 @@
import EventEmitter from '@antv/event-emitter';
import { AnimateCfg, Point } from '@antv/g-base/lib/types';
import Graph from '../graph/graph';
import { EdgeConfig, GraphData, IG6GraphEvent, Item, ITEM_TYPE, ModelConfig, ModelStyle, NodeConfig, Padding, ShapeStyle, TreeGraphData } from '../types';
import { EdgeConfig, GraphData, IG6GraphEvent, Item, ITEM_TYPE, ModelConfig, ModelStyle, NodeConfig, Padding, ShapeStyle, TreeGraphData, LayoutConfig } from '../types';
import { IEdge, INode } from './item';
import PluginBase from '../plugins/base';
export interface IModeOption {
type: string;
@ -162,7 +163,7 @@ export interface IStates {
[key: string]: INode[]
}
export interface IGraph extends EventEmitter {
getDefaultCfg(): GraphOptions;
getDefaultCfg(): Partial<GraphOptions>;
get<T = any>(key: string): T;
set<T = any>(key: string | object, value?: T): Graph;
findById(id: string): Item;
@ -445,7 +446,7 @@ export interface IGraph extends EventEmitter {
* @param {(item: T, index: number) => T} fn
* @return {T}
*/
find<T extends Item>(type: ITEM_TYPE, fn: (item: T, index: number) => boolean): T;
find<T extends Item>(type: ITEM_TYPE, fn: (item: T, index?: number) => boolean): T | undefined;
/**
*
@ -453,7 +454,7 @@ export interface IGraph extends EventEmitter {
* @param {string} fn
* @return {array}
*/
findAll<T extends Item>(type: ITEM_TYPE, fn: (item: T, index: number) => boolean): T[];
findAll<T extends Item>(type: ITEM_TYPE, fn: (item: T, index?: number) => boolean): T[];
/**
*
@ -481,7 +482,7 @@ export interface IGraph extends EventEmitter {
* cfg type String
* cfg type
*/
updateLayout(cfg): void;
updateLayout(cfg: LayoutConfig): void;
/**
*
@ -492,13 +493,13 @@ export interface IGraph extends EventEmitter {
*
* @param {object} plugin
*/
addPlugin(plugin): void;
addPlugin(plugin: PluginBase): void;
/**
*
* @param {object} plugin
*/
removePlugin(plugin): void;
removePlugin(plugin: PluginBase): void;
/**
*
@ -545,7 +546,7 @@ export interface ITreeGraph extends IGraph {
* @param {TreeGraphData | undefined} parent
* @return {TreeGraphData}
*/
findDataById(id: string, parent?: TreeGraphData | undefined): TreeGraphData;
findDataById(id: string, parent?: TreeGraphData | undefined): TreeGraphData | null;
/**
*

View File

@ -1,7 +1,7 @@
import { IGroup } from '@antv/g-base/lib/interfaces';
import { Point } from '@antv/g-base/lib/types';
import Group from "@antv/g-canvas/lib/group";
import { IBBox, IPoint, IShapeBase, Item, ModelConfig, ModelStyle, ShapeStyle } from '../types';
import { IBBox, IPoint, IShapeBase, Item, ModelConfig, ModelStyle, ShapeStyle, Indexable } from '../types';
// item 的配置项
@ -63,16 +63,16 @@ export type IItemBaseConfig = Partial<{
target: string | Item;
linkCenter: boolean;
}>
}> & Indexable<any>
export interface IItemBase {
_cfg: IItemBaseConfig;
_cfg: IItemBaseConfig | null;
destroyed: boolean;
isItem(): boolean;
getKeyShapeStyle(): ShapeStyle;
getKeyShapeStyle(): ShapeStyle | void;
/**
*
@ -101,7 +101,7 @@ export interface IItemBase {
*/
setState(state: string, enable: boolean): void;
clearStates(states: string | string[]): void;
clearStates(states?: string | string[]): void;
/**
*
@ -247,7 +247,7 @@ export interface INode extends IItemBase {
* @param {Object} point
* @return {Object} {x,y}
*/
getLinkPoint(point: IPoint): IPoint;
getLinkPoint(point: IPoint): IPoint | null;
/**
*

View File

@ -5,11 +5,11 @@ import { EdgeConfig, GraphData, IPointTuple, NodeConfig } from '../types';
*/
export interface ILayout<Cfg = any> {
/** 节点 */
nodes: NodeConfig[];
nodes: NodeConfig[] | null;
/** 边 */
edges: EdgeConfig[];
edges: EdgeConfig[] | null;
/** 布局获得的位置 */
positions: IPointTuple[];
positions: IPointTuple[] | null;
/** 是否已销毁 */
destroyed: boolean;

View File

@ -10,6 +10,7 @@ export type ILabelConfig = Partial<{
refX: number;
refY: number;
autoRotate: boolean;
style: LabelStyle;
}>
export type ShapeOptions = Partial<{
@ -33,21 +34,21 @@ export type ShapeOptions = Partial<{
drawShape(cfg?: ModelConfig, group?: GGroup): IShape
drawLabel(cfg: ModelConfig, group: GGroup): IShape
getLabelStyleByPosition(cfg?: ModelConfig, labelCfg?: ILabelConfig, group?: GGroup): LabelStyle
getLabelStyle(cfg: ModelConfig, labelCfg, group: GGroup): LabelStyle
getLabelStyle(cfg: ModelConfig, labelCfg: ILabelConfig, group: GGroup): LabelStyle
getShapeStyle(cfg: ModelConfig): ShapeStyle
getStateStyle(name: string, value: string | boolean, item: Item): ShapeStyle
/**
* 便
*/
afterDraw(cfg?: ModelConfig, group?: GGroup, rst?: IShape)
afterDraw(cfg?: ModelConfig, group?: GGroup, rst?: IShape): void
afterUpdate(cfg?: ModelConfig, item?: Item)
afterUpdate(cfg?: ModelConfig, item?: Item): void
/**
*
*/
setState(name?: string, value?: string | boolean, item?: Item)
setState(name?: string, value?: string | boolean, item?: Item): void
/**
@ -65,7 +66,7 @@ export type ShapeOptions = Partial<{
getAnchorPoints(cfg?: ModelConfig): IPoint[]
// 如果没定义 update 方法,每次都调用 draw 方法
update(cfg: ModelConfig, item: Item)
update(cfg: ModelConfig, item: Item): void
// 获取节点的大小,只对节点起效
getSize: (cfg: ModelConfig) => number | number[]

View File

@ -1,11 +1,11 @@
import isNil from '@antv/util/lib/is-nil';
import isPlainObject from '@antv/util/lib/is-plain-object'
import { IEdge, INode } from "../interface/item";
import { EdgeConfig, IPoint, NodeConfig, SourceTarget } from '../types';
import { EdgeConfig, IPoint, NodeConfig, SourceTarget, Indexable, ModelConfig } from '../types';
import Item from './item';
import Node from './node'
const END_MAP = { source: 'start', target: 'end' };
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';
@ -22,7 +22,7 @@ export default class Edge extends Item implements IEdge {
}
}
private setEnd(name: string, value: INode) {
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);
@ -86,7 +86,7 @@ export default class Edge extends Item implements IEdge {
*
* @param name
*/
private getEndPoint(name: string): NodeConfig | IPoint {
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);
@ -101,7 +101,7 @@ export default class Edge extends Item implements IEdge {
*
* @param model
*/
private getControlPointsByCenter(model) {
private getControlPointsByCenter(model: EdgeConfig) {
const sourcePoint = this.getEndPoint('source');
const targetPoint = this.getEndPoint('target');
const shapeFactory = this.get('shapeFactory');
@ -112,7 +112,7 @@ export default class Edge extends Item implements IEdge {
});
}
private getEndCenter(name: string): IPoint {
private getEndCenter(name: SourceTarget): IPoint {
const itemName = name + ITEM_NAME_SUFFIX;
const pointName = END_MAP[name] + POINT_NAME_SUFFIX;
const item = this.get(itemName);
@ -137,7 +137,7 @@ export default class Edge extends Item implements IEdge {
public getShapeCfg(model: EdgeConfig): EdgeConfig {
const self = this;
const linkCenter: boolean = self.get('linkCenter'); // 如果连接到中心,忽视锚点、忽视控制点
const cfg: any = super.getShapeCfg(model);
const cfg: ModelConfig = super.getShapeCfg(model);
if (linkCenter) {
cfg.startPoint = self.getEndCenter('source');

View File

@ -6,7 +6,7 @@ import isString from '@antv/util/lib/is-string'
import uniqueId from '@antv/util/lib/unique-id'
import { IItemBase, IItemBaseConfig } from "../interface/item";
import Shape from '../shape/shape';
import { IBBox, IPoint, IShapeBase, ModelConfig, ShapeStyle } from '../types';
import { IBBox, IPoint, IShapeBase, ModelConfig, ShapeStyle, Indexable } from '../types';
import { getBBox } from '../util/graphic';
import { translate } from '../util/math';
@ -16,13 +16,15 @@ const RESERVED_STYLES = [ 'fillStyle', 'strokeStyle',
'path', 'points', 'img', 'symbol' ];
export default class ItemBase implements IItemBase {
public _cfg: IItemBaseConfig = {}
public _cfg: IItemBaseConfig & {
[key: string]: unknown
} = {}
private defaultCfg: IItemBaseConfig = {
/**
* id
* @type {string}
*/
id: null,
id: undefined,
/**
*
@ -40,7 +42,7 @@ export default class ItemBase implements IItemBase {
* g group
* @type {G.Group}
*/
group: null,
group: undefined,
/**
* is open animate
@ -68,7 +70,7 @@ export default class ItemBase implements IItemBase {
* key shape to calculate item's bbox
* @type object
*/
keyShape: null,
keyShape: undefined,
/**
* item's states, such as selected or active
* @type Array
@ -81,7 +83,7 @@ export default class ItemBase implements IItemBase {
constructor(cfg: IItemBaseConfig) {
this._cfg = Object.assign(this.defaultCfg, this.getDefaultCfg(), cfg)
const group = cfg.group
group.set('item', this)
if (group) group.set('item', this)
let id = this.get('model').id
@ -90,7 +92,7 @@ export default class ItemBase implements IItemBase {
}
this.set('id', id)
group.set('id', id)
if (group) group.set('id', id)
const stateStyles = this.get('model').stateStyles;
this.set('stateStyles', stateStyles);
@ -131,7 +133,7 @@ export default class ItemBase implements IItemBase {
}
self.updatePosition(model);
const cfg = self.getShapeCfg(model); // 可能会附加额外信息
const shapeType: string = cfg.shape || cfg.type;
const shapeType = (cfg.shape as string )|| (cfg.type as string);
const keyShape: IShapeBase = shapeFactory.draw(shapeType, cfg, group);
if (keyShape) {
@ -141,7 +143,7 @@ export default class ItemBase implements IItemBase {
}
// 防止由于用户外部修改 model 中的 shape 导致 shape 不更新
this.set('currentShape', shapeType);
this.resetStates(shapeFactory, shapeType);
this.resetStates(shapeFactory, shapeType!);
}
/**
@ -149,7 +151,7 @@ export default class ItemBase implements IItemBase {
* @param shapeFactory
* @param shapeType
*/
private resetStates(shapeFactory, shapeType: string) {
private resetStates(shapeFactory: any, shapeType: string) {
const self = this;
const states: string[] = self.get('states');
each(states, state => {
@ -168,8 +170,8 @@ export default class ItemBase implements IItemBase {
* @param {String} key
* @return {object | string | number}
*/
public get(key: string) {
return this._cfg[key]
public get<T = any>(key: string): T {
return this._cfg[key] as T
}
/**
@ -178,7 +180,7 @@ export default class ItemBase implements IItemBase {
* @param {String|Object} key
* @param {object | string | number} val
*/
public set(key: string, val): void {
public set(key: string | object, val?: unknown): void {
if(isPlainObject(key)) {
this._cfg = Object.assign({}, this._cfg, key)
} else {
@ -228,10 +230,10 @@ export default class ItemBase implements IItemBase {
this.afterDraw()
}
public getKeyShapeStyle(): ShapeStyle {
public getKeyShapeStyle(): ShapeStyle | void {
const keyShape = this.getKeyShape();
if (keyShape) {
const styles: ShapeStyle = {};
const styles: ShapeStyle & Indexable<any> = {};
each(keyShape.attr(), (val, key) => {
if (RESERVED_STYLES.indexOf(key) < 0) {
styles[key] = val;
@ -239,6 +241,7 @@ export default class ItemBase implements IItemBase {
});
return styles;
}
return {}
}
public getShapeCfg(model: ModelConfig): ModelConfig {
@ -310,7 +313,7 @@ export default class ItemBase implements IItemBase {
const originStates = self.getStates();
const shapeFactory = self.get('shapeFactory');
const model: ModelConfig = self.get('model')
const shape: string = model.shape || model.type;
const shape = model.shape || model.type;
if (!states) {
self.set('states', []);
shapeFactory.setState(shape, originStates[0], false, self);
@ -416,7 +419,7 @@ export default class ItemBase implements IItemBase {
*/
public update(cfg: ModelConfig) {
const model: ModelConfig = this.get('model');
const originPosition: IPoint = { x: model.x, y: model.y };
const originPosition: IPoint = { x: model.x!, y: model.y! };
// 直接将更新合到原数据模型上,可以保证用户在外部修改源数据然后刷新时的样式符合期待。
Object.assign(model, cfg);
@ -477,7 +480,7 @@ export default class ItemBase implements IItemBase {
}
group.resetMatrix();
// G 4.0 element 中移除了矩阵相关方法详见https://www.yuque.com/antv/blog/kxzk9g#4rMMV
translate(group, { x, y });
translate(group, { x: x!, y: y! });
model.x = x;
model.y = y;
this.clearCache(); // 位置更新后需要清除缓存
@ -566,7 +569,7 @@ export default class ItemBase implements IItemBase {
group.stopAnimate();
}
group.remove();
this._cfg = null;
(this._cfg as IItemBaseConfig | null) = null;
this.destroyed = true;
}
}

View File

@ -65,7 +65,7 @@ export default class Node extends Item implements INode {
*
* @param {Number} index
*/
public getLinkPointByAnchor(index): IPoint {
public getLinkPointByAnchor(index: number): IPoint {
const anchorPoints = this.getAnchorPoints();
return anchorPoints[index];
}
@ -74,25 +74,25 @@ export default class Node extends Item implements INode {
*
* @param point
*/
public getLinkPoint(point: IPoint): IPoint {
public getLinkPoint(point: IPoint): IPoint | null {
const keyShape: IShapeBase = this.get('keyShape');
const type: string = keyShape.get('type');
const bbox = this.getBBox();
const { centerX, centerY } = bbox;
const anchorPoints = this.getAnchorPoints();
let intersectPoint: IPoint;
let intersectPoint: IPoint | null;
switch (type) {
case 'circle':
intersectPoint = getCircleIntersectByPoint({
x: centerX,
y: centerY,
x: centerX!,
y: centerY!,
r: bbox.width / 2
}, point);
break;
case 'ellipse':
intersectPoint = getEllispeIntersectByPoint({
x: centerX,
y: centerY,
x: centerX!,
y: centerY!,
rx: bbox.width / 2,
ry: bbox.height / 2
}, point);
@ -109,7 +109,7 @@ export default class Node extends Item implements INode {
linkPoint = this.getNearestPoint(anchorPoints, linkPoint);
}
if (!linkPoint) { // 如果最终依然没法找到锚点和连接点,直接返回中心点
linkPoint = { x: centerX, y: centerY };
linkPoint = { x: centerX, y: centerY } as IPoint;
}
return linkPoint;
}

View File

@ -3,8 +3,9 @@
* @author shiwu.wyy@antfin.com
*/
import { EdgeConfig, IPointTuple, NodeConfig } from '../types';
import { EdgeConfig, IPointTuple, NodeConfig, NodeIdxMap } from '../types';
import { BaseLayout } from './layout';
import { getDegree } from '../util/math';
type Node = NodeConfig & {
degree: number;
@ -13,34 +14,34 @@ type Node = NodeConfig & {
};
type Edge = EdgeConfig;
function getDegree(n: number, nodeMap: object, edges: Edge[]) {
const degrees = [];
for (let i = 0; i < n; i++) {
degrees[i] = 0;
}
edges.forEach((e) => {
degrees[nodeMap[e.source]] += 1;
degrees[nodeMap[e.target]] += 1;
});
return degrees;
}
function initHierarchy(nodes: Node[], edges: Edge[], nodeMap: object, directed: boolean) {
function initHierarchy(nodes: Node[], edges: Edge[], nodeMap: NodeIdxMap, directed: boolean) {
nodes.forEach((_, i: number) => {
nodes[i].children = [];
nodes[i].parent = [];
});
if (directed) {
edges.forEach((e) => {
const sourceIdx = nodeMap[e.source];
const targetIdx = nodeMap[e.target];
let sourceIdx = 0;
if (e.source) {
sourceIdx = nodeMap[e.source];
}
let targetIdx = 0;
if (e.target) {
targetIdx = nodeMap[e.target];
}
nodes[sourceIdx].children.push(nodes[targetIdx]);
nodes[targetIdx].parent.push(nodes[sourceIdx]);
});
} else {
edges.forEach((e) => {
const sourceIdx = nodeMap[e.source];
const targetIdx = nodeMap[e.target];
let sourceIdx = 0;
if (e.source) {
sourceIdx = nodeMap[e.source];
}
let targetIdx = 0;
if (e.target) {
targetIdx = nodeMap[e.target];
}
nodes[sourceIdx].children.push(nodes[targetIdx]);
nodes[targetIdx].children.push(nodes[sourceIdx]);
});
@ -75,35 +76,35 @@ function compareDegree(a: Node, b: Node) {
*/
export default class CircularLayout extends BaseLayout {
/** 布局中心 */
public center: IPointTuple;
public center: IPointTuple = [0, 0];
/** 固定半径,若设置了 radius则 startRadius 与 endRadius 不起效 */
public radius: number;
public radius: number | null = null;
/** 起始半径 */
public startRadius: number;
public startRadius: number | null = null;
/** 终止半径 */
public endRadius: number;
public endRadius: number | null = null;
/** 起始角度 */
public startAngle: number;
public startAngle: number = 0;
/** 终止角度 */
public endAngle: number;
public endAngle: number = 2 * Math.PI;
/** 是否顺时针 */
public clockwise: boolean;
public clockwise: boolean = true;
/** 节点在环上分成段数(几个段将均匀分布),在 endRadius - startRadius != 0 时生效 */
public divisions: number;
public divisions: number = 1;
/** 节点在环上排序的依据,可选: 'topology', 'degree', 'null' */
public ordering: 'topology' | 'topology-directed' | 'degree' | 'null';
public ordering: 'topology' | 'topology-directed' | 'degree' | null = null;
/** how many 2*pi from first to last nodes */
public angleRatio: 1;
public angleRatio = 1;
public nodes: Node[];
public edges: Edge[];
public nodes: Node[] = [];
public edges: Edge[] = [];
private nodeMap: object;
private degrees;
private astep;
private nodeMap: NodeIdxMap = {};
private degrees: number[] = [];
private astep: number | undefined;
public width: number;
public height: number;
public width: number = 300;
public height: number = 300;
public getDefaultCfg() {
return {
@ -144,7 +145,7 @@ export default class CircularLayout extends BaseLayout {
const endAngle = self.endAngle;
const angleStep = (endAngle - startAngle) / n;
// layout
const nodeMap = {};
const nodeMap: NodeIdxMap = {};
nodes.forEach((node, i) => {
nodeMap[node.id] = i;
});
@ -190,9 +191,12 @@ export default class CircularLayout extends BaseLayout {
const divN = Math.ceil(n / divisions); // node number in each division
for (let i = 0; i < n; ++i) {
let r = radius;
if (!r) {
if (!r && startRadius !== null && endRadius !== null) {
r = startRadius + (i * (endRadius - startRadius)) / (n - 1);
}
if (!r) {
r = 10 + (i * 100) / (n - 1);
}
let angle = startAngle + (i % divN) * astep + ((2 * Math.PI) / divisions) * Math.floor(i / divN);
if (!clockwise) {
angle = endAngle - (i % divN) * astep - ((2 * Math.PI) / divisions) * Math.floor(i / divN);
@ -213,7 +217,7 @@ export default class CircularLayout extends BaseLayout {
const nodes = self.nodes;
const nodeMap = self.nodeMap;
const orderedNodes = [nodes[0]];
const pickFlags = [];
const pickFlags: boolean[] = [];
const n = nodes.length;
pickFlags[0] = true;
initHierarchy(nodes, edges, nodeMap, directed);
@ -260,10 +264,10 @@ export default class CircularLayout extends BaseLayout {
*
* @return {array} orderedNodes
*/
public degreeOrdering() {
public degreeOrdering(): Node[] {
const self = this;
const nodes = self.nodes;
const orderedNodes = [];
const orderedNodes: Node[] = [];
const degrees = self.degrees;
nodes.forEach((node, i) => {
node.degree = degrees[i];

View File

@ -4,28 +4,21 @@
* this algorithm refers to <cytoscape.js> - https://github.com/cytoscape/cytoscape.js/
*/
import { EdgeConfig, IPointTuple, NodeConfig } from '../types';
import { EdgeConfig, IPointTuple, NodeConfig, NodeIdxMap } from '../types';
import isArray from '@antv/util/lib/is-array';
import isString from '@antv/util/lib/is-string';
import { BaseLayout } from './layout';
import { getDegree } from '../util/math';
import { isNumber } from '@antv/util';
type Node = NodeConfig & {
degree: number;
size: number | number[];
[key: string]: number;
};
type Edge = EdgeConfig;
function getDegree(n: number, nodeIdxMap: object, edges: Edge[]) {
const degrees = [];
for (let i = 0; i < n; i++) {
degrees[i] = 0;
}
edges.forEach((e) => {
degrees[nodeIdxMap[e.source]] += 1;
degrees[nodeIdxMap[e.target]] += 1;
});
return degrees;
type NodeMap = {
[key: string]: Node;
}
/**
@ -33,33 +26,33 @@ function getDegree(n: number, nodeIdxMap: object, edges: Edge[]) {
*/
export default class ConcentricLayout extends BaseLayout {
/** 布局中心 */
public center: IPointTuple;
public nodeSize: number;
public center: IPointTuple = [0, 0];
public nodeSize: number | IPointTuple = 30;
/** min spacing between outside of nodes (used for radius adjustment) */
public minNodeSpacing: number;
public minNodeSpacing: number = 10;
/** prevents node overlap, may overflow boundingBox if not enough space */
public preventOverlap: boolean;
public preventOverlap: boolean = false;
/** how many radians should be between the first and last node (defaults to full circle) */
public sweep: undefined;
public sweep: number | undefined;
/** whether levels have an equal radial distance betwen them, may cause bounding box overflow */
public equidistant: boolean;
public equidistant: boolean = false;
/** where nodes start in radians */
public startAngle: number;
public startAngle: number = (3 / 2) * Math.PI;
/** whether the layout should go clockwise (true) or counterclockwise/anticlockwise (false) */
public clockwise: boolean;
public clockwise: boolean = true;
/** the letiation of concentric values in each level */
public maxLevelDiff: undefined | number;
/** 根据 sortBy 指定的属性进行排布,数值高的放在中心,如果是 sortBy 则会计算节点度数,度数最高的放在中心 */
public sortBy: string;
public sortBy: string = 'degree';
public nodes: Node[];
public edges: Edge[];
public nodes: Node[] = [];
public edges: Edge[] = [];
public width: number;
public height: number;
public width: number = 300;
public height: number = 300;
private maxValueNode: number;
private counterclockwise: boolean;
private maxValueNode: Node | undefined;
private counterclockwise: boolean | undefined;
public getDefaultCfg() {
return {
@ -92,19 +85,19 @@ export default class ConcentricLayout extends BaseLayout {
return;
}
const layoutNodes = [];
const layoutNodes: Node[] = [];
let maxNodeSize: number;
if (isNaN(self.nodeSize)) {
if (isArray(self.nodeSize)) {
maxNodeSize = Math.max(self.nodeSize[0], self.nodeSize[1]);
} else {
maxNodeSize = self.nodeSize;
}
nodes.forEach((node) => {
layoutNodes.push(node);
let nodeSize: number;
let nodeSize: number = maxNodeSize;
if (isArray(node.size)) {
nodeSize = Math.max(node.size[0], node.size[1]);
} else {
} else if (isNumber(node.size)){
nodeSize = node.size;
}
maxNodeSize = Math.max(maxNodeSize, nodeSize);
@ -119,8 +112,8 @@ export default class ConcentricLayout extends BaseLayout {
self.clockwise = self.counterclockwise !== undefined ? !self.counterclockwise : self.clockwise;
// layout
const nodeMap = {};
const nodeIdxMap = {};
const nodeMap: NodeMap = {};
const nodeIdxMap: NodeIdxMap = {};
layoutNodes.forEach((node, i) => {
nodeMap[node.id] = node;
nodeIdxMap[node.id] = i;
@ -129,7 +122,7 @@ export default class ConcentricLayout extends BaseLayout {
// get the node degrees
if (self.sortBy === 'degree' || !isString(self.sortBy) || layoutNodes[0][self.sortBy] === undefined) {
self.sortBy = 'degree';
if (isNaN(nodes[0].degree)) {
if (!isNumber(nodes[0].degree)) {
const values = getDegree(nodes.length, nodeIdxMap, edges);
layoutNodes.forEach((node, i) => {
node.degree = values[i];
@ -137,13 +130,13 @@ export default class ConcentricLayout extends BaseLayout {
}
}
// sort nodes by value
layoutNodes.sort((n1, n2) => {
layoutNodes.sort((n1: Node, n2: Node) => {
return n2[self.sortBy] - n1[self.sortBy];
});
self.maxValueNode = layoutNodes[0];
self.maxLevelDiff = self.maxLevelDiff || self.maxValueNode[self.sortBy] / 4; // 0.5;
self.maxLevelDiff = self.maxLevelDiff || self.maxValueNode[self.sortBy] / 4;
// put the values into levels
const levels: any[] = [[]];
@ -151,7 +144,7 @@ export default class ConcentricLayout extends BaseLayout {
layoutNodes.forEach((node) => {
if (currentLevel.length > 0) {
const diff = Math.abs(currentLevel[0][self.sortBy] - node[self.sortBy]);
if (diff >= self.maxLevelDiff) {
if (self.maxLevelDiff && diff >= self.maxLevelDiff) {
currentLevel = [];
levels.push(currentLevel);
}
@ -173,7 +166,10 @@ export default class ConcentricLayout extends BaseLayout {
// find the metrics for each level
let r = 0;
levels.forEach((level) => {
const sweep = self.sweep === undefined ? 2 * Math.PI - (2 * Math.PI) / level.length : self.sweep;
let sweep = self.sweep;
if (sweep === undefined) {
sweep = 2 * Math.PI - (2 * Math.PI) / level.length;
}
const dTheta = (level.dTheta = sweep / Math.max(1, level.length - 1));
// calculate the radius

View File

@ -14,21 +14,21 @@ import { isNumber } from '@antv/util';
*/
export default class DagreLayout extends BaseLayout {
/** layout 方向, 可选 TB, BT, LR, RL */
public rankdir: 'TB' | 'BT' | 'LR' | 'RL';
public rankdir: 'TB' | 'BT' | 'LR' | 'RL' = 'TB';
/** 节点对齐方式,可选 UL, UR, DL, DR */
public align: undefined | 'UL' | 'UR' | 'DL' | 'DR';
/** 节点大小 */
public nodeSize: number | number[];
public nodeSize: number | number[] | undefined;
/** 节点水平间距(px) */
public nodesepFunc: () => number;
public nodesepFunc: ((d?: any) => number) | undefined;
/** 每一层节点之间间距 */
public ranksepFunc: () => number;
public ranksepFunc: ((d?: any) => number) | undefined;
/** 节点水平间距(px) */
public nodesep: number;
public nodesep: number = 50;
/** 每一层节点之间间距 */
public ranksep: number;
public ranksep: number = 50;
/** 是否保留布局连线的控制点 */
public controlPoints: boolean;
public controlPoints: boolean = true;
public getDefaultCfg() {
return {
@ -49,12 +49,13 @@ export default class DagreLayout extends BaseLayout {
public execute() {
const self = this;
const nodes = self.nodes;
const edges = self.edges;
if (!nodes) return;
const edges = self.edges || [];
const g = new dagre.graphlib.Graph();
const nodeSize = self.nodeSize;
let nodeSizeFunc;
let nodeSizeFunc: ((d?: any) => number[]);
if (!nodeSize) {
nodeSizeFunc = (d) => {
nodeSizeFunc = (d: any) => {
if (d.size) {
if (isArray(d.size)) {
return d.size;
@ -95,13 +96,13 @@ export default class DagreLayout extends BaseLayout {
});
dagre.layout(g);
let coord;
g.nodes().forEach(node => {
g.nodes().forEach((node: any) => {
coord = g.node(node);
const i = nodes.findIndex(it => it.id === node);
nodes[i].x = coord.x;
nodes[i].y = coord.y;
});
g.edges().forEach(edge => {
g.edges().forEach((edge: any) => {
coord = g.edge(edge);
const i = edges.findIndex(it => it.source === edge.v && it.target === edge.w);
edges[i].startPoint = coord.points[0];
@ -113,7 +114,7 @@ export default class DagreLayout extends BaseLayout {
}
}
function getFunc(func: Function, value: number, defaultValue: number ): Function {
function getFunc(func: ((d?: any) => number) | undefined, value: number, defaultValue: number ): Function {
let resultFunc;
if (func) {
resultFunc = func;

View File

@ -18,42 +18,42 @@ import { LAYOUT_MESSAGE } from './worker/layoutConst';
/**
* force-directed
*/
export default class ForceLayout extends BaseLayout {
export default class ForceLayout<Cfg = any> extends BaseLayout {
/** 向心力作用点 */
public center: IPointTuple;
public center: IPointTuple = [0, 0];
/** 节点作用力 */
public nodeStrength: any;
public nodeStrength: number | null = null;
/** 边的作用力, 默认为根据节点的入度出度自适应 */
public edgeStrength: any;
public edgeStrength: number | null = null;
/** 是否防止节点相互覆盖 */
public preventOverlap: boolean;
public preventOverlap: boolean = false;
/** 节点大小 / 直径,用于防止重叠时的碰撞检测 */
public nodeSize: number | number[] | ((d?: unknown) => number);
public nodeSize: number | number[] | ((d?: unknown) => number) | undefined;
/** 节点间距,防止节点重叠时节点之间的最小距离(两节点边缘最短距离) */
public nodeSpacing: number;
public nodeSpacing: ((d?: unknown) => number) | undefined;
/** 默认边长度 */
public linkDistance: number;
public linkDistance: number = 50;
/** 自定义 force 方法 */
public forceSimulation: any;
/** 迭代阈值的衰减率 [0, 1]0.028 对应最大迭代数为 300 */
public alphaDecay: number;
public alphaDecay: number = 0.028;
/** 停止迭代的阈值 */
public alphaMin: number;
public alphaMin: number = 0.001;
/** 当前阈值 */
public alpha: number;
public alpha: number = 0.3;
/** 防止重叠的力强度 */
public collideStrength: number;
public collideStrength: number = 1;
/** 是否启用web worker。前提是在web worker里执行布局否则无效 */
public workerEnabled: boolean;
public workerEnabled: boolean = false;
public tick: () => void;
public tick: () => void = () => {};
public onLayoutEnd: () => void;
public onLayoutEnd: () => void = () => {};
/** 布局完成回调 */
public onTick: () => void;
public onTick: () => void = () => {};
/** 是否正在布局 */
private ticking: boolean;
private ticking: boolean | undefined = undefined;
public getDefaultCfg() {
return {
@ -83,8 +83,8 @@ export default class ForceLayout extends BaseLayout {
*/
public init(data: GraphData) {
const self = this;
self.nodes = data.nodes;
self.edges = data.edges;
self.nodes = data.nodes || [];
self.edges = data.edges || [];
self.ticking = false;
}
@ -133,7 +133,7 @@ export default class ForceLayout extends BaseLayout {
});
const edgeForce = d3Force
.forceLink()
.id((d) => d.id)
.id((d: any) => d.id)
.links(d3Edges);
if (self.edgeStrength) {
edgeForce.strength(self.edgeStrength);
@ -164,7 +164,7 @@ export default class ForceLayout extends BaseLayout {
for (let currentTick = 1; currentTick <= totalTicks; currentTick++) {
simulation.tick();
// currentTick starts from 1.
postMessage({ type: LAYOUT_MESSAGE.TICK, currentTick, totalTicks, nodes }, undefined);
postMessage({ type: LAYOUT_MESSAGE.TICK, currentTick, totalTicks, nodes }, undefined as any);
}
self.ticking = false;
}
@ -188,7 +188,7 @@ export default class ForceLayout extends BaseLayout {
*
* @param {object} simulation
*/
public overlapProcess(simulation) {
public overlapProcess(simulation: any) {
const self = this;
const nodeSize = self.nodeSize;
const nodeSpacing = self.nodeSpacing;
@ -209,7 +209,7 @@ export default class ForceLayout extends BaseLayout {
}
if (!nodeSize) {
nodeSizeFunc = (d) => {
nodeSizeFunc = d => {
if (d.size) {
if (isArray(d.size)) {
const res = d.size[0] > d.size[1] ? d.size[0] : d.size[1];
@ -230,11 +230,15 @@ export default class ForceLayout extends BaseLayout {
nodeSizeFunc = (d) => {
return radius + nodeSpacingFunc(d);
};
} else if (!isNaN(nodeSize)) {
} else if (isNumber(nodeSize)) {
const radius = nodeSize / 2;
nodeSizeFunc = (d) => {
return radius + nodeSpacingFunc(d);
};
} else {
nodeSizeFunc = () => {
return 10;
};
}
// forceCollide's parameter is a radius
@ -245,14 +249,14 @@ export default class ForceLayout extends BaseLayout {
*
* @param {object} cfg
*/
public updateCfg(cfg) {
public updateCfg(cfg: Partial<Cfg>) {
const self = this;
if (self.ticking) {
self.forceSimulation.stop();
self.ticking = false;
}
self.forceSimulation = null;
mix(self, cfg);
mix(self as any, cfg);
}
public destroy() {
@ -268,7 +272,7 @@ export default class ForceLayout extends BaseLayout {
}
// Return total ticks of d3-force simulation
function getSimulationTicks(simulation): number {
function getSimulationTicks(simulation: any): number {
const alphaMin = simulation.alphaMin();
const alphaTarget = simulation.alphaTarget();
const alpha = simulation.alpha();
@ -276,7 +280,7 @@ function getSimulationTicks(simulation): number {
const totalTicks = Math.ceil(totalTicksFloat);
return totalTicks;
}
declare const WorkerGlobalScope;
declare const WorkerGlobalScope: any;
// 判断是否运行在web worker里
function isInWorker(): boolean {

View File

@ -3,16 +3,20 @@
* @author shiwu.wyy@antfin.com
*/
import { EdgeConfig, IPointTuple, NodeConfig } from '../types';
import { EdgeConfig, IPointTuple, NodeConfig, NodeIdxMap } from '../types';
import { BaseLayout } from './layout';
import { isNumber } from '@antv/util';
import { Point } from '@antv/g-base';
type Node = NodeConfig & {
cluster: string;
cluster: string | number;
};
type Edge = EdgeConfig;
type NodeMap = Map<string, Node>;
type NodeIndexMap = Map<string, string>;
type NodeMap = {
[key: string]: Node;
}
const SPEED_DIVISOR = 800;
@ -21,26 +25,26 @@ const SPEED_DIVISOR = 800;
*/
export default class FruchtermanLayout extends BaseLayout {
/** 布局中心 */
public center: IPointTuple;
public center: IPointTuple = [0, 0];
/** 停止迭代的最大迭代数 */
public maxIteration: number;
public maxIteration: number = 1000;
/** 重力大小,影响图的紧凑程度 */
public gravity: number;
public gravity: number = 10;
/** 速度 */
public speed: number;
public speed: number = 1;
/** 是否产生聚类力 */
public clustering: boolean;
public clustering: boolean = false;
/** 聚类力大小 */
public clusterGravity: number;
public clusterGravity: number = 10;
public width: number;
public height: number;
public nodes: Node[] = [];
public edges: Edge[] = [];
public nodes: Node[];
public edges: Edge[];
public width: number = 300;
public height: number = 300;
public nodeMap: object;
public nodeIndexMap: object;
public nodeMap: NodeMap = {};
public nodeIdxMap: NodeIdxMap = {};
public getDefaultCfg() {
return {
@ -60,21 +64,21 @@ export default class FruchtermanLayout extends BaseLayout {
const nodes = self.nodes;
const center = self.center;
if (nodes.length === 0) {
if (!nodes || nodes.length === 0) {
return;
} else if (nodes.length === 1) {
nodes[0].x = center[0];
nodes[0].y = center[1];
return;
}
const nodeMap = {};
const nodeIndexMap = {};
const nodeMap: NodeMap = {};
const nodeIdxMap: NodeIdxMap = {};
nodes.forEach((node, i) => {
nodeMap[node.id] = node;
nodeIndexMap[node.id] = i;
nodeIdxMap[node.id] = i;
});
self.nodeMap = nodeMap;
self.nodeIndexMap = nodeIndexMap;
self.nodeIdxMap = nodeIdxMap;
// layout
self.run();
}
@ -82,6 +86,7 @@ export default class FruchtermanLayout extends BaseLayout {
public run() {
const self = this;
const nodes = self.nodes;
if (!nodes) return;
const edges = self.edges;
const maxIteration = self.maxIteration;
if (!self.width && typeof window !== 'undefined') {
@ -92,15 +97,20 @@ export default class FruchtermanLayout extends BaseLayout {
}
const center = self.center;
const nodeMap = self.nodeMap;
const nodeIndexMap = self.nodeIndexMap;
const nodeIdxMap = self.nodeIdxMap;
const maxDisplace = self.width / 10;
const k = Math.sqrt((self.width * self.height) / (nodes.length + 1));
const gravity = self.gravity;
const speed = self.speed;
const clustering = self.clustering;
const clusterMap = {}
const clusterMap: {[key: string]: {
name: string | number,
cx: number,
cy: number,
count: number
}} = {}
if (clustering) {
nodes.forEach((n) => {
nodes.forEach(n => {
if (clusterMap[n.cluster] === undefined) {
const cluster = {
name: n.cluster,
@ -111,8 +121,12 @@ export default class FruchtermanLayout extends BaseLayout {
clusterMap[n.cluster] = cluster;
}
const c = clusterMap[n.cluster];
c.cx += n.x;
c.cy += n.y;
if (isNumber(n.x)) {
c.cx += n.x;
}
if (isNumber(n.y)) {
c.cy += n.y;
}
c.count++;
});
for (let key in clusterMap) {
@ -121,16 +135,17 @@ export default class FruchtermanLayout extends BaseLayout {
}
}
for (let i = 0; i < maxIteration; i++) {
const disp = [];
const disp: Point[] = [];
nodes.forEach((_, j) => {
disp[j] = { x: 0, y: 0 };
});
self.getDisp(nodes, edges, nodeMap, nodeIndexMap, disp, k);
self.getDisp(nodes, edges, nodeMap, nodeIdxMap, disp, k);
// gravity for clusters
if (clustering) {
const clusterGravity = self.clusterGravity || gravity;
nodes.forEach((n, j) => {
if (!isNumber(n.x) || !isNumber(n.y)) return;
const c = clusterMap[n.cluster];
const distLength = Math.sqrt((n.x - c.cx) * (n.x - c.cx) + (n.y - c.cy) * (n.y - c.cy));
const gravityForce = k * clusterGravity;
@ -147,8 +162,12 @@ export default class FruchtermanLayout extends BaseLayout {
nodes.forEach((n) => {
const c = clusterMap[n.cluster];
c.cx += n.x;
c.cy += n.y;
if (isNumber(n.x)) {
c.cx += n.x;
}
if (isNumber(n.y)) {
c.cy += n.y;
}
c.count++;
});
for (let key in clusterMap) {
@ -159,18 +178,15 @@ export default class FruchtermanLayout extends BaseLayout {
// gravity
nodes.forEach((n, j) => {
if (!isNumber(n.x) || !isNumber(n.y)) return;
const gravityForce = 0.01 * k * gravity;
disp[j].x -= gravityForce * (n.x - center[0]);
disp[j].y -= gravityForce * (n.y - center[1]);
});
// speed
nodes.forEach((_, j) => {
disp[j].dx *= speed / SPEED_DIVISOR;
disp[j].dy *= speed / SPEED_DIVISOR;
});
// move
nodes.forEach((n, j) => {
if (!isNumber(n.x) || !isNumber(n.y)) return;
const distLength = Math.sqrt(disp[j].x * disp[j].x + disp[j].y * disp[j].y);
if (distLength > 0) {
// && !n.isFixed()
@ -183,19 +199,20 @@ export default class FruchtermanLayout extends BaseLayout {
}
// TODO: nodeMap、nodeIndexMap 等根本不需要依靠参数传递
private getDisp(nodes: Node[], edges: Edge[], nodeMap: object, nodeIndexMap: object, disp, k) {
private getDisp(nodes: Node[], edges: Edge[], nodeMap: NodeMap, nodeIdxMap: NodeIdxMap, disp: Point[], k: number) {
const self = this;
self.calRepulsive(nodes, disp, k);
self.calAttractive(edges, nodeMap, nodeIndexMap, disp, k);
self.calAttractive(edges, nodeMap, nodeIdxMap, disp, k);
}
private calRepulsive(nodes: Node[], disp, k) {
private calRepulsive(nodes: Node[], disp: Point[], k: number) {
nodes.forEach((v, i) => {
disp[i] = { x: 0, y: 0 };
nodes.forEach((u, j) => {
if (i === j) {
return;
}
if (!isNumber(v.x) || !isNumber(u.x) || !isNumber(v.y) || !isNumber(u.y)) return;
let vecx = v.x - u.x;
let vecy = v.y - u.y;
let vecLengthSqr = vecx * vecx + vecy * vecy;
@ -212,15 +229,17 @@ export default class FruchtermanLayout extends BaseLayout {
});
}
private calAttractive(edges: Edge[], nodeMap: object, nodeIndexMap: object, disp, k) {
private calAttractive(edges: Edge[], nodeMap: NodeMap, nodeIdxMap: NodeIdxMap, disp: Point[], k: number) {
edges.forEach((e) => {
const uIndex = nodeIndexMap[e.source];
const vIndex = nodeIndexMap[e.target];
if (!e.source || !e.target) return;
const uIndex = nodeIdxMap[e.source];
const vIndex = nodeIdxMap[e.target];
if (uIndex === vIndex) {
return;
}
const u = nodeMap[e.source];
const v = nodeMap[e.target];
if (!isNumber(v.x) || !isNumber(u.x) || !isNumber(v.y) || !isNumber(u.y)) return;
const vecx = v.x - u.x;
const vecy = v.y - u.y;
const vecLength = Math.sqrt(vecx * vecx + vecy * vecy);

View File

@ -4,76 +4,64 @@
* this algorithm refers to <cytoscape.js> - https://github.com/cytoscape/cytoscape.js/
*/
import { EdgeConfig, IPointTuple, NodeConfig } from '../types';
import { EdgeConfig, IPointTuple, NodeConfig, NodeIdxMap } from '../types';
import isString from '@antv/util/lib/is-string';
import { BaseLayout } from './layout';
import { isArray, isNumber } from '@antv/util';
import { getDegree } from '../util/math';
type Node = NodeConfig & {
degree: number;
size: number;
};
type Edge = EdgeConfig;
function getDegree(n: number, nodeIdxMap: object, edges: Edge[]) {
const degrees = [];
for (let i = 0; i < n; i++) {
degrees[i] = 0;
}
edges.forEach((e) => {
degrees[nodeIdxMap[e.source]] += 1;
degrees[nodeIdxMap[e.target]] += 1;
});
return degrees;
}
/**
*
*/
export default class GridLayout extends BaseLayout {
/** 布局起始点 */
public begin: IPointTuple;
public begin: IPointTuple = [0, 0];
/** prevents node overlap, may overflow boundingBox if not enough space */
public preventOverlap: boolean;
public preventOverlap: boolean = true;
/** extra spacing around nodes when preventOverlap: true */
public preventOverlapPadding: 10;
public preventOverlapPadding: number = 10;
/** uses all available space on false, uses minimal space on true */
public condense: boolean;
public condense: boolean = false;
/** force num of rows in the grid */
public rows: number;
public rows: number | undefined;
/** force num of columns in the grid */
public cols: number;
public cols: number | undefined;
/** returns { row, col } for element */
public position: (node: Node) => { row: number; col: number };
public position: ((node: Node) => { row: number; col: number }) | undefined;
/** a sorting function to order the nodes; e.g. function(a, b){ return a.datapublic ('weight') - b.data('weight') } */
public sortBy: string;
public nodeSize: number | number[];
public sortBy: string = 'degree';
public nodeSize: number | number[] = 30;
public nodes: Node[];
public edges: Edge[];
public nodes: Node[] = [];
public edges: Edge[] = [];
/** 布局中心 */
public center: IPointTuple;
public width: number;
public height: number;
public center: IPointTuple = [0, 0];
public width: number = 300;
public height: number = 300;
private cells: number;
private row: number;
private col: number;
private splits: number;
private columns: number;
private cellWidth: number;
private cellHeight: number;
private cells: number | undefined;
private row: number = 0;
private col: number = 0;
private splits: number | undefined;
private columns: number | undefined;
private cellWidth: number = 0;
private cellHeight: number = 0;
private cellUsed: {
[key: string]: boolean;
};
} = {};
private id2manPos: {
[key: string]: {
row: number;
col: number;
};
};
} = {};
public getDefaultCfg() {
return {
@ -83,7 +71,7 @@ export default class GridLayout extends BaseLayout {
condense: false,
rows: undefined,
cols: undefined,
position() {},
position: undefined,
sortBy: 'degree',
nodeSize: 30
};
@ -109,7 +97,7 @@ export default class GridLayout extends BaseLayout {
nodes.forEach((node) => {
layoutNodes.push(node);
});
const nodeIdxMap = {};
const nodeIdxMap: NodeIdxMap = {};
layoutNodes.forEach((node, i) => {
nodeIdxMap[node.id] = i;
});
@ -156,8 +144,8 @@ export default class GridLayout extends BaseLayout {
if (self.cols * self.rows > self.cells) {
// otherwise use the automatic values and adjust accordingly
// if rounding was up, see if we can reduce rows or columns
const sm = self.small();
const lg = self.large();
const sm = self.small() as number;
const lg = self.large() as number;
// reducing the small side takes away the most cells, so try it first
if ((sm - 1) * lg >= self.cells) {
@ -168,8 +156,8 @@ export default class GridLayout extends BaseLayout {
} else {
// if rounding was too low, add rows or columns
while (self.cols * self.rows < self.cells) {
const sm = self.small();
const lg = self.large();
const sm = self.small() as number;
const lg = self.large() as number;
// try to add to larger side first (adds less in multiplication)
if ((lg + 1) * sm >= self.cells) {
@ -196,16 +184,16 @@ export default class GridLayout extends BaseLayout {
node.y = 0;
}
let nodew: number;
let nodeh: number;
let nodew: number | undefined;
let nodeh: number | undefined;
if (isArray(node.size)) {
nodew = node.size[0];
nodeh = node.size[1];
} else {
} else if(isNumber(node.size)){
nodew = node.size;
nodeh = node.size;
}
if (isNaN(nodew) || isNaN(nodeh)) {
if (nodew === undefined || nodeh === undefined) {
if (isArray(self.nodeSize)) {
nodew = self.nodeSize[0];
nodeh = self.nodeSize[1];
@ -238,7 +226,10 @@ export default class GridLayout extends BaseLayout {
self.id2manPos = {};
for (let i = 0; i < layoutNodes.length; i++) {
const node = layoutNodes[i];
const rcPos = self.position(node);
let rcPos;
if (self.position) {
rcPos = self.position(node);
}
if (rcPos && (rcPos.row !== undefined || rcPos.col !== undefined)) {
// must have at least row or col def'd
@ -269,13 +260,15 @@ export default class GridLayout extends BaseLayout {
self.getPos(node);
}
}
private small(val?: number) {
private small(val?: number): number | undefined {
const self = this;
let res: number;
let res: number | undefined;
const rows = self.rows || 5;
const cols = self.cols || 5;
if (val == null) {
res = Math.min(self.rows, self.cols);
res = Math.min(rows, cols);
} else {
const min = Math.min(self.rows, self.cols);
const min = Math.min(rows, cols);
if (min === self.rows) {
self.rows = val;
} else {
@ -285,13 +278,15 @@ export default class GridLayout extends BaseLayout {
return res;
}
private large(val?: number) {
private large(val?: number): number | undefined {
const self = this;
let res: number;
let res: number | undefined;
const rows = self.rows || 5;
const cols = self.cols || 5;
if (val == null) {
res = Math.max(self.rows, self.cols);
res = Math.max(rows, cols);
} else {
const max = Math.max(self.rows, self.cols);
const max = Math.max(rows, cols);
if (max === self.rows) {
self.rows = val;
} else {
@ -313,8 +308,9 @@ export default class GridLayout extends BaseLayout {
private moveToNextCell() {
const self = this;
const cols = self.cols || 5;
self.col++;
if (self.col >= self.cols) {
if (self.col >= cols) {
self.col = 0;
self.row++;
}

View File

@ -17,15 +17,15 @@ type LayoutConstructor<Cfg = any> = new () => BaseLayout<Cfg>;
*
*/
export class BaseLayout<Cfg = any> implements ILayout<Cfg> {
public nodes: NodeConfig[];
public edges: EdgeConfig[];
public positions: IPointTuple[];
public destroyed: boolean;
public nodes: NodeConfig[] | null = [];
public edges: EdgeConfig[] | null = [];
public positions: IPointTuple[] | null = [];
public destroyed: boolean = false;
public init(data: GraphData) {
const self = this;
self.nodes = data.nodes;
self.edges = data.edges;
self.nodes = data.nodes || [];
self.edges = data.edges || [];
}
public execute() {}
@ -63,7 +63,7 @@ const Layout: {
* @param {string} type
* @param {object} layout
*/
registerLayout<Cfg>(type, layout, layoutCons = BaseLayout) {
registerLayout<Cfg>(type: string, layout: LayoutOption<Cfg>, layoutCons = BaseLayout) {
if (!layout) {
throw new Error('please specify handler for this layout:' + type);
}
@ -72,11 +72,11 @@ const Layout: {
class GLayout extends layoutCons {
constructor(cfg: Cfg) {
super();
const self = this;
const props = {};
const self = this as any;
const props: object = {};
const defaultCfg = self.getDefaultCfg();
mix(props, defaultCfg, layout, cfg);
each(props, (value, key) => {
mix(props, defaultCfg, layout, cfg as unknown);
each(props, (value, key: string) => {
self[key] = value;
});
}

View File

@ -3,7 +3,7 @@
* @author shiwu.wyy@antfin.com
*/
import { IPointTuple } from '../types';
import { IPointTuple, Matrix } from '../types';
import Numeric from 'numericjs';
import { floydWarshall, getAdjMatrix, scaleMatrix } from '../util/math';
@ -14,11 +14,11 @@ import { BaseLayout } from './layout';
*/
export default class MDSLayout extends BaseLayout {
/** 布局中心 */
public center: IPointTuple;
public center: IPointTuple = [0, 0];
/** 边长度 */
public linkDistance: number;
public linkDistance: number = 50;
private scaledDistances;
private scaledDistances: Matrix[] | null = null;
public getDefaultCfg() {
return {
@ -32,9 +32,9 @@ export default class MDSLayout extends BaseLayout {
public execute() {
const self = this;
const nodes = self.nodes;
const edges = self.edges;
const edges = self.edges || [];
const center = self.center;
if (nodes.length === 0) {
if (!nodes || nodes.length === 0) {
return;
} else if (nodes.length === 1) {
nodes[0].x = center[0];
@ -54,7 +54,7 @@ export default class MDSLayout extends BaseLayout {
// get positions by MDS
const positions = self.runMDS();
self.positions = positions;
positions.forEach((p, i) => {
positions.forEach((p: number[], i: number) => {
nodes[i].x = p[0] + center[0];
nodes[i].y = p[1] + center[1];
});
@ -70,7 +70,7 @@ export default class MDSLayout extends BaseLayout {
// square distances
const M = Numeric.mul(-0.5, Numeric.pow(distances, 2));
// double centre the rows/columns
function mean(A) {
function mean(A: any) {
return Numeric.div(Numeric.add.apply(null, A), A.length);
}
const rowMeans = mean(M);
@ -85,12 +85,12 @@ export default class MDSLayout extends BaseLayout {
// points from it
const ret = Numeric.svd(M);
const eigenValues = Numeric.sqrt(ret.S);
return ret.U.map(function(row) {
return ret.U.map(function(row: any) {
return Numeric.mul(row, eigenValues).splice(0, dimension);
});
}
public handleInfinity(distances: number[][]) {
public handleInfinity(distances: Matrix[]) {
let maxDistance = -999999;
distances.forEach((row) => {
row.forEach((value) => {

View File

@ -1,20 +1,25 @@
import Numeric from 'numericjs';
import { IPointTuple, Matrix } from '../../types';
export default class MDS {
/** distance matrix */
public distances: number[][];
public distances: Matrix[];
/** dimensions */
public dimension: number;
/** link distance */
public linkDistance: number;
constructor(params) {
constructor(params: {
distances: Matrix[],
dimension?: number,
linkDistance: number
}) {
this.distances = params.distances;
this.dimension = params.dimension || 2;
this.linkDistance = params.linkDistance;
}
public layout() {
public layout(): IPointTuple[] {
const self = this;
const dimension = self.dimension;
const distances = self.distances;
@ -24,7 +29,7 @@ export default class MDS {
const M = Numeric.mul(-0.5, Numeric.pow(distances, 2));
// double centre the rows/columns
function mean(A) {
function mean(A: any) {
return Numeric.div(Numeric.add.apply(null, A), A.length);
}
const rowMeans = mean(M);
@ -40,7 +45,7 @@ export default class MDS {
// take the SVD of the double centred matrix, and return the
// points from it
let ret;
let res = [];
let res: IPointTuple[] = [];
try {
ret = Numeric.svd(M);
} catch (e) {
@ -53,7 +58,7 @@ export default class MDS {
}
if (res.length === 0) {
const eigenValues = Numeric.sqrt(ret.S);
res = ret.U.map(function(row) {
res = ret.U.map(function(row: any) {
return Numeric.mul(row, eigenValues).splice(0, dimension);
});
}

View File

@ -3,7 +3,7 @@
* @author shiwu.wyy@antfin.com
*/
import { IPointTuple, NodeConfig } from '../../types';
import { IPointTuple, NodeConfig, Matrix } from '../../types';
import isArray from '@antv/util/lib/is-array';
import isNumber from '@antv/util/lib/is-number';
@ -14,11 +14,11 @@ import { floydWarshall, getAdjMatrix } from '../../util/math';
import { BaseLayout } from '../layout';
import MDS from './mds';
import RadialNonoverlapForce from './radialNonoverlapForce';
import RadialNonoverlapForce, { RadialNonoverlapForceParam } from './radialNonoverlapForce';
type Node = NodeConfig;
function getWeightMatrix(M: number[][]) {
function getWeightMatrix(M: Matrix[]) {
const rows = M.length;
const cols = M[0].length;
const result = [];
@ -56,37 +56,37 @@ function getEDistance(p1: IPointTuple, p2: IPointTuple) {
*/
export default class RadialLayout extends BaseLayout {
/** 布局中心 */
public center: IPointTuple;
public center: IPointTuple = [0, 0];
/** 停止迭代的最大迭代数 */
public maxIteration: number;
public maxIteration: number = 1000;
/** 中心点,默认为数据中第一个点 */
public focusNode: String | Node;
public focusNode: String | Node | null = null;
/** 每一圈半径 */
public unitRadius: number;
public unitRadius: number | null = null;
/** 默认边长度 */
public linkDistance: number;
public linkDistance: number = 50;
/** 是否防止重叠 */
public preventOverlap: boolean;
public preventOverlap: boolean = false;
/** 节点直径 */
public nodeSize: number;
public nodeSize: number | undefined;
/** 节点间距,防止节点重叠时节点之间的最小距离(两节点边缘最短距离) */
public nodeSpacing: number;
public nodeSpacing: number | Function | undefined;
/** 是否必须是严格的 radial 布局即每一层的节点严格布局在一个环上。preventOverlap 为 true 时生效 */
public strictRadial: boolean;
public strictRadial: boolean = true;
/** 防止重叠步骤的最大迭代次数 */
public maxPreventOverlapIteration: number;
public maxPreventOverlapIteration: number = 200;
public sortBy: string;
public sortStrength: number;
public sortBy: string | undefined;
public sortStrength: number = 10;
public width: number;
public height: number;
public width: number | undefined;
public height: number | undefined;
private focusIndex;
private distances;
private eIdealDistances;
private weights;
private radii;
private focusIndex: number | undefined;
private distances: Matrix[] | undefined;
private eIdealDistances: Matrix[] | undefined;
private weights: Matrix[] | undefined;
private radii: number[] | undefined;
public getDefaultCfg() {
return {
@ -110,9 +110,9 @@ export default class RadialLayout extends BaseLayout {
public execute() {
const self = this;
const nodes = self.nodes;
const edges = self.edges;
const edges = self.edges || [];
const center = self.center;
if (nodes.length === 0) {
if (!nodes || nodes.length === 0) {
return;
} else if (nodes.length === 1) {
nodes[0].x = center[0];
@ -121,7 +121,7 @@ export default class RadialLayout extends BaseLayout {
}
const linkDistance = self.linkDistance;
// layout
let focusNode: Node;
let focusNode: Node | null = null;
if (isString(self.focusNode)) {
let found = false;
for (let i = 0; i < nodes.length; i++) {
@ -163,8 +163,8 @@ export default class RadialLayout extends BaseLayout {
if (!self.height && typeof window !== 'undefined') {
self.height = window.innerHeight;
}
const width = self.width;
const height = self.height;
const width = self.width || 500;
const height = self.height || 500;
let semiWidth = width - center[0] > center[0] ? center[0] : width - center[0];
let semiHeight = height - center[1] > center[1] ? center[1] : height - center[1];
if (semiWidth === 0) {
@ -177,7 +177,7 @@ export default class RadialLayout extends BaseLayout {
const maxRadius = semiHeight > semiWidth ? semiWidth : semiHeight;
const maxD = Math.max(...focusNodeD);
// the radius for each nodes away from focusNode
const radii = [];
const radii: number[] = [];
focusNodeD.forEach((value, i) => {
if (!self.unitRadius) {
self.unitRadius = maxRadius / maxD;
@ -196,7 +196,7 @@ export default class RadialLayout extends BaseLayout {
// the initial positions from mds
const mds = new MDS({ distances: eIdealD, linkDistance });
let positions = mds.layout();
positions.forEach((p) => {
positions.forEach((p: IPointTuple) => {
if (isNaN(p[0])) {
p[0] = Math.random() * linkDistance;
}
@ -205,12 +205,12 @@ export default class RadialLayout extends BaseLayout {
}
});
self.positions = positions;
positions.forEach((p, i) => {
positions.forEach((p: IPointTuple, i: number) => {
nodes[i].x = p[0] + center[0];
nodes[i].y = p[1] + center[1];
});
// move the graph to origin, centered at focusNode
positions.forEach((p) => {
positions.forEach((p: IPointTuple) => {
p[0] -= positions[focusIndex][0];
p[1] -= positions[focusIndex][1];
});
@ -222,7 +222,7 @@ export default class RadialLayout extends BaseLayout {
// stagger the overlapped nodes
if (preventOverlap) {
const nodeSpacing = self.nodeSpacing;
let nodeSpacingFunc;
let nodeSpacingFunc: Function;
if (isNumber(nodeSpacing)) {
nodeSpacingFunc = () => {
return nodeSpacing;
@ -235,7 +235,7 @@ export default class RadialLayout extends BaseLayout {
};
}
if (!nodeSize) {
nodeSizeFunc = (d) => {
nodeSizeFunc = (d: NodeConfig) => {
if (d.size) {
if (isArray(d.size)) {
const res = d.size[0] > d.size[1] ? d.size[0] : d.size[1];
@ -247,17 +247,17 @@ export default class RadialLayout extends BaseLayout {
};
} else {
if (isArray(nodeSize)) {
nodeSizeFunc = (d) => {
nodeSizeFunc = (d: NodeConfig) => {
const res = nodeSize[0] > nodeSize[1] ? nodeSize[0] : nodeSize[1];
return res + nodeSpacingFunc(d);
};
} else {
nodeSizeFunc = (d) => {
nodeSizeFunc = (d: NodeConfig) => {
return nodeSize + nodeSpacingFunc(d);
};
}
}
const nonoverlapForce = new RadialNonoverlapForce({
const nonoverlapForceParams: RadialNonoverlapForceParam = {
nodeSizeFunc,
adjMatrix,
positions,
@ -269,11 +269,12 @@ export default class RadialLayout extends BaseLayout {
iterations: self.maxPreventOverlapIteration || 200,
k: positions.length / 4.5,
nodes,
});
};
const nonoverlapForce = new RadialNonoverlapForce(nonoverlapForceParams);
positions = nonoverlapForce.layout();
}
// move the graph to center
positions.forEach((p, i) => {
positions.forEach((p: IPointTuple, i: number) => {
nodes[i].x = p[0] + center[0];
nodes[i].y = p[1] + center[1];
});
@ -281,21 +282,21 @@ export default class RadialLayout extends BaseLayout {
public run() {
const self = this;
const maxIteration = self.maxIteration;
const positions = self.positions;
const W = self.weights;
const eIdealDis = self.eIdealDistances;
const radii = self.radii;
const positions = self.positions || [];
const W = self.weights|| [];
const eIdealDis = self.eIdealDistances || [];
const radii = self.radii || [];
for (let i = 0; i <= maxIteration; i++) {
const param = i / maxIteration;
self.oneIteration(param, positions, radii, eIdealDis, W);
}
}
private oneIteration(param, positions, radii, D, W) {
private oneIteration(param: number, positions: IPointTuple[], radii: number[], D: Matrix[], W: Matrix[]) {
const self = this;
const vparam = 1 - param;
const focusIndex = self.focusIndex;
positions.forEach((v, i) => {
positions.forEach((v: IPointTuple, i: number) => {
// v
const originDis = getEDistance(v, [0, 0]);
const reciODis = originDis === 0 ? 0 : 1 / originDis;
@ -335,16 +336,17 @@ export default class RadialLayout extends BaseLayout {
});
}
private eIdealDisMatrix() {
private eIdealDisMatrix(): Matrix[] {
const self = this;
const nodes = self.nodes;
if (!nodes) return [];
const D = self.distances;
const linkDis = self.linkDistance;
const radii = self.radii;
const unitRadius = self.unitRadius;
const result = [];
const nodes = self.nodes;
D.forEach((row, i) => {
const newRow: number[] = [];
const radii = self.radii || [];
const unitRadius = self.unitRadius || 50;
const result: Matrix[] = [];
D && D.forEach((row, i) => {
const newRow: Matrix = [];
row.forEach((v, j) => {
if (i === j) {
newRow.push(0);
@ -375,7 +377,7 @@ export default class RadialLayout extends BaseLayout {
return result;
}
private handleInfinity(matrix, focusIndex, step) {
private handleInfinity(matrix: Matrix[], focusIndex: number, step: number) {
const length = matrix.length;
// 遍历 matrix 中遍历 focus 对应行
for (let i = 0; i < length; i++) {
@ -407,7 +409,7 @@ export default class RadialLayout extends BaseLayout {
}
}
private maxToFocus(matrix, focusIndex) {
private maxToFocus(matrix: Matrix[], focusIndex: number): number {
let max = 0;
for (let i = 0; i < matrix[focusIndex].length; i++) {
if (matrix[focusIndex][i] === Infinity) {

View File

@ -1,14 +1,33 @@
import { Matrix, IPointTuple } from '../../types';
import { Point } from '@antv/g-base';
const SPEED_DIVISOR = 800;
export type RadialNonoverlapForceParam = {
positions: IPointTuple[];
adjMatrix: Matrix[];
focusID: number;
radii: number[];
iterations?: number;
height?: number;
width?: number;
speed?: number;
gravity?: number;
nodeSizeFunc: (node: any) => number;
k: number;
strictRadial: boolean;
nodes: any[];
};
export default class RadialNonoverlapForce {
/** node positions */
public positions: any[];
public positions: IPointTuple[];
/** adjacency matrix */
public adjMatrix: any[];
public adjMatrix: Matrix[];
/** focus node */
public focusID: number;
/** radii */
public radii: number;
public radii: number[];
/** the number of iterations */
public iterations: number;
/** the height of the canvas */
@ -24,19 +43,14 @@ export default class RadialNonoverlapForce {
/** the strength of forces */
public k: number;
/** if each circle can be separated into subcircles to avoid overlappings */
public strictRadial: number;
public strictRadial: boolean;
/** the nodes data */
public nodes: any[];
private maxDisplace: number;
private disp: Array<{
x: number;
y: number;
dx?: number;
dy?: number;
}>;
private maxDisplace: number | undefined;
private disp: Point[] = [];
constructor(params) {
constructor(params: RadialNonoverlapForceParam) {
this.positions = params.positions;
this.adjMatrix = params.adjMatrix;
this.focusID = params.focusID;
@ -52,10 +66,10 @@ export default class RadialNonoverlapForce {
this.nodes = params.nodes;
}
public layout() {
public layout(): IPointTuple[] {
const self = this;
const positions = self.positions;
const disp = [];
const disp: Point[] = [];
const iterations = self.iterations;
const maxDisplace = self.width / 10;
self.maxDisplace = maxDisplace;
@ -77,11 +91,11 @@ export default class RadialNonoverlapForce {
const nodes = self.nodes;
const disp = self.disp;
const k = self.k;
const radii = self.radii;
const radii = self.radii || [];
positions.forEach((v, i) => {
positions.forEach((v: IPointTuple, i: number) => {
disp[i] = { x: 0, y: 0 };
positions.forEach((u, j) => {
positions.forEach((u: IPointTuple, j: number) => {
if (i === j) {
return;
}
@ -115,6 +129,7 @@ export default class RadialNonoverlapForce {
const speed = self.speed;
const strictRadial = self.strictRadial;
const f = self.focusID;
const maxDisplace = self.maxDisplace || self.width / 10;
if (strictRadial) {
disp.forEach((di, i) => {
@ -136,12 +151,6 @@ export default class RadialNonoverlapForce {
});
}
// speed
positions.forEach((_, i) => {
disp[i].dx *= speed / SPEED_DIVISOR;
disp[i].dy *= speed / SPEED_DIVISOR;
});
// move
const radii = self.radii;
positions.forEach((n, i) => {
@ -150,7 +159,7 @@ export default class RadialNonoverlapForce {
}
const distLength = Math.sqrt(disp[i].x * disp[i].x + disp[i].y * disp[i].y);
if (distLength > 0 && i !== f) {
const limitedDist = Math.min(self.maxDisplace * (speed / SPEED_DIVISOR), distLength);
const limitedDist = Math.min(maxDisplace * (speed / SPEED_DIVISOR), distLength);
n[0] += (disp[i].x / distLength) * limitedDist;
n[1] += (disp[i].y / distLength) * limitedDist;
if (strictRadial) {

View File

@ -11,11 +11,11 @@ import { BaseLayout } from './layout';
*/
export default class RandomLayout extends BaseLayout {
/** 布局中心 */
public center: IPointTuple;
public center: IPointTuple = [0, 0];
/** 宽度 */
public width: number;
public width: number = 300;
/** 高度 */
public height: number;
public height: number = 300;
public getDefaultCfg() {
return {
@ -38,7 +38,7 @@ export default class RandomLayout extends BaseLayout {
if (!self.height && typeof window !== 'undefined') {
self.height = window.innerHeight;
}
nodes.forEach((node) => {
nodes && nodes.forEach((node) => {
node.x = (Math.random() - 0.5) * layoutScale * self.width + center[0];
node.y = (Math.random() - 0.5) * layoutScale * self.height + center[1];
});

View File

@ -1,7 +1,7 @@
import Base, { IPluginBaseConfig } from '../base'
import Edge from '../../item/edge';
import Graph from '../../graph/graph';
import { GraphData, NodeConfig, NodeMapConfig, EdgeConfig } from '../../types';
import { GraphData, NodeConfig, NodeConfigMap, EdgeConfig } from '../../types';
import { Point } from '@antv/g-base/lib/types';
interface BundlingConfig extends IPluginBaseConfig {
@ -90,7 +90,7 @@ export default class Bundling extends Base {
const edges = data.edges;
const nodes = data.nodes;
const nodeIdMap: NodeMapConfig = {};
const nodeIdMap: NodeConfigMap = {};
let error = false;
nodes.forEach(node => {
@ -186,7 +186,7 @@ export default class Bundling extends Base {
public divideEdges(divisions: number): Point[][] {
const self = this;
const edges: EdgeConfig[] = self.get('data').edges;
const nodeIdMap: NodeMapConfig = self.get('nodeIdMap');
const nodeIdMap: NodeConfigMap = self.get('nodeIdMap');
let edgePoints = self.get('edgePoints');
if (!edgePoints || edgePoints === undefined) edgePoints = [];
@ -264,7 +264,7 @@ export default class Bundling extends Base {
const edges = data.edges;
const bundleThreshold: number = self.get('bundleThreshold');
const nodeIdMap: NodeMapConfig = self.get('nodeIdMap');
const nodeIdMap: NodeConfigMap = self.get('nodeIdMap');
let edgeBundles = self.get('edgeBundles');
if (!edgeBundles) edgeBundles = [];

View File

@ -5,6 +5,7 @@ import ShapeBase from '@antv/g-canvas/lib/shape/base';
import Node from '../item/node';
import { IGraph } from '../interface/graph';
import { IEdge, INode } from '../interface/item';
import { ILabelConfig } from '../interface/shape';
// Math types
export interface IPoint {
@ -117,7 +118,7 @@ export type ModelStyle = Partial<{
};
// loop edge config
loopCfg: LoopConfig;
labelCfg?: object;
labelCfg?: ILabelConfig;
anchorPoints: number[][];
controlPoints: IPoint[];
size: number | number[];
@ -178,10 +179,7 @@ export interface ModelConfig extends ModelStyle {
// 节点或边的类型
type?: string;
label?: string;
labelCfg?: {
style?: object;
[key: string]: unknown;
};
labelCfg?: ILabelConfig;
descriptionCfg?: {
style?: object;
[key: string]: unknown;
@ -205,8 +203,10 @@ export interface ModelConfig extends ModelStyle {
endPoint?: IPoint;
children?: TreeGraphData[];
}
export interface NodeConfig extends ModelConfig {
id: string;
size?: number | number[];
groupId?: string;
description?: string;
}
@ -216,10 +216,7 @@ export interface EdgeConfig extends ModelConfig {
source?: string;
target?: string;
label?: string;
labelCfg?: {
style?: object;
[key: string]: unknown;
};
labelCfg?: ILabelConfig;
sourceNode?: Node;
targetNode?: Node;
startPoint?: IPoint;
@ -236,7 +233,11 @@ export type EdgeData = EdgeConfig & {
endPoint: IPoint;
};
export interface NodeMapConfig {
export interface NodeMap {
[key: string]: INode;
}
export interface NodeConfigMap {
[key: string]: NodeConfig;
}
@ -347,7 +348,7 @@ export type BehaviorOpation<U> = {
export type IEvent = Record<G6Event, string>;
export interface IG6GraphEvent extends GraphEvent {
item: Item;
item: Item | null;
canvasX: number;
canvasY: number;
wheelDelta: number;
@ -361,8 +362,19 @@ export type Item = INode | IEdge;
export type ITEM_TYPE = 'node' | 'edge' | 'group'
export type NodeIdxMap = {
[key: string]: number
}
// 触发 viewportchange 事件的参数
export interface ViewPortEventParam {
action: string;
matrix: Matrix;
}
export interface Indexable<T> { [key:string]: T}
export interface LayoutConfig {
type?: string;
[key: string]: unknown;
}

View File

@ -2,7 +2,7 @@ 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 from '@antv/util/lib/is-array'
import { GraphData, ICircle, IEllipse, IRect, Matrix } from '../types';
import { GraphData, ICircle, IEllipse, IRect, Matrix, EdgeConfig, NodeIdxMap } from '../types';
/**
*
@ -383,3 +383,19 @@ export const rotate = (group: IGroup, angle: number) => {
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;
}

View File

@ -1,10 +1,11 @@
import G6 from '../../../src';
import { NodeConfig, EdgeConfig } from '../../../src/types';
const div = document.createElement('div');
div.id = 'graph-spec';
document.body.appendChild(div);
const data: {nodes: object, edges: object} = {
const data: {nodes: NodeConfig[], edges: EdgeConfig[]} = {
nodes: [
{
id: '0',

View File

@ -32,6 +32,9 @@ describe('layout using web worker', function() {
callback();
});
graph.render();
setTimeout(() => {
callback();
}, 1000);
function callback() {
let count = 0;
@ -41,7 +44,6 @@ describe('layout using web worker', function() {
graph.once('afterlayout', () => {
expect(node.x).not.toEqual(undefined);
expect(node.y).not.toEqual(undefined);
// FIXME:
expect(count >= 1).toEqual(true);
expect(ended).toEqual(true);
graph.destroy();