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 = { module.exports = {
extends: [require.resolve('@umijs/fabric/dist/eslint')], extends: [require.resolve('@umijs/fabric/dist/eslint')],
globals: { globals: {
$: true, $: true,
_: true, _: true,
}, },
rules: { rules: {
'no-bitwise': 0, 'no-bitwise': 0,
'import/order': 0, 'import/order': 0,
'no-plusplus': 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.g6 = require('./dist/g6.min.js') // import the package for webworker
window.insertCss = require('insert-css'); window.insertCss = require('insert-css');

View File

@ -1,6 +1,6 @@
{ {
"name": "@antv/g6", "name": "@antv/g6",
"version": "3.3.0-beta.3", "version": "3.3.0-beta.5",
"description": "A Graph Visualization Framework in JavaScript", "description": "A Graph Visualization Framework in JavaScript",
"keywords": [ "keywords": [
"antv", "antv",
@ -49,7 +49,7 @@
"site:develop": "GATSBY=true gatsby develop --open", "site:develop": "GATSBY=true gatsby develop --open",
"start": "npm run site:develop", "start": "npm run site:develop",
"test": "jest", "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", "lint-staged:js": "eslint --ext .js,.jsx,.ts,.tsx",
"watch": "father build -w", "watch": "father build -w",
"cdn": "antv-bin upload -n @antv/g6" "cdn": "antv-bin upload -n @antv/g6"

View File

@ -1,5 +1,5 @@
export default { export default {
version: '3.3.0-beta.3', version: '3.3.0-beta.5',
rootContainerClassName: 'root-container', rootContainerClassName: 'root-container',
nodeContainerClassName: 'node-container', nodeContainerClassName: 'node-container',
edgeContainerClassName: 'edge-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 each from '@antv/util/lib/each'
import isNil from '@antv/util/lib/is-nil'; import isNil from '@antv/util/lib/is-nil';
import wrapBehavior from '@antv/util/lib/wrap-behavior'; import wrapBehavior from '@antv/util/lib/wrap-behavior';
import { IGraph } from '../../interface/graph'; import Graph from "../graph"
import { IG6GraphEvent, Matrix } from '../../types'; import { IG6GraphEvent, Matrix, Item } from '../../types';
import { cloneEvent, isViewportChanged } from '../../util/base'; import { cloneEvent, isViewportChanged } from '../../util/base';
type Fun = () => void type Fun = () => void
@ -35,14 +35,14 @@ const EVENTS = [
'touchend', 'touchend',
]; ];
export default class EventController { export default class EventController {
private graph: IGraph private graph: Graph
private extendEvents: any[] private extendEvents: any[]
private canvasHandler: Fun; private canvasHandler!: Fun;
private dragging: boolean private dragging: boolean
private preItem private preItem: Item | null = null
public destroyed: boolean public destroyed: boolean
constructor(graph: IGraph) { constructor(graph: Graph) {
this.graph = graph this.graph = graph
this.extendEvents = [] this.extendEvents = []
this.dragging = false this.dragging = false
@ -53,7 +53,7 @@ export default class EventController {
// 初始化 G6 中的事件 // 初始化 G6 中的事件
private initEvents() { private initEvents() {
const self = this const self = this
const graph: IGraph = this.graph; const graph: Graph = this.graph;
const canvas: Canvas = graph.get('canvas'); const canvas: Canvas = graph.get('canvas');
const el = canvas.get('el'); const el = canvas.get('el');
const extendEvents = this.extendEvents; const extendEvents = this.extendEvents;
@ -245,8 +245,8 @@ export default class EventController {
this.dragging = false this.dragging = false
this.preItem = null this.preItem = null
this.extendEvents.length = 0 this.extendEvents.length = 0;
this.canvasHandler = null (this.canvasHandler as Fun | null) = null
this.destroyed = true 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 upperFirst from '@antv/util/lib/upper-first'
import Edge from '../../item/edge'; import Edge from '../../item/edge';
import Node from '../../item/node'; 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 Graph from '../graph';
import { IEdge, INode } from '../../interface/item'; import { IEdge, INode } from '../../interface/item';
@ -36,12 +36,12 @@ export default class ItemController {
* @returns {(Item)} * @returns {(Item)}
* @memberof ItemController * @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 graph = this.graph;
const parent: Group = graph.get(type + 'Group') || graph.get('group'); const parent: Group = graph.get(type + 'Group') || graph.get('group');
const upperType = upperFirst(type); const upperType = upperFirst(type);
let item; let item: Item | null = null;
let styles = graph.get(type + upperFirst(STATE_SUFFIX)) || {}; let styles = graph.get(type + upperFirst(STATE_SUFFIX)) || {};
const defaultModel = graph.get(CFG_PREFIX + upperType); const defaultModel = graph.get(CFG_PREFIX + upperType);
@ -79,8 +79,8 @@ export default class ItemController {
graph.emit('beforeadditem', { type, model }); graph.emit('beforeadditem', { type, model });
if(type === EDGE) { if(type === EDGE) {
let source: string | Item = (model as EdgeConfig).source let source: string | Item | undefined = (model as EdgeConfig).source
let target: string | Item = (model as EdgeConfig).target let target: string | Item | undefined = (model as EdgeConfig).target
if (source && isString(source)) { if (source && isString(source)) {
source = graph.findById(source); source = graph.findById(source);
@ -110,11 +110,13 @@ export default class ItemController {
}) })
} }
graph.get(type + 's').push(item); if (item) {
graph.get('itemMap')[item.get('id')] = item; graph.get(type + 's').push(item);
graph.autoPaint(); graph.get('itemMap')[item.get('id')] = item;
graph.emit('afteradditem', { item, model }); graph.autoPaint();
return item; graph.emit('afteradditem', { item, model });
return item as T;
}
} }
/** /**
@ -230,7 +232,7 @@ export default class ItemController {
items.splice(index, 1); items.splice(index, 1);
const itemId: string = item.get('id') const itemId: string = item.get('id')
const itemMap: NodeMapConfig = graph.get('itemMap') const itemMap: NodeMap = graph.get('itemMap')
delete itemMap[itemId]; delete itemMap[itemId];
if (type === NODE) { if (type === NODE) {
@ -276,7 +278,7 @@ export default class ItemController {
* @param {string[]} states * @param {string[]} states
* @memberof ItemController * @memberof ItemController
*/ */
public clearItemStates(item: Item | string, states: string | string[]): void { public clearItemStates(item: Item | string, states?: string | string[]): void {
const graph = this.graph; const graph = this.graph;
if (isString(item)) { if (isString(item)) {
@ -354,7 +356,7 @@ export default class ItemController {
} }
public destroy() { public destroy() {
this.graph = null; (this.graph as Graph | null) = null;
this.destroyed = true; this.destroyed = true;
} }
} }

View File

@ -4,9 +4,10 @@ import isString from '@antv/util/lib/is-string'
import Behavior from '../../behavior/behavior' import Behavior from '../../behavior/behavior'
import { IBehavior } from '../../interface/behavior'; import { IBehavior } from '../../interface/behavior';
import { IGraph, IMode, IModeType } from '../../interface/graph'; import { IGraph, IMode, IModeType } from '../../interface/graph';
import Graph from '../graph';
export default class ModeController { export default class ModeController {
private graph: IGraph private graph: Graph
public destroyed: boolean public destroyed: boolean
/** /**
* modes = { * modes = {
@ -32,7 +33,7 @@ export default class ModeController {
*/ */
public mode: string public mode: string
private currentBehaves: IBehavior[] private currentBehaves: IBehavior[]
constructor(graph: IGraph) { constructor(graph: Graph) {
this.graph = graph this.graph = graph
this.destroyed = false this.destroyed = false
this.modes = graph.get('modes') || { this.modes = graph.get('modes') || {
@ -62,7 +63,7 @@ export default class ModeController {
const behaviors = this.modes[mode]; const behaviors = this.modes[mode];
const behaves: IBehavior[] = []; const behaves: IBehavior[] = [];
let behave: IBehavior; let behave: IBehavior;
each(behaviors, behavior => { each(behaviors || [], behavior => {
const BehaviorInstance = Behavior.getBehavior(behavior.type) const BehaviorInstance = Behavior.getBehavior(behavior.type)
if (!BehaviorInstance) { if (!BehaviorInstance) {
return; return;
@ -70,14 +71,14 @@ export default class ModeController {
behave = new BehaviorInstance(behavior); behave = new BehaviorInstance(behavior);
if(behave) { if(behave) {
behave.bind(graph) behave.bind(graph as IGraph)
behaves.push(behave); behaves.push(behave);
} }
}); });
this.currentBehaves = behaves; this.currentBehaves = behaves;
} }
private mergeBehaviors(modeBehaviors: IModeType[], behaviors): IModeType[] { private mergeBehaviors(modeBehaviors: IModeType[], behaviors: IModeType[]): IModeType[] {
each(behaviors, behavior => { each(behaviors, behavior => {
if (modeBehaviors.indexOf(behavior) < 0) { if (modeBehaviors.indexOf(behavior) < 0) {
if (isString(behavior)) { if (isString(behavior)) {
@ -89,7 +90,7 @@ export default class ModeController {
return modeBehaviors; return modeBehaviors;
} }
private filterBehaviors(modeBehaviors: IModeType[], behaviors): IModeType[] { private filterBehaviors(modeBehaviors: IModeType[], behaviors: IModeType[]): IModeType[] {
const result: IModeType[] = []; const result: IModeType[] = [];
modeBehaviors.forEach(behavior => { modeBehaviors.forEach(behavior => {
let type: string = '' let type: string = ''
@ -105,7 +106,7 @@ export default class ModeController {
return result; return result;
} }
public setMode(mode: string): ModeController { public setMode(mode: string) {
const modes = this.modes; const modes = this.modes;
const graph = this.graph; const graph = this.graph;
const current = mode const current = mode
@ -143,22 +144,24 @@ export default class ModeController {
*/ */
public manipulateBehaviors(behaviors: IModeType[] | IModeType, modes: string[] | string, isAdd: boolean): ModeController { public manipulateBehaviors(behaviors: IModeType[] | IModeType, modes: string[] | string, isAdd: boolean): ModeController {
const self = this const self = this
let behaves = behaviors let behaves: IModeType[]
if(!isArray(behaviors)) { if(!isArray(behaviors)) {
behaves = [ behaviors ] behaves = [ behaviors ]
}else {
behaves = behaviors
} }
if(isArray(modes)) { if(isArray(modes)) {
each(modes, mode => { each(modes, mode => {
if (!self.modes[mode]) { if (!self.modes[mode]) {
if (isAdd) { if (isAdd) {
self.modes[mode] = [].concat(behaves); self.modes[mode] = behaves
} }
} else { } else {
if (isAdd) { if (isAdd) {
self.modes[mode] = this.mergeBehaviors(self.modes[mode], behaves); self.modes[mode] = this.mergeBehaviors(self.modes[mode] || [], behaves);
} else { } 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(!this.modes[currentMode]) {
if (isAdd) { if (isAdd) {
self.modes[currentMode] = [].concat(behaves); self.modes[currentMode] = behaves
} }
} }
if (isAdd) { if (isAdd) {
self.modes[currentMode] = this.mergeBehaviors(self.modes[currentMode], behaves); self.modes[currentMode] = this.mergeBehaviors(self.modes[currentMode] || [], behaves);
} else { } else {
self.modes[currentMode] = this.filterBehaviors(self.modes[currentMode], behaves); self.modes[currentMode] = this.filterBehaviors(self.modes[currentMode] || [], behaves);
} }
self.setMode(this.mode) self.setMode(this.mode)
@ -189,9 +192,9 @@ export default class ModeController {
} }
public destroy() { public destroy() {
this.graph = null; (this.graph as Graph | null) = null;
this.modes = null; (this.modes as IMode | null) = null;
this.currentBehaves = null; (this.currentBehaves as IBehavior[] | null) = null;
this.destroyed = true; 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 isString from '@antv/util/lib/is-string'
import { IGraph, IStates } from '../../interface/graph'; import { IGraph, IStates } from '../../interface/graph';
import { Item } from '../../types'; import { Item } from '../../types';
import Graph from '../graph';
import { INode } from '../../interface/item';
interface ICachedStates { interface ICachedStates {
enabled: IStates; enabled: IStates;
disabled: IStates; disabled: IStates;
} }
let timer = null
let timer: number | null = null
const TIME_OUT = 16; const TIME_OUT = 16;
export default class StateController { export default class StateController {
private graph: IGraph private graph: Graph
private cachedStates: ICachedStates private cachedStates: ICachedStates
public destroyed: boolean public destroyed: boolean
constructor(graph: IGraph) { constructor(graph: Graph) {
this.graph = graph this.graph = graph
/** /**
* this.cachedStates = { * this.cachedStates = {
@ -43,7 +47,7 @@ export default class StateController {
* @returns * @returns
* @memberof State * @memberof State
*/ */
private checkCache(item: Item, state: string, cache: object) { private checkCache(item: Item, state: string, cache: { [key: string]: any }) {
if (!cache[state]) { if (!cache[state]) {
return; return;
} }
@ -62,11 +66,11 @@ export default class StateController {
* @param {object} states * @param {object} states
* @memberof State * @memberof State
*/ */
private cacheState(item: Item, state: string, states: object) { private cacheState(item: Item, state: string, states: IStates) {
if (!states[state]) { if (!states[state]) {
states[state] = []; states[state] = [];
} }
states[state].push(item); states[state].push(item as INode);
} }
/** /**
@ -132,7 +136,7 @@ export default class StateController {
* @memberof State * @memberof State
*/ */
public updateGraphStates() { public updateGraphStates() {
const states = this.graph.get('states') const states = this.graph.get('states') as IStates
const cachedStates = this.cachedStates; const cachedStates = this.cachedStates;
each(cachedStates.disabled, (val, key) => { 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]) { if (!states[key]) {
states[key] = val; states[key] = val;
} else { } else {
const map = {}; const map: { [key: string]: boolean } = {};
states[key].forEach(item => { states[key].forEach(item => {
if (!item.destroyed) { if (!item.destroyed) {
map[item.get('id')] = true; map[item.get('id')] = true;
} }
}); });
val.forEach(item => { val.forEach((item:Item) => {
if (!item.destroyed) { if (!item.destroyed) {
const id = item.get('id'); const id = item.get('id');
if (!map[id]) { if (!map[id]) {
map[id] = true; map[id] = true;
states[key].push(item); states[key].push(item as INode);
} }
} }
}); });
@ -173,8 +177,8 @@ export default class StateController {
} }
public destroy() { public destroy() {
this.graph = null; (this.graph as Graph | null) = null;
this.cachedStates = null; (this.cachedStates as ICachedStates | null) = null;
if (timer) { if (timer) {
clearTimeout(timer); clearTimeout(timer);
} }

View File

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

View File

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

View File

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

View File

@ -1,8 +1,9 @@
import EventEmitter from '@antv/event-emitter'; import EventEmitter from '@antv/event-emitter';
import { AnimateCfg, Point } from '@antv/g-base/lib/types'; import { AnimateCfg, Point } from '@antv/g-base/lib/types';
import Graph from '../graph/graph'; 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 { IEdge, INode } from './item';
import PluginBase from '../plugins/base';
export interface IModeOption { export interface IModeOption {
type: string; type: string;
@ -162,7 +163,7 @@ export interface IStates {
[key: string]: INode[] [key: string]: INode[]
} }
export interface IGraph extends EventEmitter { export interface IGraph extends EventEmitter {
getDefaultCfg(): GraphOptions; getDefaultCfg(): Partial<GraphOptions>;
get<T = any>(key: string): T; get<T = any>(key: string): T;
set<T = any>(key: string | object, value?: T): Graph; set<T = any>(key: string | object, value?: T): Graph;
findById(id: string): Item; findById(id: string): Item;
@ -445,7 +446,7 @@ export interface IGraph extends EventEmitter {
* @param {(item: T, index: number) => T} fn * @param {(item: T, index: number) => T} fn
* @return {T} * @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 * @param {string} fn
* @return {array} * @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 String
* cfg type * cfg type
*/ */
updateLayout(cfg): void; updateLayout(cfg: LayoutConfig): void;
/** /**
* *
@ -492,13 +493,13 @@ export interface IGraph extends EventEmitter {
* *
* @param {object} plugin * @param {object} plugin
*/ */
addPlugin(plugin): void; addPlugin(plugin: PluginBase): void;
/** /**
* *
* @param {object} plugin * @param {object} plugin
*/ */
removePlugin(plugin): void; removePlugin(plugin: PluginBase): void;
/** /**
* *
@ -545,7 +546,7 @@ export interface ITreeGraph extends IGraph {
* @param {TreeGraphData | undefined} parent * @param {TreeGraphData | undefined} parent
* @return {TreeGraphData} * @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 { IGroup } from '@antv/g-base/lib/interfaces';
import { Point } from '@antv/g-base/lib/types'; import { Point } from '@antv/g-base/lib/types';
import Group from "@antv/g-canvas/lib/group"; 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 的配置项 // item 的配置项
@ -63,16 +63,16 @@ export type IItemBaseConfig = Partial<{
target: string | Item; target: string | Item;
linkCenter: boolean; linkCenter: boolean;
}> }> & Indexable<any>
export interface IItemBase { export interface IItemBase {
_cfg: IItemBaseConfig; _cfg: IItemBaseConfig | null;
destroyed: boolean; destroyed: boolean;
isItem(): boolean; isItem(): boolean;
getKeyShapeStyle(): ShapeStyle; getKeyShapeStyle(): ShapeStyle | void;
/** /**
* *
@ -101,7 +101,7 @@ export interface IItemBase {
*/ */
setState(state: string, enable: boolean): void; 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 * @param {Object} point
* @return {Object} {x,y} * @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> { export interface ILayout<Cfg = any> {
/** 节点 */ /** 节点 */
nodes: NodeConfig[]; nodes: NodeConfig[] | null;
/** 边 */ /** 边 */
edges: EdgeConfig[]; edges: EdgeConfig[] | null;
/** 布局获得的位置 */ /** 布局获得的位置 */
positions: IPointTuple[]; positions: IPointTuple[] | null;
/** 是否已销毁 */ /** 是否已销毁 */
destroyed: boolean; destroyed: boolean;

View File

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

View File

@ -1,11 +1,11 @@
import isNil from '@antv/util/lib/is-nil'; import isNil from '@antv/util/lib/is-nil';
import isPlainObject from '@antv/util/lib/is-plain-object' import isPlainObject from '@antv/util/lib/is-plain-object'
import { IEdge, INode } from "../interface/item"; 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 Item from './item';
import Node from './node' 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 ITEM_NAME_SUFFIX = 'Node'; // 端点的后缀,如 sourceNode, targetNode
const POINT_NAME_SUFFIX = 'Point'; // 起点或者结束点的后缀,如 startPoint, endPoint const POINT_NAME_SUFFIX = 'Point'; // 起点或者结束点的后缀,如 startPoint, endPoint
const ANCHOR_NAME_SUFFIX = 'Anchor'; 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 pointName = END_MAP[name] + POINT_NAME_SUFFIX;
const itemName = name + ITEM_NAME_SUFFIX; const itemName = name + ITEM_NAME_SUFFIX;
const preItem = this.get(itemName); const preItem = this.get(itemName);
@ -86,7 +86,7 @@ export default class Edge extends Item implements IEdge {
* *
* @param name * @param name
*/ */
private getEndPoint(name: string): NodeConfig | IPoint { private getEndPoint(name: SourceTarget): NodeConfig | IPoint {
const itemName = name + ITEM_NAME_SUFFIX; const itemName = name + ITEM_NAME_SUFFIX;
const pointName = END_MAP[name] + POINT_NAME_SUFFIX; const pointName = END_MAP[name] + POINT_NAME_SUFFIX;
const item = this.get(itemName); const item = this.get(itemName);
@ -101,7 +101,7 @@ export default class Edge extends Item implements IEdge {
* *
* @param model * @param model
*/ */
private getControlPointsByCenter(model) { private getControlPointsByCenter(model: EdgeConfig) {
const sourcePoint = this.getEndPoint('source'); const sourcePoint = this.getEndPoint('source');
const targetPoint = this.getEndPoint('target'); const targetPoint = this.getEndPoint('target');
const shapeFactory = this.get('shapeFactory'); 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 itemName = name + ITEM_NAME_SUFFIX;
const pointName = END_MAP[name] + POINT_NAME_SUFFIX; const pointName = END_MAP[name] + POINT_NAME_SUFFIX;
const item = this.get(itemName); const item = this.get(itemName);
@ -137,7 +137,7 @@ export default class Edge extends Item implements IEdge {
public getShapeCfg(model: EdgeConfig): EdgeConfig { public getShapeCfg(model: EdgeConfig): EdgeConfig {
const self = this; const self = this;
const linkCenter: boolean = self.get('linkCenter'); // 如果连接到中心,忽视锚点、忽视控制点 const linkCenter: boolean = self.get('linkCenter'); // 如果连接到中心,忽视锚点、忽视控制点
const cfg: any = super.getShapeCfg(model); const cfg: ModelConfig = super.getShapeCfg(model);
if (linkCenter) { if (linkCenter) {
cfg.startPoint = self.getEndCenter('source'); 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 uniqueId from '@antv/util/lib/unique-id'
import { IItemBase, IItemBaseConfig } from "../interface/item"; import { IItemBase, IItemBaseConfig } from "../interface/item";
import Shape from '../shape/shape'; 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 { getBBox } from '../util/graphic';
import { translate } from '../util/math'; import { translate } from '../util/math';
@ -16,13 +16,15 @@ const RESERVED_STYLES = [ 'fillStyle', 'strokeStyle',
'path', 'points', 'img', 'symbol' ]; 'path', 'points', 'img', 'symbol' ];
export default class ItemBase implements IItemBase { export default class ItemBase implements IItemBase {
public _cfg: IItemBaseConfig = {} public _cfg: IItemBaseConfig & {
[key: string]: unknown
} = {}
private defaultCfg: IItemBaseConfig = { private defaultCfg: IItemBaseConfig = {
/** /**
* id * id
* @type {string} * @type {string}
*/ */
id: null, id: undefined,
/** /**
* *
@ -40,7 +42,7 @@ export default class ItemBase implements IItemBase {
* g group * g group
* @type {G.Group} * @type {G.Group}
*/ */
group: null, group: undefined,
/** /**
* is open animate * is open animate
@ -68,7 +70,7 @@ export default class ItemBase implements IItemBase {
* key shape to calculate item's bbox * key shape to calculate item's bbox
* @type object * @type object
*/ */
keyShape: null, keyShape: undefined,
/** /**
* item's states, such as selected or active * item's states, such as selected or active
* @type Array * @type Array
@ -81,7 +83,7 @@ export default class ItemBase implements IItemBase {
constructor(cfg: IItemBaseConfig) { constructor(cfg: IItemBaseConfig) {
this._cfg = Object.assign(this.defaultCfg, this.getDefaultCfg(), cfg) this._cfg = Object.assign(this.defaultCfg, this.getDefaultCfg(), cfg)
const group = cfg.group const group = cfg.group
group.set('item', this) if (group) group.set('item', this)
let id = this.get('model').id let id = this.get('model').id
@ -90,7 +92,7 @@ export default class ItemBase implements IItemBase {
} }
this.set('id', id) this.set('id', id)
group.set('id', id) if (group) group.set('id', id)
const stateStyles = this.get('model').stateStyles; const stateStyles = this.get('model').stateStyles;
this.set('stateStyles', stateStyles); this.set('stateStyles', stateStyles);
@ -131,7 +133,7 @@ export default class ItemBase implements IItemBase {
} }
self.updatePosition(model); self.updatePosition(model);
const cfg = self.getShapeCfg(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); const keyShape: IShapeBase = shapeFactory.draw(shapeType, cfg, group);
if (keyShape) { if (keyShape) {
@ -141,7 +143,7 @@ export default class ItemBase implements IItemBase {
} }
// 防止由于用户外部修改 model 中的 shape 导致 shape 不更新 // 防止由于用户外部修改 model 中的 shape 导致 shape 不更新
this.set('currentShape', shapeType); 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 shapeFactory
* @param shapeType * @param shapeType
*/ */
private resetStates(shapeFactory, shapeType: string) { private resetStates(shapeFactory: any, shapeType: string) {
const self = this; const self = this;
const states: string[] = self.get('states'); const states: string[] = self.get('states');
each(states, state => { each(states, state => {
@ -168,8 +170,8 @@ export default class ItemBase implements IItemBase {
* @param {String} key * @param {String} key
* @return {object | string | number} * @return {object | string | number}
*/ */
public get(key: string) { public get<T = any>(key: string): T {
return this._cfg[key] return this._cfg[key] as T
} }
/** /**
@ -178,7 +180,7 @@ export default class ItemBase implements IItemBase {
* @param {String|Object} key * @param {String|Object} key
* @param {object | string | number} val * @param {object | string | number} val
*/ */
public set(key: string, val): void { public set(key: string | object, val?: unknown): void {
if(isPlainObject(key)) { if(isPlainObject(key)) {
this._cfg = Object.assign({}, this._cfg, key) this._cfg = Object.assign({}, this._cfg, key)
} else { } else {
@ -228,10 +230,10 @@ export default class ItemBase implements IItemBase {
this.afterDraw() this.afterDraw()
} }
public getKeyShapeStyle(): ShapeStyle { public getKeyShapeStyle(): ShapeStyle | void {
const keyShape = this.getKeyShape(); const keyShape = this.getKeyShape();
if (keyShape) { if (keyShape) {
const styles: ShapeStyle = {}; const styles: ShapeStyle & Indexable<any> = {};
each(keyShape.attr(), (val, key) => { each(keyShape.attr(), (val, key) => {
if (RESERVED_STYLES.indexOf(key) < 0) { if (RESERVED_STYLES.indexOf(key) < 0) {
styles[key] = val; styles[key] = val;
@ -239,6 +241,7 @@ export default class ItemBase implements IItemBase {
}); });
return styles; return styles;
} }
return {}
} }
public getShapeCfg(model: ModelConfig): ModelConfig { public getShapeCfg(model: ModelConfig): ModelConfig {
@ -310,7 +313,7 @@ export default class ItemBase implements IItemBase {
const originStates = self.getStates(); const originStates = self.getStates();
const shapeFactory = self.get('shapeFactory'); const shapeFactory = self.get('shapeFactory');
const model: ModelConfig = self.get('model') const model: ModelConfig = self.get('model')
const shape: string = model.shape || model.type; const shape = model.shape || model.type;
if (!states) { if (!states) {
self.set('states', []); self.set('states', []);
shapeFactory.setState(shape, originStates[0], false, self); shapeFactory.setState(shape, originStates[0], false, self);
@ -416,7 +419,7 @@ export default class ItemBase implements IItemBase {
*/ */
public update(cfg: ModelConfig) { public update(cfg: ModelConfig) {
const model: ModelConfig = this.get('model'); 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); Object.assign(model, cfg);
@ -477,7 +480,7 @@ export default class ItemBase implements IItemBase {
} }
group.resetMatrix(); group.resetMatrix();
// G 4.0 element 中移除了矩阵相关方法详见https://www.yuque.com/antv/blog/kxzk9g#4rMMV // 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.x = x;
model.y = y; model.y = y;
this.clearCache(); // 位置更新后需要清除缓存 this.clearCache(); // 位置更新后需要清除缓存
@ -566,7 +569,7 @@ export default class ItemBase implements IItemBase {
group.stopAnimate(); group.stopAnimate();
} }
group.remove(); group.remove();
this._cfg = null; (this._cfg as IItemBaseConfig | null) = null;
this.destroyed = true; this.destroyed = true;
} }
} }

View File

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

View File

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

View File

@ -4,28 +4,21 @@
* this algorithm refers to <cytoscape.js> - https://github.com/cytoscape/cytoscape.js/ * 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 isArray from '@antv/util/lib/is-array';
import isString from '@antv/util/lib/is-string'; import isString from '@antv/util/lib/is-string';
import { BaseLayout } from './layout'; import { BaseLayout } from './layout';
import { getDegree } from '../util/math';
import { isNumber } from '@antv/util';
type Node = NodeConfig & { type Node = NodeConfig & {
degree: number; [key: string]: number;
size: number | number[];
}; };
type Edge = EdgeConfig; type Edge = EdgeConfig;
function getDegree(n: number, nodeIdxMap: object, edges: Edge[]) { type NodeMap = {
const degrees = []; [key: string]: Node;
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;
} }
/** /**
@ -33,33 +26,33 @@ function getDegree(n: number, nodeIdxMap: object, edges: Edge[]) {
*/ */
export default class ConcentricLayout extends BaseLayout { export default class ConcentricLayout extends BaseLayout {
/** 布局中心 */ /** 布局中心 */
public center: IPointTuple; public center: IPointTuple = [0, 0];
public nodeSize: number; public nodeSize: number | IPointTuple = 30;
/** min spacing between outside of nodes (used for radius adjustment) */ /** 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 */ /** 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) */ /** 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 */ /** 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 */ /** 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) */ /** 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 */ /** the letiation of concentric values in each level */
public maxLevelDiff: undefined | number; public maxLevelDiff: undefined | number;
/** 根据 sortBy 指定的属性进行排布,数值高的放在中心,如果是 sortBy 则会计算节点度数,度数最高的放在中心 */ /** 根据 sortBy 指定的属性进行排布,数值高的放在中心,如果是 sortBy 则会计算节点度数,度数最高的放在中心 */
public sortBy: string; public sortBy: string = 'degree';
public nodes: Node[]; public nodes: Node[] = [];
public edges: Edge[]; public edges: Edge[] = [];
public width: number; public width: number = 300;
public height: number; public height: number = 300;
private maxValueNode: number; private maxValueNode: Node | undefined;
private counterclockwise: boolean; private counterclockwise: boolean | undefined;
public getDefaultCfg() { public getDefaultCfg() {
return { return {
@ -92,19 +85,19 @@ export default class ConcentricLayout extends BaseLayout {
return; return;
} }
const layoutNodes = []; const layoutNodes: Node[] = [];
let maxNodeSize: number; let maxNodeSize: number;
if (isNaN(self.nodeSize)) { if (isArray(self.nodeSize)) {
maxNodeSize = Math.max(self.nodeSize[0], self.nodeSize[1]); maxNodeSize = Math.max(self.nodeSize[0], self.nodeSize[1]);
} else { } else {
maxNodeSize = self.nodeSize; maxNodeSize = self.nodeSize;
} }
nodes.forEach((node) => { nodes.forEach((node) => {
layoutNodes.push(node); layoutNodes.push(node);
let nodeSize: number; let nodeSize: number = maxNodeSize;
if (isArray(node.size)) { if (isArray(node.size)) {
nodeSize = Math.max(node.size[0], node.size[1]); nodeSize = Math.max(node.size[0], node.size[1]);
} else { } else if (isNumber(node.size)){
nodeSize = node.size; nodeSize = node.size;
} }
maxNodeSize = Math.max(maxNodeSize, nodeSize); maxNodeSize = Math.max(maxNodeSize, nodeSize);
@ -119,8 +112,8 @@ export default class ConcentricLayout extends BaseLayout {
self.clockwise = self.counterclockwise !== undefined ? !self.counterclockwise : self.clockwise; self.clockwise = self.counterclockwise !== undefined ? !self.counterclockwise : self.clockwise;
// layout // layout
const nodeMap = {}; const nodeMap: NodeMap = {};
const nodeIdxMap = {}; const nodeIdxMap: NodeIdxMap = {};
layoutNodes.forEach((node, i) => { layoutNodes.forEach((node, i) => {
nodeMap[node.id] = node; nodeMap[node.id] = node;
nodeIdxMap[node.id] = i; nodeIdxMap[node.id] = i;
@ -129,7 +122,7 @@ export default class ConcentricLayout extends BaseLayout {
// get the node degrees // get the node degrees
if (self.sortBy === 'degree' || !isString(self.sortBy) || layoutNodes[0][self.sortBy] === undefined) { if (self.sortBy === 'degree' || !isString(self.sortBy) || layoutNodes[0][self.sortBy] === undefined) {
self.sortBy = 'degree'; self.sortBy = 'degree';
if (isNaN(nodes[0].degree)) { if (!isNumber(nodes[0].degree)) {
const values = getDegree(nodes.length, nodeIdxMap, edges); const values = getDegree(nodes.length, nodeIdxMap, edges);
layoutNodes.forEach((node, i) => { layoutNodes.forEach((node, i) => {
node.degree = values[i]; node.degree = values[i];
@ -137,13 +130,13 @@ export default class ConcentricLayout extends BaseLayout {
} }
} }
// sort nodes by value // sort nodes by value
layoutNodes.sort((n1, n2) => { layoutNodes.sort((n1: Node, n2: Node) => {
return n2[self.sortBy] - n1[self.sortBy]; return n2[self.sortBy] - n1[self.sortBy];
}); });
self.maxValueNode = layoutNodes[0]; 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 // put the values into levels
const levels: any[] = [[]]; const levels: any[] = [[]];
@ -151,7 +144,7 @@ export default class ConcentricLayout extends BaseLayout {
layoutNodes.forEach((node) => { layoutNodes.forEach((node) => {
if (currentLevel.length > 0) { if (currentLevel.length > 0) {
const diff = Math.abs(currentLevel[0][self.sortBy] - node[self.sortBy]); const diff = Math.abs(currentLevel[0][self.sortBy] - node[self.sortBy]);
if (diff >= self.maxLevelDiff) { if (self.maxLevelDiff && diff >= self.maxLevelDiff) {
currentLevel = []; currentLevel = [];
levels.push(currentLevel); levels.push(currentLevel);
} }
@ -173,7 +166,10 @@ export default class ConcentricLayout extends BaseLayout {
// find the metrics for each level // find the metrics for each level
let r = 0; let r = 0;
levels.forEach((level) => { 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)); const dTheta = (level.dTheta = sweep / Math.max(1, level.length - 1));
// calculate the radius // calculate the radius

View File

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

View File

@ -18,42 +18,42 @@ import { LAYOUT_MESSAGE } from './worker/layoutConst';
/** /**
* force-directed * 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 方法 */ /** 自定义 force 方法 */
public forceSimulation: any; public forceSimulation: any;
/** 迭代阈值的衰减率 [0, 1]0.028 对应最大迭代数为 300 */ /** 迭代阈值的衰减率 [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里执行布局否则无效 */ /** 是否启用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() { public getDefaultCfg() {
return { return {
@ -83,8 +83,8 @@ export default class ForceLayout extends BaseLayout {
*/ */
public init(data: GraphData) { public init(data: GraphData) {
const self = this; const self = this;
self.nodes = data.nodes; self.nodes = data.nodes || [];
self.edges = data.edges; self.edges = data.edges || [];
self.ticking = false; self.ticking = false;
} }
@ -133,7 +133,7 @@ export default class ForceLayout extends BaseLayout {
}); });
const edgeForce = d3Force const edgeForce = d3Force
.forceLink() .forceLink()
.id((d) => d.id) .id((d: any) => d.id)
.links(d3Edges); .links(d3Edges);
if (self.edgeStrength) { if (self.edgeStrength) {
edgeForce.strength(self.edgeStrength); edgeForce.strength(self.edgeStrength);
@ -164,7 +164,7 @@ export default class ForceLayout extends BaseLayout {
for (let currentTick = 1; currentTick <= totalTicks; currentTick++) { for (let currentTick = 1; currentTick <= totalTicks; currentTick++) {
simulation.tick(); simulation.tick();
// currentTick starts from 1. // 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; self.ticking = false;
} }
@ -188,7 +188,7 @@ export default class ForceLayout extends BaseLayout {
* *
* @param {object} simulation * @param {object} simulation
*/ */
public overlapProcess(simulation) { public overlapProcess(simulation: any) {
const self = this; const self = this;
const nodeSize = self.nodeSize; const nodeSize = self.nodeSize;
const nodeSpacing = self.nodeSpacing; const nodeSpacing = self.nodeSpacing;
@ -209,7 +209,7 @@ export default class ForceLayout extends BaseLayout {
} }
if (!nodeSize) { if (!nodeSize) {
nodeSizeFunc = (d) => { nodeSizeFunc = d => {
if (d.size) { if (d.size) {
if (isArray(d.size)) { if (isArray(d.size)) {
const res = d.size[0] > d.size[1] ? d.size[0] : d.size[1]; 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) => { nodeSizeFunc = (d) => {
return radius + nodeSpacingFunc(d); return radius + nodeSpacingFunc(d);
}; };
} else if (!isNaN(nodeSize)) { } else if (isNumber(nodeSize)) {
const radius = nodeSize / 2; const radius = nodeSize / 2;
nodeSizeFunc = (d) => { nodeSizeFunc = (d) => {
return radius + nodeSpacingFunc(d); return radius + nodeSpacingFunc(d);
}; };
} else {
nodeSizeFunc = () => {
return 10;
};
} }
// forceCollide's parameter is a radius // forceCollide's parameter is a radius
@ -245,14 +249,14 @@ export default class ForceLayout extends BaseLayout {
* *
* @param {object} cfg * @param {object} cfg
*/ */
public updateCfg(cfg) { public updateCfg(cfg: Partial<Cfg>) {
const self = this; const self = this;
if (self.ticking) { if (self.ticking) {
self.forceSimulation.stop(); self.forceSimulation.stop();
self.ticking = false; self.ticking = false;
} }
self.forceSimulation = null; self.forceSimulation = null;
mix(self, cfg); mix(self as any, cfg);
} }
public destroy() { public destroy() {
@ -268,7 +272,7 @@ export default class ForceLayout extends BaseLayout {
} }
// Return total ticks of d3-force simulation // Return total ticks of d3-force simulation
function getSimulationTicks(simulation): number { function getSimulationTicks(simulation: any): number {
const alphaMin = simulation.alphaMin(); const alphaMin = simulation.alphaMin();
const alphaTarget = simulation.alphaTarget(); const alphaTarget = simulation.alphaTarget();
const alpha = simulation.alpha(); const alpha = simulation.alpha();
@ -276,7 +280,7 @@ function getSimulationTicks(simulation): number {
const totalTicks = Math.ceil(totalTicksFloat); const totalTicks = Math.ceil(totalTicksFloat);
return totalTicks; return totalTicks;
} }
declare const WorkerGlobalScope; declare const WorkerGlobalScope: any;
// 判断是否运行在web worker里 // 判断是否运行在web worker里
function isInWorker(): boolean { function isInWorker(): boolean {

View File

@ -3,16 +3,20 @@
* @author shiwu.wyy@antfin.com * @author shiwu.wyy@antfin.com
*/ */
import { EdgeConfig, IPointTuple, NodeConfig } from '../types'; import { EdgeConfig, IPointTuple, NodeConfig, NodeIdxMap } from '../types';
import { BaseLayout } from './layout'; import { BaseLayout } from './layout';
import { isNumber } from '@antv/util';
import { Point } from '@antv/g-base';
type Node = NodeConfig & { type Node = NodeConfig & {
cluster: string; cluster: string | number;
}; };
type Edge = EdgeConfig; type Edge = EdgeConfig;
type NodeMap = Map<string, Node>; type NodeMap = {
type NodeIndexMap = Map<string, string>; [key: string]: Node;
}
const SPEED_DIVISOR = 800; const SPEED_DIVISOR = 800;
@ -21,26 +25,26 @@ const SPEED_DIVISOR = 800;
*/ */
export default class FruchtermanLayout extends BaseLayout { 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 nodes: Node[] = [];
public height: number; public edges: Edge[] = [];
public nodes: Node[]; public width: number = 300;
public edges: Edge[]; public height: number = 300;
public nodeMap: object; public nodeMap: NodeMap = {};
public nodeIndexMap: object; public nodeIdxMap: NodeIdxMap = {};
public getDefaultCfg() { public getDefaultCfg() {
return { return {
@ -60,21 +64,21 @@ export default class FruchtermanLayout extends BaseLayout {
const nodes = self.nodes; const nodes = self.nodes;
const center = self.center; const center = self.center;
if (nodes.length === 0) { if (!nodes || nodes.length === 0) {
return; return;
} else if (nodes.length === 1) { } else if (nodes.length === 1) {
nodes[0].x = center[0]; nodes[0].x = center[0];
nodes[0].y = center[1]; nodes[0].y = center[1];
return; return;
} }
const nodeMap = {}; const nodeMap: NodeMap = {};
const nodeIndexMap = {}; const nodeIdxMap: NodeIdxMap = {};
nodes.forEach((node, i) => { nodes.forEach((node, i) => {
nodeMap[node.id] = node; nodeMap[node.id] = node;
nodeIndexMap[node.id] = i; nodeIdxMap[node.id] = i;
}); });
self.nodeMap = nodeMap; self.nodeMap = nodeMap;
self.nodeIndexMap = nodeIndexMap; self.nodeIdxMap = nodeIdxMap;
// layout // layout
self.run(); self.run();
} }
@ -82,6 +86,7 @@ export default class FruchtermanLayout extends BaseLayout {
public run() { public run() {
const self = this; const self = this;
const nodes = self.nodes; const nodes = self.nodes;
if (!nodes) return;
const edges = self.edges; const edges = self.edges;
const maxIteration = self.maxIteration; const maxIteration = self.maxIteration;
if (!self.width && typeof window !== 'undefined') { if (!self.width && typeof window !== 'undefined') {
@ -92,15 +97,20 @@ export default class FruchtermanLayout extends BaseLayout {
} }
const center = self.center; const center = self.center;
const nodeMap = self.nodeMap; const nodeMap = self.nodeMap;
const nodeIndexMap = self.nodeIndexMap; const nodeIdxMap = self.nodeIdxMap;
const maxDisplace = self.width / 10; const maxDisplace = self.width / 10;
const k = Math.sqrt((self.width * self.height) / (nodes.length + 1)); const k = Math.sqrt((self.width * self.height) / (nodes.length + 1));
const gravity = self.gravity; const gravity = self.gravity;
const speed = self.speed; const speed = self.speed;
const clustering = self.clustering; const clustering = self.clustering;
const clusterMap = {} const clusterMap: {[key: string]: {
name: string | number,
cx: number,
cy: number,
count: number
}} = {}
if (clustering) { if (clustering) {
nodes.forEach((n) => { nodes.forEach(n => {
if (clusterMap[n.cluster] === undefined) { if (clusterMap[n.cluster] === undefined) {
const cluster = { const cluster = {
name: n.cluster, name: n.cluster,
@ -111,8 +121,12 @@ export default class FruchtermanLayout extends BaseLayout {
clusterMap[n.cluster] = cluster; clusterMap[n.cluster] = cluster;
} }
const c = clusterMap[n.cluster]; const c = clusterMap[n.cluster];
c.cx += n.x; if (isNumber(n.x)) {
c.cy += n.y; c.cx += n.x;
}
if (isNumber(n.y)) {
c.cy += n.y;
}
c.count++; c.count++;
}); });
for (let key in clusterMap) { for (let key in clusterMap) {
@ -121,16 +135,17 @@ export default class FruchtermanLayout extends BaseLayout {
} }
} }
for (let i = 0; i < maxIteration; i++) { for (let i = 0; i < maxIteration; i++) {
const disp = []; const disp: Point[] = [];
nodes.forEach((_, j) => { nodes.forEach((_, j) => {
disp[j] = { x: 0, y: 0 }; 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 // gravity for clusters
if (clustering) { if (clustering) {
const clusterGravity = self.clusterGravity || gravity; const clusterGravity = self.clusterGravity || gravity;
nodes.forEach((n, j) => { nodes.forEach((n, j) => {
if (!isNumber(n.x) || !isNumber(n.y)) return;
const c = clusterMap[n.cluster]; 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 distLength = Math.sqrt((n.x - c.cx) * (n.x - c.cx) + (n.y - c.cy) * (n.y - c.cy));
const gravityForce = k * clusterGravity; const gravityForce = k * clusterGravity;
@ -147,8 +162,12 @@ export default class FruchtermanLayout extends BaseLayout {
nodes.forEach((n) => { nodes.forEach((n) => {
const c = clusterMap[n.cluster]; const c = clusterMap[n.cluster];
c.cx += n.x; if (isNumber(n.x)) {
c.cy += n.y; c.cx += n.x;
}
if (isNumber(n.y)) {
c.cy += n.y;
}
c.count++; c.count++;
}); });
for (let key in clusterMap) { for (let key in clusterMap) {
@ -159,18 +178,15 @@ export default class FruchtermanLayout extends BaseLayout {
// gravity // gravity
nodes.forEach((n, j) => { nodes.forEach((n, j) => {
if (!isNumber(n.x) || !isNumber(n.y)) return;
const gravityForce = 0.01 * k * gravity; const gravityForce = 0.01 * k * gravity;
disp[j].x -= gravityForce * (n.x - center[0]); disp[j].x -= gravityForce * (n.x - center[0]);
disp[j].y -= gravityForce * (n.y - center[1]); 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 // move
nodes.forEach((n, j) => { 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); const distLength = Math.sqrt(disp[j].x * disp[j].x + disp[j].y * disp[j].y);
if (distLength > 0) { if (distLength > 0) {
// && !n.isFixed() // && !n.isFixed()
@ -183,19 +199,20 @@ export default class FruchtermanLayout extends BaseLayout {
} }
// TODO: nodeMap、nodeIndexMap 等根本不需要依靠参数传递 // 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; const self = this;
self.calRepulsive(nodes, disp, k); 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) => { nodes.forEach((v, i) => {
disp[i] = { x: 0, y: 0 }; disp[i] = { x: 0, y: 0 };
nodes.forEach((u, j) => { nodes.forEach((u, j) => {
if (i === j) { if (i === j) {
return; return;
} }
if (!isNumber(v.x) || !isNumber(u.x) || !isNumber(v.y) || !isNumber(u.y)) return;
let vecx = v.x - u.x; let vecx = v.x - u.x;
let vecy = v.y - u.y; let vecy = v.y - u.y;
let vecLengthSqr = vecx * vecx + vecy * vecy; 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) => { edges.forEach((e) => {
const uIndex = nodeIndexMap[e.source]; if (!e.source || !e.target) return;
const vIndex = nodeIndexMap[e.target]; const uIndex = nodeIdxMap[e.source];
const vIndex = nodeIdxMap[e.target];
if (uIndex === vIndex) { if (uIndex === vIndex) {
return; return;
} }
const u = nodeMap[e.source]; const u = nodeMap[e.source];
const v = nodeMap[e.target]; 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 vecx = v.x - u.x;
const vecy = v.y - u.y; const vecy = v.y - u.y;
const vecLength = Math.sqrt(vecx * vecx + vecy * vecy); 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/ * 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 isString from '@antv/util/lib/is-string';
import { BaseLayout } from './layout'; import { BaseLayout } from './layout';
import { isArray, isNumber } from '@antv/util'; import { isArray, isNumber } from '@antv/util';
import { getDegree } from '../util/math';
type Node = NodeConfig & { type Node = NodeConfig & {
degree: number; degree: number;
size: number;
}; };
type Edge = EdgeConfig; 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 { export default class GridLayout extends BaseLayout {
/** 布局起始点 */ /** 布局起始点 */
public begin: IPointTuple; public begin: IPointTuple = [0, 0];
/** prevents node overlap, may overflow boundingBox if not enough space */ /** prevents node overlap, may overflow boundingBox if not enough space */
public preventOverlap: boolean; public preventOverlap: boolean = true;
/** extra spacing around nodes when preventOverlap: 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 */ /** 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 */ /** force num of rows in the grid */
public rows: number; public rows: number | undefined;
/** force num of columns in the grid */ /** force num of columns in the grid */
public cols: number; public cols: number | undefined;
/** returns { row, col } for element */ /** 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') } */ /** a sorting function to order the nodes; e.g. function(a, b){ return a.datapublic ('weight') - b.data('weight') } */
public sortBy: string; public sortBy: string = 'degree';
public nodeSize: number | number[]; public nodeSize: number | number[] = 30;
public nodes: Node[]; public nodes: Node[] = [];
public edges: Edge[]; public edges: Edge[] = [];
/** 布局中心 */ /** 布局中心 */
public center: IPointTuple; public center: IPointTuple = [0, 0];
public width: number; public width: number = 300;
public height: number; public height: number = 300;
private cells: number; private cells: number | undefined;
private row: number; private row: number = 0;
private col: number; private col: number = 0;
private splits: number; private splits: number | undefined;
private columns: number; private columns: number | undefined;
private cellWidth: number; private cellWidth: number = 0;
private cellHeight: number; private cellHeight: number = 0;
private cellUsed: { private cellUsed: {
[key: string]: boolean; [key: string]: boolean;
}; } = {};
private id2manPos: { private id2manPos: {
[key: string]: { [key: string]: {
row: number; row: number;
col: number; col: number;
}; };
}; } = {};
public getDefaultCfg() { public getDefaultCfg() {
return { return {
@ -83,7 +71,7 @@ export default class GridLayout extends BaseLayout {
condense: false, condense: false,
rows: undefined, rows: undefined,
cols: undefined, cols: undefined,
position() {}, position: undefined,
sortBy: 'degree', sortBy: 'degree',
nodeSize: 30 nodeSize: 30
}; };
@ -109,7 +97,7 @@ export default class GridLayout extends BaseLayout {
nodes.forEach((node) => { nodes.forEach((node) => {
layoutNodes.push(node); layoutNodes.push(node);
}); });
const nodeIdxMap = {}; const nodeIdxMap: NodeIdxMap = {};
layoutNodes.forEach((node, i) => { layoutNodes.forEach((node, i) => {
nodeIdxMap[node.id] = i; nodeIdxMap[node.id] = i;
}); });
@ -156,8 +144,8 @@ export default class GridLayout extends BaseLayout {
if (self.cols * self.rows > self.cells) { if (self.cols * self.rows > self.cells) {
// otherwise use the automatic values and adjust accordingly // otherwise use the automatic values and adjust accordingly
// if rounding was up, see if we can reduce rows or columns // if rounding was up, see if we can reduce rows or columns
const sm = self.small(); const sm = self.small() as number;
const lg = self.large(); const lg = self.large() as number;
// reducing the small side takes away the most cells, so try it first // reducing the small side takes away the most cells, so try it first
if ((sm - 1) * lg >= self.cells) { if ((sm - 1) * lg >= self.cells) {
@ -168,8 +156,8 @@ export default class GridLayout extends BaseLayout {
} else { } else {
// if rounding was too low, add rows or columns // if rounding was too low, add rows or columns
while (self.cols * self.rows < self.cells) { while (self.cols * self.rows < self.cells) {
const sm = self.small(); const sm = self.small() as number;
const lg = self.large(); const lg = self.large() as number;
// try to add to larger side first (adds less in multiplication) // try to add to larger side first (adds less in multiplication)
if ((lg + 1) * sm >= self.cells) { if ((lg + 1) * sm >= self.cells) {
@ -196,16 +184,16 @@ export default class GridLayout extends BaseLayout {
node.y = 0; node.y = 0;
} }
let nodew: number; let nodew: number | undefined;
let nodeh: number; let nodeh: number | undefined;
if (isArray(node.size)) { if (isArray(node.size)) {
nodew = node.size[0]; nodew = node.size[0];
nodeh = node.size[1]; nodeh = node.size[1];
} else { } else if(isNumber(node.size)){
nodew = node.size; nodew = node.size;
nodeh = node.size; nodeh = node.size;
} }
if (isNaN(nodew) || isNaN(nodeh)) { if (nodew === undefined || nodeh === undefined) {
if (isArray(self.nodeSize)) { if (isArray(self.nodeSize)) {
nodew = self.nodeSize[0]; nodew = self.nodeSize[0];
nodeh = self.nodeSize[1]; nodeh = self.nodeSize[1];
@ -238,7 +226,10 @@ export default class GridLayout extends BaseLayout {
self.id2manPos = {}; self.id2manPos = {};
for (let i = 0; i < layoutNodes.length; i++) { for (let i = 0; i < layoutNodes.length; i++) {
const node = layoutNodes[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)) { if (rcPos && (rcPos.row !== undefined || rcPos.col !== undefined)) {
// must have at least row or col def'd // must have at least row or col def'd
@ -269,13 +260,15 @@ export default class GridLayout extends BaseLayout {
self.getPos(node); self.getPos(node);
} }
} }
private small(val?: number) { private small(val?: number): number | undefined {
const self = this; const self = this;
let res: number; let res: number | undefined;
const rows = self.rows || 5;
const cols = self.cols || 5;
if (val == null) { if (val == null) {
res = Math.min(self.rows, self.cols); res = Math.min(rows, cols);
} else { } else {
const min = Math.min(self.rows, self.cols); const min = Math.min(rows, cols);
if (min === self.rows) { if (min === self.rows) {
self.rows = val; self.rows = val;
} else { } else {
@ -285,13 +278,15 @@ export default class GridLayout extends BaseLayout {
return res; return res;
} }
private large(val?: number) { private large(val?: number): number | undefined {
const self = this; const self = this;
let res: number; let res: number | undefined;
const rows = self.rows || 5;
const cols = self.cols || 5;
if (val == null) { if (val == null) {
res = Math.max(self.rows, self.cols); res = Math.max(rows, cols);
} else { } else {
const max = Math.max(self.rows, self.cols); const max = Math.max(rows, cols);
if (max === self.rows) { if (max === self.rows) {
self.rows = val; self.rows = val;
} else { } else {
@ -313,8 +308,9 @@ export default class GridLayout extends BaseLayout {
private moveToNextCell() { private moveToNextCell() {
const self = this; const self = this;
const cols = self.cols || 5;
self.col++; self.col++;
if (self.col >= self.cols) { if (self.col >= cols) {
self.col = 0; self.col = 0;
self.row++; self.row++;
} }

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@
* @author shiwu.wyy@antfin.com * @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 isArray from '@antv/util/lib/is-array';
import isNumber from '@antv/util/lib/is-number'; import isNumber from '@antv/util/lib/is-number';
@ -14,11 +14,11 @@ import { floydWarshall, getAdjMatrix } from '../../util/math';
import { BaseLayout } from '../layout'; import { BaseLayout } from '../layout';
import MDS from './mds'; import MDS from './mds';
import RadialNonoverlapForce from './radialNonoverlapForce'; import RadialNonoverlapForce, { RadialNonoverlapForceParam } from './radialNonoverlapForce';
type Node = NodeConfig; type Node = NodeConfig;
function getWeightMatrix(M: number[][]) { function getWeightMatrix(M: Matrix[]) {
const rows = M.length; const rows = M.length;
const cols = M[0].length; const cols = M[0].length;
const result = []; const result = [];
@ -56,37 +56,37 @@ function getEDistance(p1: IPointTuple, p2: IPointTuple) {
*/ */
export default class RadialLayout extends BaseLayout { 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 时生效 */ /** 是否必须是严格的 radial 布局即每一层的节点严格布局在一个环上。preventOverlap 为 true 时生效 */
public strictRadial: boolean; public strictRadial: boolean = true;
/** 防止重叠步骤的最大迭代次数 */ /** 防止重叠步骤的最大迭代次数 */
public maxPreventOverlapIteration: number; public maxPreventOverlapIteration: number = 200;
public sortBy: string; public sortBy: string | undefined;
public sortStrength: number; public sortStrength: number = 10;
public width: number; public width: number | undefined;
public height: number; public height: number | undefined;
private focusIndex; private focusIndex: number | undefined;
private distances; private distances: Matrix[] | undefined;
private eIdealDistances; private eIdealDistances: Matrix[] | undefined;
private weights; private weights: Matrix[] | undefined;
private radii; private radii: number[] | undefined;
public getDefaultCfg() { public getDefaultCfg() {
return { return {
@ -110,9 +110,9 @@ export default class RadialLayout extends BaseLayout {
public execute() { public execute() {
const self = this; const self = this;
const nodes = self.nodes; const nodes = self.nodes;
const edges = self.edges; const edges = self.edges || [];
const center = self.center; const center = self.center;
if (nodes.length === 0) { if (!nodes || nodes.length === 0) {
return; return;
} else if (nodes.length === 1) { } else if (nodes.length === 1) {
nodes[0].x = center[0]; nodes[0].x = center[0];
@ -121,7 +121,7 @@ export default class RadialLayout extends BaseLayout {
} }
const linkDistance = self.linkDistance; const linkDistance = self.linkDistance;
// layout // layout
let focusNode: Node; let focusNode: Node | null = null;
if (isString(self.focusNode)) { if (isString(self.focusNode)) {
let found = false; let found = false;
for (let i = 0; i < nodes.length; i++) { for (let i = 0; i < nodes.length; i++) {
@ -163,8 +163,8 @@ export default class RadialLayout extends BaseLayout {
if (!self.height && typeof window !== 'undefined') { if (!self.height && typeof window !== 'undefined') {
self.height = window.innerHeight; self.height = window.innerHeight;
} }
const width = self.width; const width = self.width || 500;
const height = self.height; const height = self.height || 500;
let semiWidth = width - center[0] > center[0] ? center[0] : width - center[0]; let semiWidth = width - center[0] > center[0] ? center[0] : width - center[0];
let semiHeight = height - center[1] > center[1] ? center[1] : height - center[1]; let semiHeight = height - center[1] > center[1] ? center[1] : height - center[1];
if (semiWidth === 0) { if (semiWidth === 0) {
@ -177,7 +177,7 @@ export default class RadialLayout extends BaseLayout {
const maxRadius = semiHeight > semiWidth ? semiWidth : semiHeight; const maxRadius = semiHeight > semiWidth ? semiWidth : semiHeight;
const maxD = Math.max(...focusNodeD); const maxD = Math.max(...focusNodeD);
// the radius for each nodes away from focusNode // the radius for each nodes away from focusNode
const radii = []; const radii: number[] = [];
focusNodeD.forEach((value, i) => { focusNodeD.forEach((value, i) => {
if (!self.unitRadius) { if (!self.unitRadius) {
self.unitRadius = maxRadius / maxD; self.unitRadius = maxRadius / maxD;
@ -196,7 +196,7 @@ export default class RadialLayout extends BaseLayout {
// the initial positions from mds // the initial positions from mds
const mds = new MDS({ distances: eIdealD, linkDistance }); const mds = new MDS({ distances: eIdealD, linkDistance });
let positions = mds.layout(); let positions = mds.layout();
positions.forEach((p) => { positions.forEach((p: IPointTuple) => {
if (isNaN(p[0])) { if (isNaN(p[0])) {
p[0] = Math.random() * linkDistance; p[0] = Math.random() * linkDistance;
} }
@ -205,12 +205,12 @@ export default class RadialLayout extends BaseLayout {
} }
}); });
self.positions = positions; self.positions = positions;
positions.forEach((p, i) => { positions.forEach((p: IPointTuple, i: number) => {
nodes[i].x = p[0] + center[0]; nodes[i].x = p[0] + center[0];
nodes[i].y = p[1] + center[1]; nodes[i].y = p[1] + center[1];
}); });
// move the graph to origin, centered at focusNode // move the graph to origin, centered at focusNode
positions.forEach((p) => { positions.forEach((p: IPointTuple) => {
p[0] -= positions[focusIndex][0]; p[0] -= positions[focusIndex][0];
p[1] -= positions[focusIndex][1]; p[1] -= positions[focusIndex][1];
}); });
@ -222,7 +222,7 @@ export default class RadialLayout extends BaseLayout {
// stagger the overlapped nodes // stagger the overlapped nodes
if (preventOverlap) { if (preventOverlap) {
const nodeSpacing = self.nodeSpacing; const nodeSpacing = self.nodeSpacing;
let nodeSpacingFunc; let nodeSpacingFunc: Function;
if (isNumber(nodeSpacing)) { if (isNumber(nodeSpacing)) {
nodeSpacingFunc = () => { nodeSpacingFunc = () => {
return nodeSpacing; return nodeSpacing;
@ -235,7 +235,7 @@ export default class RadialLayout extends BaseLayout {
}; };
} }
if (!nodeSize) { if (!nodeSize) {
nodeSizeFunc = (d) => { nodeSizeFunc = (d: NodeConfig) => {
if (d.size) { if (d.size) {
if (isArray(d.size)) { if (isArray(d.size)) {
const res = d.size[0] > d.size[1] ? d.size[0] : d.size[1]; 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 { } else {
if (isArray(nodeSize)) { if (isArray(nodeSize)) {
nodeSizeFunc = (d) => { nodeSizeFunc = (d: NodeConfig) => {
const res = nodeSize[0] > nodeSize[1] ? nodeSize[0] : nodeSize[1]; const res = nodeSize[0] > nodeSize[1] ? nodeSize[0] : nodeSize[1];
return res + nodeSpacingFunc(d); return res + nodeSpacingFunc(d);
}; };
} else { } else {
nodeSizeFunc = (d) => { nodeSizeFunc = (d: NodeConfig) => {
return nodeSize + nodeSpacingFunc(d); return nodeSize + nodeSpacingFunc(d);
}; };
} }
} }
const nonoverlapForce = new RadialNonoverlapForce({ const nonoverlapForceParams: RadialNonoverlapForceParam = {
nodeSizeFunc, nodeSizeFunc,
adjMatrix, adjMatrix,
positions, positions,
@ -269,11 +269,12 @@ export default class RadialLayout extends BaseLayout {
iterations: self.maxPreventOverlapIteration || 200, iterations: self.maxPreventOverlapIteration || 200,
k: positions.length / 4.5, k: positions.length / 4.5,
nodes, nodes,
}); };
const nonoverlapForce = new RadialNonoverlapForce(nonoverlapForceParams);
positions = nonoverlapForce.layout(); positions = nonoverlapForce.layout();
} }
// move the graph to center // move the graph to center
positions.forEach((p, i) => { positions.forEach((p: IPointTuple, i: number) => {
nodes[i].x = p[0] + center[0]; nodes[i].x = p[0] + center[0];
nodes[i].y = p[1] + center[1]; nodes[i].y = p[1] + center[1];
}); });
@ -281,21 +282,21 @@ export default class RadialLayout extends BaseLayout {
public run() { public run() {
const self = this; const self = this;
const maxIteration = self.maxIteration; const maxIteration = self.maxIteration;
const positions = self.positions; const positions = self.positions || [];
const W = self.weights; const W = self.weights|| [];
const eIdealDis = self.eIdealDistances; const eIdealDis = self.eIdealDistances || [];
const radii = self.radii; const radii = self.radii || [];
for (let i = 0; i <= maxIteration; i++) { for (let i = 0; i <= maxIteration; i++) {
const param = i / maxIteration; const param = i / maxIteration;
self.oneIteration(param, positions, radii, eIdealDis, W); 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 self = this;
const vparam = 1 - param; const vparam = 1 - param;
const focusIndex = self.focusIndex; const focusIndex = self.focusIndex;
positions.forEach((v, i) => { positions.forEach((v: IPointTuple, i: number) => {
// v // v
const originDis = getEDistance(v, [0, 0]); const originDis = getEDistance(v, [0, 0]);
const reciODis = originDis === 0 ? 0 : 1 / originDis; 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 self = this;
const nodes = self.nodes;
if (!nodes) return [];
const D = self.distances; const D = self.distances;
const linkDis = self.linkDistance; const linkDis = self.linkDistance;
const radii = self.radii; const radii = self.radii || [];
const unitRadius = self.unitRadius; const unitRadius = self.unitRadius || 50;
const result = []; const result: Matrix[] = [];
const nodes = self.nodes; D && D.forEach((row, i) => {
D.forEach((row, i) => { const newRow: Matrix = [];
const newRow: number[] = [];
row.forEach((v, j) => { row.forEach((v, j) => {
if (i === j) { if (i === j) {
newRow.push(0); newRow.push(0);
@ -375,7 +377,7 @@ export default class RadialLayout extends BaseLayout {
return result; return result;
} }
private handleInfinity(matrix, focusIndex, step) { private handleInfinity(matrix: Matrix[], focusIndex: number, step: number) {
const length = matrix.length; const length = matrix.length;
// 遍历 matrix 中遍历 focus 对应行 // 遍历 matrix 中遍历 focus 对应行
for (let i = 0; i < length; i++) { 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; let max = 0;
for (let i = 0; i < matrix[focusIndex].length; i++) { for (let i = 0; i < matrix[focusIndex].length; i++) {
if (matrix[focusIndex][i] === Infinity) { 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; 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 { export default class RadialNonoverlapForce {
/** node positions */ /** node positions */
public positions: any[]; public positions: IPointTuple[];
/** adjacency matrix */ /** adjacency matrix */
public adjMatrix: any[]; public adjMatrix: Matrix[];
/** focus node */ /** focus node */
public focusID: number; public focusID: number;
/** radii */ /** radii */
public radii: number; public radii: number[];
/** the number of iterations */ /** the number of iterations */
public iterations: number; public iterations: number;
/** the height of the canvas */ /** the height of the canvas */
@ -24,19 +43,14 @@ export default class RadialNonoverlapForce {
/** the strength of forces */ /** the strength of forces */
public k: number; public k: number;
/** if each circle can be separated into subcircles to avoid overlappings */ /** if each circle can be separated into subcircles to avoid overlappings */
public strictRadial: number; public strictRadial: boolean;
/** the nodes data */ /** the nodes data */
public nodes: any[]; public nodes: any[];
private maxDisplace: number; private maxDisplace: number | undefined;
private disp: Array<{ private disp: Point[] = [];
x: number;
y: number;
dx?: number;
dy?: number;
}>;
constructor(params) { constructor(params: RadialNonoverlapForceParam) {
this.positions = params.positions; this.positions = params.positions;
this.adjMatrix = params.adjMatrix; this.adjMatrix = params.adjMatrix;
this.focusID = params.focusID; this.focusID = params.focusID;
@ -52,10 +66,10 @@ export default class RadialNonoverlapForce {
this.nodes = params.nodes; this.nodes = params.nodes;
} }
public layout() { public layout(): IPointTuple[] {
const self = this; const self = this;
const positions = self.positions; const positions = self.positions;
const disp = []; const disp: Point[] = [];
const iterations = self.iterations; const iterations = self.iterations;
const maxDisplace = self.width / 10; const maxDisplace = self.width / 10;
self.maxDisplace = maxDisplace; self.maxDisplace = maxDisplace;
@ -77,11 +91,11 @@ export default class RadialNonoverlapForce {
const nodes = self.nodes; const nodes = self.nodes;
const disp = self.disp; const disp = self.disp;
const k = self.k; 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 }; disp[i] = { x: 0, y: 0 };
positions.forEach((u, j) => { positions.forEach((u: IPointTuple, j: number) => {
if (i === j) { if (i === j) {
return; return;
} }
@ -115,6 +129,7 @@ export default class RadialNonoverlapForce {
const speed = self.speed; const speed = self.speed;
const strictRadial = self.strictRadial; const strictRadial = self.strictRadial;
const f = self.focusID; const f = self.focusID;
const maxDisplace = self.maxDisplace || self.width / 10;
if (strictRadial) { if (strictRadial) {
disp.forEach((di, i) => { 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 // move
const radii = self.radii; const radii = self.radii;
positions.forEach((n, i) => { 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); const distLength = Math.sqrt(disp[i].x * disp[i].x + disp[i].y * disp[i].y);
if (distLength > 0 && i !== f) { 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[0] += (disp[i].x / distLength) * limitedDist;
n[1] += (disp[i].y / distLength) * limitedDist; n[1] += (disp[i].y / distLength) * limitedDist;
if (strictRadial) { if (strictRadial) {

View File

@ -11,11 +11,11 @@ import { BaseLayout } from './layout';
*/ */
export default class RandomLayout extends BaseLayout { 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() { public getDefaultCfg() {
return { return {
@ -38,7 +38,7 @@ export default class RandomLayout extends BaseLayout {
if (!self.height && typeof window !== 'undefined') { if (!self.height && typeof window !== 'undefined') {
self.height = window.innerHeight; self.height = window.innerHeight;
} }
nodes.forEach((node) => { nodes && nodes.forEach((node) => {
node.x = (Math.random() - 0.5) * layoutScale * self.width + center[0]; node.x = (Math.random() - 0.5) * layoutScale * self.width + center[0];
node.y = (Math.random() - 0.5) * layoutScale * self.height + center[1]; node.y = (Math.random() - 0.5) * layoutScale * self.height + center[1];
}); });

View File

@ -1,7 +1,7 @@
import Base, { IPluginBaseConfig } from '../base' import Base, { IPluginBaseConfig } from '../base'
import Edge from '../../item/edge'; import Edge from '../../item/edge';
import Graph from '../../graph/graph'; 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'; import { Point } from '@antv/g-base/lib/types';
interface BundlingConfig extends IPluginBaseConfig { interface BundlingConfig extends IPluginBaseConfig {
@ -90,7 +90,7 @@ export default class Bundling extends Base {
const edges = data.edges; const edges = data.edges;
const nodes = data.nodes; const nodes = data.nodes;
const nodeIdMap: NodeMapConfig = {}; const nodeIdMap: NodeConfigMap = {};
let error = false; let error = false;
nodes.forEach(node => { nodes.forEach(node => {
@ -186,7 +186,7 @@ export default class Bundling extends Base {
public divideEdges(divisions: number): Point[][] { public divideEdges(divisions: number): Point[][] {
const self = this; const self = this;
const edges: EdgeConfig[] = self.get('data').edges; const edges: EdgeConfig[] = self.get('data').edges;
const nodeIdMap: NodeMapConfig = self.get('nodeIdMap'); const nodeIdMap: NodeConfigMap = self.get('nodeIdMap');
let edgePoints = self.get('edgePoints'); let edgePoints = self.get('edgePoints');
if (!edgePoints || edgePoints === undefined) edgePoints = []; if (!edgePoints || edgePoints === undefined) edgePoints = [];
@ -264,7 +264,7 @@ export default class Bundling extends Base {
const edges = data.edges; const edges = data.edges;
const bundleThreshold: number = self.get('bundleThreshold'); const bundleThreshold: number = self.get('bundleThreshold');
const nodeIdMap: NodeMapConfig = self.get('nodeIdMap'); const nodeIdMap: NodeConfigMap = self.get('nodeIdMap');
let edgeBundles = self.get('edgeBundles'); let edgeBundles = self.get('edgeBundles');
if (!edgeBundles) 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 Node from '../item/node';
import { IGraph } from '../interface/graph'; import { IGraph } from '../interface/graph';
import { IEdge, INode } from '../interface/item'; import { IEdge, INode } from '../interface/item';
import { ILabelConfig } from '../interface/shape';
// Math types // Math types
export interface IPoint { export interface IPoint {
@ -117,7 +118,7 @@ export type ModelStyle = Partial<{
}; };
// loop edge config // loop edge config
loopCfg: LoopConfig; loopCfg: LoopConfig;
labelCfg?: object; labelCfg?: ILabelConfig;
anchorPoints: number[][]; anchorPoints: number[][];
controlPoints: IPoint[]; controlPoints: IPoint[];
size: number | number[]; size: number | number[];
@ -178,10 +179,7 @@ export interface ModelConfig extends ModelStyle {
// 节点或边的类型 // 节点或边的类型
type?: string; type?: string;
label?: string; label?: string;
labelCfg?: { labelCfg?: ILabelConfig;
style?: object;
[key: string]: unknown;
};
descriptionCfg?: { descriptionCfg?: {
style?: object; style?: object;
[key: string]: unknown; [key: string]: unknown;
@ -205,8 +203,10 @@ export interface ModelConfig extends ModelStyle {
endPoint?: IPoint; endPoint?: IPoint;
children?: TreeGraphData[]; children?: TreeGraphData[];
} }
export interface NodeConfig extends ModelConfig { export interface NodeConfig extends ModelConfig {
id: string; id: string;
size?: number | number[];
groupId?: string; groupId?: string;
description?: string; description?: string;
} }
@ -216,10 +216,7 @@ export interface EdgeConfig extends ModelConfig {
source?: string; source?: string;
target?: string; target?: string;
label?: string; label?: string;
labelCfg?: { labelCfg?: ILabelConfig;
style?: object;
[key: string]: unknown;
};
sourceNode?: Node; sourceNode?: Node;
targetNode?: Node; targetNode?: Node;
startPoint?: IPoint; startPoint?: IPoint;
@ -236,7 +233,11 @@ export type EdgeData = EdgeConfig & {
endPoint: IPoint; endPoint: IPoint;
}; };
export interface NodeMapConfig { export interface NodeMap {
[key: string]: INode;
}
export interface NodeConfigMap {
[key: string]: NodeConfig; [key: string]: NodeConfig;
} }
@ -347,7 +348,7 @@ export type BehaviorOpation<U> = {
export type IEvent = Record<G6Event, string>; export type IEvent = Record<G6Event, string>;
export interface IG6GraphEvent extends GraphEvent { export interface IG6GraphEvent extends GraphEvent {
item: Item; item: Item | null;
canvasX: number; canvasX: number;
canvasY: number; canvasY: number;
wheelDelta: number; wheelDelta: number;
@ -361,8 +362,19 @@ export type Item = INode | IEdge;
export type ITEM_TYPE = 'node' | 'edge' | 'group' export type ITEM_TYPE = 'node' | 'edge' | 'group'
export type NodeIdxMap = {
[key: string]: number
}
// 触发 viewportchange 事件的参数 // 触发 viewportchange 事件的参数
export interface ViewPortEventParam { export interface ViewPortEventParam {
action: string; action: string;
matrix: Matrix; 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 { IGroup } from '@antv/g-canvas/lib/interfaces';
import { mat3, transform, vec3 } from '@antv/matrix-util'; import { mat3, transform, vec3 } from '@antv/matrix-util';
import isArray from '@antv/util/lib/is-array' 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) 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 G6 from '../../../src';
import { NodeConfig, EdgeConfig } from '../../../src/types';
const div = document.createElement('div'); const div = document.createElement('div');
div.id = 'graph-spec'; div.id = 'graph-spec';
document.body.appendChild(div); document.body.appendChild(div);
const data: {nodes: object, edges: object} = { const data: {nodes: NodeConfig[], edges: EdgeConfig[]} = {
nodes: [ nodes: [
{ {
id: '0', id: '0',

View File

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