Interactive creation of Annotations
Learn how to let users draw annotations interactively by clicking and dragging on the graph. This is the recommended approach for most user-facing annotation tools.
Overview
Interactive creation enables users to:
- Click to place annotations
- Drag to size boxes and arrows
- Click multiple points for polygons
- Draw arrows that automatically link to targets
All annotation types support interactive creation through dedicated enable*Drawing() methods.
Drawing Workflow
The typical workflow for interactive annotation creation:
- User clicks a toolbar button (e.g., "Add Arrow")
- Your code calls
controller.enableArrowDrawing() - User clicks on the graph to start drawing
- User drags/clicks to complete the annotation
- Annotation is automatically added to the controller
Drawing Arrows
Enable arrow drawing mode with enableArrowDrawing():
// Basic arrow drawing
addArrowButton.addEventListener("click", () => {
controller.enableArrowDrawing();
});With Custom Styling
addArrowButton.addEventListener("click", () => {
controller.enableArrowDrawing({
strokeColor: "#3498db",
strokeWidth: 2,
strokeType: "plain",
head: "arrow",
tail: "none"
});
});Interactive Behavior
When arrow drawing is enabled:
- User clicks to set the arrow's start point
- User drags to position the end point
- Arrow renders in real-time during drag
- User releases mouse to complete
- If the end point is over another annotation or node, the arrow automatically links to it
Automatic Linking
Arrows automatically link when you release the mouse over a target:
controller.enableArrowDrawing({ head: "arrow" });
// When user releases mouse over a text annotation:
// - Arrow end automatically links to the text
// - Moving the text will now move the arrow's end pointDrawing Text
Enable text drawing mode with enableTextDrawing():
addTextButton.addEventListener("click", () => {
controller.enableTextDrawing();
});With Custom Styling
addTextButton.addEventListener("click", () => {
controller.enableTextDrawing({
fontSize: 16,
color: "#2c3e50",
background: "#ecf0f1",
padding: 12,
borderRadius: 8
});
});Interactive Behavior
When text drawing is enabled:
- User clicks to place the text box
- Text box appears with placeholder text
- User can immediately start typing
- Box auto-sizes to fit content
- User presses Enter or clicks outside to finish
Drawing Boxes
Enable box drawing mode with enableBoxDrawing():
addBoxButton.addEventListener("click", () => {
controller.enableBoxDrawing();
});With Custom Styling
addBoxButton.addEventListener("click", () => {
controller.enableBoxDrawing({
background: "rgba(52, 152, 219, 0.2)",
strokeColor: "#3498db",
strokeWidth: 2,
borderRadius: 8
});
});Interactive Behavior
When box drawing is enabled:
- User clicks to set the box's top-left corner
- User drags to size the box
- Box renders in real-time during drag
- User releases mouse to complete
Drawing Polygons
Enable polygon drawing mode with enablePolygonDrawing():
addPolygonButton.addEventListener("click", () => {
controller.enablePolygonDrawing();
});With Custom Styling
addPolygonButton.addEventListener("click", () => {
controller.enablePolygonDrawing({
background: "rgba(46, 204, 113, 0.3)",
strokeColor: "#27ae60",
strokeWidth: 2
});
});Interactive Behavior
When polygon drawing is enabled:
- User clicks to place the first vertex
- User moves mouse (polygon edge follows cursor)
- User clicks to place additional vertices
- Polygon closes automatically when user double-clicks or presses Enter
- Press Escape to cancel
TIP
Polygons require at least 3 points. The system automatically closes the polygon by connecting the last point to the first.
Drawing Comments
Enable comment drawing mode with enableCommentDrawing():
addCommentButton.addEventListener("click", () => {
controller.enableCommentDrawing();
});With Custom Styling
addCommentButton.addEventListener("click", () => {
controller.enableCommentDrawing({
commentStyle: {
content: "Add your comment here...",
style: {
background: "#FFFACD",
color: "#333",
fontSize: 14,
padding: 12
}
},
arrowStyle: {
strokeColor: "#666",
strokeWidth: 2,
head: "arrow"
}
});
});Smart Positioning
Comments automatically position themselves to avoid overlapping with other content:
// Automatic smart positioning (default)
controller.enableCommentDrawing();
// Manual offset from target
controller.enableCommentDrawing({
offsetX: 200, // Comment box 200px right of target
offsetY: -150 // Comment box 150px above target
});Interactive Behavior
When comment drawing is enabled:
- User clicks on the target (node, annotation, or empty space)
- Comment box appears at an optimal position
- Arrow automatically connects target to comment
- User can immediately start typing
- User presses Enter or clicks outside to finish
Canceling Drawing
Cancel the current drawing operation programmatically:
// User clicks cancel button
cancelButton.addEventListener("click", () => {
controller.cancelDrawing();
});
// Or listen for Escape key
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
controller.cancelDrawing();
}
});Drawing Events
Listen to drawing lifecycle events:
// Drawing completed
controller.on("completeDrawing", ({ annotation }) => {
console.log("Created:", annotation);
// Show success message, update UI, etc.
});
// Drawing canceled
controller.on("cancelDrawing", () => {
console.log("Drawing canceled");
// Reset toolbar state
});
// Annotation added
controller.on("add", ({ annotation }) => {
console.log("Annotation added:", annotation.id);
});Building a Toolbar
Complete example of an annotation toolbar:
import { Control } from "@linkurious/ogma-annotations";
// Create toolbar
const toolbar = document.createElement("div");
toolbar.className = "annotation-toolbar";
// Drawing mode buttons
const buttons = [
{
label: "Arrow",
action: () => controller.enableArrowDrawing({ head: "arrow" })
},
{
label: "Text",
action: () => controller.enableTextDrawing()
},
{
label: "Box",
action: () => controller.enableBoxDrawing()
},
{
label: "Polygon",
action: () => controller.enablePolygonDrawing()
},
{
label: "Comment",
action: () => controller.enableCommentDrawing()
},
{
label: "Cancel",
action: () => controller.cancelDrawing()
}
];
// Create buttons
buttons.forEach(({ label, action }) => {
const button = document.createElement("button");
button.textContent = label;
button.addEventListener("click", action);
toolbar.appendChild(button);
});
document.body.appendChild(toolbar);With Active State
Track which drawing mode is active:
let activeButton = null;
buttons.forEach(({ label, action }) => {
const button = document.createElement("button");
button.textContent = label;
button.addEventListener("click", () => {
// Remove active state from previous button
if (activeButton) {
activeButton.classList.remove("active");
}
// Set active state
if (label !== "Cancel") {
button.classList.add("active");
activeButton = button;
}
action();
});
toolbar.appendChild(button);
});
// Clear active state when drawing completes
controller.on("completeDrawing", () => {
if (activeButton) {
activeButton.classList.remove("active");
activeButton = null;
}
});
controller.on("cancelDrawing", () => {
if (activeButton) {
activeButton.classList.remove("active");
activeButton = null;
}
});Style Picker Integration
Let users choose annotation styles before drawing:
const styleOptions = {
arrow: {
color: "#3498db",
width: 2,
head: "arrow"
},
text: {
fontSize: 14,
color: "#2c3e50",
background: "#ecf0f1"
}
};
// Color picker
colorPicker.addEventListener("change", (e) => {
styleOptions.arrow.color = e.target.value;
styleOptions.text.color = e.target.value;
});
// Width slider
widthSlider.addEventListener("input", (e) => {
styleOptions.arrow.width = parseInt(e.target.value);
});
// Apply current styles when drawing
addArrowButton.addEventListener("click", () => {
controller.enableArrowDrawing({
strokeColor: styleOptions.arrow.color,
strokeWidth: styleOptions.arrow.width,
head: styleOptions.arrow.head
});
});
addTextButton.addEventListener("click", () => {
controller.enableTextDrawing({
fontSize: styleOptions.text.fontSize,
color: styleOptions.text.color,
background: styleOptions.text.background
});
});Advanced: Custom Mouse Handling
For advanced use cases, you can implement custom mouse handling:
import { createArrow } from "@linkurious/ogma-annotations";
// Custom click handler
ogma.events.on("click", (evt) => {
const { x, y } = ogma.view.screenToGraphCoordinates(evt);
// Create annotation at clicked position
const arrow = createArrow(x, y, x + 100, y + 100, {
strokeColor: "#3498db",
head: "arrow"
});
controller.add(arrow);
});WARNING
Using custom mouse handling means you're responsible for handling all edge cases, cancellation, and cleanup. For most use cases, use the built-in enable*Drawing() methods instead.
Best Practices
1. Provide Visual Feedback
Show which drawing mode is active:
const enableDrawing = (mode, styleOptions) => {
// Update cursor
ogma.view.getContainer().style.cursor = "crosshair";
// Update toolbar
updateActiveButton(mode);
// Enable drawing
controller[`enable${mode}Drawing`](styleOptions);
};
controller.on("completeDrawing", () => {
// Reset cursor
ogma.view.getContainer().style.cursor = "default";
});2. Support Keyboard Shortcuts
document.addEventListener("keydown", (e) => {
if (e.metaKey || e.ctrlKey) {
switch (e.key) {
case "a":
e.preventDefault();
controller.enableArrowDrawing();
break;
case "t":
e.preventDefault();
controller.enableTextDrawing();
break;
case "b":
e.preventDefault();
controller.enableBoxDrawing();
break;
}
}
if (e.key === "Escape") {
controller.cancelDrawing();
}
});3. Handle Drawing Completion
controller.on("completeDrawing", ({ annotation }) => {
// Auto-select newly created annotation
controller.select(annotation.id);
// Show properties panel
showPropertiesPanel(annotation);
// Log for undo/redo UI
console.log("Created:", annotation.properties.type);
});4. Prevent Mode Conflicts
let currentMode = null;
const setDrawingMode = (mode, styleOptions) => {
// Cancel previous mode
if (currentMode) {
controller.cancelDrawing();
}
// Set new mode
currentMode = mode;
controller[`enable${mode}Drawing`](styleOptions);
};
controller.on("completeDrawing", () => {
currentMode = null;
});
controller.on("cancelDrawing", () => {
currentMode = null;
});Next Steps
- Programmatic Creation - Create annotations from code
- Styling - Customize annotation appearance
- Managing Annotations - Update and modify annotations
- Events - Listen to annotation changes