Bullet Chart in JavaScript
Using ApexCharts with JavaScript
This Bullet Chart example uses ApexCharts.js directly in JavaScript, with no wrapper component.
Install it with npm install apexcharts, then mount the chart with new ApexCharts(element, options).render().
// Marks (#11): a bullet mark's drawn span is not simply its value: the target
// tick and the top qualitative band extend past it. yExtent lets the type
// declare the [min, max] each datum occupies, so those bounds fold into the
// y-axis scale (here the 100 band and the targets all fit) even though the
// measured values are lower.
ApexCharts.registerSeriesType('bullet', {
yExtent: function (datum) {
return [0, Math.max(datum.target, datum.ranges[datum.ranges.length - 1])]
},
renderItem: function (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,
})
},
})
var bulletData = [
{ 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] },
]
var options = {
series: [{ name: 'Performance', data: 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: function (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: function (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>'
)
},
},
}
var chart = new ApexCharts(document.querySelector('#chart'), options)
chart.render()