Appearance
Konva.js integration
This example shows integration with Konva.js Canvas rendering library. With the level of abstraction it offers, we cannot use it like d3 for low-level rendering with our CanvasLayer
interface, but we still can add its canvas as an overlay to Ogma and render in the same coordiante system.
ts
import Ogma, { Layer, View } from '@linkurious/ogma';
import { GUI } from 'dat.gui';
import Konva from 'konva';
const ogma = new Ogma({
container: 'graph-container'
});
/**
* @typedef OgmaKonvaLayer
* @property stage Konva.js container. You can rander inside this
* stage using Ogma coordinate system
* @property destroy Destroy wrapper: removes the Konva.js layer
* from Ogma and stops the view synchronisation
*/
interface OgmaKonvaLayer extends Layer {
stage: Konva.Stage;
destroy: () => this;
}
/**
* Adds a Konva.js layer to Ogma that is synchronised with the
* camera position and movement
*/
function addKonvajsLayer(ogma: Ogma): OgmaKonvaLayer {
const { width, height } = ogma.view.get();
// we create an empty container for Konva.js
const container = document.createElement('div');
const stage = new Konva.Stage({
container,
draggable: false,
width: width,
height: height,
offsetX: -width / 2,
offsetY: -height / 2
});
const ogmaLayer = ogma.layers.addLayer(stage.getContainer());
const synchronizeViews = () => {
console.log('synchronizeViews');
const { x, y, zoom, width, height } = ogma.view.get();
stage.position({ x: -x * zoom, y: -y * zoom });
stage.scale({ x: zoom, y: zoom });
stage.offset({ x: -width / 2 / zoom, y: -height / 2 / zoom });
stage.draw();
};
ogma.events
.on('viewExportStart', ({ view }) => {
const { x, y, zoom, width, height } = view;
stage.position({ x: -x! * zoom!, y: -y! * zoom! });
stage.scale({ x: zoom, y: zoom });
stage.offset({ x: -width! / 2 / zoom!, y: -height! / 2 / zoom! });
})
.on('dragProgress', () => update(layer, ogma))
.on('frame', synchronizeViews);
return {
...ogmaLayer,
stage,
destroy() {
ogma.events.off(synchronizeViews);
ogmaLayer.destroy();
return this;
}
} as OgmaKonvaLayer;
}
// add Konva.js integration layer
const layer = addKonvajsLayer(ogma);
/**
* Renders additional markup with Konva.js
* @param {OgmaKonvaLayer} layer
* @param {Ogma} ogma
*/
function render(layer: OgmaKonvaLayer, ogma: Ogma) {
const group = new Konva.Layer();
layer.stage.add(group);
// randomly placed circle
const circle = new Konva.Circle({
x: 0,
y: 0,
radius: 15,
opacity: 0.25,
fill: 'green'
});
group.add(circle);
// a little triangle mark at each node,
// offset by a small margin
ogma.getNodes().forEach(() => {
const triangle = new Konva.RegularPolygon({
x: 0,
y: 0,
radius: 3,
sides: 3,
fill: 'red',
stroke: 'black',
strokeWidth: 1
});
group.add(triangle);
});
}
function writeUrlToCanvas(url: string, canvas: HTMLCanvasElement) {
return new Promise<void>(resolve => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
const ctx = canvas.getContext('2d')!;
ctx.drawImage(img, 0, 0);
resolve();
};
img.src = url;
});
}
async function exportPNG(options = {}) {
let konvasUrl: string;
let width: number, height: number;
ogma.events.once('viewExportStart', ({ view }) => {
konvasUrl = layer.stage.toDataURL({ mimeType: 'image/png' });
width = view.width!;
height = view.height!;
});
const ogmaURL = await ogma.export.png({ ...options, download: false });
if (!konvasUrl!) return;
const canvas = document.createElement('canvas');
canvas.width = width!;
canvas.height = height!;
await writeUrlToCanvas(ogmaURL, canvas);
await writeUrlToCanvas(konvasUrl, canvas);
document.body.appendChild(canvas);
return canvas;
}
function downloadCanvas(canvas: HTMLCanvasElement) {
const link = document.createElement('a');
link.download = 'ogma-export-konvas.png';
link.href = canvas.toDataURL();
link.click();
}
/**
* Updates the shapes created with Konva.js
*/
function update(layer: OgmaKonvaLayer, ogma: Ogma) {
const group = layer.stage.children[0];
const circle = group.children[0];
const triangles = group.children.slice(1);
const firstNode = ogma.getNodes().get(0).getPosition();
circle.position(firstNode);
ogma
.getNodes()
.getAttributes(['x', 'y', 'radius'])
.forEach(({ x, y, radius }, i) => {
triangles[i].position({ x: x + +radius / 2, y: y + +radius / 2 });
});
}
// The UI
const params = {
over: true,
show: () => layer.show(),
hide: () => layer.hide(),
runLayout: async () => {
layer.hide();
await ogma.layouts.force({ locate: true });
update(layer, ogma);
layer.show();
},
exportPNG: async () => {
const canvas = await exportPNG({ width: 256, height: 256 });
await downloadCanvas(canvas!);
},
removeLayer: () => layer.destroy()
};
const gui = new GUI({ name: 'Konva.js integration' });
gui
.add(params, 'over')
.name('Over/under')
.onChange(() => {
if (params.over) layer.moveToTop();
else layer.moveToBottom();
});
gui.add(params, 'show').name('Show layer');
gui.add(params, 'hide').name('Hide layer');
gui.add(params, 'runLayout').name('Run layout');
gui.add(params, 'exportPNG').name('Export PNG');
gui.add(params, 'removeLayer').name('Remove layer');
const graph = await ogma.generate.grid({ rows: 4, columns: 4 });
await ogma.setGraph(graph);
await ogma.view.locateGraph();
// use Konva group layer
render(layer, ogma);
update(layer, ogma);
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 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;
}