Appearance
Visual grouping highlight
Hover a node to see how it's connections are highlighted.
ts
import Ogma, {
Color as ColorOrNull,
NodeId,
Node,
NodeList,
EdgeList
} from '@linkurious/ogma';
import chroma from 'chroma-js';
type Color = NonNullable<ColorOrNull>;
const formatRGBA = (color: [number, number, number, number]) =>
`rgba(${color.join(', ')})`;
const ogma = new Ogma({
container: 'graph-container',
options: {
backgroundColor: '#f5f6f6'
}
});
const names: Record<Color, string> = {
'rgba(51, 153, 255, 1)': 'Arctic, communities',
'rgba(0, 204, 204, 1)': 'Research',
'rgba(153, 255, 255, 1)': 'Population',
'rgba(255, 204, 51, 1)': 'Aerospace',
'rgba(255, 204, 102, 1)': 'Geosciences',
'rgba(255, 255, 51, 1)': 'Physics',
'rgba(102, 0, 102, 1)': 'Petrol',
'rgba(153, 0, 0, 1)': 'Political Studies',
'rgba(102, 102, 0, 1)': 'Pollution',
'rgba(0, 153, 0, 1)': 'Energy resources',
'rgba(153, 255, 0, 1)': 'Biodiversity',
'rgba(0, 204, 51, 1)': 'Global warming'
};
function applyGrouping() {
const transformation = ogma.transformations.addNodeGrouping({
groupIdFunction: node => node.getData('groupId'),
nodeGenerator: (nodes, groupId) => ({
id: 'special group ' + groupId,
data: {
groupId: groupId
},
attributes: {
text: names[groupId] || 'Other',
color: formatRGBA(chroma(groupId).brighten(0.7).rgba()),
opacity: 0.82
}
}),
edgeGenerator: (edges, id) => ({
id: id,
attributes: {
width: 1 + Math.log(edges.size)
}
}),
showContents: metaNode => true,
onGroupUpdate: (_, subNodes, open) => {
if (open) {
return ogma.layouts.force({
nodes: subNodes,
duration: 0
}) as unknown as Promise<void>;
}
}
});
return transformation.whenApplied().then(() => {
ogma.layouts.force({
steps: 150,
charge: 0.125,
gravity: 0.01,
edgeStrength: 1,
theta: 0.9,
locate: true
});
});
}
const graph = await Ogma.parse.gexfFromUrl('files/arctic.gexf');
await ogma.setGraph(graph);
await ogma.view.locateGraph();
const byColor = ogma
.getNodes()
.getAttribute('color')
.reduce(
(acc, color) => {
acc[color as Color] = [];
return acc;
},
{} as Record<Color, NodeId[]>
);
ogma.getNodes().forEach(n => {
byColor[n.getAttribute('color') as Color].push(n.getId());
});
Object.keys(byColor).forEach(color => {
ogma.getNodes(byColor[color]).fillData('groupId', color);
});
await applyGrouping();
ogma.styles.setHoveredNodeAttributes({ outline: false });
ogma.styles.setSelectedNodeAttributes({ outline: false });
ogma.styles.addRule({
nodeAttributes: {
innerStroke: { width: 0.1, scalingMethod: 'scaled', color: '#555' }
},
edgeAttributes: {
color: '#555'
}
});
interface State {
hoveredNode: null | Node;
edgesToHighlight: null | EdgeList;
nodesToHighlight: null | NodeList;
edgesToDim: null | EdgeList;
edgesVisibility: boolean[];
nodesToDim: null | NodeList;
}
const state: State = {
hoveredNode: null,
edgesToHighlight: null,
nodesToHighlight: null,
edgesToDim: null,
edgesVisibility: [],
nodesToDim: null
};
ogma.styles.createClass({
name: 'dimmed',
nodeAttributes: { opacity: 0.15 },
edgeAttributes: { opacity: 0.15 }
});
ogma.styles.createClass({
name: 'highlighted',
nodeAttributes: { color: 'red', radius: n => +n.getAttribute('radius') * 2 },
edgeAttributes: { color: 'red', width: e => +e.getAttribute('width') * 2 }
});
const ANIMATION_DURATION = 250;
ogma.events.on('mouseover', ({ target }) => {
if (!target || !target.isNode) return;
const metaNode = target.getMetaNode();
if (!metaNode) return;
state.hoveredNode = target;
state.edgesToHighlight = target.getAdjacentEdges({ filter: 'all' });
state.edgesVisibility = state.edgesToHighlight.map(e => e.isVisible());
state.nodesToHighlight = ogma.getNodes(
state.edgesToHighlight
.map(edge =>
edge.getSource() === target ? edge.getTarget() : edge.getSource()
)
.filter(node => !node.isVirtual())
.concat(target)
.map(node => node.getId())
);
state.edgesToDim = ogma
.getEdges()
.filter(edge => !state.edgesToHighlight!.filter(e => e === edge).size);
state.nodesToDim = ogma.getNodes(
state.edgesToDim
.filter(
edge => (edge.getTarget() === target) !== (edge.getSource() === target)
)
.map(edge =>
edge.getSource() === target ? edge.getTarget() : edge.getSource()
)
.filter(node => !node.isVirtual() && node !== target)
.map(node => node.getId())
);
state.edgesToHighlight.setVisible(true);
state.edgesToHighlight.addClass('highlighted');
state.nodesToHighlight.addClass('highlighted');
state.nodesToDim.addClass('dimmed', ANIMATION_DURATION);
state.edgesToDim.addClass('dimmed', ANIMATION_DURATION);
});
ogma.events.on('mouseout', () => {
if (!state.hoveredNode) return;
state.nodesToDim!.removeClass('dimmed', ANIMATION_DURATION);
state.edgesToDim!.removeClass('dimmed', ANIMATION_DURATION);
state.edgesToHighlight!.removeClass('highlighted');
state.nodesToHighlight!.removeClass('highlighted');
state.edgesToHighlight!.forEach((e, i) =>
e.setVisible(state.edgesVisibility[i])
);
state.edgesToHighlight = null;
state.hoveredNode = null;
});
html
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<link type="text/css" rel="stylesheet" href="styles.css" />
<link
type="text/css"
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/css/solid.min.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;
}
button {
position: absolute;
top: 10px;
left: 10px;
}
#custom-group-btn {
top: 40px;
}