# Canvas Element System
This document details the Canvas element system, including the element registry, element types, and how to create custom elements.
## Element Registry
All canvas elements are registered in `public/app/features/canvas/registry.ts`:
```typescript
import { Registry } from '@grafana/data';
import { CanvasElementItem } from './element';
export const canvasElementRegistry = new Registry<CanvasElementItem>(() => [
...defaultElementItems,
...advancedElementItems,
]);
```
### Default Elements
| Element ID | Name | Description |
|------------|------|-------------|
| `metric-value` | Metric Value | Display a field value with configurable styling |
| `text` | Text | Static or data-driven text display |
| `ellipse` | Ellipse | Circle/ellipse shape with customizable fill |
| `rectangle` | Rectangle | Rectangle shape (formerly "text-box") |
| `icon` | Icon | SVG icon from Grafana icon library |
| `server` | Server | Server visualization with status indicators |
| `triangle` | Triangle | Triangle shape |
| `cloud` | Cloud | Cloud shape |
| `parallelogram` | Parallelogram | Parallelogram shape |
### Advanced/Experimental Elements
These require `showAdvancedTypes` option enabled:
| Element ID | Name | Description |
|------------|------|-------------|
| `button` | Button | Clickable button with API actions |
| `windTurbine` | Wind Turbine | Animated wind turbine visualization |
| `droneTop` | Drone Top | Drone from above with animated propellers |
| `droneFront` | Drone Front | Drone from front view |
| `droneSide` | Drone Side | Drone from side view |
## CanvasElementItem Interface
Every element must implement this interface:
```typescript
interface CanvasElementItem<TConfig = any, TData = any> extends RegistryItem {
// Registry item properties
id: string;
name: string;
description: string;
// Default dimensions when adding new element
defaultSize?: Placement;
// Whether element supports inline edit mode (double-click)
hasEditMode?: boolean;
// React component to render the element
display: ComponentType<CanvasElementProps<TConfig, TData>>;
// Generate default options for new elements
getNewOptions: (options?: CanvasElementOptions) =>
Omit<CanvasElementOptions<TConfig>, 'type' | 'name'>;
// Transform data into element-specific format
prepareData?: (
dimensionContext: DimensionContext,
elementOptions: CanvasElementOptions<TConfig>
) => TData;
// Register element-specific option editors
registerOptionsUI?: PanelOptionsSupplier<CanvasElementOptions<TConfig>>;
// Custom anchor points for connections (default: 4 corners + 4 midpoints)
customConnectionAnchors?: Array<{x: number; y: number}>;
// Configure which standard editors to show
standardEditorConfig?: StandardEditorConfig;
}
```
## Element Props
Elements receive these props when rendered:
```typescript
interface CanvasElementProps<TConfig = unknown, TData = unknown> {
// Element-specific configuration
config: TConfig;
// Processed data from prepareData()
data?: TData;
// Whether element is currently selected
isSelected?: boolean;
}
```
## Creating a Custom Element
### Step 1: Define Configuration Types
```typescript
// In your element file
interface MyElementConfig {
text?: TextDimensionConfig;
color?: ColorDimensionConfig;
size?: number;
}
interface MyElementData {
displayText: string;
backgroundColor: string;
fontSize: number;
}
```
### Step 2: Create Display Component
```typescript
const MyElementDisplay = (props: CanvasElementProps<MyElementConfig, MyElementData>) => {
const { data, config, isSelected } = props;
const styles = useStyles2(getStyles(data));
return (
<div className={styles.container}>
<span>{data?.displayText ?? 'No data'}</span>
</div>
);
};
const getStyles = (data: MyElementData | undefined) => (theme: GrafanaTheme2) => ({
container: css({
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: data?.backgroundColor,
fontSize: `${data?.fontSize}px`,
}),
});
```
### Step 3: Implement prepareData
```typescript
const prepareData = (
ctx: DimensionContext,
options: CanvasElementOptions<MyElementConfig>
): MyElementData => {
const config = options.config ?? {};
return {
displayText: config.text ? ctx.getText(config.text).value() : '',
backgroundColor: config.color ? ctx.getColor(config.color).value() : 'transparent',
fontSize: config.size ?? 14,
};
};
```
### Step 4: Define Element Item
```typescript
export const myElementItem: CanvasElementItem<MyElementConfig, MyElementData> = {
id: 'my-element',
name: 'My Element',
description: 'A custom canvas element',
display: MyElementDisplay,
defaultSize: {
width: 150,
height: 50,
},
hasEditMode: false,
getNewOptions: (options) => ({
...options,
config: {
text: { mode: TextDimensionMode.Fixed, fixed: 'Hello' },
color: { fixed: '#ffffff' },
size: 14,
},
background: {
color: { fixed: defaultBgColor },
},
placement: {
width: options?.placement?.width ?? 150,
height: options?.placement?.height ?? 50,
top: options?.placement?.top ?? 100,
left: options?.placement?.left ?? 100,
},
}),
prepareData: prepareData,
registerOptionsUI: (builder) => {
const category = ['My Element'];
builder
.addCustomEditor({
category,
id: 'config.text',
path: 'config.text',
name: 'Text',
editor: TextDimensionEditor,
})
.addCustomEditor({
category,
id: 'config.color',
path: 'config.color',
name: 'Color',
editor: ColorDimensionEditor,
})
.addNumberInput({
category,
path: 'config.size',
name: 'Font Size',
defaultValue: 14,
});
},
};
```
### Step 5: Register the Element
Add to `registry.ts`:
```typescript
import { myElementItem } from './elements/myElement';
export const defaultElementItems = [
// ... existing items
myElementItem,
];
```
## SVG-Based Elements
Some elements (triangle, cloud, parallelogram, ellipse) are SVG-based and require special handling:
### Identifying SVG Elements
```typescript
// In element.tsx
export const SVGElements = new Set<string>(['parallelogram', 'triangle', 'cloud', 'ellipse']);
```
### Custom Connection Anchors
SVG elements often need custom anchor points:
```typescript
export const triangleItem: CanvasElementItem = {
// ...
customConnectionAnchors: [
{ x: 0, y: 1 }, // Top vertex
{ x: -1, y: -1 }, // Bottom-left
{ x: 1, y: -1 }, // Bottom-right
{ x: -0.5, y: 0 }, // Left edge midpoint
{ x: 0.5, y: 0 }, // Right edge midpoint
{ x: 0, y: -1 }, // Bottom midpoint
],
};
```
### Style Handling Differences
SVG elements have their data styles applied to the SVG itself, not the wrapper div:
```typescript
// In applyLayoutStylesToDiv()
if (!SVGElements.has(elementType)) {
// Apply styles to div
applyStyles(this.dataStyle, this.div);
} else {
// Clean data styles from div for SVG elements
removeStyles(this.dataStyle, this.div);
}
```
## Element Edit Mode
Elements with `hasEditMode: true` can implement inline editing:
```typescript
const MetricValueDisplay = (props: CanvasElementProps<TextConfig, TextData>) => {
const { data, isSelected, config } = props;
const context = usePanelContext();
const scene = context.instanceState?.scene;
const isEditMode = useObservable<boolean>(scene?.editModeEnabled ?? of(false));
// Show edit UI when in edit mode and selected
if (isEditMode && isSelected) {
return <MetricValueEdit {...props} />;
}
// Otherwise show display UI
return <MetricValueDisplay {...props} />;
};
```
Edit mode is triggered by double-clicking the element:
```typescript
// In sceneAbleManagement.ts
.on('click', (event) => {
const targetedElement = findElementByTarget(event.target, scene.root.elements);
let elementSupportsEditing = targetedElement?.item.hasEditMode ?? false;
if (event.isDouble && allowChanges && !scene.editModeEnabled.getValue() && elementSupportsEditing) {
scene.editModeEnabled.next(true);
}
});
```
## Standard Editor Config
Control which standard editors appear:
```typescript
interface StandardEditorConfig {
background?: boolean | StandardEditorOptionsConfig;
border?: boolean | StandardEditorOptionsConfig;
}
interface StandardEditorOptionsConfig {
disabled?: boolean;
}
```
Example usage:
```typescript
export const cloudItem: CanvasElementItem = {
// ...
standardEditorConfig: {
background: false, // Hide background editor (SVG handles its own)
},
};
```
## Element Lifecycle
### Creation
```typescript
// In utils.ts: onAddItem()
export function onAddItem(sel: SelectableValue<string>, rootLayer: FrameState, anchorPoint?: AnchorPoint) {
const newItem = canvasElementRegistry.getIfExists(sel.value) ?? notFoundItem;
const newElementOptions = {
...newItem.getNewOptions(),
type: newItem.id,
name: '', // Will be assigned by Scene
};
if (anchorPoint) {
newElementOptions.placement = { ...newElementOptions.placement, top: anchorPoint.y, left: anchorPoint.x };
}
const newElement = new ElementState(newItem, newElementOptions, rootLayer);
newElement.updateData(rootLayer.scene.context);
rootLayer.elements.push(newElement);
rootLayer.scene.save();
rootLayer.reinitializeMoveable();
}
```
### Update
```typescript
// ElementState.updateData()
updateData(ctx: DimensionContext) {
// Call element-specific prepareData
if (this.item.prepareData) {
this.data = this.item.prepareData(ctx, this.options);
this.revId++;
}
// Process links and actions
// ...
// Calculate data-driven styles
const { background, border } = this.options;
const css: CSSProperties = {};
// ... process background and border
this.dataStyle = css;
this.applyLayoutStylesToDiv();
}
```
### Deletion
```typescript
// FrameState.doAction()
case LayerActionID.Delete:
this.elements = this.elements.filter((e) => e !== element);
updateConnectionsForSource(element, this.scene); // Clean up connections
this.scene.byName.delete(element.options.name);
this.scene.save();
this.reinitializeMoveable();
break;
```
## Element Examples by Category
### Shape Elements
Basic geometric shapes that support:
- Background color (solid or data-driven)
- Border (color, width, radius)
- Text content (optional)
Examples: `rectangle`, `ellipse`, `triangle`, `cloud`, `parallelogram`
### Data Display Elements
Elements specifically designed to show data values:
- `metric-value`: Displays field values with formatting
- `text`: Static or dynamic text with styling options
### Interactive Elements
Elements with click/action behavior:
- `button`: API calls, navigation, custom actions
- All elements support data links
### Visualization Elements
Domain-specific visualizations:
- `server`: Network/infrastructure monitoring
- `windTurbine`: Energy/industrial monitoring
- `drone*`: Drone/UAV monitoring
## Best Practices
1. **Use prepareData:** Transform data once rather than in render
2. **Handle missing data:** Always provide fallback displays
3. **Support dimensions:** Use DimensionContext for data binding
4. **Keep renders pure:** Avoid side effects in display components
5. **Implement sensible defaults:** getNewOptions should create usable elements
6. **Consider accessibility:** Add appropriate ARIA attributes
7. **Test with streaming data:** Ensure elements handle frequent updates