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 DAY: any = 86400000;

  private calNow: any = new Date();

  private calEnd: any = Date.UTC(
          this.calNow.getUTCFullYear(),
          this.calNow.getUTCMonth(),
          this.calNow.getUTCDate(),
        );

  private calStart: any = this.calEnd - 364 * this.DAY;

  private weekStartOf = (ms: any): any => {
          return ms - new Date(ms).getUTCDay() * this.DAY
        };

  private buildCalendar = (): any => {
          var weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
          var byDay = weekdays.map(function (n) {
            return { name: n, data: [] }
          })
  
          // Deterministic PRNG so the demo looks the same on every reload.
          var seed = 20240407
          function rand() {
            seed = (seed * 1103515245 + 12345) & 0x7fffffff
            return seed / 0x7fffffff
          }
  
          var totalWeeks = Math.round(
            (this.weekStartOf(this.calEnd) - this.weekStartOf(this.calStart)) / (7 * this.DAY),
          )
          for (var t = this.calStart; t <= this.calEnd; t += this.DAY) {
            var dow = new Date(t).getUTCDay()
            var wk = Math.round((this.weekStartOf(t) - this.weekStartOf(this.calStart)) / (7 * this.DAY))
            // A gentle seasonal wave so some months look busier than others.
            var season = 0.5 + 0.5 * Math.sin((wk / totalWeeks) * Math.PI * 2 - 1)
            var count = 0
            // ~55% of days have activity; weekends are lighter.
            if (rand() < 0.35 + 0.4 * season) {
              var bias = dow === 0 || dow === 6 ? 0.5 : 1
              // rand()*rand() skews toward the low buckets, like real activity.
              count = Math.round(rand() * rand() * 16 * bias) + 1
            }
            // x is the week's Sunday (so the column lines up); the cell's true date is
            // stashed on the datum for the tooltip (x can't carry it: all 7 weekdays in
            // a column share the same x).
            byDay[dow].data.push({ x: this.weekStartOf(t), y: count, date: t })
          }
  
          // Rows read Sun (top) -> Sat (bottom). ApexCharts draws the last series on
          // top, so reverse the weekday order before returning.
          return byDay.reverse()
        };

  private calendarData: any = this.buildCalendar();

  private calMinX: any = this.weekStartOf(this.calStart) - 3.5 * this.DAY;

  private calMaxX: any = this.weekStartOf(this.calEnd) + 3.5 * this.DAY;

  public chartOptions: Partial<ChartOptions> = {
          series: this.calendarData,
          chart: {
            height: 186,
            width: '100%',
            type: 'heatmap',
            toolbar: { show: false },
            animations: { enabled: false },
          },
          title: {
            text: 'Contribution activity',
            align: 'center',
            style: { fontSize: '14px', fontWeight: 600 },
          },
          dataLabels: { enabled: false },
          // A small light gap between cells, like a contributions calendar.
          stroke: { width: 3, colors: ['#fff'] },
          legend: { show: false },
          states: {
            active: {
              filter: {
                type: 'none',
              },
            },
          },
          plotOptions: {
            heatmap: {
              radius: 2,
              // Flat bucket colors (no within-range shading), so each level is one color.
              enableShades: false,
              colorScale: {
                ranges: [
                  { from: 0, to: 0, name: '0', color: '#ebedf0' },
                  { from: 1, to: 3, name: '1-3', color: '#9be9a8' },
                  { from: 4, to: 7, name: '4-7', color: '#40c463' },
                  { from: 8, to: 11, name: '8-11', color: '#30a14e' },
                  { from: 12, to: 100, name: '12+', color: '#216e39' },
                ],
              },
            },
          },
          xaxis: {
            type: 'datetime',
            min: this.calMinX,
            max: this.calMaxX,
            position: 'top',
            labels: {
              format: 'MMM',
              datetimeUTC: false,
              style: { colors: '#767676', fontSize: '12px' },
            },
            axisBorder: { show: false },
            axisTicks: { show: false },
            tooltip: { enabled: false },
            crosshairs: {
              show: false,
            },
          },
          yaxis: {
            // Show only alternate weekday labels (Mon / Wed / Fri), like the original.
            labels: {
              formatter: (val) => {
                return ['Mon', 'Wed', 'Fri'].indexOf(val) >= 0 ? val : ''
              },
              style: { colors: ['#767676'], fontSize: '12px' },
            },
          },
          grid: {
            yaxis: {
              lines: {
                show: false,
              },
            },
          },
          tooltip: {
            // Custom tooltip so it can show the cell's real date (stashed on the datum)
            // plus the count, e.g. "5 contributions on Monday, January 8, 2024".
            custom: (opts) => {
              var pt = opts.w.config.series[opts.seriesIndex].data[opts.dataPointIndex]
              var n = pt.y
              var when = new Date(pt.date).toLocaleDateString('en-US', {
                dateStyle: 'medium',
                timeZone: 'UTC',
              })
              var count =
                n === 0
                  ? 'No contributions'
                  : n + (n === 1 ? ' contribution' : ' contributions')
              return (
                '<div style="padding:6px 10px;font-size:13px">' +
                '<b>' +
                count +
                '</b> on ' +
                when +
                '</div>'
              )
            },
          },
        };
  ngAfterViewInit() {
    (window as any).ApexCharts.setLicense('APEX-eyJpc3N1ZURhdGUiOiIyMDI2LTA0LTE2IiwiZXhwaXJ5RGF0ZSI6IjIwNTItMDQtMTciLCJwbGFuIjoiZW50ZXJwcmlzZSIsImRvbWFpbnMiOlsiYXBleGNoYXJ0cy5jb20iLCIxMjcuMC4wLjEiXX0=');
  }

  ngOnDestroy() {
    // no cleanup needed
  }
}
Calendar Heatmap - Angular Heatmap Charts | ApexCharts.js | ApexCharts.js