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 bulletData: any = [
{ x: 'Revenue', y: 78, target: 85, ranges: [50, 75, 100] },
{ x: 'Profit', y: 62, target: 70, ranges: [40, 65, 90] },
{ x: 'Cust. Sat', y: 88, target: 80, ranges: [60, 80, 95] },
{ x: 'Retention', y: 71, target: 75, ranges: [55, 70, 90] },
{ x: 'On-time', y: 94, target: 90, ranges: [70, 85, 100] },
];
public chartOptions: Partial<ChartOptions> = {
series: [{ name: 'Performance', data: this.bulletData }],
chart: {
height: 440,
type: 'bullet',
animations: { enabled: false },
toolbar: { show: false },
},
grid: {
padding: { left: 15, right: 15 },
},
title: {
text: 'Q2 KPIs vs target (percent of goal)',
align: 'left',
},
subtitle: {
text: 'A custom "bullet" series type built with Marks; yExtent drives auto-scaling',
align: 'left',
},
xaxis: {
type: 'category',
tickPlacement: 'between',
tooltip: { enabled: false },
},
yaxis: {
min: 0,
max: 100,
tickAmount: 5,
labels: {
formatter: (val) => {
return Math.round(val) + '%'
},
},
},
tooltip: {
// The default tooltip would show the yExtent range (0% - 100%), which is not
// meaningful for a bullet. Show the datum's actual value vs its target.
custom: (opts) => {
var d = opts.w.config.series[opts.seriesIndex].data[opts.dataPointIndex]
var hit = d.y >= d.target
return (
'<div style="padding:7px 11px;font-family:sans-serif;font-size:12px;line-height:1.5">' +
'<div style="font-weight:600;margin-bottom:3px">' +
d.x +
'</div>' +
'Actual: <b>' +
d.y +
'%</b><br>' +
'Target: <b>' +
d.target +
'%</b> ' +
'<span style="color:' +
(hit ? '#1f9d57' : '#e53e3e') +
'">' +
(hit ? '(met)' : '(below)') +
'</span>' +
'</div>'
)
},
},
};
ngAfterViewInit() {
(window as any).ApexCharts.setLicense('APEX-eyJleHBpcnlEYXRlIjoiMjEyNi0wNy0wNCIsImlzc3VlRGF0ZSI6IjIwMjYtMDctMjgiLCJwbGFuIjoicHJlbWl1bSIsImRvbWFpbnMiOlsiYXBleGNoYXJ0cy5jb20iLCIxMjcuMC4wLjEiLCJsb2NhbGhvc3QiXSwic2lnIjoieVBmb1VCc0Z3TU9ZdUEyaEZkR0I2Y1FtZ0JITUtXcVdJSjB2NVRESXRZbFR3eDJMUmh6R2x0RUc3VXJ4X0s3b25ZMWRZb2Z2VGItN01ydFYyNDVyOWcifQ==');
ApexCharts.registerSeriesType('bullet', {
yExtent: (datum) => {
return [0, Math.max(datum.target, datum.ranges[datum.ranges.length - 1])]
},
renderItem: (ctx) => {
var datum = ctx.datum
var scales = ctx.scales
var api = ctx.api
// Bullets are bar-like. With xaxis.tickPlacement:'between' the axis is a
// band axis, so ctx.x is the center of this datum's band and scales.band
// is the band width. Sizing the bullet under the band keeps it inside the
// plot and aligned with its label.
var cx = ctx.x
var half = (scales.band * 0.62) / 2
// qualitative range bands, light to dark, stacked from the baseline up
var shades = ['#e8eef5', '#cdd8e6', '#aebfd4']
var prev = 0
for (var r = 0; r < datum.ranges.length; r++) {
var top = scales.y(datum.ranges[r])
var bottom = scales.y(prev)
api.rect({
x: cx - half,
y: top,
w: half * 2,
h: bottom - top,
fill: shades[r],
})
prev = datum.ranges[r]
}
// measured value: a narrow dark bar from the baseline up to y
var yVal = scales.y(datum.y)
var mHalf = half * 0.38
api.rect({
x: cx - mHalf,
y: yVal,
w: mHalf * 2,
h: scales.y(0) - yVal,
fill: '#2d3748',
})
// target: a red tick across the band
var yTarget = scales.y(datum.target)
api.line({
x1: cx - half,
y1: yTarget,
x2: cx + half,
y2: yTarget,
stroke: '#e53e3e',
width: 3,
})
},
})
}
ngOnDestroy() {
// no cleanup needed
}
}