# 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`; ```