Skip to content
  1. Examples

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