Skip to content
  1. Examples

Overlays + annotations

This example shows how you can dynamically illustrate the data flow in a neural network. It combines area annotations with zoomable node annotations. You can also click on the nodes to show, hide or fade their overlays.

ts
import Ogma from '@linkurious/ogma';

const ogma = new Ogma({
  container: 'graph-container',
  options: {
    backgroundColor: null
  }
});

await ogma.addGraph({
  nodes: [
    { id: 'source', data: { type: 'source' } },
    {
      id: 'pattern1',
      attributes: { x: 200, y: -300 },
      data: { type: 'pattern' }
    },
    {
      id: 'pattern2',
      attributes: { x: 200, y: -100 },
      data: { type: 'pattern' }
    },
    {
      id: 'pattern3',
      attributes: { x: 200, y: 100 },
      data: { type: 'pattern' }
    },
    {
      id: 'pattern4',
      attributes: { x: 200, y: 300 },
      data: { type: 'pattern' }
    },
    {
      id: 'classifier',
      attributes: { x: 500, y: 0 },
      data: { type: 'classifier' }
    }
  ],
  edges: [
    { source: 'source', target: 'pattern1' },
    { source: 'source', target: 'pattern2' },
    { source: 'source', target: 'pattern3' },
    { source: 'source', target: 'pattern4' },
    { source: 'pattern1', target: 'classifier' },
    { source: 'pattern2', target: 'classifier' },
    { source: 'pattern3', target: 'classifier' },
    { source: 'pattern4', target: 'classifier' }
  ]
});
ogma.styles.addNodeRule({
  color: n => (n.getData('type') === 'source' ? 'red' : '#307700'),
  radius: n => (n.getData('type') === 'pattern' ? 15 : 30),
  icon: { color: 'white' },
  text: n => ({
    content: n.getId(),
    minVisibleSize: 0,
    position: n.getData('type') === 'source' ? 'right' : 'left'
  })
});

const overlays = ogma.getNodes().map((node, i) => {
  const type = node.getData('type');
  const pos = node.getPosition();
  const imageSize = 700;
  const width = 100,
    height = 100;
  const offset = type === 'source' ? -105 : 5;
  const level = type === 'source' ? 0 : 1;

  // additional scaling to fit into the bubble
  const scale = width / imageSize;
  const halfSize = imageSize / 2;

  // calculate pattern position
  let cssPosition =
    level === 0
      ? getQuadrant(imageSize, imageSize, imageSize, imageSize, 0, scale)
      : getQuadrant(imageSize, imageSize, halfSize, halfSize, i - 1, scale);

  // link the overlay with the node for updates
  node.setData('overlayId', i);

  // edge detector picture
  if (type === 'classifier')
    return ogma.layers.addOverlay({
      element: `<div class="image">
        <img src="img/aerial-filtered.jpg" />
      </div>`,
      position: {
        x: pos.x + 10,
        y: pos.y - 150
      },
      size: { width: '300px', height: '300px' }
    });

  return ogma.layers.addOverlay({
    element: `<div class="image">
        <img src="img/aerial.jpg" style="${cssPosition}" />
      </div>`,
    position: { x: pos.x + offset, y: pos.y - height / 2 },
    size: { width: `${width}px`, height: `${height}px` }
  });
});
overlays.forEach(overlay => overlay.moveToBottom());

const zones = ogma.layers.addCanvasLayer(ctx => {
  const src = ogma.getNode('source')!.getPosition();
  const target = ogma.getNode('classifier')!.getPosition();

  ctx.fillStyle = 'rgba(0,120,256, 0.25)';
  ctx.beginPath();
  ctx.rect(src.x - 130, src.y - 350, 210, 700);
  ctx.fill();

  ctx.fillStyle = 'rgba(40, 40, 40, 0.85)';
  ctx.font = '24px Georgia, serif';
  ctx.fillText('Input source', src.x - 120, src.y - 315);

  ctx.fillStyle = 'rgba(0,120,256, 0.25)';
  ctx.beginPath();
  ctx.rect(target.x - 130, target.y - 350, 510, 700);
  ctx.fill();

  ctx.fillStyle = 'rgba(40, 40, 40, 0.85)';
  ctx.font = '24px Georgia, serif';
  ctx.fillText(
    'Edge detector & classification',
    target.x - 120,
    target.y - 315
  );
});
zones.moveToBottom();

// disable node dragging
await ogma.getNodes().setAttribute('draggable', false);

const FADED = 0.3;
const SHOWN = 1;
// show/hide overlays on click
ogma.events.on('click', ({ target }) => {
  if (target && target.isNode) {
    const overlay = overlays[target.getData('overlayId')];
    // show/hide for patterns
    if (target.getData('type') === 'pattern') {
      if (overlay.isHidden()) {
        target.setAttribute('icon.content', null);
        return overlay.show();
      }
      target.setAttribute('icon.content', '+');
      return overlay.hide();
    }
    // fade/reveal for the others
    overlay.setOpacity(overlay.getOpacity() === FADED ? SHOWN : FADED);
  }
});

await ogma.view.locateGraph({
  padding: {
    top: 50,
    left: 100,
    right: 250,
    bottom: 50
  }
});

function getQuadrant(
  imageWidth: number,
  imageHeight: number,
  w: number,
  h: number,
  n: number,
  s: number
) {
  const zoom = w / imageWidth;
  const l = imageWidth / w;
  const col = n >> 1,
    row = n - l * col;

  return `
  position: absolute;
  width: ${imageWidth / zoom}px;
  height: ${imageHeight / zoom}px;
  left: ${(-col * w) / zoom}px;
  top: ${(-row * h) / zoom}px;
  zoom: ${(s / zoom) * zoom};
  `.replace(/\n\s+/g, ''); // should be 1 line
}
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
html,
body {
  margin: 0;
  padding: 0;
}

#graph-container {
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  position: absolute;
  margin: 0;
  overflow: hidden;
}

.image {
  border: 1px solid white;
  overflow: hidden;
  box-shadow: 0 0 10px rgba(0,0,0,0.5)
}