mirror of
https://github.com/weaveworks/scope.git
synced 2026-03-04 10:41:14 +00:00
Fix arrow heads positioning (#2505)
* Fix arrow heads positioning. * Addressed @foot's comment. * Beautified the code and added some comments.
This commit is contained in:
@@ -5,14 +5,9 @@ import { line, curveBasis } from 'd3-shape';
|
||||
import { each, omit, times, constant } from 'lodash';
|
||||
|
||||
import { NODES_SPRING_ANIMATION_CONFIG } from '../constants/animation';
|
||||
import { uniformSelect } from '../utils/array-utils';
|
||||
import { EDGE_WAYPOINTS_CAP } from '../constants/styles';
|
||||
import Edge from './edge';
|
||||
|
||||
// Tweak this value for the number of control
|
||||
// points along the edge curve, e.g. values:
|
||||
// * 2 -> edges are simply straight lines
|
||||
// * 4 -> minimal value for loops to look ok
|
||||
const WAYPOINTS_COUNT = 8;
|
||||
|
||||
const spline = line()
|
||||
.curve(curveBasis)
|
||||
@@ -23,11 +18,11 @@ const transformedEdge = (props, path) => (
|
||||
<Edge {...props} path={spline(path)} />
|
||||
);
|
||||
|
||||
// Converts a waypoints map of the format {x0: 11, y0: 22, x1: 33, y1: 44}
|
||||
// Converts a waypoints map of the format { x0: 11, y0: 22, x1: 33, y1: 44 }
|
||||
// that is used by Motion to an array of waypoints in the format
|
||||
// [{x: 11, y: 22}, {x: 33, y: 44}] that can be used by D3.
|
||||
// [{ x: 11, y: 22 }, { x: 33, y: 44 }] that can be used by D3.
|
||||
const waypointsMapToArray = (waypointsMap) => {
|
||||
const waypointsArray = times(WAYPOINTS_COUNT, () => ({ x: 0, y: 0}));
|
||||
const waypointsArray = times(EDGE_WAYPOINTS_CAP, () => ({}));
|
||||
each(waypointsMap, (value, key) => {
|
||||
const [axis, index] = [key[0], key.slice(1)];
|
||||
waypointsArray[index][axis] = value;
|
||||
@@ -35,6 +30,17 @@ const waypointsMapToArray = (waypointsMap) => {
|
||||
return waypointsArray;
|
||||
};
|
||||
|
||||
// Converts a waypoints array of the input format [{ x: 11, y: 22 }, { x: 33, y: 44 }]
|
||||
// to an array of waypoints that is used by Motion in the format { x0: 11, y0: 22, x1: 33, y1: 44 }.
|
||||
const waypointsArrayToMap = (waypointsArray) => {
|
||||
let waypointsMap = makeMap();
|
||||
waypointsArray.forEach((point, index) => {
|
||||
waypointsMap = waypointsMap.set(`x${index}`, spring(point.x, NODES_SPRING_ANIMATION_CONFIG));
|
||||
waypointsMap = waypointsMap.set(`y${index}`, spring(point.y, NODES_SPRING_ANIMATION_CONFIG));
|
||||
});
|
||||
return waypointsMap;
|
||||
};
|
||||
|
||||
|
||||
export default class EdgeContainer extends React.PureComponent {
|
||||
constructor(props, context) {
|
||||
@@ -65,39 +71,24 @@ export default class EdgeContainer extends React.PureComponent {
|
||||
|
||||
return (
|
||||
// For the Motion interpolation to work, the waypoints need to be in a map format like
|
||||
// {x0: 11, y0: 22, x1: 33, y1: 44} that we convert to the array format when rendering.
|
||||
// { x0: 11, y0: 22, x1: 33, y1: 44 } that we convert to the array format when rendering.
|
||||
<Motion style={this.state.waypointsMap.toJS()}>
|
||||
{interpolated => transformedEdge(forwardedProps, waypointsMapToArray(interpolated))}
|
||||
</Motion>
|
||||
);
|
||||
}
|
||||
|
||||
prepareWaypointsForMotion(nextWaypoints) {
|
||||
nextWaypoints = nextWaypoints.toJS();
|
||||
prepareWaypointsForMotion(waypoints) {
|
||||
waypoints = waypoints.toJS();
|
||||
|
||||
// Motion requires a constant number of waypoints along the path of each edge
|
||||
// for the animation to work correctly, but dagre might be changing their number
|
||||
// depending on the dynamic topology reconfiguration. Here we are transforming
|
||||
// the waypoints array given by dagre to the fixed size of `WAYPOINTS_COUNT` that
|
||||
// Motion could take over.
|
||||
const waypointsMissing = WAYPOINTS_COUNT - nextWaypoints.length;
|
||||
// The Motion library requires the number of waypoints to be constant, so we fill in for
|
||||
// the missing ones by reusing the edge source point, which doesn't affect the edge shape
|
||||
// because of how the curveBasis interpolation is done.
|
||||
const waypointsMissing = EDGE_WAYPOINTS_CAP - waypoints.length;
|
||||
if (waypointsMissing > 0) {
|
||||
// Whenever there are some waypoints missing, we simply populate the beginning of the
|
||||
// array with the first element, as this leaves the curve interpolation unchanged.
|
||||
nextWaypoints = times(waypointsMissing, constant(nextWaypoints[0])).concat(nextWaypoints);
|
||||
} else if (waypointsMissing < 0) {
|
||||
// If there are 'too many' waypoints given by dagre, we select a sub-array of
|
||||
// uniformly distributed indices. Note that it is very important to keep the first
|
||||
// and the last endpoints in the array as they are the ones connecting the nodes.
|
||||
nextWaypoints = uniformSelect(nextWaypoints, WAYPOINTS_COUNT);
|
||||
waypoints = times(waypointsMissing, constant(waypoints[0])).concat(waypoints);
|
||||
}
|
||||
|
||||
let { waypointsMap } = this.state;
|
||||
nextWaypoints.forEach((point, index) => {
|
||||
waypointsMap = waypointsMap.set(`x${index}`, spring(point.x, NODES_SPRING_ANIMATION_CONFIG));
|
||||
waypointsMap = waypointsMap.set(`y${index}`, spring(point.y, NODES_SPRING_ANIMATION_CONFIG));
|
||||
});
|
||||
|
||||
this.setState({ waypointsMap });
|
||||
this.setState({ waypointsMap: waypointsArrayToMap(waypoints) });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,18 +2,21 @@ import dagre from 'dagre';
|
||||
import debug from 'debug';
|
||||
import { fromJS, Map as makeMap, Set as ImmSet } from 'immutable';
|
||||
|
||||
import { NODE_BASE_SIZE } from '../constants/styles';
|
||||
import { NODE_BASE_SIZE, EDGE_WAYPOINTS_CAP } from '../constants/styles';
|
||||
import { EDGE_ID_SEPARATOR } from '../constants/naming';
|
||||
import { featureIsEnabledAny } from '../utils/feature-utils';
|
||||
import { buildTopologyCacheId, updateNodeDegrees } from '../utils/topology-utils';
|
||||
import { uniformSelect } from '../utils/array-utils';
|
||||
|
||||
const log = debug('scope:nodes-layout');
|
||||
|
||||
const topologyCaches = {};
|
||||
export const DEFAULT_MARGINS = {top: 0, left: 0};
|
||||
const NODE_SIZE_FACTOR = NODE_BASE_SIZE;
|
||||
const NODE_SEPARATION_FACTOR = 1.5 * NODE_BASE_SIZE;
|
||||
const RANK_SEPARATION_FACTOR = 2.5 * NODE_BASE_SIZE;
|
||||
export const DEFAULT_MARGINS = { top: 0, left: 0 };
|
||||
// Pretend the nodes are bigger than they are so that the edges would not enter
|
||||
// them under a high curvature which would cause arrow heads to be misplaced.
|
||||
const NODE_SIZE_FACTOR = 1.5 * NODE_BASE_SIZE;
|
||||
const NODE_SEPARATION_FACTOR = 1 * NODE_BASE_SIZE;
|
||||
const RANK_SEPARATION_FACTOR = 2 * NODE_BASE_SIZE;
|
||||
let layoutRuns = 0;
|
||||
let layoutRunsTrivial = 0;
|
||||
|
||||
@@ -25,6 +28,42 @@ function fromGraphNodeId(encodedId) {
|
||||
return encodedId.replace('<DOT>', '.');
|
||||
}
|
||||
|
||||
// Adds some additional waypoints to the edge to make sure the it connects the node
|
||||
// centers and that the edge enters the target node relatively straight so that the
|
||||
// arrow is drawn correctly. The total number of waypoints is capped to EDGE_WAYPOINTS_CAP.
|
||||
function correctedEdgePath(waypoints, source, target) {
|
||||
// Get the relevant waypoints that will be added/replicated.
|
||||
const sourcePoint = fromJS({ x: source.get('x'), y: source.get('y') });
|
||||
const targetPoint = fromJS({ x: target.get('x'), y: target.get('y') });
|
||||
const entrancePoint = waypoints.last();
|
||||
|
||||
if (target !== source) {
|
||||
// The strategy for the non-loop edges is the following:
|
||||
// * Uniformly select at most CAP - 4 of the central waypoints ignoring the target node
|
||||
// entrance point. Such a selection will ensure that both the source node exit point and
|
||||
// the point before the target node entrance point are taken as boundaries of the interval.
|
||||
// * Now manually add those 4 points that we always want to have included in the edge path -
|
||||
// centers of source/target nodes and twice the target node entrance point to ensure the
|
||||
// edge path actually goes through it and thus doesn't miss the arrow element.
|
||||
// * In the end, what matters for the arrow is that the last 4 points of the array are always
|
||||
// fixed regardless of the total number of waypoints. That way we ensure the arrow is drawn
|
||||
// correctly, but also that the edge path enters the target node smoothly.
|
||||
waypoints = fromJS(uniformSelect(waypoints.butLast().toJS(), EDGE_WAYPOINTS_CAP - 4));
|
||||
waypoints = waypoints.unshift(sourcePoint);
|
||||
waypoints = waypoints.push(entrancePoint);
|
||||
waypoints = waypoints.push(entrancePoint);
|
||||
waypoints = waypoints.push(targetPoint);
|
||||
} else {
|
||||
// For loops we simply set the endpoints at the center of source/target node to
|
||||
// make them smoother and, of course, we cap the total number of waypoints.
|
||||
waypoints = fromJS(uniformSelect(waypoints.toJS(), EDGE_WAYPOINTS_CAP));
|
||||
waypoints = waypoints.set(0, sourcePoint);
|
||||
waypoints = waypoints.set(waypoints.size - 1, targetPoint);
|
||||
}
|
||||
|
||||
return waypoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Layout engine runner
|
||||
* After the layout engine run nodes and edges have x-y-coordinates. Engine is
|
||||
@@ -102,15 +141,12 @@ function runLayoutEngine(graph, imNodes, imEdges) {
|
||||
graph.edges().forEach((graphEdge) => {
|
||||
const graphEdgeMeta = graph.edge(graphEdge);
|
||||
const edge = edges.get(graphEdgeMeta.id);
|
||||
let points = fromJS(graphEdgeMeta.points);
|
||||
|
||||
// set beginning and end points to node coordinates to ignore node bounding box
|
||||
const source = nodes.get(fromGraphNodeId(edge.get('source')));
|
||||
const target = nodes.get(fromGraphNodeId(edge.get('target')));
|
||||
points = points.mergeIn([0], {x: source.get('x'), y: source.get('y')});
|
||||
points = points.mergeIn([points.size - 1], {x: target.get('x'), y: target.get('y')});
|
||||
const waypoints = correctedEdgePath(fromJS(graphEdgeMeta.points), source, target);
|
||||
|
||||
edges = edges.setIn([graphEdgeMeta.id, 'points'], points);
|
||||
edges = edges.setIn([graphEdgeMeta.id, 'points'], waypoints);
|
||||
});
|
||||
|
||||
// return object with the width and height of layout
|
||||
|
||||
@@ -31,6 +31,11 @@ export const UNIT_CLOUD_PATH = 'M-1.25 0.233Q-1.25 0.44-1.104 0.587-0.957 0.733-
|
||||
// are given on a small unit scale as foreign objects in SVG.
|
||||
export const NODE_BASE_SIZE = 100;
|
||||
|
||||
// This value represents the upper bound on the number of control points along the graph edge
|
||||
// curve. Any integer value >= 6 should result in valid edges, but generally the greater this
|
||||
// value is, the nicer the edge bundling will be. On the other hand, big values would result
|
||||
// in slower rendering of the graph.
|
||||
export const EDGE_WAYPOINTS_CAP = 10;
|
||||
|
||||
export const CANVAS_MARGINS = {
|
||||
[GRAPH_VIEW_MODE]: { top: 160, left: 40, right: 40, bottom: 150 },
|
||||
|
||||
Reference in New Issue
Block a user