DEV Community

vishalmysore
vishalmysore

Posted on

Building Interactive Data Visualizations in A2UI Angular: A Complete Guide

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...
}
Enter fullscreen mode Exit fullscreen mode

Key aspects:

  • Extends DynamicComponent: Provides access to component(), surfaceId(), weight() signals and processor for data resolution
  • TypeScript types: Uses Types.CustomNode from @a2ui/lit/0.8 for 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;
}
Enter fullscreen mode Exit fullscreen mode

The GraphNode interface:

  • Extends Types.CustomNode from A2UI's type system
  • Defines the structure expected by A2UI's MessageProcessor
  • Includes [k: string]: any index 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,
    }),
  ],
};
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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"
    }
  }
]
Enter fullscreen mode Exit fullscreen mode

Message flow:

  1. surfaceUpdate - Declares Graph components with data path bindings
  2. dataModelUpdate - Provides data in A2UI's adjacency list format
  3. 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);
Enter fullscreen mode Exit fullscreen mode

Angular Renderer:

provideA2UI({
  catalog: {
    Graph: () => import('./graph.component').then(m => m.GraphComponent)
  }
})
Enter fullscreen mode Exit fullscreen mode

Angular uses declarative configuration at app startup rather than runtime registration.

Benefits of This Approach

  1. Type Safety: Full TypeScript support with A2UI's type system
  2. Lazy Loading: Graph component only loads when needed
  3. No Workarounds: No manual component extraction or custom message handling
  4. Standard Pipeline: Uses A2UI's MessageProcessor for all data resolution
  5. Reactive: Automatically updates when data changes via Angular signals
  6. 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 }
}
Enter fullscreen mode Exit fullscreen mode

We convert this to Chart.js format:

[
  { x: 'Jan', y: 12500 },
  { x: 'Feb', y: 15800 }
]
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Testing

  1. Start dev server: ng serve
  2. Open http://localhost:4200
  3. Click "Test JSON" button
  4. Paste JSON from GraphDashboard-Backend.json
  5. Click "Render Test"
  6. 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)