Appearance
Group on zoom
This example shows how to group nodes depending on zoom level. It uses the nodeGrouping transformation to group nodes and the zoom event to detect when the zoom level changes.
ts
import Ogma, { NodeId, Point } from '@linkurious/ogma';
const ogma = new Ogma({
container: 'graph-container',
options: {
interactions: {
zoom: {
minValue: () => 1,
maxValue: () => 10
},
drag: { enabled: false }
}
}
});
type GroupsData = {
nodeToCellIndex: Map<NodeId, number>;
gridSize: number;
minX: number;
minY: number;
widthCell: number;
heightCell: number;
};
let positions: Map<NodeId, Point>;
let groupsData: GroupsData;
function getNodes() {
return ogma.getNodes('all').filter(node => !node.isVirtual());
}
/**
* Compute the grid parameters we'll use to group nodes. we use the positions
* from the initial layout. As once grouped, the positions will be overwritten
*/
function getCellSize() {
const gridSize1 = 2;
const { minX, minY, maxX, maxY } = [...positions.values()].reduce(
(acc, { x, y }) => {
acc.minX = Math.min(acc.minX, x);
acc.minY = Math.min(acc.minY, y);
acc.maxX = Math.max(acc.maxX, x);
acc.maxY = Math.max(acc.maxY, y);
return acc;
},
{ minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity }
);
const width = maxX - minX;
const height = maxY - minY;
const gridSize = Math.floor(
Math.log(ogma.view.getZoom() * 4 + 1) + gridSize1
);
const widthCell = width / (gridSize - 1);
const heightCell = height / (gridSize - 1);
return {
widthCell,
heightCell,
gridSize,
// shift the grid to center nodes perfectly
minY: minY - heightCell / 2,
minX: minX - widthCell / 2,
maxX: maxX - widthCell / 2,
maxY: maxY - heightCell / 2
};
}
/**
* Compute in which cell each node is
*/
function getGroups() {
if (!positions) return;
const nodes = getNodes();
const { widthCell, heightCell, gridSize, minX, minY } = getCellSize();
const nodeToCellIndex = nodes.reduce((map, node, i) => {
const { x, y } = positions.get(node.getId());
map.set(
node.getId(),
Math.floor((y - minY) / heightCell) * gridSize +
Math.floor((x - minX) / widthCell)
);
return map;
}, new Map());
return { nodeToCellIndex, gridSize, minX, minY, widthCell, heightCell };
}
/**
* Setup styles
*/
ogma.styles.addNodeRule({
innerStroke: {
color: '#999'
},
text: {
content: node =>
(groupsData && groupsData.nodeToCellIndex.get(node.getId())) ||
'undefined',
minVisibleSize: 0
},
badges: {
bottomRight: {
stroke: {
color: '#999'
}
}
}
});
ogma.generate
.grid({ rows: 5, columns: 5 })
.then(graph => ogma.setGraph(graph))
.then(() => {
//Store initial positions
const ids = ogma.getNodes().getId();
positions = ogma
.getNodes()
.getPosition()
.reduce((map, pos, i) => {
map.set(ids[i], pos);
return map;
}, new Map());
const data = getGroups();
if (!data) return;
groupsData = data;
// setup the transformation
const transformation = ogma.transformations.addNodeGrouping({
groupIdFunction: node => groupsData.nodeToCellIndex.get(node.getId())!,
nodeGenerator: (nodes, cellIndex) => {
return {
id: 'special group ' + cellIndex + ' ' + ogma.view.getZoom(),
data: {
cellIndex
},
attributes: {
radius: 5 + nodes.size * 2,
text: cellIndex,
badges: {
bottomRight: {
text: nodes.size
}
}
}
};
},
showContents: false,
duration: 0,
enabled: true
});
ogma.transformations.onGroupsUpdated(groups => {
return Promise.resolve(
groups.getData('cellIndex').map((cellIndex, i) => {
const x =
groupsData.minX +
(cellIndex % groupsData.gridSize) * groupsData.widthCell;
const y =
groupsData.minY +
Math.floor(cellIndex / groupsData.gridSize) * groupsData.heightCell;
return { x, y };
})
);
});
// refresh transformation on zoom
ogma.events.on('zoom', () => {
const newGroups = getGroups();
if (newGroups?.gridSize === groupsData.gridSize) return;
groupsData = newGroups;
transformation.refresh();
ogma.styles.getNodeRules().forEach(rule => rule.refresh());
});
});
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 src="index.ts"></script>
</body>
</html>
css
#graph-container {
top: 0;
bottom: 0;
left: 0;
right: 0;
position: absolute;
margin: 0;
overflow: hidden;
}