Skip to content

Programmatic Creation

Learn how to create annotations programmatically using factory functions. This approach gives you full control over annotation positioning, styling, and properties.

Overview

Programmatic creation is useful when you need to:

  • Add annotations based on data or events
  • Import annotations from saved data
  • Create annotations with precise positioning
  • Build custom annotation workflows

All annotation types can be created using dedicated factory functions that return GeoJSON Feature objects.

Creating Arrows

Arrows are created using the createArrow() function, which takes start and end coordinates plus optional styling.

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

// Create a simple line
const line = createArrow(0, 0, 100, 100);
controller.add(line);

// Create an arrow with styling
const styledArrow = createArrow(0, 0, 100, 100, {
  strokeColor: "#3498db",
  strokeWidth: 2,
  strokeType: "dashed",
  head: "arrow", // Add arrowhead at end
  tail: "none" // No tail
});
controller.add(styledArrow);

// Create a bidirectional arrow
const bidirectional = createArrow(0, 0, 100, 100, {
  strokeColor: "#e74c3c",
  strokeWidth: 3,
  head: "arrow",
  tail: "arrow"
});
controller.add(bidirectional);

Arrow Extremity Types

Arrows support different extremity styles:

  • "none" - No decoration (default)
  • "arrow" - Standard arrow head
  • "arrow-plain" - Filled arrow head
  • "dot" - Circle at the end
  • "halo-dot" - Circle with halo effect

Creating Text

Text annotations are created using createText(), which positions text at specific coordinates.

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

// Create basic text
const label = createText(
  50,
  50, // x, y position (top-left corner)
  150,
  40, // width, height
  "Hello World" // content
);
controller.add(label);

// Create styled text
const styledText = createText(50, 50, 200, 60, "Important Node", {
  fontSize: 16,
  color: "#2c3e50",
  background: "#ecf0f1",
  padding: 12,
  borderRadius: 8,
  strokeColor: "#bdc3c7",
  strokeWidth: 1
});
controller.add(styledText);

// Create fixed-size text (doesn't scale with zoom)
const fixedText = createText(100, 100, 150, 40, "Always visible", {
  fixedSize: true,
  fontSize: 14,
  color: "#ffffff",
  background: "#3498db"
});
controller.add(fixedText);

Creating Boxes

Boxes are rectangular areas created with createBox().

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

// Create a simple highlight box
const highlight = createBox(
  0,
  0, // x, y position (top-left corner)
  200,
  150 // width, height
);
controller.add(highlight);

// Create styled box
const styledBox = createBox(0, 0, 200, 150, {
  background: "rgba(52, 152, 219, 0.2)",
  strokeColor: "#3498db",
  strokeWidth: 2,
  strokeType: "dashed",
  borderRadius: 8
});
controller.add(styledBox);

// Create a box with no fill (border only)
const borderBox = createBox(0, 0, 200, 150, {
  background: "transparent",
  strokeColor: "#e74c3c",
  strokeWidth: 3
});
controller.add(borderBox);

Creating Polygons

Polygons are multi-point shapes created with createPolygon().

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

// Create a triangle
const triangle = createPolygon([
  [
    [0, 0],
    [100, 0],
    [50, 100],
    [0, 0] // Close the polygon
  ]
]);
controller.add(triangle);

// Create an irregular shape
const shape = createPolygon(
  [
    [
      [0, 0],
      [100, 20],
      [120, 100],
      [50, 150],
      [0, 100],
      [0, 0]
    ]
  ],
  {
    style: {
      background: "rgba(46, 204, 113, 0.3)",
      strokeColor: "#27ae60",
      strokeWidth: 2,
      strokeType: "plain"
    }
  }
);
controller.add(shape);

WARNING

Polygons use GeoJSON coordinate format: [[[x1, y1], [x2, y2], ...]]. Note the triple array nesting - this allows for polygons with holes (not currently supported).

Creating Comments

Comments combine a text box with an arrow. Use createCommentWithArrow() to ensure proper linking.

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

// Create a comment pointing to a specific location
const { comment, arrow } = createCommentWithArrow(
  100,
  100, // Target position (where arrow points FROM)
  300,
  50, // Comment position (where arrow points TO)
  "This is important!", // Comment text
  {
    commentStyle: {
      style: {
        background: "#FFFACD",
        color: "#333",
        fontSize: 14
      }
    },
    arrowStyle: {
      strokeColor: "#666",
      strokeWidth: 2,
      head: "arrow"
    }
  }
);

// Add both to the controller
controller.add(comment);
controller.add(arrow);

TIP

Always use createCommentWithArrow() rather than creating comments manually. This ensures the arrow is properly linked to the comment, which is required for comments to work correctly.

Creating Linked Arrows

Arrows can be linked to other annotations or graph nodes, making them follow when the target moves.

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

// Create a text annotation
const label = createText(100, 100, 150, 40, "Target");
controller.add(label);

// Create an arrow linked to the text
const arrow = createArrow(0, 0, 100, 120, {
  strokeColor: "#3498db",
  head: "arrow"
});

// Link the arrow's end to the text annotation
arrow.properties.link = {
  end: {
    id: label.id,
    side: "end",
    type: "text",
    magnet: { x: 0, y: 0.5 } // Attach to left-center of text box
  }
};

controller.add(arrow);

// Now when you move the text, the arrow follows!

The magnet property specifies where on the target the arrow attaches:

  • { x: 0, y: 0 } - Top-left corner
  • { x: 0.5, y: 0 } - Top-center
  • { x: 1, y: 0.5 } - Right-center
  • { x: 0.5, y: 1 } - Bottom-center
  • { x: 0, y: 0.5 } - Left-center

Batch Creation

Add multiple annotations efficiently using batch operations:

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

// Create multiple annotations
const annotations = [
  createText(50, 50, 150, 40, "Node 1"),
  createText(250, 50, 150, 40, "Node 2"),
  createArrow(125, 70, 250, 70, { head: "arrow" }),
  createBox(20, 20, 400, 100, { background: "rgba(52, 152, 219, 0.1)" })
];

// Add all at once
annotations.forEach((annotation) => controller.add(annotation));

Creating from Data

Common pattern for creating annotations from data:

typescript
interface AnnotationData {
  type: "text" | "arrow" | "box";
  position: { x: number; y: number };
  content?: string;
  style?: any;
}

function createAnnotationsFromData(data: AnnotationData[]) {
  data.forEach((item) => {
    let annotation;

    switch (item.type) {
      case "text":
        annotation = createText(
          item.position.x,
          item.position.y,
          150,
          40,
          item.content || "",
          item.style
        );
        break;

      case "arrow":
        annotation = createArrow(
          item.position.x,
          item.position.y,
          item.position.x + 100,
          item.position.y + 100,
          item.style
        );
        break;

      case "box":
        annotation = createBox(
          item.position.x,
          item.position.y,
          200,
          150,
          item.style
        );
        break;
    }

    if (annotation) {
      controller.add(annotation);
    }
  });
}

Importing Annotations

Import saved annotations from JSON:

typescript
// Saved annotations (GeoJSON FeatureCollection)
const savedData = {
  type: "FeatureCollection",
  features: [
    {
      id: "text-1",
      type: "Feature",
      geometry: { type: "Point", coordinates: [50, 50] },
      properties: {
        type: "text",
        content: "Imported text",
        width: 150,
        height: 40,
        style: { fontSize: 14 }
      }
    }
    // ... more annotations
  ]
};

// Import all annotations
controller.add(savedData);

Best Practices

1. Generate Unique IDs

By default, annotations get auto-generated IDs. For data persistence, you may want custom IDs:

typescript
// same as used by the library internally
import { nanoid } from "nanoid";

const text = createText(50, 50, 150, 40, "Hello");
text.id = `text-${nanoid()}`;
controller.add(text);

2. Use Type-Safe Styling

Define reusable style configurations:

typescript
const theme = {
  primary: {
    strokeColor: "#3498db",
    strokeWidth: 2,
    background: "rgba(52, 152, 219, 0.2)"
  } as BoxStyle,
  secondary: {
    strokeColor: "#95a5a6",
    strokeWidth: 1,
    background: "rgba(149, 165, 166, 0.2)"
  } as BoxStyle
};

const box = createBox(0, 0, 200, 150, theme.primary);
controller.add(box);

3. Coordinate System

All coordinates are in graph space (Ogma's coordinate system), not screen pixels:

typescript
// Get graph coordinates from screen position
const graphCoords = ogma.view.screenToGraphCoordinates({ x: 100, y: 100 });

// Create annotation at graph position
const text = createText(
  graphCoords.x,
  graphCoords.y,
  150,
  40,
  "At graph position"
);
controller.add(text);

Next Steps