This demo uses imperative chart updates. The generated code is a faithful Angular translation: open it in CodeSandbox to run and tweak.
import { Component, AfterViewInit, OnDestroy, ViewChild } from '@angular/core';
import {
ChartComponent,
ApexAxisChartSeries,
ApexNonAxisChartSeries,
ApexChart,
ApexXAxis,
ApexYAxis,
ApexTitleSubtitle,
ApexDataLabels,
ApexStroke,
ApexFill,
ApexLegend,
ApexTooltip,
ApexMarkers,
ApexPlotOptions,
ApexResponsive,
ApexGrid,
ApexAnnotations,
ApexStates,
ApexTheme,
NgApexchartsModule,
} from 'ng-apexcharts';
export type ChartOptions = {
series?: ApexAxisChartSeries | ApexNonAxisChartSeries;
chart?: ApexChart;
xaxis?: ApexXAxis;
yaxis?: ApexYAxis | ApexYAxis[];
title?: ApexTitleSubtitle;
subtitle?: ApexTitleSubtitle;
dataLabels?: ApexDataLabels;
stroke?: ApexStroke;
fill?: ApexFill;
legend?: ApexLegend;
tooltip?: ApexTooltip;
markers?: ApexMarkers;
plotOptions?: ApexPlotOptions;
responsive?: ApexResponsive[];
grid?: ApexGrid;
annotations?: ApexAnnotations;
states?: ApexStates;
theme?: ApexTheme;
colors?: string[];
labels?: any;
};
@Component({
selector: 'app-chart',
standalone: true,
imports: [NgApexchartsModule],
templateUrl: './chart.component.html',
})
export class AppChart implements AfterViewInit, OnDestroy {
@ViewChild('chart') chart!: ChartComponent;
private REV: any = [12, 14, 9, 15, 18, 22, 26, 31, 37, 44, 52, 61];
private MONTHS: any = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
];
private ACCENT: any = '#0EA5E9';
private FADE: any = '#e3e8f4';
private RED: any = '#f87171';
private QCOLORS: any = ['#0EA5E9', '#7dd3fc', '#4ade80', '#fbbf24'];
private colors12 = (fn: any): any => {
var a = []
for (var i = 0; i < 12; i++) a.push(fn(i))
return a
};
private C_ALL: any = this.colors12(function () {
return this.ACCENT
});
private C_OUTAGE: any = this.colors12(function (i) {
if (i === 2) return this.RED // March
return i >= 1 && i <= 3 ? this.ACCENT : this.FADE
});
private C_V2: any = this.colors12(function (i) {
return i >= 4 ? this.ACCENT : this.FADE
});
private C_QUARTERS: any = this.colors12(function (i) {
return this.QCOLORS[Math.floor(i / 3)]
});
private COLUMN_OPTS: any = {
chart: { type: 'bar' },
series: [{ name: 'Revenue (k$)', data: this.REV }],
labels: [],
stroke: { width: 0, colors: undefined },
plotOptions: {
bar: { columnWidth: '58%', borderRadius: 3, distributed: true },
},
legend: { show: false },
};
private columnBeat = (colors: any): any => {
return Object.assign({}, this.COLUMN_OPTS, { colors: colors })
};
private DONUT_OPTS: any = {
chart: { type: 'donut' },
series: this.REV,
labels: this.MONTHS,
colors: this.C_QUARTERS,
stroke: { width: 2, colors: ['#fff'] },
legend: { show: false },
plotOptions: {
pie: {
expandOnClick: false,
donut: {
size: '68%',
labels: {
show: true,
total: {
show: true,
label: 'FY25 revenue',
formatter: function () {
return '341 k$'
},
},
},
},
},
},
};
private BEATS: any = [
{
selector: '#sb-step-1',
view: { window: { xaxis: null, yaxis: [null] }, theme: { mode: 'light' } },
this.chartOptions: this.columnBeat(this.C_ALL),
announce: 'Overview: fiscal 2025 revenue, month by month',
},
{
selector: '#sb-step-2',
view: {
window: { xaxis: null, yaxis: [null] },
theme: { mode: 'light' },
annotations: {
static: {
xaxis: [
{
x: 'Mar',
strokeDashArray: 4,
borderColor: '#f87171',
label: {
text: 'The outage',
borderColor: '#fca5a5',
style: { color: '#7f1d1d', background: '#fee2e2' },
},
},
],
},
},
},
this.chartOptions: this.columnBeat(this.C_OUTAGE),
announce: 'Spotlight on March: the outage month',
},
{
selector: '#sb-step-3',
view: {
window: { xaxis: null, yaxis: [null] },
theme: { mode: 'light' },
annotations: {
static: {
xaxis: [
{
x: 'May',
strokeDashArray: 4,
borderColor: '#4ade80',
label: {
text: 'v2 ships',
borderColor: '#86efac',
style: { color: '#14532d', background: '#dcfce7' },
},
},
],
},
},
},
this.chartOptions: this.columnBeat(this.C_V2),
announce: 'Spotlight from May on: v2 changes the slope',
},
{
selector: '#sb-step-4',
view: { window: { xaxis: null, yaxis: [null] }, theme: { mode: 'light' } },
this.chartOptions: this.columnBeat(this.C_QUARTERS),
announce: 'The months grouped into quarters by color',
},
{
selector: '#sb-step-5',
view: { window: { xaxis: null, yaxis: [null] }, theme: { mode: 'light' } },
this.chartOptions: this.DONUT_OPTS,
announce: 'The twelve months as a donut ring',
},
];
private steps: any = document.querySelectorAll('.sb-step');
private dots: any = document.querySelectorAll('.sb-dot');
private chip: any = document.getElementById('sb-chip');
private prevBtn: any = document.getElementById('sb-prev');
private nextBtn: any = document.getElementById('sb-next');
private scroller: any = document.getElementById('sb-scroller');
private current: any = 0;
private scrollToBeat = (i: any): any => {
var step = document.getElementById('sb-step-' + (i + 1))
if (!step || !this.scroller) return
var sRect = step.getBoundingClientRect()
var cRect = this.scroller.getBoundingClientRect()
this.scroller.scrollTop +=
sRect.top - cRect.top - (this.scroller.clientHeight - step.clientHeight) / 2
};
private goToBeat = (i: any): any => {
i = Math.max(0, Math.min(this.BEATS.length - 1, i))
this.chart.storyboard.goTo(i)
this.scrollToBeat(i)
};
public chartOptions: Partial<ChartOptions> = {
series: [
{
name: 'Revenue (k$)',
data: [12, 14, 9, 15, 18, 22, 26, 31, 37, 44, 52, 61],
},
],
chart: {
id: 'storyChart',
type: 'bar',
height: 300,
fontFamily: 'Helvetica, Arial, sans-serif',
animations: { speed: 700, dynamicAnimation: { speed: 500 } },
toolbar: { show: false },
zoom: { enabled: false },
},
colors: [
'#0EA5E9',
'#0EA5E9',
'#0EA5E9',
'#0EA5E9',
'#0EA5E9',
'#0EA5E9',
'#0EA5E9',
'#0EA5E9',
'#0EA5E9',
'#0EA5E9',
'#0EA5E9',
'#0EA5E9',
],
plotOptions: {
bar: { columnWidth: '58%', borderRadius: 3, distributed: true },
},
stroke: { width: 0 },
dataLabels: { enabled: false },
legend: { show: false },
grid: { borderColor: '#eef0f6' },
xaxis: {
categories: [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
],
},
};
ngAfterViewInit() {
(window as any).ApexCharts.setLicense('APEX-eyJleHBpcnlEYXRlIjoiMjEyNi0wNy0wNCIsImlzc3VlRGF0ZSI6IjIwMjYtMDctMjgiLCJwbGFuIjoicHJlbWl1bSIsImRvbWFpbnMiOlsiYXBleGNoYXJ0cy5jb20iLCIxMjcuMC4wLjEiLCJsb2NhbGhvc3QiXSwic2lnIjoieVBmb1VCc0Z3TU9ZdUEyaEZkR0I2Y1FtZ0JITUtXcVdJSjB2NVRESXRZbFR3eDJMUmh6R2x0RUc3VXJ4X0s3b25ZMWRZb2Z2VGItN01ydFYyNDVyOWcifQ==');
this.chart.addEventListener('beatChange', (c, info) => {
this.current = info.index
this.steps.forEach(function (el, i) {
el.classList.toggle('is-active', i === info.index)
})
this.dots.forEach(function (dot, i) {
dot.classList.toggle('is-active', i === info.index)
})
this.chip.textContent = 'Beat ' + (info.index + 1) + ' of ' + this.BEATS.length
this.prevBtn.disabled = info.index === 0
this.nextBtn.disabled = info.index === this.BEATS.length - 1
})
this.dots.forEach(function (dot, i) {
dot.addEventListener('click', () => {
this.goToBeat(i)
})
})
this.prevBtn.addEventListener('click', () => {
this.goToBeat(this.current - 1)
})
this.nextBtn.addEventListener('click', () => {
this.goToBeat(this.current + 1)
})
window.addEventListener('load', () => {
// scroller binds the observer to the story column, so the story is driven by
// that panel's own scroll (not the page/iframe viewport).
this.chart.storyboard.bind({ beats: this.BEATS, scroller: '#sb-scroller' })
})
}
ngOnDestroy() {
// no cleanup needed
}
}