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;
}