Appearance
Visual grouping expand/collapse new
This example shows how to load nodes asynchronously into visual groups. Double-click on the node to expand or collapse it. It will load its children asynchronously and run a recursive layout inside of the group to remove overlap.
ts
import Ogma from '@linkurious/ogma';
import { init, API } from './api';
import { ND, ED } from './types';
import { colors, greyColors } from './colors';
const ogma = new Ogma<ND, ED>({
container: 'graph-container'
});
ogma.styles.addNodeRule({
color: node => {
const level = node.getData('level');
return node.isVirtual() ? greyColors[level] : colors[level];
}
});
const api: API = await init(ogma);
const subgraph = await api.getSubgraph(0, true);
await ogma.addGraph(subgraph);
await ogma.layouts.hierarchical({ locate: true });
const drilldown = ogma.transformations.addDrillDown({
onGetSubgraph: node => {
const position = node.getPosition();
return api.getSubgraph(node.getId(), true).then(subgraph => {
subgraph.nodes.forEach(
n => (n.attributes = { ...(n.attributes || {}), ...position })
);
return subgraph;
});
},
nodeGenerator: (nodes, id) => ({ id, data: { open: true } }),
showContents: target => target.getData('open'),
onGroupUpdate: (_, nodes) => ogma.layouts.hierarchical({ nodes }),
duration: 250
});
ogma.transformations.onGroupsUpdated(() => ogma.layouts.hierarchical());
ogma.events.on('doubleclick', ({ target }) => {
if (!target || !target.isNode || target.getId() === 0) return;
// the group is already opened
if (ogma.getNode(`ogma-group-${target.getId()}`)) return;
if (target.isVirtual()) {
target.setData('open', !target.getData('open'));
} else {
drilldown.drill(target);
}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
html
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<link type="text/css" rel="stylesheet" href="styles.css" />
</head>
<body>
<div id="graph-container"></div>
<script type="module" src="index.ts"></script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
css
#graph-container {
top: 0;
bottom: 0;
left: 0;
right: 0;
position: absolute;
margin: 0;
overflow: hidden;
}
button {
position: absolute;
top: 10px;
left: 10px;
}
#custom-group-btn {
top: 40px;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ts
import Ogma, { RawNode, RawEdge, RawGraph, NodeId } from '@linkurious/ogma';
import { Awaited } from './types';
let db: {
nodes: RawNode[];
edges: RawEdge[];
nodesMap: Record<NodeId, RawNode>;
};
export const init = (ogma: Ogma, children = 3, levels = 6) =>
ogma.generate
.balancedTree({
children,
height: levels
})
.then(tree => {
const nodesMap = tree.nodes.reduce(
(acc, node) => {
node.attributes = { ...node.attributes, x: 0, y: 0 };
acc[node.id!] = node;
return acc;
},
{} as Record<NodeId, RawNode>
);
tree.edges.forEach(edge => {
const { source, target } = edge;
const sourceNode = nodesMap[source];
const targetNode = nodesMap[target];
sourceNode.data = sourceNode.data || {};
sourceNode.data.children = sourceNode.data.children || [];
targetNode.data = targetNode.data || {};
const sourceLevel = (sourceNode.data.level =
sourceNode.data.level || 0);
const targetLevel = sourceLevel + 1;
sourceNode.data = {
...sourceNode.data,
level: sourceLevel,
children: [...(sourceNode.data.children as NodeId[]), target]
};
targetNode.data = {
...targetNode.data,
level: targetLevel,
parent: source
};
});
db = { ...tree, nodesMap };
return {
getSubgraph(root: NodeId, includeRoot = false): Promise<RawGraph> {
const edges = db.edges.filter(edge => edge.source === root);
const nodes = edges.map(edge => db.nodesMap[edge.target]);
if (includeRoot) nodes.push(db.nodesMap[root]);
return Promise.resolve({ nodes, edges });
},
getGraph() {
return Promise.resolve({ edges: db.edges, nodes: db.nodes });
}
};
});
export type API = Awaited<ReturnType<typeof init>>;
// function createNode(id: NodeId, level: number, parent?: NodeId): RawNode {
// const children = new Array(Math.max(2)).fill(0).map((_, i) => +id * 10 + i);
// return {
// id,
// data: { level, children, parent }
// };
// }
// function retrieveNodes(ids: NodeId[], parent?: NodeId): Promise<RawNode[]> {
// if (ids.length === 0) return Promise.resolve([]);
// const level = Math.floor(Math.log10(+ids[0]));
// return new Promise(resolve => {
// setTimeout(() => {
// const nodes: RawNode[] = ids.map(id => createNode(id, level, parent));
// resolve(nodes);
// }, 10);
// });
// }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
ts
// 10 different colors for layers, shades of blue from darker to lighter
export const colors = [
'#0d47a1',
'#1565c0',
'#1976d2',
'#1e88e5',
'#2196f3',
'#42a5f5',
'#64b5f6',
'#90caf9',
'#bbdefb',
'#e3f2fd'
];
// 10 different shades of grey from #eee to #999
export const greyColors = [
'#eee',
'#e0e0e0',
'#d1d1d1',
'#c2c2c2',
'#b3b3b3',
'#a4a4a4',
'#959595',
'#868686',
'#777777',
'#686868'
];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
ts
import { NodeId } from '@linkurious/ogma';
export interface ND {
level: number;
parent?: NodeId;
children?: NodeId[];
}
export interface ED {
isVirtual?: boolean;
}
export type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T;
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13