Skip to content

Commit

Permalink
[KED-2212] Improve graph layout code quality, performance and docs (#347
Browse files Browse the repository at this point in the history
)

* [KED-2212] Refactor to tidy, improve comments on graph layout code

* [KED-2212] Invert control of graph constraint solver

* [KED-2212] Support multiple variables per object in strict solver

* [KED-2212] Update graph tests
  • Loading branch information
bru5 authored Feb 12, 2021
1 parent a535c71 commit ff9057f
Show file tree
Hide file tree
Showing 5 changed files with 432 additions and 352 deletions.
24 changes: 0 additions & 24 deletions src/utils/graph/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,30 +29,6 @@ export const snap = (value, unit) => Math.round(value / unit) * unit;
*/
export const distance1d = (a, b) => Math.abs(a - b);

/**
* Returns the value `a - b`
* @param {number} a The first number
* @param {number} b The second number
* @returns {number} The result
*/
export const subtract = (a, b) => a - b;

/**
* Returns `true` if `a === b` otherwise `false`
* @param {number} a The first value
* @param {number} b The second value
* @returns {boolean} The result
*/
export const equalTo = (a, b) => a === b;

/**
* Returns `true` if `a >= b` otherwise `false`
* @param {number} a The first number
* @param {number} b The second number
* @returns {boolean} The result
*/
export const greaterOrEqual = (a, b) => a >= b;

/**
* Returns the angle in radians between the points a and b relative to the X-axis about the origin
* @param {object} a The first point
Expand Down
177 changes: 113 additions & 64 deletions src/utils/graph/constraints.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { distance1d, greaterOrEqual, equalTo, subtract } from './common';
import { Constraint, Operator, Strength } from 'kiwi.js';

/**
* Constraint base definitions.
Expand All @@ -11,96 +11,145 @@ import { distance1d, greaterOrEqual, equalTo, subtract } from './common';
*/
export const rowConstraint = {
property: 'y',
difference: subtract,
distance: distance1d,
operator: greaterOrEqual,
target: (a, b, co, constants) => constants.spaceY,
strength: () => 1,
weightA: () => 0,
weightB: () => 1,
required: true,

solve: (constraint, constants) => {
const { a, b } = constraint;
const difference = a.y - b.y;
const target = constants.spaceY;

if (difference >= target) {
return;
}

const resolve = difference - target;
a.y -= 0.5 * resolve;
b.y += 0.5 * resolve;
},

strict: (constraint, constants, variableA, variableB) => {
return new Constraint(
variableA.minus(variableB),
Operator.Ge,
constants.spaceY,
Strength.required
);
},
};

/**
* Layout constraint in Y for separating layers
*/
export const layerConstraint = {
property: 'y',
difference: subtract,
distance: distance1d,
operator: greaterOrEqual,
target: (a, b, co, constants) => constants.layerSpace,
strength: () => 1,
weightA: () => 0,
weightB: () => 1,
required: false,

solve: (constraint, constants) => {
const { a, b } = constraint;
const difference = a.y - b.y;
const target = constants.layerSpace;

if (difference >= target) {
return;
}

const resolve = difference - target;
a.y -= 0.5 * resolve;
b.y += 0.5 * resolve;
},

strict: (constraint, constants, variableA, variableB) => {
return new Constraint(
variableA.minus(variableB),
Operator.Ge,
constants.layerSpace,
Strength.required
);
},
};

/**
* Layout constraint in X for minimising distance from source to target for straight edges
*/
export const parallelConstraint = {
property: 'x',
difference: subtract,
distance: distance1d,
operator: equalTo,
target: () => 0,
// Lower degree nodes can be moved more freely than higher
strength: (co) =>
1 / Math.max(1, 0.5 * (co.a.targets.length + co.b.sources.length)),
weightA: () => 0.5,
weightB: () => 0.5,
required: false,

solve: (constraint) => {
const { a, b } = constraint;
const difference = a.x - b.x;

if (difference === 0) {
return;
}

const strength =
1 / Math.max(1, 0.5 * (a.targets.length + b.sources.length));

const resolve = strength * difference;
a.x -= 0.5 * resolve;
b.x += 0.5 * resolve;
},

strict: (constraint, constants, variableA, variableB) => {
return new Constraint(
variableA.minus(variableB),
Operator.Eq,
0,
Strength.strong
);
},
};

/**
* Layout constraint in X for minimising edge crossings
*/
export const crossingConstraint = {
property: 'x',
difference: subtract,
distance: distance1d,
operator: (distance, target, difference) =>
target >= 0 ? difference >= target : difference <= target,
target: (a, b, co, constants) => {
// Find the minimal target position that separates both nodes
const sourceDelta = co.edgeA.sourceNode.x - co.edgeB.sourceNode.x;
const targetDelta = co.edgeA.targetNode.x - co.edgeB.targetNode.x;
return sourceDelta + targetDelta < 0 ? -constants.basisX : constants.basisX;

solve: (constraint, constants) => {
const { a, b, edgeA, edgeB } = constraint;
const difference = a.x - b.x;
const sourceDelta = edgeA.sourceNode.x - edgeB.sourceNode.x;
const targetDelta = edgeA.targetNode.x - edgeB.targetNode.x;
const target =
sourceDelta + targetDelta < 0 ? -constants.basisX : constants.basisX;

if (target >= 0 ? difference >= target : difference <= target) {
return;
}

const strength = 1 / constants.basisX;

const resolve = strength * (difference - target);
a.x -= 0.5 * resolve;
b.x += 0.5 * resolve;
},
strength: (co, constants) => 1 / constants.basisX,
weightA: () => 0.5,
weightB: () => 0.5,
required: false,
};

/**
* Layout constraint in X for minimum node separation (loose)
* Layout constraint in X for minimum node separation
*/
export const separationConstraint = {
property: 'x',
difference: subtract,
distance: distance1d,
operator: (distance, target, difference) => difference <= target,
target: (ax, bx, co, constants) =>
-constants.spaceX - co.a.width * 0.5 - co.b.width * 0.5,
strength: () => 1,
weightA: () => 0.5,
weightB: () => 0.5,
required: false,
};

/**
* Layout constraint in X for minimum node separation (strict)
*/
export const separationStrictConstraint = {
property: 'x',
difference: subtract,
distance: distance1d,
operator: greaterOrEqual,
target: (ax, bx, co) => co.separation,
strength: () => 1,
weightA: () => 0,
weightB: () => 1,
required: true,
solve: (constraint) => {
const { a, b } = constraint;
const difference = b.x - a.x;
const target = constraint.separation;

if (difference >= target) {
return;
}

const resolve = difference - target;
a.x += 0.5 * resolve;
b.x -= 0.5 * resolve;
},

strict: (constraint, constants, variableA, variableB) => {
return new Constraint(
variableB.minus(variableA),
Operator.Ge,
constraint.separation,
Strength.required
);
},
};
Loading

0 comments on commit ff9057f

Please sign in to comment.