Introduction

Labels in bar charts often face readability issues when the chart is resized, displayed on different screen resolutions, or zoomed in/out using amCharts zoom controls. By default, labels in amCharts 4 have a fixed width, which can lead to overlapping, truncation, or unreadable text on small screens.

To make labels truly responsive, we need to:

  • Dynamically calculate the maximum width of labels based on the chart size and column width.
  • Truncate long labels if necessary to prevent overflow.
  • Use event hooks to ensure labels adjust properly when:
    • The chart resizes.
    • Zooming/panning occurs.
    • Data updates dynamically.

In this article, we’ll explore how to adjust label width dynamically and truncate labels when needed using amCharts 4 events.

An example of overlapping labels and lack of responsiveness in a column chart

Implementation code with the issue:


am4core.ready(function () {
    // Create the chart instance
    let chart = am4core.create("chartdiv", am4charts.XYChart);
    chart.hiddenState.properties.opacity = 0; // Initial fade-in
    chart.logo.disabled = true;
    chart.responsive.enabled = true; // Enable responsive behavior

    // Chart data
    chart.data = [
      { company: "Apple Inc.", visits: 23725 },
      { company: "Microsoft Corporation", visits: 1882 },
      { company: "Alphabet Inc. (Google)", visits: 1809 },
      { company: "Amazon.com, Inc.", visits: 1322 },
      { company: "Tesla, Inc.", visits: 1122 },
      { company: "Meta Platforms, Inc. (Facebook)", visits: 1114 },
      { company: "Berkshire Hathaway Inc.", visits: 711 },
      { company: "PepsiCo, Inc.", visits: 443 },
      { company: "Johnson & Johnson Services, Inc.", visits: 350 },
      { company: "McDonald's Corporation", visits: 298 },
      { company: "NVIDIA Corporation", visits: 150 },
      { company: "Netflix, Inc.", visits: 100 }
    ];

    // Create Category Axis (X-axis)
    let categoryAxis = chart.xAxes.push(new am4charts.CategoryAxis());
    categoryAxis.dataFields.category = "company";
    categoryAxis.renderer.grid.template.location = 0;
    categoryAxis.renderer.minGridDistance = 40;
    categoryAxis.fontSize = 11;
    categoryAxis.renderer.labels.template.wrap = true;
    categoryAxis.renderer.labels.template.maxWidth = 80;
    categoryAxis.renderer.labels.template.padding(10, 0, 0, 0); // Set all sides to 0

    // Create Value Axis (Y-axis)
    let valueAxis = chart.yAxes.push(new am4charts.ValueAxis());
    valueAxis.min = 0;
    valueAxis.max = 24000;
    valueAxis.strictMinMax = true;
    valueAxis.renderer.minGridDistance = 30;

    // Create Column Series
    let series = chart.series.push(new am4charts.ColumnSeries());
    series.dataFields.categoryX = "company";
    series.dataFields.valueY = "visits";
    series.columns.template.tooltipText = "{valueY.value}";
    series.columns.template.tooltipY = 0;
    series.columns.template.strokeOpacity = 0;

    // Apply different colors to each column
    series.columns.template.adapter.add("fill", function (fill, target) {
      return chart.colors.getIndex(target.dataItem.index);
    });

    // Add Scrollbar
    chart.scrollbarX = new am4core.Scrollbar();
    chart.scrollbarX.marginTop = 30;
    chart.scrollbarX.parent = chart.bottomAxesContainer;

    // Add Axis Break for large values
    let axisBreak = valueAxis.axisBreaks.create();
    axisBreak.startValue = 2100;
    axisBreak.endValue = 22900;

    // Adjust Axis Break Size
    let d = (axisBreak.endValue - axisBreak.startValue) / (valueAxis.max - valueAxis.min);
    axisBreak.breakSize = 0.05 * (1 - d) / d; // Increased break size for better visibility

    // Adjust hover effect for better interaction
    let hoverState = axisBreak.states.create("hover");
    hoverState.properties.breakSize = 1; // Slightly larger break when hovered
    hoverState.properties.opacity = 0.1;
    hoverState.transitionDuration = 1500;

    axisBreak.defaultState.transitionDuration = 1000;

}); // end am4core.ready()

Setting a Responsive Max Width for Labels

In amCharts 4, labels for the xAxis in a column chart have a fixed maxWidth. However, since this width does not adjust dynamically, we must calculate it based on the column width and chart size.

To achieve this, we hook into the rangechangeended and sizechanged event, check the width of the columns, and set the maxWidth accordingly.

Implementation

// Function to update label max width based on the widest column
const updateLabelMaxWidth = () => {
  let maxColumnWidth = 0;

  // Loop through each column and find the maximum width
  series.columns.each(function (column) {
    if (column && column.pixelWidth > maxColumnWidth) {
      maxColumnWidth = column.pixelWidth;
    }
  });

  // Apply max width to labels
  categoryAxis.renderer.labels.template.maxWidth = maxColumnWidth;
  console.log("Updated Max Label Width: " + maxColumnWidth);

  // Refresh the chart to apply updated label width
  chart.invalidateRawData();
}

// Trigger label width update on zoom/pan
categoryAxis.events.on("rangechangeended", function () {
  updateLabelMaxWidth();
});

// Trigger label width update on screen resize
chart.events.on("sizechanged", function () {
  updateLabelMaxWidth();
});

Explanation

The function updateLabelMaxWidth dynamically adjusts x-axis label width based on the widest column in the chart. It loops through all columns, finds the maximum width, and applies it to the labels to prevent overlapping.

Event Hooks:

  • rangechangeended → Updates label width when zooming/panning.
  • sizechanged → Adjusts labels when the chart is resized.

This ensures that labels resize dynamically based on the available space.

Truncating Long Labels for Better Readability

Even with a dynamic max width, some labels may still be too long and can cause clutter. To handle this, we truncate labels when they exceed a certain length.

Implementation

categoryAxis.renderer.labels.template.adapter.add("textOutput", function(text, target) {
    let maxLabelLength = 10; // Set the maximum number of characters before truncation

    if (text.length > maxLabelLength) {
        return text.substring(0, maxLabelLength) + "..."; // Truncate and add ellipsis
    }
    return text; // Return original text if it's within the limit
});

Explanation

  • This function checks the length of each label.
  • If the label is too long, it truncates the text to first word and adds ".." at the end.
  • Labels within the max character limit remain unchanged.

This ensures that long labels don’t overlap while still providing enough information and staying responsive when resized or zoomed in/out.

Final result after applying dynamic label adjustments

Full Implementation code after applying the solution


am4core.ready(function () {
  // Create the chart instance
  let chart = am4core.create("chartdiv", am4charts.XYChart);
  chart.hiddenState.properties.opacity = 0; // Initial fade-in
  chart.logo.disabled = true;
  chart.responsive.enabled = true; // Enable responsive behavior

  // Chart data
  chart.data = [
    { company: "Apple Inc.", visits: 23725 },
    { company: "Microsoft Corporation", visits: 1882 },
    { company: "Alphabet Inc. (Google)", visits: 1809 },
    { company: "Amazon.com, Inc.", visits: 1322 },
    { company: "Tesla, Inc.", visits: 1122 },
    { company: "Meta Platforms, Inc. (Facebook)", visits: 1114 },
    { company: "Berkshire Hathaway Inc.", visits: 711 },
    { company: "PepsiCo, Inc.", visits: 443 },
    { company: "Johnson & Johnson Services, Inc.", visits: 350 },
    { company: "McDonald's Corporation", visits: 298 },
    { company: "NVIDIA Corporation", visits: 150 },
    { company: "Netflix, Inc.", visits: 100 }
  ];

  // Create Category Axis (X-axis)
  let categoryAxis = chart.xAxes.push(new am4charts.CategoryAxis());
  categoryAxis.dataFields.category = "company";
  categoryAxis.renderer.grid.template.location = 0;
  categoryAxis.renderer.minGridDistance = 40;
  categoryAxis.fontSize = 11;
  categoryAxis.renderer.labels.template.wrap = true;
  categoryAxis.renderer.labels.template.maxWidth = 80;
  categoryAxis.renderer.labels.template.padding(10, 0, 0, 0); // Set all sides to 0

  // Create Value Axis (Y-axis)
  let valueAxis = chart.yAxes.push(new am4charts.ValueAxis());
  valueAxis.min = 0;
  valueAxis.max = 24000;
  valueAxis.strictMinMax = true;
  valueAxis.renderer.minGridDistance = 30;

  // Function to update label max width based on the widest column
  const updateLabelMaxWidth = () => {
    let maxColumnWidth = 0;

    // Loop through each column and find the maximum width
    series.columns.each(function (column) {
      if (column && column.pixelWidth > maxColumnWidth) {
        maxColumnWidth = column.pixelWidth;
      }
    });

    // Apply max width to labels
    categoryAxis.renderer.labels.template.maxWidth = maxColumnWidth;
    console.log("Updated Max Label Width: " + maxColumnWidth);

    // Refresh the chart to apply updated label width
    chart.invalidateRawData();
  }

  // Trigger label width update on zoom/pan
  categoryAxis.events.on("rangechangeended", function () {
    updateLabelMaxWidth();
  });

  // Trigger label width update on screen resize
  chart.events.on("sizechanged", function () {
    updateLabelMaxWidth();
  });

  // Truncate labels at the end of the first word when necessary
  categoryAxis.renderer.labels.template.adapter.add("textOutput", function (text, target) {
    let labelMaxWidth = target.maxWidth;

    if (labelMaxWidth < 60 && text) { // Check if label width is less than 60 pixels
      let words = text.split(" "); // Split text into words
      return words.length > 1 ? words[0] + ".." : text; // Keep only the first word with ".."
    }

    return text;
  });

  // Create Column Series
  let series = chart.series.push(new am4charts.ColumnSeries());
  series.dataFields.categoryX = "company";
  series.dataFields.valueY = "visits";
  series.columns.template.tooltipText = "{valueY.value}";
  series.columns.template.tooltipY = 0;
  series.columns.template.strokeOpacity = 0;

  // Apply different colors to each column
  series.columns.template.adapter.add("fill", function (fill, target) {
    return chart.colors.getIndex(target.dataItem.index);
  });

  // Add Scrollbar
  chart.scrollbarX = new am4core.Scrollbar();
  chart.scrollbarX.marginTop = 30;
  chart.scrollbarX.parent = chart.bottomAxesContainer;

  // Add Axis Break for large values
  let axisBreak = valueAxis.axisBreaks.create();
  axisBreak.startValue = 2100;
  axisBreak.endValue = 22900;

  // Adjust Axis Break Size
  let d = (axisBreak.endValue - axisBreak.startValue) / (valueAxis.max - valueAxis.min);
  axisBreak.breakSize = 0.05 * (1 - d) / d; // Increased break size for better visibility

  // Adjust hover effect for better interaction
  let hoverState = axisBreak.states.create("hover");
  hoverState.properties.breakSize = 1; // Slightly larger break when hovered
  hoverState.properties.opacity = 0.1;
  hoverState.transitionDuration = 1500;

  axisBreak.defaultState.transitionDuration = 1000;

}); // end am4core.ready()

With these tweaks, our chart now adapts perfectly—labels stay readable, no more ugly overlaps, and everything resizes smoothly. By dynamically adjusting widths, truncating long names, and hooking into events, we’ve made sure the chart looks great on any screen. Now, go ahead and try it out!