Search code examples
amchartsamcharts5

How do i sort the segments within each column for a stacked bar chart so that the largest value for each category is always at the top?


I am trying to create a bar chart using AmCharts 5 where the segments for each column are sorted from smallest to largest, so that the largest segment is always placed at the top of the column.

My chart code so far looks like this:

// Get chart div by id
  var chartDiv = document.getElementById(selector);
  const data = JSON.parse(chartDiv.getAttribute('data-chart'));

  const fontSize = 12;
  const markerSize = 12;

  var root = am5.Root.new(selector);
  var chart = root.container.children.push(
    am5xy.XYChart.new(root, {
      panY: false,
      layout: root.verticalLayout,
      dy: 10,
    })
  );

  // Create X-axis
  let xAxis = chart.xAxes.push(
    am5xy.CategoryAxis.new(root, {
      categoryField: "week_ending_date",
      renderer: am5xy.AxisRendererX.new(root, {})
    })
  );
  let xRenderer = xAxis.get("renderer");
  xRenderer.labels.template.setAll({
    fontSize: fontSize
  });
  xAxis.data.setAll(data);

  // Create Y-Axis
  var yAxis = chart.yAxes.push(
    am5xy.ValueAxis.new(root, {
      renderer: am5xy.AxisRendererY.new(root, {}),
    })
  );
  let yRenderer = yAxis.get("renderer");
  yRenderer.labels.template.setAll({
    fontSize: fontSize
  });

  // Add series
  // https://www.amcharts.com/docs/v5/charts/xy-chart/series/
  function makeSeries(name, fieldName, colorHex) {
    var series = chart.series.push(am5xy.ColumnSeries.new(root, {
      name: name,
      stacked: true,
      xAxis: xAxis,
      yAxis: yAxis,
      baseAxis: xAxis,
      valueYField: fieldName,
      categoryXField: "week_ending_date",
      fill: colorHex,
    }));

    series.columns.template.setAll({
      tooltipText: "{name} - {valueY}",
      tooltipY: am5.percent(90)
    });
    series.data.setAll(data);

    series.bullets.push(function() {
      return am5.Bullet.new(root, {
        sprite: am5.Label.new(root, {
          text: "{valueX}",
          fill: root.interfaceColors.get("alternativeText"),
          centerY: am5.p50,
          centerX: am5.p50,
          populateText: true,
          fontSize: 12,
        })
      });
    });
    legend.data.push(series);
  }

  // Add series for each site in the data

  // Get all site names
  let sites = Object.keys(data[0])
  // Remove first value (week_ending_date)
  sites.shift()
  sites.forEach((site, index) => {
    makeSeries(site, site, COLOR_SCHEME[index]);
  })

And my data looks like this:

[
  {
    "week_ending_date": "2022-10-02",
    "site A": 100,
    "site B": 150,
    "site C": 200,
  },
  {
    "week_ending_date": "2022-10-09",
    "site A": 400,
    "site B": 150,
    "site C": 50,
  },
  ...

So in my first column, I would want site C to appear at the top of it, and in the second column I want site A to be at the top.

Anyone know if this is possible and how to achieve it?

Thanks in advance.

I've tried searching through the documentation but I cant find any properties that would allow me to reorder segments.


Solution

  • A possible solution is to preprocess the data, sort the entries in a data structure and make the columns floating rather then stacked.

    This means setting a starting y value and an ending y value for each bar (that is for each data point); this allows us to place sorted columns in order: the end of the shortest will be the start of the second shortest and the end of the second shortest will be the start of the longest bar.

    Here's a snippet based on your code that implements this method:

    //var chartDiv = document.getElementById(selector);
    //const data = JSON.parse(chartDiv.getAttribute('data-chart'));
    const data = [
        {
            "week_ending_date": "2022-10-02",
            "site A": 100,
            "site B": 150,
            "site C": 200,
        },
        {
            "week_ending_date": "2022-10-09",
            "site A": 400,
            "site B": 150,
            "site C": 50,
        },
        {
            "week_ending_date": "2022-10-16",
            "site A": 200,
            "site B": 150,
            "site C": 200,
        },
        {
            "week_ending_date": "2022-10-23",
            "site A": 120,
            "site B": 150,
            "site C": 200,
        }];
    const fontSize = 12;
    //const markerSize = 12;
    
    var root = am5.Root.new(selector);
    var chart = root.container.children.push(
        am5xy.XYChart.new(root, {
            panY: false,
            layout: root.verticalLayout,
            dy: 10,
        })
    );
    
    // Create X-axis
    let xAxis = chart.xAxes.push(
        am5xy.CategoryAxis.new(root, {
            categoryField: "week_ending_date",
            renderer: am5xy.AxisRendererX.new(root, {})
        })
    );
    let xRenderer = xAxis.get("renderer");
    xRenderer.labels.template.setAll({
        fontSize: fontSize
    });
    xAxis.data.setAll(data);
    
    // Create Y-Axis
    var yAxis = chart.yAxes.push(
        am5xy.ValueAxis.new(root, {
            renderer: am5xy.AxisRendererY.new(root, {}),
        })
    );
    let yRenderer = yAxis.get("renderer");
    yRenderer.labels.template.setAll({
        fontSize: fontSize
    });
    
    // Add series
    // https://www.amcharts.com/docs/v5/charts/xy-chart/series/
    function makeSeries(name, fieldName, colorHex) {
        var series = chart.series.push(am5xy.ColumnSeries.new(root, {
            name: name,
            //stacked: false, // default
            clustered: false,
            xAxis: xAxis,
            yAxis: yAxis,
            baseAxis: xAxis,
            trueValue: data,
            valueYField: fieldName+'_end',
            openValueYField: fieldName+'_start',
            categoryXField: "week_ending_date",
            fill: colorHex,
        }));
    
        series.columns.template.setAll({
            tooltipText: "{name} - {"+fieldName+"}",
            tooltipY: am5.percent(90),
        });
        series.data.setAll(data);
    
        series.bullets.push(function() {
            return am5.Bullet.new(root, {
                sprite: am5.Label.new(root, {
                    text: "{valueX}",
                    fill: root.interfaceColors.get("alternativeText"),
                    centerY: am5.p50,
                    centerX: am5.p50,
                    populateText: true,
                    fontSize: 12,
                })
            });
        });
    
        const legend = chart.children.push(am5.Legend.new(root, {}));
        legend.data.push(series);
    }
    
    // Get all site names
    let sites = Object.keys(data[0])
    // Remove first value (week_ending_date)
    sites.shift()
    
    // preprocess data
    const dataEntriesSorted = data.map(
        o => Object.entries(o).filter(([_, v]) => typeof v === "number").
            sort(([_key1, v1], [_key2, v2]) => v1 - v2)
    );
    dataEntriesSorted.forEach(function(entries, i){
        let sum = 0;
        entries.forEach(function([key, val]){
            data[i][key+'_start'] = sum; // the start of the bar
            data[i][key+'_end'] = val + sum; // the end of the bar
            sum += data[i][key];
        })
    });
    
    // Add series for each site in the data
    sites.forEach((site, index) => {
        makeSeries(site, site, chart.get("colors").next());
    });
    <div id="selector" style="width: 400px;height: 400px"></div>
    <script src="https://cdn.amcharts.com/lib/5/index.js"></script>
    <script src="https://cdn.amcharts.com/lib/5/xy.js"></script>