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 WATER: any = {
          values: [576, 168, 42, 34],
          labels: ['Safely managed', 'Basic', 'Limited', 'Unsafe'],
          colors: ['#008FFB', '#00B8D9', '#FFAB00', '#FF4560'],
        };

  private DROPLET: any = 'M50 4 C50 4 14 46 14 66 A36 36 0 0 0 86 66 C86 46 50 4 50 4 Z';

  private DROPLET_CORE: any = { x: 50, y: 60 };

  private DROPLET_REACH: any = 40;

  private _path: any = null;

  private dropletPath = (): any => {
          if (this._path) return this._path
          var NS = 'http://www.w3.org/2000/svg'
          var svg = document.createElementNS(NS, 'svg')
          svg.setAttribute('width', '0')
          svg.setAttribute('height', '0')
          svg.setAttribute(
            'style',
            'position:absolute;width:0;height:0;overflow:hidden',
          )
          var p = document.createElementNS(NS, 'path')
          p.setAttribute('d', this.DROPLET)
          svg.appendChild(p)
          // getTotalLength needs the element in the document to measure reliably.
          document.body.appendChild(svg)
          this._path = p
          return this._path
        };

  private silhouetteLayout = (objects: any, rect: any): any => {
          var path = this.dropletPath()
          var len = path.getTotalLength()
          if (!len) return []
  
          // Fit the 100x100 box into the plot rect, leaving a little breathing room.
          var scale = (Math.min(rect.width, rect.height) / 100) * 0.94
          var offX = rect.x + rect.width / 2 - 50 * scale
          var offY = rect.y + rect.height / 2 - 50 * scale
  
          // Inset copies of the outline, outermost first. Sampling one path only draws
          // a wire outline; nesting scaled copies fills the shape, and keeps this to
          // the widely supported getPointAtLength rather than point-in-fill testing.
          //
          // The inset step comes from the mark radius the engine hands us, so rings sit
          // about one dot apart whatever the plot size or dot count. A fixed list of
          // insets looks like a bullseye as soon as the dots are smaller than the gaps.
          var r = objects[0] && objects[0].r > 0 ? objects[0].r : 3
          var step = (2.1 * r) / (scale * this.DROPLET_REACH)
          var rings = []
          for (var k = 1; k > 0.06 && rings.length < 60; k -= step) rings.push(k)
          if (!rings.length) rings.push(1)
  
          // Each ring gets dots in proportion to its perimeter, so density stays even
          // rather than crowding the middle.
          var totalWeight = rings.reduce(function (a, kk) {
            return a + kk
          }, 0)
  
          var out = []
          var idx = 0
          rings.forEach(function (k, ri) {
            var remaining = objects.length - idx
            var count =
              ri === rings.length - 1
                ? remaining
                : Math.min(remaining, Math.round((objects.length * k) / totalWeight))
            for (var i = 0; i < count; i++) {
              var pt = path.getPointAtLength(((i + 0.5) / count) * len)
              // Shrink toward the core to make this ring's inset copy.
              var sx = this.DROPLET_CORE.x + (pt.x - this.DROPLET_CORE.x) * k
              var sy = this.DROPLET_CORE.y + (pt.y - this.DROPLET_CORE.y) * k
              out.push({
                id: objects[idx].id,
                x: offX + sx * scale,
                y: offY + sy * scale,
              })
              idx++
            }
          })
          return out
        };

  private VIEWS: any = {
          droplet: { layout: 'custom', positions: 'droplet' },
          packed: { layout: 'packed', positions: undefined },
          columns: { layout: 'columns', positions: undefined },
        };

  private setActive = (id: any): any => {
          document
            .querySelectorAll('.actions button[data-view]')
            .forEach(function (btn) {
              btn.classList.toggle('active', btn.getAttribute('data-view') === id)
            })
        };

  private current: any = 'droplet';

  public chartOptions: Partial<ChartOptions> = {
          series: [576, 168, 42, 34],
          chart: {
            id: 'waterChart',
            type: 'unit',
            height: 460,
            animations: {
              enabled: true,
              speed: 900,
            },
          },
          labels: ['Safely managed', 'Basic', 'Limited', 'Unsafe'],
          colors: ['#008FFB', '#00B8D9', '#FFAB00', '#FF4560'],
          plotOptions: {
            unit: {
              layout: 'custom',
              positions: 'droplet',
              transition: 'flow',
              spacing: 1.15,
              clusterLabels: {
                show: false,
              },
            },
          },
          legend: {
            position: 'bottom',
          },
        };
  ngAfterViewInit() {
    (window as any).ApexCharts.setLicense('APEX-eyJleHBpcnlEYXRlIjoiMjEyNi0wNy0wNCIsImlzc3VlRGF0ZSI6IjIwMjYtMDctMjgiLCJwbGFuIjoicHJlbWl1bSIsImRvbWFpbnMiOlsiYXBleGNoYXJ0cy5jb20iLCIxMjcuMC4wLjEiLCJsb2NhbGhvc3QiXSwic2lnIjoieVBmb1VCc0Z3TU9ZdUEyaEZkR0I2Y1FtZ0JITUtXcVdJSjB2NVRESXRZbFR3eDJMUmh6R2x0RUc3VXJ4X0s3b25ZMWRZb2Z2VGItN01ydFYyNDVyOWcifQ==');
    ApexCharts.registerUnitLayout('droplet', this.silhouetteLayout)
    this.setActive(this.current)

    document.querySelectorAll('.actions button[data-view]').forEach(function (btn) {
            btn.addEventListener('click', () => {
              var id = btn.getAttribute('data-view')
              if (id === this.current) return
              this.current = id
              this.setActive(this.current)
              this.chart.updateOptions({
                plotOptions: { unit: this.VIEWS[id] },
              })
            })
          })
  }

  ngOnDestroy() {
    // no cleanup needed
  }
}
Water Silhouette - Angular Unit Charts | ApexCharts.js | ApexCharts.js