# 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({...});
```