Discover how to create powerful, interactive charts and graphs in your A2UI Angular applications using Chart.js. This comprehensive guide walks you through implementing a custom Graph component that seamlessly integrates with A2UI's message-based rendering system, enabling agents to generate dynamic data visualizations while maintaining full architectural compliance.
Why Custom Graph Components Matter
The ability to display interactive charts, graphs, and dashboards is crucial. While A2UI provides a robust foundation for agent-driven UIs, it doesn't include built-in visualization components. This guide solves that challenge by showing you how to extend A2UI's catalog system with custom Chart.js components that can render line charts, bar graphs, pie charts, and more—all while preserving A2UI's security model and data binding architecture.
Demo https://vishalmysore.github.io/simplea2ui/
Code https://github.com/vishalmysore/simplea2ui
What You'll Learn
1 Create a custom Graph component that extends DynamicComponent
Register components using Angular's declarative catalog system
3 Handle A2UI's v0.8 data format conversion from adjacency lists to Chart.js-compatible arrays
3 Implement reactive updates using Angular's effect() for real-time chart updates
4 Maintain type safety with TypeScript interfaces extending Types.CustomNode
Implementation Steps
Step 1: Create Custom Graph Component
File: src/app/graph.component.ts
The component extends DynamicComponent<GraphNode> from @a2ui/angular:
@Component({
selector: 'app-graph',
standalone: true,
template: `
<div class="graph-container">
<canvas #chartCanvas></canvas>
</div>
`,
styles: [...]
})
export class GraphComponent extends DynamicComponent<GraphNode>
implements OnInit, OnDestroy {
// Implementation...
}
Key aspects:
-
Extends DynamicComponent: Provides access to
component(),surfaceId(),weight()signals andprocessorfor data resolution -
TypeScript types: Uses
Types.CustomNodefrom@a2ui/lit/0.8for type safety -
Reactive updates: Uses Angular's
effect()to react to component signal changes - Chart.js integration: Uses Chart.js library for actual rendering
Step 2: Define Type Interfaces
interface GraphProperties {
[k: string]: any; // Index signature for A2UI compatibility
data: any;
graphType?: string;
interactive?: boolean;
title?: string;
xLabel?: string;
yLabel?: string;
}
interface GraphNode extends Types.CustomNode {
type: 'Graph';
properties: GraphProperties;
}
The GraphNode interface:
- Extends
Types.CustomNodefrom A2UI's type system - Defines the structure expected by A2UI's MessageProcessor
- Includes
[k: string]: anyindex signature for compatibility
Step 3: Register in Component Catalog
File: src/app/app.config.ts
import { provideA2UI, DEFAULT_CATALOG } from '@a2ui/angular';
import { GraphComponent } from './graph.component';
export const appConfig: ApplicationConfig = {
providers: [
provideA2UI({
catalog: {
...DEFAULT_CATALOG,
Graph: () => import('./graph.component').then(m => m.GraphComponent)
},
theme: theme,
}),
],
};
Why this approach:
- Lazy loading: Uses dynamic import for optimal bundle size
- Extends DEFAULT_CATALOG: Preserves all standard A2UI components
- Angular-specific: Angular uses declarative catalog configuration vs. runtime registration
Step 4: Data Resolution Through MessageProcessor
The critical part - resolving data paths through A2UI's system:
private createChart() {
const comp = this.component();
const props = comp.properties;
const surfaceId = this.surfaceId();
let data: any[] = [];
if (props.data && typeof props.data === 'object' && 'path' in props.data) {
// Resolve through MessageProcessor
const resolvedData = this.processor.getData(comp, props.data.path, surfaceId || '');
// Convert A2UI v0.8 Map format to array
if (resolvedData instanceof Map) {
data = Array.from(resolvedData.values()).map((innerMap: any) => {
if (innerMap instanceof Map) {
const obj: any = {};
innerMap.forEach((value: any, key: string) => {
obj[key] = value;
});
return obj;
}
return innerMap;
});
}
}
// Create Chart.js chart with resolved data
this.chart = new Chart(ctx, config);
}
Key points:
- Uses
this.processor.getData()to resolve data paths - Handles A2UI's v0.8 data format (nested Maps)
- Converts to simple JavaScript objects for Chart.js
Step 5: Backend JSON Message Format
File: examples/GraphDashboard-Backend.json
The backend sends standard A2UI messages:
[
{
"surfaceUpdate": {
"surfaceId": "analytics_dashboard",
"components": [
{
"id": "monthly_sales_graph",
"component": {
"Graph": {
"data": { "path": "/monthlySales" },
"graphType": "line",
"title": "Monthly Sales (2025)",
"xLabel": "Month",
"yLabel": "Revenue ($)",
"interactive": true
}
}
}
]
}
},
{
"dataModelUpdate": {
"surfaceId": "analytics_dashboard",
"contents": [
{
"key": "monthlySales",
"valueArray": [
{
"key": "0",
"valueMap": [
{ "key": "x", "valueString": "Jan" },
{ "key": "y", "valueNumber": 12500 }
]
}
]
}
]
}
},
{
"beginRendering": {
"root": "root",
"surfaceId": "analytics_dashboard"
}
}
]
Message flow:
-
surfaceUpdate- Declares Graph components with data path bindings -
dataModelUpdate- Provides data in A2UI's adjacency list format -
beginRendering- Triggers rendering
Why This Architecture Works
Angular vs. Web Renderer Differences
Web Renderer (Lit):
import { componentRegistry } from './component-registry.js';
componentRegistry.register('Graph', A2uiGraphComponent);
Angular Renderer:
provideA2UI({
catalog: {
Graph: () => import('./graph.component').then(m => m.GraphComponent)
}
})
Angular uses declarative configuration at app startup rather than runtime registration.
Benefits of This Approach
- Type Safety: Full TypeScript support with A2UI's type system
- Lazy Loading: Graph component only loads when needed
- No Workarounds: No manual component extraction or custom message handling
- Standard Pipeline: Uses A2UI's MessageProcessor for all data resolution
- Reactive: Automatically updates when data changes via Angular signals
- Framework Aligned: Follows Angular's dependency injection philosophy
Data Format Conversion
A2UI v0.8 uses an adjacency list format with Maps:
Map {
'0' => Map { 'x' => 'Jan', 'y' => 12500 },
'1' => Map { 'x' => 'Feb', 'y' => 15800 }
}
We convert this to Chart.js format:
[
{ x: 'Jan', y: 12500 },
{ x: 'Feb', y: 15800 }
]
Supported Graph Types
-
line- Line charts -
bar- Bar charts -
pie- Pie charts -
doughnut- Doughnut charts -
radar- Radar charts -
polarArea- Polar area charts
Mobile Responsiveness
The component includes responsive styles:
.graph-container {
height: 400px;
}
@media (max-width: 768px) {
.graph-container {
height: 300px;
}
}
Chart.js automatically handles responsive resizing.
Debugging
Console logging shows the data flow:
GraphComponent - Creating chart: {componentId, surfaceId, props}
GraphComponent - Raw resolved data: Map(12) {...}
GraphComponent - Converted data: [{x: 'Jan', y: 12500}, ...]
GraphComponent - Final data array length: 12
GraphComponent - Chart data: {labels, values, graphType, title}
Complete File Structure
src/app/
├── graph.component.ts # Custom Graph component
├── app.config.ts # Catalog registration
└── theme.ts # (existing theme config)
examples/
├── GraphDashboard-Backend.json # Full dashboard example
└── SimpleGraph-Backend.json # Simple single graph example
Testing
- Start dev server:
ng serve - Open http://localhost:4200
- Click "Test JSON" button
- Paste JSON from
GraphDashboard-Backend.json - Click "Render Test"
- Verify graphs display with correct data
Conclusion
This implementation demonstrates how to create custom components for A2UI Angular that:
- Follow A2UI's architecture strictly
- Use the catalog system for component registration
- Resolve data through MessageProcessor
- Maintain type safety with TypeScript
- Work seamlessly with A2UI's message-based rendering
The key difference from the web renderer is using declarative catalog configuration instead of runtime registration, which aligns with Angular's architectural philosophy.
Top comments (0)