Skip to content

Getting Started

Welcome to Ogma Annotations! This guide will help you add interactive arrows and text annotations to your Ogma graphs in just a few minutes. See it in action with Ogma.

What is Ogma Annotations?

Ogma Annotations is a plugin that adds a drawing layer on top of Ogma graphs. It allows you to:

  • Draw arrows to connect or point at graph elements
  • Add text labels to annotate and explain parts of your graph
  • Create annotations programmatically or let users draw them interactively
  • Fully customize the appearance of all annotations
  • Handle user interactions through a rich event system

Installation

bash
npm install @linkurious/ogma-annotations
bash
yarn add @linkurious/ogma-annotations
bash
pnpm add @linkurious/ogma-annotations

React Users

If you're building a React application, you'll also want to install the React wrapper:

bash
npm install @linkurious/ogma-annotations-react

See the React Integration Guide for React-specific instructions.

Quick Start

Let's create your first annotation in just a few steps!

Step 1: Import and Initialize

First, import Ogma Annotations and create a Controller instance:

typescript
import { Ogma } from "@linkurious/ogma";
import { Control } from "@linkurious/ogma-annotations";

// Create your Ogma instance
const ogma = new Ogma({ container: "graph-container" });

// Create the annotations controller
const controller = new Control(ogma);

The Control instance is your main entry point for managing all annotations.

Step 2: Create Your First Annotations

Now let's add an arrow and a text annotation:

typescript
import { createArrow, createText } from "@linkurious/ogma-annotations";

// Create an arrow from (0, 0) to (100, 100)
const arrow = createArrow(0, 0, 100, 100, {
  stroke: "#ff6b6b",
  strokeWidth: 3,
  ext: "end" // Arrow head at the end
});

// Create a text annotation at (50, 50)
const text = createText(50, 50, "Hello Annotations!", {
  color: "#2c3e50",
  fontSize: 16,
  background: "#ffffff",
  padding: 8
});

// Add them to the controller
controller.add(arrow);
controller.add(text);

Coordinates

The coordinates you provide are in graph space, not screen pixels. Use ogma.view.screenToGraphCoordinates() to convert mouse positions to graph coordinates.

Step 3: Try It Out!

Here's a complete working example:

html
<!doctype html>
<html>
  <head>
    <style>
      #graph-container {
        width: 800px;
        height: 600px;
      }
    </style>
  </head>
  <body>
    <div id="graph-container"></div>

    <script type="module">
      import { Ogma } from "@linkurious/ogma";
      import {
        Control,
        createArrow,
        createText
      } from "@linkurious/ogma-annotations";

      const ogma = new Ogma({ container: "graph-container" });
      const controller = new Control(ogma);

      // Add some nodes to make it interesting
      ogma.addNodes([
        { id: 0, x: -100, y: 0 },
        { id: 1, x: 100, y: 0 }
      ]);

      // Create arrow pointing from first to second node
      const arrow = createArrow(-100, 0, 100, 0, {
        stroke: "#ff6b6b",
        strokeWidth: 3,
        ext: "end"
      });
      controller.add(arrow);

      // Add explanatory text
      const text = createText(0, -50, "Connection", {
        color: "#2c3e50",
        fontSize: 14
      });
      controller.add(text);
    </script>
  </body>
</html>

Basic Concepts

Before diving deeper, let's understand the key concepts:

Controller

The Controller (Control class) is the central management system for all annotations. It handles:

  • Adding and removing annotations
  • Managing selection state
  • Dispatching events
  • Coordinating with the Ogma instance

You typically create one controller per Ogma instance.

Annotations

There are two types of annotations:

  • Arrow: A line with optional arrow heads at either or both ends
  • Text: A text label with optional background and styling

Both types share some common properties:

  • Unique id for identification
  • Position in graph coordinates
  • Customizable styles
  • Selection state

Coordinate System

Annotations use graph coordinates (the same coordinate system as Ogma nodes), not screen pixels:

  • Graph coordinates: Position in the logical graph space (e.g., where nodes are placed)
  • Screen coordinates: Pixel position on the screen (e.g., mouse position)

Convert between them using:

typescript
// Screen to graph
const { x, y } = ogma.view.screenToGraphCoordinates({ x: screenX, y: screenY });

// Graph to screen
const { x, y } = ogma.view.graphToScreenCoordinates({ x: graphX, y: graphY });

Events

The controller emits events when annotations change:

typescript
// Listen for new annotations
controller.on("add", (annotation) => {
  console.log("Annotation added:", annotation);
});

// Listen for selection changes
controller.on("select", (annotation) => {
  console.log("Annotation selected:", annotation);
});

// Listen for deletions
controller.on("remove", (annotation) => {
  console.log("Annotation removed:", annotation);
});

Locking and Hiding Annotations

Some annotations shouldn't be editable or visible to every user - one owned by another team, say, or one tied to a permission the current viewer lacks. Pass isEditable/isVisible to the controller:

typescript
const controller = new Control(ogma, {
  isEditable: (annotation) => currentUser.canEdit(annotation),
  isVisible: (annotation) => currentUser.canSee(annotation)
});
  • isEditable gates drag, resize, restyle, text edit, delete, and re-linking. The annotation stays selectable - a user can still click it to see its content - just not change it.
  • isVisible gates rendering and hit-testing, including SVG export. The annotation stays in getAnnotations()/getAnnotation() - visibility only controls the canvas, not the data.

Why a callback instead of a locked/hidden field on the annotation itself? A field on the annotation's own JSON is trivial to bypass - anyone with devtools, or anyone re-importing edited JSON, can just flip it. A callback keeps the decision entirely on your side, evaluated live against whatever your app actually trusts (a role, a permission check), so the annotation's own data can never override it.

Both default to always true. Since they're functions, not data, the controller can't tell on its own when your answer for a given annotation changes for reasons outside that annotation's data (e.g. the user's role changed) - call controller.setOptions({ isEditable, isVisible }) again with new function references to force a refresh; passing the same reference back is a no-op.

Interactive Annotations

Want to let users create annotations by clicking and dragging? Use the startArrow() and startText() methods:

typescript
import { createArrow, isArrow } from "@linkurious/ogma-annotations";

// Change cursor to indicate drawing mode
const savedOptions = ogma.getOptions();
ogma.setOptions({ cursor: { default: "crosshair" } });

// Wait for user click
ogma.events.once("click", (evt) => {
  // Convert click position to graph coordinates
  const { x, y } = ogma.view.screenToGraphCoordinates(evt);

  // Create an arrow at that position
  const annotation = createArrow(x, y, x, y, {
    stroke: "#3498db",
    strokeWidth: 2,
    ext: "end"
  });

  // Start interactive creation (user drags to set endpoint)
  setTimeout(() => {
    controller.startArrow(x, y, annotation);
  }, 50);

  // Restore cursor when done
  controller.once("add", () => {
    ogma.setOptions(savedOptions);
  });
});

Timing

Note the setTimeout - it's necessary to wait for the next frame before starting interactive creation, otherwise the initial click event may interfere.

What's Next?

Now that you understand the basics, explore more advanced topics:

For TypeScript/JavaScript Users:

For React Users:

Examples & Inspiration:

Need Help?