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