# 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