Appearance
Expand/collapse in place new
Double-click group nodes to expand or collapse them.
ts
import Ogma, { Node, NodeList } from '@linkurious/ogma';
import { addStyles } from './styles';
const ogma = new Ogma({
container: 'graph-container'
});
const NodeTypes = [
'red',
'green',
'blue',
'orange',
'purple',
'pink',
'brown',
'grey',
'black'
];
addStyles(ogma);
// generate the graph
const graph = await ogma.generate.random({ nodes: 100, edges: 140 });
graph.nodes.forEach(node => {
node.attributes = node.attributes || {};
node.attributes.radius = 10;
node.data = {
type: NodeTypes[Math.floor(Math.random() * NodeTypes.length)]
};
});
await ogma.setGraph(graph);
await ogma.view.locateGraph();
// add the node grouping
await ogma.transformations
.addNodeGrouping({
groupIdFunction: node => node.getData('type'),
nodeGenerator: (_, groupId) => ({
attributes: {
color: 'white',
innerStroke: groupId
},
data: {
type: groupId,
open: groupId === 'red'
}
}),
padding: 10,
onGroupUpdate(group, nodes) {
if (group.getData('type') === 'red')
return computePositions(group, nodes);
},
showContents: node => node.getData('open')
})
.whenApplied();
await ogma.layouts.force({ locate: true });
/**
* This is how we compute the positions of the subnodes within the group.
* @param target virtual node
* @param subNodes subnodes of the group
* @returns subnodes positions
*/
async function computePositions(target: Node, subNodes: NodeList) {
const targetPosition = target.getPosition();
const origin = target.getPosition();
// circle pack the subnodes
const subNodePositions = await ogma.algorithms.circlePack({
origin,
nodes: subNodes,
margin: 10,
dryRun: true
});
// we want to keep the barycenter of the subnodes at the target position
const centroid = Ogma.geometry.computeCentroid(subNodePositions);
const offsetX = targetPosition.x - centroid.x;
const offsetY = targetPosition.y - centroid.y;
subNodePositions.forEach(p => {
p.x += offsetX;
p.y += offsetY;
});
return subNodePositions;
}
// expand or collapse the group on double click
ogma.events.on('doubleclick', async ({ target }) => {
if (target && target.isNode && target.isVirtual()) {
const isOpen = target.getData('open');
// if the group is open, collapse it, otherwise expand it
if (isOpen)
ogma.transformations.collapseGroup({
node: target,
onCollapse: () => {
target.setData('open', false);
}
});
else
ogma.transformations.expandGroup({
node: target,
computeSubNodesPosition: (target, subNodes) =>
computePositions(target, subNodes),
onExpand: () => {
target.setData('open', true);
}
});
}
});
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>
css
#graph-container {
top: 0;
bottom: 0;
left: 0;
right: 0;
position: absolute;
margin: 0;
overflow: hidden;
}
ts
import Ogma from '@linkurious/ogma';
export const addStyles = (ogma: Ogma) => {
ogma.styles.setSelectedNodeAttributes({
outline: false
});
// color the types
ogma.styles.addRule({
nodeAttributes: {
color: n => (n.isVirtual() ? 'white' : n.getData('type')),
innerStroke: {
minVisibleSize: 0,
width: 2
},
badges: {
topRight: n => {
if (!n.isVirtual() || n.getData('open')) return;
const color = n.getData('type');
return {
text: {
content: n.getSubNodes()?.size,
color: 'white'
},
color,
stroke: {
width: 0
}
};
}
}
},
edgeAttributes: {
width: 1,
detectable: false,
color: '#ccc'
}
});
};