Appearance
Visual grouping
This example shows how to use node grouping to group nodes and show their content. Double click on a group to open/close it.
ts
import Ogma, { NodeGrouping } from '@linkurious/ogma';
const FLAGS = {
France: 'flags/fr.svg',
Japan: 'flags/jp.svg',
USA: 'flags/us.svg',
Brazil: 'flags/br.svg'
};
type Country = keyof typeof FLAGS;
interface NodeData {
location: Country;
open: boolean;
}
const backgroundColor = '#F5F6F6';
const ogma = new Ogma<NodeData>({
container: 'graph-container',
options: {
backgroundColor
}
});
// animation duration for the layout
const duration = 250;
Ogma.parse
.jsonFromUrl<NodeData>('countries.json')
.then(graph => ogma.addGraph(graph))
.then(() => ogma.layouts.force({ locate: true, duration }));
const selectedColor = '#04ddcb';
ogma.styles.setSelectedNodeAttributes({
outerStroke: { color: selectedColor }
});
ogma.styles.setHoveredNodeAttributes({
outerStroke: { color: selectedColor }
});
ogma.styles.setHoveredEdgeAttributes({
color: selectedColor
});
ogma.styles.setSelectedEdgeAttributes({
color: selectedColor
});
ogma.styles.setHoveredNodeAttributes({ outline: undefined });
ogma.styles.setSelectedNodeAttributes({ outline: undefined });
ogma.styles.addNodeRule({
innerStroke: { color: '#555', width: 1 },
radius: 10,
image: {
url: node => FLAGS[node.getData('location')],
minVisibleSize: 0
},
opacity: node => (node.getData('open') ? 0.32 : undefined),
badges: {
bottomRight: { stroke: { color: '#999' } }
}
});
const byCountry = ogma.transformations.addNodeGrouping({
groupIdFunction: node => node.getData('location'),
nodeGenerator: (nodes, country) => {
const isVisible = country !== 'Japan';
return {
id: 'special group ' + country,
data: {
country: country,
open: isVisible
},
attributes: {
text: country,
// fixed radius for the nodes with hidden contents
radius: 20,
image: FLAGS[country as Country]
}
};
},
showContents: node => node.getData('open'),
onGroupUpdate: (metaNode, subNodes, visible) => {
const groupId = metaNode.getId();
if (!layoutPerGroup.has(groupId)) {
layoutPerGroup.set(groupId, defaultLayout);
}
if (!visible) return Promise.resolve();
if (layoutPerGroup.get(groupId) === 'hierarchical') {
return ogma.layouts.hierarchical({
nodes: subNodes,
duration: 0
}) as unknown as Promise<void>;
}
return ogma.layouts.force({
nodes: subNodes,
duration: 0
}) as unknown as Promise<void>;
},
duration: 300,
enabled: false
});
const layoutPerGroup = new Map();
const defaultLayout = 'force';
const customGroup = ogma.transformations.addNodeGrouping({
groupIdFunction: node => {
const id = node.getData('country');
if (id === 'France' || id === 'Brazil') return 'Level 2';
return undefined;
},
nodeGenerator: (nodes, country) => ({
id: 'special group ' + country,
data: {
country: country,
open: true
},
attributes: {
color: backgroundColor,
text: country,
opacity: 0.32
}
}),
onGroupUpdate: (metaNode, subNodes, visible) => {
return ogma.layouts.force({
nodes: subNodes,
duration: 0
}) as unknown as Promise<void>;
},
showContents: node => node.getData('open'),
enabled: false,
duration: 300
});
// UI buttons
const buttonFranceBrazilOnly = document.getElementById(
'custom-group-btn'
) as HTMLButtonElement;
const buttonSwitchLayout = document.getElementById(
'switch-layout'
)! as HTMLButtonElement;
const buttonGroupAll = document.getElementById('group-btn')!;
buttonFranceBrazilOnly.disabled = true;
document.getElementById('group-btn')!.addEventListener('click', () => {
// Toggle the grouping transformation
byCountry.toggle().then(() => {
const buttonText = byCountry!.isEnabled()
? 'Ungroup by country'
: 'Group by country';
buttonGroupAll.textContent = buttonText;
buttonFranceBrazilOnly.disabled = !byCountry!.isEnabled();
// return ogma.layouts.force({ duration });
});
});
document.getElementById('custom-group-btn')!.addEventListener('click', () => {
// Toggle the grouping
customGroup.toggle().then(() => {
const buttonText = customGroup!.isEnabled()
? 'Ungroup France & Brazil'
: 'Group France & Brazil';
buttonFranceBrazilOnly.textContent = buttonText;
});
});
buttonSwitchLayout.disabled = true;
buttonSwitchLayout.addEventListener('click', () => {
const groups = byCountry.getContext().metaNodes;
// setup the layout for each group
groups.getId().forEach(id => {
layoutPerGroup.set(id, 'hierarchical');
});
// tell transformations to rerun the group update callback
ogma.transformations.triggerGroupUpdated(groups);
// trigger the refresh
byCountry.refresh();
});
ogma.events
.on('click', evt => {
const target = evt.target;
if (target && target.isNode && target.isVirtual()) {
console.log('sub nodes', target.getSubNodes()!.getId());
}
})
.on('doubleclick', evt => {
const target = evt.target;
if (target && target.isNode && target.isVirtual()) {
target.setData('open', !target.getData('open'));
}
});
ogma.transformations.onGroupsUpdated(() => {
return ogma.layouts.force({ duration });
});
ogma.events.on('transformationDisabled', () => {
buttonSwitchLayout.disabled = !ogma.transformations
.getList()
.some(e => e.isEnabled());
return ogma.layouts.force({ duration });
});
ogma.events.on('transformationEnabled', () => {
buttonSwitchLayout.disabled = false;
});
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>
<div class="panel">
<div class="section">
<button id="group-btn">Group by country</button>
</div>
<div class="section">
<button id="custom-group-btn">Group France & Brazil</button>
</div>
<div class="section">
<button id="switch-layout">Run Hierarchical layout</button>
</div>
</div>
<script type="module" src="index.ts"></script>
</body>
</html>
css
html,
body {
font-family: 'Inter', sans-serif;
}
:root {
--base-color: #4999f7;
--active-color: var(--base-color);
--gray: #d9d9d9;
--lighter-gray: #f4f4f4;
--light-gray: #e6e6e6;
--inactive-color: #cee5ff;
--group-color: #525fe1;
--group-inactive-color: #c2c8ff;
--selection-color: #04ddcb;
--darker-gray: #b6b6b6;
--dark-gray: #555;
--dark-color: #3a3535;
--edge-color: var(--dark-color);
--border-radius: 5px;
--button-border-radius: var(--border-radius);
--edge-inactive-color: var(--light-gray);
--button-background-color: #ffffff;
--shadow-color: rgba(0, 0, 0, 0.25);
--shadow-hover-color: rgba(0, 0, 0, 0.5);
--button-shadow: 0 0 4px var(--shadow-color);
--button-shadow-hover: 0 0 4px var(--shadow-hover-color);
--button-icon-color: #000000;
--button-icon-hover-color: var(--active-color);
}
#graph-container {
top: 0;
bottom: 0;
left: 0;
right: 0;
position: absolute;
margin: 0;
overflow: hidden;
}
.ui {
position: absolute;
display: flex;
flex-direction: column;
gap: 0.5em;
}
#custom-group-btn {
top: 40px;
}
.panel {
position: absolute;
top: 20px;
left: 20px;
background: var(--button-background-color);
border-radius: var(--button-border-radius);
box-shadow: var(--button-shadow);
padding: 10px;
}
.panel h2 {
text-transform: uppercase;
font-weight: 400;
font-size: 14px;
margin: 0;
}
.panel .section {
margin-top: 1px;
padding: 5px 10px;
text-align: center;
}
.panel .section button {
background: var(--button-background-color);
border: none;
border-radius: var(--button-border-radius);
border-color: var(--shadow-color);
padding: 5px 10px;
cursor: pointer;
width: 100%;
color: var(--dark-gray);
border: 1px solid var(--light-gray);
}
.panel .section button:hover {
background: var(--lighter-gray);
border: 1px solid var(--darker-gray);
}
.panel .section button[disabled] {
color: var(--light-gray);
border: 1px solid var(--light-gray);
background-color: var(--lighter-gray);
}
json
{
"nodes": [
{
"id": 0,
"attributes": {
"x": -58.08613537669512,
"y": -42.17457357539305,
"color": "grey",
"radius": 5,
"shape": "circle",
"text": null
},
"data": {
"location": "France"
}
},
{
"id": 1,
"attributes": {
"x": -13.713836084599476,
"y": -102.43032443249912,
"color": "grey",
"radius": 5,
"shape": "circle",
"text": null
},
"data": {
"location": "France"
}
},
{
"id": 2,
"attributes": {
"x": 78.18022166331382,
"y": -78.83085224468655,
"color": "grey",
"radius": 5,
"shape": "circle",
"text": null
},
"data": {
"location": "France"
}
},
{
"id": 3,
"attributes": {
"x": -80.04579597181308,
"y": -92.80349816780331,
"color": "grey",
"radius": 5,
"shape": "circle",
"text": null
},
"data": {
"location": "France"
}
},
{
"id": 4,
"attributes": {
"x": 26.866002889626042,
"y": -142.31666618081036,
"color": "grey",
"radius": 5,
"shape": "circle",
"text": null
},
"data": {
"location": "France"
}
},
{
"id": 10,
"attributes": {
"x": 86.6948842918209,
"y": 7.607028981568861,
"color": "grey",
"radius": 5,
"shape": "circle",
"text": null
},
"data": {
"location": "USA"
}
},
{
"id": 11,
"attributes": {
"x": 55.14918974638689,
"y": 77.21612384739898,
"color": "grey",
"radius": 5,
"shape": "circle",
"text": null
},
"data": {
"location": "USA"
}
},
{
"id": 12,
"attributes": {
"x": 15.939748427451477,
"y": 121.42557416014343,
"color": "grey",
"radius": 5,
"shape": "circle",
"text": null
},
"data": {
"location": "USA"
}
},
{
"id": 13,
"attributes": {
"x": 100.35233210155475,
"y": 104.92327128191509,
"color": "grey",
"radius": 5,
"shape": "circle",
"text": null
},
"data": {
"location": "USA"
}
},
{
"id": 14,
"attributes": {
"x": 119.38828745374323,
"y": 43.29808488050811,
"color": "grey",
"radius": 5,
"shape": "circle",
"text": null
},
"data": {
"location": "USA"
}
},
{
"id": 20,
"attributes": {
"x": 16.877716464648596,
"y": 29.54224307483783,
"color": "grey",
"radius": 5,
"shape": "circle",
"text": null
},
"data": {
"location": "Japan"
}
},
{
"id": 21,
"attributes": {
"x": 43.739538316030774,
"y": -25.700465065025504,
"color": "grey",
"radius": 5,
"shape": "circle",
"text": null
},
"data": {
"location": "Japan"
}
},
{
"id": 22,
"attributes": {
"x": -9.497046215814374,
"y": 4.685025820713196,
"color": "grey",
"radius": 5,
"shape": "circle",
"text": null
},
"data": {
"location": "Japan"
}
},
{
"id": 23,
"attributes": {
"x": -12.597386365123791,
"y": -39.355838537079244,
"color": "grey",
"radius": 5,
"shape": "circle",
"text": null
},
"data": {
"location": "Japan"
}
},
{
"id": 24,
"attributes": {
"x": 123.01811984577611,
"y": -28.078297607827192,
"color": "grey",
"radius": 5,
"shape": "circle",
"text": null
},
"data": {
"location": "Japan"
}
},
{
"id": 30,
"attributes": {
"x": -122.81169804344762,
"y": 40.91164279174578,
"color": "grey",
"radius": 5,
"shape": "circle",
"text": null
},
"data": {
"location": "Brazil"
}
},
{
"id": 31,
"attributes": {
"x": -71.9453110279818,
"y": 29.53969162232996,
"color": "grey",
"radius": 5,
"shape": "circle",
"text": null
},
"data": {
"location": "Brazil"
}
},
{
"id": 32,
"attributes": {
"x": -106.86025795760797,
"y": -30.401856621318228,
"color": "grey",
"radius": 5,
"shape": "circle",
"text": null
},
"data": {
"location": "Brazil"
}
},
{
"id": 33,
"attributes": {
"x": -18.280941662091053,
"y": 78.21279781316291,
"color": "grey",
"radius": 5,
"shape": "circle",
"text": null
},
...