Skip to content

Commit

Permalink
Added customizable snap points for edges, for graphs with 'snapped' e…
Browse files Browse the repository at this point in the history
…dge type in graph.

The position is calculated using percentage based anchor (defaulting to current behavior) + pixel based offset (defaulting to zero)

PiperOrigin-RevId: 712891596
  • Loading branch information
Googler authored and copybara-github committed Jan 8, 2025
1 parent 49144e5 commit e6964ed
Show file tree
Hide file tree
Showing 5 changed files with 212 additions and 39 deletions.
24 changes: 22 additions & 2 deletions src/app/directed_acyclic_graph.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ import {ScreenshotTest} from '../screenshot_test';
import {ColorThemeLoader} from './color_theme_loader';
import {DagStateService} from './dag-state.service';
import {STATE_SERVICE_PROVIDER} from './dag-state.service.provider';
import {DirectedAcyclicGraph, DirectedAcyclicGraphModule} from './directed_acyclic_graph';
import {DirectedAcyclicGraph, DirectedAcyclicGraphModule, generateTheme} from './directed_acyclic_graph';
import {DagNode as Node, type GraphSpec, type NodeRef} from './node_spec';
import {TEST_IMPORTS, TEST_PROVIDERS} from './test_providers';
import {DirectedAcyclicGraphHarness} from './test_resources/directed_acyclic_graph_harness';
import {createDagSkeletonWithCustomGroups, createDagSkeletonWithGroups, fakeGraph} from './test_resources/fake_data';
import {createDagSkeletonWithCustomGroups, createDagSkeletonWithGroups, fakeGraph, fakeGraphWithEdgeOffsets} from './test_resources/fake_data';
import {initTestBed} from './test_resources/test_utils';

const FAKE_DATA: GraphSpec =
Expand Down Expand Up @@ -212,6 +212,24 @@ describe('Directed Acyclic Graph Renderer', () => {
await screenShot.expectMatch(`graph_loading`);
});
});

describe('with edge offsets', () => {
let fixture: ComponentFixture<TestComponent>;

beforeEach(() => {
fixture = TestBed.createComponent(TestComponent);
const graphSpec = Node.createFromSkeleton(
fakeGraphWithEdgeOffsets.skeleton, fakeGraphWithEdgeOffsets.state);
fixture.componentRef.setInput('graph', graphSpec);
fixture.componentRef.setInput(
'theme', generateTheme({edgeStyle: 'snapped'}));
fixture.detectChanges();
});

it('renders correctly', async () => {
await screenShot.expectMatch(`graph_with_edge_offsets`);
});
});
});
});

Expand All @@ -226,6 +244,7 @@ describe('Directed Acyclic Graph Renderer', () => {
[followNode]="followNode"
[loading]="loading"
[customNodeTemplates]="{'outlineBasic': outlineBasic}"
[theme]="theme"
>
</ai-dag-renderer>
</div>
Expand Down Expand Up @@ -267,4 +286,5 @@ class TestComponent {
@Input() graph: GraphSpec = FAKE_DATA;
@Input() followNode: NodeRef|null = null;
@Input() loading = false;
@Input() theme = generateTheme({});
}
130 changes: 94 additions & 36 deletions src/app/directed_acyclic_graph_raw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,64 @@ import {fetchIcon, generateFullIconFor} from './icon_util';
import {WorkflowGraphIconModule} from './icon_wrapper';
import {DagNodeModule} from './node';
import {NodeRefBadgeModule} from './node_ref_badge';
import {CustomNode, type DagEdge, DagGroup, DagNode, GroupIterationRecord, GroupToggleEvent, isDagreInit, isSamePath, NodeMap, NodeRef, NodeType, Point, type SelectedNode} from './node_spec';
import {CustomNode, type DagEdge, DagGroup, DagNode, GroupIterationRecord, GroupToggleEvent, isDagreInit, isSamePath, NodeMap, NodeRef, NodeType, Point, type SelectedNode, SnapPoint} from './node_spec';
import {UserConfigService} from './user_config.service';
import {debounce, isPinch} from './util_functions';

// tslint:disable:no-dict-access-on-struct-type

const CENTER = 0.5;
const START = 0;
const END = 1;

const DEFAULT_EDGE_START_SNAP_ANCHORS = {
'LR': {
horizontalPercent: END,
verticalPercent: CENTER,
},
'RL': {
horizontalPercent: START,
verticalPercent: CENTER,
},
'BT': {
horizontalPercent: CENTER,
verticalPercent: START,
},
'TB': {
horizontalPercent: CENTER,
verticalPercent: END,
},

};

const DEFAULT_EDGE_END_SNAP_ANCHORS = {
'LR': {
horizontalPercent: START,
verticalPercent: CENTER,
},
'RL': {
horizontalPercent: END,
verticalPercent: CENTER,
},
'BT': {
horizontalPercent: CENTER,
verticalPercent: END,
},
'TB': {
horizontalPercent: CENTER,
verticalPercent: START,
},

};

/**
* Calculates the where alongside an axis will the edge be attached, based on
* the node length along said axis, and edge's anchor and offset.
*/
function edgeAttachmentPoint(length: number, percent: number, offset: number) {
return length * (percent - 0.5) + offset;
}

/**
* To better position edge labels, we need a higher resolution representation of
* edge points than what dagre returns. This function will calculate the points
Expand Down Expand Up @@ -735,19 +787,6 @@ export class DagRaw implements DoCheck, OnInit, OnDestroy {
this.cdr.detectChanges();
}

getTopCenterPoint(node: DagNode|DagGroup): Point {
return {x: node.x, y: node.y - node.height / 2};
}
getBottomCenterPoint(node: DagNode|DagGroup): Point {
return {x: node.x, y: node.y + node.height / 2};
}
getLeftCenterPoint(node: DagNode|DagGroup): Point {
return {x: node.x - node.width / 2, y: node.y};
}
getRightCenterPoint(node: DagNode|DagGroup): Point {
return {x: node.x + node.width / 2, y: node.y};
}

/**
* Set edge points with the node border center point based on direction.
* http://screen/9pr65gGswBgzicS
Expand All @@ -767,28 +806,47 @@ export class DagRaw implements DoCheck, OnInit, OnDestroy {
const fromTarget = this.ensureTargetEntity(edge.from);
const toTarget = this.ensureTargetEntity(edge.to);

const layoutDirection = this.layout.rankDirection;
if (layoutDirection === 'LR') {
edge.points = [
this.getRightCenterPoint(fromTarget),
this.getLeftCenterPoint(toTarget),
];
} else if (layoutDirection === 'RL') {
edge.points = [
this.getLeftCenterPoint(fromTarget),
this.getRightCenterPoint(toTarget),
];
} else if (layoutDirection === 'BT') {
edge.points = [
this.getTopCenterPoint(fromTarget),
this.getBottomCenterPoint(toTarget),
];
} else {
edge.points = [
this.getBottomCenterPoint(fromTarget),
this.getTopCenterPoint(toTarget),
];
}
const layoutDirection = this.layout.rankDirection ?? 'TB';

const startHorizontalAnchor = edge.startSnapPoint?.horizontalPercent ??
DEFAULT_EDGE_START_SNAP_ANCHORS[layoutDirection].horizontalPercent;
const startVerticalAnchor = edge.startSnapPoint?.verticalPercent ??
DEFAULT_EDGE_START_SNAP_ANCHORS[layoutDirection].verticalPercent;
const endHorizontalAnchor = edge.endSnapPoint?.horizontalPercent ??
DEFAULT_EDGE_END_SNAP_ANCHORS[layoutDirection].horizontalPercent;
const endVerticalAnchor = edge.endSnapPoint?.verticalPercent ??
DEFAULT_EDGE_END_SNAP_ANCHORS[layoutDirection].verticalPercent;

const startPoint = {
x: fromTarget.x +
edgeAttachmentPoint(
fromTarget.width,
startHorizontalAnchor,
edge.startSnapPoint?.horizontalOffset ?? 0,
),
y: fromTarget.y +
edgeAttachmentPoint(
fromTarget.height,
startVerticalAnchor,
edge.startSnapPoint?.verticalOffset ?? 0,
),
};
const endPoint = {
x: toTarget.x +
edgeAttachmentPoint(
toTarget.width,
endHorizontalAnchor,
edge.endSnapPoint?.horizontalOffset ?? 0,
),
y: toTarget.y +
edgeAttachmentPoint(
toTarget.height,
endVerticalAnchor,
edge.endSnapPoint?.verticalOffset ?? 0,
),
};

edge.points = [startPoint, endPoint];
}

/**
Expand Down
29 changes: 29 additions & 0 deletions src/app/node_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,29 @@ export class DagGroup implements
}
}

/**
* Position in the node where the edge should be attached.
* Only relevant if using theme with edge mode set to 'snapped'.
*/
export interface SnapPoint {
/**
* Anchor point, in percentage of width/height of the node from which real
* edge attachment point is then offset from.
*
* Anchor is calculated from top-left of the node, and defaults to the
* corresponding edge (0 or 1) of the node in the layout axis, and center of
* the node (0.5) on the other axis.
*/
horizontalPercent?: number;
verticalPercent?: number;
/**
* Offset, in px, from the anchor point, to where the edge is be attached.
* Both offsets default to 0.
*/
horizontalOffset?: number;
verticalOffset?: number;
}

/**
* Dag Edge relationship
*
Expand All @@ -436,6 +459,12 @@ export interface DagEdge {
weight?: number;
/** The number of ranks to keep between the source and target of the edge. */
minlen?: number;
/**
* Only relevant for snapped edges.
* Edge mode can be set to 'snapped' in the theme.
*/
startSnapPoint?: SnapPoint;
endSnapPoint?: SnapPoint;
}

/**
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
68 changes: 67 additions & 1 deletion src/app/test_resources/fake_data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
* various testing scenarios
*/

import {baseColors, CustomNode, DagNode, DagNodeSkeleton, DagSkeleton, repeatedMetaNodes, StateTable} from '../node_spec';
import {baseColors, CustomNode, DagNode, DagNodeSkeleton, DagSkeleton, repeatedMetaNodes, SnapPoint, StateTable} from '../node_spec';

/**
* The standard Fake Graph that can be used by `DagNode.createFromSkeleton()` to
Expand Down Expand Up @@ -657,3 +657,69 @@ export function createDagSkeletonWithGroups(treatAsLoop: boolean): DagSkeleton {
] as DagNodeSkeleton[],
};
}

export const fakeGraphWithEdgeOffsets: DagSkeleton = {
skeleton: [
{
id: 'node1',
type: 'execution',
next: [
{
id: 'node2',
type: 'execution',
edgeOpts: {
startSnapPoint: {
horizontalOffset: 5,
verticalOffset: 15,
horizontalPercent: 0.4,
verticalPercent: 0.6,
},
endSnapPoint: {
horizontalPercent: 0.5,
verticalPercent: 0.5,
},
},
},
],
},
{
id: 'node2',
type: 'execution',
next: [
{
id: 'node3',
type: 'execution',
edgeOpts: {
startSnapPoint: {
horizontalOffset: 15,
verticalOffset: -5,
},
endSnapPoint: {
horizontalOffset: 30,
verticalPercent: 0.2,
},
},
},
{
id: 'node4',
type: 'execution',
edgeOpts: {
startSnapPoint: {},
endSnapPoint: {
horizontalAnchor: -0.1,
verticalAnchor: -0.1,
},
},
},
],
},
{
id: 'node3',
type: 'execution',
},
{
id: 'node4',
type: 'execution',
},
] as DagNodeSkeleton[],
};

0 comments on commit e6964ed

Please sign in to comment.