Skip to content
This repository was archived by the owner on Apr 7, 2024. It is now read-only.

Commit 3630fee

Browse files
authored
Improve disconnected graph placement for circular strategy (#70)
1 parent fb5daa9 commit 3630fee

File tree

12 files changed

+241
-101
lines changed

12 files changed

+241
-101
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"@shopify/react-native-skia": "^0.1.185",
2222
"nativewind": "^2.0.11",
2323
"postcss": "^8.0.9",
24+
"potpack": "^2.0.0",
2425
"react": "18.2.0",
2526
"react-native": "0.71.5",
2627
"react-native-gesture-handler": "^2.9.0",

src/App.tsx

Lines changed: 61 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -6,101 +6,94 @@ import DefaultEdgeLabelRenderer from '@/components/graphs/labels/renderers/Defau
66
import { DirectedGraph } from '@/models/graphs';
77
import PannableScalableView from '@/views/PannableScalableView';
88

9-
import GraphEventsProvider from './context/graphEvents';
10-
11-
const CATEGORIES_GRAPH = {
9+
const GRAPH1 = {
1210
vertices: [
13-
{ key: 'Root', data: [] },
14-
{ key: 'Sport', data: [] },
15-
{ key: 'Health', data: [] },
16-
{ key: 'Diet', data: [] },
17-
{ key: 'Sleep', data: [] },
18-
{ key: 'Work', data: [] }
11+
{ key: 'A', data: 'A' },
12+
{ key: 'B', data: 'B' },
13+
{ key: 'C', data: 'C' },
14+
{ key: 'D', data: 'D' },
15+
{ key: 'E', data: 'E' }
1916
],
2017
edges: [
21-
{ from: 'Root', to: 'Sport', key: 'E1', data: [] },
22-
{ from: 'Root', to: 'Health', key: 'E2', data: [] },
23-
{ from: 'Root', to: 'Diet', key: 'E3', data: [] },
24-
{ from: 'Root', to: 'Sleep', key: 'E4', data: [] },
25-
{ from: 'Root', to: 'Work', key: 'E5', data: [] }
18+
{ key: 'AB', from: 'A', to: 'B', data: 'AB' },
19+
{ key: 'AC', from: 'A', to: 'C', data: 'AC' },
20+
{ key: 'AD', from: 'A', to: 'D', data: 'AD' },
21+
{ key: 'AE', from: 'A', to: 'E', data: 'AE' }
2622
]
2723
};
2824

29-
const SPORT_GRAPH = {
25+
const GRAPH2 = {
3026
vertices: [
31-
{ key: 'Sport', data: [] },
32-
{ key: 'Football', data: [] },
33-
{ key: 'Basketball', data: [] },
34-
{ key: 'Gym', data: [] },
35-
{ key: 'Running', data: [] },
36-
{ key: 'Cycling', data: [] },
37-
{ key: 'Root', data: [] }
27+
{ key: 'F', data: 'F' },
28+
{ key: 'G', data: 'G' },
29+
{ key: 'H', data: 'H' },
30+
{ key: 'I', data: 'I' },
31+
{ key: 'J', data: 'J' },
32+
{ key: 'K', data: 'K' },
33+
{ key: 'L', data: 'L' },
34+
{ key: 'M', data: 'M' },
35+
{ key: 'N', data: 'N' },
36+
{ key: 'O', data: 'O' }
3837
],
3938
edges: [
40-
{ from: 'Sport', to: 'Football', key: 'E1', data: [] },
41-
{ from: 'Sport', to: 'Basketball', key: 'E2', data: [] },
42-
{ from: 'Sport', to: 'Gym', key: 'E3', data: [] },
43-
{ from: 'Sport', to: 'Running', key: 'E4', data: [] },
44-
{ from: 'Sport', to: 'Cycling', key: 'E5', data: [] },
45-
{ from: 'Root', to: 'Sport', key: 'E6', data: [] }
39+
{ key: 'FG', from: 'F', to: 'G', data: 'FG' },
40+
{ key: 'FH', from: 'F', to: 'H', data: 'FH' },
41+
{ key: 'FI', from: 'F', to: 'I', data: 'FI' },
42+
{ key: 'GJ', from: 'G', to: 'J', data: 'GJ' },
43+
{ key: 'GK', from: 'G', to: 'K', data: 'GK' },
44+
{ key: 'GL', from: 'G', to: 'L', data: 'GL' },
45+
{ key: 'GM', from: 'G', to: 'M', data: 'GM' },
46+
{ key: 'GN', from: 'G', to: 'N', data: 'GN' },
47+
{ key: 'GO', from: 'G', to: 'O', data: 'GO' }
4648
]
4749
};
4850

49-
const GYM_GRAPH = {
51+
const GRAPH3 = {
5052
vertices: [
51-
{ key: 'Gym', data: [] },
52-
{ key: 'Bench Press', data: [] },
53-
{ key: 'Squats', data: [] },
54-
{ key: 'Deadlift', data: [] },
55-
{ key: 'Push Ups', data: [] },
56-
{ key: 'Sport', data: [] }
53+
{ key: 'P', data: 'P' },
54+
{ key: 'Q', data: 'Q' }
5755
],
5856
edges: [
59-
{ from: 'Gym', to: 'Bench Press', key: 'E1', data: [] },
60-
{ from: 'Gym', to: 'Squats', key: 'E2', data: [] },
61-
{ from: 'Gym', to: 'Deadlift', key: 'E3', data: [] },
62-
{ from: 'Gym', to: 'Push Ups', key: 'E5', data: [] },
63-
{ from: 'Sport', to: 'Gym', key: 'E6', data: [] }
57+
{ key: 'PQ1', from: 'P', to: 'Q', data: 'PQ1' },
58+
{ key: 'PQ2', from: 'P', to: 'Q', data: 'PQ2' }
6459
]
6560
};
6661

62+
const DISCONNECTED_GRAPH = {
63+
vertices: [...GRAPH1.vertices, ...GRAPH2.vertices, ...GRAPH3.vertices],
64+
edges: [...GRAPH1.edges, ...GRAPH2.edges, ...GRAPH3.edges]
65+
};
66+
6767
export default function App() {
6868
const graph = DirectedGraph.fromData(
69-
CATEGORIES_GRAPH.vertices,
70-
CATEGORIES_GRAPH.edges
69+
DISCONNECTED_GRAPH.vertices,
70+
DISCONNECTED_GRAPH.edges
7171
);
7272

7373
return (
7474
<SafeAreaView className='grow'>
7575
<GestureHandlerRootView className='grow'>
7676
<View className='grow bg-black'>
77-
<GraphEventsProvider
78-
onVertexPress={({ key }) => {
79-
if (key === 'Sport') {
80-
graph.replaceBatch(SPORT_GRAPH);
81-
} else if (key === 'Gym') {
82-
graph.replaceBatch(GYM_GRAPH);
83-
} else if (key === 'Root') {
84-
graph.replaceBatch(CATEGORIES_GRAPH);
85-
}
86-
}}>
87-
<PannableScalableView objectFit='contain' controls>
88-
<DirectedGraphComponent
89-
graph={graph}
90-
settings={{
91-
// TODO - fix orbits strategy padding
92-
placement: {
93-
strategy: 'orbits',
94-
layerSizing: 'equal',
95-
minVertexSpacing: 125
77+
<PannableScalableView objectFit='contain' controls>
78+
<DirectedGraphComponent
79+
graph={graph}
80+
settings={{
81+
// TODO - fix orbits strategy padding
82+
placement: {
83+
strategy: 'circles',
84+
minVertexSpacing: 100
85+
},
86+
components: {
87+
edge: {
88+
type: 'curved'
9689
}
97-
}}
98-
renderers={{
99-
label: DefaultEdgeLabelRenderer
100-
}}
101-
/>
102-
</PannableScalableView>
103-
</GraphEventsProvider>
90+
}
91+
}}
92+
renderers={{
93+
label: DefaultEdgeLabelRenderer
94+
}}
95+
/>
96+
</PannableScalableView>
10497
</View>
10598
</GestureHandlerRootView>
10699
</SafeAreaView>

src/types/settings/placement.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,18 @@ import { Vector } from '@shopify/react-native-skia';
22

33
import { Vertex } from '../graphs';
44

5-
export type PlacementStrategy = 'random' | 'circular' | 'orbits' | 'tree';
5+
export type PlacementStrategy =
6+
| 'random'
7+
| 'circle'
8+
| 'circles'
9+
| 'orbits'
10+
| 'trees';
611

712
export type DirectedGraphPlacementSettings<V, E> =
813
| RandomPlacementSettings
914
| CircularPlacementSettings<V, E>
1015
| OrbitsPlacementSettings
11-
| TreePlacementSettings;
16+
| TreesPlacementSettings;
1217

1318
export type UndirectedGraphPlacementSettings<V, E> =
1419
| RandomPlacementSettings
@@ -78,11 +83,11 @@ export type OrbitsPlacementSettings = (SharedPlacementSettings & {
7883

7984
export type CircularPlacementSettings<V, E> = SharedPlacementSettings &
8085
SortablePlacementSettings<V, E> & {
81-
strategy: 'circular';
86+
strategy: 'circle' | 'circles';
8287
};
8388

84-
export type TreePlacementSettings = SharedPlacementSettings & {
85-
strategy: 'tree';
89+
export type TreesPlacementSettings = SharedPlacementSettings & {
90+
strategy: 'trees';
8691
};
8792

8893
export type PlacedVerticesPositions = Record<string, Vector>;

src/utils/algorithms/graphs.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import Queue from '@/data/Queue';
2+
import { Graph, Vertex } from '@/types/graphs';
3+
4+
const bfs = <V, E>(
5+
graph: Graph<V, E>,
6+
callback: (data: {
7+
vertex: Vertex<V, E>;
8+
parent: Vertex<V, E> | null;
9+
startVertex: Vertex<V, E>;
10+
depth: number;
11+
}) => boolean,
12+
startVertex?: string
13+
): Record<string, Vertex<V, E> | null> => {
14+
const parents: Record<string, Vertex<V, E> | null> = {};
15+
const startVertexNode = startVertex && graph.vertex(startVertex);
16+
const startVertices = startVertexNode ? [startVertexNode] : graph.vertices;
17+
18+
for (const sv of startVertices) {
19+
const queue = new Queue<{
20+
vertex: Vertex<V, E>;
21+
parent: Vertex<V, E> | null;
22+
depth: number;
23+
}>();
24+
queue.enqueue({ vertex: sv, parent: null, depth: 0 });
25+
26+
while (!queue.isEmpty()) {
27+
const { vertex, parent, depth } = queue.dequeue() as {
28+
vertex: Vertex<V, E>;
29+
parent: Vertex<V, E>;
30+
depth: number;
31+
};
32+
if (parents[vertex.key] !== undefined) {
33+
continue;
34+
}
35+
parents[vertex.key] = parent;
36+
if (callback({ vertex, parent, depth, startVertex: sv })) {
37+
return parents;
38+
}
39+
vertex.edges.forEach(edge => {
40+
const nextVertex = edge.vertices.find(v => v.key !== vertex.key);
41+
if (nextVertex) {
42+
queue.enqueue({
43+
vertex: nextVertex,
44+
parent: vertex,
45+
depth: depth + 1
46+
});
47+
}
48+
});
49+
}
50+
}
51+
52+
return parents;
53+
};
54+
55+
export const findGraphComponents = <V, E>(
56+
graph: Graph<V, E>
57+
): Array<Array<Vertex<V, E>>> => {
58+
const components: Record<string, Array<Vertex<V, E>>> = {};
59+
60+
bfs(graph, ({ vertex, startVertex }) => {
61+
if (!components[startVertex.key]) {
62+
components[startVertex.key] = [];
63+
}
64+
components[startVertex.key]!.push(vertex);
65+
return false;
66+
});
67+
68+
return Object.values(components);
69+
};

src/utils/placement/index.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import { DirectedGraph } from '@/models/graphs';
12
import { Graph } from '@/types/graphs';
23
import { GraphLayout, PlacementSettings } from '@/types/settings';
34

4-
import placeVerticesCircular from './strategies/circular.placement';
5+
import { findGraphComponents } from '../algorithms/graphs';
6+
import placeVerticesOnCircle from './strategies/circle.placement';
7+
import placeVerticesOnCircles from './strategies/circles.placement';
58
import placeVerticesOnOrbits from './strategies/orbits.placement';
69
import placeVerticesRandomly from './strategies/random.placement';
710
import placeVerticesOnTree from './strategies/tree.placement';
@@ -12,14 +15,24 @@ export const placeVertices = <V, E>(
1215
settings?: PlacementSettings<V, E>
1316
): GraphLayout => {
1417
switch (settings?.strategy) {
15-
case 'circular':
16-
return placeVerticesCircular(graph, vertexRadius, settings);
18+
case 'circle':
19+
return placeVerticesOnCircle(graph.vertices, vertexRadius, settings);
20+
case 'circles':
21+
return placeVerticesOnCircles(
22+
findGraphComponents(graph),
23+
vertexRadius,
24+
settings
25+
);
1726
case 'orbits':
18-
return placeVerticesOnOrbits(graph, vertexRadius, settings);
19-
case 'tree':
20-
return placeVerticesOnTree(graph, vertexRadius, settings);
27+
return placeVerticesOnOrbits(
28+
graph as DirectedGraph<V, E>, // TODO - change this to take graph components after adding undirected graphs
29+
vertexRadius,
30+
settings
31+
);
32+
case 'trees':
33+
return placeVerticesOnTree(graph, vertexRadius, settings); // TODO - change this to take graph components after adding undirected graphs support
2134
default:
2235
case 'random':
23-
return placeVerticesRandomly(graph, vertexRadius, settings);
36+
return placeVerticesRandomly(graph.vertices, vertexRadius, settings);
2437
}
2538
};

src/utils/placement/shared.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
import potpack from 'potpack';
2+
13
import { Vertex } from '@/types/graphs';
4+
import { GraphLayout } from '@/types/settings/placement';
25

36
export const defaultSortComparator = <V, E>(
47
u: Vertex<V, E>,
@@ -12,3 +15,36 @@ export const defaultSortComparator = <V, E>(
1215
}
1316
return 0;
1417
};
18+
19+
export const arrangeGraphComponents = (
20+
graphComponents: Array<GraphLayout>
21+
): GraphLayout => {
22+
// Prepare graph components for packing
23+
const preparedComponents = graphComponents.map(
24+
({ width: w, height: h, verticesPositions }) => ({
25+
w,
26+
h,
27+
x: 0,
28+
y: 0,
29+
verticesPositions
30+
})
31+
);
32+
// Pack graph components on the screen
33+
const packed = potpack(preparedComponents);
34+
// Translate graph components to correct positions on the screen
35+
return {
36+
width: packed.w,
37+
height: packed.h,
38+
verticesPositions: Object.fromEntries(
39+
preparedComponents.flatMap(({ verticesPositions, x, y, w, h }) =>
40+
Object.entries(verticesPositions).map(([key, { x: vx, y: vy }]) => [
41+
key,
42+
{
43+
x: vx + x + (w - packed.w) / 2,
44+
y: vy + y + (h - packed.h) / 2
45+
}
46+
])
47+
)
48+
)
49+
};
50+
};

0 commit comments

Comments
 (0)