Creating a Bullet Chart

This short tutorial will show how to easily create a bullet chart.

The chart

Let's start with the complete code to the chart, and dissect the code afterwards:

/* Create chart instance */
let chart = am4core.create("chartdiv", am4charts.XYChart);
chart.paddingRight = 25;

/* Add data */
chart.data = [{
  "category": "Research",
  "value": 45,
  "target": 80
}, {
  "category": "Marketing",
  "value": 60,
  "target": 75
}, {
  "category": "Distribution",
  "value": 92,
  "target": 96
}];

/* Create axes */
let categoryAxis = chart.yAxes.push(new am4charts.CategoryAxis());
categoryAxis.dataFields.category = "category";
categoryAxis.renderer.minGridDistance = 30;
categoryAxis.renderer.grid.template.disabled = true;

let valueAxis = chart.xAxes.push(new am4charts.ValueAxis());
valueAxis.renderer.minGridDistance = 30;
valueAxis.renderer.grid.template.disabled = true;
valueAxis.min = 0;
valueAxis.max = 100;
valueAxis.strictMinMax = true;
valueAxis.renderer.labels.template.adapter.add("text", function(text) {
  return text + "%";
});

/* Create ranges */
function createRange(axis, from, to, color) {
  let range = axis.axisRanges.create();
  range.value = from;
  range.endValue = to;
  range.axisFill.fill = color;
  range.axisFill.fillOpacity = 0.8;
  range.label.disabled = true;
}

createRange(valueAxis, 0, 20, am4core.color("#19d228"));
createRange(valueAxis, 20, 40, am4core.color("#b4dd1e"));
createRange(valueAxis, 40, 60, am4core.color("#f4fb16"));
createRange(valueAxis, 60, 80, am4core.color("#f6d32b"));
createRange(valueAxis, 80, 100, am4core.color("#fb7116"));

/* Create series */
let series = chart.series.push(new am4charts.ColumnSeries());
series.dataFields.valueX = "value";
series.dataFields.categoryY = "category";
series.columns.template.fill = am4core.color("#000");
series.columns.template.stroke = am4core.color("#fff");
series.columns.template.strokeWidth = 1;
series.columns.template.strokeOpacity = 0.5;
series.columns.template.height = am4core.percent(25);

let series2 = chart.series.push(new am4charts.LineSeries());
series2.dataFields.valueX = "target";
series2.dataFields.categoryY = "category";
series2.strokeWidth = 0;

let bullet = series2.bullets.push(new am4charts.Bullet());
let line = bullet.createChild(am4core.Line);
line.x1 = 0;
line.y1 = -15;
line.x2 = 0;
line.y2 = 15;
line.stroke = am4core.color("#000");
line.strokeWidth = 4;
/* Create chart instance */
var chart = am4core.create("chartdiv", am4charts.XYChart);
chart.paddingRight = 25;

/* Add data */
chart.data = [{
  "category": "Research",
  "value": 45,
  "target": 80
}, {
  "category": "Marketing",
  "value": 60,
  "target": 75
}, {
  "category": "Distribution",
  "value": 92,
  "target": 96
}];

/* Create axes */
var categoryAxis = chart.yAxes.push(new am4charts.CategoryAxis());
categoryAxis.dataFields.category = "category";
categoryAxis.renderer.minGridDistance = 30;
categoryAxis.renderer.grid.template.disabled = true;

var valueAxis = chart.xAxes.push(new am4charts.ValueAxis());
valueAxis.renderer.minGridDistance = 30;
valueAxis.renderer.grid.template.disabled = true;
valueAxis.min = 0;
valueAxis.max = 100;
valueAxis.strictMinMax = true;
valueAxis.renderer.labels.template.adapter.add("text", function(text) {
  return text + "%";
});

/* Create ranges */
function createRange(axis, from, to, color) {
  var range = axis.axisRanges.create();
  range.value = from;
  range.endValue = to;
  range.axisFill.fill = color;
  range.axisFill.fillOpacity = 0.8;
  range.label.disabled = true;
}

createRange(valueAxis, 0, 20, am4core.color("#19d228"));
createRange(valueAxis, 20, 40, am4core.color("#b4dd1e"));
createRange(valueAxis, 40, 60, am4core.color("#f4fb16"));
createRange(valueAxis, 60, 80, am4core.color("#f6d32b"));
createRange(valueAxis, 80, 100, am4core.color("#fb7116"));

/* Create series */
var series = chart.series.push(new am4charts.ColumnSeries());
series.dataFields.valueX = "value";
series.dataFields.categoryY = "category";
series.columns.template.fill = am4core.color("#000");
series.columns.template.stroke = am4core.color("#fff");
series.columns.template.strokeWidth = 1;
series.columns.template.strokeOpacity = 0.5;
series.columns.template.height = am4core.percent(25);

var series2 = chart.series.push(new am4charts.LineSeries());
series2.dataFields.valueX = "target";
series2.dataFields.categoryY = "category";
series2.strokeWidth = 0;

var bullet = series2.bullets.push(new am4charts.Bullet());
var line = bullet.createChild(am4core.Line);
line.x1 = 0;
line.y1 = -15;
line.x2 = 0;
line.y2 = 15;
line.stroke = am4core.color("#000");
line.strokeWidth = 4;
am4core.createFromConfig({
  "paddingRight": 25,

  /* Add data */
  "data": [{
    "category": "Research",
    "value": 45,
    "target": 80
  }, {
    "category": "Marketing",
    "value": 60,
    "target": 75
  }, {
    "category": "Distribution",
    "value": 92,
    "target": 96
  }],

  /* Create axes */
  "yAxes": [{
    "type": "CategoryAxis",
    "dataFields": {
      "category": "category"
    },
    "renderer": {
      "minGridDistance": 30,
      "grid": {
        "disabled": "true"
      }
    }
  }],

  "xAxes": [{
    "type": "ValueAxis",
    "min": 0,
    "max": 100,
    "strictMinMax": true,
    "renderer": {
      "minGridDistance": 30,
      "grid": {
        "disabled": true
      },
      "labels": {
        "adapter": {
          "text": function(text) {
            return text + "%";
          }
        }
      }
    },
    "axisRanges":[{
      "value": 0,
      "endValue": 20,
      "axisFill": {
        "fill": "#19d228",
        "fillOpacity": 0.8
      },
      "label": {
        "disabled": true
      }
    }, {
      "value": 20,
      "endValue": 40,
      "axisFill": {
        "fill": "#19d228",
        "fillOpacity": 0.8
      },
      "label": {
        "disabled": true
      }
    }, {
      "value": 40,
      "endValue": 60,
      "axisFill": {
        "fill": "#19d228",
        "fillOpacity": 0.8
      },
      "label": {
        "disabled": true
      }
    }, {
      "value": 60,
      "endValue": 80,
      "axisFill": {
        "fill": "#19d228",
        "fillOpacity": 0.8
      },
      "label": {
        "disabled": true
      }
    }, {
      "value": 80,
      "endValue": 100,
      "axisFill": {
        "fill": "#19d228",
        "fillOpacity": 0.8
      },
      "label": {
        "disabled": true
      }
    }]
  }],

  /* Create series */
  "series": [{
    "type": "ColumnSeries",
    "dataFields": {
      "valueX": "value",
      "categoryY": "category"
    },
    "columns": {
      "fill": "#000",
      "stroke": "#fff",
      "strokeWidth": 1,
      "strokeOpacity": 0.5,
      "height": "25%"
    }
  }, {
    "type": "LineSeries",
    "strokeWidth": 0,
    "dataFields": {
      "valueX": "target",
      "categoryY": "category"
    },
    "bullets": [{
      "type": "Bullet",
      "children": [{
        "type": "Line",
        "x1": 0,
        "y1": -15,
        "x2": 0,
        "y2": 15,
        "stroke": "#000",
        "strokeWidth": 4
      }]
    }]
  }]
}, "chartdiv", am4charts.XYChart);

See the Pen amCharts 4: Bullet chart by amCharts (@amcharts) on CodePen.24419

If you run the above example, you'll see that we have three narrow bars streaming towards target threshold lines, over striped background.

Spoiler alert! For the former two, we are using Series, and for the latter we are using Axis Ranges.

Setting up axes

Since we want our bars to be horizontal, we add a ValueAxis to xAxes and a CategoryAxis to yAxes.

Both axes have their grid disabled:

categoryAxis.renderer.grid.template.disabled = true;
// ...
valueAxis.renderer.grid.template.disabled = true;
categoryAxis.renderer.grid.template.disabled = true;
// ...
valueAxis.renderer.grid.template.disabled = true;
{
  // ...
  "yAxes": [{
    // ...
    "renderer": {
      // ...
      "grid": {
        "disabled": "true"
      }
    }
  }],

  "xAxes": [{
    // ...
    "renderer": {
      // ...
      "grid": {
        "disabled": true
      }
    },
    }]
  }],
  // ...
}

We also make Value axis to strictly from 0 to 100, as well as slap an Adapter on it to append "%" to its values:

valueAxis.min = 0;
valueAxis.max = 100;
valueAxis.strictMinMax = true;
valueAxis.renderer.labels.template.adapter.add("text", function(text) {
  return text + "%";
});
valueAxis.min = 0;
valueAxis.max = 100;
valueAxis.strictMinMax = true;
valueAxis.renderer.labels.template.adapter.add("text", function(text) {
  return text + "%";
});
{
  // ...
  "yAxes": [{
    // ...
    "renderer": {
      // ...
      "grid": {
        "disabled": "true"
      }
    }
  }],

  "xAxes": [{
    "type": "ValueAxis",
    "min": 0,
    "max": 100,
    "strictMinMax": true,
    "renderer": {
      // ...
      "labels": {
        "adapter": {
          "text": function(text) {
            return text + "%";
          }
        }
      }
    },
    // ...
  }],
  // ...
}

Adding axis ranges

We are using Axis Ranges for the colored stripes in the background. Axis ranges are powerful tools, capable of much more than coloring backgrounds, however, for the purpose of this demo, we're going to be using them just like that.

As the name implies, an Axis Range must be attached to an Axis. In this case we need them to attach to a horizontal axis, which happens to be a Value axis.

We create 5 ranges to color different ranges of values with different color:

function createRange(axis, from, to, color) {
  let range = axis.axisRanges.create();
  range.value = from;
  range.endValue = to;
  range.axisFill.fill = color;
  range.axisFill.fillOpacity = 0.8;
  range.label.disabled = true;
}

createRange(valueAxis, 0, 20, am4core.color("#19d228"));
createRange(valueAxis, 20, 40, am4core.color("#b4dd1e"));
createRange(valueAxis, 40, 60, am4core.color("#f4fb16"));
createRange(valueAxis, 60, 80, am4core.color("#f6d32b"));
createRange(valueAxis, 80, 100, am4core.color("#fb7116"));
function createRange(axis, from, to, color) {
  var range = axis.axisRanges.create();
  range.value = from;
  range.endValue = to;
  range.axisFill.fill = color;
  range.axisFill.fillOpacity = 0.8;
  range.label.disabled = true;
}

createRange(valueAxis, 0, 20, am4core.color("#19d228"));
createRange(valueAxis, 20, 40, am4core.color("#b4dd1e"));
createRange(valueAxis, 40, 60, am4core.color("#f4fb16"));
createRange(valueAxis, 60, 80, am4core.color("#f6d32b"));
createRange(valueAxis, 80, 100, am4core.color("#fb7116"));
{
  // ...
  "xAxes": [{
    "type": "ValueAxis",
    // ...
    "axisRanges":[{
      "value": 0,
      "endValue": 20,
      "axisFill": {
        "fill": "#19d228",
        "fillOpacity": 0.8
      },
      "label": {
        "disabled": true
      }
    }, {
      "value": 20,
      "endValue": 40,
      "axisFill": {
        "fill": "#19d228",
        "fillOpacity": 0.8
      },
      "label": {
        "disabled": true
      }
    }, {
      "value": 40,
      "endValue": 60,
      "axisFill": {
        "fill": "#19d228",
        "fillOpacity": 0.8
      },
      "label": {
        "disabled": true
      }
    }, {
      "value": 60,
      "endValue": 80,
      "axisFill": {
        "fill": "#19d228",
        "fillOpacity": 0.8
      },
      "label": {
        "disabled": true
      }
    }, {
      "value": 80,
      "endValue": 100,
      "axisFill": {
        "fill": "#19d228",
        "fillOpacity": 0.8
      },
      "label": {
        "disabled": true
      }
    }]
  }],

  // ...
}

NOTE In TypeScript/ES6 we are using a function to automate the creation of ranges. In JSON-based setup we just list each range individually.

Adding series

Now, if we would run our chart, we'd have a and empty chart with an axis and striped background.

Let's add the actual data - Series.

Bullet series

We are using ColumnSeries to create bars that get their value from value field in data.

The setup is pretty straightforward. Let's just mention that we are setting series' column template height in order to make it narrower, as opposed to wide default bars:

let series = chart.series.push(new am4charts.ColumnSeries());
series.dataFields.valueX = "value";
series.dataFields.categoryY = "category";
series.columns.template.fill = am4core.color("#000");
series.columns.template.stroke = am4core.color("#fff");
series.columns.template.strokeWidth = 1;
series.columns.template.strokeOpacity = 0.5;
series.columns.template.height = am4core.percent(25);
var series = chart.series.push(new am4charts.ColumnSeries());
series.dataFields.valueX = "value";
series.dataFields.categoryY = "category";
series.columns.template.fill = am4core.color("#000");
series.columns.template.stroke = am4core.color("#fff");
series.columns.template.strokeWidth = 1;
series.columns.template.strokeOpacity = 0.5;
series.columns.template.height = am4core.percent(25);
{
  // ...
  "series": [{
    "type": "ColumnSeries",
    "dataFields": {
      "valueX": "value",
      "categoryY": "category"
    },
    "columns": {
      "fill": "#000",
      "stroke": "#fff",
      "strokeWidth": 1,
      "strokeOpacity": 0.5,
      "height": "25%"
    }
  },
  // ...
  ]
}

Target series

We're left with the target/threshold series, that are supposed to display those lines, our bullet bars are streaming towards to.

For this last series, we're going to be using LineSeries with a couple of twists:

  • Actual line is disabled;
  • Uses Line elements as bullets.

Disabling line is easy: we just setting series' strokeWidth = 0.

Custom bullets are trickier, but no rocket science either. To add them we create a new Bullet in our series, which essentially is an empty container.

We then create Line instance in Bullet, set a few configuration options, and we're done.

let series2 = chart.series.push(new am4charts.LineSeries());
series2.dataFields.valueX = "target";
series2.dataFields.categoryY = "category";
series2.strokeWidth = 0;

let bullet = series2.bullets.push(new am4charts.Bullet());
let line = bullet.createChild(am4core.Line);
line.verticalCenter = "middle";
line.horizontalCenter = "middle";
line.x1 = 0;
line.y1 = -15;
line.x2 = 0;
line.y2 = 15;
line.stroke = am4core.color("#000");
line.strokeWidth = 4;
var series2 = chart.series.push(new am4charts.LineSeries());
series2.dataFields.valueX = "target";
series2.dataFields.categoryY = "category";
series2.strokeWidth = 0;

var bullet = series2.bullets.push(new am4charts.Bullet());
var line = bullet.createChild(am4core.Line);
line.verticalCenter = "middle";
line.horizontalCenter = "middle";
line.x1 = 0;
line.y1 = -15;
line.x2 = 0;
line.y2 = 15;
line.stroke = am4core.color("#000");
line.strokeWidth = 4;
{
  /* Create series */
  "series": [{
    // ...
  }, {
    "type": "LineSeries",
    "strokeWidth": 0,
    "dataFields": {
      "valueX": "target",
      "categoryY": "category"
    },
    "bullets": [{
      "type": "Bullet",
      "children": [{
        "type": "Line",
        "verticalCenter": "middle",
        "horizontalCenter": "middle",
        "x1": 0,
        "y1": -15,
        "x2": 0,
        "y2": 15,
        "stroke": "#000",
        "strokeWidth": 4
      }]
    }]
  }]
}

MORE INFO At this point we recommend visiting our "Bullets" article, for more tricks and info on series' bullets.