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
Drawing Sticky Notes
Enable sticky note drawing mode with enableStickyNoteDrawing(). A sticky note is a plain, resizable text annotation (Miro-style: padded, colored, no connector arrow), empty, with a "Quick note…" ghost placeholder — unlike a comment, it has no target and isn't collapsible. It places the same way as a box: click for a default square size, or drag to size it yourself.
addStickyNoteButton.addEventListener("click", () => {
controller.enableStickyNoteDrawing();
});With Custom Styling
addStickyNoteButton.addEventListener("click", () => {
controller.enableStickyNoteDrawing({
background: "#FFEB99",
color: "#4A3B00",
fontSize: 18,
padding: 16,
placeholder: "Jot something down…"
});
});placeholder is any text annotation's ghost text, shown via the textarea's native placeholder attribute while content is empty - it overrides the global ControllerOptions.textPlaceholder for that one annotation. See Text Styles.
Interactive Behavior
When sticky note drawing is enabled:
- User clicks to drop the note, already selected - a plain click (no drag) sizes it to a default square; dragging sizes it to match instead, same as a box
- On a plain click, it drops straight into editing - the placeholder is just ghost text (empty content), so typing immediately replaces it, no extra click or selection needed (dragging instead needs a follow-up click to start typing, same as text/box)
- Because it isn't
fixedSize, the usual corner/edge drag handles let the user resize it afterward, same as atextannotation
Erasing Annotations
Unlike the enable*Drawing() tools, erase mode isn't a one-shot draw - once armed with enableEraseMode(), every click on an annotation deletes it immediately, and it stays armed across multiple clicks until you turn it off.
eraseButton.addEventListener("click", () => {
if (controller.isEraseModeActive()) {
controller.disableEraseMode();
} else {
controller.enableEraseMode();
}
});Interactive Behavior
When erase mode is enabled:
- Clicking an annotation deletes it immediately (cascades to a comment's linked arrows, same as
remove()) - Clicking empty space does nothing
- Stays active across multiple clicks - no need to re-arm it between deletes
- Turned off by
disableEraseMode(), or automatically when another drawing tool is enabled orcancelDrawing()is called
TIP
This is a separate workflow from selecting an annotation and clicking a trash/delete button - use whichever fits your UI, or offer both.
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: "Sticky Note",
action: () => controller.enableStickyNoteDrawing()
},
{
label: "Erase",
action: () => controller.enableEraseMode()
},
{
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