mirror of
https://gitee.com/antv/g6.git
synced 2024-12-02 19:58:46 +08:00
Merge branch 'master' of https://github.com/antvis/G6 into elint-fix-behavior
This commit is contained in:
commit
98864ecf6c
22
.eslintrc.js
22
.eslintrc.js
@ -1,12 +1,12 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@umijs/fabric/dist/eslint')],
|
||||
globals: {
|
||||
$: true,
|
||||
_: true,
|
||||
},
|
||||
rules: {
|
||||
'no-bitwise': 0,
|
||||
'import/order': 0,
|
||||
'no-plusplus': 0,
|
||||
},
|
||||
};
|
||||
extends: [require.resolve('@umijs/fabric/dist/eslint')],
|
||||
globals: {
|
||||
$: true,
|
||||
_: true,
|
||||
},
|
||||
rules: {
|
||||
'no-bitwise': 0,
|
||||
'import/order': 0,
|
||||
'no-plusplus': 0,
|
||||
},
|
||||
};
|
@ -1,3 +1,3 @@
|
||||
// window.g6 = require('./src/index.ts'); // import the source for debugging
|
||||
// window.g6 = require('./src/index.ts') // import the source for debugging
|
||||
window.g6 = require('./dist/g6.min.js') // import the package for webworker
|
||||
window.insertCss = require('insert-css');
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@antv/g6",
|
||||
"version": "3.3.0-beta.3",
|
||||
"version": "3.3.0-beta.5",
|
||||
"description": "A Graph Visualization Framework in JavaScript",
|
||||
"keywords": [
|
||||
"antv",
|
||||
@ -49,7 +49,7 @@
|
||||
"site:develop": "GATSBY=true gatsby develop --open",
|
||||
"start": "npm run site:develop",
|
||||
"test": "jest",
|
||||
"test-live": "DEBUG_MODE=1 jest --watch ./tests/unit/behavior/brush-select-spec.ts",
|
||||
"test-live": "DEBUG_MODE=1 jest --watch ./tests/unit/layout/web-worker-spec.ts",
|
||||
"lint-staged:js": "eslint --ext .js,.jsx,.ts,.tsx",
|
||||
"watch": "father build -w",
|
||||
"cdn": "antv-bin upload -n @antv/g6"
|
||||
|
@ -1,5 +1,5 @@
|
||||
export default {
|
||||
version: '3.3.0-beta.3',
|
||||
version: '3.3.0-beta.5',
|
||||
rootContainerClassName: 'root-container',
|
||||
nodeContainerClassName: 'node-container',
|
||||
edgeContainerClassName: 'edge-container',
|
||||
|
@ -5,8 +5,8 @@ import ShapeBase from '@antv/g-canvas/lib/shape/base';
|
||||
import each from '@antv/util/lib/each'
|
||||
import isNil from '@antv/util/lib/is-nil';
|
||||
import wrapBehavior from '@antv/util/lib/wrap-behavior';
|
||||
import { IGraph } from '../../interface/graph';
|
||||
import { IG6GraphEvent, Matrix } from '../../types';
|
||||
import Graph from "../graph"
|
||||
import { IG6GraphEvent, Matrix, Item } from '../../types';
|
||||
import { cloneEvent, isViewportChanged } from '../../util/base';
|
||||
|
||||
type Fun = () => void
|
||||
@ -35,14 +35,14 @@ const EVENTS = [
|
||||
'touchend',
|
||||
];
|
||||
export default class EventController {
|
||||
private graph: IGraph
|
||||
private graph: Graph
|
||||
private extendEvents: any[]
|
||||
private canvasHandler: Fun;
|
||||
private canvasHandler!: Fun;
|
||||
private dragging: boolean
|
||||
private preItem
|
||||
private preItem: Item | null = null
|
||||
public destroyed: boolean
|
||||
|
||||
constructor(graph: IGraph) {
|
||||
constructor(graph: Graph) {
|
||||
this.graph = graph
|
||||
this.extendEvents = []
|
||||
this.dragging = false
|
||||
@ -53,7 +53,7 @@ export default class EventController {
|
||||
// 初始化 G6 中的事件
|
||||
private initEvents() {
|
||||
const self = this
|
||||
const graph: IGraph = this.graph;
|
||||
const graph: Graph = this.graph;
|
||||
const canvas: Canvas = graph.get('canvas');
|
||||
const el = canvas.get('el');
|
||||
const extendEvents = this.extendEvents;
|
||||
@ -245,8 +245,8 @@ export default class EventController {
|
||||
|
||||
this.dragging = false
|
||||
this.preItem = null
|
||||
this.extendEvents.length = 0
|
||||
this.canvasHandler = null
|
||||
this.extendEvents.length = 0;
|
||||
(this.canvasHandler as Fun | null) = null
|
||||
this.destroyed = true
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import isString from '@antv/util/lib/is-string'
|
||||
import upperFirst from '@antv/util/lib/upper-first'
|
||||
import Edge from '../../item/edge';
|
||||
import Node from '../../item/node';
|
||||
import { EdgeConfig, Item, ITEM_TYPE, ModelConfig, NodeConfig, NodeMapConfig } from '../../types';
|
||||
import { EdgeConfig, Item, ITEM_TYPE, ModelConfig, NodeConfig, NodeMap } from '../../types';
|
||||
import Graph from '../graph';
|
||||
|
||||
import { IEdge, INode } from '../../interface/item';
|
||||
@ -36,12 +36,12 @@ export default class ItemController {
|
||||
* @returns {(Item)}
|
||||
* @memberof ItemController
|
||||
*/
|
||||
public addItem<T extends Item>(type: ITEM_TYPE, model: ModelConfig): T {
|
||||
public addItem<T extends Item>(type: ITEM_TYPE, model: ModelConfig) {
|
||||
const graph = this.graph;
|
||||
const parent: Group = graph.get(type + 'Group') || graph.get('group');
|
||||
const upperType = upperFirst(type);
|
||||
|
||||
let item;
|
||||
let item: Item | null = null;
|
||||
let styles = graph.get(type + upperFirst(STATE_SUFFIX)) || {};
|
||||
const defaultModel = graph.get(CFG_PREFIX + upperType);
|
||||
|
||||
@ -79,8 +79,8 @@ export default class ItemController {
|
||||
graph.emit('beforeadditem', { type, model });
|
||||
|
||||
if(type === EDGE) {
|
||||
let source: string | Item = (model as EdgeConfig).source
|
||||
let target: string | Item = (model as EdgeConfig).target
|
||||
let source: string | Item | undefined = (model as EdgeConfig).source
|
||||
let target: string | Item | undefined = (model as EdgeConfig).target
|
||||
|
||||
if (source && isString(source)) {
|
||||
source = graph.findById(source);
|
||||
@ -110,11 +110,13 @@ export default class ItemController {
|
||||
})
|
||||
}
|
||||
|
||||
graph.get(type + 's').push(item);
|
||||
graph.get('itemMap')[item.get('id')] = item;
|
||||
graph.autoPaint();
|
||||
graph.emit('afteradditem', { item, model });
|
||||
return item;
|
||||
if (item) {
|
||||
graph.get(type + 's').push(item);
|
||||
graph.get('itemMap')[item.get('id')] = item;
|
||||
graph.autoPaint();
|
||||
graph.emit('afteradditem', { item, model });
|
||||
return item as T;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -230,7 +232,7 @@ export default class ItemController {
|
||||
items.splice(index, 1);
|
||||
|
||||
const itemId: string = item.get('id')
|
||||
const itemMap: NodeMapConfig = graph.get('itemMap')
|
||||
const itemMap: NodeMap = graph.get('itemMap')
|
||||
delete itemMap[itemId];
|
||||
|
||||
if (type === NODE) {
|
||||
@ -276,7 +278,7 @@ export default class ItemController {
|
||||
* @param {string[]} states 状态名称集合
|
||||
* @memberof ItemController
|
||||
*/
|
||||
public clearItemStates(item: Item | string, states: string | string[]): void {
|
||||
public clearItemStates(item: Item | string, states?: string | string[]): void {
|
||||
const graph = this.graph;
|
||||
|
||||
if (isString(item)) {
|
||||
@ -354,7 +356,7 @@ export default class ItemController {
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.graph = null;
|
||||
(this.graph as Graph | null) = null;
|
||||
this.destroyed = true;
|
||||
}
|
||||
}
|
||||
|
@ -4,9 +4,10 @@ import isString from '@antv/util/lib/is-string'
|
||||
import Behavior from '../../behavior/behavior'
|
||||
import { IBehavior } from '../../interface/behavior';
|
||||
import { IGraph, IMode, IModeType } from '../../interface/graph';
|
||||
import Graph from '../graph';
|
||||
|
||||
export default class ModeController {
|
||||
private graph: IGraph
|
||||
private graph: Graph
|
||||
public destroyed: boolean
|
||||
/**
|
||||
* modes = {
|
||||
@ -32,7 +33,7 @@ export default class ModeController {
|
||||
*/
|
||||
public mode: string
|
||||
private currentBehaves: IBehavior[]
|
||||
constructor(graph: IGraph) {
|
||||
constructor(graph: Graph) {
|
||||
this.graph = graph
|
||||
this.destroyed = false
|
||||
this.modes = graph.get('modes') || {
|
||||
@ -62,7 +63,7 @@ export default class ModeController {
|
||||
const behaviors = this.modes[mode];
|
||||
const behaves: IBehavior[] = [];
|
||||
let behave: IBehavior;
|
||||
each(behaviors, behavior => {
|
||||
each(behaviors || [], behavior => {
|
||||
const BehaviorInstance = Behavior.getBehavior(behavior.type)
|
||||
if (!BehaviorInstance) {
|
||||
return;
|
||||
@ -70,14 +71,14 @@ export default class ModeController {
|
||||
|
||||
behave = new BehaviorInstance(behavior);
|
||||
if(behave) {
|
||||
behave.bind(graph)
|
||||
behave.bind(graph as IGraph)
|
||||
behaves.push(behave);
|
||||
}
|
||||
});
|
||||
this.currentBehaves = behaves;
|
||||
}
|
||||
|
||||
private mergeBehaviors(modeBehaviors: IModeType[], behaviors): IModeType[] {
|
||||
private mergeBehaviors(modeBehaviors: IModeType[], behaviors: IModeType[]): IModeType[] {
|
||||
each(behaviors, behavior => {
|
||||
if (modeBehaviors.indexOf(behavior) < 0) {
|
||||
if (isString(behavior)) {
|
||||
@ -89,7 +90,7 @@ export default class ModeController {
|
||||
return modeBehaviors;
|
||||
}
|
||||
|
||||
private filterBehaviors(modeBehaviors: IModeType[], behaviors): IModeType[] {
|
||||
private filterBehaviors(modeBehaviors: IModeType[], behaviors: IModeType[]): IModeType[] {
|
||||
const result: IModeType[] = [];
|
||||
modeBehaviors.forEach(behavior => {
|
||||
let type: string = ''
|
||||
@ -105,7 +106,7 @@ export default class ModeController {
|
||||
return result;
|
||||
}
|
||||
|
||||
public setMode(mode: string): ModeController {
|
||||
public setMode(mode: string) {
|
||||
const modes = this.modes;
|
||||
const graph = this.graph;
|
||||
const current = mode
|
||||
@ -143,22 +144,24 @@ export default class ModeController {
|
||||
*/
|
||||
public manipulateBehaviors(behaviors: IModeType[] | IModeType, modes: string[] | string, isAdd: boolean): ModeController {
|
||||
const self = this
|
||||
let behaves = behaviors
|
||||
let behaves: IModeType[]
|
||||
if(!isArray(behaviors)) {
|
||||
behaves = [ behaviors ]
|
||||
}else {
|
||||
behaves = behaviors
|
||||
}
|
||||
|
||||
if(isArray(modes)) {
|
||||
each(modes, mode => {
|
||||
if (!self.modes[mode]) {
|
||||
if (isAdd) {
|
||||
self.modes[mode] = [].concat(behaves);
|
||||
self.modes[mode] = behaves
|
||||
}
|
||||
} else {
|
||||
if (isAdd) {
|
||||
self.modes[mode] = this.mergeBehaviors(self.modes[mode], behaves);
|
||||
self.modes[mode] = this.mergeBehaviors(self.modes[mode] || [], behaves);
|
||||
} else {
|
||||
self.modes[mode] = this.filterBehaviors(self.modes[mode], behaves);
|
||||
self.modes[mode] = this.filterBehaviors(self.modes[mode] || [], behaves);
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -173,14 +176,14 @@ export default class ModeController {
|
||||
|
||||
if(!this.modes[currentMode]) {
|
||||
if (isAdd) {
|
||||
self.modes[currentMode] = [].concat(behaves);
|
||||
self.modes[currentMode] = behaves
|
||||
}
|
||||
}
|
||||
|
||||
if (isAdd) {
|
||||
self.modes[currentMode] = this.mergeBehaviors(self.modes[currentMode], behaves);
|
||||
self.modes[currentMode] = this.mergeBehaviors(self.modes[currentMode] || [], behaves);
|
||||
} else {
|
||||
self.modes[currentMode] = this.filterBehaviors(self.modes[currentMode], behaves);
|
||||
self.modes[currentMode] = this.filterBehaviors(self.modes[currentMode] || [], behaves);
|
||||
}
|
||||
|
||||
self.setMode(this.mode)
|
||||
@ -189,9 +192,9 @@ export default class ModeController {
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.graph = null;
|
||||
this.modes = null;
|
||||
this.currentBehaves = null;
|
||||
(this.graph as Graph | null) = null;
|
||||
(this.modes as IMode | null) = null;
|
||||
(this.currentBehaves as IBehavior[] | null) = null;
|
||||
this.destroyed = true;
|
||||
}
|
||||
}
|
@ -2,21 +2,25 @@ import each from '@antv/util/lib/each'
|
||||
import isString from '@antv/util/lib/is-string'
|
||||
import { IGraph, IStates } from '../../interface/graph';
|
||||
import { Item } from '../../types';
|
||||
import Graph from '../graph';
|
||||
import { INode } from '../../interface/item';
|
||||
|
||||
interface ICachedStates {
|
||||
enabled: IStates;
|
||||
disabled: IStates;
|
||||
}
|
||||
|
||||
let timer = null
|
||||
|
||||
let timer: number | null = null
|
||||
const TIME_OUT = 16;
|
||||
|
||||
|
||||
export default class StateController {
|
||||
private graph: IGraph
|
||||
private graph: Graph
|
||||
private cachedStates: ICachedStates
|
||||
public destroyed: boolean
|
||||
|
||||
constructor(graph: IGraph) {
|
||||
constructor(graph: Graph) {
|
||||
this.graph = graph
|
||||
/**
|
||||
* this.cachedStates = {
|
||||
@ -43,7 +47,7 @@ export default class StateController {
|
||||
* @returns
|
||||
* @memberof State
|
||||
*/
|
||||
private checkCache(item: Item, state: string, cache: object) {
|
||||
private checkCache(item: Item, state: string, cache: { [key: string]: any }) {
|
||||
if (!cache[state]) {
|
||||
return;
|
||||
}
|
||||
@ -62,11 +66,11 @@ export default class StateController {
|
||||
* @param {object} states
|
||||
* @memberof State
|
||||
*/
|
||||
private cacheState(item: Item, state: string, states: object) {
|
||||
private cacheState(item: Item, state: string, states: IStates) {
|
||||
if (!states[state]) {
|
||||
states[state] = [];
|
||||
}
|
||||
states[state].push(item);
|
||||
states[state].push(item as INode);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -132,7 +136,7 @@ export default class StateController {
|
||||
* @memberof State
|
||||
*/
|
||||
public updateGraphStates() {
|
||||
const states = this.graph.get('states')
|
||||
const states = this.graph.get('states') as IStates
|
||||
const cachedStates = this.cachedStates;
|
||||
|
||||
each(cachedStates.disabled, (val, key) => {
|
||||
@ -143,22 +147,22 @@ export default class StateController {
|
||||
}
|
||||
});
|
||||
|
||||
each(cachedStates.enabled, (val, key) => {
|
||||
each(cachedStates.enabled, (val: INode[], key) => {
|
||||
if (!states[key]) {
|
||||
states[key] = val;
|
||||
} else {
|
||||
const map = {};
|
||||
const map: { [key: string]: boolean } = {};
|
||||
states[key].forEach(item => {
|
||||
if (!item.destroyed) {
|
||||
map[item.get('id')] = true;
|
||||
}
|
||||
});
|
||||
val.forEach(item => {
|
||||
val.forEach((item:Item) => {
|
||||
if (!item.destroyed) {
|
||||
const id = item.get('id');
|
||||
if (!map[id]) {
|
||||
map[id] = true;
|
||||
states[key].push(item);
|
||||
states[key].push(item as INode);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -173,8 +177,8 @@ export default class StateController {
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.graph = null;
|
||||
this.cachedStates = null;
|
||||
(this.graph as Graph | null) = null;
|
||||
(this.cachedStates as ICachedStates | null) = null;
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
|
@ -7,11 +7,12 @@ import { IGraph } from "../../interface/graph";
|
||||
import { Item, Matrix, Padding } from '../../types';
|
||||
import { formatPadding } from '../../util/base'
|
||||
import { applyMatrix, invertMatrix } from '../../util/math';
|
||||
import Graph from '../graph';
|
||||
|
||||
export default class ViewController {
|
||||
private graph: IGraph = null
|
||||
private graph: Graph
|
||||
public destroyed: boolean = false
|
||||
constructor(graph: IGraph) {
|
||||
constructor(graph: Graph) {
|
||||
this.graph = graph
|
||||
this.destroyed = false
|
||||
}
|
||||
@ -53,7 +54,7 @@ export default class ViewController {
|
||||
}
|
||||
|
||||
public getFormatPadding(): number[] {
|
||||
const padding = this.graph.get<Padding>('fitViewPadding')
|
||||
const padding = this.graph.get('fitViewPadding') as Padding
|
||||
return formatPadding(padding)
|
||||
}
|
||||
|
||||
@ -91,7 +92,7 @@ export default class ViewController {
|
||||
* @param x 视口 x 坐标
|
||||
* @param y 视口 y 坐标
|
||||
*/
|
||||
public getClientByPoint(x, y): Point {
|
||||
public getClientByPoint(x: number, y: number): Point {
|
||||
const canvas: Canvas = this.graph.get('canvas');
|
||||
const canvasPoint = this.getCanvasByPoint(x, y);
|
||||
const point = canvas.getClientByPoint(canvasPoint.x, canvasPoint.y);
|
||||
@ -104,7 +105,7 @@ export default class ViewController {
|
||||
* @param x 视口 x 坐标
|
||||
* @param y 视口 y 坐标
|
||||
*/
|
||||
public getCanvasByPoint(x, y): Point {
|
||||
public getCanvasByPoint(x: number, y: number): Point {
|
||||
const viewportMatrix: Matrix = this.graph.get('group').getMatrix();
|
||||
return applyMatrix({ x, y }, viewportMatrix);
|
||||
}
|
||||
@ -146,7 +147,7 @@ export default class ViewController {
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.graph = null
|
||||
(this.graph as Graph | null) = null
|
||||
this.destroyed = false
|
||||
}
|
||||
}
|
@ -10,7 +10,7 @@ import each from '@antv/util/lib/each';
|
||||
import isPlainObject from '@antv/util/lib/is-plain-object';
|
||||
import isString from '@antv/util/lib/is-string';
|
||||
import { GraphAnimateConfig, GraphOptions, IGraph, IModeOption, IModeType, IStates } from '../interface/graph';
|
||||
import { IEdge, INode } from '../interface/item';
|
||||
import { IEdge, INode, IItemBase } from '../interface/item';
|
||||
import {
|
||||
EdgeConfig,
|
||||
GraphData,
|
||||
@ -20,7 +20,7 @@ import {
|
||||
Matrix,
|
||||
ModelConfig,
|
||||
NodeConfig,
|
||||
NodeMapConfig,
|
||||
NodeMap,
|
||||
Padding,
|
||||
TreeGraphData,
|
||||
} from '../types';
|
||||
@ -37,6 +37,7 @@ import {
|
||||
StateController,
|
||||
ViewController,
|
||||
} from './controller';
|
||||
import PluginBase from "../plugins/base"
|
||||
|
||||
const NODE = 'node';
|
||||
const EDGE = 'edge';
|
||||
@ -57,13 +58,13 @@ export interface PrivateGraphOption extends GraphOptions {
|
||||
|
||||
groups: GroupConfig[];
|
||||
|
||||
itemMap: NodeMapConfig;
|
||||
itemMap: NodeMap;
|
||||
|
||||
callback: () => void;
|
||||
|
||||
groupBBoxs: IGroupBBox;
|
||||
|
||||
groupNodes: NodeMapConfig;
|
||||
groupNodes: NodeMap;
|
||||
|
||||
/**
|
||||
* 格式:
|
||||
@ -77,7 +78,7 @@ export interface PrivateGraphOption extends GraphOptions {
|
||||
|
||||
export default class Graph extends EventEmitter implements IGraph {
|
||||
private animating: boolean;
|
||||
private _cfg: GraphOptions;
|
||||
private _cfg: GraphOptions & { [key: string]: any };
|
||||
public destroyed: boolean;
|
||||
|
||||
constructor(cfg: GraphOptions) {
|
||||
@ -114,7 +115,7 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
}
|
||||
|
||||
private initCanvas() {
|
||||
let container: string | HTMLElement = this.get('container');
|
||||
let container: string | HTMLElement | null = this.get('container');
|
||||
if (isString(container)) {
|
||||
container = document.getElementById(container);
|
||||
this.set('container', container);
|
||||
@ -186,7 +187,7 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
this.set('group', group);
|
||||
}
|
||||
|
||||
public getDefaultCfg(): PrivateGraphOption {
|
||||
public getDefaultCfg(): Partial<PrivateGraphOption> {
|
||||
return {
|
||||
/**
|
||||
* Container could be dom object or dom id
|
||||
@ -315,7 +316,7 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
/**
|
||||
* 帧回调函数,用于自定义节点运动路径,为空时线性运动
|
||||
*/
|
||||
onFrame: null,
|
||||
onFrame: undefined,
|
||||
/**
|
||||
* 动画时长(ms)
|
||||
*/
|
||||
@ -325,7 +326,7 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
*/
|
||||
easing: 'easeLinear',
|
||||
},
|
||||
callback: null,
|
||||
callback: undefined,
|
||||
/**
|
||||
* group类型
|
||||
*/
|
||||
@ -441,18 +442,18 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
* @param {(item: T, index: number) => T} fn 指定规则
|
||||
* @return {T} 元素实例
|
||||
*/
|
||||
public find<T extends Item>(type: ITEM_TYPE, fn: (item: T, index?: number) => boolean): T {
|
||||
let result;
|
||||
public find<T extends Item>(type: ITEM_TYPE, fn: (item: T, index?: number) => boolean): T | undefined {
|
||||
let result: T | undefined;
|
||||
const items = this.get(type + 's');
|
||||
|
||||
each(items, (item, i) => {
|
||||
if (fn(item, i)) {
|
||||
result = item;
|
||||
return false;
|
||||
return result
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
@ -462,7 +463,7 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
* @return {array} 元素实例
|
||||
*/
|
||||
public findAll<T extends Item>(type: ITEM_TYPE, fn: (item: T, index?: number) => boolean): T[] {
|
||||
const result = [];
|
||||
const result: T[] = [];
|
||||
|
||||
each(this.get(type + 's'), (item, i) => {
|
||||
if (fn(item, i)) {
|
||||
@ -739,7 +740,7 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
* @param {ModelConfig} model 元素数据模型
|
||||
* @return {Item} 元素实例
|
||||
*/
|
||||
public addItem(type: ITEM_TYPE, model: ModelConfig): Item {
|
||||
public addItem(type: ITEM_TYPE, model: ModelConfig) {
|
||||
if (type === 'group') {
|
||||
const { groupId, nodes, type: groupType, zIndex, title } = model;
|
||||
let groupTitle = title;
|
||||
@ -813,23 +814,25 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
throw new Error('data must be defined first');
|
||||
}
|
||||
|
||||
const { nodes = [], edges = [] } = data;
|
||||
|
||||
this.clear();
|
||||
|
||||
this.emit('beforerender');
|
||||
const autoPaint = this.get('autoPaint');
|
||||
this.setAutoPaint(false);
|
||||
|
||||
each(data.nodes, (node: NodeConfig) => {
|
||||
each(nodes, (node: NodeConfig) => {
|
||||
self.add('node', node);
|
||||
});
|
||||
|
||||
each(data.edges, (edge: EdgeConfig) => {
|
||||
each(edges, (edge: EdgeConfig) => {
|
||||
self.add('edge', edge);
|
||||
});
|
||||
|
||||
if (!this.get('groupByTypes')) {
|
||||
// 为提升性能,选择数量少的进行操作
|
||||
if (data.nodes.length < data.edges.length) {
|
||||
if (data.nodes && data.edges && data.nodes.length < data.edges.length) {
|
||||
const nodes = this.getNodes();
|
||||
|
||||
// 遍历节点实例,将所有节点提前。
|
||||
@ -883,12 +886,12 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
this.data(data);
|
||||
this.render();
|
||||
}
|
||||
|
||||
|
||||
// 比较item
|
||||
private diffItems(type: ITEM_TYPE, items, models: NodeConfig[] | EdgeConfig[]) {
|
||||
private diffItems(type: ITEM_TYPE, items: { nodes: INode[], edges: IEdge[]}, models: NodeConfig[] | EdgeConfig[]) {
|
||||
const self = this;
|
||||
let item;
|
||||
const itemMap: NodeMapConfig = this.get('itemMap');
|
||||
let item: INode;
|
||||
const itemMap: NodeMap = this.get('itemMap');
|
||||
|
||||
each(models, (model) => {
|
||||
item = itemMap[model.id];
|
||||
@ -906,7 +909,7 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
} else {
|
||||
item = self.addItem(type, model);
|
||||
}
|
||||
items[type + 's'].push(item);
|
||||
(items as { [key:string]: any[]})[type + 's'].push(item);
|
||||
});
|
||||
}
|
||||
|
||||
@ -928,19 +931,22 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
}
|
||||
|
||||
const autoPaint: boolean = this.get('autoPaint');
|
||||
const itemMap: NodeMapConfig = this.get('itemMap');
|
||||
const itemMap: NodeMap = this.get('itemMap');
|
||||
|
||||
const items = {
|
||||
const items: {
|
||||
nodes: INode[],
|
||||
edges: IEdge[]
|
||||
} = {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
};
|
||||
|
||||
this.setAutoPaint(false);
|
||||
|
||||
this.diffItems('node', items, (data as GraphData).nodes);
|
||||
this.diffItems('edge', items, (data as GraphData).edges);
|
||||
this.diffItems('node', items, (data as GraphData).nodes!);
|
||||
this.diffItems('edge', items, (data as GraphData).edges!);
|
||||
|
||||
each(itemMap, (item: INode, id: number) => {
|
||||
each(itemMap, (item: INode & IEdge, id: number) => {
|
||||
if (items.nodes.indexOf(item) < 0 && items.edges.indexOf(item) < 0) {
|
||||
delete itemMap[id];
|
||||
self.remove(item);
|
||||
@ -969,7 +975,7 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
* @param {string} groupType group类型
|
||||
*/
|
||||
public renderCustomGroup(data: GraphData, groupType: string) {
|
||||
const { groups, nodes } = data;
|
||||
const { groups, nodes = [] } = data;
|
||||
|
||||
// 第一种情况,,不存在groups,则不存在嵌套群组
|
||||
let groupIndex = 10;
|
||||
@ -977,7 +983,7 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
// 存在单个群组
|
||||
// 获取所有有groupID的node
|
||||
const nodeInGroup = nodes.filter((node) => node.groupId);
|
||||
const groupsArr = [];
|
||||
const groupsArr: GroupConfig[] = [];
|
||||
// 根据groupID分组
|
||||
const groupIds = groupBy(nodeInGroup, 'groupId');
|
||||
// tslint:disable-next-line:forin
|
||||
@ -1020,10 +1026,10 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
* @return {object} data
|
||||
*/
|
||||
public save(): TreeGraphData | GraphData {
|
||||
const nodes = [];
|
||||
const edges = [];
|
||||
const nodes: NodeConfig[] = [];
|
||||
const edges: EdgeConfig[] = [];
|
||||
each(this.get('nodes'), (node: INode) => {
|
||||
nodes.push(node.getModel());
|
||||
nodes.push(node.getModel() as NodeConfig);
|
||||
});
|
||||
|
||||
each(this.get('edges'), (edge: IEdge) => {
|
||||
@ -1123,7 +1129,7 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
const canvas: GCanvas = self.get('canvas');
|
||||
|
||||
canvas.animate(
|
||||
(ratio) => {
|
||||
(ratio: number) => {
|
||||
each(toNodes, (data) => {
|
||||
const node: Item = self.findById(data.id);
|
||||
|
||||
@ -1188,7 +1194,7 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
|
||||
each(nodes, (node: INode) => {
|
||||
model = node.getModel() as NodeConfig;
|
||||
node.updatePosition({ x: model.x, y: model.y });
|
||||
node.updatePosition({ x: model.x!, y: model.y! });
|
||||
});
|
||||
|
||||
each(edges, (edge: IEdge) => {
|
||||
@ -1280,7 +1286,12 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
if (typeof window !== 'undefined') {
|
||||
if (window.Blob && window.URL) {
|
||||
const arr = dataURL.split(',');
|
||||
const mime = arr[0].match(/:(.*?);/)[1];
|
||||
let mime = ""
|
||||
if (arr && arr.length > 0 ) {
|
||||
const match = arr[0].match(/:(.*?);/);
|
||||
if (match && match.length >= 2) mime = match[1]
|
||||
}
|
||||
|
||||
const bstr = atob(arr[1]);
|
||||
let n = bstr.length;
|
||||
const u8arr = new Uint8Array(n);
|
||||
@ -1315,7 +1326,7 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
* 若 cfg 含有 type 字段或为 String 类型,且与现有布局方法不同,则更换布局
|
||||
* 若 cfg 不包括 type ,则保持原有布局方法,仅更新布局配置项
|
||||
*/
|
||||
public updateLayout(cfg): void {
|
||||
public updateLayout(cfg: any): void {
|
||||
const layoutController = this.get('layoutController');
|
||||
let newLayoutType;
|
||||
|
||||
@ -1388,7 +1399,7 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
* 添加插件
|
||||
* @param {object} plugin 插件实例
|
||||
*/
|
||||
public addPlugin(plugin): void {
|
||||
public addPlugin(plugin: PluginBase): void {
|
||||
const self = this;
|
||||
if (plugin.destroyed) {
|
||||
return;
|
||||
@ -1401,7 +1412,7 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
* 添加插件
|
||||
* @param {object} plugin 插件实例
|
||||
*/
|
||||
public removePlugin(plugin): void {
|
||||
public removePlugin(plugin: PluginBase): void {
|
||||
const plugins = this.get('plugins');
|
||||
const index = plugins.indexOf(plugin);
|
||||
if (index >= 0) {
|
||||
@ -1428,7 +1439,7 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
this.get('layoutController').destroy();
|
||||
this.get('customGroupControll').destroy();
|
||||
this.get('canvas').destroy();
|
||||
this._cfg = null;
|
||||
(this._cfg as any) = null;
|
||||
this.destroyed = true;
|
||||
}
|
||||
}
|
||||
|
@ -38,13 +38,13 @@ export default class TreeGraph extends Graph implements ITreeGraph {
|
||||
layout.direction = 'TB';
|
||||
}
|
||||
if (layout.radial) {
|
||||
return function(data) {
|
||||
return (data: any) => {
|
||||
const layoutData = Hierarchy[layout.type](data, layout);
|
||||
radialLayout(layoutData);
|
||||
return layoutData;
|
||||
};
|
||||
}
|
||||
return function(data) {
|
||||
return (data: any) => {
|
||||
return Hierarchy[layout.type](data, layout);
|
||||
};
|
||||
}
|
||||
@ -65,7 +65,7 @@ export default class TreeGraph extends Graph implements ITreeGraph {
|
||||
return index;
|
||||
}
|
||||
|
||||
public getDefaultCfg(): PrivateGraphOption {
|
||||
public getDefaultCfg(): Partial<PrivateGraphOption> {
|
||||
const cfg = super.getDefaultCfg();
|
||||
// 树图默认打开动画
|
||||
cfg.animate = true;
|
||||
@ -78,14 +78,18 @@ export default class TreeGraph extends Graph implements ITreeGraph {
|
||||
* @param parent 父节点实例
|
||||
* @param animate 是否开启动画
|
||||
*/
|
||||
private innerAddChild(treeData: TreeGraphData, parent: Item, animate: boolean): Item {
|
||||
private innerAddChild(treeData: TreeGraphData, parent: Item | undefined, animate: boolean): Item {
|
||||
const self = this;
|
||||
const model = treeData.data;
|
||||
// model 中应存储真实的数据,特别是真实的 children
|
||||
model.x = treeData.x;
|
||||
model.y = treeData.y;
|
||||
model.depth = treeData.depth;
|
||||
const node = self.addItem('node', model);
|
||||
|
||||
if (model) {
|
||||
// model 中应存储真实的数据,特别是真实的 children
|
||||
model.x = treeData.x;
|
||||
model.y = treeData.y;
|
||||
model.depth = treeData.depth;
|
||||
}
|
||||
|
||||
const node = self.addItem('node', model!);
|
||||
if (parent) {
|
||||
node.set('parent', parent);
|
||||
if (animate) {
|
||||
@ -113,7 +117,7 @@ export default class TreeGraph extends Graph implements ITreeGraph {
|
||||
});
|
||||
}
|
||||
// 渲染到视图上应参考布局的children, 避免多绘制了收起的节点
|
||||
each(treeData.children, child => {
|
||||
each(treeData.children || [], child => {
|
||||
self.innerAddChild(child, node, animate);
|
||||
});
|
||||
return node;
|
||||
@ -125,7 +129,7 @@ export default class TreeGraph extends Graph implements ITreeGraph {
|
||||
* @param parent
|
||||
* @param animate
|
||||
*/
|
||||
private innerUpdateChild(data: TreeGraphData, parent: Item, animate: boolean) {
|
||||
private innerUpdateChild(data: TreeGraphData, parent: Item | undefined, animate: boolean) {
|
||||
const self = this;
|
||||
const current = self.findById(data.id);
|
||||
|
||||
@ -136,7 +140,7 @@ export default class TreeGraph extends Graph implements ITreeGraph {
|
||||
}
|
||||
|
||||
// 更新新节点下所有子节点
|
||||
each(data.children, (child: TreeGraphData) => {
|
||||
each(data.children || [], (child: TreeGraphData) => {
|
||||
self.innerUpdateChild(child, current, animate);
|
||||
});
|
||||
|
||||
@ -148,10 +152,10 @@ export default class TreeGraph extends Graph implements ITreeGraph {
|
||||
for (let i = children.length - 1; i >= 0; i--) {
|
||||
const child = children[i].getModel();
|
||||
|
||||
if (self.indexOfChild(data.children, child.id) === -1) {
|
||||
if (self.indexOfChild(data.children || [], child.id) === -1) {
|
||||
self.innerRemoveChild(child.id, {
|
||||
x: data.x,
|
||||
y: data.y
|
||||
x: data.x!,
|
||||
y: data.y!
|
||||
}, animate);
|
||||
|
||||
// 更新父节点下缓存的子节点 item 实例列表
|
||||
@ -169,7 +173,7 @@ export default class TreeGraph extends Graph implements ITreeGraph {
|
||||
});
|
||||
}
|
||||
current.set('model', data.data);
|
||||
current.updatePosition({ x: data.x, y: data.y });
|
||||
current.updatePosition({ x: data.x!, y: data.y! });
|
||||
}
|
||||
|
||||
/**
|
||||
@ -218,7 +222,7 @@ export default class TreeGraph extends Graph implements ITreeGraph {
|
||||
* 更改并应用树布局算法
|
||||
* @param {object} layout 布局算法
|
||||
*/
|
||||
public updateLayout(layout) {
|
||||
public updateLayout(layout: any) {
|
||||
const self = this;
|
||||
if (!layout) {
|
||||
console.warn('layout cannot be null');
|
||||
@ -246,7 +250,7 @@ export default class TreeGraph extends Graph implements ITreeGraph {
|
||||
|
||||
self.setAutoPaint(false);
|
||||
|
||||
self.innerUpdateChild(layoutData, null, animate);
|
||||
self.innerUpdateChild(layoutData, undefined, animate);
|
||||
|
||||
if (fitView) {
|
||||
const viewController: ViewController = self.get('viewController')
|
||||
@ -258,7 +262,7 @@ export default class TreeGraph extends Graph implements ITreeGraph {
|
||||
self.refresh();
|
||||
self.paint();
|
||||
} else {
|
||||
self.layoutAnimate(layoutData, null);
|
||||
self.layoutAnimate(layoutData);
|
||||
}
|
||||
self.setAutoPaint(autoPaint);
|
||||
self.emit('afterrefreshlayout', { data, layoutData });
|
||||
@ -276,13 +280,15 @@ export default class TreeGraph extends Graph implements ITreeGraph {
|
||||
parent = parent.get('id') as string;
|
||||
}
|
||||
|
||||
const parentData = self.findDataById(parent);
|
||||
|
||||
if (!parentData.children) {
|
||||
parentData.children = [];
|
||||
const parentData = self.findDataById(parent)
|
||||
|
||||
if (parentData) {
|
||||
if (!parentData.children) {
|
||||
parentData.children = [];
|
||||
}
|
||||
parentData.children.push(data);
|
||||
self.changeData();
|
||||
}
|
||||
parentData.children.push(data);
|
||||
self.changeData();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -302,12 +308,14 @@ export default class TreeGraph extends Graph implements ITreeGraph {
|
||||
const parentModel = self.findById(parent).getModel();
|
||||
|
||||
const current = self.findById(data.id);
|
||||
|
||||
if (!parentModel.children) {
|
||||
// 当 current 不存在时,children 为空数组
|
||||
parentModel.children = [];
|
||||
}
|
||||
|
||||
// 如果不存在该节点,则添加
|
||||
if (!current) {
|
||||
if (!parentModel.children) {
|
||||
// 当 current 不存在时,children 为空数组
|
||||
parentModel.children = [];
|
||||
}
|
||||
parentModel.children.push(data);
|
||||
} else {
|
||||
const index = self.indexOfChild(parentModel.children, data.id);
|
||||
@ -330,7 +338,8 @@ export default class TreeGraph extends Graph implements ITreeGraph {
|
||||
|
||||
const parent = node.get('parent');
|
||||
if (parent && !parent.destroyed) {
|
||||
const siblings = self.findDataById(parent.get('id')).children;
|
||||
const parentNode = self.findDataById(parent.get('id'));
|
||||
const siblings = (parentNode && parentNode.children) || [];
|
||||
const model: NodeConfig = node.getModel() as NodeConfig
|
||||
|
||||
const index = self.indexOfChild(siblings, model.id);
|
||||
@ -345,19 +354,19 @@ export default class TreeGraph extends Graph implements ITreeGraph {
|
||||
* @param {TreeGraphData | undefined} parent 从哪个节点开始寻找,为空时从根节点开始查找
|
||||
* @return {TreeGraphData} 对应源数据
|
||||
*/
|
||||
public findDataById(id: string, parent?: TreeGraphData | undefined): TreeGraphData {
|
||||
public findDataById(id: string, parent?: TreeGraphData | undefined): TreeGraphData | null {
|
||||
const self = this;
|
||||
|
||||
if (!parent) {
|
||||
parent = self.get('data');
|
||||
parent = self.get('data') as TreeGraphData;
|
||||
}
|
||||
|
||||
if (id === parent.id) {
|
||||
return parent;
|
||||
}
|
||||
|
||||
let result = null;
|
||||
each(parent.children, child => {
|
||||
let result: TreeGraphData | null = null;
|
||||
each(parent.children || [], child => {
|
||||
if (child.id === id) {
|
||||
result = child;
|
||||
return false;
|
||||
@ -389,7 +398,7 @@ export default class TreeGraph extends Graph implements ITreeGraph {
|
||||
}
|
||||
});
|
||||
|
||||
this.get('canvas').animate(ratio => {
|
||||
this.get('canvas').animate((ratio: number) => {
|
||||
traverseTree<TreeGraphData>(data, child => {
|
||||
const node = self.findById(child.id);
|
||||
|
||||
@ -410,8 +419,8 @@ export default class TreeGraph extends Graph implements ITreeGraph {
|
||||
const attrs = onFrame(node, ratio, origin, data);
|
||||
node.set('model', Object.assign(model, attrs));
|
||||
} else {
|
||||
model.x = origin.x + (child.x - origin.x) * ratio;
|
||||
model.y = origin.y + (child.y - origin.y) * ratio;
|
||||
model.x = origin.x + (child.x! - origin.x) * ratio;
|
||||
model.y = origin.y + (child.y! - origin.y) * ratio;
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
@ -22,8 +22,8 @@ export class G6GraphEvent extends GraphEvent implements IG6GraphEvent {
|
||||
public canvasY: number
|
||||
public wheelDelta: number
|
||||
public detail: number
|
||||
public target: Item & Canvas;
|
||||
constructor(type, event) {
|
||||
public target!: Item & Canvas;
|
||||
constructor(type: string, event: IG6GraphEvent) {
|
||||
super(type, event)
|
||||
this.item = event.item
|
||||
this.canvasX = event.canvasX
|
||||
|
@ -1,8 +1,9 @@
|
||||
import EventEmitter from '@antv/event-emitter';
|
||||
import { AnimateCfg, Point } from '@antv/g-base/lib/types';
|
||||
import Graph from '../graph/graph';
|
||||
import { EdgeConfig, GraphData, IG6GraphEvent, Item, ITEM_TYPE, ModelConfig, ModelStyle, NodeConfig, Padding, ShapeStyle, TreeGraphData } from '../types';
|
||||
import { EdgeConfig, GraphData, IG6GraphEvent, Item, ITEM_TYPE, ModelConfig, ModelStyle, NodeConfig, Padding, ShapeStyle, TreeGraphData, LayoutConfig } from '../types';
|
||||
import { IEdge, INode } from './item';
|
||||
import PluginBase from '../plugins/base';
|
||||
|
||||
export interface IModeOption {
|
||||
type: string;
|
||||
@ -162,7 +163,7 @@ export interface IStates {
|
||||
[key: string]: INode[]
|
||||
}
|
||||
export interface IGraph extends EventEmitter {
|
||||
getDefaultCfg(): GraphOptions;
|
||||
getDefaultCfg(): Partial<GraphOptions>;
|
||||
get<T = any>(key: string): T;
|
||||
set<T = any>(key: string | object, value?: T): Graph;
|
||||
findById(id: string): Item;
|
||||
@ -445,7 +446,7 @@ export interface IGraph extends EventEmitter {
|
||||
* @param {(item: T, index: number) => T} fn 指定规则
|
||||
* @return {T} 元素实例
|
||||
*/
|
||||
find<T extends Item>(type: ITEM_TYPE, fn: (item: T, index: number) => boolean): T;
|
||||
find<T extends Item>(type: ITEM_TYPE, fn: (item: T, index?: number) => boolean): T | undefined;
|
||||
|
||||
/**
|
||||
* 查找所有满足规则的元素
|
||||
@ -453,7 +454,7 @@ export interface IGraph extends EventEmitter {
|
||||
* @param {string} fn 指定规则
|
||||
* @return {array} 元素实例
|
||||
*/
|
||||
findAll<T extends Item>(type: ITEM_TYPE, fn: (item: T, index: number) => boolean): T[];
|
||||
findAll<T extends Item>(type: ITEM_TYPE, fn: (item: T, index?: number) => boolean): T[];
|
||||
|
||||
/**
|
||||
* 查找所有处于指定状态的元素
|
||||
@ -481,7 +482,7 @@ export interface IGraph extends EventEmitter {
|
||||
* 若 cfg 含有 type 字段或为 String 类型,且与现有布局方法不同,则更换布局
|
||||
* 若 cfg 不包括 type ,则保持原有布局方法,仅更新布局配置项
|
||||
*/
|
||||
updateLayout(cfg): void;
|
||||
updateLayout(cfg: LayoutConfig): void;
|
||||
|
||||
/**
|
||||
* 重新以当前示例中配置的属性进行一次布局
|
||||
@ -492,13 +493,13 @@ export interface IGraph extends EventEmitter {
|
||||
* 添加插件
|
||||
* @param {object} plugin 插件实例
|
||||
*/
|
||||
addPlugin(plugin): void;
|
||||
addPlugin(plugin: PluginBase): void;
|
||||
|
||||
/**
|
||||
* 添加插件
|
||||
* @param {object} plugin 插件实例
|
||||
*/
|
||||
removePlugin(plugin): void;
|
||||
removePlugin(plugin: PluginBase): void;
|
||||
|
||||
/**
|
||||
* 收起分组
|
||||
@ -545,7 +546,7 @@ export interface ITreeGraph extends IGraph {
|
||||
* @param {TreeGraphData | undefined} parent 从哪个节点开始寻找,为空时从根节点开始查找
|
||||
* @return {TreeGraphData} 对应源数据
|
||||
*/
|
||||
findDataById(id: string, parent?: TreeGraphData | undefined): TreeGraphData;
|
||||
findDataById(id: string, parent?: TreeGraphData | undefined): TreeGraphData | null;
|
||||
|
||||
/**
|
||||
* 布局动画接口,用于数据更新时做节点位置更新的动画
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { IGroup } from '@antv/g-base/lib/interfaces';
|
||||
import { Point } from '@antv/g-base/lib/types';
|
||||
import Group from "@antv/g-canvas/lib/group";
|
||||
import { IBBox, IPoint, IShapeBase, Item, ModelConfig, ModelStyle, ShapeStyle } from '../types';
|
||||
import { IBBox, IPoint, IShapeBase, Item, ModelConfig, ModelStyle, ShapeStyle, Indexable } from '../types';
|
||||
|
||||
|
||||
// item 的配置项
|
||||
@ -63,16 +63,16 @@ export type IItemBaseConfig = Partial<{
|
||||
target: string | Item;
|
||||
|
||||
linkCenter: boolean;
|
||||
}>
|
||||
}> & Indexable<any>
|
||||
|
||||
export interface IItemBase {
|
||||
_cfg: IItemBaseConfig;
|
||||
_cfg: IItemBaseConfig | null;
|
||||
|
||||
destroyed: boolean;
|
||||
|
||||
isItem(): boolean;
|
||||
|
||||
getKeyShapeStyle(): ShapeStyle;
|
||||
getKeyShapeStyle(): ShapeStyle | void;
|
||||
|
||||
/**
|
||||
* 获取当前元素的所有状态
|
||||
@ -101,7 +101,7 @@ export interface IItemBase {
|
||||
*/
|
||||
setState(state: string, enable: boolean): void;
|
||||
|
||||
clearStates(states: string | string[]): void;
|
||||
clearStates(states?: string | string[]): void;
|
||||
|
||||
/**
|
||||
* 节点的图形容器
|
||||
@ -247,7 +247,7 @@ export interface INode extends IItemBase {
|
||||
* @param {Object} point 节点外面的一个点,用于计算交点、最近的锚点
|
||||
* @return {Object} 连接点 {x,y}
|
||||
*/
|
||||
getLinkPoint(point: IPoint): IPoint;
|
||||
getLinkPoint(point: IPoint): IPoint | null;
|
||||
|
||||
/**
|
||||
* 添加边
|
||||
|
@ -5,11 +5,11 @@ import { EdgeConfig, GraphData, IPointTuple, NodeConfig } from '../types';
|
||||
*/
|
||||
export interface ILayout<Cfg = any> {
|
||||
/** 节点 */
|
||||
nodes: NodeConfig[];
|
||||
nodes: NodeConfig[] | null;
|
||||
/** 边 */
|
||||
edges: EdgeConfig[];
|
||||
edges: EdgeConfig[] | null;
|
||||
/** 布局获得的位置 */
|
||||
positions: IPointTuple[];
|
||||
positions: IPointTuple[] | null;
|
||||
/** 是否已销毁 */
|
||||
destroyed: boolean;
|
||||
|
||||
|
@ -10,6 +10,7 @@ export type ILabelConfig = Partial<{
|
||||
refX: number;
|
||||
refY: number;
|
||||
autoRotate: boolean;
|
||||
style: LabelStyle;
|
||||
}>
|
||||
|
||||
export type ShapeOptions = Partial<{
|
||||
@ -33,21 +34,21 @@ export type ShapeOptions = Partial<{
|
||||
drawShape(cfg?: ModelConfig, group?: GGroup): IShape
|
||||
drawLabel(cfg: ModelConfig, group: GGroup): IShape
|
||||
getLabelStyleByPosition(cfg?: ModelConfig, labelCfg?: ILabelConfig, group?: GGroup): LabelStyle
|
||||
getLabelStyle(cfg: ModelConfig, labelCfg, group: GGroup): LabelStyle
|
||||
getLabelStyle(cfg: ModelConfig, labelCfg: ILabelConfig, group: GGroup): LabelStyle
|
||||
getShapeStyle(cfg: ModelConfig): ShapeStyle
|
||||
getStateStyle(name: string, value: string | boolean, item: Item): ShapeStyle
|
||||
|
||||
/**
|
||||
* 绘制完成后的操作,便于用户继承现有的节点、边
|
||||
*/
|
||||
afterDraw(cfg?: ModelConfig, group?: GGroup, rst?: IShape)
|
||||
afterDraw(cfg?: ModelConfig, group?: GGroup, rst?: IShape): void
|
||||
|
||||
afterUpdate(cfg?: ModelConfig, item?: Item)
|
||||
afterUpdate(cfg?: ModelConfig, item?: Item): void
|
||||
|
||||
/**
|
||||
* 设置节点、边状态
|
||||
*/
|
||||
setState(name?: string, value?: string | boolean, item?: Item)
|
||||
setState(name?: string, value?: string | boolean, item?: Item): void
|
||||
|
||||
|
||||
/**
|
||||
@ -65,7 +66,7 @@ export type ShapeOptions = Partial<{
|
||||
getAnchorPoints(cfg?: ModelConfig): IPoint[]
|
||||
|
||||
// 如果没定义 update 方法,每次都调用 draw 方法
|
||||
update(cfg: ModelConfig, item: Item)
|
||||
update(cfg: ModelConfig, item: Item): void
|
||||
|
||||
// 获取节点的大小,只对节点起效
|
||||
getSize: (cfg: ModelConfig) => number | number[]
|
||||
|
@ -1,11 +1,11 @@
|
||||
import isNil from '@antv/util/lib/is-nil';
|
||||
import isPlainObject from '@antv/util/lib/is-plain-object'
|
||||
import { IEdge, INode } from "../interface/item";
|
||||
import { EdgeConfig, IPoint, NodeConfig, SourceTarget } from '../types';
|
||||
import { EdgeConfig, IPoint, NodeConfig, SourceTarget, Indexable, ModelConfig } from '../types';
|
||||
import Item from './item';
|
||||
import Node from './node'
|
||||
|
||||
const END_MAP = { source: 'start', target: 'end' };
|
||||
const END_MAP: Indexable<string> = { source: 'start', target: 'end' };
|
||||
const ITEM_NAME_SUFFIX = 'Node'; // 端点的后缀,如 sourceNode, targetNode
|
||||
const POINT_NAME_SUFFIX = 'Point'; // 起点或者结束点的后缀,如 startPoint, endPoint
|
||||
const ANCHOR_NAME_SUFFIX = 'Anchor';
|
||||
@ -22,7 +22,7 @@ export default class Edge extends Item implements IEdge {
|
||||
}
|
||||
}
|
||||
|
||||
private setEnd(name: string, value: INode) {
|
||||
private setEnd(name: SourceTarget, value: INode) {
|
||||
const pointName = END_MAP[name] + POINT_NAME_SUFFIX;
|
||||
const itemName = name + ITEM_NAME_SUFFIX;
|
||||
const preItem = this.get(itemName);
|
||||
@ -86,7 +86,7 @@ export default class Edge extends Item implements IEdge {
|
||||
* 获取端点的位置
|
||||
* @param name
|
||||
*/
|
||||
private getEndPoint(name: string): NodeConfig | IPoint {
|
||||
private getEndPoint(name: SourceTarget): NodeConfig | IPoint {
|
||||
const itemName = name + ITEM_NAME_SUFFIX;
|
||||
const pointName = END_MAP[name] + POINT_NAME_SUFFIX;
|
||||
const item = this.get(itemName);
|
||||
@ -101,7 +101,7 @@ export default class Edge extends Item implements IEdge {
|
||||
* 通过端点的中心获取控制点
|
||||
* @param model
|
||||
*/
|
||||
private getControlPointsByCenter(model) {
|
||||
private getControlPointsByCenter(model: EdgeConfig) {
|
||||
const sourcePoint = this.getEndPoint('source');
|
||||
const targetPoint = this.getEndPoint('target');
|
||||
const shapeFactory = this.get('shapeFactory');
|
||||
@ -112,7 +112,7 @@ export default class Edge extends Item implements IEdge {
|
||||
});
|
||||
}
|
||||
|
||||
private getEndCenter(name: string): IPoint {
|
||||
private getEndCenter(name: SourceTarget): IPoint {
|
||||
const itemName = name + ITEM_NAME_SUFFIX;
|
||||
const pointName = END_MAP[name] + POINT_NAME_SUFFIX;
|
||||
const item = this.get(itemName);
|
||||
@ -137,7 +137,7 @@ export default class Edge extends Item implements IEdge {
|
||||
public getShapeCfg(model: EdgeConfig): EdgeConfig {
|
||||
const self = this;
|
||||
const linkCenter: boolean = self.get('linkCenter'); // 如果连接到中心,忽视锚点、忽视控制点
|
||||
const cfg: any = super.getShapeCfg(model);
|
||||
const cfg: ModelConfig = super.getShapeCfg(model);
|
||||
|
||||
if (linkCenter) {
|
||||
cfg.startPoint = self.getEndCenter('source');
|
||||
|
@ -6,7 +6,7 @@ import isString from '@antv/util/lib/is-string'
|
||||
import uniqueId from '@antv/util/lib/unique-id'
|
||||
import { IItemBase, IItemBaseConfig } from "../interface/item";
|
||||
import Shape from '../shape/shape';
|
||||
import { IBBox, IPoint, IShapeBase, ModelConfig, ShapeStyle } from '../types';
|
||||
import { IBBox, IPoint, IShapeBase, ModelConfig, ShapeStyle, Indexable } from '../types';
|
||||
import { getBBox } from '../util/graphic';
|
||||
import { translate } from '../util/math';
|
||||
|
||||
@ -16,13 +16,15 @@ const RESERVED_STYLES = [ 'fillStyle', 'strokeStyle',
|
||||
'path', 'points', 'img', 'symbol' ];
|
||||
|
||||
export default class ItemBase implements IItemBase {
|
||||
public _cfg: IItemBaseConfig = {}
|
||||
public _cfg: IItemBaseConfig & {
|
||||
[key: string]: unknown
|
||||
} = {}
|
||||
private defaultCfg: IItemBaseConfig = {
|
||||
/**
|
||||
* id
|
||||
* @type {string}
|
||||
*/
|
||||
id: null,
|
||||
id: undefined,
|
||||
|
||||
/**
|
||||
* 类型
|
||||
@ -40,7 +42,7 @@ export default class ItemBase implements IItemBase {
|
||||
* g group
|
||||
* @type {G.Group}
|
||||
*/
|
||||
group: null,
|
||||
group: undefined,
|
||||
|
||||
/**
|
||||
* is open animate
|
||||
@ -68,7 +70,7 @@ export default class ItemBase implements IItemBase {
|
||||
* key shape to calculate item's bbox
|
||||
* @type object
|
||||
*/
|
||||
keyShape: null,
|
||||
keyShape: undefined,
|
||||
/**
|
||||
* item's states, such as selected or active
|
||||
* @type Array
|
||||
@ -81,7 +83,7 @@ export default class ItemBase implements IItemBase {
|
||||
constructor(cfg: IItemBaseConfig) {
|
||||
this._cfg = Object.assign(this.defaultCfg, this.getDefaultCfg(), cfg)
|
||||
const group = cfg.group
|
||||
group.set('item', this)
|
||||
if (group) group.set('item', this)
|
||||
|
||||
let id = this.get('model').id
|
||||
|
||||
@ -90,7 +92,7 @@ export default class ItemBase implements IItemBase {
|
||||
}
|
||||
|
||||
this.set('id', id)
|
||||
group.set('id', id)
|
||||
if (group) group.set('id', id)
|
||||
|
||||
const stateStyles = this.get('model').stateStyles;
|
||||
this.set('stateStyles', stateStyles);
|
||||
@ -131,7 +133,7 @@ export default class ItemBase implements IItemBase {
|
||||
}
|
||||
self.updatePosition(model);
|
||||
const cfg = self.getShapeCfg(model); // 可能会附加额外信息
|
||||
const shapeType: string = cfg.shape || cfg.type;
|
||||
const shapeType = (cfg.shape as string )|| (cfg.type as string);
|
||||
|
||||
const keyShape: IShapeBase = shapeFactory.draw(shapeType, cfg, group);
|
||||
if (keyShape) {
|
||||
@ -141,7 +143,7 @@ export default class ItemBase implements IItemBase {
|
||||
}
|
||||
// 防止由于用户外部修改 model 中的 shape 导致 shape 不更新
|
||||
this.set('currentShape', shapeType);
|
||||
this.resetStates(shapeFactory, shapeType);
|
||||
this.resetStates(shapeFactory, shapeType!);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -149,7 +151,7 @@ export default class ItemBase implements IItemBase {
|
||||
* @param shapeFactory
|
||||
* @param shapeType
|
||||
*/
|
||||
private resetStates(shapeFactory, shapeType: string) {
|
||||
private resetStates(shapeFactory: any, shapeType: string) {
|
||||
const self = this;
|
||||
const states: string[] = self.get('states');
|
||||
each(states, state => {
|
||||
@ -168,8 +170,8 @@ export default class ItemBase implements IItemBase {
|
||||
* @param {String} key 属性名
|
||||
* @return {object | string | number} 属性值
|
||||
*/
|
||||
public get(key: string) {
|
||||
return this._cfg[key]
|
||||
public get<T = any>(key: string): T {
|
||||
return this._cfg[key] as T
|
||||
}
|
||||
|
||||
/**
|
||||
@ -178,7 +180,7 @@ export default class ItemBase implements IItemBase {
|
||||
* @param {String|Object} key 属性名,也可以是对象
|
||||
* @param {object | string | number} val 属性值
|
||||
*/
|
||||
public set(key: string, val): void {
|
||||
public set(key: string | object, val?: unknown): void {
|
||||
if(isPlainObject(key)) {
|
||||
this._cfg = Object.assign({}, this._cfg, key)
|
||||
} else {
|
||||
@ -228,10 +230,10 @@ export default class ItemBase implements IItemBase {
|
||||
this.afterDraw()
|
||||
}
|
||||
|
||||
public getKeyShapeStyle(): ShapeStyle {
|
||||
public getKeyShapeStyle(): ShapeStyle | void {
|
||||
const keyShape = this.getKeyShape();
|
||||
if (keyShape) {
|
||||
const styles: ShapeStyle = {};
|
||||
const styles: ShapeStyle & Indexable<any> = {};
|
||||
each(keyShape.attr(), (val, key) => {
|
||||
if (RESERVED_STYLES.indexOf(key) < 0) {
|
||||
styles[key] = val;
|
||||
@ -239,6 +241,7 @@ export default class ItemBase implements IItemBase {
|
||||
});
|
||||
return styles;
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
public getShapeCfg(model: ModelConfig): ModelConfig {
|
||||
@ -310,7 +313,7 @@ export default class ItemBase implements IItemBase {
|
||||
const originStates = self.getStates();
|
||||
const shapeFactory = self.get('shapeFactory');
|
||||
const model: ModelConfig = self.get('model')
|
||||
const shape: string = model.shape || model.type;
|
||||
const shape = model.shape || model.type;
|
||||
if (!states) {
|
||||
self.set('states', []);
|
||||
shapeFactory.setState(shape, originStates[0], false, self);
|
||||
@ -416,7 +419,7 @@ export default class ItemBase implements IItemBase {
|
||||
*/
|
||||
public update(cfg: ModelConfig) {
|
||||
const model: ModelConfig = this.get('model');
|
||||
const originPosition: IPoint = { x: model.x, y: model.y };
|
||||
const originPosition: IPoint = { x: model.x!, y: model.y! };
|
||||
|
||||
// 直接将更新合到原数据模型上,可以保证用户在外部修改源数据然后刷新时的样式符合期待。
|
||||
Object.assign(model, cfg);
|
||||
@ -477,7 +480,7 @@ export default class ItemBase implements IItemBase {
|
||||
}
|
||||
group.resetMatrix();
|
||||
// G 4.0 element 中移除了矩阵相关方法,详见https://www.yuque.com/antv/blog/kxzk9g#4rMMV
|
||||
translate(group, { x, y });
|
||||
translate(group, { x: x!, y: y! });
|
||||
model.x = x;
|
||||
model.y = y;
|
||||
this.clearCache(); // 位置更新后需要清除缓存
|
||||
@ -566,7 +569,7 @@ export default class ItemBase implements IItemBase {
|
||||
group.stopAnimate();
|
||||
}
|
||||
group.remove();
|
||||
this._cfg = null;
|
||||
(this._cfg as IItemBaseConfig | null) = null;
|
||||
this.destroyed = true;
|
||||
}
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ export default class Node extends Item implements INode {
|
||||
* 根据锚点的索引获取连接点
|
||||
* @param {Number} index 索引
|
||||
*/
|
||||
public getLinkPointByAnchor(index): IPoint {
|
||||
public getLinkPointByAnchor(index: number): IPoint {
|
||||
const anchorPoints = this.getAnchorPoints();
|
||||
return anchorPoints[index];
|
||||
}
|
||||
@ -74,25 +74,25 @@ export default class Node extends Item implements INode {
|
||||
* 获取连接点
|
||||
* @param point
|
||||
*/
|
||||
public getLinkPoint(point: IPoint): IPoint {
|
||||
public getLinkPoint(point: IPoint): IPoint | null {
|
||||
const keyShape: IShapeBase = this.get('keyShape');
|
||||
const type: string = keyShape.get('type');
|
||||
const bbox = this.getBBox();
|
||||
const { centerX, centerY } = bbox;
|
||||
const anchorPoints = this.getAnchorPoints();
|
||||
let intersectPoint: IPoint;
|
||||
let intersectPoint: IPoint | null;
|
||||
switch (type) {
|
||||
case 'circle':
|
||||
intersectPoint = getCircleIntersectByPoint({
|
||||
x: centerX,
|
||||
y: centerY,
|
||||
x: centerX!,
|
||||
y: centerY!,
|
||||
r: bbox.width / 2
|
||||
}, point);
|
||||
break;
|
||||
case 'ellipse':
|
||||
intersectPoint = getEllispeIntersectByPoint({
|
||||
x: centerX,
|
||||
y: centerY,
|
||||
x: centerX!,
|
||||
y: centerY!,
|
||||
rx: bbox.width / 2,
|
||||
ry: bbox.height / 2
|
||||
}, point);
|
||||
@ -109,7 +109,7 @@ export default class Node extends Item implements INode {
|
||||
linkPoint = this.getNearestPoint(anchorPoints, linkPoint);
|
||||
}
|
||||
if (!linkPoint) { // 如果最终依然没法找到锚点和连接点,直接返回中心点
|
||||
linkPoint = { x: centerX, y: centerY };
|
||||
linkPoint = { x: centerX, y: centerY } as IPoint;
|
||||
}
|
||||
return linkPoint;
|
||||
}
|
||||
|
@ -3,8 +3,9 @@
|
||||
* @author shiwu.wyy@antfin.com
|
||||
*/
|
||||
|
||||
import { EdgeConfig, IPointTuple, NodeConfig } from '../types';
|
||||
import { EdgeConfig, IPointTuple, NodeConfig, NodeIdxMap } from '../types';
|
||||
import { BaseLayout } from './layout';
|
||||
import { getDegree } from '../util/math';
|
||||
|
||||
type Node = NodeConfig & {
|
||||
degree: number;
|
||||
@ -13,34 +14,34 @@ type Node = NodeConfig & {
|
||||
};
|
||||
type Edge = EdgeConfig;
|
||||
|
||||
function getDegree(n: number, nodeMap: object, edges: Edge[]) {
|
||||
const degrees = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
degrees[i] = 0;
|
||||
}
|
||||
edges.forEach((e) => {
|
||||
degrees[nodeMap[e.source]] += 1;
|
||||
degrees[nodeMap[e.target]] += 1;
|
||||
});
|
||||
return degrees;
|
||||
}
|
||||
|
||||
function initHierarchy(nodes: Node[], edges: Edge[], nodeMap: object, directed: boolean) {
|
||||
function initHierarchy(nodes: Node[], edges: Edge[], nodeMap: NodeIdxMap, directed: boolean) {
|
||||
nodes.forEach((_, i: number) => {
|
||||
nodes[i].children = [];
|
||||
nodes[i].parent = [];
|
||||
});
|
||||
if (directed) {
|
||||
edges.forEach((e) => {
|
||||
const sourceIdx = nodeMap[e.source];
|
||||
const targetIdx = nodeMap[e.target];
|
||||
let sourceIdx = 0;
|
||||
if (e.source) {
|
||||
sourceIdx = nodeMap[e.source];
|
||||
}
|
||||
let targetIdx = 0;
|
||||
if (e.target) {
|
||||
targetIdx = nodeMap[e.target];
|
||||
}
|
||||
nodes[sourceIdx].children.push(nodes[targetIdx]);
|
||||
nodes[targetIdx].parent.push(nodes[sourceIdx]);
|
||||
});
|
||||
} else {
|
||||
edges.forEach((e) => {
|
||||
const sourceIdx = nodeMap[e.source];
|
||||
const targetIdx = nodeMap[e.target];
|
||||
let sourceIdx = 0;
|
||||
if (e.source) {
|
||||
sourceIdx = nodeMap[e.source];
|
||||
}
|
||||
let targetIdx = 0;
|
||||
if (e.target) {
|
||||
targetIdx = nodeMap[e.target];
|
||||
}
|
||||
nodes[sourceIdx].children.push(nodes[targetIdx]);
|
||||
nodes[targetIdx].children.push(nodes[sourceIdx]);
|
||||
});
|
||||
@ -75,35 +76,35 @@ function compareDegree(a: Node, b: Node) {
|
||||
*/
|
||||
export default class CircularLayout extends BaseLayout {
|
||||
/** 布局中心 */
|
||||
public center: IPointTuple;
|
||||
public center: IPointTuple = [0, 0];
|
||||
/** 固定半径,若设置了 radius,则 startRadius 与 endRadius 不起效 */
|
||||
public radius: number;
|
||||
public radius: number | null = null;
|
||||
/** 起始半径 */
|
||||
public startRadius: number;
|
||||
public startRadius: number | null = null;
|
||||
/** 终止半径 */
|
||||
public endRadius: number;
|
||||
public endRadius: number | null = null;
|
||||
/** 起始角度 */
|
||||
public startAngle: number;
|
||||
public startAngle: number = 0;
|
||||
/** 终止角度 */
|
||||
public endAngle: number;
|
||||
public endAngle: number = 2 * Math.PI;
|
||||
/** 是否顺时针 */
|
||||
public clockwise: boolean;
|
||||
public clockwise: boolean = true;
|
||||
/** 节点在环上分成段数(几个段将均匀分布),在 endRadius - startRadius != 0 时生效 */
|
||||
public divisions: number;
|
||||
public divisions: number = 1;
|
||||
/** 节点在环上排序的依据,可选: 'topology', 'degree', 'null' */
|
||||
public ordering: 'topology' | 'topology-directed' | 'degree' | 'null';
|
||||
public ordering: 'topology' | 'topology-directed' | 'degree' | null = null;
|
||||
/** how many 2*pi from first to last nodes */
|
||||
public angleRatio: 1;
|
||||
public angleRatio = 1;
|
||||
|
||||
public nodes: Node[];
|
||||
public edges: Edge[];
|
||||
public nodes: Node[] = [];
|
||||
public edges: Edge[] = [];
|
||||
|
||||
private nodeMap: object;
|
||||
private degrees;
|
||||
private astep;
|
||||
private nodeMap: NodeIdxMap = {};
|
||||
private degrees: number[] = [];
|
||||
private astep: number | undefined;
|
||||
|
||||
public width: number;
|
||||
public height: number;
|
||||
public width: number = 300;
|
||||
public height: number = 300;
|
||||
|
||||
public getDefaultCfg() {
|
||||
return {
|
||||
@ -144,7 +145,7 @@ export default class CircularLayout extends BaseLayout {
|
||||
const endAngle = self.endAngle;
|
||||
const angleStep = (endAngle - startAngle) / n;
|
||||
// layout
|
||||
const nodeMap = {};
|
||||
const nodeMap: NodeIdxMap = {};
|
||||
nodes.forEach((node, i) => {
|
||||
nodeMap[node.id] = i;
|
||||
});
|
||||
@ -190,9 +191,12 @@ export default class CircularLayout extends BaseLayout {
|
||||
const divN = Math.ceil(n / divisions); // node number in each division
|
||||
for (let i = 0; i < n; ++i) {
|
||||
let r = radius;
|
||||
if (!r) {
|
||||
if (!r && startRadius !== null && endRadius !== null) {
|
||||
r = startRadius + (i * (endRadius - startRadius)) / (n - 1);
|
||||
}
|
||||
if (!r) {
|
||||
r = 10 + (i * 100) / (n - 1);
|
||||
}
|
||||
let angle = startAngle + (i % divN) * astep + ((2 * Math.PI) / divisions) * Math.floor(i / divN);
|
||||
if (!clockwise) {
|
||||
angle = endAngle - (i % divN) * astep - ((2 * Math.PI) / divisions) * Math.floor(i / divN);
|
||||
@ -213,7 +217,7 @@ export default class CircularLayout extends BaseLayout {
|
||||
const nodes = self.nodes;
|
||||
const nodeMap = self.nodeMap;
|
||||
const orderedNodes = [nodes[0]];
|
||||
const pickFlags = [];
|
||||
const pickFlags: boolean[] = [];
|
||||
const n = nodes.length;
|
||||
pickFlags[0] = true;
|
||||
initHierarchy(nodes, edges, nodeMap, directed);
|
||||
@ -260,10 +264,10 @@ export default class CircularLayout extends BaseLayout {
|
||||
* 根据节点度数大小排序
|
||||
* @return {array} orderedNodes 排序后的结果
|
||||
*/
|
||||
public degreeOrdering() {
|
||||
public degreeOrdering(): Node[] {
|
||||
const self = this;
|
||||
const nodes = self.nodes;
|
||||
const orderedNodes = [];
|
||||
const orderedNodes: Node[] = [];
|
||||
const degrees = self.degrees;
|
||||
nodes.forEach((node, i) => {
|
||||
node.degree = degrees[i];
|
||||
|
@ -4,28 +4,21 @@
|
||||
* this algorithm refers to <cytoscape.js> - https://github.com/cytoscape/cytoscape.js/
|
||||
*/
|
||||
|
||||
import { EdgeConfig, IPointTuple, NodeConfig } from '../types';
|
||||
import { EdgeConfig, IPointTuple, NodeConfig, NodeIdxMap } from '../types';
|
||||
|
||||
import isArray from '@antv/util/lib/is-array';
|
||||
import isString from '@antv/util/lib/is-string';
|
||||
import { BaseLayout } from './layout';
|
||||
import { getDegree } from '../util/math';
|
||||
import { isNumber } from '@antv/util';
|
||||
|
||||
type Node = NodeConfig & {
|
||||
degree: number;
|
||||
size: number | number[];
|
||||
[key: string]: number;
|
||||
};
|
||||
type Edge = EdgeConfig;
|
||||
|
||||
function getDegree(n: number, nodeIdxMap: object, edges: Edge[]) {
|
||||
const degrees = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
degrees[i] = 0;
|
||||
}
|
||||
edges.forEach((e) => {
|
||||
degrees[nodeIdxMap[e.source]] += 1;
|
||||
degrees[nodeIdxMap[e.target]] += 1;
|
||||
});
|
||||
return degrees;
|
||||
type NodeMap = {
|
||||
[key: string]: Node;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -33,33 +26,33 @@ function getDegree(n: number, nodeIdxMap: object, edges: Edge[]) {
|
||||
*/
|
||||
export default class ConcentricLayout extends BaseLayout {
|
||||
/** 布局中心 */
|
||||
public center: IPointTuple;
|
||||
public nodeSize: number;
|
||||
public center: IPointTuple = [0, 0];
|
||||
public nodeSize: number | IPointTuple = 30;
|
||||
/** min spacing between outside of nodes (used for radius adjustment) */
|
||||
public minNodeSpacing: number;
|
||||
public minNodeSpacing: number = 10;
|
||||
/** prevents node overlap, may overflow boundingBox if not enough space */
|
||||
public preventOverlap: boolean;
|
||||
public preventOverlap: boolean = false;
|
||||
/** how many radians should be between the first and last node (defaults to full circle) */
|
||||
public sweep: undefined;
|
||||
public sweep: number | undefined;
|
||||
/** whether levels have an equal radial distance betwen them, may cause bounding box overflow */
|
||||
public equidistant: boolean;
|
||||
public equidistant: boolean = false;
|
||||
/** where nodes start in radians */
|
||||
public startAngle: number;
|
||||
public startAngle: number = (3 / 2) * Math.PI;
|
||||
/** whether the layout should go clockwise (true) or counterclockwise/anticlockwise (false) */
|
||||
public clockwise: boolean;
|
||||
public clockwise: boolean = true;
|
||||
/** the letiation of concentric values in each level */
|
||||
public maxLevelDiff: undefined | number;
|
||||
/** 根据 sortBy 指定的属性进行排布,数值高的放在中心,如果是 sortBy 则会计算节点度数,度数最高的放在中心 */
|
||||
public sortBy: string;
|
||||
public sortBy: string = 'degree';
|
||||
|
||||
public nodes: Node[];
|
||||
public edges: Edge[];
|
||||
public nodes: Node[] = [];
|
||||
public edges: Edge[] = [];
|
||||
|
||||
public width: number;
|
||||
public height: number;
|
||||
public width: number = 300;
|
||||
public height: number = 300;
|
||||
|
||||
private maxValueNode: number;
|
||||
private counterclockwise: boolean;
|
||||
private maxValueNode: Node | undefined;
|
||||
private counterclockwise: boolean | undefined;
|
||||
|
||||
public getDefaultCfg() {
|
||||
return {
|
||||
@ -92,19 +85,19 @@ export default class ConcentricLayout extends BaseLayout {
|
||||
return;
|
||||
}
|
||||
|
||||
const layoutNodes = [];
|
||||
const layoutNodes: Node[] = [];
|
||||
let maxNodeSize: number;
|
||||
if (isNaN(self.nodeSize)) {
|
||||
if (isArray(self.nodeSize)) {
|
||||
maxNodeSize = Math.max(self.nodeSize[0], self.nodeSize[1]);
|
||||
} else {
|
||||
maxNodeSize = self.nodeSize;
|
||||
}
|
||||
nodes.forEach((node) => {
|
||||
layoutNodes.push(node);
|
||||
let nodeSize: number;
|
||||
let nodeSize: number = maxNodeSize;
|
||||
if (isArray(node.size)) {
|
||||
nodeSize = Math.max(node.size[0], node.size[1]);
|
||||
} else {
|
||||
} else if (isNumber(node.size)){
|
||||
nodeSize = node.size;
|
||||
}
|
||||
maxNodeSize = Math.max(maxNodeSize, nodeSize);
|
||||
@ -119,8 +112,8 @@ export default class ConcentricLayout extends BaseLayout {
|
||||
self.clockwise = self.counterclockwise !== undefined ? !self.counterclockwise : self.clockwise;
|
||||
|
||||
// layout
|
||||
const nodeMap = {};
|
||||
const nodeIdxMap = {};
|
||||
const nodeMap: NodeMap = {};
|
||||
const nodeIdxMap: NodeIdxMap = {};
|
||||
layoutNodes.forEach((node, i) => {
|
||||
nodeMap[node.id] = node;
|
||||
nodeIdxMap[node.id] = i;
|
||||
@ -129,7 +122,7 @@ export default class ConcentricLayout extends BaseLayout {
|
||||
// get the node degrees
|
||||
if (self.sortBy === 'degree' || !isString(self.sortBy) || layoutNodes[0][self.sortBy] === undefined) {
|
||||
self.sortBy = 'degree';
|
||||
if (isNaN(nodes[0].degree)) {
|
||||
if (!isNumber(nodes[0].degree)) {
|
||||
const values = getDegree(nodes.length, nodeIdxMap, edges);
|
||||
layoutNodes.forEach((node, i) => {
|
||||
node.degree = values[i];
|
||||
@ -137,13 +130,13 @@ export default class ConcentricLayout extends BaseLayout {
|
||||
}
|
||||
}
|
||||
// sort nodes by value
|
||||
layoutNodes.sort((n1, n2) => {
|
||||
layoutNodes.sort((n1: Node, n2: Node) => {
|
||||
return n2[self.sortBy] - n1[self.sortBy];
|
||||
});
|
||||
|
||||
self.maxValueNode = layoutNodes[0];
|
||||
|
||||
self.maxLevelDiff = self.maxLevelDiff || self.maxValueNode[self.sortBy] / 4; // 0.5;
|
||||
self.maxLevelDiff = self.maxLevelDiff || self.maxValueNode[self.sortBy] / 4;
|
||||
|
||||
// put the values into levels
|
||||
const levels: any[] = [[]];
|
||||
@ -151,7 +144,7 @@ export default class ConcentricLayout extends BaseLayout {
|
||||
layoutNodes.forEach((node) => {
|
||||
if (currentLevel.length > 0) {
|
||||
const diff = Math.abs(currentLevel[0][self.sortBy] - node[self.sortBy]);
|
||||
if (diff >= self.maxLevelDiff) {
|
||||
if (self.maxLevelDiff && diff >= self.maxLevelDiff) {
|
||||
currentLevel = [];
|
||||
levels.push(currentLevel);
|
||||
}
|
||||
@ -173,7 +166,10 @@ export default class ConcentricLayout extends BaseLayout {
|
||||
// find the metrics for each level
|
||||
let r = 0;
|
||||
levels.forEach((level) => {
|
||||
const sweep = self.sweep === undefined ? 2 * Math.PI - (2 * Math.PI) / level.length : self.sweep;
|
||||
let sweep = self.sweep;
|
||||
if (sweep === undefined) {
|
||||
sweep = 2 * Math.PI - (2 * Math.PI) / level.length;
|
||||
}
|
||||
const dTheta = (level.dTheta = sweep / Math.max(1, level.length - 1));
|
||||
|
||||
// calculate the radius
|
||||
|
@ -14,21 +14,21 @@ import { isNumber } from '@antv/util';
|
||||
*/
|
||||
export default class DagreLayout extends BaseLayout {
|
||||
/** layout 方向, 可选 TB, BT, LR, RL */
|
||||
public rankdir: 'TB' | 'BT' | 'LR' | 'RL';
|
||||
public rankdir: 'TB' | 'BT' | 'LR' | 'RL' = 'TB';
|
||||
/** 节点对齐方式,可选 UL, UR, DL, DR */
|
||||
public align: undefined | 'UL' | 'UR' | 'DL' | 'DR';
|
||||
/** 节点大小 */
|
||||
public nodeSize: number | number[];
|
||||
public nodeSize: number | number[] | undefined;
|
||||
/** 节点水平间距(px) */
|
||||
public nodesepFunc: () => number;
|
||||
public nodesepFunc: ((d?: any) => number) | undefined;
|
||||
/** 每一层节点之间间距 */
|
||||
public ranksepFunc: () => number;
|
||||
public ranksepFunc: ((d?: any) => number) | undefined;
|
||||
/** 节点水平间距(px) */
|
||||
public nodesep: number;
|
||||
public nodesep: number = 50;
|
||||
/** 每一层节点之间间距 */
|
||||
public ranksep: number;
|
||||
public ranksep: number = 50;
|
||||
/** 是否保留布局连线的控制点 */
|
||||
public controlPoints: boolean;
|
||||
public controlPoints: boolean = true;
|
||||
|
||||
public getDefaultCfg() {
|
||||
return {
|
||||
@ -49,12 +49,13 @@ export default class DagreLayout extends BaseLayout {
|
||||
public execute() {
|
||||
const self = this;
|
||||
const nodes = self.nodes;
|
||||
const edges = self.edges;
|
||||
if (!nodes) return;
|
||||
const edges = self.edges || [];
|
||||
const g = new dagre.graphlib.Graph();
|
||||
const nodeSize = self.nodeSize;
|
||||
let nodeSizeFunc;
|
||||
let nodeSizeFunc: ((d?: any) => number[]);
|
||||
if (!nodeSize) {
|
||||
nodeSizeFunc = (d) => {
|
||||
nodeSizeFunc = (d: any) => {
|
||||
if (d.size) {
|
||||
if (isArray(d.size)) {
|
||||
return d.size;
|
||||
@ -95,13 +96,13 @@ export default class DagreLayout extends BaseLayout {
|
||||
});
|
||||
dagre.layout(g);
|
||||
let coord;
|
||||
g.nodes().forEach(node => {
|
||||
g.nodes().forEach((node: any) => {
|
||||
coord = g.node(node);
|
||||
const i = nodes.findIndex(it => it.id === node);
|
||||
nodes[i].x = coord.x;
|
||||
nodes[i].y = coord.y;
|
||||
});
|
||||
g.edges().forEach(edge => {
|
||||
g.edges().forEach((edge: any) => {
|
||||
coord = g.edge(edge);
|
||||
const i = edges.findIndex(it => it.source === edge.v && it.target === edge.w);
|
||||
edges[i].startPoint = coord.points[0];
|
||||
@ -113,7 +114,7 @@ export default class DagreLayout extends BaseLayout {
|
||||
}
|
||||
}
|
||||
|
||||
function getFunc(func: Function, value: number, defaultValue: number ): Function {
|
||||
function getFunc(func: ((d?: any) => number) | undefined, value: number, defaultValue: number ): Function {
|
||||
let resultFunc;
|
||||
if (func) {
|
||||
resultFunc = func;
|
||||
|
@ -18,42 +18,42 @@ import { LAYOUT_MESSAGE } from './worker/layoutConst';
|
||||
/**
|
||||
* 经典力导布局 force-directed
|
||||
*/
|
||||
export default class ForceLayout extends BaseLayout {
|
||||
export default class ForceLayout<Cfg = any> extends BaseLayout {
|
||||
/** 向心力作用点 */
|
||||
public center: IPointTuple;
|
||||
public center: IPointTuple = [0, 0];
|
||||
/** 节点作用力 */
|
||||
public nodeStrength: any;
|
||||
public nodeStrength: number | null = null;
|
||||
/** 边的作用力, 默认为根据节点的入度出度自适应 */
|
||||
public edgeStrength: any;
|
||||
public edgeStrength: number | null = null;
|
||||
/** 是否防止节点相互覆盖 */
|
||||
public preventOverlap: boolean;
|
||||
public preventOverlap: boolean = false;
|
||||
/** 节点大小 / 直径,用于防止重叠时的碰撞检测 */
|
||||
public nodeSize: number | number[] | ((d?: unknown) => number);
|
||||
public nodeSize: number | number[] | ((d?: unknown) => number) | undefined;
|
||||
/** 节点间距,防止节点重叠时节点之间的最小距离(两节点边缘最短距离) */
|
||||
public nodeSpacing: number;
|
||||
public nodeSpacing: ((d?: unknown) => number) | undefined;
|
||||
/** 默认边长度 */
|
||||
public linkDistance: number;
|
||||
public linkDistance: number = 50;
|
||||
/** 自定义 force 方法 */
|
||||
public forceSimulation: any;
|
||||
/** 迭代阈值的衰减率 [0, 1],0.028 对应最大迭代数为 300 */
|
||||
public alphaDecay: number;
|
||||
public alphaDecay: number = 0.028;
|
||||
/** 停止迭代的阈值 */
|
||||
public alphaMin: number;
|
||||
public alphaMin: number = 0.001;
|
||||
/** 当前阈值 */
|
||||
public alpha: number;
|
||||
public alpha: number = 0.3;
|
||||
/** 防止重叠的力强度 */
|
||||
public collideStrength: number;
|
||||
public collideStrength: number = 1;
|
||||
/** 是否启用web worker。前提是在web worker里执行布局,否则无效 */
|
||||
public workerEnabled: boolean;
|
||||
public workerEnabled: boolean = false;
|
||||
|
||||
public tick: () => void;
|
||||
public tick: () => void = () => {};
|
||||
|
||||
public onLayoutEnd: () => void;
|
||||
public onLayoutEnd: () => void = () => {};
|
||||
/** 布局完成回调 */
|
||||
public onTick: () => void;
|
||||
public onTick: () => void = () => {};
|
||||
|
||||
/** 是否正在布局 */
|
||||
private ticking: boolean;
|
||||
private ticking: boolean | undefined = undefined;
|
||||
|
||||
public getDefaultCfg() {
|
||||
return {
|
||||
@ -83,8 +83,8 @@ export default class ForceLayout extends BaseLayout {
|
||||
*/
|
||||
public init(data: GraphData) {
|
||||
const self = this;
|
||||
self.nodes = data.nodes;
|
||||
self.edges = data.edges;
|
||||
self.nodes = data.nodes || [];
|
||||
self.edges = data.edges || [];
|
||||
self.ticking = false;
|
||||
}
|
||||
|
||||
@ -133,7 +133,7 @@ export default class ForceLayout extends BaseLayout {
|
||||
});
|
||||
const edgeForce = d3Force
|
||||
.forceLink()
|
||||
.id((d) => d.id)
|
||||
.id((d: any) => d.id)
|
||||
.links(d3Edges);
|
||||
if (self.edgeStrength) {
|
||||
edgeForce.strength(self.edgeStrength);
|
||||
@ -164,7 +164,7 @@ export default class ForceLayout extends BaseLayout {
|
||||
for (let currentTick = 1; currentTick <= totalTicks; currentTick++) {
|
||||
simulation.tick();
|
||||
// currentTick starts from 1.
|
||||
postMessage({ type: LAYOUT_MESSAGE.TICK, currentTick, totalTicks, nodes }, undefined);
|
||||
postMessage({ type: LAYOUT_MESSAGE.TICK, currentTick, totalTicks, nodes }, undefined as any);
|
||||
}
|
||||
self.ticking = false;
|
||||
}
|
||||
@ -188,7 +188,7 @@ export default class ForceLayout extends BaseLayout {
|
||||
* 防止重叠
|
||||
* @param {object} simulation 力模拟模型
|
||||
*/
|
||||
public overlapProcess(simulation) {
|
||||
public overlapProcess(simulation: any) {
|
||||
const self = this;
|
||||
const nodeSize = self.nodeSize;
|
||||
const nodeSpacing = self.nodeSpacing;
|
||||
@ -209,7 +209,7 @@ export default class ForceLayout extends BaseLayout {
|
||||
}
|
||||
|
||||
if (!nodeSize) {
|
||||
nodeSizeFunc = (d) => {
|
||||
nodeSizeFunc = d => {
|
||||
if (d.size) {
|
||||
if (isArray(d.size)) {
|
||||
const res = d.size[0] > d.size[1] ? d.size[0] : d.size[1];
|
||||
@ -230,11 +230,15 @@ export default class ForceLayout extends BaseLayout {
|
||||
nodeSizeFunc = (d) => {
|
||||
return radius + nodeSpacingFunc(d);
|
||||
};
|
||||
} else if (!isNaN(nodeSize)) {
|
||||
} else if (isNumber(nodeSize)) {
|
||||
const radius = nodeSize / 2;
|
||||
nodeSizeFunc = (d) => {
|
||||
return radius + nodeSpacingFunc(d);
|
||||
};
|
||||
} else {
|
||||
nodeSizeFunc = () => {
|
||||
return 10;
|
||||
};
|
||||
}
|
||||
|
||||
// forceCollide's parameter is a radius
|
||||
@ -245,14 +249,14 @@ export default class ForceLayout extends BaseLayout {
|
||||
* 更新布局配置,但不执行布局
|
||||
* @param {object} cfg 需要更新的配置项
|
||||
*/
|
||||
public updateCfg(cfg) {
|
||||
public updateCfg(cfg: Partial<Cfg>) {
|
||||
const self = this;
|
||||
if (self.ticking) {
|
||||
self.forceSimulation.stop();
|
||||
self.ticking = false;
|
||||
}
|
||||
self.forceSimulation = null;
|
||||
mix(self, cfg);
|
||||
mix(self as any, cfg);
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
@ -268,7 +272,7 @@ export default class ForceLayout extends BaseLayout {
|
||||
}
|
||||
|
||||
// Return total ticks of d3-force simulation
|
||||
function getSimulationTicks(simulation): number {
|
||||
function getSimulationTicks(simulation: any): number {
|
||||
const alphaMin = simulation.alphaMin();
|
||||
const alphaTarget = simulation.alphaTarget();
|
||||
const alpha = simulation.alpha();
|
||||
@ -276,7 +280,7 @@ function getSimulationTicks(simulation): number {
|
||||
const totalTicks = Math.ceil(totalTicksFloat);
|
||||
return totalTicks;
|
||||
}
|
||||
declare const WorkerGlobalScope;
|
||||
declare const WorkerGlobalScope: any;
|
||||
|
||||
// 判断是否运行在web worker里
|
||||
function isInWorker(): boolean {
|
||||
|
@ -3,16 +3,20 @@
|
||||
* @author shiwu.wyy@antfin.com
|
||||
*/
|
||||
|
||||
import { EdgeConfig, IPointTuple, NodeConfig } from '../types';
|
||||
import { EdgeConfig, IPointTuple, NodeConfig, NodeIdxMap } from '../types';
|
||||
import { BaseLayout } from './layout';
|
||||
import { isNumber } from '@antv/util';
|
||||
import { Point } from '@antv/g-base';
|
||||
|
||||
type Node = NodeConfig & {
|
||||
cluster: string;
|
||||
cluster: string | number;
|
||||
};
|
||||
|
||||
type Edge = EdgeConfig;
|
||||
|
||||
type NodeMap = Map<string, Node>;
|
||||
type NodeIndexMap = Map<string, string>;
|
||||
type NodeMap = {
|
||||
[key: string]: Node;
|
||||
}
|
||||
|
||||
const SPEED_DIVISOR = 800;
|
||||
|
||||
@ -21,26 +25,26 @@ const SPEED_DIVISOR = 800;
|
||||
*/
|
||||
export default class FruchtermanLayout extends BaseLayout {
|
||||
/** 布局中心 */
|
||||
public center: IPointTuple;
|
||||
public center: IPointTuple = [0, 0];
|
||||
/** 停止迭代的最大迭代数 */
|
||||
public maxIteration: number;
|
||||
public maxIteration: number = 1000;
|
||||
/** 重力大小,影响图的紧凑程度 */
|
||||
public gravity: number;
|
||||
public gravity: number = 10;
|
||||
/** 速度 */
|
||||
public speed: number;
|
||||
public speed: number = 1;
|
||||
/** 是否产生聚类力 */
|
||||
public clustering: boolean;
|
||||
public clustering: boolean = false;
|
||||
/** 聚类力大小 */
|
||||
public clusterGravity: number;
|
||||
public clusterGravity: number = 10;
|
||||
|
||||
public width: number;
|
||||
public height: number;
|
||||
public nodes: Node[] = [];
|
||||
public edges: Edge[] = [];
|
||||
|
||||
public nodes: Node[];
|
||||
public edges: Edge[];
|
||||
public width: number = 300;
|
||||
public height: number = 300;
|
||||
|
||||
public nodeMap: object;
|
||||
public nodeIndexMap: object;
|
||||
public nodeMap: NodeMap = {};
|
||||
public nodeIdxMap: NodeIdxMap = {};
|
||||
|
||||
public getDefaultCfg() {
|
||||
return {
|
||||
@ -60,21 +64,21 @@ export default class FruchtermanLayout extends BaseLayout {
|
||||
const nodes = self.nodes;
|
||||
const center = self.center;
|
||||
|
||||
if (nodes.length === 0) {
|
||||
if (!nodes || nodes.length === 0) {
|
||||
return;
|
||||
} else if (nodes.length === 1) {
|
||||
nodes[0].x = center[0];
|
||||
nodes[0].y = center[1];
|
||||
return;
|
||||
}
|
||||
const nodeMap = {};
|
||||
const nodeIndexMap = {};
|
||||
const nodeMap: NodeMap = {};
|
||||
const nodeIdxMap: NodeIdxMap = {};
|
||||
nodes.forEach((node, i) => {
|
||||
nodeMap[node.id] = node;
|
||||
nodeIndexMap[node.id] = i;
|
||||
nodeIdxMap[node.id] = i;
|
||||
});
|
||||
self.nodeMap = nodeMap;
|
||||
self.nodeIndexMap = nodeIndexMap;
|
||||
self.nodeIdxMap = nodeIdxMap;
|
||||
// layout
|
||||
self.run();
|
||||
}
|
||||
@ -82,6 +86,7 @@ export default class FruchtermanLayout extends BaseLayout {
|
||||
public run() {
|
||||
const self = this;
|
||||
const nodes = self.nodes;
|
||||
if (!nodes) return;
|
||||
const edges = self.edges;
|
||||
const maxIteration = self.maxIteration;
|
||||
if (!self.width && typeof window !== 'undefined') {
|
||||
@ -92,15 +97,20 @@ export default class FruchtermanLayout extends BaseLayout {
|
||||
}
|
||||
const center = self.center;
|
||||
const nodeMap = self.nodeMap;
|
||||
const nodeIndexMap = self.nodeIndexMap;
|
||||
const nodeIdxMap = self.nodeIdxMap;
|
||||
const maxDisplace = self.width / 10;
|
||||
const k = Math.sqrt((self.width * self.height) / (nodes.length + 1));
|
||||
const gravity = self.gravity;
|
||||
const speed = self.speed;
|
||||
const clustering = self.clustering;
|
||||
const clusterMap = {}
|
||||
const clusterMap: {[key: string]: {
|
||||
name: string | number,
|
||||
cx: number,
|
||||
cy: number,
|
||||
count: number
|
||||
}} = {}
|
||||
if (clustering) {
|
||||
nodes.forEach((n) => {
|
||||
nodes.forEach(n => {
|
||||
if (clusterMap[n.cluster] === undefined) {
|
||||
const cluster = {
|
||||
name: n.cluster,
|
||||
@ -111,8 +121,12 @@ export default class FruchtermanLayout extends BaseLayout {
|
||||
clusterMap[n.cluster] = cluster;
|
||||
}
|
||||
const c = clusterMap[n.cluster];
|
||||
c.cx += n.x;
|
||||
c.cy += n.y;
|
||||
if (isNumber(n.x)) {
|
||||
c.cx += n.x;
|
||||
}
|
||||
if (isNumber(n.y)) {
|
||||
c.cy += n.y;
|
||||
}
|
||||
c.count++;
|
||||
});
|
||||
for (let key in clusterMap) {
|
||||
@ -121,16 +135,17 @@ export default class FruchtermanLayout extends BaseLayout {
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < maxIteration; i++) {
|
||||
const disp = [];
|
||||
const disp: Point[] = [];
|
||||
nodes.forEach((_, j) => {
|
||||
disp[j] = { x: 0, y: 0 };
|
||||
});
|
||||
self.getDisp(nodes, edges, nodeMap, nodeIndexMap, disp, k);
|
||||
self.getDisp(nodes, edges, nodeMap, nodeIdxMap, disp, k);
|
||||
|
||||
// gravity for clusters
|
||||
if (clustering) {
|
||||
const clusterGravity = self.clusterGravity || gravity;
|
||||
nodes.forEach((n, j) => {
|
||||
if (!isNumber(n.x) || !isNumber(n.y)) return;
|
||||
const c = clusterMap[n.cluster];
|
||||
const distLength = Math.sqrt((n.x - c.cx) * (n.x - c.cx) + (n.y - c.cy) * (n.y - c.cy));
|
||||
const gravityForce = k * clusterGravity;
|
||||
@ -147,8 +162,12 @@ export default class FruchtermanLayout extends BaseLayout {
|
||||
|
||||
nodes.forEach((n) => {
|
||||
const c = clusterMap[n.cluster];
|
||||
c.cx += n.x;
|
||||
c.cy += n.y;
|
||||
if (isNumber(n.x)) {
|
||||
c.cx += n.x;
|
||||
}
|
||||
if (isNumber(n.y)) {
|
||||
c.cy += n.y;
|
||||
}
|
||||
c.count++;
|
||||
});
|
||||
for (let key in clusterMap) {
|
||||
@ -159,18 +178,15 @@ export default class FruchtermanLayout extends BaseLayout {
|
||||
|
||||
// gravity
|
||||
nodes.forEach((n, j) => {
|
||||
if (!isNumber(n.x) || !isNumber(n.y)) return;
|
||||
const gravityForce = 0.01 * k * gravity;
|
||||
disp[j].x -= gravityForce * (n.x - center[0]);
|
||||
disp[j].y -= gravityForce * (n.y - center[1]);
|
||||
});
|
||||
// speed
|
||||
nodes.forEach((_, j) => {
|
||||
disp[j].dx *= speed / SPEED_DIVISOR;
|
||||
disp[j].dy *= speed / SPEED_DIVISOR;
|
||||
});
|
||||
|
||||
// move
|
||||
nodes.forEach((n, j) => {
|
||||
if (!isNumber(n.x) || !isNumber(n.y)) return;
|
||||
const distLength = Math.sqrt(disp[j].x * disp[j].x + disp[j].y * disp[j].y);
|
||||
if (distLength > 0) {
|
||||
// && !n.isFixed()
|
||||
@ -183,19 +199,20 @@ export default class FruchtermanLayout extends BaseLayout {
|
||||
}
|
||||
|
||||
// TODO: nodeMap、nodeIndexMap 等根本不需要依靠参数传递
|
||||
private getDisp(nodes: Node[], edges: Edge[], nodeMap: object, nodeIndexMap: object, disp, k) {
|
||||
private getDisp(nodes: Node[], edges: Edge[], nodeMap: NodeMap, nodeIdxMap: NodeIdxMap, disp: Point[], k: number) {
|
||||
const self = this;
|
||||
self.calRepulsive(nodes, disp, k);
|
||||
self.calAttractive(edges, nodeMap, nodeIndexMap, disp, k);
|
||||
self.calAttractive(edges, nodeMap, nodeIdxMap, disp, k);
|
||||
}
|
||||
|
||||
private calRepulsive(nodes: Node[], disp, k) {
|
||||
private calRepulsive(nodes: Node[], disp: Point[], k: number) {
|
||||
nodes.forEach((v, i) => {
|
||||
disp[i] = { x: 0, y: 0 };
|
||||
nodes.forEach((u, j) => {
|
||||
if (i === j) {
|
||||
return;
|
||||
}
|
||||
if (!isNumber(v.x) || !isNumber(u.x) || !isNumber(v.y) || !isNumber(u.y)) return;
|
||||
let vecx = v.x - u.x;
|
||||
let vecy = v.y - u.y;
|
||||
let vecLengthSqr = vecx * vecx + vecy * vecy;
|
||||
@ -212,15 +229,17 @@ export default class FruchtermanLayout extends BaseLayout {
|
||||
});
|
||||
}
|
||||
|
||||
private calAttractive(edges: Edge[], nodeMap: object, nodeIndexMap: object, disp, k) {
|
||||
private calAttractive(edges: Edge[], nodeMap: NodeMap, nodeIdxMap: NodeIdxMap, disp: Point[], k: number) {
|
||||
edges.forEach((e) => {
|
||||
const uIndex = nodeIndexMap[e.source];
|
||||
const vIndex = nodeIndexMap[e.target];
|
||||
if (!e.source || !e.target) return;
|
||||
const uIndex = nodeIdxMap[e.source];
|
||||
const vIndex = nodeIdxMap[e.target];
|
||||
if (uIndex === vIndex) {
|
||||
return;
|
||||
}
|
||||
const u = nodeMap[e.source];
|
||||
const v = nodeMap[e.target];
|
||||
if (!isNumber(v.x) || !isNumber(u.x) || !isNumber(v.y) || !isNumber(u.y)) return;
|
||||
const vecx = v.x - u.x;
|
||||
const vecy = v.y - u.y;
|
||||
const vecLength = Math.sqrt(vecx * vecx + vecy * vecy);
|
||||
|
@ -4,76 +4,64 @@
|
||||
* this algorithm refers to <cytoscape.js> - https://github.com/cytoscape/cytoscape.js/
|
||||
*/
|
||||
|
||||
import { EdgeConfig, IPointTuple, NodeConfig } from '../types';
|
||||
import { EdgeConfig, IPointTuple, NodeConfig, NodeIdxMap } from '../types';
|
||||
|
||||
import isString from '@antv/util/lib/is-string';
|
||||
import { BaseLayout } from './layout';
|
||||
import { isArray, isNumber } from '@antv/util';
|
||||
import { getDegree } from '../util/math';
|
||||
|
||||
type Node = NodeConfig & {
|
||||
degree: number;
|
||||
size: number;
|
||||
};
|
||||
type Edge = EdgeConfig;
|
||||
|
||||
function getDegree(n: number, nodeIdxMap: object, edges: Edge[]) {
|
||||
const degrees = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
degrees[i] = 0;
|
||||
}
|
||||
edges.forEach((e) => {
|
||||
degrees[nodeIdxMap[e.source]] += 1;
|
||||
degrees[nodeIdxMap[e.target]] += 1;
|
||||
});
|
||||
return degrees;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网格布局
|
||||
*/
|
||||
export default class GridLayout extends BaseLayout {
|
||||
/** 布局起始点 */
|
||||
public begin: IPointTuple;
|
||||
public begin: IPointTuple = [0, 0];
|
||||
/** prevents node overlap, may overflow boundingBox if not enough space */
|
||||
public preventOverlap: boolean;
|
||||
public preventOverlap: boolean = true;
|
||||
/** extra spacing around nodes when preventOverlap: true */
|
||||
public preventOverlapPadding: 10;
|
||||
public preventOverlapPadding: number = 10;
|
||||
/** uses all available space on false, uses minimal space on true */
|
||||
public condense: boolean;
|
||||
public condense: boolean = false;
|
||||
/** force num of rows in the grid */
|
||||
public rows: number;
|
||||
public rows: number | undefined;
|
||||
/** force num of columns in the grid */
|
||||
public cols: number;
|
||||
public cols: number | undefined;
|
||||
/** returns { row, col } for element */
|
||||
public position: (node: Node) => { row: number; col: number };
|
||||
public position: ((node: Node) => { row: number; col: number }) | undefined;
|
||||
/** a sorting function to order the nodes; e.g. function(a, b){ return a.datapublic ('weight') - b.data('weight') } */
|
||||
public sortBy: string;
|
||||
public nodeSize: number | number[];
|
||||
public sortBy: string = 'degree';
|
||||
public nodeSize: number | number[] = 30;
|
||||
|
||||
public nodes: Node[];
|
||||
public edges: Edge[];
|
||||
public nodes: Node[] = [];
|
||||
public edges: Edge[] = [];
|
||||
|
||||
/** 布局中心 */
|
||||
public center: IPointTuple;
|
||||
public width: number;
|
||||
public height: number;
|
||||
public center: IPointTuple = [0, 0];
|
||||
public width: number = 300;
|
||||
public height: number = 300;
|
||||
|
||||
private cells: number;
|
||||
private row: number;
|
||||
private col: number;
|
||||
private splits: number;
|
||||
private columns: number;
|
||||
private cellWidth: number;
|
||||
private cellHeight: number;
|
||||
private cells: number | undefined;
|
||||
private row: number = 0;
|
||||
private col: number = 0;
|
||||
private splits: number | undefined;
|
||||
private columns: number | undefined;
|
||||
private cellWidth: number = 0;
|
||||
private cellHeight: number = 0;
|
||||
private cellUsed: {
|
||||
[key: string]: boolean;
|
||||
};
|
||||
} = {};
|
||||
private id2manPos: {
|
||||
[key: string]: {
|
||||
row: number;
|
||||
col: number;
|
||||
};
|
||||
};
|
||||
} = {};
|
||||
|
||||
public getDefaultCfg() {
|
||||
return {
|
||||
@ -83,7 +71,7 @@ export default class GridLayout extends BaseLayout {
|
||||
condense: false,
|
||||
rows: undefined,
|
||||
cols: undefined,
|
||||
position() {},
|
||||
position: undefined,
|
||||
sortBy: 'degree',
|
||||
nodeSize: 30
|
||||
};
|
||||
@ -109,7 +97,7 @@ export default class GridLayout extends BaseLayout {
|
||||
nodes.forEach((node) => {
|
||||
layoutNodes.push(node);
|
||||
});
|
||||
const nodeIdxMap = {};
|
||||
const nodeIdxMap: NodeIdxMap = {};
|
||||
layoutNodes.forEach((node, i) => {
|
||||
nodeIdxMap[node.id] = i;
|
||||
});
|
||||
@ -156,8 +144,8 @@ export default class GridLayout extends BaseLayout {
|
||||
if (self.cols * self.rows > self.cells) {
|
||||
// otherwise use the automatic values and adjust accordingly
|
||||
// if rounding was up, see if we can reduce rows or columns
|
||||
const sm = self.small();
|
||||
const lg = self.large();
|
||||
const sm = self.small() as number;
|
||||
const lg = self.large() as number;
|
||||
|
||||
// reducing the small side takes away the most cells, so try it first
|
||||
if ((sm - 1) * lg >= self.cells) {
|
||||
@ -168,8 +156,8 @@ export default class GridLayout extends BaseLayout {
|
||||
} else {
|
||||
// if rounding was too low, add rows or columns
|
||||
while (self.cols * self.rows < self.cells) {
|
||||
const sm = self.small();
|
||||
const lg = self.large();
|
||||
const sm = self.small() as number;
|
||||
const lg = self.large() as number;
|
||||
|
||||
// try to add to larger side first (adds less in multiplication)
|
||||
if ((lg + 1) * sm >= self.cells) {
|
||||
@ -196,16 +184,16 @@ export default class GridLayout extends BaseLayout {
|
||||
node.y = 0;
|
||||
}
|
||||
|
||||
let nodew: number;
|
||||
let nodeh: number;
|
||||
let nodew: number | undefined;
|
||||
let nodeh: number | undefined;
|
||||
if (isArray(node.size)) {
|
||||
nodew = node.size[0];
|
||||
nodeh = node.size[1];
|
||||
} else {
|
||||
} else if(isNumber(node.size)){
|
||||
nodew = node.size;
|
||||
nodeh = node.size;
|
||||
}
|
||||
if (isNaN(nodew) || isNaN(nodeh)) {
|
||||
if (nodew === undefined || nodeh === undefined) {
|
||||
if (isArray(self.nodeSize)) {
|
||||
nodew = self.nodeSize[0];
|
||||
nodeh = self.nodeSize[1];
|
||||
@ -238,7 +226,10 @@ export default class GridLayout extends BaseLayout {
|
||||
self.id2manPos = {};
|
||||
for (let i = 0; i < layoutNodes.length; i++) {
|
||||
const node = layoutNodes[i];
|
||||
const rcPos = self.position(node);
|
||||
let rcPos;
|
||||
if (self.position) {
|
||||
rcPos = self.position(node);
|
||||
}
|
||||
|
||||
if (rcPos && (rcPos.row !== undefined || rcPos.col !== undefined)) {
|
||||
// must have at least row or col def'd
|
||||
@ -269,13 +260,15 @@ export default class GridLayout extends BaseLayout {
|
||||
self.getPos(node);
|
||||
}
|
||||
}
|
||||
private small(val?: number) {
|
||||
private small(val?: number): number | undefined {
|
||||
const self = this;
|
||||
let res: number;
|
||||
let res: number | undefined;
|
||||
const rows = self.rows || 5;
|
||||
const cols = self.cols || 5;
|
||||
if (val == null) {
|
||||
res = Math.min(self.rows, self.cols);
|
||||
res = Math.min(rows, cols);
|
||||
} else {
|
||||
const min = Math.min(self.rows, self.cols);
|
||||
const min = Math.min(rows, cols);
|
||||
if (min === self.rows) {
|
||||
self.rows = val;
|
||||
} else {
|
||||
@ -285,13 +278,15 @@ export default class GridLayout extends BaseLayout {
|
||||
return res;
|
||||
}
|
||||
|
||||
private large(val?: number) {
|
||||
private large(val?: number): number | undefined {
|
||||
const self = this;
|
||||
let res: number;
|
||||
let res: number | undefined;
|
||||
const rows = self.rows || 5;
|
||||
const cols = self.cols || 5;
|
||||
if (val == null) {
|
||||
res = Math.max(self.rows, self.cols);
|
||||
res = Math.max(rows, cols);
|
||||
} else {
|
||||
const max = Math.max(self.rows, self.cols);
|
||||
const max = Math.max(rows, cols);
|
||||
if (max === self.rows) {
|
||||
self.rows = val;
|
||||
} else {
|
||||
@ -313,8 +308,9 @@ export default class GridLayout extends BaseLayout {
|
||||
|
||||
private moveToNextCell() {
|
||||
const self = this;
|
||||
const cols = self.cols || 5;
|
||||
self.col++;
|
||||
if (self.col >= self.cols) {
|
||||
if (self.col >= cols) {
|
||||
self.col = 0;
|
||||
self.row++;
|
||||
}
|
||||
|
@ -17,15 +17,15 @@ type LayoutConstructor<Cfg = any> = new () => BaseLayout<Cfg>;
|
||||
* 基础布局,将被自定义布局所继承
|
||||
*/
|
||||
export class BaseLayout<Cfg = any> implements ILayout<Cfg> {
|
||||
public nodes: NodeConfig[];
|
||||
public edges: EdgeConfig[];
|
||||
public positions: IPointTuple[];
|
||||
public destroyed: boolean;
|
||||
public nodes: NodeConfig[] | null = [];
|
||||
public edges: EdgeConfig[] | null = [];
|
||||
public positions: IPointTuple[] | null = [];
|
||||
public destroyed: boolean = false;
|
||||
|
||||
public init(data: GraphData) {
|
||||
const self = this;
|
||||
self.nodes = data.nodes;
|
||||
self.edges = data.edges;
|
||||
self.nodes = data.nodes || [];
|
||||
self.edges = data.edges || [];
|
||||
}
|
||||
|
||||
public execute() {}
|
||||
@ -63,7 +63,7 @@ const Layout: {
|
||||
* @param {string} type 布局类型,外部引用指定必须,不要与已有布局类型重名
|
||||
* @param {object} layout 布局方法
|
||||
*/
|
||||
registerLayout<Cfg>(type, layout, layoutCons = BaseLayout) {
|
||||
registerLayout<Cfg>(type: string, layout: LayoutOption<Cfg>, layoutCons = BaseLayout) {
|
||||
if (!layout) {
|
||||
throw new Error('please specify handler for this layout:' + type);
|
||||
}
|
||||
@ -72,11 +72,11 @@ const Layout: {
|
||||
class GLayout extends layoutCons {
|
||||
constructor(cfg: Cfg) {
|
||||
super();
|
||||
const self = this;
|
||||
const props = {};
|
||||
const self = this as any;
|
||||
const props: object = {};
|
||||
const defaultCfg = self.getDefaultCfg();
|
||||
mix(props, defaultCfg, layout, cfg);
|
||||
each(props, (value, key) => {
|
||||
mix(props, defaultCfg, layout, cfg as unknown);
|
||||
each(props, (value, key: string) => {
|
||||
self[key] = value;
|
||||
});
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
* @author shiwu.wyy@antfin.com
|
||||
*/
|
||||
|
||||
import { IPointTuple } from '../types';
|
||||
import { IPointTuple, Matrix } from '../types';
|
||||
|
||||
import Numeric from 'numericjs';
|
||||
import { floydWarshall, getAdjMatrix, scaleMatrix } from '../util/math';
|
||||
@ -14,11 +14,11 @@ import { BaseLayout } from './layout';
|
||||
*/
|
||||
export default class MDSLayout extends BaseLayout {
|
||||
/** 布局中心 */
|
||||
public center: IPointTuple;
|
||||
public center: IPointTuple = [0, 0];
|
||||
/** 边长度 */
|
||||
public linkDistance: number;
|
||||
public linkDistance: number = 50;
|
||||
|
||||
private scaledDistances;
|
||||
private scaledDistances: Matrix[] | null = null;
|
||||
|
||||
public getDefaultCfg() {
|
||||
return {
|
||||
@ -32,9 +32,9 @@ export default class MDSLayout extends BaseLayout {
|
||||
public execute() {
|
||||
const self = this;
|
||||
const nodes = self.nodes;
|
||||
const edges = self.edges;
|
||||
const edges = self.edges || [];
|
||||
const center = self.center;
|
||||
if (nodes.length === 0) {
|
||||
if (!nodes || nodes.length === 0) {
|
||||
return;
|
||||
} else if (nodes.length === 1) {
|
||||
nodes[0].x = center[0];
|
||||
@ -54,7 +54,7 @@ export default class MDSLayout extends BaseLayout {
|
||||
// get positions by MDS
|
||||
const positions = self.runMDS();
|
||||
self.positions = positions;
|
||||
positions.forEach((p, i) => {
|
||||
positions.forEach((p: number[], i: number) => {
|
||||
nodes[i].x = p[0] + center[0];
|
||||
nodes[i].y = p[1] + center[1];
|
||||
});
|
||||
@ -70,7 +70,7 @@ export default class MDSLayout extends BaseLayout {
|
||||
// square distances
|
||||
const M = Numeric.mul(-0.5, Numeric.pow(distances, 2));
|
||||
// double centre the rows/columns
|
||||
function mean(A) {
|
||||
function mean(A: any) {
|
||||
return Numeric.div(Numeric.add.apply(null, A), A.length);
|
||||
}
|
||||
const rowMeans = mean(M);
|
||||
@ -85,12 +85,12 @@ export default class MDSLayout extends BaseLayout {
|
||||
// points from it
|
||||
const ret = Numeric.svd(M);
|
||||
const eigenValues = Numeric.sqrt(ret.S);
|
||||
return ret.U.map(function(row) {
|
||||
return ret.U.map(function(row: any) {
|
||||
return Numeric.mul(row, eigenValues).splice(0, dimension);
|
||||
});
|
||||
}
|
||||
|
||||
public handleInfinity(distances: number[][]) {
|
||||
public handleInfinity(distances: Matrix[]) {
|
||||
let maxDistance = -999999;
|
||||
distances.forEach((row) => {
|
||||
row.forEach((value) => {
|
||||
|
@ -1,20 +1,25 @@
|
||||
import Numeric from 'numericjs';
|
||||
import { IPointTuple, Matrix } from '../../types';
|
||||
|
||||
export default class MDS {
|
||||
/** distance matrix */
|
||||
public distances: number[][];
|
||||
public distances: Matrix[];
|
||||
/** dimensions */
|
||||
public dimension: number;
|
||||
/** link distance */
|
||||
public linkDistance: number;
|
||||
|
||||
constructor(params) {
|
||||
constructor(params: {
|
||||
distances: Matrix[],
|
||||
dimension?: number,
|
||||
linkDistance: number
|
||||
}) {
|
||||
this.distances = params.distances;
|
||||
this.dimension = params.dimension || 2;
|
||||
this.linkDistance = params.linkDistance;
|
||||
}
|
||||
|
||||
public layout() {
|
||||
public layout(): IPointTuple[] {
|
||||
const self = this;
|
||||
const dimension = self.dimension;
|
||||
const distances = self.distances;
|
||||
@ -24,7 +29,7 @@ export default class MDS {
|
||||
const M = Numeric.mul(-0.5, Numeric.pow(distances, 2));
|
||||
|
||||
// double centre the rows/columns
|
||||
function mean(A) {
|
||||
function mean(A: any) {
|
||||
return Numeric.div(Numeric.add.apply(null, A), A.length);
|
||||
}
|
||||
const rowMeans = mean(M);
|
||||
@ -40,7 +45,7 @@ export default class MDS {
|
||||
// take the SVD of the double centred matrix, and return the
|
||||
// points from it
|
||||
let ret;
|
||||
let res = [];
|
||||
let res: IPointTuple[] = [];
|
||||
try {
|
||||
ret = Numeric.svd(M);
|
||||
} catch (e) {
|
||||
@ -53,7 +58,7 @@ export default class MDS {
|
||||
}
|
||||
if (res.length === 0) {
|
||||
const eigenValues = Numeric.sqrt(ret.S);
|
||||
res = ret.U.map(function(row) {
|
||||
res = ret.U.map(function(row: any) {
|
||||
return Numeric.mul(row, eigenValues).splice(0, dimension);
|
||||
});
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
* @author shiwu.wyy@antfin.com
|
||||
*/
|
||||
|
||||
import { IPointTuple, NodeConfig } from '../../types';
|
||||
import { IPointTuple, NodeConfig, Matrix } from '../../types';
|
||||
|
||||
import isArray from '@antv/util/lib/is-array';
|
||||
import isNumber from '@antv/util/lib/is-number';
|
||||
@ -14,11 +14,11 @@ import { floydWarshall, getAdjMatrix } from '../../util/math';
|
||||
import { BaseLayout } from '../layout';
|
||||
|
||||
import MDS from './mds';
|
||||
import RadialNonoverlapForce from './radialNonoverlapForce';
|
||||
import RadialNonoverlapForce, { RadialNonoverlapForceParam } from './radialNonoverlapForce';
|
||||
|
||||
type Node = NodeConfig;
|
||||
|
||||
function getWeightMatrix(M: number[][]) {
|
||||
function getWeightMatrix(M: Matrix[]) {
|
||||
const rows = M.length;
|
||||
const cols = M[0].length;
|
||||
const result = [];
|
||||
@ -56,37 +56,37 @@ function getEDistance(p1: IPointTuple, p2: IPointTuple) {
|
||||
*/
|
||||
export default class RadialLayout extends BaseLayout {
|
||||
/** 布局中心 */
|
||||
public center: IPointTuple;
|
||||
public center: IPointTuple = [0, 0];
|
||||
/** 停止迭代的最大迭代数 */
|
||||
public maxIteration: number;
|
||||
public maxIteration: number = 1000;
|
||||
/** 中心点,默认为数据中第一个点 */
|
||||
public focusNode: String | Node;
|
||||
public focusNode: String | Node | null = null;
|
||||
/** 每一圈半径 */
|
||||
public unitRadius: number;
|
||||
public unitRadius: number | null = null;
|
||||
/** 默认边长度 */
|
||||
public linkDistance: number;
|
||||
public linkDistance: number = 50;
|
||||
/** 是否防止重叠 */
|
||||
public preventOverlap: boolean;
|
||||
public preventOverlap: boolean = false;
|
||||
/** 节点直径 */
|
||||
public nodeSize: number;
|
||||
public nodeSize: number | undefined;
|
||||
/** 节点间距,防止节点重叠时节点之间的最小距离(两节点边缘最短距离) */
|
||||
public nodeSpacing: number;
|
||||
public nodeSpacing: number | Function | undefined;
|
||||
/** 是否必须是严格的 radial 布局,即每一层的节点严格布局在一个环上。preventOverlap 为 true 时生效 */
|
||||
public strictRadial: boolean;
|
||||
public strictRadial: boolean = true;
|
||||
/** 防止重叠步骤的最大迭代次数 */
|
||||
public maxPreventOverlapIteration: number;
|
||||
public maxPreventOverlapIteration: number = 200;
|
||||
|
||||
public sortBy: string;
|
||||
public sortStrength: number;
|
||||
public sortBy: string | undefined;
|
||||
public sortStrength: number = 10;
|
||||
|
||||
public width: number;
|
||||
public height: number;
|
||||
public width: number | undefined;
|
||||
public height: number | undefined;
|
||||
|
||||
private focusIndex;
|
||||
private distances;
|
||||
private eIdealDistances;
|
||||
private weights;
|
||||
private radii;
|
||||
private focusIndex: number | undefined;
|
||||
private distances: Matrix[] | undefined;
|
||||
private eIdealDistances: Matrix[] | undefined;
|
||||
private weights: Matrix[] | undefined;
|
||||
private radii: number[] | undefined;
|
||||
|
||||
public getDefaultCfg() {
|
||||
return {
|
||||
@ -110,9 +110,9 @@ export default class RadialLayout extends BaseLayout {
|
||||
public execute() {
|
||||
const self = this;
|
||||
const nodes = self.nodes;
|
||||
const edges = self.edges;
|
||||
const edges = self.edges || [];
|
||||
const center = self.center;
|
||||
if (nodes.length === 0) {
|
||||
if (!nodes || nodes.length === 0) {
|
||||
return;
|
||||
} else if (nodes.length === 1) {
|
||||
nodes[0].x = center[0];
|
||||
@ -121,7 +121,7 @@ export default class RadialLayout extends BaseLayout {
|
||||
}
|
||||
const linkDistance = self.linkDistance;
|
||||
// layout
|
||||
let focusNode: Node;
|
||||
let focusNode: Node | null = null;
|
||||
if (isString(self.focusNode)) {
|
||||
let found = false;
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
@ -163,8 +163,8 @@ export default class RadialLayout extends BaseLayout {
|
||||
if (!self.height && typeof window !== 'undefined') {
|
||||
self.height = window.innerHeight;
|
||||
}
|
||||
const width = self.width;
|
||||
const height = self.height;
|
||||
const width = self.width || 500;
|
||||
const height = self.height || 500;
|
||||
let semiWidth = width - center[0] > center[0] ? center[0] : width - center[0];
|
||||
let semiHeight = height - center[1] > center[1] ? center[1] : height - center[1];
|
||||
if (semiWidth === 0) {
|
||||
@ -177,7 +177,7 @@ export default class RadialLayout extends BaseLayout {
|
||||
const maxRadius = semiHeight > semiWidth ? semiWidth : semiHeight;
|
||||
const maxD = Math.max(...focusNodeD);
|
||||
// the radius for each nodes away from focusNode
|
||||
const radii = [];
|
||||
const radii: number[] = [];
|
||||
focusNodeD.forEach((value, i) => {
|
||||
if (!self.unitRadius) {
|
||||
self.unitRadius = maxRadius / maxD;
|
||||
@ -196,7 +196,7 @@ export default class RadialLayout extends BaseLayout {
|
||||
// the initial positions from mds
|
||||
const mds = new MDS({ distances: eIdealD, linkDistance });
|
||||
let positions = mds.layout();
|
||||
positions.forEach((p) => {
|
||||
positions.forEach((p: IPointTuple) => {
|
||||
if (isNaN(p[0])) {
|
||||
p[0] = Math.random() * linkDistance;
|
||||
}
|
||||
@ -205,12 +205,12 @@ export default class RadialLayout extends BaseLayout {
|
||||
}
|
||||
});
|
||||
self.positions = positions;
|
||||
positions.forEach((p, i) => {
|
||||
positions.forEach((p: IPointTuple, i: number) => {
|
||||
nodes[i].x = p[0] + center[0];
|
||||
nodes[i].y = p[1] + center[1];
|
||||
});
|
||||
// move the graph to origin, centered at focusNode
|
||||
positions.forEach((p) => {
|
||||
positions.forEach((p: IPointTuple) => {
|
||||
p[0] -= positions[focusIndex][0];
|
||||
p[1] -= positions[focusIndex][1];
|
||||
});
|
||||
@ -222,7 +222,7 @@ export default class RadialLayout extends BaseLayout {
|
||||
// stagger the overlapped nodes
|
||||
if (preventOverlap) {
|
||||
const nodeSpacing = self.nodeSpacing;
|
||||
let nodeSpacingFunc;
|
||||
let nodeSpacingFunc: Function;
|
||||
if (isNumber(nodeSpacing)) {
|
||||
nodeSpacingFunc = () => {
|
||||
return nodeSpacing;
|
||||
@ -235,7 +235,7 @@ export default class RadialLayout extends BaseLayout {
|
||||
};
|
||||
}
|
||||
if (!nodeSize) {
|
||||
nodeSizeFunc = (d) => {
|
||||
nodeSizeFunc = (d: NodeConfig) => {
|
||||
if (d.size) {
|
||||
if (isArray(d.size)) {
|
||||
const res = d.size[0] > d.size[1] ? d.size[0] : d.size[1];
|
||||
@ -247,17 +247,17 @@ export default class RadialLayout extends BaseLayout {
|
||||
};
|
||||
} else {
|
||||
if (isArray(nodeSize)) {
|
||||
nodeSizeFunc = (d) => {
|
||||
nodeSizeFunc = (d: NodeConfig) => {
|
||||
const res = nodeSize[0] > nodeSize[1] ? nodeSize[0] : nodeSize[1];
|
||||
return res + nodeSpacingFunc(d);
|
||||
};
|
||||
} else {
|
||||
nodeSizeFunc = (d) => {
|
||||
nodeSizeFunc = (d: NodeConfig) => {
|
||||
return nodeSize + nodeSpacingFunc(d);
|
||||
};
|
||||
}
|
||||
}
|
||||
const nonoverlapForce = new RadialNonoverlapForce({
|
||||
const nonoverlapForceParams: RadialNonoverlapForceParam = {
|
||||
nodeSizeFunc,
|
||||
adjMatrix,
|
||||
positions,
|
||||
@ -269,11 +269,12 @@ export default class RadialLayout extends BaseLayout {
|
||||
iterations: self.maxPreventOverlapIteration || 200,
|
||||
k: positions.length / 4.5,
|
||||
nodes,
|
||||
});
|
||||
};
|
||||
const nonoverlapForce = new RadialNonoverlapForce(nonoverlapForceParams);
|
||||
positions = nonoverlapForce.layout();
|
||||
}
|
||||
// move the graph to center
|
||||
positions.forEach((p, i) => {
|
||||
positions.forEach((p: IPointTuple, i: number) => {
|
||||
nodes[i].x = p[0] + center[0];
|
||||
nodes[i].y = p[1] + center[1];
|
||||
});
|
||||
@ -281,21 +282,21 @@ export default class RadialLayout extends BaseLayout {
|
||||
public run() {
|
||||
const self = this;
|
||||
const maxIteration = self.maxIteration;
|
||||
const positions = self.positions;
|
||||
const W = self.weights;
|
||||
const eIdealDis = self.eIdealDistances;
|
||||
const radii = self.radii;
|
||||
const positions = self.positions || [];
|
||||
const W = self.weights|| [];
|
||||
const eIdealDis = self.eIdealDistances || [];
|
||||
const radii = self.radii || [];
|
||||
for (let i = 0; i <= maxIteration; i++) {
|
||||
const param = i / maxIteration;
|
||||
self.oneIteration(param, positions, radii, eIdealDis, W);
|
||||
}
|
||||
}
|
||||
|
||||
private oneIteration(param, positions, radii, D, W) {
|
||||
private oneIteration(param: number, positions: IPointTuple[], radii: number[], D: Matrix[], W: Matrix[]) {
|
||||
const self = this;
|
||||
const vparam = 1 - param;
|
||||
const focusIndex = self.focusIndex;
|
||||
positions.forEach((v, i) => {
|
||||
positions.forEach((v: IPointTuple, i: number) => {
|
||||
// v
|
||||
const originDis = getEDistance(v, [0, 0]);
|
||||
const reciODis = originDis === 0 ? 0 : 1 / originDis;
|
||||
@ -335,16 +336,17 @@ export default class RadialLayout extends BaseLayout {
|
||||
});
|
||||
}
|
||||
|
||||
private eIdealDisMatrix() {
|
||||
private eIdealDisMatrix(): Matrix[] {
|
||||
const self = this;
|
||||
const nodes = self.nodes;
|
||||
if (!nodes) return [];
|
||||
const D = self.distances;
|
||||
const linkDis = self.linkDistance;
|
||||
const radii = self.radii;
|
||||
const unitRadius = self.unitRadius;
|
||||
const result = [];
|
||||
const nodes = self.nodes;
|
||||
D.forEach((row, i) => {
|
||||
const newRow: number[] = [];
|
||||
const radii = self.radii || [];
|
||||
const unitRadius = self.unitRadius || 50;
|
||||
const result: Matrix[] = [];
|
||||
D && D.forEach((row, i) => {
|
||||
const newRow: Matrix = [];
|
||||
row.forEach((v, j) => {
|
||||
if (i === j) {
|
||||
newRow.push(0);
|
||||
@ -375,7 +377,7 @@ export default class RadialLayout extends BaseLayout {
|
||||
return result;
|
||||
}
|
||||
|
||||
private handleInfinity(matrix, focusIndex, step) {
|
||||
private handleInfinity(matrix: Matrix[], focusIndex: number, step: number) {
|
||||
const length = matrix.length;
|
||||
// 遍历 matrix 中遍历 focus 对应行
|
||||
for (let i = 0; i < length; i++) {
|
||||
@ -407,7 +409,7 @@ export default class RadialLayout extends BaseLayout {
|
||||
}
|
||||
}
|
||||
|
||||
private maxToFocus(matrix, focusIndex) {
|
||||
private maxToFocus(matrix: Matrix[], focusIndex: number): number {
|
||||
let max = 0;
|
||||
for (let i = 0; i < matrix[focusIndex].length; i++) {
|
||||
if (matrix[focusIndex][i] === Infinity) {
|
||||
|
@ -1,14 +1,33 @@
|
||||
import { Matrix, IPointTuple } from '../../types';
|
||||
import { Point } from '@antv/g-base';
|
||||
|
||||
const SPEED_DIVISOR = 800;
|
||||
|
||||
export type RadialNonoverlapForceParam = {
|
||||
positions: IPointTuple[];
|
||||
adjMatrix: Matrix[];
|
||||
focusID: number;
|
||||
radii: number[];
|
||||
iterations?: number;
|
||||
height?: number;
|
||||
width?: number;
|
||||
speed?: number;
|
||||
gravity?: number;
|
||||
nodeSizeFunc: (node: any) => number;
|
||||
k: number;
|
||||
strictRadial: boolean;
|
||||
nodes: any[];
|
||||
};
|
||||
|
||||
export default class RadialNonoverlapForce {
|
||||
/** node positions */
|
||||
public positions: any[];
|
||||
public positions: IPointTuple[];
|
||||
/** adjacency matrix */
|
||||
public adjMatrix: any[];
|
||||
public adjMatrix: Matrix[];
|
||||
/** focus node */
|
||||
public focusID: number;
|
||||
/** radii */
|
||||
public radii: number;
|
||||
public radii: number[];
|
||||
/** the number of iterations */
|
||||
public iterations: number;
|
||||
/** the height of the canvas */
|
||||
@ -24,19 +43,14 @@ export default class RadialNonoverlapForce {
|
||||
/** the strength of forces */
|
||||
public k: number;
|
||||
/** if each circle can be separated into subcircles to avoid overlappings */
|
||||
public strictRadial: number;
|
||||
public strictRadial: boolean;
|
||||
/** the nodes data */
|
||||
public nodes: any[];
|
||||
|
||||
private maxDisplace: number;
|
||||
private disp: Array<{
|
||||
x: number;
|
||||
y: number;
|
||||
dx?: number;
|
||||
dy?: number;
|
||||
}>;
|
||||
private maxDisplace: number | undefined;
|
||||
private disp: Point[] = [];
|
||||
|
||||
constructor(params) {
|
||||
constructor(params: RadialNonoverlapForceParam) {
|
||||
this.positions = params.positions;
|
||||
this.adjMatrix = params.adjMatrix;
|
||||
this.focusID = params.focusID;
|
||||
@ -52,10 +66,10 @@ export default class RadialNonoverlapForce {
|
||||
this.nodes = params.nodes;
|
||||
}
|
||||
|
||||
public layout() {
|
||||
public layout(): IPointTuple[] {
|
||||
const self = this;
|
||||
const positions = self.positions;
|
||||
const disp = [];
|
||||
const disp: Point[] = [];
|
||||
const iterations = self.iterations;
|
||||
const maxDisplace = self.width / 10;
|
||||
self.maxDisplace = maxDisplace;
|
||||
@ -77,11 +91,11 @@ export default class RadialNonoverlapForce {
|
||||
const nodes = self.nodes;
|
||||
const disp = self.disp;
|
||||
const k = self.k;
|
||||
const radii = self.radii;
|
||||
const radii = self.radii || [];
|
||||
|
||||
positions.forEach((v, i) => {
|
||||
positions.forEach((v: IPointTuple, i: number) => {
|
||||
disp[i] = { x: 0, y: 0 };
|
||||
positions.forEach((u, j) => {
|
||||
positions.forEach((u: IPointTuple, j: number) => {
|
||||
if (i === j) {
|
||||
return;
|
||||
}
|
||||
@ -115,6 +129,7 @@ export default class RadialNonoverlapForce {
|
||||
const speed = self.speed;
|
||||
const strictRadial = self.strictRadial;
|
||||
const f = self.focusID;
|
||||
const maxDisplace = self.maxDisplace || self.width / 10;
|
||||
|
||||
if (strictRadial) {
|
||||
disp.forEach((di, i) => {
|
||||
@ -136,12 +151,6 @@ export default class RadialNonoverlapForce {
|
||||
});
|
||||
}
|
||||
|
||||
// speed
|
||||
positions.forEach((_, i) => {
|
||||
disp[i].dx *= speed / SPEED_DIVISOR;
|
||||
disp[i].dy *= speed / SPEED_DIVISOR;
|
||||
});
|
||||
|
||||
// move
|
||||
const radii = self.radii;
|
||||
positions.forEach((n, i) => {
|
||||
@ -150,7 +159,7 @@ export default class RadialNonoverlapForce {
|
||||
}
|
||||
const distLength = Math.sqrt(disp[i].x * disp[i].x + disp[i].y * disp[i].y);
|
||||
if (distLength > 0 && i !== f) {
|
||||
const limitedDist = Math.min(self.maxDisplace * (speed / SPEED_DIVISOR), distLength);
|
||||
const limitedDist = Math.min(maxDisplace * (speed / SPEED_DIVISOR), distLength);
|
||||
n[0] += (disp[i].x / distLength) * limitedDist;
|
||||
n[1] += (disp[i].y / distLength) * limitedDist;
|
||||
if (strictRadial) {
|
||||
|
@ -11,11 +11,11 @@ import { BaseLayout } from './layout';
|
||||
*/
|
||||
export default class RandomLayout extends BaseLayout {
|
||||
/** 布局中心 */
|
||||
public center: IPointTuple;
|
||||
public center: IPointTuple = [0, 0];
|
||||
/** 宽度 */
|
||||
public width: number;
|
||||
public width: number = 300;
|
||||
/** 高度 */
|
||||
public height: number;
|
||||
public height: number = 300;
|
||||
|
||||
public getDefaultCfg() {
|
||||
return {
|
||||
@ -38,7 +38,7 @@ export default class RandomLayout extends BaseLayout {
|
||||
if (!self.height && typeof window !== 'undefined') {
|
||||
self.height = window.innerHeight;
|
||||
}
|
||||
nodes.forEach((node) => {
|
||||
nodes && nodes.forEach((node) => {
|
||||
node.x = (Math.random() - 0.5) * layoutScale * self.width + center[0];
|
||||
node.y = (Math.random() - 0.5) * layoutScale * self.height + center[1];
|
||||
});
|
||||
|
@ -1,7 +1,7 @@
|
||||
import Base, { IPluginBaseConfig } from '../base'
|
||||
import Edge from '../../item/edge';
|
||||
import Graph from '../../graph/graph';
|
||||
import { GraphData, NodeConfig, NodeMapConfig, EdgeConfig } from '../../types';
|
||||
import { GraphData, NodeConfig, NodeConfigMap, EdgeConfig } from '../../types';
|
||||
import { Point } from '@antv/g-base/lib/types';
|
||||
|
||||
interface BundlingConfig extends IPluginBaseConfig {
|
||||
@ -90,7 +90,7 @@ export default class Bundling extends Base {
|
||||
|
||||
const edges = data.edges;
|
||||
const nodes = data.nodes;
|
||||
const nodeIdMap: NodeMapConfig = {};
|
||||
const nodeIdMap: NodeConfigMap = {};
|
||||
let error = false;
|
||||
|
||||
nodes.forEach(node => {
|
||||
@ -186,7 +186,7 @@ export default class Bundling extends Base {
|
||||
public divideEdges(divisions: number): Point[][] {
|
||||
const self = this;
|
||||
const edges: EdgeConfig[] = self.get('data').edges;
|
||||
const nodeIdMap: NodeMapConfig = self.get('nodeIdMap');
|
||||
const nodeIdMap: NodeConfigMap = self.get('nodeIdMap');
|
||||
let edgePoints = self.get('edgePoints');
|
||||
|
||||
if (!edgePoints || edgePoints === undefined) edgePoints = [];
|
||||
@ -264,7 +264,7 @@ export default class Bundling extends Base {
|
||||
const edges = data.edges;
|
||||
|
||||
const bundleThreshold: number = self.get('bundleThreshold');
|
||||
const nodeIdMap: NodeMapConfig = self.get('nodeIdMap');
|
||||
const nodeIdMap: NodeConfigMap = self.get('nodeIdMap');
|
||||
let edgeBundles = self.get('edgeBundles');
|
||||
|
||||
if (!edgeBundles) edgeBundles = [];
|
||||
|
@ -5,6 +5,7 @@ import ShapeBase from '@antv/g-canvas/lib/shape/base';
|
||||
import Node from '../item/node';
|
||||
import { IGraph } from '../interface/graph';
|
||||
import { IEdge, INode } from '../interface/item';
|
||||
import { ILabelConfig } from '../interface/shape';
|
||||
|
||||
// Math types
|
||||
export interface IPoint {
|
||||
@ -117,7 +118,7 @@ export type ModelStyle = Partial<{
|
||||
};
|
||||
// loop edge config
|
||||
loopCfg: LoopConfig;
|
||||
labelCfg?: object;
|
||||
labelCfg?: ILabelConfig;
|
||||
anchorPoints: number[][];
|
||||
controlPoints: IPoint[];
|
||||
size: number | number[];
|
||||
@ -171,17 +172,14 @@ export type Easeing =
|
||||
| 'easeQuadOut'
|
||||
| 'easeQuadInOut'
|
||||
| string;
|
||||
|
||||
|
||||
export interface ModelConfig extends ModelStyle {
|
||||
// ⚠️ 节点或边的类型,后续会废弃
|
||||
shape?: string;
|
||||
// 节点或边的类型
|
||||
type?: string;
|
||||
label?: string;
|
||||
labelCfg?: {
|
||||
style?: object;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
labelCfg?: ILabelConfig;
|
||||
descriptionCfg?: {
|
||||
style?: object;
|
||||
[key: string]: unknown;
|
||||
@ -205,8 +203,10 @@ export interface ModelConfig extends ModelStyle {
|
||||
endPoint?: IPoint;
|
||||
children?: TreeGraphData[];
|
||||
}
|
||||
|
||||
export interface NodeConfig extends ModelConfig {
|
||||
id: string;
|
||||
size?: number | number[];
|
||||
groupId?: string;
|
||||
description?: string;
|
||||
}
|
||||
@ -216,10 +216,7 @@ export interface EdgeConfig extends ModelConfig {
|
||||
source?: string;
|
||||
target?: string;
|
||||
label?: string;
|
||||
labelCfg?: {
|
||||
style?: object;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
labelCfg?: ILabelConfig;
|
||||
sourceNode?: Node;
|
||||
targetNode?: Node;
|
||||
startPoint?: IPoint;
|
||||
@ -236,7 +233,11 @@ export type EdgeData = EdgeConfig & {
|
||||
endPoint: IPoint;
|
||||
};
|
||||
|
||||
export interface NodeMapConfig {
|
||||
export interface NodeMap {
|
||||
[key: string]: INode;
|
||||
}
|
||||
|
||||
export interface NodeConfigMap {
|
||||
[key: string]: NodeConfig;
|
||||
}
|
||||
|
||||
@ -347,7 +348,7 @@ export type BehaviorOpation<U> = {
|
||||
export type IEvent = Record<G6Event, string>;
|
||||
|
||||
export interface IG6GraphEvent extends GraphEvent {
|
||||
item: Item;
|
||||
item: Item | null;
|
||||
canvasX: number;
|
||||
canvasY: number;
|
||||
wheelDelta: number;
|
||||
@ -361,8 +362,19 @@ export type Item = INode | IEdge;
|
||||
|
||||
export type ITEM_TYPE = 'node' | 'edge' | 'group'
|
||||
|
||||
export type NodeIdxMap = {
|
||||
[key: string]: number
|
||||
}
|
||||
|
||||
// 触发 viewportchange 事件的参数
|
||||
export interface ViewPortEventParam {
|
||||
action: string;
|
||||
matrix: Matrix;
|
||||
}
|
||||
|
||||
export interface Indexable<T> { [key:string]: T}
|
||||
|
||||
export interface LayoutConfig {
|
||||
type?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { Point } from '@antv/g-base/lib/types';
|
||||
import { IGroup } from '@antv/g-canvas/lib/interfaces';
|
||||
import { mat3, transform, vec3 } from '@antv/matrix-util';
|
||||
import isArray from '@antv/util/lib/is-array'
|
||||
import { GraphData, ICircle, IEllipse, IRect, Matrix } from '../types';
|
||||
import { GraphData, ICircle, IEllipse, IRect, Matrix, EdgeConfig, NodeIdxMap } from '../types';
|
||||
|
||||
/**
|
||||
* 是否在区间内
|
||||
@ -382,4 +382,20 @@ export const rotate = (group: IGroup, angle: number) => {
|
||||
])
|
||||
|
||||
group.setMatrix(matrix)
|
||||
}
|
||||
|
||||
export const getDegree = (n: number, nodeIdxMap: NodeIdxMap, edges: EdgeConfig[]): number[] => {
|
||||
const degrees: number[] = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
degrees[i] = 0;
|
||||
}
|
||||
edges.forEach((e) => {
|
||||
if (e.source) {
|
||||
degrees[nodeIdxMap[e.source]] += 1;
|
||||
}
|
||||
if (e.target) {
|
||||
degrees[nodeIdxMap[e.target]] += 1;
|
||||
}
|
||||
});
|
||||
return degrees;
|
||||
}
|
@ -1,10 +1,11 @@
|
||||
import G6 from '../../../src';
|
||||
import { NodeConfig, EdgeConfig } from '../../../src/types';
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.id = 'graph-spec';
|
||||
document.body.appendChild(div);
|
||||
|
||||
const data: {nodes: object, edges: object} = {
|
||||
const data: {nodes: NodeConfig[], edges: EdgeConfig[]} = {
|
||||
nodes: [
|
||||
{
|
||||
id: '0',
|
||||
|
@ -32,6 +32,9 @@ describe('layout using web worker', function() {
|
||||
callback();
|
||||
});
|
||||
graph.render();
|
||||
setTimeout(() => {
|
||||
callback();
|
||||
}, 1000);
|
||||
|
||||
function callback() {
|
||||
let count = 0;
|
||||
@ -41,7 +44,6 @@ describe('layout using web worker', function() {
|
||||
graph.once('afterlayout', () => {
|
||||
expect(node.x).not.toEqual(undefined);
|
||||
expect(node.y).not.toEqual(undefined);
|
||||
// FIXME:
|
||||
expect(count >= 1).toEqual(true);
|
||||
expect(ended).toEqual(true);
|
||||
graph.destroy();
|
||||
|
Loading…
Reference in New Issue
Block a user