# Canvas Panel Architecture Deep Dive This document provides detailed architectural information about the Canvas panel's internal structure and design decisions. ## Runtime Class Hierarchy ``` ElementState (base class) │ ├── FrameState extends ElementState │ │ │ └── RootElement extends FrameState │ └── (individual elements use ElementState directly) ``` ## Scene Lifecycle ### Initialization Flow ``` 1. CanvasPanel constructor └── new Scene(options, onUpdateScene, this) ├── getDashboardSrv().getCurrent() - check if editing enabled ├── this.load(options, enableEditing) │ └── new RootElement(root, this, this.save) │ └── Creates ElementState/FrameState for each child └── new Connections(this) or new Connections2(this) 2. CanvasPanel.componentDidMount ├── Set activeCanvasPanel ├── Subscribe to selection changes ├── Subscribe to connection selection └── Register with canvasInstances array 3. Scene.load (called after DOM ready via setTimeout) └── initMoveable(destroySelecto, enableEditing, this) ├── new Selecto({...}) ├── new Moveable({...}) │ └── Event handlers: rotate, click, drag, resize ├── Selecto event handlers: dragStart, select, selectEnd └── (if panZoom) new InfiniteViewer({...}) ``` ### Data Update Flow ``` CanvasPanel receives new props.data │ ▼ shouldComponentUpdate detects data change │ ▼ scene.updateData(nextProps.data) │ ▼ scene.root.updateData(this.context) │ ▼ Recursively calls updateData on all elements │ ▼ Each element: ├── Calls item.prepareData(ctx, options) if defined ├── Updates getLinks supplier ├── Calculates dataStyle (background, border) └── Calls applyLayoutStylesToDiv() ``` ### Selection Flow ``` User clicks element │ ▼ Selecto 'dragStart' event │ ├── Check if connection anchor → handleConnectionDragStart ├── Check if vertex → handleVertexDragStart └── Check if moveable element → allow/prevent selection box │ ▼ Selecto 'selectEnd' event │ ▼ scene.updateSelection({ targets }) │ ▼ moveable.target = selection.targets │ ▼ scene.selection.next(selectedElements) │ ▼ CanvasPanel subscription receives selection │ ▼ panelContext.onInstanceStateChange({ selected: v, ... }) │ ▼ Panel options editor receives new instanceState │ ▼ Options editor rebuilds with element-specific editors ``` ### Save Flow ``` User modifies element (drag, resize, property change) │ ▼ ElementState.onChange(options) or direct manipulation │ ▼ element.revId++ (trigger re-render) │ ▼ Traverse up to root, incrementing revIds │ ▼ trav.scene.save() │ ▼ scene.save() └── this.onSave(this.root.getSaveModel()) │ ▼ CanvasPanel.onUpdateScene(root) │ ▼ props.onOptionsChange({ ...options, root }) │ ▼ Grafana persists new panel options ``` ## Constraint System The constraint system enables responsive layouts by defining how elements behave when the canvas resizes. ### Constraint Types | Constraint | Behavior | |------------|----------| | `left`/`top` | Fixed distance from left/top edge | | `right`/`bottom` | Fixed distance from right/bottom edge | | `leftright`/`topbottom` | Fixed distances from both edges (stretches) | | `center` | Centered with fixed offset from center | | `scale` | Percentage-based positioning | ### Calculation Logic (from `element.tsx`) For **Top** constraint: ```typescript placement.top = placement.top ?? 0; placement.height = placement.height ?? 100; style.top = `${placement.top}px`; style.height = `${placement.height}px`; ``` For **Center** constraint: ```typescript placement.top = placement.top ?? 0; placement.height = placement.height ?? 100; translate[1] = '-50%'; style.top = `calc(50% - ${placement.top}px)`; style.height = `${placement.height}px`; ``` For **Scale** constraint: ```typescript placement.top = placement.top ?? 0; placement.bottom = placement.bottom ?? 0; style.top = `${placement.top}%`; style.bottom = `${placement.bottom}%`; ``` ### Pan/Zoom Differences When `canvasPanelPanZoom` feature is enabled, constraints use CSS transforms instead of positioning: ```typescript // Non-pan/zoom (CSS positioning) style.top = `${placement.top}px`; style.left = `${placement.left}px`; // Pan/zoom (CSS transforms) style.transform = `translate(${transformX}, ${transformY}) rotate(${rotation}deg)`; ``` ## Connection Coordinate System Connections use a normalized coordinate system: - **Origin:** Center of element - **X range:** -1 (left edge) to +1 (right edge) - **Y range:** -1 (bottom edge) to +1 (top edge) ### Coordinate Conversion ```typescript // DOM coordinates → Connection coordinates const sourceX = (connectionLineX1 - sourceHorizontalCenter) / (sourceRect.width / 2); const sourceY = (sourceVerticalCenter - connectionLineY1) / (sourceRect.height / 2); // Connection coordinates → DOM coordinates const x = (sourceHorizontalCenter + (info.source.x * sourceRect.width) / 2) / transformScale; const y = (sourceVerticalCenter - (info.source.y * sourceRect.height) / 2) / transformScale; ``` ### Rotation Handling For rotated elements, connection points require rotation matrix transformation: ```typescript const rad = rotation * (Math.PI / 180); const cos = Math.cos(rad); const sin = Math.sin(rad); const rotatedOffsetX = offsetX * cos - offsetY * sin; const rotatedOffsetY = offsetX * sin + offsetY * cos; ``` ## Dimension Context The `DimensionContext` provides data-driven styling: ```typescript context: DimensionContext = { getColor: (color: ColorDimensionConfig) => getColorDimensionFromData(this.data, color), getScale: (scale: ScaleDimensionConfig) => getScaleDimensionFromData(this.data, scale), getScalar: (scalar: ScalarDimensionConfig) => getScalarDimensionFromData(this.data, scalar), getText: (text: TextDimensionConfig) => getTextDimensionFromData(this.data, text), getResource: (res: ResourceDimensionConfig) => getResourceDimensionFromData(this.data, res), getDirection: (direction: DirectionDimensionConfig) => getDirectionDimensionFromData(this.data, direction), getPanelData: () => this.data, }; ``` Each dimension getter returns a value accessor that can: - Return a fixed value - Look up a value from a data field - Apply thresholds/mappings ## Feature Toggle: canvasPanelPanZoom This feature toggle enables the pan and zoom functionality with significant architectural differences: ### DOM Structure Differences **Without Pan/Zoom:** ```html <div class="wrap" ref="div"> <!-- connections SVG --> <!-- root element --> </div> ``` **With Pan/Zoom:** ```html <div class="viewer" ref="viewerDiv"> <div class="viewport" ref="viewportDiv"> <!-- connections SVG --> <!-- root element --> </div> </div> ``` ### Component Variants | Component | Standard | Pan/Zoom | |-----------|----------|----------| | Connections | `Connections.tsx` | `Connections2.tsx` | | ConnectionAnchors | `ConnectionAnchors.tsx` | `ConnectionAnchors2.tsx` | | ConnectionSVG | `ConnectionSVG.tsx` | `ConnectionSVG2.tsx` | ### Key Differences in Behavior 1. **Coordinate calculations:** Account for zoom scale and scroll position 2. **Event handling:** InfiniteViewer intercepts some mouse events 3. **SVG sizing:** Connections SVG resized dynamically based on viewport 4. **Selection containers:** Selecto uses viewerDiv instead of div ## Moveable Custom Ables Custom Moveable "ables" extend element manipulation: ### dimensionViewable Displays element dimensions during resize. ### constraintViewable Shows visual constraint indicators (arrows/lines). ### settingsViewable Provides quick access to element settings button. ```typescript scene.moveable = new Moveable(container, { // ... standard options ables: [dimensionViewable, constraintViewable(scene), settingsViewable(scene)], props: { dimensionViewable: allowChanges, constraintViewable: allowChanges, settingsViewable: allowChanges, }, }); ``` ## Event Coordination Multiple systems need coordination to prevent conflicts: ### Drag Events 1. Selecto `dragStart` → Check for connection/vertex handles 2. Moveable `dragStart` → Set `ignoreDataUpdate = true` 3. Moveable `drag` → Apply transform, check connection updates 4. Moveable `dragEnd` → Calculate placement, save, reset flags ### Selection Events 1. Clear other canvas instances' selections 2. Update moveable targets 3. Broadcast via selection subject 4. Update panel instance state ### Edit Mode 1. Double-click activates edit mode for element 2. `editModeEnabled` BehaviorSubject broadcasts state 3. Moveable draggable disabled during edit 4. Element renders edit UI (e.g., field picker) ## Memory Management ### Subscription Cleanup ```typescript // CanvasPanel private subs = new Subscription(); componentDidMount() { this.subs.add(this.scene.selection.subscribe({...})); this.subs.add(this.scene.connections.selection.subscribe({...})); } componentWillUnmount() { this.scene.subscription.unsubscribe(); this.subs.unsubscribe(); } ``` ### Instance Tracking ```typescript let canvasInstances: CanvasPanel[] = []; componentDidMount() { canvasInstances.push(this); } componentWillUnmount() { canvasInstances = canvasInstances.filter(ci => ci.props.id !== this.props.id); } ``` ### Selecto Lifecycle ```typescript // Destroy and recreate on inline editing toggle if (inlineEditingSwitched) { this.scene.revId++; // Force new key for React } // In initMoveable: if (destroySelecto && scene.selecto) { scene.selecto.destroy(); } scene.selecto = new Selecto({...}); ```