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 makeRng = (seed: any): any => {
var s = seed >>> 0
return function () {
s = (Math.imul(s, 1664525) + 1013904223) >>> 0
return s / 4294967296
}
};
private pick = (rand: any, items: any, weights: any): any => {
var total = 0
for (var i = 0; i < weights.length; i++) total += weights[i]
var r = rand() * total
for (var k = 0; k < items.length; k++) {
r -= weights[k]
if (r <= 0) return items[k]
}
return items[items.length - 1]
};
private BASE: any = [
'Nimbus',
'Cobalt',
'Lumen',
'Pathwise',
'Flowdesk',
'Draftly',
'Vault',
'Ledgr',
'Payline',
'Northbank',
'Kitepay',
'Vitalis',
'Pulse',
'CareLoop',
'Mendly',
'Orbit',
'Quill',
'Sable',
'Tandem',
'Verve',
'Wisp',
'Zephyr',
'Arbor',
'Bloomly',
'Cinder',
'Dune',
'Ember',
'Forge',
'Glint',
'Harbor',
'Iris',
'Juno',
'Kelp',
'Lark',
'Mica',
'Nova',
'Onyx',
'Plume',
'Quartz',
'Rune',
'Slate',
'Talon',
'Umbra',
'Vesper',
'Willow',
'Yarn',
'Zenith',
'Ashen',
];
private SUFFIX: any = [
'',
' Labs',
' AI',
' Systems',
' Cloud',
' Works',
' Base',
' Go',
];
private SECTORS: any = ['AI', 'SaaS', 'Fintech', 'Health', 'Commerce'];
private SECTOR_COLORS: any = ['#2563EB', '#06B6D4', '#22C55E', '#EC4899', '#F97316'];
private SECTOR_W: any = [13, 11, 10, 8, 7];
private STAGES: any = ['Seed', 'Series A', 'Series B', 'Series C+'];
private STAGE_COLORS: any = ['#BAE6FD', '#7DD3FC', '#0EA5E9', '#0369A1'];
private STAGE_W: any = [0.42, 0.3, 0.18, 0.1];
private STAGE_FUND: any = {
Seed: [1, 6],
'Series A': [8, 28],
'Series B': [40, 95],
'Series C+': [150, 620],
};
private COUNT: any = 220;
private rand: any = this.makeRng(20260724);
private STARTUPS: any = [];
private groupBy = (field: any, order: any): any => {
return order.map(function (g) {
return {
name: g,
data: this.STARTUPS.filter(function (s) {
return s[field] === g
}),
}
})
};
private bySector: any = this.groupBy('sector', this.SECTORS);
private byStage: any = this.groupBy('stage', this.STAGES);
private startupTooltip = (o: any): any => {
if (!o.datum) return o.seriesName
return (
'<div style="font-weight:700">' +
o.datum.name +
'</div><div style="opacity:0.75">' +
o.datum.sector +
' · ' +
o.datum.stage +
'</div><div style="opacity:0.75">$' +
o.datum.value +
'M raised</div>'
)
};
private countLabel = (name: any, opts: any): any => {
return name + ' (' + opts.value + ')'
};
private VIEWS: any = [
{
label: 'The market',
caption:
'220 startups, one dot each, packed into a single cloud and coloured by sector. The whole cohort at a glance.',
this.chartOptions: {
series: this.bySector,
colors: this.SECTOR_COLORS,
plotOptions: {
unit: {
layout: 'packed',
transition: 'identity',
sortByGroup: true,
sizeByValue: { enabled: false },
clusterLabels: { show: false },
},
},
},
},
{
label: 'By sector',
caption:
'The same dots fan out into their sectors. The clusters show which corners of the market are crowded and which are thin.',
this.chartOptions: {
series: this.bySector,
colors: this.SECTOR_COLORS,
plotOptions: {
unit: {
layout: 'grouped',
transition: 'identity',
sizeByValue: { enabled: false },
// curved labels ride the top of each sector cluster
clusterLabels: { show: true, position: 'top', formatter: this.countLabel },
},
},
},
},
{
label: 'By stage',
caption:
'Regrouped by funding stage. Every company travels to its stage bar and the classic funnel appears: many seed, few late rounds.',
this.chartOptions: {
series: this.byStage,
colors: this.STAGE_COLORS,
plotOptions: {
unit: {
layout: 'columns',
transition: 'identity',
sizeByValue: { enabled: false },
// labels sit BELOW each stage bar
clusterLabels: {
show: true,
position: 'bottom',
formatter: this.countLabel,
},
},
},
},
},
{
label: 'By capital',
caption:
"Back into one cloud, but now each dot's area is the money it raised: a handful of late-stage bubbles dwarf the crowd of small rounds.",
this.chartOptions: {
series: this.bySector,
colors: this.SECTOR_COLORS,
plotOptions: {
unit: {
layout: 'packed',
transition: 'identity',
sortByGroup: true,
clusterLabels: { show: false },
sizeByValue: {
enabled: true,
maxRadius: 4.5,
minRadius: 1.4,
scale: 'area',
},
},
},
},
},
];
private buttons: any = document.querySelectorAll('#view-nav button');
private caption: any = document.getElementById('caption');
private showView = (i: any): any => {
this.buttons.forEach(function (b, k) {
b.classList.toggle('active', k === i)
})
this.caption.textContent = this.VIEWS[i].caption
this.chart.updateOptions(this.VIEWS[i].this.chartOptions)
};
public chartOptions: Partial<ChartOptions> = {
series: this.bySector,
chart: {
id: 'startupCohort',
type: 'unit',
height: 440,
fontFamily: 'Helvetica, Arial, sans-serif',
animations: {
enabled: true,
speed: 800,
},
toolbar: { show: false },
},
colors: this.SECTOR_COLORS,
legend: {
position: 'bottom',
},
plotOptions: {
unit: {
layout: 'packed',
transition: 'identity',
size: 4,
spacing: 1.15,
sortByGroup: true,
clusterLabels: {
show: false,
},
tooltip: {
// startupTooltip lives in the shared head script.
formatter: this.startupTooltip,
},
},
},
};
ngAfterViewInit() {
(window as any).ApexCharts.setLicense('APEX-eyJpc3N1ZURhdGUiOiIyMDI2LTA0LTE2IiwiZXhwaXJ5RGF0ZSI6IjIwNTItMDQtMTciLCJwbGFuIjoiZW50ZXJwcmlzZSIsImRvbWFpbnMiOlsiYXBleGNoYXJ0cy5jb20iLCIxMjcuMC4wLjEiXX0=');
for (var i = 0; i < this.COUNT; i++) {
// base x suffix keeps every generated name unique (COUNT <= BASE * SUFFIX).
var nm =
this.BASE[i % this.BASE.length] + this.SUFFIX[Math.floor(i / this.BASE.length) % this.SUFFIX.length]
var sector = this.pick(this.rand, this.SECTORS, this.SECTOR_W)
var stage = this.pick(this.rand, this.STAGES, this.STAGE_W)
var f = this.STAGE_FUND[stage]
this.STARTUPS.push({
name: nm,
sector: sector,
stage: stage,
value: Math.round(f[0] + this.rand() * (f[1] - f[0])),
})
}
function showView(i) {
this.buttons.forEach(function (b, k) {
b.classList.toggle('active', k === i)
})
this.caption.textContent = this.VIEWS[i].caption
this.chart.updateOptions(this.VIEWS[i].this.chartOptions)
}
this.buttons.forEach(function (btn) {
btn.addEventListener('click', () => {
this.showView(parseInt(btn.getAttribute('data-view'), 10))
})
})
}
ngOnDestroy() {
// no cleanup needed
}
}