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 seed: any = 11;

  private rand = (): any => {
          this.seed = (this.seed * 16807) % 2147483647
          return (this.seed - 1) / 2147483646
        };

  private gauss = (): any => {
          var u1 = Math.max(this.rand(), 1e-9)
          var u2 = this.rand()
          return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2)
        };

  private logNormal = (n: any, median: any, sigma: any, lo: any, hi: any): any => {
          var out = []
          for (var i = 0; i < n; i++) {
            var v = Math.exp(Math.log(median) + sigma * this.gauss())
            out.push(Math.round(Math.min(hi, Math.max(lo, v))))
          }
          return out
        };

  private PLANS: any = [
          // The cap: anything the distribution puts past 30 lands ON 30 exactly.
          { name: 'Free', color: '#12b3a8', values: this.logNormal(150, 24, 0.5, 3, 30) },
          { name: 'Pro', color: '#5a67d8', values: this.logNormal(170, 34, 0.45, 6, 105) },
          { name: 'Trial', color: '#e8890c', values: this.logNormal(14, 26, 0.55, 4, 95) },
        ];

  private COLORS: any = this.PLANS.map(function (p) {
          return p.color
        });

  private VIOLIN_SERIES: any = [
          {
            name: 'Minutes',
            data: this.PLANS.map(function (p) {
              // Raw observations only: the library runs the density estimate.
              return { x: p.name, points: p.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 median = (values: any): any => {
          var s = values.slice().sort(function (a, b) {
            return a - b
          })
          var m = (s.length - 1) / 2
          return (s[Math.floor(m)] + s[Math.ceil(m)]) / 2
        };

  private renderSummary = (): any => {
          var el = document.querySelector('#summary')
          if (!el) return
          var rows = this.PLANS.map(function (p) {
            var pinned = p.values.filter(function (v) {
              return v === 30
            }).length
            return (
              '<tr><td>' +
              p.name +
              '</td>' +
              '<td>' +
              p.values.length +
              '</td>' +
              '<td>' +
              this.median(p.values) +
              ' min</td>' +
              '<td>' +
              (p.name === 'Free' ? pinned : '-') +
              '</td></tr>'
            )
          })
          el.innerHTML =
            '<table><thead><tr><th>Plan</th><th>Readings</th><th>Median</th>' +
            '<th>Pinned at the 30 min cap</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) {
                // The violins were estimated from the observations, so the chart can
                // hand each violin's own readings back: every dot leaves from the
                // curve it was smoothed into.
                var rows = chart.rowSeries()
                // rowSeries() colours by series, and this violin is ONE series split
                // across three lanes (distributed). Re-key the colour by lane so each
                // violin's ink keeps its own colour on the way out.
                rows.forEach(function (cluster, k) {
                  cluster.data.forEach(function (d) {
                    d.fillColor = this.COLORS[k]
                  })
                })
                chart.updateOptions({
                  chart: { type: 'unit' },
                  series: rows,
                  plotOptions: {
                    unit: {
                      layout: 'scatter',
                      unitValue: 1,
                      size: 3.5,
                      scatter: {
                        // Value stays on Y, one lane per plan across X, matching the
                        // violins. The value-axis keys keep their x* names in either
                        // orientation.
                        orientation: 'vertical',
                        spread: 'jitter',
                        xTitle: 'Minutes per day',
                        // The same 0..120 window the violin state pins its yaxis to;
                        // 7 ticks puts a line every 20 minutes, matching its grid.
                        xMin: 0,
                        xMax: 120,
                        tickAmount: 7,
                      },
                    },
                  },
                  legend: { show: false },
                })
              } else {
                chart.updateOptions({
                  chart: { type: 'violin' },
                  series: this.VIOLIN_SERIES,
                  legend: { show: false },
                })
              }
            })
          })
  
          this.setActive(false)
          this.renderSummary()
        };

  public chartOptions: Partial<ChartOptions> = {
          series: this.VIOLIN_SERIES,
          chart: {
            id: 'violinJitter',
            type: 'violin',
            height: 430,
            toolbar: {
              show: false,
            },
            animations: {
              chartTypeMorph: {
                speed: 900,
              },
            },
          },
          colors: this.COLORS,
          plotOptions: {
            bar: { distributed: true }, // one colour per plan
            violin: {
              normalize: 'group',
              // The toggle is the reveal here; the built-in overlay would spoil it.
              points: { show: false },
            },
          },
          stroke: {
            width: 1,
            colors: ['#8a97a3'],
          },
          legend: {
            show: false,
          },
          yaxis: {
            // Same domain and ticks as the jitter view, so the two states share one
            // grid and the morph never re-scales the room. Minutes cannot be negative,
            // which the auto-domain's padding would otherwise imply.
            min: 0,
            max: 120,
            tickAmount: 6,
            labels: {
              formatter: (v) => {
                return Math.round(v) + ' min'
              },
            },
          },
        };
  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) {
                  // The violins were estimated from the observations, so the chart can
                  // hand each violin's own readings back: every dot leaves from the
                  // curve it was smoothed into.
                  var rows = this.chart.rowSeries()
                  // rowSeries() colours by series, and this violin is ONE series split
                  // across three lanes (distributed). Re-key the colour by lane so each
                  // violin's ink keeps its own colour on the way out.
                  rows.forEach(function (cluster, k) {
                    cluster.data.forEach(function (d) {
                      d.fillColor = this.COLORS[k]
                    })
                  })
                  this.chart.updateOptions({
                    chart: { type: 'unit' },
                    series: rows,
                    plotOptions: {
                      unit: {
                        layout: 'scatter',
                        unitValue: 1,
                        size: 3.5,
                        scatter: {
                          // Value stays on Y, one lane per plan across X, matching the
                          // violins. The value-axis keys keep their x* names in either
                          // orientation.
                          orientation: 'vertical',
                          spread: 'jitter',
                          xTitle: 'Minutes per day',
                          // The same 0..120 window the violin state pins its yaxis to;
                          // 7 ticks puts a line every 20 minutes, matching its grid.
                          xMin: 0,
                          xMax: 120,
                          tickAmount: 7,
                        },
                      },
                    },
                    legend: { show: false },
                  })
                } else {
                  this.chart.updateOptions({
                    chart: { type: 'violin' },
                    series: this.VIOLIN_SERIES,
                    legend: { show: false },
                  })
                }
              })
            })

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

  ngOnDestroy() {
    // no cleanup needed
  }
}
Violin to Jitter Morph - Angular Violin Charts | ApexCharts.js | ApexCharts.js