Appearance
Free drawing
This example shows how to use Ogma layers to handle canvas, draw on it and automatically synchronize it with the view. Click on the canvas to draw, zoom, and drag to move the view or some object. Try highlighting some nodes by drawing lines with your mouse!
ts
import Ogma, {
CanvasLayer,
Point,
MouseButtonEvent,
MouseMoveEvent
} from '@linkurious/ogma';
import { GUI } from 'dat.gui';
import simplify from 'simplify-js';
const ogma = new Ogma({
container: 'graph-container'
});
type Color = string;
interface Circle {
type: 'circle';
cx: number;
cy: number;
radius: number;
fill: Color;
strokeWidth: number;
strokeColor?: Color;
}
interface Rect {
type: 'rect';
minX: number;
minY: number;
maxX: number;
maxY: number;
fill: string | null;
strokeWidth: number;
strokeColor?: Color;
}
interface LineString {
type: 'LineString';
points: Point[];
strokeColor: Color;
lineWidth: number;
fill: null;
strokeWidth?: number;
}
type Shape = Circle | Rect | LineString;
const params = {
drawing: true,
fill: [103, 182, 220],
strokeWidth: 25,
drawingInProgress: false,
over: false,
runLayout,
undo,
clear
};
let markup: Shape[] = [];
let layer: CanvasLayer;
function clear() {
markup.length = 0;
layer.refresh();
}
function undo() {
markup.pop();
layer.refresh();
}
function update() {
applySettings();
layer.refresh();
}
const gui = new GUI({ name: 'Drawing' });
gui.add(params, 'drawing').onChange(applySettings);
gui.addColor(params, 'fill').onChange(update);
gui.add(params, 'strokeWidth', 1, 50, 1).onChange(update);
gui
.add(params, 'over')
.name('On top')
.onChange(() => {
if (params.over) layer.moveToTop();
else layer.moveToBottom();
});
gui.add(params, 'runLayout').name('Layout');
gui.add(params, 'undo').name('Remove last');
gui.add(params, 'clear').name('Clear drawing');
function draw(ctx: CanvasRenderingContext2D) {
markup.forEach(shape => {
ctx.beginPath();
ctx.fillStyle = shape.fill || 'transparent';
ctx.globalAlpha = 0.35;
ctx.lineWidth = shape.strokeWidth || 2;
ctx.strokeStyle = shape.strokeColor || '#000';
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
if (shape.type === 'circle') {
const { radius, cx, cy } = shape;
ctx.moveTo(cx + radius, cy);
ctx.arc(cx, cy, radius, 0, 2 * Math.PI, false);
ctx.closePath();
}
if (shape.type === 'rect') {
const { minX, minY, maxX, maxY } = shape;
ctx.moveTo(minX, minY);
ctx.rect(minX, minY, maxX - minX, maxY - minY);
ctx.closePath();
}
if (shape.type === 'LineString') {
ctx.strokeStyle = shape.strokeColor;
ctx.lineWidth = shape.lineWidth;
const points = shape.points;
ctx.moveTo(points[0].x, points[0].y);
for (let i = 0; i < points.length; i++) {
const { x, y } = points[i];
ctx.lineTo(x, y);
}
}
ctx.stroke();
if (shape.fill !== null) ctx.fill();
});
}
function runLayout() {
return ogma.layouts
.force({
gravity: 0.015,
locate: { padding: 50 }
})
.then(refreshComponents);
}
function circumscribeBigComps() {
const biggestCoponents = ogma
.getConnectedComponents()
.filter(component => component.size > 2);
biggestCoponents.forEach((component, i) => {
const bbox = component.getBoundingBox();
const radius = Math.max(bbox.width, bbox.height) / 2;
const color = rgbToString({ r: 255, g: 127, b: 0 });
markup.push({
type: 'circle',
cx: bbox.cx,
cy: bbox.cy,
fill: color,
strokeWidth: 5,
radius
});
markup.push({
type: 'rect',
fill: null,
strokeColor: color,
strokeWidth: 2,
minX: bbox.minX,
minY: bbox.minY,
maxX: bbox.maxX,
maxY: bbox.maxY
});
});
}
function onDown({ button, x, y }: MouseButtonEvent<unknown, unknown>) {
if (button === 'left') {
params.drawingInProgress = true;
const [r, g, b] = params.fill;
markup.push({
type: 'LineString',
strokeColor: rgbToString({ r, g, b }),
lineWidth: params.strokeWidth,
fill: null,
points: [ogma.view.screenToGraphCoordinates({ x, y })]
});
}
}
function onMove({ x, y }: MouseMoveEvent) {
requestAnimationFrame(() => {
if (params.drawingInProgress) {
(markup[markup.length - 1] as LineString).points.push(
ogma.view.screenToGraphCoordinates({ x, y })
);
layer.refresh();
}
});
}
function onUp() {
if (params.drawingInProgress) {
params.drawingInProgress = false;
const shape = markup[markup.length - 1] as LineString;
shape.points = simplify(shape.points, 2) as Point[];
layer.refresh();
}
}
function refreshComponents() {
markup = markup.filter(shape => shape.type === 'LineString');
circumscribeBigComps();
layer.refresh();
}
function applySettings() {
if (params.drawing && ogma.getOptions().interactions!.drag!.enabled) {
console.log('disable drag');
ogma.events.on('mousemove', onMove);
ogma.events.on('mousedown', onDown);
ogma.events.on('mouseup', onUp);
return ogma.setOptions({
interactions: {
pan: { enabled: false },
drag: { enabled: false }
},
detect: { nodes: false, edges: false }
});
}
ogma.events.off(onDown).off(onMove).off(onUp);
return ogma.setOptions({
interactions: {
pan: { enabled: true },
drag: { enabled: true }
},
detect: { nodes: true, edges: true }
});
}
const graph = await ogma.generate.random({ nodes: 100, edges: 50 });
graph.nodes.forEach(node => {
const rmax = 13;
const rmin = 5;
node.attributes = node.attributes || {};
node.attributes.radius = Math.max(rmin, Math.min(rmax, Math.random() * rmax));
});
await ogma.setGraph(graph);
layer = ogma.layers.addCanvasLayer(draw);
layer.moveDown();
await runLayout();
applySettings();
// create component boundaries markup
refreshComponents();
// refresh component boundaries when dragging
ogma.events.on('nodesDragProgress', refreshComponents);
function rgbToString({ r, g, b }: { r: number; g: number; b: number }) {
return `rgb(${r}, ${g}, ${b})`;
}
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;
}
#focus-select {
position: absolute;
top: 20px;
right: 20px;
padding: 10px;
background: white;
z-index: 400;
}
#focus-select label {
display: block;
}
#focus-select .controls {
text-align: center;
margin-top: 10px;
}
#focus-select .content {
line-height: 1.5em;
}
.control-bar {
font-family: Helvetica, Arial, sans-serif;
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.65);
border-radius: 4px;
}