Donut and Polar Area Charts with One Device Highlighted in Angular

Using ApexCharts with Angular

This Donut and Polar Area Charts with One Device Highlighted 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
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/features/highlight-filter';

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 CHANNELS: any = ['Organic', 'Paid search', 'Social', 'Email', 'Referral'];

  private SESSIONS: any = [4200, 2600, 1900, 1100, 700];

  private SESSIONS_BY_DEVICE: any = {
        mobile: [2500, 1300, 1500, 400, 300],
        desktop: [1400, 1100, 300, 600, 330],
        tablet: [300, 200, 100, 100, 70],
      };

  private ORDERS: any = [300, 240, 160, 180, 120];

  private ORDERS_BY_DEVICE: any = {
        mobile: [140, 100, 100, 60, 45],
        desktop: [130, 120, 45, 100, 60],
        tablet: [30, 20, 15, 20, 15],
      };

  private ORDER_VALUE: any = [66, 85, 49, 76, 95];

  private ORDER_VALUE_BY_DEVICE: any = {
        mobile: [52, 70, 41, 60, 76],
        desktop: [81, 98, 66, 84, 109],
        tablet: [70, 85, 55, 80, 95],
      };

  private pickDevice = (device: any): any => {
        document.querySelectorAll('[data-device]').forEach(function (b) {
          b.classList.toggle('active', b.getAttribute('data-device') === device)
        })
        // A pick is an ordinary update; null clears it.
        ApexCharts.getChartByID('sessions').updateOptions({
          highlightFilter: { data: device ? this.SESSIONS_BY_DEVICE[device] : null },
        })
        ApexCharts.getChartByID('orders').updateOptions({
          highlightFilter: { data: device ? this.ORDERS_BY_DEVICE[device] : null },
        })
        ApexCharts.getChartByID('order-value').updateOptions({
          highlightFilter: { data: device ? this.ORDER_VALUE_BY_DEVICE[device] : null },
        })
      };

  private wireDevices = (): any => {
        document.querySelectorAll('[data-device]').forEach(function (b) {
          b.addEventListener('click', function () {
            this.pickDevice(b.getAttribute('data-device'))
          })
        })
      };

  public chartOptions: Partial<ChartOptions> = {
          series: this.SESSIONS,
          chart: {
            id: 'sessions',
            type: 'donut',
            height: 360,
            toolbar: { show: false },
          },
          labels: this.CHANNELS,
          colors: ['#0284c7', '#0f766e', '#f59e0b', '#e11d48', '#64748b'],
          plotOptions: {
            pie: {
              donut: {
                size: '58%',
                labels: {
                  show: true,
                  value: {
                    formatter: (v) => {
                      return Number(v).toLocaleString()
                    },
                  },
                  total: {
                    show: true,
                    label: 'Sessions',
                    formatter: (w) => {
                      return w.globals.seriesTotals
                        .reduce(function (a, b) {
                          return a + b
                        }, 0)
                        .toLocaleString()
                    },
                  },
                },
              },
            },
          },
          // No slice borders: the faded tints alone keep the slices apart.
          stroke: { width: 0 },
          dataLabels: { enabled: true },
          legend: { position: 'bottom' },
          title: { text: 'Sessions by channel', style: { fontSize: '13px' } },
          tooltip: {
            y: {
              formatter: (v) => {
                return Number(v).toLocaleString()
              },
            },
          },
          highlightFilter: { data: this.SESSIONS_BY_DEVICE.mobile },
        };

  public chartOptions2: Partial<ChartOptions> = {
          series: this.ORDERS,
          chart: {
            id: 'orders',
            type: 'polarArea',
            height: 360,
            toolbar: { show: false },
          },
          labels: this.CHANNELS,
          colors: ['#0284c7', '#0f766e', '#f59e0b', '#e11d48', '#64748b'],
          fill: { opacity: 0.85 },
          stroke: { width: 0 },
          legend: { position: 'bottom' },
          title: { text: 'Orders by channel', style: { fontSize: '13px' } },
          highlightFilter: { data: this.ORDERS_BY_DEVICE.mobile },
        };

  public chartOptions3: Partial<ChartOptions> = {
          series: this.ORDER_VALUE,
          chart: {
            id: 'order-value',
            type: 'polarArea',
            height: 360,
            toolbar: { show: false },
          },
          labels: this.CHANNELS,
          colors: ['#0284c7', '#0f766e', '#f59e0b', '#e11d48', '#64748b'],
          fill: { opacity: 0.85 },
          stroke: { width: 0 },
          yaxis: {
            labels: {
              formatter: (v) => {
                return '$' + v
              },
            },
          },
          legend: { position: 'bottom' },
          title: {
            text: 'Average order value by channel',
            style: { fontSize: '13px' },
          },
          tooltip: {
            y: {
              formatter: (v) => {
                return '$' + v
              },
            },
          },
          highlightFilter: { data: this.ORDER_VALUE_BY_DEVICE.mobile },
        };
  ngAfterViewInit() {
    (window as any).ApexCharts.setLicense('APEX-eyJleHBpcnlEYXRlIjoiMjEyNi0wNy0wNCIsImlzc3VlRGF0ZSI6IjIwMjYtMDctMjgiLCJwbGFuIjoicHJlbWl1bSIsImRvbWFpbnMiOlsiYXBleGNoYXJ0cy5jb20iLCIxMjcuMC4wLjEiLCJsb2NhbGhvc3QiXSwic2lnIjoieVBmb1VCc0Z3TU9ZdUEyaEZkR0I2Y1FtZ0JITUtXcVdJSjB2NVRESXRZbFR3eDJMUmh6R2x0RUc3VXJ4X0s3b25ZMWRZb2Z2VGItN01ydFYyNDVyOWcifQ==');
    this.wireDevices()

    function pickDevice(device) {
          document.querySelectorAll('[data-device]').forEach(function (b) {
            b.classList.toggle('active', b.getAttribute('data-device') === device)
          })
          // A pick is an ordinary update; null clears it.
          ApexCharts.getChartByID('sessions').updateOptions({
            highlightFilter: { data: device ? this.SESSIONS_BY_DEVICE[device] : null },
          })
          ApexCharts.getChartByID('orders').updateOptions({
            highlightFilter: { data: device ? this.ORDERS_BY_DEVICE[device] : null },
          })
          ApexCharts.getChartByID('order-value').updateOptions({
            highlightFilter: { data: device ? this.ORDER_VALUE_BY_DEVICE[device] : null },
          })
        }
    function wireDevices() {
          document.querySelectorAll('[data-device]').forEach(function (b) {
            b.addEventListener('click', () => {
              this.pickDevice(b.getAttribute('data-device'))
            })
          })
        }
  }

  ngOnDestroy() {
    // no cleanup needed
  }
}