Skip to content
  1. Examples

Fish-eye

Fisheye plugin is used to have a quick look at the details of the graph without losing the context of the whole structure. It is very useful for large graphs.

ts
import Ogma from '@linkurious/ogma';
import { FishEye } from './fish-eye';
// Create an instance of Ogma and bind it to the graph-container.
const ogma = new Ogma({
  container: 'graph-container'
});

ogma.styles.addRule({
  nodeAttributes: {
    radius: n => +n?.getAttribute('radius') / 2,
    innerStroke: {
      color: '#222',
      width: 0.5,
      minVisibleSize: 2,
      scalingMethod: 'scaled'
    }
  },
  edgeAttributes: {
    width: e => +e?.getAttribute('width') / 4
  }
});

const fishEye = new FishEye(ogma);

const graph = await Ogma.parse.gexfFromUrl('files/arctic.gexf');
await ogma.setGraph(graph);
fishEye.enable({
  radius: 150,
  strokeWidth: 4,
  k: 2,
  strokeColor: '#445'
});
html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <link
      href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/css/all.min.css"
      rel="stylesheet"
    />
    <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;
}
ts
import Ogma, { CanvasLayer, Point, MouseMoveEvent } from '@linkurious/ogma';

interface Options {
  radius?: number;
  k?: number;
  strokeColor?: string;
  strokeWidth?: number;
}

export class FishEye {
  private ogma: Ogma;
  private positions: Point[] = [];
  private cached: Point[] = [];
  private layer?: CanvasLayer;
  private cx = 0;
  private cy = 0;
  private r = 200;
  private strokeWidth = 1;
  private strokeColor = '#247BA0';
  private k = 1.5;

  constructor(ogma: Ogma) {
    this.ogma = ogma;
  }

  public enable({
    k = 1.5,
    radius = 200,
    strokeColor = '#247BA0',
    strokeWidth = 2
  }: Options = {}) {
    this.k = k;
    this.r = radius;
    this.strokeWidth = strokeWidth;
    this.strokeColor = strokeColor;

    this.layer = this.ogma.layers.addCanvasLayer(this.draw);
    this.ogma.events.on('mousemove', this.onMouseMove).on('zoom', this.onZoom);
    this.ogma.setOptions({
      interactions: { drag: { enabled: false } },
      detect: {
        nodes: false,
        edges: false
      }
    });
    this.onZoom();

    this.cached = this.ogma.getNodes().getPosition();
    this.positions = this.ogma.getNodes().getPosition();
  }

  private onMouseMove = (evt: MouseMoveEvent) => {
    const pos = this.ogma.view.screenToGraphCoordinates(evt);
    const cx = (this.cx = pos.x);
    const cy = (this.cy = pos.y);
    const cr = this.r / this.ogma.view.getZoom();
    const k = this.k;

    const p = (k + 1) * cr;

    let temp = this.positions;
    this.positions = this.cached;
    this.cached = temp;

    const r2 = cr * cr;
    this.positions.forEach((node, i) => {
      const nx = node.x;
      const ny = node.y;

      const cachedNode = this.cached[i];
      cachedNode.x = nx;
      cachedNode.y = ny;

      const dx = nx - cx;
      const dy = ny - cy;

      const distSq = dx * dx + dy * dy;

      if (distSq > 0 && distSq <= r2) {
        const dist = Math.sqrt(distSq);
        const dm = (p * dist) / (k * dist + cr);
        const cos = (nx - cx) / dist;
        const sin = (ny - cy) / dist;

        node.x = cos * dm + cx;
        node.y = sin * dm + cy;
      }
    });

    this.ogma.getNodes().setAttributes(this.positions);
    this.layer?.refresh();
  };

  private onZoom = () => {
    this.layer?.refresh();
  };

  private draw = (ctx: CanvasRenderingContext2D) => {
    const zoom = this.ogma.view.getZoom();
    const cr = this.r / zoom;

    ctx.strokeStyle = this.strokeColor;
    ctx.lineWidth = this.strokeWidth / zoom;
    ctx.beginPath();
    ctx.moveTo(this.cx + cr, this.cy);
    ctx.arc(this.cx, this.cy, cr, 0, 2 * Math.PI);
    ctx.closePath();
    ctx.stroke();
  };

  public disable() {
    if (this.layer) {
      this.layer.destroy();
      delete this.layer;
    }
    this.ogma.events.off(this.onMouseMove).off(this.onZoom);
    this.ogma.setOptions({
      interactions: { drag: { enabled: true } },
      detect: {
        nodes: true,
        edges: true
      }
    });
  }
}