Life Expectancy Swarm in Angular

Using ApexCharts with Angular

This Life Expectancy Swarm example wires ApexCharts into Angular through the ng-apexcharts component.

Install it with npm install ng-apexcharts apexcharts, then mount the chart with <apx-chart [series]="series" [chart]="chart"></apx-chart>.

Angular charts guide

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';

import 'apexcharts/unit';

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 REGIONS: any = [
        { name: 'Europe', color: '#0EA5E9' },
        { name: 'Americas', color: '#22C55E' },
        { name: 'Asia', color: '#F59E0B' },
        { name: 'Africa', color: '#E11D48' },
      ];

  private DATA: any = {
        Europe: [
          ['Norway', 73.6, 83.3],
          ['Sweden', 73.0, 82.4],
          ['Switzerland', 71.3, 83.1],
          ['Netherlands', 73.4, 81.4],
          ['United Kingdom', 71.1, 80.9],
          ['France', 70.2, 82.3],
          ['Germany', 69.3, 81.1],
          ['Italy', 69.1, 82.7],
          ['Spain', 69.1, 82.3],
          ['Greece', 68.2, 81.1],
          ['Portugal', 62.8, 81.1],
          ['Poland', 67.7, 76.6],
          ['Russia', 66.1, 71.3],
          ['Ukraine', 68.3, 71.6],
        ],
        Americas: [
          ['Canada', 71.1, 81.7],
          ['United States', 69.8, 78.5],
          ['Cuba', 64.2, 78.8],
          ['Costa Rica', 60.0, 80.3],
          ['Chile', 57.4, 80.2],
          ['Argentina', 65.2, 76.5],
          ['Mexico', 57.1, 75.0],
          ['Colombia', 56.7, 77.3],
          ['Peru', 49.5, 76.5],
          ['Brazil', 54.2, 74.0],
          ['Guatemala', 46.2, 74.3],
          ['Bolivia', 42.1, 71.5],
        ],
        Asia: [
          ['Japan', 67.7, 84.6],
          ['Israel', 71.5, 82.9],
          ['South Korea', 55.4, 83.5],
          ['China', 43.7, 78.1],
          ['Thailand', 55.0, 78.7],
          ['Iran', 45.0, 77.3],
          ['Turkey', 46.0, 77.7],
          ['Vietnam', 59.0, 75.4],
          ['Saudi Arabia', 45.7, 75.1],
          ['Bangladesh', 45.9, 72.6],
          ['Indonesia', 48.3, 71.7],
          ['India', 45.2, 70.9],
          ['Philippines', 59.0, 71.2],
          ['Pakistan', 45.3, 67.3],
        ],
        Africa: [
          ['Algeria', 46.1, 76.9],
          ['Morocco', 48.5, 76.7],
          ['Egypt', 48.0, 71.8],
          ['Senegal', 38.2, 67.9],
          ['Kenya', 46.3, 66.7],
          ['Ethiopia', 38.4, 66.6],
          ['Tanzania', 43.8, 65.5],
          ['South Africa', 52.8, 64.9],
          ['Ghana', 45.8, 64.1],
          ['Uganda', 44.0, 63.4],
          ['Zimbabwe', 54.0, 61.5],
          ['DR Congo', 41.0, 60.7],
          ['Nigeria', 37.0, 55.0],
          ['Rwanda', 42.0, 69.0],
        ],
      };

  private seriesFor = (yearIdx: any): any => {
        return this.REGIONS.map(function (r) {
          return {
            name: r.name,
            data: this.DATA[r.name].map(function (row) {
              return { name: row[0], value: row[yearIdx] }
            }),
          }
        })
      };

  private YEARS: any = {
        1960: {
          label: '1960',
          caption:
            'In 1960 the world was starkly split: much of Africa and Asia lived to barely 45, while Europe already reached past 70.',
          series: this.seriesFor(1),
        },
        2020: {
          label: '2020',
          caption:
            'Sixty years on the swarms have surged right and converged: most of the world now lives past 70, though Africa still trails and spreads the widest.',
          series: this.seriesFor(2),
        },
      };

  private buttons: any = document.querySelectorAll('#year-nav button');

  private caption: any = document.getElementById('caption');

  private showYear = (key: any): any => {
          this.buttons.forEach(function (b) {
            b.classList.toggle('active', b.getAttribute('data-year') === key)
          })
          this.caption.textContent = this.YEARS[key].caption
          this.chart.updateSeries(this.YEARS[key].series)
        };

  public chartOptions: Partial<ChartOptions> = {
          series: this.YEARS['1960'].series,
          chart: {
            id: 'lifeSwarm',
            type: 'unit',
            height: 540,
            animations: {
              enabled: true,
              speed: 800,
            },
          },
          colors: this.REGIONS.map(function (r) {
            return r.color
          }),
          legend: {
            position: 'bottom',
          },
          plotOptions: {
            unit: {
              layout: 'scatter',
              size: 4.2,
              scatter: {
                spread: 'swarm',
                // Pin the axis so both years share it (the swarms glide right, rather
                // than the axis rescaling under them). 35..85 -> ticks every 10.
                xMin: 35,
                xMax: 85,
                xTitle: 'Life expectancy at birth (years)',
                tickAmount: 6,
              },
            },
          },
          tooltip: {
            enabled: true,
          },
        };
  ngAfterViewInit() {
    (window as any).ApexCharts.setLicense('APEX-eyJleHBpcnlEYXRlIjoiMjEyNi0wNy0wNCIsImlzc3VlRGF0ZSI6IjIwMjYtMDctMjgiLCJwbGFuIjoicHJlbWl1bSIsImRvbWFpbnMiOlsiYXBleGNoYXJ0cy5jb20iLCIxMjcuMC4wLjEiLCJsb2NhbGhvc3QiXSwic2lnIjoieVBmb1VCc0Z3TU9ZdUEyaEZkR0I2Y1FtZ0JITUtXcVdJSjB2NVRESXRZbFR3eDJMUmh6R2x0RUc3VXJ4X0s3b25ZMWRZb2Z2VGItN01ydFYyNDVyOWcifQ==');

    function showYear(key) {
            this.buttons.forEach(function (b) {
              b.classList.toggle('active', b.getAttribute('data-year') === key)
            })
            this.caption.textContent = this.YEARS[key].caption
            this.chart.updateSeries(this.YEARS[key].series)
          }
    this.buttons.forEach(function (btn) {
            btn.addEventListener('click', () => {
              this.showYear(btn.getAttribute('data-year'))
            })
          })
  }

  ngOnDestroy() {
    // no cleanup needed
  }
}