perf: incremental force

This commit is contained in:
Yanyan-Wang 2023-10-30 13:50:45 +08:00
parent af28e35fc5
commit 40976cf3ee
8 changed files with 404 additions and 234 deletions

View File

@ -186,7 +186,7 @@ export default class Node extends Item {
{ group: position } as any, // targetStylesMap
this.shapeMap, // shapeMap
undefined,
[group, labelGroup],
[group],
'update',
[],
this.animateFrameListener,

View File

@ -1847,7 +1847,6 @@ export class ItemController {
(model, canceled) => {
this.graph.hideItem(model.id, { disableAnimate: canceled });
},
undefined,
);
}

View File

@ -108,17 +108,30 @@ export class LayoutController {
animationEffectTiming = {
duration: 1000,
} as Partial<IAnimationEffectTiming>,
preset,
execute,
...rest
} = options;
// preset layout
const nodesWithPosition = await this.presetLayout(
layoutData,
nodeSize,
width,
height,
center,
preset,
params,
layoutGraphCore,
);
// It will ignore some layout options such as `type` and `workerEnabled`.
positions = await execute(layoutGraphCore, {
nodeSize,
width,
height,
center,
getMass: this.genericGetMass(options),
getMass: this.genericGetMass(options, nodesWithPosition),
preset: layoutNodes.map((node) => {
const { x, y, z } = node.data;
if (isNaN(x) || isNaN(y)) return;
@ -147,6 +160,53 @@ export class LayoutController {
);
}
} else {
const { preset } = options;
// preset layout
const nodesWithPosition = await this.presetLayout(
layoutData,
nodeSize,
width,
height,
center,
preset,
params,
layoutGraphCore,
);
// layout
// TODO: input positions affect the layout
positions = await this.layoutOnce(
layoutData,
nodeSize,
width,
height,
center,
options,
params,
layoutGraphCore,
nodesWithPosition,
);
}
// Update nodes' positions.
this.updateNodesPosition(positions, animate);
}
private layoutOnce = async (
layoutData,
nodeSize,
width,
height,
center,
options,
params,
layoutGraphCore,
nodesWithPosition?,
) => {
let positions: LayoutMapping;
const { graphCore, animate = true } = params;
const { nodes: layoutNodes } = layoutData;
const {
type = 'grid',
animated = false,
@ -190,7 +250,7 @@ export class LayoutController {
width,
height,
center,
getMass: this.genericGetMass(options),
getMass: this.genericGetMass(options, nodesWithPosition),
preset: useCache
? layoutNodes
.map((node) => {
@ -266,11 +326,70 @@ export class LayoutController {
}
}
}
return positions;
};
private presetLayout = async (
layoutData,
nodeSize,
width,
height,
center,
preset,
params,
layoutGraphCore,
) => {
// preset has higher priority than the positions in data
if (preset?.type) {
const presetPositions = await this.layoutOnce(
layoutData,
nodeSize,
width,
height,
center,
preset,
params,
layoutGraphCore,
);
presetPositions.nodes.forEach((node) => {
layoutGraphCore.updateNodeData(node.id, {
x: node.data.x,
y: node.data.y,
});
});
return;
}
// Update nodes' positions.
this.updateNodesPosition(positions, animate);
const nodeWithPostions = new Map();
// find the neighbors' mean center as the initial position for the nodes
layoutData.nodes.forEach((node) => {
const { x, y } = node.data;
if (isNaN(x) || isNaN(y)) {
const meanCenter = { x: 0, y: 0, z: 0, count: 0 };
layoutGraphCore.getNeighbors(node.id).forEach((neighbor) => {
const { x: nx, y: ny, z: nz } = neighbor.data;
if (!isNaN(nx) && !isNaN(ny)) {
meanCenter.x += nx;
meanCenter.y += ny;
meanCenter.z += nz;
meanCenter.count++;
}
});
if (meanCenter.count) {
node.data.x = meanCenter.x / meanCenter.count + Math.random();
node.data.y = meanCenter.y / meanCenter.count + Math.random();
node.data.z = meanCenter.z / meanCenter.count + Math.random();
} else {
node.data.x = center[0] + Math.random();
node.data.y = center[1] + Math.random();
node.data.z = Math.random();
}
} else {
nodeWithPostions.set(node.id, 1);
}
});
return nodeWithPostions;
};
async handleTreeLayout(
type,
@ -450,14 +569,15 @@ export class LayoutController {
await this.currentAnimation.finished;
}
private genericGetMass = (options) => {
private genericGetMass = (options, nodesWithPosition) => {
const { getMass: propGetMass } = options;
return (node) => {
const propGetMassVal = propGetMass?.(node);
if (!isNaN(propGetMassVal)) return propGetMassVal;
const { x, y, mass } = node.data;
if (!isNaN(mass)) return mass;
if (!isNaN(x) && !isNaN(y)) return 100;
if (nodesWithPosition?.get(node.id)) return 10;
if (!nodesWithPosition && !isNaN(x) && !isNaN(y)) return 10;
return 1;
};
};

View File

@ -1258,7 +1258,8 @@ export class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
}
/**
* Remove one or more node/edge/combo data from the graph.
* @param item the item to be removed
* @param itemType the type the item(s) to be removed.
* @param id the id or the ids' array of the items to be removed.
* @returns whether success
* @group Data
*/
@ -1360,8 +1361,8 @@ export class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
}
/**
* Update one or more node/edge/combo data on the graph.
* @param {ITEM_TYPE} itemType 'node' | 'edge' | 'combo'
* @param models new configurations for every node/edge/combo, which has id field to indicate the specific item
* @param {ITEM_TYPE} itemType the type the item(s) to be udated.
* @param models update configs.
* @group Data
*/
public updateData(
@ -1421,7 +1422,10 @@ export class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
/**
* Update one or more nodes' positions,
* do not update other styles which leads to better performance than updating positions by updateData.
* @param models new configurations with x and y for every node, which has id field to indicate the specific item
* @param models new configurations with x and y for every node, which has id field to indicate the specific item.
* @param upsertAncestors whether update the ancestors in combo tree.
* @param disableAnimate whether disable the animation for this call.
* @param callback callback function after update nodes done.
* @group Data
*/
public updateNodePosition(
@ -1436,7 +1440,6 @@ export class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
model: NodeModel | EdgeModel | ComboModel,
canceled?: boolean,
) => void,
stack?: boolean,
) {
return this.updatePosition(
'node',
@ -1444,7 +1447,6 @@ export class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
upsertAncestors,
disableAnimate,
callback,
stack,
);
}
@ -1452,7 +1454,10 @@ export class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
* Update one or more combos' positions,
* do not update other styles which leads to better performance than updating positions by updateData.
* In fact, it changes the succeed nodes positions to affect the combo's position, but not modify the combo's position directly.
* @param models new configurations with x and y for every combo, which has id field to indicate the specific item
* @param models new configurations with x and y for every combo, which has id field to indicate the specific item.
* @param upsertAncestors whether update the ancestors in combo tree.
* @param disableAnimate whether disable the animation for this call.
* @param callback callback function after update combos done.
* @group Data
*/
public updateComboPosition(
@ -1464,7 +1469,6 @@ export class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
upsertAncestors?: boolean,
disableAnimate = false,
callback?: (model: NodeModel | EdgeModel | ComboModel) => void,
stack?: boolean,
) {
return this.updatePosition(
'combo',
@ -1472,7 +1476,6 @@ export class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
upsertAncestors,
disableAnimate,
callback,
stack,
);
}
@ -1503,7 +1506,6 @@ export class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
model: NodeModel | EdgeModel | ComboModel,
canceled?: boolean,
) => void,
stack?: boolean,
) {
const modelArr = isArray(models) ? models : [models];
const { graphCore } = this.dataController;
@ -1865,7 +1867,8 @@ export class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
/**
* Add a new combo to the graph, and update the structure of the existed child in childrenIds to be the children of the new combo.
* Different from addData with combo type, this API update the succeeds' combo tree strucutres in the same time.
* @param model combo user data
* @param model combo user data.
* @param childrenIds the ids of the children nodes / combos to move into the new combo.
* @returns whether success
* @group Combo
*/
@ -1913,7 +1916,7 @@ export class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
}
/**
* Collapse a combo.
* @param comboId combo ids
* @param comboId combo id or ids' array.
* @group Combo
*/
public collapseCombo(comboIds: ID | ID[]) {
@ -1933,7 +1936,7 @@ export class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
}
/**
* Expand a combo.
* @param combo combo ids
* @param comboId combo id or ids' array.
* @group Combo
*/
public expandCombo(comboIds: ID | ID[]) {
@ -1956,7 +1959,11 @@ export class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
* Move one or more combos a distance (dx, dy) relatively,
* do not update other styles which leads to better performance than updating positions by updateData.
* In fact, it changes the succeed nodes positions to affect the combo's position, but not modify the combo's position directly.
* @param models new configurations with x and y for every combo, which has id field to indicate the specific item
* @param models new configurations with x and y for every combo, which has id field to indicate the specific item.
* @param dx the distance alone x-axis to move the combo.
* @param dy the distance alone y-axis to move the combo.
* @param upsertAncestors whether update the ancestors in the combo tree.
* @param callback callback function after move combo done.
* @group Combo
*/
public moveCombo(
@ -2909,7 +2916,7 @@ export class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
* @returns
* @group Tree
*/
public collapse(ids: ID | ID[], disableAnimate = false, stack?: boolean) {
public collapse(ids: ID | ID[], disableAnimate = false) {
this.hooks.treecollapseexpand.emit({
ids: isArray(ids) ? ids : [ids],
action: 'collapse',
@ -2925,7 +2932,7 @@ export class Graph<B extends BehaviorRegistry, T extends ThemeRegistry>
* @returns
* @group Tree
*/
public expand(ids: ID | ID[], disableAnimate = false, stack?: boolean) {
public expand(ids: ID | ID[], disableAnimate = false) {
this.hooks.treecollapseexpand.emit({
ids: isArray(ids) ? ids : [ids],
action: 'expand',

View File

@ -425,12 +425,12 @@ export class LodController extends Base {
}
return false;
};
const inBoundary = (rowIdx, colIdx) => {
const outBoundary = (rowIdx, colIdx) => {
return (
rowIdx < canvasCellRowRange[0] ||
rowIdx > canvasCellRowRange[1] ||
colIdx < canvasCellColRange[0] ||
colIdx > canvasCellColRange[1]
rowIdx <= canvasCellRowRange[0] ||
rowIdx >= canvasCellRowRange[1] ||
colIdx <= canvasCellColRange[0] ||
colIdx >= canvasCellColRange[1]
);
};
const newlyOutView = [];
@ -456,7 +456,7 @@ export class LodController extends Base {
outView.push(model);
if (!previousOutView.has(model.id)) newlyOutView.push(model);
}
} else if (inBoundary(rowIdx, colIdx)) {
} else if (outBoundary(rowIdx, colIdx)) {
outView.push(model);
if (!previousOutView.has(model.id)) newlyOutView.push(model);
} else {
@ -478,7 +478,7 @@ export class LodController extends Base {
} else {
outViewNew.push(model);
}
} else if (inBoundary(rowIdx, colIdx)) {
} else if (outBoundary(rowIdx, colIdx)) {
outViewNew.push(model);
} else {
inView.push(model);
@ -500,7 +500,7 @@ export class LodController extends Base {
} else {
inViewNew.push(model);
}
} else if (inBoundary(rowIdx, colIdx)) {
} else if (outBoundary(rowIdx, colIdx)) {
outView.push(model);
newlyOutView.push(model);
} else {

View File

@ -194,7 +194,6 @@ export interface IGraph<
* Add one or more node/edge/combo data to the graph.
* @param itemType item type
* @param model user data
* @param stack whether push this operation to stack
* @returns whehter success
* @group Data
*/
@ -207,7 +206,6 @@ export interface IGraph<
| NodeUserModel[]
| EdgeUserModel[]
| ComboUserModel[],
stack?: boolean,
) =>
| NodeModel
| EdgeModel
@ -217,17 +215,16 @@ export interface IGraph<
| ComboModel[];
/**
* Remove one or more node/edge/combo data from the graph.
* @param item the item to be removed
* @param stack whether push this operation to stack
* @param itemType the type the item(s) to be removed.
* @param id the id or the ids' array of the items to be removed.
* @returns whehter success
* @group Data
*/
removeData: (itemType: ITEM_TYPE, id: ID | ID[], stack?: boolean) => void;
removeData: (itemType: ITEM_TYPE, id: ID | ID[]) => void;
/**
* Update one or more node/edge/combo data on the graph.
* @param item the item to be updated
* @param model update configs
* @param {boolean} stack whether push this operation to stack
* @param itemType the type the item(s) to be udated.
* @param models update configs.
* @group Data
*/
updateData: (
@ -241,7 +238,6 @@ export interface IGraph<
| Partial<EdgeUserModel>[]
| Partial<ComboUserModel>[]
>,
stack?: boolean,
) =>
| NodeModel
| EdgeModel
@ -253,8 +249,10 @@ export interface IGraph<
/**
* Update one or more nodes' positions,
* do not update other styles which leads to better performance than updating positions by updateData.
* @param models new configurations with x and y for every node, which has id field to indicate the specific item
* @param {boolean} stack whether push this operation into graph's stack, true by default
* @param models new configurations with x and y for every node, which has id field to indicate the specific item.
* @param upsertAncestors whether update the ancestors in combo tree.
* @param disableAnimate whether disable the animation for this call.
* @param callback callback function after update nodes done.
* @group Data
*/
updateNodePosition: (
@ -269,14 +267,15 @@ export interface IGraph<
model: NodeModel | EdgeModel | ComboModel,
canceled?: boolean,
) => void,
stack?: boolean,
) => NodeModel | ComboModel | NodeModel[] | ComboModel[];
/**
* Update one or more combos' positions, it is achieved by move the succeed nodes.
* Do not update other styles which leads to better performance than updating positions by updateData.
* @param models new configurations with x and y for every combo, which has id field to indicate the specific item
* @param {boolean} stack whether push this operation into graph's stack, true by default
* @param models new configurations with x and y for every combo, which has id field to indicate the specific item.
* @param upsertAncestors whether update the ancestors in combo tree.
* @param disableAnimate whether disable the animation for this call.
* @param callback callback function after update combos done.
* @group Data
*/
updateComboPosition: (
@ -288,15 +287,17 @@ export interface IGraph<
upsertAncestors?: boolean,
disableAnimate?: boolean,
callback?: (model: NodeModel | EdgeModel | ComboModel) => void,
stack?: boolean,
) => NodeModel | ComboModel | NodeModel[] | ComboModel[];
/**
* Move one or more combos a distance (dx, dy) relatively,
* do not update other styles which leads to better performance than updating positions by updateData.
* In fact, it changes the succeed nodes positions to affect the combo's position, but not modify the combo's position directly.
* @param models new configurations with x and y for every combo, which has id field to indicate the specific item
* @param {boolean} stack whether push this operation into graph's stack, true by default
* @param models new configurations with x and y for every combo, which has id field to indicate the specific item.
* @param dx the distance alone x-axis to move the combo.
* @param dy the distance alone y-axis to move the combo.
* @param upsertAncestors whether update the ancestors in the combo tree.
* @param callback callback function after move combo done.
* @group Data
*/
moveCombo: (
@ -308,7 +309,6 @@ export interface IGraph<
model: NodeModel | EdgeModel | ComboModel,
canceled?: boolean,
) => void,
stack?: boolean,
) => ComboModel[];
// ===== view operations =====
@ -515,14 +515,14 @@ export interface IGraph<
* @returns
* @group Item
*/
frontItem: (ids: ID | ID[], stack?: boolean) => void;
frontItem: (ids: ID | ID[]) => void;
/**
* Make the item(s) to the back.
* @param ids the item id(s) to back
* @returns
* @group Item
*/
backItem: (ids: ID | ID[], stack?: boolean) => void;
backItem: (ids: ID | ID[]) => void;
/**
* Set state for the item(s).
* @param ids the id(s) for the item(s) to be set
@ -531,12 +531,7 @@ export interface IGraph<
* @returns
* @group Item
*/
setItemState: (
ids: ID | ID[],
state: string,
value: boolean,
stack?: boolean,
) => void;
setItemState: (ids: ID | ID[], state: string, value: boolean) => void;
/**
* Get the state value for an item.
* @param id the id for the item
@ -559,7 +554,7 @@ export interface IGraph<
* @returns
* @group Item
*/
clearItemState: (ids: ID | ID[], states?: string[], stack?: boolean) => void;
clearItemState: (ids: ID | ID[], states?: string[]) => void;
/**
* Get the rendering bbox for a node / edge / combo, or the graph (when the id is not assigned).
@ -591,29 +586,25 @@ export interface IGraph<
/**
* Add a new combo to the graph, and update the structure of the existed child in childrenIds to be the children of the new combo.
* Different from addData with combo type, this API update the succeeds' combo tree strucutres in the same time.
* @param model combo user data
* @param stack whether push this operation to stack
* @param model combo user data.
* @param childrenIds the ids of the children nodes / combos to move into the new combo.
* @returns whether success
* @group Combo
*/
addCombo: (
model: ComboUserModel,
childrenIds: ID[],
stack?: boolean,
) => ComboModel;
addCombo: (model: ComboUserModel, childrenIds: ID[]) => ComboModel;
/**
* Collapse a combo.
* @param comboId combo id or item
* @param comboId combo id or ids' array.
* @group Combo
*/
collapseCombo: (comboIds: ID | ID[], stack?: boolean) => void;
collapseCombo: (comboIds: ID | ID[]) => void;
/**
* Expand a combo.
* @group Combo
* @param combo combo ID combo
* @param comboId combo id or ids' array.
* @group Combo
*/
expandCombo: (comboIds: ID | ID[], stack?: boolean) => void;
expandCombo: (comboIds: ID | ID[]) => void;
// ===== layout =====
/**
@ -829,18 +820,16 @@ export interface IGraph<
* Collapse sub tree(s).
* @param ids Root id(s) of the sub trees.
* @param disableAnimate Whether disable the animations for this operation.
* @param stack Whether push this operation to stack.
* @returns
* @group Tree
*/
collapse: (ids: ID | ID[], disableAnimate?: boolean, stack?: boolean) => void;
collapse: (ids: ID | ID[], disableAnimate?: boolean) => void;
/**
* Expand sub tree(s).
* @param ids Root id(s) of the sub trees.
* @param disableAnimate Whether disable the animations for this operation.
* @param stack Whether push this operation to stack.
* @returns
* @group Tree
*/
expand: (ids: ID | ID[], disableAnimate?: boolean, stack?: boolean) => void;
expand: (ids: ID | ID[], disableAnimate?: boolean) => void;
}

View File

@ -40,19 +40,7 @@ type Workerized = {
iterations?: number;
};
export type ImmediatelyInvokedLayoutOptions = {
/**
* like an IIFE.
*/
execute: (graph: GraphCore, options?: any) => Promise<LayoutMapping>;
} & Animatable;
type CustomLayout = {
type: string;
[option: string]: any;
};
export type StandardLayoutOptions = (
type PureLayoutOptions =
| CircularLayout
| RandomLayout
| ConcentricLayout
@ -63,10 +51,27 @@ export type StandardLayoutOptions = (
| D3ForceLayout
| ForceLayout
| ForceAtlas2
| CustomLayout
) &
| CustomLayout;
export type ImmediatelyInvokedLayoutOptions = {
/**
* like an IIFE.
*/
execute: (graph: GraphCore, options?: any) => Promise<LayoutMapping>;
} & Animatable & {
preset?: PureLayoutOptions;
};
type CustomLayout = {
type: string;
[option: string]: any;
};
export type StandardLayoutOptions = PureLayoutOptions &
Animatable &
Workerized;
Workerized & {
preset?: PureLayoutOptions;
};
export type LayoutOptions =
| StandardLayoutOptions

View File

@ -1711,6 +1711,10 @@ const data = {
],
};
data.edges.forEach((edge, i) => (edge.id = 'edge' + i));
data.nodes.forEach((node) => {
delete node.data.x;
delete node.data.y;
});
export default (
context: TestCaseContext,
@ -1770,37 +1774,42 @@ export default (
data: {
...model.data,
animates: {
buildIn: [
update: [
{
fields: ['opacity'],
duration: 1000,
delay: 1000 + Math.random() * 1000,
},
],
hide: [
{
fields: ['opacity'],
duration: 1000,
shapeId: 'labelShape',
},
{
fields: ['opaicty'],
duration: 1000,
shapeId: 'labelBackgroundShape',
},
],
show: [
{
fields: ['opacity'],
duration: 1000,
shapeId: 'labelShape',
},
{
fields: ['opaicty'],
duration: 1000,
shapeId: 'labelBackgroundShape',
fields: ['x', 'y'],
},
],
// buildIn: [
// {
// fields: ['opacity'],
// duration: 1000,
// delay: 1000 + Math.random() * 1000,
// },
// ],
// hide: [
// {
// fields: ['opacity'],
// duration: 1000,
// shapeId: 'labelShape',
// },
// {
// fields: ['opaicty'],
// duration: 1000,
// shapeId: 'labelBackgroundShape',
// },
// ],
// show: [
// {
// fields: ['opacity'],
// duration: 1000,
// shapeId: 'labelShape',
// },
// {
// fields: ['opaicty'],
// duration: 1000,
// shapeId: 'labelBackgroundShape',
// },
// ],
},
labelShape: {
position: 'bottom',
@ -1822,6 +1831,18 @@ export default (
},
};
},
layout: {
preset: {
type: 'concentric',
},
type: 'force',
linkDistance: 100,
edgeStrength: 1000,
nodeStrength: 2000,
// animated: true,
// maxSpeed: 10000,
// minMovement: 0.1,
},
});
graph.on('canvas:click', (e) => {
@ -1834,21 +1855,50 @@ export default (
// y: y + 10,
// },
// });
// graph.updateData('node', {
// id: 'node1',
// data: {
// label: 'changedasdfasfasdfasdf',
// },
// });
// graph.downloadFullImage();
// graph.fitView();
// graph.changeData(data);
graph.hideItem('Marius');
graph.showItem('Marius');
// graph.hideItem('Marius');
// graph.showItem('Marius');
console.log(
'click',
graph.itemController.itemMap.get('Gervais')?.labelGroup,
);
});
let allData = { ...data };
graph.on('node:click', (e) => {
const newNodes = [];
for (let i = 0; i < 5; i++) {
newNodes.push({
id: `node-${e.itemId}-${i}`,
data: {},
});
}
const newEdges = [];
for (let i = 0; i < 5; i++) {
newEdges.push({
id: `newedge-${e.itemId}-${i}`,
source: e.itemId,
target: `node-${e.itemId}-${i}`,
data: {},
});
}
console.log('chagneDat');
allData = {
nodes: allData.nodes.concat(newNodes),
edges: allData.edges.concat(newEdges),
};
graph.changeData(allData, 'mergeReplace', false);
graph.layout({
preset: {},
});
});
return graph;
};