Dom
Dom

Reputation: 3

Stacked, grouped column chart with variable width of x points

I'm trying to dynamically set the width of each x point based on the number of non-zero stacks with that x value.

Example:

What I have: What I have

What I want: What I want

I want to get rid of the empty spaces by lowering the width of each date. (Sep 7 should have width 2, Sep 8 and Sep 9 should have width 1, instead of all 3 dates having width 3)

Here's a more extreme example, lots of wasted space:

Real data example

I've looked into variwide charts but I can't seem to find an example of a variwide chart with both stacking and grouping. The closest question I was able to find on stackoverflow is Display Different Number of Groups in Highcharts Stacked Column Graph but that answer doesn't change the width of the points, it just centers the bars.

JSFiddle: http://jsfiddle.net/t3z76o4b/3/

$(function() {
    $('#container').highcharts({
        chart: {
            type: 'column',
        },
        legend: {
            enabled: false
        },
        title: {
            text: 'Fruits by day'
        },
        xAxis: {
            type: 'category',
            labels: {
                rotation: -45,
                formatter: function() {
                    var placeholder = Number(this.value);
                    if (!!placeholder) {
                        return ""
                    }
                    //var obj = data[this.value];
                    if (this.axis.series[0].levelNumber == 1 && !this.isFirst) {
                        return '';
                    } else {
                        return this.value;
                    }
                }
            },
            crosshair: true,
        },
        plotOptions: {
            series: {
                stacking: 'normal'
            }
        },
        series: [{
                name: "Apples",
                stack: "Apples",
                date: "7 Sep 2018",
                data: [{
                        color: "rgba(51,193,59,1)",
                        name: "7 Sep 2018",
                        y: 1
                    },
                    {
                        color: "rgba(51,193,50,0.4)",
                        name: "7 Sep 2018",
                        y: 2
                    }],
            },
            {
                name: "Blueberries",
                stack: "Blueberries",
                date: "7 Sep 2018",
                data: [{
                        color: "rgba(51,50,250,1)",
                        name: "7 Sep 2018",
                        y: 3
                    },
                    {
                        color: "rgba(51,50,250,0.4)",
                        name: "7 Sep 2018",
                        y: 1
                    }],
            },
            {
                name: "Oranges",
                stack: "Oranges",
                date: "8 Sep 2018",
                data: [{
                    color: "rgba(250,193,10,0.5)",
                    name: "8 Sep 2018",
                    y: 1
                }, ],
            },
            {
                name: "Blueberries",
                stack: "Blueberries",
                date: "9 Sep 2018",
                data: [{
                    color: "rgba(51,50,250,1)",
                    name: "9 Sep 2018",
                    y: 2
                }],
            },
        ]
    });
});

In the above JSFiddle, each series element represents a certain fruit on a certain day:

Thanks in advance!

Upvotes: 0

Views: 450

Answers (1)

Wojciech Chmiel
Wojciech Chmiel

Reputation: 7372

Unfortunately, Highcharts calculate groups space as xAxis width divided by categories length. So this space will be always equal among categories. The chart as you showed above requires a lot of changes in the core Highcharts functions and it is tricky to achieve.

Only centring columns in the group can be done with a bit of custom code:

var stacks = [
  'section 1',
  'section 2',
  'section 3',
  'section 4',
  'section 5',
  'section 6',
  'section 7'
];

var categoriesStacksColl = [];
var seriesStackColl = {};


function calculateColumnTranslate(params) {
  var m = params.stackLen / 2 + 0.5, // middle bar + offset to calc
    barIndex = params.stackIndex,
    a = Math.abs(barIndex - m), // bar offset from the middle point
    barW = params.barW,
    p = params.padding,
    posX,
    translateX;

  if (barIndex === m) {
    posX = -barW / 2;
  } else if (barIndex > m) {
    posX = a * barW + a * p - barW / 2;
  } else {
    posX = -a * barW - a * p - barW / 2;
  }

  translateX = posX - params.offset;

  return translateX;
}

    // Inside Highcharts options
  chart: {
    type: 'column',
    events: {
      load: function() {
        var chart = this,
          series = chart.series,
          categoriesLen = chart.xAxis[0].tickPositions.length,
          changeWidthFlag = false,
          seriesPoints,
          nextSeriesPoints,
          stack,
          length,
          arrIndex,
          i,
          j;

        categoriesStacksColl = [];

        // Init stacks per categories array
        for (j = 0; j < categoriesLen; j++) {
          categoriesStacksColl.push(stacks.slice());
        }

        series.forEach(function(singleSeries) {
          stack = singleSeries.options.stack;

          if (!seriesStackColl[stack]) {
            seriesStackColl[stack] = [];
          }
          seriesStackColl[stack].push(singleSeries);
        });

        stacks.forEach(function(initStack) {
          seriesPoints = seriesStackColl[initStack][0].points;
          length = seriesStackColl[initStack].length;

          seriesPoints.forEach(function(point, index) {
            if (!point.y && length === 1) {
              // increase column width
              changeWidthFlag = true;
            } else if (!point.y && length > 1) {
              changeWidthFlag = true;

              for (i = 1; i < length; i++) {
                nextSeriesPoints = seriesStackColl[initStack][i].points;

                if (nextSeriesPoints[index].y) {
                  changeWidthFlag = false;
                }
              }
            }

            // when all points in category stack are null
            if (changeWidthFlag) {
              arrIndex = categoriesStacksColl[index].indexOf(initStack);
              categoriesStacksColl[index].splice(arrIndex, 1);

              changeWidthFlag = false;
            }
          });
        });
      },
      render: function() {

        var chart = this,
          series = chart.series[0],
          columnMetrics = series.columnMetrics,
          barW = columnMetrics.width,
          barOffsets = {},
          offsets = [],
          columnsToTranslate = [],
          offsetMin = 0,
          offsetMax = 0,
          columnsGroupLen = stacks.length,
          offset,
          columnsGroupWidth,
          padding,
          point,
          pointOffset,
          stackIndex,
          stackLen,
          pointOffsetTemp,
          translateBarX;

        stacks.forEach(function(stack) {
            if (seriesStackColl[stack][0].visible) {
            offset = seriesStackColl[stack][0].columnMetrics.offset;
            offsetMax = offsetMax < offset ? offset : offsetMax;
            offsetMin = offsetMin > offset ? offset : offsetMin;
            barOffsets[stack] = offset;
            offsets.push(offset);
          }
        });

        columnsGroupWidth = Math.abs(offsetMin) + Math.abs(offsetMax) + barW;
        padding = (columnsGroupWidth - columnsGroupLen * barW) / (columnsGroupLen - 1);

        categoriesStacksColl.forEach(function(cat, index) {
          if (cat.length < stacks.length) {
            columnsToTranslate.push({
              index: index,
              stack: cat
            });
          }
        });

        columnsToTranslate.forEach(function(elem) {
            stackIndex = 0;
          pointOffsetTemp = 0;
          chart.series.forEach(function(singleSeries) {

            point = singleSeries.points[elem.index];
            if (point.y && singleSeries.visible) {

              pointOffset = point.series.columnMetrics.offset;
              stackLen = elem.stack.length;

              if (pointOffsetTemp !== pointOffset) {
                pointOffsetTemp = pointOffset;
                stackIndex++;
              }

              translateBarX = calculateColumnTranslate({
                padding: padding,
                barW: barW,
                offset: pointOffset,
                stackIndex: stackIndex,
                stackLen: stackLen
              });

              point.graphic.translate(translateBarX);
            }

          });
        });
      }
    }
  }

Demo: https://jsfiddle.net/wchmiel/9eb74hys/2/

Upvotes: 1

Related Questions