Skip to main content

Bullet Chart

A bullet chart shows a measure against a target and qualitative ranges, packing KPI-versus-goal context into a compact bar.

CustomChartDef bullet_chart {
label: 'Bullet Chart'
description: 'Use this chart to compare target, pace, and current values across categories in a compact bullet chart layout.'

fields {
field category {
label: 'Category'
type: 'dimension'
data_type: 'string'
sort {
apply_order: 1
direction: 'asc'
}
}

field target {
label: 'Target'
type: 'measure'
data_type: 'number'
sort {
apply_order: 2
direction: 'desc'
}
}

field pace {
label: 'Pace'
type: 'measure'
data_type: 'number'
sort {
apply_order: 3
direction: 'desc'
}
}

field current {
label: 'Current'
type: 'measure'
data_type: 'number'
sort {
apply_order: 4
direction: 'desc'
}
}
}

options {
option show_tooltip {
label: 'Show tooltip'
type: 'toggle'
default_value: true
}

option current_bar_height {
label: 'Current bar height'
type: 'number-input'
default_value: 8
}

option target_tick_thickness {
label: 'Target tick thickness'
type: 'number-input'
default_value: 2
}
}

template: @vgl {
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"description": "A reusable target-vs-pace-vs-current bullet-style comparison chart.",
"data": {"values": @{values}},
"width": "container",
"height": "container",
"transform": [
{
"calculate": "datum['@{fields.target.name}']",
"as": "target_value"
},
{
"calculate": "datum['@{fields.pace.name}']",
"as": "pace_value"
},
{
"calculate": "datum['@{fields.current.name}']",
"as": "current_value"
},
{
"calculate": "datum['@{fields.category.name}']",
"as": "category_value"
},
{
"fold": ["target_value", "pace_value", "current_value"],
"as": ["series_key", "series_value"]
},
{
"calculate": "{'target_value':'Target','pace_value':'Pace','current_value':'Current'}[datum.series_key]",
"as": "series_label"
},
{
"calculate": "indexof(['Target','Pace','Current'], datum.series_label)",
"as": "series_order"
}
],
"encoding": {
"y": {
"field": "category_value",
"type": "ordinal",
"title": null,
"axis": {
"labelOverlap": true,
"domain": false,
"ticks": false,
"grid": false,
"labelPadding": 8
}
},
"x": {
"field": "series_value",
"type": "quantitative",
"stack": null,
"axis": {
"title": null,
"domain": false,
"tickColor": "#E5E7EB",
"grid": true,
"gridColor": "#F3F4F6",
"gridOpacity": 1
}
},
"order": {
"field": "series_order",
"type": "quantitative",
"sort": "ascending"
},
"tooltip": [
{
"field": "category_value",
"type": "nominal",
"title": "Category"
},
{
"field": "current_value",
"type": "quantitative",
"title": "Current"
},
{
"field": "pace_value",
"type": "quantitative",
"title": "Pace"
},
{
"field": "target_value",
"type": "quantitative",
"title": "Target"
}
]
},
"layer": [
{
"mark": "bar",
"params": [
{
"name": "series_hover",
"select": {
"type": "point",
"fields": ["series_label"]
},
"bind": {"legend": "pointerover"}
}
],
"encoding": {
"color": {
"field": "series_label",
"type": "nominal",
"legend": {
"title": null,
"orient": "top",
"direction": "horizontal",
"symbolType": "square",
"symbolSize": 80,
"labelLimit": 140,
"symbolOpacity": 1
},
"scale": {
"domain": ["Target", "Pace", "Current"],
"range": ["#9CA3AF", "#E5E7EB", "#4F46E5"]
}
},
"opacity": {
"value": 0
}
}
},
{
"transform": [
{
"filter": "datum.series_label === 'Pace'"
}
],
"mark": {
"type": "bar",
"tooltip": @{options.show_tooltip.value}
},
"encoding": {
"color": {
"field": "series_label",
"type": "nominal",
"legend": null,
"scale": {
"domain": ["Target", "Pace", "Current"],
"range": ["#9CA3AF", "#E5E7EB", "#4F46E5"]
}
},
"opacity": {
"condition": {
"param": "series_hover",
"value": 1
},
"value": 0.25
}
}
},
{
"transform": [
{
"filter": "datum.series_label === 'Current'"
}
],
"mark": {
"type": "bar",
"height": @{options.current_bar_height.value},
"tooltip": @{options.show_tooltip.value}
},
"encoding": {
"color": {
"field": "series_label",
"type": "nominal",
"legend": null,
"scale": {
"domain": ["Target", "Pace", "Current"],
"range": ["#9CA3AF", "#E5E7EB", "#4F46E5"]
}
},
"opacity": {
"condition": {
"param": "series_hover",
"value": 1
},
"value": 0.25
}
}
},
{
"transform": [
{
"filter": "datum.series_label === 'Target'"
}
],
"mark": {
"type": "tick",
"thickness": @{options.target_tick_thickness.value},
"tooltip": @{options.show_tooltip.value}
},
"encoding": {
"color": {
"field": "series_label",
"type": "nominal",
"legend": null,
"scale": {
"domain": ["Target", "Pace", "Current"],
"range": ["#9CA3AF", "#E5E7EB", "#4F46E5"]
}
},
"opacity": {
"condition": {
"param": "series_hover",
"value": 1
},
"value": 0.25
}
}
}
],
"config": {
"background": null,
"view": {
"stroke": null
},
"axis": {
"labelFontSize": 12,
"titleFontSize": 12,
"labelFontWeight": 400,
"titleFontWeight": 500
},
"legend": {
"labelFontSize": 12,
"titleFontSize": 12,
"labelFontWeight": 400,
"symbolStrokeWidth": 0
},
"bar": {
"cornerRadius": 2
},
"tick": {
"thickness": 2,
"size": 22
}
}
};;
}

Open Markdown
Let us know what you think about this document :)