Skip to main content

Gauge Chart

A gauge chart visualizes a metric against a threshold, to monitor progress or benchmark against a target.

  • Good for: showing a single metric against its maximum, monitoring progress toward a goal, at-a-glance KPI panels with a benchmark.
  • Not great for: comparing many categories at once (use a bar or bullet chart), trends over time, or part-to-whole composition.

Syntax

Two variants are available:

  • Simple Gauge Chart: value against a maximum.
  • Gauge Chart with Target: adds a target marker to benchmark against a goal.

Use one of the following AML definitions to add the Gauge Chart to your custom chart library.

Simple Gauge Chart

reporting-custom-chart/simple-gauge-chart
Legacy syntax
CustomChart {
fields {
field value {
type: "measure"
label: "Value"
}
field max_value {
type: "measure"
label: "Max Value"
}
}

options {
option background_color {
type: "color-picker"
label: "Background Color"
default_value: "#cbd1d6"
}
option value_color {
type: "color-picker"
label: "Value Color"
default_value: "black"
}
option fillColor {
type: "color-picker"
label: "Fill Color"
default_value: "#3b60d2"
}
option needle_color {
type: "color-picker"
label: "Needle Color"
default_value: "black"
}
option needleScale {
label: 'Needle Scaling Factor'
type: 'number-input'
default_value: 0.8
}
option showLabels {
label: 'Show Labels'
type: 'toggle'
default_value: true
}
option showTicks {
label: 'Show Ticks'
type: 'toggle'
default_value: true
}
option tickGaps {
label: 'Tick Gaps'
type: 'number-input'
default_value: 0.25
}
}
template: @vgl {
"config": {
"padding": {"left": 0, "top": 0, "right": 0, "bottom": 0},
"autosize": "fit",
"view": {
"stroke": "transparent"
},
"font": "Inter"
},
"params": [
{"name": "centerX", "expr": "width/2"},
{"name": "centerY", "expr": "height/2 + outerRadius/2"},
{"name": "outerRadius", "expr": "radiusRef"},
{"name": "radiusRef", "expr": "min(width/2, height/2)"},
{"name": "innerRadius", "expr": "outerRadius - outerRadius * 0.25"},
{"name": "fontFactor", "expr": "radiusRef/5"},
{"name": "backgroundColor", "value": @{options.background_color.value}},
{"name": "fillColor", "value": @{options.fillColor.value}},
{"name": "needleColor", "value": @{options.needle_color.value}},
{"name": "needleSize", "expr": "innerRadius * @{options.needleScale.value}"}
],
"data": {
"values": @{values}
},
"transform": [
{"calculate": "datum['@{fields.value.name}'] / datum['@{fields.max_value.name}']", "as": "percentage"},
{"calculate": "(datum.percentage) * (PI) + (-PI/2)", "as": "arcValue"}
],
"layer": [
// Base Gauge
{
"mark": {
"type": "arc",
"name": "gauge",
"theta": {"expr": "-PI/2"},
"theta2": {"expr": "PI/2"},
"x": {"expr": "centerX"},
"y": {"expr": "centerY"},
"innerRadius": {"expr": "innerRadius"},
"outerRadius": {"expr": "outerRadius"},
"fill": {"expr": "backgroundColor"},
"stroke": {"expr": "backgroundColor"},
"strokeWidth": 1,
}
},
// Value Gauge
{
"mark": {
"type": "arc",
"name": "gauge",
"theta": {"expr": "-PI/2"},
"theta2": {"expr": "datum.arcValue"},
"x": {"expr": "centerX"},
"y": {"expr": "centerY"},
"innerRadius": {"expr": "innerRadius"},
"outerRadius": {"expr": "outerRadius"},
"fill": {"expr": "fillColor"}
}
},
// Label
{
"transform": [
{"calculate": "[0, datum['@{fields.max_value.name}']]", "as": "label"},
{"flatten": ["label"]},
{"calculate": "datum.label === 0 ? 0 : 1", "as": "angle"}
],
"encoding": {
"text": {
"field": "label",
"format": @{fields.max_value.format},
"formatType": "holisticsFormat"
},
"theta": {
"field": "angle",
"type": "quantitative",
"scale": {"domain": [0, 1], "range": [{"expr": "-PI/2"}, {"expr": "PI/2"}]}
}
},
"mark": {
"type": "text",
"fontSize": {"expr": "fontFactor/2"},
"fontWeight": 600,
"x": {"expr": "centerX"},
"y": {"expr": "centerY"},
"radius": {"expr": "outerRadius*1.05"},
"align": {"expr": "datum.angle === 0 ? 'right' : 'left'"},
"baseline": "alphabetic",
"opacity": {"expr": "@{options.showLabels.value} ? 1 : 0"}
}
},
// Main Value
{
"transform": [
{"calculate": "datum['@{fields.value.name}'] + ' (' + format(datum.percentage, '.0%') + ')'", "as": "mainValue"}
],
"encoding": {
"text": {
"field": "mainValue"
}
},
"mark": {
"type": "text",
"fontSize": {"expr": "fontFactor"},
"fontWeight": 600,
"x": {"expr": "centerX"},
"y": {"expr": "centerY + fontFactor*1.2"},
"baseline": "alphabetic",
"align": "center",
"color": @{options.value_color.value}
}
},
// Needle
{
"encoding": {
"angle": {
"field": "percentage",
"type": "quantitative",
"scale": {"domain": [0, 1], "range": [-90, 90]}
}
},
"mark": {
"type": "point",
"shape": {
"expr": "'M 5 -2 A 7 7 0 0 1 -10 -7 L 0 -' + toString(needleSize) + ' Z'"
},
"size": 4,
"opacity": 1,
"fill": {"expr": "needleColor"},
"stroke": {"expr": "needleColor"},
"x": {"expr": "centerX"},
"y": {"expr": "centerY"}
}
},
// Ticks
{
"data": {
"sequence": {
"as": "ticks",
"start": 0,
"stop": 1.01,
"step": @{options.tickGaps.value}
}
},
"encoding": {
"theta": {
"field": "ticks",
"type": "quantitative",
"scale": {"domain": [0, 1], "range": [{"expr": "-PI/2"}, {"expr": "PI/2"}]}
},
"theta2": {
"field": "ticks",
"type": "quantitative",
"scale": {"domain": [0, 1], "range": [{"expr": "-PI/2"}, {"expr": "PI/2"}]}
}
},
"mark": {
"type": "arc",
"outerRadius": {"expr": "outerRadius"},
"innerRadius": {"expr": "innerRadius"},
"x": {"expr": "centerX"},
"y": {"expr": "centerY"},
"stroke": {"expr": "needleColor"},
"opacity": {"expr": "@{options.showTicks.value} ? 1 : 0"}
}
}
]
};;
}

Gauge Chart with Target

reporting-custom-chart/gauge-chart-with-target
Legacy syntax
CustomChart {
fields {
field value {
type: "measure"
label: "Value"
}
field max_value {
type: "measure"
label: "Max Value"
}
field target {
type: "measure"
label: "Target"
}
}

options {
option background_color {
type: "color-picker"
label: "Background Color"
default_value: "#cbd1d6"
}
option fillColor {
type: "color-picker"
label: "Fill Color"
default_value: "#3b60d2"
}
option value_color {
type: "color-picker"
label: "Value Color"
default_value: "black"
}
option needle_color {
type: "color-picker"
label: "Needle Color"
default_value: "black"
}
option needleScale {
label: 'Needle Scaling Factor'
type: 'number-input'
default_value: 0.8
}
option showLabels {
label: 'Show Labels'
type: 'toggle'
default_value: false
}
option showTicks {
label: 'Show Ticks'
type: 'toggle'
default_value: false
}
option tickGaps {
label: 'Tick Gaps'
type: 'number-input'
default_value: 0.25
}
option targetColor {
type: "color-picker"
label: "Target Color"
default_value: "#5fc93c"
}
}
template: @vgl {
"config": {
"padding": {"left": 0, "top": 0, "right": 0, "bottom": 0},
"autosize": "fit",
"view": {
"stroke": "transparent"
},
"font": "Inter"
},
"params": [
{"name": "centerX", "expr": "width/2"},
{"name": "centerY", "expr": "height/2 + outerRadius/2"},
{"name": "outerRadius", "expr": "radiusRef"},
{"name": "radiusRef", "expr": "min(width/2, height/2)"},
{"name": "innerRadius", "expr": "outerRadius - outerRadius * 0.25"},
{"name": "fontFactor", "expr": "radiusRef/5"},
{"name": "backgroundColor", "value": @{options.background_color.value}},
{"name": "fillColor", "value": @{options.fillColor.value}},
{"name": "needleColor", "value": @{options.needle_color.value}},
{"name": "needleSize", "expr": "innerRadius * @{options.needleScale.value}"},
{"name": "targetColor", "value": @{options.targetColor.value}}
],
"data": {
"values": @{values}
},
"transform": [
{"calculate": "datum['@{fields.value.name}'] / datum['@{fields.max_value.name}']", "as": "percentage"},
{"calculate": "(datum.percentage) * (PI) + (-PI/2)", "as": "arcValue"},
{"calculate": "datum['@{fields.target.name}'] / datum['@{fields.max_value.name}']", "as": "targetPercentage"}
],
"encoding": {
"tooltip": [
{"field": @{fields.value.name}, "format": @{fields.value.format}, "formatType": "holisticsFormat"},
{"field": @{fields.target.name}, "format": @{fields.target.format}, "formatType": "holisticsFormat"},
{"field": @{fields.max_value.name}, "format": @{fields.max_value.format}, "formatType": "holisticsFormat"}
]
},
"layer": [
// Base Gauge
{
"mark": {
"type": "arc",
"name": "gauge",
"theta": {"expr": "-PI/2"},
"theta2": {"expr": "PI/2"},
"x": {"expr": "centerX"},
"y": {"expr": "centerY"},
"innerRadius": {"expr": "innerRadius"},
"outerRadius": {"expr": "outerRadius"},
"fill": {"expr": "backgroundColor"},
"stroke": {"expr": "backgroundColor"},
"strokeWidth": 1,
}
},
// Value Gauge
{
"mark": {
"type": "arc",
"name": "gauge",
"theta": {"expr": "-PI/2"},
"theta2": {"expr": "datum.arcValue"},
"x": {"expr": "centerX"},
"y": {"expr": "centerY"},
"innerRadius": {"expr": "innerRadius"},
"outerRadius": {"expr": "outerRadius"},
"fill": {"expr": "fillColor"},
}
},
// Label
{
"transform": [
{"calculate": "[0, datum['@{fields.max_value.name}']]", "as": "label"},
{"flatten": ["label"]},
{"calculate": "datum.label === 0 ? 0 : 1", "as": "angle"}
],
"encoding": {
"text": {
"field": "label",
"format": @{fields.max_value.format},
"formatType": "holisticsFormat"
},
"theta": {
"field": "angle",
"type": "quantitative",
"scale": {"domain": [0, 1], "range": [{"expr": "-PI/2"}, {"expr": "PI/2"}]}
}
},
"mark": {
"type": "text",
"fontSize": {"expr": "fontFactor/2"},
"fontWeight": 600,
"x": {"expr": "centerX"},
"y": {"expr": "centerY"},
"radius": {"expr": "outerRadius*1.05"},
"align": {"expr": "datum.angle === 0 ? 'right' : 'left'"},
"baseline": "alphabetic",
"opacity": {"expr": "@{options.showLabels.value} ? 1 : 0"}
}
},
// Main Value
{
"transform": [
{"calculate": "datum['@{fields.value.name}'] + ' (' + format(datum.percentage, '.0%') + ')'", "as": "mainValue"},
{
"calculate": "format(datum['@{fields.value.name}'] - datum['@{fields.target.name}'], ',.0f') + ' (' + format(datum['@{fields.value.name}']/datum['@{fields.target.name}'] - 1, '.0%') + ')'",
"as": "reference"
}
],
"encoding": {
"text": {
"field": "mainValue"
}
},
"mark": {
"type": "text",
"fontSize": {"expr": "fontFactor"},
"fontWeight": 600,
"x": {"expr": "centerX"},
"y": {"expr": "centerY + fontFactor*1.2"},
"baseline": "alphabetic",
"align": "center",
"color": @{options.value_color.value}
}
},
// Needle
{
"encoding": {
"angle": {
"field": "percentage",
"type": "quantitative",
"scale": {"domain": [0, 1], "range": [-90, 90]}
}
},
"mark": {
"type": "point",
"shape": {
"expr": "'M 5 -2 A 7 7 0 0 1 -10 -7 L 0 -' + toString(needleSize) + ' Z'"
},
"size": 4,
"opacity": 1,
"fill": {"expr": "needleColor"},
"stroke": {"expr": "needleColor"},
"x": {"expr": "centerX"},
"y": {"expr": "centerY"}
}
},
// Ticks
{
"data": {
"sequence": {
"as": "ticks",
"start": 0,
"stop": 1.01,
"step": @{options.tickGaps.value}
}
},
"encoding": {
"theta": {
"field": "ticks",
"type": "quantitative",
"scale": {"domain": [0, 1], "range": [{"expr": "-PI/2"}, {"expr": "PI/2"}]}
},
"theta2": {
"field": "ticks",
"type": "quantitative",
"scale": {"domain": [0, 1], "range": [{"expr": "-PI/2"}, {"expr": "PI/2"}]}
}
},
"mark": {
"type": "arc",
"outerRadius": {"expr": "outerRadius"},
"innerRadius": {"expr": "innerRadius"},
"x": {"expr": "centerX"},
"y": {"expr": "centerY"},
"stroke": {"expr": "needleColor"},
"opacity": {"expr": "@{options.showTicks.value} ? 1 : 0"}
}
},
// Target Gauge
{
"encoding": {
"theta": {
"field": "targetPercentage",
"type": "quantitative",
"scale": {"domain": [0, 1], "range": [{"expr": "-PI/2"}, {"expr": "PI/2"}]}
},
"theta2": {
"field": "targetPercentage",
"type": "quantitative",
"scale": {"domain": [0, 1], "range": [{"expr": "-PI/2"}, {"expr": "PI/2"}]}
}
},
"mark": {
"type": "arc",
"outerRadius": {"expr": "outerRadius*1.05"},
"innerRadius": {"expr": "innerRadius*0.95"},
"x": {"expr": "centerX"},
"y": {"expr": "centerY"},
"stroke": {"expr": "targetColor"},
"strokeWidth": 2
}
}
]
};;
}

Required fields

Both variants take a single row of measures. The Simple Gauge Chart expects two fields; the Gauge Chart with Target expects three. The template draws the value as a needle and an arc filling toward the maximum.

Simple Gauge Chart:

FieldLabelTypeRole
valueValuemeasureCurrent metric, drawn as the filled arc and needle. Sorted descending (apply_order: 1).
max_valueMax ValuemeasureTop of the gauge range (the 100% end). Sorted descending (apply_order: 2).

Gauge Chart with Target:

FieldLabelTypeRole
valueValuemeasureCurrent metric, drawn as the filled arc and needle. Sorted descending (apply_order: 1).
max_valueMax ValuemeasureTop of the gauge range (the 100% end). Sorted descending (apply_order: 2).
targetTargetmeasureGoal value, drawn as a colored marker on the arc. Sorted descending (apply_order: 3).

Data requirements: Provide a single row. The template reads one record and computes value / max_value as the fill percentage, so pre-aggregate to one value per measure. Keep max_value greater than zero, since it is the divisor for the gauge percentage.

Sample data:

Simple Gauge Chart:

valuemax_value
7201000

Gauge Chart with Target:

valuemax_valuetarget
7201000850

Options

Set these options to adjust the chart without editing the Vega template. The CustomChartDef block above declares each option's type and allowed values.

Both variants share these options:

OptionDefaultEffect
background_color#cbd1d6Color of the unfilled background arc.
value_colorblackColor of the main value label text.
fillColor#3b60d2Color of the filled arc up to the current value.
needle_colorblackColor of the needle (and tick marks).
needleScale0.8Needle length as a fraction of the inner radius.
showLabelstrueToggles the 0 and max-value end labels. Defaults to false in the Target variant.
showTickstrueToggles the evenly spaced tick marks. Defaults to false in the Target variant.
tickGaps0.25Spacing between ticks as a fraction of the range (smaller means more ticks).

The Gauge Chart with Target adds one more option:

OptionDefaultEffect
targetColor#5fc93cColor of the target marker on the arc.

Known limitations

  • Single value only. Each gauge renders one record. To compare categories, use a separate gauge per value or switch to a bar or bullet chart.

  • max_value must be greater than zero. It is the divisor for the fill percentage, so a zero or missing maximum produces an invalid gauge.

  • Half-circle range fixed at 0 to max. The arc always runs from 0 to max_value across a 180-degree sweep. Custom minimums or full-circle gauges require editing the template.


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