mirror of
https://gitee.com/antv/g6.git
synced 2024-11-29 18:28:19 +08:00
feat: collapse expand combo to add virtual edges to represent the edges between collapsed combos.
This commit is contained in:
parent
4a0752de14
commit
a67c1c4b56
@ -40,6 +40,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
graph.collapseExpandCombo(comboId);
|
||||
graph.layout();
|
||||
if (graph.get('layoutCfg')) graph.layout();
|
||||
else graph.refreshPositions();
|
||||
},
|
||||
};
|
||||
|
@ -64,6 +64,7 @@ export default {
|
||||
},
|
||||
size: [20, 5],
|
||||
color: '#A3B1BF',
|
||||
padding: [25, 20, 15, 20]
|
||||
},
|
||||
// 节点应用状态后的样式,默认仅提供 active 和 selected 用户可以自己扩展
|
||||
nodeStateStyle: {},
|
||||
|
@ -17,6 +17,7 @@ import { traverseTreeUp, traverseTree, getComboBBox } from '../../util/graphic';
|
||||
|
||||
const NODE = 'node';
|
||||
const EDGE = 'edge';
|
||||
const VEDGE = 'vedge';
|
||||
const COMBO = 'combo';
|
||||
const CFG_PREFIX = 'default';
|
||||
const MAPPER_SUFFIX = 'Mapper';
|
||||
@ -46,11 +47,12 @@ export default class ItemController {
|
||||
public addItem<T extends Item>(type: ITEM_TYPE, model: ModelConfig) {
|
||||
const { graph } = this;
|
||||
const parent: Group = graph.get(`${type}Group`) || graph.get('group');
|
||||
const upperType = upperFirst(type);
|
||||
const vType = type === VEDGE ? EDGE : type;
|
||||
const upperType = upperFirst(vType);
|
||||
|
||||
let item: Item | null = null;
|
||||
// 获取 this.get('styles') 中的值
|
||||
let styles = graph.get(type + upperFirst(STATE_SUFFIX)) || {};
|
||||
let styles = graph.get(vType + upperFirst(STATE_SUFFIX)) || {};
|
||||
const defaultModel = graph.get(CFG_PREFIX + upperType);
|
||||
|
||||
if (model[STATE_SUFFIX]) {
|
||||
@ -58,7 +60,7 @@ export default class ItemController {
|
||||
styles = model[STATE_SUFFIX];
|
||||
}
|
||||
|
||||
const mapper = graph.get(type + MAPPER_SUFFIX);
|
||||
const mapper = graph.get(vType + MAPPER_SUFFIX);
|
||||
if (mapper) {
|
||||
const mappedModel = mapper(model);
|
||||
if (mappedModel[STATE_SUFFIX]) {
|
||||
@ -88,7 +90,7 @@ export default class ItemController {
|
||||
|
||||
graph.emit('beforeadditem', { type, model });
|
||||
|
||||
if (type === EDGE) {
|
||||
if (type === EDGE || type === VEDGE) {
|
||||
let source: Id;
|
||||
let target: Id;
|
||||
source = (model as EdgeConfig).source; // eslint-disable-line prefer-destructuring
|
||||
@ -96,9 +98,11 @@ export default class ItemController {
|
||||
|
||||
if (source && isString(source)) {
|
||||
source = graph.findById(source);
|
||||
if (source.getType() === 'combo') model.isComboEdge = true;
|
||||
}
|
||||
if (target && isString(target)) {
|
||||
target = graph.findById(target);
|
||||
if (target.getType() === 'combo') model.isComboEdge = true;
|
||||
}
|
||||
|
||||
if (!source || !target) {
|
||||
@ -123,6 +127,7 @@ export default class ItemController {
|
||||
}
|
||||
else if (type === COMBO) {
|
||||
const children: ComboTree[] = (model as ComboConfig).children;
|
||||
|
||||
const comboBBox = getComboBBox(children, graph);
|
||||
model.x = comboBBox.x || Math.random() * 100;
|
||||
model.y = comboBBox.y || Math.random() * 100;
|
||||
@ -252,7 +257,6 @@ export default class ItemController {
|
||||
if (!combo || combo.destroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const comboBBox = getComboBBox(children, graph);
|
||||
|
||||
combo.set('bbox', comboBBox);
|
||||
@ -260,6 +264,7 @@ export default class ItemController {
|
||||
x: comboBBox.x,
|
||||
y: comboBBox.y
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -322,9 +327,14 @@ export default class ItemController {
|
||||
graph.emit('beforeremoveitem', { item });
|
||||
|
||||
const type = item.getType();
|
||||
const items = graph.get(`${item.getType()}s`);
|
||||
const items = graph.get(`${type}s`);
|
||||
const index = items.indexOf(item);
|
||||
items.splice(index, 1);
|
||||
if (index > -1) items.splice(index, 1);
|
||||
if (type === EDGE) {
|
||||
const vitems = graph.get(`v${type}s`);
|
||||
const vindex = vitems.indexOf(item);
|
||||
if (vindex > -1) vitems.splice(vindex, 1);
|
||||
}
|
||||
|
||||
const itemId: string = item.get('id');
|
||||
const itemMap: NodeMap = graph.get('itemMap');
|
||||
|
@ -366,12 +366,12 @@ export default class LayoutController {
|
||||
nodes.push(model);
|
||||
});
|
||||
edgeItems.forEach(edgeItem => {
|
||||
if (!edgeItem.isVisible()) return;
|
||||
if (edgeItem.destroyed || !edgeItem.isVisible()) return;
|
||||
const model = edgeItem.getModel();
|
||||
edges.push(model);
|
||||
if (!model.isComboEdge) edges.push(model);
|
||||
});
|
||||
comboItems.forEach(comboItem => {
|
||||
if (!comboItem.isVisible()) return;
|
||||
if (comboItem.destroyed || !comboItem.isVisible()) return;
|
||||
const model = comboItem.getModel();
|
||||
combos.push(model);
|
||||
});
|
||||
|
@ -70,6 +70,8 @@ export interface PrivateGraphOption extends GraphOptions {
|
||||
|
||||
edges: EdgeConfig[];
|
||||
|
||||
vedges: EdgeConfig[];
|
||||
|
||||
groups: GroupConfig[];
|
||||
|
||||
combos: ComboConfig[];
|
||||
@ -299,6 +301,10 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
* store all the combo instances
|
||||
*/
|
||||
combos: [],
|
||||
/**
|
||||
* store all the edge instances which are virtual edges related to collapsed combo
|
||||
*/
|
||||
vedges: [],
|
||||
/**
|
||||
* all the instances indexed by id
|
||||
*/
|
||||
@ -844,8 +850,8 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
found = true;
|
||||
const newCombo: ComboTree = {
|
||||
id: model.id as string,
|
||||
depth: child.depth + 1,
|
||||
...model as ComboTree
|
||||
depth: child.depth + 2,
|
||||
...model
|
||||
}
|
||||
if (child.children) child.children.push(newCombo);
|
||||
else child.children = [newCombo];
|
||||
@ -891,7 +897,7 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
|
||||
const combos = this.get('combos');
|
||||
if (combos && combos.length > 0) {
|
||||
this.sortCombos(this.save() as GraphData);
|
||||
this.sortCombos();
|
||||
}
|
||||
this.autoPaint();
|
||||
return item;
|
||||
@ -972,9 +978,6 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
self.add('node', node);
|
||||
});
|
||||
|
||||
each(edges, (edge: EdgeConfig) => {
|
||||
self.add('edge', edge);
|
||||
});
|
||||
|
||||
// process the data to tree structure
|
||||
if (combos && combos.length !== 0) {
|
||||
@ -984,6 +987,10 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
self.addCombos(combos);
|
||||
}
|
||||
|
||||
each(edges, (edge: EdgeConfig) => {
|
||||
self.add('edge', edge);
|
||||
});
|
||||
|
||||
// layout
|
||||
const layoutController = self.get('layoutController');
|
||||
if (!layoutController.layout(success)) {
|
||||
@ -999,7 +1006,7 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
|
||||
if (!this.get('groupByTypes')) {
|
||||
if (combos && combos.length !== 0) {
|
||||
this.sortCombos(data);
|
||||
this.sortCombos();
|
||||
} else {
|
||||
// 为提升性能,选择数量少的进行操作
|
||||
if (data.nodes && data.edges && data.nodes.length < data.edges.length) {
|
||||
@ -1122,6 +1129,15 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
}
|
||||
});
|
||||
|
||||
// clear the destroyed combos here to avoid removing sub nodes before removing the parent combo
|
||||
const comboItems = this.getCombos();
|
||||
const combosLength = comboItems.length;
|
||||
for (let i = combosLength - 1; i >= 0; i--) {
|
||||
if (comboItems[i].destroyed) {
|
||||
comboItems.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// process the data to tree structure
|
||||
const combosData = (data as GraphData).combos;
|
||||
if (combosData) {
|
||||
@ -1129,9 +1145,9 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
this.set('comboTrees', comboTrees);
|
||||
// add combos
|
||||
self.addCombos(combosData);
|
||||
if (!this.get('groupByTypes')) this.sortCombos(data as GraphData);
|
||||
}
|
||||
|
||||
if (!this.get('groupByTypes')) this.sortCombos();
|
||||
}
|
||||
|
||||
this.set({ nodes: items.nodes, edges: items.edges });
|
||||
|
||||
@ -1231,7 +1247,7 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
return true;
|
||||
});
|
||||
});
|
||||
self.sortCombos(self.get('data'));
|
||||
self.sortCombos();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1376,6 +1392,7 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
} else {
|
||||
const nodes: INode[] = self.get('nodes');
|
||||
const edges: IEdge[] = self.get('edges');
|
||||
const vedges: IEdge[] = self.get('edges');
|
||||
|
||||
each(nodes, (node: INode) => {
|
||||
node.refresh();
|
||||
@ -1384,6 +1401,10 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
each(edges, (edge: IEdge) => {
|
||||
edge.refresh();
|
||||
});
|
||||
|
||||
each(vedges, (vedge: IEdge) => {
|
||||
vedge.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -1534,6 +1555,7 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
|
||||
const nodes: INode[] = self.get('nodes');
|
||||
const edges: IEdge[] = self.get('edges');
|
||||
const vedges: IEdge[] = self.get('vedges');
|
||||
const combos: ICombo[] = self.get('combos')
|
||||
|
||||
let model: NodeConfig;
|
||||
@ -1549,17 +1571,21 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
updatedNodes[model.id] = true;
|
||||
});
|
||||
|
||||
if (combos && combos.length !== 0) {
|
||||
self.updateCombos();
|
||||
}
|
||||
|
||||
each(edges, (edge: IEdge) => {
|
||||
const sourceModel = edge.getSource().getModel();
|
||||
const targetModel = edge.getTarget().getModel();
|
||||
if (updatedNodes[sourceModel.id as string] || updatedNodes[targetModel.id as string]) {
|
||||
if (updatedNodes[sourceModel.id as string] || updatedNodes[targetModel.id as string] || edge.getModel().isComboEdge) {
|
||||
edge.refresh();
|
||||
}
|
||||
});
|
||||
|
||||
if (combos && combos.length !== 0) {
|
||||
self.updateCombos();
|
||||
}
|
||||
each(vedges, (vedge: IEdge) => {
|
||||
vedge.refresh();
|
||||
});
|
||||
|
||||
self.emit('aftergraphrefreshposition');
|
||||
self.autoPaint();
|
||||
@ -1852,6 +1878,7 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
public layout(): void {
|
||||
const layoutController = this.get('layoutController');
|
||||
const layoutCfg = this.get('layout');
|
||||
if (!layoutCfg) return;
|
||||
|
||||
if (layoutCfg.workerEnabled) {
|
||||
// 如果使用web worker布局
|
||||
@ -1874,6 +1901,83 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
combo = this.findById(combo) as ICombo;
|
||||
}
|
||||
const comboModel = combo.getModel();
|
||||
|
||||
// add virtual edges
|
||||
const edges = this.getEdges().concat(this.get('vedges'));
|
||||
const cnodes = combo.getNodes();
|
||||
const ccombos = combo.getCombos();
|
||||
|
||||
const processedNodes = {};
|
||||
const addedVEdges = [];
|
||||
edges.forEach(edge => {
|
||||
const source = edge.getSource();
|
||||
const target = edge.getTarget();
|
||||
if (((cnodes.includes(source) || ccombos.includes(source))
|
||||
&& (!cnodes.includes(target) && !ccombos.includes(target)))
|
||||
|| (source.getModel().id === comboModel.id)) {
|
||||
const edgeModel = edge.getModel();
|
||||
if (edgeModel.isVEdge) {
|
||||
this.removeItem(edge);
|
||||
return;
|
||||
}
|
||||
let targetModel = target.getModel();
|
||||
if (!target.isVisible()) {
|
||||
targetModel = this.findById((targetModel.parentId as string) || (targetModel.comboId as string)).getModel();
|
||||
}
|
||||
const targetId = targetModel.id;
|
||||
|
||||
if (processedNodes[targetId]) {
|
||||
processedNodes[targetId] += (edgeModel.size || 1);
|
||||
return;
|
||||
}
|
||||
// the source is in the combo, the target is not
|
||||
const vedge = this.addItem('vedge', {
|
||||
source: comboModel.id,
|
||||
target: targetId,
|
||||
isVEdge: true,
|
||||
});
|
||||
processedNodes[targetId] = edgeModel.size || 1;
|
||||
addedVEdges.push(vedge);
|
||||
} else if (((!cnodes.includes(source) && !ccombos.includes(source))
|
||||
&& (cnodes.includes(target) || ccombos.includes(target)))
|
||||
|| (target.getModel().id === comboModel.id)) {
|
||||
const edgeModel = edge.getModel();
|
||||
if (edgeModel.isVEdge) {
|
||||
this.removeItem(edge);
|
||||
return;
|
||||
}
|
||||
let sourceModel = source.getModel();
|
||||
if (!target.isVisible()) {
|
||||
sourceModel = this.findById((sourceModel.parentId as string) || (sourceModel.comboId as string)).getModel();
|
||||
}
|
||||
const sourceId = sourceModel.id;
|
||||
if (processedNodes[sourceId]) {
|
||||
processedNodes[sourceId] += (edgeModel.size || 1);
|
||||
return;
|
||||
}
|
||||
// the target is in the combo, the source is not
|
||||
const vedge = this.addItem('vedge', {
|
||||
target: comboModel.id,
|
||||
source: sourceId,
|
||||
isVEdge: true
|
||||
});
|
||||
processedNodes[sourceId] = edgeModel.size || 1;
|
||||
addedVEdges.push(vedge);
|
||||
}
|
||||
});
|
||||
addedVEdges.forEach(vedge => {
|
||||
const vedgeModel = vedge.getModel();
|
||||
if (vedgeModel.source === comboModel.id) {
|
||||
this.updateItem(vedge, {
|
||||
size: processedNodes[vedge.getTarget().get('id')]
|
||||
})
|
||||
} else if (vedgeModel.target === comboModel.id) {
|
||||
this.updateItem(vedge, {
|
||||
size: processedNodes[vedge.getSource().get('id')]
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
const itemController: ItemController = this.get('itemController');
|
||||
itemController.collapseCombo(combo);
|
||||
// update combo size
|
||||
@ -1889,24 +1993,81 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
if (isString(combo)) {
|
||||
combo = this.findById(combo) as ICombo;
|
||||
}
|
||||
const comboModel = combo.getModel();
|
||||
|
||||
// add virtual edges
|
||||
const edges = this.getEdges().concat(this.get('vedges'));
|
||||
const cnodes = combo.getNodes();
|
||||
const ccombos = combo.getCombos();
|
||||
|
||||
const processedNodes = {};
|
||||
const addedVEdges = {};
|
||||
edges.forEach(edge => {
|
||||
const source = edge.getSource();
|
||||
const target = edge.getTarget();
|
||||
const sourceId = source.get('id');
|
||||
const targetId = target.get('id');
|
||||
if (((cnodes.includes(source) || ccombos.includes(source))
|
||||
&& (!cnodes.includes(target) && !ccombos.includes(target)))
|
||||
|| sourceId === comboModel.id) {
|
||||
if (edge.getModel().isVEdge) {
|
||||
this.removeItem(edge);
|
||||
return;
|
||||
}
|
||||
// the source is in the combo, the target is not
|
||||
if (!target.isVisible()) {
|
||||
const oppsiteComboId: string = (target.getModel().comboId as string) || (target.getModel().parentId as string);
|
||||
if (oppsiteComboId) {
|
||||
const vedgeId = `${sourceId}-${oppsiteComboId}`;
|
||||
if (processedNodes[vedgeId]) {
|
||||
processedNodes[vedgeId] += (edge.getModel().size || 1);
|
||||
this.updateItem(addedVEdges[vedgeId], {
|
||||
size: processedNodes[vedgeId]
|
||||
})
|
||||
return;
|
||||
}
|
||||
const vedge = this.addItem('vedge', {
|
||||
source: sourceId,
|
||||
target: oppsiteComboId,
|
||||
isVEdge: true
|
||||
});
|
||||
processedNodes[vedgeId] = edge.getModel().size || 1;
|
||||
addedVEdges[vedgeId] = vedge;
|
||||
}
|
||||
}
|
||||
} else if (((!cnodes.includes(source) && !ccombos.includes(source))
|
||||
&& (cnodes.includes(target) || ccombos.includes(target)))
|
||||
|| targetId === comboModel.id) {
|
||||
if (edge.getModel().isVEdge) {
|
||||
this.removeItem(edge);
|
||||
return;
|
||||
}
|
||||
// the target is in the combo, the source is not
|
||||
if (!source.isVisible()) {
|
||||
const oppsiteComboId: string = (source.getModel().comboId as string) || (target.getModel().parentId as string);
|
||||
if (oppsiteComboId) {
|
||||
const vedgeId = `${oppsiteComboId}-${targetId}`;
|
||||
if (processedNodes[vedgeId]) {
|
||||
processedNodes[vedgeId] += (edge.getModel().size || 1);
|
||||
this.updateItem(addedVEdges[vedgeId], {
|
||||
size: processedNodes[vedgeId]
|
||||
})
|
||||
return;
|
||||
}
|
||||
const vedge = this.addItem('vedge', {
|
||||
target: targetId,
|
||||
source: oppsiteComboId,
|
||||
isVEdge: true
|
||||
});
|
||||
processedNodes[vedgeId] = edge.getModel().size || 1;
|
||||
addedVEdges[vedgeId] = vedge;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const itemController: ItemController = this.get('itemController');
|
||||
itemController.expandCombo(combo);
|
||||
|
||||
const comboModel = combo.getModel();
|
||||
// find the children from comboTrees
|
||||
const comboTrees = this.get('comboTrees');
|
||||
let children = [];
|
||||
comboTrees.forEach((ctree: ComboTree) => {
|
||||
let found = false;
|
||||
traverseTreeUp<ComboTree>(ctree, child => {
|
||||
if (comboModel.id === child.id) {
|
||||
children = child.children;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
// update combo size
|
||||
// itemController.updateCombo(combo, children);
|
||||
comboModel.collapsed = false;
|
||||
}
|
||||
|
||||
@ -1972,7 +2133,7 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
* 根据 comboTree 结构整理 Combo 相关的图形绘制层级,包括 Combo 本身、节点、边
|
||||
* @param {GraphData} data 数据
|
||||
*/
|
||||
private sortCombos(data: GraphData) {
|
||||
private sortCombos() {
|
||||
const depthMap = [];
|
||||
const dataDepthMap = {};
|
||||
const comboTrees = this.get('comboTrees');
|
||||
@ -1984,10 +2145,11 @@ export default class Graph extends EventEmitter implements IGraph {
|
||||
return true;
|
||||
});
|
||||
});
|
||||
const edges = data.edges;
|
||||
edges && edges.forEach(edge => {
|
||||
const sourceDepth: number = dataDepthMap[edge.source] || 0;
|
||||
const targetDepth: number = dataDepthMap[edge.target] || 0;
|
||||
const edges = this.getEdges().concat(this.get('vedges'));
|
||||
edges && edges.forEach(edgeItem => {
|
||||
const edge = edgeItem.getModel();
|
||||
const sourceDepth: number = dataDepthMap[edge.source as string] || 0;
|
||||
const targetDepth: number = dataDepthMap[edge.target as string] || 0;
|
||||
const depth = Math.max(sourceDepth, targetDepth);
|
||||
if (depthMap[depth]) depthMap[depth].push(edge.id);
|
||||
else depthMap[depth] = [edge.id];
|
||||
|
@ -227,10 +227,10 @@ export interface IItemBase {
|
||||
}
|
||||
|
||||
export interface IEdge extends IItemBase {
|
||||
setSource(source: INode): void;
|
||||
setTarget(target: INode): void;
|
||||
getSource(): INode;
|
||||
getTarget(): INode;
|
||||
setSource(source: INode | ICombo): void;
|
||||
setTarget(target: INode | ICombo): void;
|
||||
getSource(): INode | ICombo;
|
||||
getTarget(): INode | ICombo;
|
||||
}
|
||||
|
||||
export interface INode extends IItemBase {
|
||||
|
@ -4,11 +4,14 @@ import Node from './node';
|
||||
import { ComboConfig, IBBox, IShapeBase } from '../types';
|
||||
import Global from '../global';
|
||||
import { getBBox } from '../util/graphic';
|
||||
|
||||
import isNumber from '@antv/util/lib/is-number';
|
||||
import { IItemBaseConfig } from '../interface/item';
|
||||
|
||||
const CACHE_BBOX = 'bboxCache';
|
||||
const CACHE_CANVAS_BBOX = 'bboxCanvasCache';
|
||||
const CACHE_SIZE = 'sizeCache';
|
||||
const CACHE_ANCHOR_POINTS = 'anchorPointsCache';
|
||||
|
||||
|
||||
export default class Combo extends Node implements ICombo {
|
||||
public getDefaultCfg() {
|
||||
@ -26,14 +29,23 @@ export default class Combo extends Node implements ICombo {
|
||||
if (styles) {
|
||||
// merge graph的item样式与数据模型中的样式
|
||||
const newModel = model;
|
||||
const itemType = this.getType();
|
||||
const size = {
|
||||
r: Math.hypot(bbox.height, bbox.width) / 2 || Global.defaultCombo.size[0] / 2,
|
||||
width: bbox.width || Global.defaultCombo.size[0],
|
||||
height: bbox.height || Global.defaultCombo.size[1]
|
||||
};
|
||||
this.set(CACHE_SIZE, size);
|
||||
newModel.style = Object.assign({}, styles, model.style, size);
|
||||
let padding = model.padding || Global.defaultCombo.padding
|
||||
if (isNumber(padding)) {
|
||||
size.r += padding;
|
||||
size.width += padding * 2;
|
||||
size.height += padding * 2;
|
||||
} else {
|
||||
size.r += padding[0];
|
||||
size.width += (padding[1] + padding[3]) || padding[1] * 2;
|
||||
size.height += (padding[0] + padding[2]) || padding[0] * 2;
|
||||
}
|
||||
this.set(CACHE_SIZE, size);
|
||||
return newModel;
|
||||
}
|
||||
return model;
|
||||
@ -202,10 +214,44 @@ export default class Combo extends Node implements ICombo {
|
||||
public isOnlyMove(cfg?: ComboConfig): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取 item 的包围盒,这个包围盒是相对于 item 自己,不会将 matrix 计算在内
|
||||
* @return {Object} 包含 x,y,width,height, centerX, centerY
|
||||
*/
|
||||
public getBBox(): IBBox {
|
||||
this.set(CACHE_CANVAS_BBOX, null);
|
||||
let bbox: IBBox = this.calculateCanvasBBox();
|
||||
// 计算 bbox 开销有些大,缓存
|
||||
// let bbox: IBBox = this.get(CACHE_BBOX);
|
||||
// if (!bbox) {
|
||||
// bbox = this.getCanvasBBox();
|
||||
// this.set(CACHE_BBOX, bbox);
|
||||
// }
|
||||
return bbox;
|
||||
}
|
||||
|
||||
public clearCache() {
|
||||
this.set(CACHE_BBOX, null); // 清理缓存的 bbox
|
||||
this.set(CACHE_CANVAS_BBOX, null);
|
||||
this.set(CACHE_ANCHOR_POINTS, null);
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
if (!this.destroyed) {
|
||||
const animate = this.get('animate');
|
||||
const group: Group = this.get('group');
|
||||
if (animate) {
|
||||
group.stopAnimate();
|
||||
}
|
||||
this.clearCache();
|
||||
this.set(CACHE_SIZE, null);
|
||||
this.set('bbox', null);
|
||||
group.remove();
|
||||
(this._cfg as IItemBaseConfig | null) = null;
|
||||
this.destroyed = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import isNil from '@antv/util/lib/is-nil';
|
||||
import isPlainObject from '@antv/util/lib/is-plain-object';
|
||||
import { IEdge, INode } from '../interface/item';
|
||||
import { IEdge, INode, ICombo } from '../interface/item';
|
||||
import { EdgeConfig, IPoint, NodeConfig, SourceTarget, Indexable, ModelConfig } from '../types';
|
||||
import Item from './item';
|
||||
import Node from './node';
|
||||
@ -174,21 +174,21 @@ export default class Edge extends Item implements IEdge {
|
||||
return out;
|
||||
}
|
||||
|
||||
public setSource(source: INode) {
|
||||
public setSource(source: INode | ICombo) {
|
||||
this.setEnd('source', source);
|
||||
this.set('source', source);
|
||||
}
|
||||
|
||||
public setTarget(target: INode) {
|
||||
public setTarget(target: INode | ICombo) {
|
||||
this.setEnd('target', target);
|
||||
this.set('target', target);
|
||||
}
|
||||
|
||||
public getSource(): INode {
|
||||
public getSource(): INode | ICombo {
|
||||
return this.get('source');
|
||||
}
|
||||
|
||||
public getTarget(): INode {
|
||||
public getTarget(): INode | ICombo {
|
||||
return this.get('target');
|
||||
}
|
||||
|
||||
|
@ -699,6 +699,7 @@ export default class ItemBase implements IItemBase {
|
||||
if (animate) {
|
||||
group.stopAnimate();
|
||||
}
|
||||
this.clearCache();
|
||||
group.remove();
|
||||
(this._cfg as IItemBaseConfig | null) = null;
|
||||
this.destroyed = true;
|
||||
|
@ -15,7 +15,7 @@ const CACHE_ANCHOR_POINTS = 'anchorPointsCache';
|
||||
const CACHE_BBOX = 'bboxCache';
|
||||
|
||||
export default class Node extends Item implements INode {
|
||||
private getNearestPoint(points: IPoint[], curPoint: IPoint): IPoint {
|
||||
public getNearestPoint(points: IPoint[], curPoint: IPoint): IPoint {
|
||||
let index = 0;
|
||||
let nearestPoint = points[0];
|
||||
let minDistance = distance(points[0], curPoint);
|
||||
|
@ -5,9 +5,10 @@
|
||||
|
||||
import { EdgeConfig, IPointTuple, NodeConfig, NodeIdxMap, ComboTree, ComboConfig } from '../types';
|
||||
import { BaseLayout } from './layout';
|
||||
import { isNumber, isArray, isFunction, clone } from '@antv/util';
|
||||
import { isNumber, isArray, isFunction, clone, isNil } from '@antv/util';
|
||||
import { Point } from '@antv/g-base';
|
||||
import { traverseTreeUp } from '../util/graphic';
|
||||
import Global from '../global';
|
||||
|
||||
type Node = NodeConfig & {
|
||||
depth: number;
|
||||
@ -44,7 +45,7 @@ export default class ComboForce extends BaseLayout {
|
||||
/** 群组中心力大小 */
|
||||
public comboGravity: number = 10;
|
||||
/** 默认边长度 */
|
||||
public linkDistance: number | ((d?: unknown) => number) = 50;
|
||||
public linkDistance: number | ((d?: unknown) => number) = 10;
|
||||
/** 每次迭代位移的衰减相关参数 */
|
||||
public alpha: number = 1;
|
||||
public alphaMin: number = 0.001;
|
||||
@ -63,7 +64,7 @@ export default class ComboForce extends BaseLayout {
|
||||
/** 是否开启 Combo 之间的防止重叠 */
|
||||
public preventComboOverlap: boolean = false;
|
||||
/** 防止重叠的碰撞力大小 */
|
||||
public collideStrength: number = 0.2;
|
||||
public collideStrength: number | undefined = undefined;
|
||||
/** 防止重叠的碰撞力大小 */
|
||||
public nodeCollideStrength: number | undefined = undefined;
|
||||
/** 防止重叠的碰撞力大小 */
|
||||
@ -82,9 +83,10 @@ export default class ComboForce extends BaseLayout {
|
||||
public optimizeRangeFactor: number = 1;
|
||||
/** 每次迭代的回调函数 */
|
||||
public tick: () => void = () => { };
|
||||
/** 根据边两端节点层级差距的调整函数 */
|
||||
public depthAttractiveForceScale: number = 0.3; // ((d?: unknown) => number);
|
||||
public depthRepulsiveForceScale: number = 2; // ((d?: unknown) => number);
|
||||
/** 根据边两端节点层级差距的调整引力的因子,[0, 1] 代表层级差距越大,引力越小 */
|
||||
public depthAttractiveForceScale: number = 0.5;
|
||||
/** 根据边两端节点层级差距的调整斥力的因子,[1, Infinity] 代表层级差距越大,斥力越大 */
|
||||
public depthRepulsiveForceScale: number = 2;
|
||||
|
||||
/** 内部计算参数 */
|
||||
public nodes: Node[] = [];
|
||||
@ -105,18 +107,24 @@ export default class ComboForce extends BaseLayout {
|
||||
|
||||
public getDefaultCfg() {
|
||||
return {
|
||||
maxIteration: 1000,
|
||||
maxIteration: 100,
|
||||
center: [0, 0],
|
||||
gravity: 10,
|
||||
speed: 1,
|
||||
comboGravity: 10,
|
||||
comboGravity: 30,
|
||||
preventOverlap: false,
|
||||
preventComboOverlap: true,
|
||||
preventNodeOverlap: true,
|
||||
nodeSpacing: undefined,
|
||||
collideStrength: 0.2,
|
||||
nodeCollideStrength: undefined,
|
||||
comboCollideStrength: undefined,
|
||||
comboSpacing: 5,
|
||||
comboPadding: 10
|
||||
collideStrength: undefined,
|
||||
nodeCollideStrength: 0.5,
|
||||
comboCollideStrength: 0.5,
|
||||
comboSpacing: 20,
|
||||
comboPadding: 10,
|
||||
linkStrength: 0.2,
|
||||
nodeStrength: 30,
|
||||
linkDistance: 10,
|
||||
|
||||
};
|
||||
}
|
||||
/**
|
||||
@ -125,7 +133,6 @@ export default class ComboForce extends BaseLayout {
|
||||
public execute() {
|
||||
const self = this;
|
||||
const nodes = self.nodes;
|
||||
const combos = self.combos;
|
||||
const center = self.center;
|
||||
self.comboTree = {
|
||||
id: 'comboTreeRoot',
|
||||
@ -238,14 +245,14 @@ export default class ComboForce extends BaseLayout {
|
||||
self.comboMap = self.getComboMap();
|
||||
|
||||
const preventOverlap = self.preventOverlap;
|
||||
if (preventOverlap) {
|
||||
self.preventComboOverlap = true;
|
||||
self.preventNodeOverlap = true;
|
||||
}
|
||||
self.preventComboOverlap = self.preventComboOverlap || preventOverlap;
|
||||
self.preventNodeOverlap = self.preventNodeOverlap || preventOverlap;
|
||||
|
||||
const collideStrength = self.collideStrength;
|
||||
if (!self.comboCollideStrength) self.comboCollideStrength = collideStrength;
|
||||
if (!self.nodeCollideStrength) self.nodeCollideStrength = collideStrength;
|
||||
if (!isNil(collideStrength)) {
|
||||
self.comboCollideStrength = collideStrength;
|
||||
self.nodeCollideStrength = collideStrength;
|
||||
}
|
||||
|
||||
// get edge bias
|
||||
for (let i = 0; i < edges.length; ++i) {
|
||||
@ -334,7 +341,7 @@ export default class ComboForce extends BaseLayout {
|
||||
let linkDistance = this.linkDistance;
|
||||
let linkDistanceFunc;
|
||||
if (!linkDistance) {
|
||||
linkDistance = 50;
|
||||
linkDistance = 10;
|
||||
}
|
||||
if (isNumber(linkDistance)) {
|
||||
linkDistanceFunc = d => {
|
||||
@ -595,7 +602,11 @@ export default class ComboForce extends BaseLayout {
|
||||
if (c.maxX < nodeMaxX) c.maxX = nodeMaxX;
|
||||
if (c.maxY < nodeMaxY) c.maxY = nodeMaxY;
|
||||
});
|
||||
c.r = Math.max(c.maxX - c.minX, c.maxY - c.minY) / 2 + comboSpacing(c) / 2 + comboPadding(c);
|
||||
let minSize = self.oriComboMap[treeNode.id].size || Global.defaultCombo.size;
|
||||
if (isArray(minSize)) minSize = minSize[0];
|
||||
const maxLength = Math.max(c.maxX - c.minX, c.maxY - c.minY, minSize as number);
|
||||
c.r = maxLength / 2 + comboSpacing(c) / 2 + comboPadding(c);
|
||||
|
||||
return true;
|
||||
});
|
||||
});
|
||||
@ -614,6 +625,7 @@ export default class ComboForce extends BaseLayout {
|
||||
traverseTreeUp<ComboTree>(comboTree, treeNode => {
|
||||
if (!comboMap[treeNode.id] && !nodeMap[treeNode.id] && treeNode.id !== 'comboTreeRoot') return; // means it is hidden
|
||||
const children = treeNode.children;
|
||||
// 同个子树下的子 combo 间两两对比
|
||||
if (children && children.length > 1) {
|
||||
children.forEach((v, i) => {
|
||||
if (v.itemType === 'node') return;
|
||||
@ -642,6 +654,7 @@ export default class ComboForce extends BaseLayout {
|
||||
const yl = vy * ll;
|
||||
let rratio = ru2 / (rv2 + ru2);
|
||||
let irratio = 1 - rratio;
|
||||
// 两兄弟 combo 的子节点上施加斥力
|
||||
vnodes.forEach(vn => {
|
||||
if (vn.itemType !== 'node') return;
|
||||
if (!nodeMap[vn.id]) return; // means it is hidden
|
||||
@ -704,9 +717,9 @@ export default class ComboForce extends BaseLayout {
|
||||
|
||||
let { vx, vy } = vecMap[`${v.id}-${u.id}`];
|
||||
|
||||
const depthDiff = (Math.abs(u.depth - v.depth) + 1) || 1;
|
||||
let depthDiff = (Math.abs(u.depth - v.depth) + 1) || 1;
|
||||
if (u.comboId !== v.comboId) depthDiff++;
|
||||
let depthParam = depthDiff ? Math.pow(scale, depthDiff) : 1;
|
||||
// let depthParam = depthDiff ? scale * depthDiff : 1;
|
||||
|
||||
const params = nodeStrength(u) * alpha / vl * depthParam;
|
||||
displacements[i].x += vx * params;
|
||||
@ -763,13 +776,16 @@ export default class ComboForce extends BaseLayout {
|
||||
const vecX = vx * l;
|
||||
const vecY = vy * l;
|
||||
const b = bias[i];
|
||||
const depthDiff = Math.abs(u.depth - v.depth);
|
||||
|
||||
let depthDiff = Math.abs(u.depth - v.depth);
|
||||
if (u.comboId === v.comboId) depthDiff / 2;
|
||||
let depthParam = depthDiff ? Math.pow(scale, depthDiff) : 1;
|
||||
if (u.comboId !== v.comboId && depthParam === 1) {
|
||||
depthParam = scale;
|
||||
} else if (u.comboId === v.comboId) {
|
||||
depthParam = 1;
|
||||
}
|
||||
// depthParam = 1;
|
||||
displacements[vIndex].x -= vecX * b * depthParam;
|
||||
displacements[vIndex].y -= vecY * b * depthParam;
|
||||
displacements[uIndex].x += vecX * (1 - b) * depthParam;
|
||||
|
@ -6,7 +6,7 @@ import GGroup from '@antv/g-canvas/lib/group';
|
||||
import { IShape } from '@antv/g-canvas/lib/interfaces';
|
||||
import { isArray, isNil, clone, isNumber } from '@antv/util';
|
||||
import { ILabelConfig, ShapeOptions } from '../interface/shape';
|
||||
import { Item, LabelStyle, NodeConfig, ModelConfig } from '../types';
|
||||
import { Item, LabelStyle, NodeConfig, ModelConfig, ShapeStyle } from '../types';
|
||||
import Global from '../global';
|
||||
import Shape from './shape';
|
||||
import { shapeBase } from './shapeBase';
|
||||
@ -121,12 +121,12 @@ const singleCombo: ShapeOptions = {
|
||||
});
|
||||
return shape;
|
||||
},
|
||||
updateShape(cfg: NodeConfig, item: Item, keyShapeStyle: object) {
|
||||
updateShape(cfg: NodeConfig, item: Item, keyShapeStyle: ShapeStyle) {
|
||||
const keyShape = item.get('keyShape');
|
||||
const animate = this.options.animate;
|
||||
if (animate && keyShape.animate) {
|
||||
keyShape.animate(keyShapeStyle, {
|
||||
duration: 280,
|
||||
duration: 200,
|
||||
easing: 'easeLinear',
|
||||
})
|
||||
} else {
|
||||
|
@ -14,7 +14,7 @@ Shape.registerCombo(
|
||||
// 自定义节点时的配置
|
||||
options: {
|
||||
size: [Global.defaultCombo.size[0], Global.defaultCombo.size[0]],
|
||||
padding: 20,
|
||||
padding: Global.defaultCombo.padding[0],
|
||||
animate: true,
|
||||
style: {
|
||||
stroke: Global.defaultCombo.style.stroke,
|
||||
@ -87,6 +87,12 @@ Shape.registerCombo(
|
||||
const cfgStyle = clone(cfg.style);
|
||||
const r = Math.max(cfgStyle.r, size[0] / 2) || size[0] / 2;;
|
||||
cfgStyle.r = r + padding;
|
||||
|
||||
const itemCacheSize = item.get('sizeCache');
|
||||
if (itemCacheSize) {
|
||||
itemCacheSize.r = cfgStyle.r;
|
||||
}
|
||||
|
||||
// 下面这些属性需要覆盖默认样式与目前样式,但若在 cfg 中有指定则应该被 cfg 的相应配置覆盖。
|
||||
const strokeStyle = {
|
||||
stroke: cfg.color
|
||||
|
@ -275,8 +275,16 @@ Shape.registerCombo(
|
||||
const cfgStyle = clone(cfg.style);
|
||||
const width = (Math.max(cfgStyle.width, size[0]) || size[0]);
|
||||
const height = (Math.max(cfgStyle.height, size[1]) || size[1]);
|
||||
|
||||
cfgStyle.width = width + padding[1] + padding[3];
|
||||
cfgStyle.height = height + padding[0] + padding[2];
|
||||
|
||||
const itemCacheSize = item.get('sizeCache');
|
||||
if (itemCacheSize) {
|
||||
itemCacheSize.width = cfgStyle.width;
|
||||
itemCacheSize.height = cfgStyle.height;
|
||||
}
|
||||
|
||||
cfgStyle.x = -width / 2 - padding[3];
|
||||
cfgStyle.y = -height / 2 - padding[0];
|
||||
// 下面这些属性需要覆盖默认样式与目前样式,但若在 cfg 中有指定则应该被 cfg 的相应配置覆盖。
|
||||
@ -298,9 +306,17 @@ Shape.registerCombo(
|
||||
},
|
||||
updateShape(cfg: ComboConfig, item: Item, keyShapeStyle: object) {
|
||||
const keyShape = item.get('keyShape');
|
||||
keyShape.attr({
|
||||
...keyShapeStyle,
|
||||
});
|
||||
const animate = this.options.animate;
|
||||
if (animate && keyShape.animate) {
|
||||
keyShape.animate(keyShapeStyle, {
|
||||
duration: 200,
|
||||
easing: 'easeLinear',
|
||||
})
|
||||
} else {
|
||||
keyShape.attr({
|
||||
...keyShapeStyle,
|
||||
});
|
||||
}
|
||||
|
||||
(this as any).updateLabel(cfg, item);
|
||||
(this as any).updateCollapseIcon(cfg, item, keyShapeStyle)
|
||||
|
@ -598,7 +598,7 @@ export interface IG6GraphEvent extends GraphEvent {
|
||||
// Node Edge Combo 实例
|
||||
export type Item = INode | IEdge | ICombo;
|
||||
|
||||
export type ITEM_TYPE = 'node' | 'edge' | 'combo' | 'group';
|
||||
export type ITEM_TYPE = 'node' | 'edge' | 'combo' | 'group' | 'vedge';
|
||||
|
||||
export type NodeIdxMap = {
|
||||
[key: string]: number;
|
||||
|
@ -506,13 +506,15 @@ export const plainCombosToTrees = (array: ComboConfig[], nodes?: INode[]) => {
|
||||
tree.depth = 0;
|
||||
traverse<ComboTree>(tree, child => {
|
||||
let parent;
|
||||
if (addedMap[child.id]['itemType'] === 'node') {
|
||||
const itemType = addedMap[child.id]['itemType'];
|
||||
if (itemType === 'node') {
|
||||
parent = addedMap[child['comboId'] as string];
|
||||
} else {
|
||||
parent = addedMap[child.parentId];
|
||||
}
|
||||
if (parent) {
|
||||
child.depth = parent.depth + 1;
|
||||
if (itemType === 'node') child.depth = parent.depth + 1;
|
||||
else child.depth = parent.depth + 2;
|
||||
} else {
|
||||
child.depth = 0;
|
||||
}
|
||||
|
@ -600,6 +600,10 @@ const Tutorial = () => {
|
||||
'https://gw.alipayobjects.com/os/basement_prod/6cae02ab-4c29-44b2-b1fd-4005688febcb.json',
|
||||
);
|
||||
const data = await response.json();
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
console.log(data);
|
||||
>>>>>>> feat: collapse expand combo to add virtual edges to represent the edges between collapsed combos.
|
||||
const nodes = data.nodes;
|
||||
const edges = data.edges;
|
||||
nodes.forEach(node => {
|
||||
|
@ -3,8 +3,10 @@ import { storiesOf } from '@storybook/react';
|
||||
import React from 'react';
|
||||
import DefaultCombo from './component/default-combo';
|
||||
import RegisterCombo from './component/register-combo';
|
||||
import CollapseExpand from './component/collapse-expand-combo'
|
||||
import ComboLayoutCollapseExpand from './component/combo-layout-collapse-expand'
|
||||
import CollapseExpand from './component/collapse-expand-combo';
|
||||
import ComboLayoutCollapseExpand from './component/combo-layout-collapse-expand';
|
||||
import CollapseExpandVEdge from './component/collapse-expand-vedge';
|
||||
import ComboCollapseExpandTree from './component/combo-collapse-expand-tree';
|
||||
|
||||
export default { title: 'Combo' };
|
||||
|
||||
@ -20,4 +22,10 @@ storiesOf('Combo', module)
|
||||
))
|
||||
.add('force + collapse expand', () => (
|
||||
<ComboLayoutCollapseExpand />
|
||||
))
|
||||
.add('collapse expand vedge', () => (
|
||||
<CollapseExpandVEdge />
|
||||
))
|
||||
.add('collapse expand tree', () => (
|
||||
<ComboCollapseExpandTree />
|
||||
));
|
||||
|
177
stories/Combo/component/collapse-expand-vedge.tsx
Normal file
177
stories/Combo/component/collapse-expand-vedge.tsx
Normal file
@ -0,0 +1,177 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import G6 from '../../../src';
|
||||
import { IGraph } from '../../../src/interface/graph';
|
||||
|
||||
let graph: IGraph = null;
|
||||
|
||||
const colors = {
|
||||
a: '#BDD2FD',
|
||||
b: '#BDEFDB',
|
||||
c: '#C2C8D5',
|
||||
d: '#FBE5A2',
|
||||
e: '#F6C3B7',
|
||||
f: '#B6E3F5',
|
||||
g: '#D3C6EA',
|
||||
h: '#FFD8B8',
|
||||
i: '#AAD8D8',
|
||||
j: '#FFD6E7',
|
||||
};
|
||||
|
||||
|
||||
const testData = {
|
||||
nodes: [
|
||||
{
|
||||
id: '0',
|
||||
label: '0',
|
||||
comboId: 'a',
|
||||
x: 100,
|
||||
y: 100
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
label: '1',
|
||||
comboId: 'a',
|
||||
x: 150,
|
||||
y: 140
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
label: '2',
|
||||
comboId: 'b',
|
||||
x: 300,
|
||||
y: 200
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
label: '3',
|
||||
comboId: 'b',
|
||||
x: 370,
|
||||
y: 260
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
label: '4',
|
||||
comboId: 'c',
|
||||
x: 360,
|
||||
y: 510
|
||||
},
|
||||
// {
|
||||
// id: '5',
|
||||
// label: '5',
|
||||
// comboId: 'd',
|
||||
// x: 420,
|
||||
// y: 510
|
||||
// },
|
||||
],
|
||||
edges: [
|
||||
// {
|
||||
// source: 'a',
|
||||
// target: 'b',
|
||||
// size: 3,
|
||||
// style: {
|
||||
// stroke: 'red'
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// source: 'b',
|
||||
// target: '1',
|
||||
// size: 3,
|
||||
// style: {
|
||||
// stroke: 'blue'
|
||||
// }
|
||||
// },
|
||||
{
|
||||
source: '0',
|
||||
target: '1',
|
||||
},
|
||||
{
|
||||
source: '0',
|
||||
target: '2',
|
||||
},
|
||||
{
|
||||
source: '0',
|
||||
target: '3',
|
||||
},
|
||||
// {
|
||||
// source: '3',
|
||||
// target: '5',
|
||||
// },
|
||||
{
|
||||
source: '4',
|
||||
target: '1',
|
||||
}
|
||||
],
|
||||
combos: [{
|
||||
id: 'a',
|
||||
label: 'combo a'
|
||||
}, {
|
||||
id: 'b',
|
||||
label: 'combo b'
|
||||
}, {
|
||||
id: 'c',
|
||||
label: 'combo c'
|
||||
},
|
||||
// {
|
||||
// id: 'd',
|
||||
// label: 'combo d',
|
||||
// parentId: 'c'
|
||||
// }
|
||||
]
|
||||
};
|
||||
|
||||
const CollapseExpandVEdge = () => {
|
||||
const container = React.useRef();
|
||||
useEffect(() => {
|
||||
if (!graph) {
|
||||
graph = new G6.Graph({
|
||||
container: container.current as string | HTMLElement,
|
||||
width: 1000,
|
||||
height: 800,
|
||||
fitView: true,
|
||||
modes: {
|
||||
default: ['drag-canvas', 'drag-node', 'zoom-canvas', 'collapse-expand-combo'],
|
||||
},
|
||||
defaultEdge: {
|
||||
size: 1,
|
||||
color: '#666',
|
||||
},
|
||||
defaultCombo: {
|
||||
type: 'circle',
|
||||
//padding: 1
|
||||
},
|
||||
groupByTypes: false,
|
||||
//animate: true
|
||||
});
|
||||
|
||||
graph.node(node => {
|
||||
const color = colors[node.comboId as string];
|
||||
return {
|
||||
size: 20,
|
||||
style: {
|
||||
lineWidth: 2,
|
||||
stroke: '#ccc',
|
||||
fill: color,
|
||||
},
|
||||
}
|
||||
});
|
||||
graph.combo(combo => {
|
||||
const color = colors[combo.id as string];
|
||||
return {
|
||||
// size: 80,
|
||||
style: {
|
||||
lineWidth: 2,
|
||||
stroke: color,
|
||||
fillOpacity: 0.8
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
graph.data(testData);//testData_pos
|
||||
graph.render();
|
||||
}
|
||||
});
|
||||
return <div ref={container}></div>;
|
||||
};
|
||||
|
||||
export default CollapseExpandVEdge;
|
141
stories/Combo/component/combo-collapse-expand-tree.tsx
Normal file
141
stories/Combo/component/combo-collapse-expand-tree.tsx
Normal file
@ -0,0 +1,141 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import G6 from '../../../src';
|
||||
import { IGraph } from '../../../src/interface/graph';
|
||||
|
||||
let graph: IGraph = null;
|
||||
|
||||
const colors = {
|
||||
a: '#BDD2FD',
|
||||
b: '#BDEFDB',
|
||||
c: '#C2C8D5',
|
||||
d: '#FBE5A2',
|
||||
e: '#F6C3B7',
|
||||
f: '#B6E3F5',
|
||||
g: '#D3C6EA',
|
||||
h: '#FFD8B8',
|
||||
i: '#AAD8D8',
|
||||
j: '#FFD6E7',
|
||||
};
|
||||
const data = {
|
||||
"id": "Modeling Methods",
|
||||
"children": [
|
||||
{
|
||||
"id": "Classification",
|
||||
comboId: 'a',
|
||||
"children": [
|
||||
{ "id": "Logistic regression", comboId: 'a', },
|
||||
{ "id": "Linear discriminant analysis", comboId: 'a' },
|
||||
{ "id": "Rules", comboId: 'a' },
|
||||
{ "id": "Decision trees", comboId: 'a' },
|
||||
{ "id": "Naive Bayes", comboId: 'a' },
|
||||
{ "id": "K nearest neighbor", comboId: 'a' },
|
||||
{ "id": "Probabilistic neural network", comboId: 'a' },
|
||||
{ "id": "Support vector machine", comboId: 'a' }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "Consensus",
|
||||
"children": [
|
||||
{
|
||||
"id": "Models diversity",
|
||||
comboId: 'a',
|
||||
"children": [
|
||||
{ "id": "Different initializations", comboId: 'a' },
|
||||
{ "id": "Different parameter choices", comboId: 'a' },
|
||||
{ "id": "Different architectures", comboId: 'a' },
|
||||
{ "id": "Different modeling methods", comboId: 'a' },
|
||||
{ "id": "Different training sets", comboId: 'a' },
|
||||
{ "id": "Different feature sets", comboId: 'a' }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "Methods",
|
||||
comboId: 'b',
|
||||
"children": [
|
||||
{ "id": "Classifier selection", comboId: 'b' },
|
||||
{ "id": "Classifier fusion", comboId: 'b' }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "Common",
|
||||
comboId: 'c',
|
||||
"children": [
|
||||
{ "id": "Bagging", comboId: 'c' },
|
||||
{ "id": "Boosting", comboId: 'c' },
|
||||
{ "id": "AdaBoost", comboId: 'c' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "Regression",
|
||||
comboId: 'd',
|
||||
"children": [
|
||||
{ "id": "Multiple linear regression", comboId: 'd' },
|
||||
{ "id": "Partial least squares", comboId: 'd' },
|
||||
{ "id": "Multi-layer feedforward neural network" },
|
||||
{ "id": "General regression neural network" },
|
||||
{ "id": "Support vector regression" }
|
||||
]
|
||||
}
|
||||
],
|
||||
combos: []
|
||||
};
|
||||
|
||||
const ComboCollapseExpandTree = () => {
|
||||
const container = React.useRef();
|
||||
useEffect(() => {
|
||||
if (!graph) {
|
||||
graph = new G6.TreeGraph({
|
||||
container: container.current as string | HTMLElement,
|
||||
width: 1000,
|
||||
height: 800,
|
||||
fitView: true,
|
||||
modes: {
|
||||
default: ['drag-canvas', 'drag-node', 'zoom-canvas', 'collapse-expand-combo'],
|
||||
},
|
||||
layout: {
|
||||
type: 'compactBox',
|
||||
},
|
||||
defaultEdge: {
|
||||
size: 1,
|
||||
color: '#666',
|
||||
},
|
||||
defaultCombo: {
|
||||
type: 'circle',
|
||||
padding: 50
|
||||
},
|
||||
groupByTypes: false,
|
||||
//animate: true
|
||||
});
|
||||
|
||||
graph.combo(combo => {
|
||||
const color = colors[combo.id as string];
|
||||
return {
|
||||
// size: 80,
|
||||
style: {
|
||||
lineWidth: 2,
|
||||
stroke: color,
|
||||
fillOpacity: 0.8
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
data.combos = [{
|
||||
id: 'a'
|
||||
}, {
|
||||
id: 'b'
|
||||
}, {
|
||||
id: 'c'
|
||||
}, {
|
||||
id: 'd'
|
||||
}];
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
}
|
||||
});
|
||||
return <div ref={container}></div>;
|
||||
};
|
||||
|
||||
export default ComboCollapseExpandTree;
|
@ -628,6 +628,22 @@ const testData = {
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
source: 'a',
|
||||
target: 'b',
|
||||
size: 3,
|
||||
style: {
|
||||
stroke: 'red'
|
||||
}
|
||||
},
|
||||
{
|
||||
source: 'a',
|
||||
target: '33',
|
||||
size: 3,
|
||||
style: {
|
||||
stroke: 'blue'
|
||||
}
|
||||
},
|
||||
{
|
||||
source: '0',
|
||||
target: '1',
|
||||
@ -922,6 +938,8 @@ const testData2 = {
|
||||
target: 'node0'
|
||||
}]
|
||||
}
|
||||
|
||||
const testData_pos = {"nodes":[{"id":"0","x":519.9756011152062,"y":312.3748588848735,"comboId":"a","label":"0"},{"id":"1","x":516.9130868036522,"y":300.01298088318964,"comboId":"a","label":"1"},{"id":"2","x":532.135761126721,"y":292.4828444555691,"comboId":"a","label":"2"},{"id":"3","x":503.08139107396323,"y":274.4859385079485,"comboId":"a","label":"3"},{"id":"4","x":515.7858143951453,"y":288.9459640448524,"comboId":"a","label":"4"},{"id":"5","x":523.1699237148887,"y":271.80995614771984,"comboId":"a","label":"5"},{"id":"6","x":542.5161585695183,"y":269.44295161957444,"comboId":"a","label":"6"},{"id":"7","x":540.3068131495662,"y":312.3305908975899,"comboId":"a","label":"7"},{"id":"8","x":540.1624078071186,"y":332.7802388427267,"comboId":"a","label":"8"},{"id":"9","x":497.2349390488828,"y":294.6747309770873,"comboId":"a","label":"9"},{"id":"10","x":477.27853085824415,"y":310.52154966368073,"comboId":"a","label":"10"},{"id":"11","x":498.2935956770408,"y":315.7336916123234,"comboId":"a","label":"11"},{"id":"12","x":511.8111914629553,"y":331.4660202536512,"comboId":"a","label":"12"},{"id":"13","x":719.9144466443125,"y":657.5827638668379,"comboId":"b","label":"13"},{"id":"14","x":705.1704215932026,"y":623.1349052942683,"comboId":"b","label":"14"},{"id":"15","x":748.888997314242,"y":693.5395656445546,"comboId":"b","label":"15"},{"id":"16","x":691.790150377325,"y":647.2895235684261,"comboId":"b","label":"16"},{"id":"17","x":700.453909738154,"y":666.3168884837505,"comboId":"b","label":"17"},{"id":"18","x":377.3258580235555,"y":373.66554381437624,"comboId":"c","label":"18"},{"id":"19","x":361.8579868952546,"y":350.20312804345934,"comboId":"c","label":"19"},{"id":"20","x":380.71103876474103,"y":332.78105195763504,"comboId":"c","label":"20"},{"id":"21","x":369.1201243167846,"y":332.02692181101764,"comboId":"c","label":"21"},{"id":"22","x":377.8110758618865,"y":350.0158128077823,"comboId":"c","label":"22"},{"id":"23","x":359.4471586647803,"y":335.9621566320535,"comboId":"c","label":"23"},{"id":"24","x":372.74841352346505,"y":314.6483478876071,"comboId":"c","label":"24"},{"id":"25","x":352.7557971300988,"y":331.4794537088405,"comboId":"c","label":"25"},{"id":"26","x":344.9855032420906,"y":350.16490710174355,"comboId":"c","label":"26"},{"id":"27","x":347.46959717648184,"y":313.6154232044856,"comboId":"c","label":"27"},{"id":"28","x":361.4870734755764,"y":322.1957223205074,"comboId":"c","label":"28"},{"id":"29","x":329.63246460052113,"y":320.22474499903063,"comboId":"c","label":"29"},{"id":"30","x":339.92007768802193,"y":335.1429986552043,"comboId":"c","label":"30"},{"id":"31","x":680.1030572348665,"y":679.0745385883383,"comboId":"d","label":"31"},{"id":"32","x":701.6575625058663,"y":703.0463672244064,"comboId":"d","label":"32"},{"id":"33","x":658.0840704258677,"y":660.8269175948863,"comboId":"d","label":"33"}],"combos":[{"id":"a","label":"combo a"},{"id":"b","label":"combo b"},{"id":"c","label":"combo c"},{"id":"d","parentId":"b","label":"combo d"}]}
|
||||
const ComboLayoutCollapseExpand = () => {
|
||||
const container = React.useRef();
|
||||
useEffect(() => {
|
||||
@ -929,36 +947,24 @@ const ComboLayoutCollapseExpand = () => {
|
||||
graph = new G6.Graph({
|
||||
container: container.current as string | HTMLElement,
|
||||
width: 1000,
|
||||
height: 800,
|
||||
// fitView: true,
|
||||
height: 500,
|
||||
fitView: true,
|
||||
modes: {
|
||||
default: ['drag-canvas', 'drag-node', 'zoom-canvas', 'collapse-expand-combo'],
|
||||
},
|
||||
layout: {
|
||||
type: 'comboForce',
|
||||
linkDistance: 10,
|
||||
comboGravity: 30,
|
||||
nodeSpacing: 1,
|
||||
nodeStrength: 30,
|
||||
linkStrength: 0.1,
|
||||
preventNodeOverlap: true,
|
||||
preventComboOverlap: true,
|
||||
// preventOverlap: true,
|
||||
comboCollideStrength: 0.5,
|
||||
nodeCollideStrength: 0.1,
|
||||
maxIteration: 100,
|
||||
comboPadding: 10,
|
||||
comboSpacing: 30
|
||||
type: 'comboForce'
|
||||
},
|
||||
defaultEdge: {
|
||||
size: 3,
|
||||
size: 1,
|
||||
color: '#666',
|
||||
},
|
||||
defaultCombo: {
|
||||
type: 'rect'
|
||||
type: 'circle',
|
||||
padding: 10
|
||||
},
|
||||
groupByTypes: false,
|
||||
animate: true
|
||||
// animate: true
|
||||
});
|
||||
|
||||
graph.node(node => {
|
||||
@ -975,8 +981,7 @@ const ComboLayoutCollapseExpand = () => {
|
||||
graph.combo(combo => {
|
||||
const color = colors[combo.id as string];
|
||||
return {
|
||||
size: 20,
|
||||
// padding: 5,
|
||||
// size: 80,
|
||||
style: {
|
||||
lineWidth: 2,
|
||||
stroke: color,
|
||||
@ -985,14 +990,35 @@ const ComboLayoutCollapseExpand = () => {
|
||||
}
|
||||
});
|
||||
|
||||
fetch(
|
||||
'https://gw.alipayobjects.com/os/basement_prod/7bacd7d1-4119-4ac1-8be3-4c4b9bcbc25f.json',
|
||||
)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
graph.data(testData);
|
||||
graph.render();
|
||||
});
|
||||
graph.data(testData);//testData_pos
|
||||
graph.render();
|
||||
// const outputData = {
|
||||
// nodes: [],
|
||||
// combos: []
|
||||
// };
|
||||
// graph.getNodes().forEach((n: any) => {
|
||||
// const node = n.getModel();
|
||||
// console.log(node.x, node.y)
|
||||
// outputData.nodes.push({
|
||||
// id: node.id,
|
||||
// x: node.x,
|
||||
// y: node.y,
|
||||
// comboId: node.comboId,
|
||||
// label: node.label
|
||||
// });
|
||||
// })
|
||||
// testData.combos.forEach((combo: any) => {
|
||||
// outputData.combos.push({
|
||||
// id: combo.id,
|
||||
// parentId: combo.parentId,
|
||||
// label: combo.label
|
||||
// });
|
||||
// });
|
||||
// console.log(JSON.stringify(outputData));
|
||||
|
||||
// graph.on('canvas:click', e => {
|
||||
// graph.changeData(testData_pos);
|
||||
// });
|
||||
}
|
||||
});
|
||||
return <div ref={container}></div>;
|
||||
|
@ -497,13 +497,6 @@ const ComboForceLayout = () => {
|
||||
modes: {
|
||||
default: ['drag-canvas', 'drag-node', 'zoom-canvas'],
|
||||
},
|
||||
layout: {
|
||||
type: 'comboForce',
|
||||
linkDistance: 1000,
|
||||
fitView: true,
|
||||
modes: {
|
||||
default: ['drag-canvas', 'drag-node', 'zoom-canvas'],
|
||||
},
|
||||
layout: {
|
||||
type: 'comboForce',
|
||||
linkDistance: 100,
|
||||
|
@ -51,7 +51,7 @@ describe('combo node test', () => {
|
||||
group,
|
||||
);
|
||||
canvas.draw();
|
||||
expect(shape.attr('r')).toBe(40);
|
||||
expect(shape.attr('r')).toBe(45); // size / 2 + padding
|
||||
expect(group.getCount()).toBe(1);
|
||||
});
|
||||
|
||||
@ -156,7 +156,11 @@ describe('combo node test', () => {
|
||||
fill: 'steelblue'
|
||||
}
|
||||
});
|
||||
expect(shape.attr('fill')).toBe('steelblue');
|
||||
// since the update is animated, check it after 300ms
|
||||
setTimeout(() => {
|
||||
expect(shape.attr('fill')).toBe('steelblue');
|
||||
}, 300);
|
||||
|
||||
});
|
||||
|
||||
it('active', () => {
|
||||
|
Loading…
Reference in New Issue
Block a user