# Canvas Connection System
This document details the connection system that allows visual links between canvas elements.
## Overview
The connection system enables users to create visual links (lines/arrows) between elements. Connections are:
- Stored as part of the **source** element's configuration
- Rendered as SVG paths
- Support styling (color, width, style, direction)
- Can be data-driven
- Support intermediate vertices for custom paths
## Connection Architecture
### Class Structure
Two parallel implementations exist based on feature toggle:
| Standard | Pan/Zoom (`canvasPanelPanZoom`) |
|----------|--------------------------------|
| `Connections.tsx` | `Connections2.tsx` |
| `ConnectionAnchors.tsx` | `ConnectionAnchors2.tsx` |
| `ConnectionSVG.tsx` | `ConnectionSVG2.tsx` |
### Key Classes
```typescript
class Connections {
scene: Scene;
// DOM references
connectionAnchorDiv?: HTMLDivElement; // Anchor container
anchorsDiv?: HTMLDivElement; // Individual anchor points
connectionSVG?: SVGElement; // Line rendering
connectionLine?: SVGLineElement; // Active drawing line
// Vertex editing
connectionSVGVertex?: SVGElement;
connectionVertexPath?: SVGPathElement;
connectionVertex?: SVGCircleElement;
// Connection state
connectionSource?: ElementState;
connectionTarget?: ElementState;
isDrawingConnection?: boolean;
selectedVertexIndex?: number;
// State management
state: ConnectionState[] = [];
selection: BehaviorSubject<ConnectionState | undefined>;
}
```
### ConnectionState Interface
```typescript
interface ConnectionState {
index: number; // Index in source's connections array
source: ElementState; // Source element
target: ElementState; // Target element
info: CanvasConnection; // Configuration
vertices?: ConnectionCoordinates[];
sourceOriginal?: ConnectionCoordinates;
targetOriginal?: ConnectionCoordinates;
}
```
## Connection Data Model
### CanvasConnection
```typescript
interface CanvasConnection {
// Anchor points (normalized coordinates)
source: ConnectionCoordinates; // -1 to 1 from center
target: ConnectionCoordinates; // -1 to 1 from center
// Target identification
targetName?: string; // Named element or parent
// Path type (currently only straight supported)
path: ConnectionPath; // 'straight'
// Styling
color?: ColorDimensionConfig; // Line color (fixed or from data)
size?: ScaleDimensionConfig; // Line width (fixed or from data)
lineStyle?: LineStyleConfig; // Solid, dashed, dotted
direction?: DirectionDimensionConfig; // Arrow direction
radius?: ScaleDimensionConfig; // Corner radius for vertices
// Vertex support
vertices?: ConnectionCoordinates[]; // Intermediate points
// Original positions (for vertex calculations)
sourceOriginal?: ConnectionCoordinates;
targetOriginal?: ConnectionCoordinates;
}
```
### ConnectionCoordinates
```typescript
interface ConnectionCoordinates {
x: number; // -1 (left) to 1 (right) from center
y: number; // -1 (bottom) to 1 (top) from center
}
```
## Coordinate System
### Normalized Coordinates
Connections use a normalized coordinate system:
```
y = 1 (top)
│
│
x = -1 ─────┼───── x = 1
(left) │ (right)
│
y = -1 (bottom)
```
Center of element = (0, 0)
### Conversion: DOM → Normalized
```typescript
// From Connections.tsx
const sourceX = (connectionLineX1 - sourceHorizontalCenter) / (sourceRect.width / 2 / transformScale);
const sourceY = (sourceVerticalCenter - connectionLineY1) / (sourceRect.height / 2 / transformScale);
```
### Conversion: Normalized → DOM
```typescript
// From utils.ts
const x1 = (sourceHorizontalCenter + (info.source.x * sourceRect.width) / 2) / transformScale;
const y1 = (sourceVerticalCenter - (info.source.y * sourceRect.height) / 2) / transformScale;
```
### Rotation Handling
For rotated elements, connection points must be transformed:
```typescript
// From utils.ts: getRotatedConnectionPoint
const rad = rotation * (Math.PI / 180);
const cos = Math.cos(rad);
const sin = Math.sin(rad);
// Apply rotation to offset
const rotatedOffsetX = offsetX * cos - offsetY * sin;
const rotatedOffsetY = offsetX * sin + offsetY * cos;
const x = centerX + rotatedOffsetX;
const y = centerY + rotatedOffsetY;
```
## Connection Anchors
### Default Anchors
8 anchor points on each element:
```typescript
// From ConnectionAnchors.tsx
export const ANCHORS = [
{ x: 0, y: 1 }, // Top center
{ x: 0, y: -1 }, // Bottom center
{ x: -1, y: 0 }, // Left center
{ x: 1, y: 0 }, // Right center
{ x: -1, y: 1 }, // Top-left
{ x: 1, y: 1 }, // Top-right
{ x: -1, y: -1 }, // Bottom-left
{ x: 1, y: -1 }, // Bottom-right
];
```
### Custom Anchors
SVG elements can define custom anchor points:
```typescript
// Triangle element
customConnectionAnchors: [
{ x: 0, y: 1 }, // Top vertex
{ x: -1, y: -1 }, // Bottom-left
{ x: 1, y: -1 }, // Bottom-right
]
```
## Creating Connections
### User Flow
1. User hovers over element → Anchor points appear
2. User drags from anchor point → Connection line drawn
3. User drops on another element (or canvas) → Connection created
4. Connection saved to source element's options
### Code Flow
```typescript
// 1. Mouse enters element
handleMouseEnter(event) {
// Position anchor container over element
connectionAnchorDiv.style.top = `${relativeTop / transformScale}px`;
connectionAnchorDiv.style.left = `${relativeLeft / transformScale}px`;
connectionAnchorDiv.style.display = 'block';
}
// 2. Drag starts from anchor
handleConnectionDragStart(selectedTarget, clientX, clientY) {
// Set connection line start position
connectionLine.setAttribute('x1', `${x}`);
connectionLine.setAttribute('y1', `${y}`);
// Start listening for mouse movement
scene.selecto?.rootContainer?.addEventListener('mousemove', connectionListener);
}
// 3. Mouse moves
connectionListener(event) {
// Update line end position
connectionLine.setAttribute('x2', `${x / transformScale}`);
connectionLine.setAttribute('y2', `${y / transformScale}`);
// Check if we've left the highlight area
if (connectionLength > CONNECTION_ANCHOR_HIGHLIGHT_OFFSET) {
isDrawingConnection = true;
connectionSVG.style.display = 'block';
}
}
// 4. Mouse released
// (in connectionListener when !event.buttons)
const connection = {
source: { x: sourceX, y: sourceY },
target: { x: targetX, y: targetY },
targetName: targetName,
color: { fixed: config.theme2.colors.text.primary },
size: { fixed: 2, min: 1, max: 10 },
path: ConnectionPath.Straight,
};
connectionSource.options.connections.push(connection);
connectionSource.onChange(connectionSource.options);
```
## Connection Rendering
### ConnectionSVG Component
```typescript
// Simplified structure
<svg ref={setSVGRef}>
{/* Active drawing line */}
<line ref={setLineRef} stroke={theme.colors.text.primary} />
</svg>
{/* Vertex editing overlay */}
<svg ref={setSVGVertexRef}>
<path ref={setVertexPathRef} />
<circle ref={setVertexRef} />
</svg>
{/* All saved connections */}
{connections.map(connection => (
<ConnectionPath
key={`${connection.source.UID}-${connection.index}`}
connection={connection}
scene={scene}
/>
))}
```
### Connection Path Rendering
```typescript
// Calculate coordinates
const { x1, y1, x2, y2 } = calculateCoordinates(
sourceRect, parentRect, info, target, transformScale
);
// Build path with vertices
let pathData = `M ${x1} ${y1}`;
if (vertices?.length) {
for (const vertex of vertices) {
const vx = vertex.x * (x2 - x1) + x1;
const vy = vertex.y * (y2 - y1) + y1;
pathData += ` L ${vx} ${vy}`;
}
}
pathData += ` L ${x2} ${y2}`;
```
## Vertex System
Vertices allow creating non-straight connection paths.
### Adding Vertices
1. Click on connection line segment → Add point appears
2. Drag add point → Creates new vertex
3. Vertex stored as relative position between source and target
### Vertex Coordinates
Vertices are stored as relative positions:
```typescript
const newVertex = {
x: (actualX - xStart) / (xEnd - xStart),
y: (actualY - yStart) / (yEnd - yStart),
};
```
### Vertex Dragging
```typescript
vertexListener(event) {
// Get current position
const x = (event.pageX - parentBoundingRect.x) / transformScale;
const y = (event.pageY - parentBoundingRect.y) / transformScale;
// Update visual position
connectionVertex?.setAttribute('cx', `${x}`);
connectionVertex?.setAttribute('cy', `${y}`);
// Calculate snapping
// Check for horizontal/vertical alignment
// Check for deletion (aligned with adjacent segments)
// On mouse up: save new vertex position
}
```
### Vertex Snapping
Vertices snap to horizontal/vertical alignment:
```typescript
const CONNECTION_VERTEX_ORTHO_TOLERANCE = 0.05;
// Check if segment is nearly vertical
const verticalBefore = Math.abs((x - vx1) / (y - vy1)) < CONNECTION_VERTEX_ORTHO_TOLERANCE;
// Snap to vertical
if (verticalBefore) {
xSnap = vx1;
}
```
### Vertex Deletion
Vertices are deleted when dragged to align with adjacent segments:
```typescript
const CONNECTION_VERTEX_SNAP_TOLERANCE = (5 / 180) * Math.PI;
const angleOverall = calculateAngle(vx1, vy1, vx2, vy2);
const angleBefore = calculateAngle(vx1, vy1, x, y);
deleteVertex = Math.abs(angleBefore - angleOverall) < CONNECTION_VERTEX_SNAP_TOLERANCE;
```
## Connection Styling
### Line Style
```typescript
enum LineStyle {
Solid = 'solid',
Dashed = 'dashed',
Dotted = 'dotted',
}
enum StrokeDasharray {
Solid = '0',
Dashed = '8 8',
Dotted = '3',
}
```
### Direction (Arrows)
```typescript
// ConnectionDirection from @grafana/schema
enum ConnectionDirection {
Forward = 'forward', // Arrow at target
Reverse = 'reverse', // Arrow at source
Both = 'both', // Arrows at both ends
None = 'none', // No arrows
}
```
### Animation
Connections support animated dashes:
```typescript
if (shouldAnimate) {
// CSS animation applied to stroke-dashoffset
}
```
### Data-Driven Styling
Styling can be driven by data fields:
```typescript
const strokeColor = info.color
? scene.context.getColor(info.color).value()
: defaultArrowColor;
const strokeWidth = info.size
? scene.context.getScale(info.size).get(lastRowIndex)
: defaultArrowSize;
```
## Connection Updates on Move
When elements move, connections must update:
```typescript
// In sceneAbleManagement.ts
.on('drag', (event) => {
const targetedElement = findElementByTarget(event.target, scene.root.elements);
if (targetedElement) {
targetedElement.applyDrag(event);
if (scene.connections.connectionsNeedUpdate(targetedElement)) {
scene.moveableActionCallback(true);
}
}
});
```
```typescript
// In Connections
connectionsNeedUpdate(element: ElementState): boolean {
return isConnectionSource(element) || isConnectionTarget(element, this.scene.byName);
}
```
## Known Issues
### Current Limitations
1. **Path Types:** Only straight lines supported (no curved/orthogonal)
2. **Vertex Originals:** TODOs about clearing originals on vertex removal
3. **Rotation:** Complex rotation handling for connection points
4. **Performance:** All connections recalculate on any element move
### TODOs from Code
```typescript
// Connections.tsx:245
// TODO: Break this out into util function and add tests
// Connections.tsx:444
// TODO for vertex removal, clear out originals?
```
### Pan/Zoom Considerations
In pan/zoom mode:
- Connection SVG must resize with viewport
- Coordinates must account for zoom scale and scroll position
- Connection anchors positioned differently
```typescript
// In Scene.updateConnectionsSize()
const scale = this.infiniteViewer!.getZoom();
const left = this.infiniteViewer!.getScrollLeft() || 0;
const top = this.infiniteViewer!.getScrollTop() || 0;
svgConnections.style.left = `${left}px`;
svgConnections.style.top = `${top}px`;
svgConnections.style.width = `${width / scale}px`;
svgConnections.style.height = `${height / scale}px`;
```