Appearance
All styles
This example provides a tweak panel to edit all NodeAttributes and EdgeAttributes availiable in Ogma. So you can see what they do and how they affect the style of your graph.
ts
import Ogma, {
Badge,
Theme,
NodeAttributes,
Icon,
NodeImage
} from '@linkurious/ogma';
import { theme, ICONS, SHAPES } from './theme';
import { ThemeWithSwitches } from './types';
import { GUI } from '@linkurious/ogma-ui-kit/gui';
// Ogma instance
const ogma = new Ogma({ container: 'graph-container' });
// dummy icon element to retrieve the HEX code
const placeholder = document.getElementById('icon-placeholder')!;
placeholder.style.visibility = 'hidden';
const graph = await ogma.generate.barabasiAlbert({
nodes: 20,
scale: 40
});
await ogma.setGraph(graph);
await ogma.layouts.force({ locate: true, duration: 0 });
ogma.styles.setTheme(cleanup(theme));
const gui = new GUI({ title: 'Attributes' });
const nodeAttributesFolder = gui.addFolder('Node Attributes');
nodeAttributesFolder.addColor(theme.nodeAttributes, 'color');
nodeAttributesFolder.add(theme.nodeAttributes, 'radius', 1, 100).step(1);
nodeAttributesFolder.add(theme.nodeAttributes!, 'shape', SHAPES);
nodeAttributesFolder.add(theme.nodeAttributes, 'opacity', 0, 1).step(0.01);
nodeAttributesFolder.add(theme.nodeAttributes, 'draggable');
nodeAttributesFolder.add(theme.nodeAttributes, 'detectable');
const outerStroke = nodeAttributesFolder.addFolder('Outer Stroke');
outerStroke.add(theme.nodeAttributes!.outerStroke, 'width', 0, 50).step(0.1);
outerStroke.addColor(theme.nodeAttributes!.outerStroke, 'color');
outerStroke.add(theme.nodeAttributes!.outerStroke, 'scalingMethod', [
'fixed',
'scaled'
]);
outerStroke
.add(theme.nodeAttributes!.outerStroke, 'minVisibleSize', 0, 100)
.step(1);
const innerStroke = nodeAttributesFolder.addFolder('Inner Stroke');
innerStroke.add(theme.nodeAttributes!.innerStroke, 'width', 0, 50).step(0.1);
innerStroke.addColor(theme.nodeAttributes!.innerStroke, 'color');
innerStroke.add(theme.nodeAttributes!.innerStroke, 'scalingMethod', [
'fixed',
'scaled'
]);
innerStroke
.add(theme.nodeAttributes!.innerStroke, 'minVisibleSize', 0, 100)
.step(1);
const halo = nodeAttributesFolder.addFolder('Halo');
halo.add(theme.nodeAttributes!.halo, 'width', 0, 50).step(0.1);
halo.addColor(theme.nodeAttributes!.halo, 'color');
halo.add(theme.nodeAttributes!.halo, 'scalingMethod', ['fixed', 'scaled']);
halo.add(theme.nodeAttributes!.halo, 'strokeColor');
halo.add(theme.nodeAttributes!.halo, 'strokeWidth', 0, 50).step(0.1);
halo.add(theme.nodeAttributes!.halo, 'hideNonAdjacentEdges');
halo.add(theme.nodeAttributes!.halo, 'size', 0, 50).step(0.1);
const badges = nodeAttributesFolder.addFolder('Badges').close();
const icon = nodeAttributesFolder.addFolder('Icon');
icon.add(theme.nodeAttributes!.icon, 'scale', 0, 1).step(0.01);
icon.addColor(theme.nodeAttributes!.icon, 'color');
icon.add(theme.nodeAttributes!.icon, 'minVisibleSize', 0, 100).step(1);
icon.add(theme.nodeAttributes!.icon, 'content', ICONS);
const image = nodeAttributesFolder.addFolder('Image');
image.add(theme.nodeAttributes!.image, 'url', ['flags/nz.svg', 'none']);
image.add(theme.nodeAttributes!.image, 'scale', 0, 10).step(0.1);
createBadgeControls(theme, badges);
const outline = nodeAttributesFolder.addFolder('Outline');
outline.add(theme.nodeAttributes!.outline, 'enabled');
outline.addColor(theme.nodeAttributes!.outline, 'color');
outline.add(theme.nodeAttributes!.outline, 'minVisibleSize', 0, 100).step(1);
// ===== Edge attributes
const edgeAttributesFolder = gui.addFolder('Edge Attributes');
edgeAttributesFolder.addColor(theme.edgeAttributes, 'color');
edgeAttributesFolder.add(theme.edgeAttributes, 'width', 1, 50).step(1);
edgeAttributesFolder.add(theme.edgeAttributes, 'opacity', 0, 1).step(0.01);
edgeAttributesFolder.add(theme.edgeAttributes, 'detectable');
const haloEdgeAttributes = edgeAttributesFolder.addFolder('Halo');
haloEdgeAttributes.add(theme.edgeAttributes!.halo, 'width', 0, 50).step(0.1);
haloEdgeAttributes.addColor(theme.edgeAttributes!.halo, 'color');
haloEdgeAttributes.add(theme.edgeAttributes!.halo, 'scalingMethod', [
'fixed',
'scaled'
]);
const outlineEdgeAttributes = edgeAttributesFolder.addFolder('Outline');
outlineEdgeAttributes.add(theme.edgeAttributes!.outline, 'enabled');
outlineEdgeAttributes.addColor(theme.edgeAttributes!.outline, 'color');
outlineEdgeAttributes
.add(theme.edgeAttributes!.outline, 'minVisibleSize', 0, 100)
.step(1);
gui.onChange(() => {
ogma.styles.setTheme(cleanup(theme));
});
const actions = {
export: async () => {
const dataStr =
'data:text/json;charset=utf-8,' +
encodeURIComponent(JSON.stringify(theme));
const downloadLink = document.createElement('a');
downloadLink.setAttribute('href', dataStr);
downloadLink.setAttribute(
'download',
`styles ${new Date().toLocaleString()}.json`.replace(/\s/g, '_')
);
downloadLink.style.display = 'none';
document.body.appendChild(downloadLink);
downloadLink.click();
setTimeout(() => {
document.body.removeChild(downloadLink);
}, 500);
},
import: (evt: Event) => {
const fileInput = evt.target as HTMLInputElement;
const file = fileInput.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = e => {
try {
const importedTheme = JSON.parse(e.target!.result as string);
ogma.styles.setTheme(cleanup(importedTheme));
} catch (err) {
console.error('Cannot import the file. Reason:', err);
}
};
reader.readAsText(file);
fileInput.value = ''; // Reset the input so we can load the same file twice
}
};
gui.add(actions, 'export').name('Export styles');
gui
.add(actions, 'import')
.name('Import styles')
.onChange(() => {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
fileInput.click();
});
function cleanup(inputTheme: ThemeWithSwitches): Theme {
const theme = JSON.parse(JSON.stringify(inputTheme)) as ThemeWithSwitches;
// Remove badges that are not enabled
if (theme.nodeAttributes.badges) {
Object.entries(theme.nodeAttributes.badges).forEach(([key, badge]) => {
if (badge.enabled === false) {
badge.scale = 0;
badge.stroke!.width = 0;
} else {
delete badge.enabled; // Remove the enabled property
}
});
}
(theme.nodeAttributes.icon! as Icon).content = getIconCode(
((theme.nodeAttributes.icon! as Icon).content as string) || ICONS[0]
);
if ((theme.nodeAttributes.image as NodeImage).url === 'none')
(theme.nodeAttributes.image as NodeImage).url =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR42mP8//8/AwAI/wH+9Q4AAAAASUVORK5CYII=';
return theme as Theme;
}
function createBadgeControls(theme: ThemeWithSwitches, root: GUI.Folder): void {
['topLeft', 'topRight', 'bottomLeft', 'bottomRight'].forEach(position => {
const badge = theme.nodeAttributes?.badges?.[
position as keyof NodeAttributes['badges']
]! as BadgeWithSwitch;
const folder = root.addFolder(position);
folder.add(badge, 'enabled').name('Enabled');
folder.add(badge?.text, 'content').name('text.content');
folder.addColor(badge?.text, 'color').name('text.color');
folder.add(badge?.text, 'scale', 0, 1).step(0.01).name('text.scale');
folder.add(badge, 'scale', 0, 1).step(0.01);
folder.addColor(badge, 'color').name('color');
folder.add(badge, 'scale', 0, 1).step(0.01);
folder.add(badge, 'minVisibleSize', 0, 100).step(1);
folder.add(badge.stroke, 'width', 0, 100).step(1).name('stroke.width');
folder.addColor(badge.stroke, 'color').name('stroke.color');
folder
.add(badge.stroke, 'scalingMethod', ['scaled', 'fixed'])
.name('stroke.scalingMethod');
folder.close();
});
}
// helper routine to get the icon HEX code
function getIconCode(icon: string) {
placeholder.className = `icon-${icon}`;
const content = getComputedStyle(placeholder, ':before').content;
return content[1];
}
html
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<link
href="https://cdn.jsdelivr.net/npm/lucide-static@0.483.0/font/lucide.css"
rel="stylesheet"
/>
<link type="text/css" rel="stylesheet" href="styles.css" />
</head>
<body>
<div id="graph-container"></div>
<span id="icon-placeholder"></span>
<script src="index.ts"></script>
</body>
</html>
css
html {
margin: 0;
overflow: hidden;
}
#graph-container {
top: 0;
bottom: 0;
left: 0;
right: 0;
position: absolute;
margin: 0;
overflow: hidden;
}
#gui-container {
overflow: auto;
width: 400px;
height: 100vh;
position: absolute;
top: 0px;
right: 0;
}
.menu {
border: 1px solid #ddd;
width: 80%;
font-size: 14px;
}
ts
import { ThemeWithSwitches } from './types';
const fontName = 'lucide';
export const ICONS = [
'shield',
'chevron-down',
'folder',
'target',
'bug',
'martini',
'star',
'glasses',
'cloud',
'arrow-left',
'user',
'message-square',
'play',
'hammer'
];
export const theme: ThemeWithSwitches = {
nodeAttributes: {
color: '#145DA0',
radius: 5,
opacity: 1,
draggable: true,
detectable: true,
outerStroke: {
width: 1,
color: '#2E8BC0',
scalingMethod: 'fixed',
minVisibleSize: 1
},
innerStroke: {
width: 1,
color: '#ffffff',
scalingMethod: 'fixed',
minVisibleSize: 1
},
halo: {
width: 20,
color: '#dedede',
scalingMethod: 'fixed',
strokeColor: '#bbb',
strokeWidth: 1,
hideNonAdjacentEdges: true,
size: 5
},
badges: {
topLeft: {
enabled: true,
text: {
content: 'Tl',
color: '#ffffff',
scale: 0.6
},
stroke: {
width: 0.3,
color: '#ffffff',
scalingMethod: 'scaled'
},
scale: 0.5,
color: '#145DA0',
minVisibleSize: 5
},
topRight: {
enabled: false,
text: {
content: 'Tr',
color: '#ffffff',
scale: 0.6
},
stroke: {
width: 0.3,
color: '#ffffff',
scalingMethod: 'scaled'
},
scale: 0,
color: '#145DA0',
minVisibleSize: 5
},
bottomLeft: {
enabled: false,
text: {
content: 'Bl',
color: '#ffffff',
scale: 0.6
},
stroke: {
width: 0.3,
color: '#ffffff',
scalingMethod: 'scaled'
},
scale: 0,
color: '#145DA0',
minVisibleSize: 5
},
bottomRight: {
enabled: false,
text: {
content: 'Br',
color: '#ffffff',
scale: 0.6
},
stroke: {
width: 0.3,
color: '#ffffff',
scalingMethod: 'scaled'
},
scale: 0.5,
color: '#DA5DA0',
minVisibleSize: 5
}
},
image: {
url: 'flags/nz.svg',
scale: 2.5
},
icon: {
scale: 0.35,
color: '#ffffff',
minVisibleSize: 5,
font: fontName,
content: ICONS[0]
},
outline: {
enabled: false,
color: '#ffffff',
minVisibleSize: 1
},
shape: 'circle'
},
edgeAttributes: {
color: '#145DA0',
width: 1,
opacity: 1,
detectable: true,
halo: {
width: 0,
color: '#dedede',
scalingMethod: 'fixed'
},
outline: {
enabled: false,
color: '#ffffff',
minVisibleSize: 1
}
},
hoveredEdgeAttributes: { color: '#2E8BC0', width: 2, opacity: 1 },
hoveredNodeAttributes: { color: '#2E8BC0', radius: 6, opacity: 1 },
selectedEdgeAttributes: { color: '#0C2D48', width: 3, opacity: 1 },
selectedNodeAttributes: { color: '#0C2D48', radius: 6, opacity: 1 }
};
export const SHAPES = [
'none',
'circle',
'square',
'cross',
'diamond',
'star',
'equilateral',
'triangle',
'triangleDown',
'triangleRight',
'triangleLeft',
'hexagon'
];
ts
import { Badge, NodeAttributes, Theme } from '@linkurious/ogma';
export interface BadgeWithSwitch extends Badge {
enabled?: boolean;
}
export interface ThemeWithSwitches extends Theme {
nodeAttributes: NodeAttributes & {
badges?: {
bottomLeft?: BadgeWithSwitch;
bottomRight?: BadgeWithSwitch;
topRight?: BadgeWithSwitch;
topLeft?: BadgeWithSwitch;
};
};
}