---
url: https://chartbuddy.io/embed/docs/guides/angular.md
---
# Angular

Angular uses the `<chartbuddy-insight>` **custom element** rather than a compiled
Angular library. Angular binds properties and listens to events on custom
elements natively, so you get the same lifecycle guarantees as the React and Vue
bindings.

```bash
npm install @chartbuddy.io/embed
```

No extra peer dependency — the element is plain DOM.

## Why an element instead of an Angular package

A native Angular library has to be compiled with Angular's own toolchain and
published against a specific Angular major, which would tie your ChartBuddy
upgrades to your Angular upgrades. The element has no such coupling and works
unchanged across Angular versions. If you would rather have typed Angular
inputs, the [thin wrapper](#optional-typed-wrapper-component) below is about
twenty lines and lives in your codebase, where it can follow your Angular version.

## Setup

Import once — importing registers the element — and allow custom element tags in
the components that use it.

```ts
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import '@chartbuddy.io/embed/element';

@Component({
  selector: 'app-revenue',
  standalone: true,
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `
    <chartbuddy-insight
      [chartData]="chartData"
      instance-id="revenue"
      style="display:block; height:400px"
      (change)="onChange($event)"
    ></chartbuddy-insight>
  `,
})
export class RevenueComponent {
  chartData = {
    chartType: 'clusteredBar',
    isDataTransposed: true,
    seriesData: [['', 'Q1', 'Q2'], ['Revenue', 100, 112]],
  };

  onChange(event: Event) {
    const cd = (event as CustomEvent).detail;
    // …
  }
}
```

For an NgModule app, put `CUSTOM_ELEMENTS_SCHEMA` in the module's `schemas`
instead.

The chart fills its container, so **give it a height**.

## Properties vs attributes

`chartData` is an object, so it must be set as a **property** — `[chartData]="…"`
with square brackets. Angular sets DOM properties for bracket bindings on custom
elements, so this works as written.

| Option | Binding |
|---|---|
| `chartData` | `[chartData]="cd"` — property only |
| `instanceId` | `instance-id="revenue"` or `[instanceId]="id"` |
| `editable` | `editable` / `[editable]="true"` |
| `assetBase` | `asset-base="https://…/"` |

Assigning a new object to `chartData` patches the live chart via `update()`.
Changing any of the others remounts it. A burst of attribute changes in the same
tick is coalesced into a single remount.

## Mutation is not detected

`chartData` is compared **by identity**. With Angular's default change detection
you must assign a new object:

```ts
// Not picked up
this.chartData.title.text = 'FY26';

// Picked up
this.chartData = { ...this.chartData, title: { text: 'FY26' } };
```

This also keeps you compatible with `OnPush`.

## Events

All four are `CustomEvent`s; read `event.detail`.

| Event | `detail` |
|---|---|
| `ready` | The `Insight` instance |
| `change` | `ChartData \| null` |
| `mode` | `'view' \| 'edit'` |
| `error` | `Error` — a `ChartDataValidationError` for invalid config |

Note that `error` is a `CustomEvent` named `error`, not an `ErrorEvent`, and it
does not bubble.

## Imperative access

Grab the element with a template ref and read `.insight`:

```ts
@ViewChild('chart') chartRef!: ElementRef<HTMLElement & { insight: any; ready: boolean }>;

async downloadPng() {
  const { insight } = this.chartRef.nativeElement;
  if (insight) await insight.downloadPng();
}
```

```html
<chartbuddy-insight #chart [chartData]="chartData" style="display:block; height:400px" />
```

`insight` is `null` until the mount resolves; `ready` tells you when it has.

## Optional: typed wrapper component

If you want Angular-native inputs and outputs, wrap the element once:

```ts
import {
  Component,
  CUSTOM_ELEMENTS_SCHEMA,
  EventEmitter,
  Input,
  Output,
} from '@angular/core';
import '@chartbuddy.io/embed/element';
import type { ChartData, ChartDataInput } from '@chartbuddy.io/embed';

@Component({
  selector: 'cb-insight',
  standalone: true,
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `
    <chartbuddy-insight
      [chartData]="chartData"
      [instanceId]="instanceId"
      [editable]="editable"
      style="display:block; height:100%"
      (change)="change.emit($any($event).detail)"
      (error)="error.emit($any($event).detail)"
    ></chartbuddy-insight>
  `,
})
export class CbInsightComponent {
  @Input() chartData?: ChartDataInput;
  @Input() instanceId?: string;
  @Input() editable = false;
  @Output() change = new EventEmitter<ChartData | null>();
  @Output() error = new EventEmitter<Error>();
}
```

Then `<cb-insight [chartData]="cd" (change)="save($event)" />` with no schema
boilerplate at each call site.

## Handling invalid data

Chart data is validated at the API boundary. Invalid config fires the `error`
event instead of rendering a blank chart:

```ts
import { ChartDataValidationError, validateChartData } from '@chartbuddy.io/embed';

onError(event: Event) {
  const err = (event as CustomEvent).detail;
  if (err instanceof ChartDataValidationError) {
    this.issues = err.errors; // [{ path, code, expected, suggestion }, …]
  }
}
```

You can also check before binding, with no chart involved:

```ts
const { valid, errors } = validateChartData(candidate);
```

See [Validation](/quality-assurance/validation).

## Dashboards

One element per chart, each with a stable `instance-id`. Duplicate ids on a page
throw, so derive them from your data rather than the loop index.

```html
@for (panel of panels; track panel.id) {
  <chartbuddy-insight
    [chartData]="panel.chartData"
    [instanceId]="panel.id"
    style="display:block; height:300px"
  ></chartbuddy-insight>
}
```

The engine loads once per page, not once per chart — but read
[Installation](/getting-started/installation#bundle-size-and-many-charts) before
putting a lot of charts on one screen.

## SSR

The element needs a real DOM, so don't import it on the server. Under Angular
Universal, import it inside a browser-only guard:

```ts
if (isPlatformBrowser(this.platformId)) {
  await import('@chartbuddy.io/embed/element');
}
```

There is no headless/Node render path today, so charts cannot be pre-rendered to
PNG on a server.

## Other frameworks

The same element works in Svelte, Solid, Lit, Astro, and plain HTML:

```html
<script type="module">
  import 'https://unpkg.com/@chartbuddy.io/embed/element.mjs';
  const el = document.querySelector('chartbuddy-insight');
  el.chartData = { chartType: 'pie', seriesData: [['Category', 'Value'], ['A', 3]] };
</script>

<chartbuddy-insight style="display:block; height:400px"></chartbuddy-insight>
```
