Skip to content

Controller

The Controller (implemented as the Control class) is the central management system for all annotations in your Ogma graph. It provides methods to add, remove, select, and interact with annotations, as well as an event system to respond to changes.

Overview

Every Ogma Annotations setup starts with creating a Controller instance:

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

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

The controller maintains the state of all annotations and coordinates their rendering with the Ogma instance.

Core Responsibilities

The Controller handles:

  1. Annotation Management - Adding, removing, and tracking all annotations
  2. Selection State - Managing which annotation is currently selected
  3. Event Dispatching - Notifying your code when annotations change
  4. User Interaction - Handling interactive creation and editing
  5. Rendering Coordination - Working with Ogma to display annotations

Basic Usage

Adding Annotations

Use the add() method to add annotations to the controller:

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

const arrow = createArrow(0, 0, 100, 100, { stroke: "#ff6b6b" });
controller.add(arrow);

const text = createText(50, 50, "Label", { fontSize: 16 });
controller.add(text);

You can also add multiple annotations at once:

typescript
const annotations = [
  createArrow(0, 0, 100, 100),
  createArrow(0, 0, -100, -100),
  createText(0, 0, "Center")
];

annotations.forEach((annotation) => controller.add(annotation));

Accessing Annotations

Get all annotations managed by the controller:

typescript
const allAnnotations = controller.getAnnotations();
console.log(`Total annotations: ${allAnnotations.length}`);

Get a specific annotation by ID:

typescript
const annotation = controller.getAnnotation(annotationId);
if (annotation) {
  console.log("Found:", annotation);
}

Removing Annotations

Remove an annotation by reference:

typescript
controller.remove(arrow);

Remove by ID:

typescript
controller.removeById(annotationId);

Remove all annotations:

typescript
controller.clear();

Selection

The controller maintains a selection state - at most one annotation can be selected at a time.

Getting Selection

typescript
const selected = controller.getSelectedAnnotation();
if (selected) {
  console.log("Selected annotation:", selected.id);
} else {
  console.log("No annotation selected");
}

Setting Selection

typescript
// Select an annotation
controller.select(arrow);

// Or select by ID
controller.selectById(annotationId);

// Deselect
controller.deselect();

Selection Events

Listen for selection changes:

typescript
controller.on("select", (annotation) => {
  console.log("Selected:", annotation.id);
});

controller.on("deselect", (annotation) => {
  console.log("Deselected:", annotation.id);
});

Event System

The controller emits events for all significant changes. This allows you to react to user interactions and update your UI accordingly.

Available Events

EventDescriptionPayload
addAn annotation was addedThe added annotation
removeAn annotation was removedThe removed annotation
updateAn annotation was modifiedThe updated annotation
selectAn annotation was selectedThe selected annotation
deselectAn annotation was deselectedThe deselected annotation
clearAll annotations were removedundefined

Listening to Events

typescript
// Listen for additions
controller.on("add", (annotation) => {
  console.log("New annotation:", annotation);
  updateAnnotationList();
});

// Listen for updates
controller.on("update", (annotation) => {
  console.log("Modified:", annotation);
  refreshUI();
});

// Listen for removals
controller.on("remove", (annotation) => {
  console.log("Removed:", annotation.id);
  updateCount();
});

One-Time Listeners

For events you only need to hear once:

typescript
controller.once("add", (annotation) => {
  console.log("First annotation added!");
  showWelcomeMessage();
});

Removing Listeners

typescript
const handler = (annotation) => {
  console.log("Added:", annotation);
};

// Add listener
controller.on("add", handler);

// Remove listener
controller.off("add", handler);

Remove all listeners for an event:

typescript
controller.off("add"); // Removes all 'add' listeners

Interactive Creation

The controller provides methods to enable user-driven annotation creation:

Enable Arrow Drawing

Use enableArrowDrawing() to let users draw arrows by clicking and dragging:

typescript
// Enable arrow drawing mode
controller.enableArrowDrawing({
  strokeType: "plain",
  strokeColor: "#3498db",
  strokeWidth: 2,
  head: "arrow"
});

// Listen for completion
controller.once("completeDrawing", (annotation) => {
  console.log("Arrow created:", annotation);
});

// Listen for cancellation (e.g., user pressed Escape)
controller.once("cancelDrawing", () => {
  console.log("Drawing cancelled");
});

When this method is called, the user can:

  1. Click on the graph to set the arrow's start point
  2. Drag to set the arrow's end point
  3. Release to complete the arrow
  4. Press Escape to cancel

Enable Text Drawing

Use enableTextDrawing() to let users create text annotations:

typescript
// Enable text drawing mode
controller.enableTextDrawing({
  font: "Arial",
  fontSize: 16,
  color: "#2c3e50",
  background: "#ffffff",
  borderRadius: 4,
  padding: 8
});

// Listen for completion
controller.once("completeDrawing", (annotation) => {
  console.log("Text created:", annotation);
});

// Listen for cancellation
controller.once("cancelDrawing", () => {
  console.log("Drawing cancelled");
});

When this method is called, the user can:

  1. Click on the graph to place the text
  2. Drag to size the text box
  3. Type to edit the content
  4. Click outside or press Enter to complete
  5. Press Escape to cancel

Cancel Drawing

To programmatically cancel an ongoing drawing operation:

typescript
controller.cancelDrawing();

This will trigger the cancelDrawing event.

Controller Options

When creating a controller, you can pass options:

typescript
const controller = new Control(ogma, {
  // Options will be added here in future versions
});

Currently, the controller uses sensible defaults and doesn't require configuration.

Advanced: Controller State

The controller maintains internal state about:

  • All annotations - The complete list of managed annotations
  • Selection - The currently selected annotation (if any)
  • Interaction mode - Whether the user is currently creating/editing an annotation

You generally don't need to access this state directly, but it's useful to understand:

typescript
// Check if something is selected
if (controller.getSelectedAnnotation()) {
  console.log("User has selected an annotation");
}

// Check annotation count
const count = controller.getAnnotations().length;
console.log(`Managing ${count} annotations`);

Lifecycle and Cleanup

Initialization

The controller is ready to use immediately after creation:

typescript
const controller = new Control(ogma);
// Ready to use!

Cleanup

When you're done with the controller (e.g., unmounting a component or destroying the graph):

typescript
// Remove all annotations and clean up
controller.destroy();

This will:

  • Remove all annotations from the display
  • Remove all event listeners
  • Clean up internal state

Destroy Before Ogma

If you're also destroying the Ogma instance, destroy the controller first:

typescript
controller.destroy();
ogma.destroy();

Best Practices

1. One Controller Per Ogma Instance

typescript
// ✅ Good: One controller per Ogma instance
const ogma = new Ogma({ container: "graph" });
const controller = new Control(ogma);

// ❌ Avoid: Multiple controllers for the same Ogma instance
const controller1 = new Control(ogma);
const controller2 = new Control(ogma); // Don't do this!

2. Use Events for UI Updates

typescript
// ✅ Good: React to events
controller.on("add", () => {
  updateAnnotationCount();
  refreshUI();
});

// ❌ Avoid: Polling
setInterval(() => {
  const count = controller.getAnnotations().length;
  // This is inefficient
}, 1000);

3. Clean Up When Done

typescript
// ✅ Good: Clean up properly
function cleanup() {
  controller.destroy();
  ogma.destroy();
}

// In a React component
useEffect(() => {
  const controller = new Control(ogma);
  return () => controller.destroy(); // Cleanup on unmount
}, [ogma]);

4. Separate Concerns

typescript
// ✅ Good: Keep UI logic separate
controller.on("select", (annotation) => {
  updateStylePanel(annotation);
  highlightInList(annotation);
});

// Business logic
controller.on("add", (annotation) => {
  logAnalytics("annotation_created", annotation.type);
  saveToBackend(annotation);
});

Examples

Complete Setup with Events

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

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

// Set up event handlers
controller.on("add", (annotation) => {
  console.log("Added:", annotation.type, annotation.id);
  document.getElementById("count").textContent = String(
    controller.getAnnotations().length
  );
});

controller.on("select", (annotation) => {
  console.log("Selected:", annotation.id);
  showStylePanel(annotation);
});

controller.on("remove", (annotation) => {
  console.log("Removed:", annotation.id);
  hideStylePanelIfEmpty();
});

// Add some annotations
controller.add(createArrow(0, 0, 100, 100, { stroke: "#e74c3c" }));
controller.add(createText(50, 50, "Example", { fontSize: 16 }));

// Cleanup function
function cleanup() {
  controller.destroy();
  ogma.destroy();
}

Interactive Annotation Creation

typescript
function enableDrawingMode(type: "arrow" | "text") {
  if (type === "arrow") {
    controller.enableArrowDrawing({
      strokeType: "plain",
      strokeColor: "#3498db",
      strokeWidth: 2,
      head: "arrow"
    });
  } else {
    controller.enableTextDrawing({
      font: "Arial",
      fontSize: 14,
      color: "#2c3e50",
      background: "#ffffff",
      padding: 8
    });
  }

  // Handle completion or cancellation
  const cleanup = () => {
    console.log("Drawing finished");
  };

  controller.once("completeDrawing", cleanup);
  controller.once("cancelDrawing", cleanup);
}

// Usage with buttons
document.getElementById("add-arrow-btn").addEventListener("click", () => {
  enableDrawingMode("arrow");
});

document.getElementById("add-text-btn").addEventListener("click", () => {
  enableDrawingMode("text");
});

// Cancel with Escape key
document.addEventListener("keydown", (evt) => {
  if (evt.key === "Escape") {
    controller.cancelDrawing();
  }
});

API Reference

For complete API documentation including all methods, parameters, and return types, see:

Next Steps