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 cellCount: any = 10000;
private DAY: any = 86400000;
private shapeFor = (cells: any): any => {
if (cells <= 10000) return { rows: 50, cols: cells / 50 }
return { rows: 100, cols: cells / 100 }
};
private formatLat = (lat: any): any => {
var r = Math.round(lat)
if (r > 0) return r + '°N'
if (r < 0) return -r + '°S'
return '0°'
};
private generateHeatmap = (cells: any): any => {
var shape = this.shapeFor(cells)
// Columns are consecutive days ending today, anchored to UTC midnight so
// every day is exactly 86400000 ms and the seasonal phase never drifts.
var now = new Date()
var end = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())
var start = end - (shape.cols - 1) * this.DAY
// Deterministic PRNG so the field looks the same on every reload.
var seed = 1013904223
function rand() {
seed = (seed * 1103515245 + 12345) & 0x7fffffff
return seed / 0x7fffffff
}
var series = new Array(shape.rows)
for (var r = 0; r < shape.rows; r++) {
var lat = 90 - (r / (shape.rows - 1)) * 180
var absLat = Math.abs(lat) / 90
// Warm at the equator (~27C), cold at the poles (~-30C).
var baseTemp = 27 - 57 * Math.pow(absLat, 1.05)
// Seasonal swing: tiny at the equator, large toward the poles.
var swing = 2 + 22 * absLat
var hemi = lat >= 0 ? 1 : -1
var data = new Array(shape.cols)
for (var c = 0; c < shape.cols; c++) {
var t = start + c * this.DAY
var d = new Date(t)
var doy = (t - Date.UTC(d.getUTCFullYear(), 0, 0)) / this.DAY
// Northern hemisphere peaks in mid-July; southern is the opposite.
var seasonal = swing * Math.sin((2 * Math.PI * doy) / 365 - 1.87) * hemi
var temp = baseTemp + seasonal + (rand() - 0.5) * 3
data[c] = { x: t, y: Math.round(temp) }
}
series[r] = { name: this.formatLat(lat), data: data }
}
// Rows read north (top) -> south (bottom); ApexCharts draws the last series
// on top, so reverse before returning.
return series.reverse()
};
private heatmapData: any = this.generateHeatmap(this.cellCount);
private domNodeCount = (): any => {
var el = document.querySelector('#chart')
return el ? el.getElementsByTagName('*').length : 0
};
private lastRenderMs: any = 0;
private updateStats = (): any => {
var el = document.getElementById('stats')
if (!el || typeof this.chart === 'undefined') return
var active = this.chart.getActiveRenderer ? this.chart.getActiveRenderer() : 'svg'
el.innerHTML =
'Cells: <b>' +
this.cellCount.toLocaleString() +
'</b> · Active renderer: <b>' +
active.toUpperCase() +
'</b> · DOM nodes in chart: <b>' +
this.domNodeCount().toLocaleString() +
'</b> · Render: <b>' +
(this.lastRenderMs > 0 ? this.lastRenderMs + ' ms' : 'change a control to time it') +
'</b>'
};
private rebuild = (): any => {
this.cellCount = parseInt(document.getElementById('cellCount').value, 10)
var mode = document.getElementById('rendererMode').value
this.heatmapData = this.generateHeatmap(this.cellCount)
this.chartOptions.chart.renderer = mode
this.chartOptions.series = this.heatmapData
this.chartOptions.title.text = 'Heatmap: ' + this.cellCount.toLocaleString() + ' cells'
var t = performance.now()
this.chart.destroy()
this.chart = new ApexCharts(document.querySelector('#chart'), this.chartOptions)
this.chart.render().then(function () {
this.lastRenderMs = Math.round(performance.now() - t)
this.updateStats()
})
};
public chartOptions: Partial<ChartOptions> = {
series: this.heatmapData,
chart: {
height: 500,
type: 'heatmap',
renderer: 'canvas',
rendererThreshold: 8000,
animations: { enabled: false },
toolbar: { show: false },
},
title: {
text: 'Heatmap: 10,000 cells',
align: 'left',
},
subtitle: {
text: 'Daily mean surface temperature (°C) by latitude, cold (blue) to warm (red)',
align: 'left',
},
dataLabels: { enabled: false },
// No cell stroke: at high cell counts each cell is ~1px, and a stroke that
// wide would cover the fill entirely (cells look blank). Solid fills only.
stroke: { show: false },
plotOptions: {
heatmap: {
// Flat temperature buckets (no within-bucket shading) on a cold-to-warm
// scale, so a cell's color reads as a temperature straight off the legend.
enableShades: false,
colorScale: {
ranges: [
{ from: -60, to: -25, color: '#313695', name: 'below -25' },
{ from: -25, to: -15, color: '#4575b4', name: '-25 to -15' },
{ from: -15, to: -5, color: '#74add1', name: '-15 to -5' },
{ from: -5, to: 2, color: '#abd9e9', name: '-5 to 2' },
{ from: 2, to: 9, color: '#fee090', name: '2 to 9' },
{ from: 9, to: 16, color: '#fdae61', name: '9 to 16' },
{ from: 16, to: 23, color: '#f46d43', name: '16 to 23' },
{ from: 23, to: 60, color: '#d73027', name: 'above 23' },
],
},
},
},
xaxis: {
type: 'datetime',
labels: { datetimeUTC: true, style: { fontSize: '12px' } },
axisTicks: { show: false },
axisBorder: { show: false },
},
yaxis: {
labels: { style: { fontSize: '12px' } },
},
};
ngAfterViewInit() {
(window as any).ApexCharts.setLicense('APEX-eyJpc3N1ZURhdGUiOiIyMDI2LTA0LTE2IiwiZXhwaXJ5RGF0ZSI6IjIwNTItMDQtMTciLCJwbGFuIjoiZW50ZXJwcmlzZSIsImRvbWFpbnMiOlsiYXBleGNoYXJ0cy5jb20iLCIxMjcuMC4wLjEiXX0=');
requestAnimationFrame(() => {
if (this.chart && this.chart.chart?.w && this.chart.chart?.w.globals && this.chart.chart?.w.globals.series) {
this.updateStats()
} else {
requestAnimationFrame(poll)
}
})
document.getElementById('rerender').addEventListener('click', this.rebuild)
document.getElementById('rendererMode').addEventListener('change', this.rebuild)
document.getElementById('cellCount').addEventListener('change', this.rebuild)
}
ngOnDestroy() {
// no cleanup needed
}
}