Appearance
Transformation pipeline
This example illustrates the Ogma transformation pipeline. In Ogma all transformations are executed successively depending on their index. Select a node in the top panel and click to change its index. You will see how the transformation pipeline is executed and how diifferent is the result.
ts
import Ogma, { RawGraph } from '@linkurious/ogma';
import { ui } from './ui';
import { count } from 'd3';
type NodeData = {
type: 'person' | 'company' | 'country';
country?: string;
name: string;
};
const ogma = new Ogma<NodeData>({
container: 'graph-container'
});
const graph: RawGraph<NodeData> = {
nodes: [
{ id: 0, data: { type: 'person', country: 'USA', name: 'Sylvestre' } },
{ id: 1, data: { type: 'person', country: 'UK', name: 'Kevin' } },
{ id: 2, data: { type: 'company', country: 'USA', name: 'World Company' } },
{ id: 3, data: { type: 'company', country: 'UK', name: 'Moon Company' } }
],
edges: [
{ source: 0, target: 2 },
{ source: 1, target: 3 }
]
};
const COLORS = {
person: 'green',
company: 'cyan',
country: 'yellow'
};
ogma.styles.addRule({
nodeAttributes: {
text: node => node.getData('name'),
color: node => COLORS[node.getData('type')],
opacity: node => (node.isVirtual() ? 0.32 : undefined)
}
});
await ogma.setGraph(graph);
await ogma.layouts.force({ locate: true });
const filterNodes = ogma.transformations.addNodeFilter({
criteria: node => node.getData('type') === 'person'
});
const generating = ogma.transformations.addNeighborGeneration({
selector: node => !node.isVirtual(),
neighborIdFunction: node => node.getData('country')!,
nodeGenerator: countryName => {
return { data: { type: 'country', name: countryName } };
}
});
const grouping = ogma.transformations.addNodeGrouping({
groupIdFunction: node => node.getData('type'),
nodeGenerator: nodes => ({
data: {
location: nodes.get(0).getData('type')
},
attributes: {
text: nodes.get(0).getData('type'),
color: 'rgba(127, 127, 127, 0.5)'
}
}),
showContents: true,
onGroupUpdate: (metaNode, nodes) => {
return ogma.layouts.force({ nodes });
}
});
ogma.events.once('transformationEnabled', () => {
ogma.layouts.force({ locate: true });
});
ui.onChange(async data => {
console.log({ data });
const groupIndex = data.find(({ name }) => name === 'grouping')!.index;
const filterIndex = data.find(({ name }) => name === 'filter')!.index;
const generateIndex = data.find(({ name }) => name === 'generating')!.index;
await Promise.all(ogma.transformations.getList().map(t => t.disable()));
await Promise.all([
grouping.setIndex(groupIndex),
filterNodes.setIndex(filterIndex),
generating.setIndex(generateIndex)
]);
await new Promise(resolve => setTimeout(resolve, 500));
await ogma.transformations.getList()[0].enable(1000);
await ogma.layouts.force({ duration: 1000 });
await ogma.transformations.getList()[1].enable(1000);
await ogma.layouts.force({ duration: 1000 });
await ogma.transformations.getList()[2].enable(1000);
await ogma.layouts.force({ duration: 1000 });
});
html
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<link type="text/css" rel="stylesheet" href="styles.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/eventemitter3/4.0.7/index.min.js"></script>
</head>
<body>
<div id="ui-container"></div>
<div id="graph-container"></div>
<script type="module" src="index.ts"></script>
</body>
</html>
css
html,
body {
font-family: 'Inter', sans-serif;
height: 100%;
padding: 0;
margin: 0;
}
:root {
--base-color: #4999f7;
--active-color: var(--base-color);
--gray: #d9d9d9;
--white: #ffffff;
--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);
}
body {
display: grid;
grid-auto-flow: row;
grid-template-rows: 200px auto;
min-height: 100%;
overflow: hidden;
}
#ui-container {
border-bottom: 1px solid black;
}
.context-menu {
display: flex;
justify-content: space-evenly;
padding-top: 1em;
margin-left: 25px;
}
.ui {
position: absolute;
display: flex;
flex-direction: column;
gap: 0.5em;
}
.panel {
background: var(--button-background-color);
border-radius: var(--button-border-radius);
box-shadow: var(--button-shadow);
padding: 10px;
}
.panel {
position: absolute;
top: 20px;
left: 20px;
}
.panel h2 {
text-transform: uppercase;
font-weight: 400;
font-size: 14px;
margin: 0;
}
.panel {
margin-top: 1px;
padding: 5px 10px;
text-align: center;
}
.panel 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 button:hover {
background: var(--lighter-gray);
border: 1px solid var(--darker-gray);
}
.panel button[disabled] {
color: var(--light-gray);
border: 1px solid var(--light-gray);
background-color: var(--lighter-gray);
}
ts
import Ogma, { Overlay, type Node } from '@linkurious/ogma';
export type NodeData = {
index: number;
name: string;
};
class UI {
private onChangeCallbacks: ((data: NodeData[]) => void)[] = [];
onChange(callback: (data: NodeData[]) => void) {
this.onChangeCallbacks.push(callback);
}
change(data: NodeData[]) {
this.onChangeCallbacks.forEach(callback => callback(data));
}
}
export const ui = new UI();
const ogma = new Ogma({
container: 'ui-container',
options: {
minimumHeight: 150,
interactions: {
drag: { enabled: false },
zoom: { enabled: false },
pan: { enabled: false }
}
}
});
ogma.addNodes([
{
id: 'n0',
attributes: { x: 0, text: 'filter' },
data: { index: 0, name: 'filter' }
},
{
id: 'n1',
attributes: { x: 50, text: 'grouping' },
data: { index: 1, name: 'grouping' }
},
{
id: 'n2',
attributes: { x: 100, text: 'generating' },
data: { index: 2, name: 'generating' }
}
]);
ogma.addEdges([
{ source: 'n0', target: 'n1' },
{ source: 'n1', target: 'n2' }
]);
ogma.view.locateGraph({ padding: 80 });
ogma.styles.addEdgeRule(() => true, {
shape: {
head: 'arrow'
}
});
const pulseOptions = {
duration: 2000,
width: 5,
endColor: '#fff',
startColor: '#ccc'
};
function setPos(nodeA: Node<NodeData>, indexB: number) {
ogma.removeEdges(ogma.getEdges());
const indexA = nodeA.getData('index');
const nodeB = ogma
.getNodes()
.filter(node => node.getData('index') === indexB)
.get(0);
nodeA.setData('index', indexB);
nodeB.setData('index', indexA);
ui.change(
ogma
.getNodes()
.getData()
.sort((a, b) => a.index - b.index)
);
return Promise.all([
nodeA.setAttribute('x', nodeB.getAttribute('x'), 500),
nodeB.setAttribute('x', nodeA.getAttribute('x'), 500)
]).then(() => {
const nodes = ogma
.getNodes()
.sort((a, b) => a.getData('index') - b.getData('index'));
ogma.addEdge({
source: nodes.get(0).getId(),
target: nodes.get(1).getId()
});
ogma.addEdge({
source: nodes.get(1).getId(),
target: nodes.get(2).getId()
});
nodes.get(0).pulse(pulseOptions);
setTimeout(() => {
nodes.get(1).pulse(pulseOptions);
}, 2000);
setTimeout(() => {
nodes.get(2).pulse(pulseOptions);
}, 4000);
});
}
function createMenuItem(text: string, action: () => void) {
const item = document.createElement('button');
item.classList.add('item');
item.innerText = text;
item.addEventListener('click', action);
return item;
}
let contextmenu: Overlay | null = null;
function showContextMenu(node: Node<NodeData>) {
destroyContextMenu();
const index = node.getData('index');
const buttons = [];
if (index > 0) {
buttons.push(
createMenuItem('<<', () => {
setPos(node, index - 1);
destroyContextMenu();
})
);
}
if (index < 2) {
buttons.push(
createMenuItem('>>', () => {
setPos(node, index + 1);
destroyContextMenu();
})
);
}
const div = document.createElement('div');
div.classList.add('context-menu');
buttons.forEach(button => {
div.appendChild(button);
});
const position = node.getPosition();
contextmenu = ogma.layers.addOverlay({
element: div,
position: {
x: position.x - 4 * buttons.length,
y: position.y - Number(node.getAttribute('radius')) - 6
},
scaled: false,
size: { width: 50 * buttons.length, height: 'auto' }
});
}
function destroyContextMenu() {
if (!contextmenu) return;
contextmenu.destroy();
contextmenu = null;
}
ogma.events.on('nodesSelected', ({ nodes }) => {
showContextMenu(nodes.get(0));
});
ogma.getNodes().get(1).setSelected(true);