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 N: any = 60;

  private QUARTERS: any = [
          { lo: 20, hi: 35 },
          { lo: 35, hi: 50 },
          { lo: 50, hi: 65 },
          { lo: 65, hi: 80 },
        ];

  private fillQuarter = (q: any, pull: any): any => {
          var out = []
          for (var i = 0; i < 15; i++) {
            var t = i / 14
            var f =
              pull === 'low'
                ? Math.pow(t, 2.6)
                : pull === 'high'
                  ? 1 - Math.pow(1 - t, 2.6)
                  : t
            out.push(Math.round((q.lo + (q.hi - q.lo) * f) * 10) / 10)
          }
          return out
        };

  private sampleFrom = (pulls: any): any => {
          return this.QUARTERS.reduce(function (acc, q, k) {
            return acc.concat(this.fillQuarter(q, pulls[k]))
          }, [])
        };

  private SHAPES: any = [
          {
            name: 'Two camps',
            // Quarters 2 and 3 crowd outwards, hollowing out the centre: one camp
            // around 35, another around 65, and a conspicuous gap between them.
            values: this.sampleFrom(['even', 'low', 'high', 'even']),
          },
          {
            name: 'Perfectly even',
            // Every reading about as likely as any other.
            values: this.sampleFrom(['even', 'even', 'even', 'even']),
          },
          {
            name: 'Bunched in the middle',
            // The mirror image: quarters 2 and 3 crowd inwards, so almost everything
            // piles onto the median and the whiskers are reached by stragglers.
            values: this.sampleFrom(['even', 'high', 'low', 'even']),
          },
        ];

  private BOX_SERIES: any = [
          {
            name: 'Readings',
            data: this.SHAPES.map(function (s) {
              return { x: s.name, points: s.values }
            }),
          },
        ];

  private setActive = (exploded: any): any => {
          var buttons = [].slice.call(document.querySelectorAll('[data-explode]'))
          buttons.forEach(function (b) {
            b.className =
              (b.getAttribute('data-explode') === 'true') === exploded ? 'on' : ''
          })
        };

  private renderSummary = (chart: any): any => {
          var el = document.querySelector('#summary')
          if (!el) return
          var rows = (chart.w.config.series[0].data || []).map(function (d) {
            var y = d.y || []
            return (
              '<tr><td>' +
              d.x +
              '</td>' +
              y
                .map(function (v) {
                  return '<td>' + v + '</td>'
                })
                .join('') +
              '</tr>'
            )
          })
          el.innerHTML =
            '<table><thead><tr><th>Group</th><th>Min</th><th>Q1</th>' +
            '<th>Median</th><th>Q3</th><th>Max</th></tr></thead><tbody>' +
            rows.join('') +
            '</tbody></table>'
        };

  private wireExplode = (chart: any): any => {
          var buttons = [].slice.call(document.querySelectorAll('[data-explode]'))
  
          buttons.forEach(function (b) {
            b.addEventListener('click', function () {
              // The active view's button is a no-op: re-requesting the readings while
              // already exploded would ask rowSeries() of a unit chart, which has no
              // rows to hand back.
              if (b.className === 'on') return
              var explode = b.getAttribute('data-explode') === 'true'
              this.setActive(explode)
  
              if (explode) {
                // Nothing about the samples is passed in: the boxes were built from the
                // observations, so the chart can still hand each box's own readings
                // back. Every dot leaves from the box it was summarised into.
                chart.updateOptions({
                  chart: { type: 'unit' },
                  series: chart.rowSeries(),
                  plotOptions: {
                    unit: {
                      layout: 'scatter',
                      unitValue: 1,
                      size: 4,
                      scatter: {
                        y: 'lanes',
                        spread: 'swarm',
                        xTitle: 'Reading',
                        // One decade of margin each side, and ticks every 10 like the
                        // box view's axis, so the room reads unchanged across the
                        // morph.
                        xMin: 10,
                        xMax: 90,
                        tickAmount: 9,
                        // Wide enough for the longest lane name; the gutter clips
                        // rather than wraps, so this has to clear "Bunched in the
                        // middle" outright.
                        laneLabelWidth: 155,
                      },
                    },
                  },
                  legend: { show: false },
                })
              } else {
                chart.updateOptions({
                  chart: { type: 'boxPlot' },
                  series: this.BOX_SERIES,
                  legend: { show: false },
                })
              }
            })
          })
  
          this.setActive(false)
          this.renderSummary(chart)
        };

  public chartOptions: Partial<ChartOptions> = {
          series: this.BOX_SERIES,
          chart: {
            id: 'sameBox',
            type: 'boxPlot',
            height: 430,
            toolbar: {
              show: false,
            },
            animations: {
              chartTypeMorph: {
                speed: 900,
              },
            },
          },
          colors: ['#12b3a8'],
          plotOptions: {
            bar: {
              horizontal: true,
            },
            boxPlot: {
              colors: {
                upper: '#c8ece9',
                lower: '#9fdcd7',
              },
              points: {
                show: false,
              },
            },
          },
          legend: {
            show: false,
          },
          xaxis: {
            // The boxes are horizontal, so the reading runs along X in BOTH views: the
            // box view titles this axis, the exploded beeswarm names its own value axis
            // the same (scatter.xTitle). One explicit label colour keeps the beeswarm's
            // axis chrome (ticks, title, lane names) on the same near-black as the box
            // view's axes, instead of lane names taking the series colour.
            title: {
              text: 'Reading',
            },
            labels: {
              style: {
                colors: '#373d3f',
              },
            },
          },
        };
  ngAfterViewInit() {
    (window as any).ApexCharts.setLicense('APEX-eyJleHBpcnlEYXRlIjoiMjEyNi0wNy0wNCIsImlzc3VlRGF0ZSI6IjIwMjYtMDctMjgiLCJwbGFuIjoicHJlbWl1bSIsImRvbWFpbnMiOlsiYXBleGNoYXJ0cy5jb20iLCIxMjcuMC4wLjEiLCJsb2NhbGhvc3QiXSwic2lnIjoieVBmb1VCc0Z3TU9ZdUEyaEZkR0I2Y1FtZ0JITUtXcVdJSjB2NVRESXRZbFR3eDJMUmh6R2x0RUc3VXJ4X0s3b25ZMWRZb2Z2VGItN01ydFYyNDVyOWcifQ==');
    this.wireExplode(this.chart)

    function wireExplode(chart) {
            var buttons = [].slice.call(document.querySelectorAll('[data-explode]'))

            buttons.forEach(function (b) {
              b.addEventListener('click', () => {
                // The active view's button is a no-op: re-requesting the readings while
                // already exploded would ask rowSeries() of a unit chart, which has no
                // rows to hand back.
                if (b.className === 'on') return
                var explode = b.getAttribute('data-explode') === 'true'
                this.setActive(explode)

                if (explode) {
                  // Nothing about the samples is passed in: the boxes were built from the
                  // observations, so the chart can still hand each box's own readings
                  // back. Every dot leaves from the box it was summarised into.
                  this.chart.updateOptions({
                    chart: { type: 'unit' },
                    series: this.chart.rowSeries(),
                    plotOptions: {
                      unit: {
                        layout: 'scatter',
                        unitValue: 1,
                        size: 4,
                        scatter: {
                          y: 'lanes',
                          spread: 'swarm',
                          xTitle: 'Reading',
                          // One decade of margin each side, and ticks every 10 like the
                          // box view's axis, so the room reads unchanged across the
                          // morph.
                          xMin: 10,
                          xMax: 90,
                          tickAmount: 9,
                          // Wide enough for the longest lane name; the gutter clips
                          // rather than wraps, so this has to clear "Bunched in the
                          // middle" outright.
                          laneLabelWidth: 155,
                        },
                      },
                    },
                    legend: { show: false },
                  })
                } else {
                  this.chart.updateOptions({
                    chart: { type: 'boxPlot' },
                    series: this.BOX_SERIES,
                    legend: { show: false },
                  })
                }
              })
            })

            this.setActive(false)
            this.renderSummary(chart)
          }
  }

  ngOnDestroy() {
    // no cleanup needed
  }
}
Same Box, Different Data - Angular BoxPlot Charts | ApexCharts.js | ApexCharts.js