Search code examples
javascripthighchartsvisualizationwaffle-chart

Using Highcharts to create an items chart (aka "waffle" chart) - how to I control padding?


I am using Highcharts to create a waffle chart. Here's a good example:

https://datavizproject.com/data-type/percentage-grid/

waffle chart

Highcharts calls them "item charts", and they work pretty well.

However, I can't figure out how to control the spacing/padding between the items.

I've tried using the itemPadding property, but that only seems to set the padding between the rows, but not the columns.

Here's a working fiddle:

https://jsfiddle.net/stuehler/frt80u2p/4/

As you can see in this screenshot below, the row padding is 1px (itemPadding: 1 - that's what I want), but the column padding is much larger:

Screenshot from jsfiddle - column padding too wide

Here is the code I'm using:

Highcharts.chart('container', {

  chart: {
    type: 'item'
  },

  series: [{
    marker: {
      symbol: 'square'
    },
    itemPadding: 1,
    rows: 10,
    data: [
      {
        name: 'Male',
        y: 43
      },
      {
        name: 'Female',
        y: 57
      }
    ]
  }]
});

Thanks in advance for your help!


Solution

  • If you are willing to extend/overwrite, here is some food for thought on how to achieve the spacing. Take the examples below as they are. The code is not production ready in terms of maturity or testing.

    Example 1

    (function (H) {
        const ItemSeries = H.Series.types.item;
        H.wrap(ItemSeries.prototype, 'drawPoints', function (proceed) {
            const originalPlotWidth = this.chart.plotWidth;
            this.chart.plotWidth = this.chart.plotHeight;
    
            proceed.apply(this, Array.prototype.slice.call(arguments, 1));
    
            this.chart.plotWidth = originalPlotWidth;
        });
    }(Highcharts));
    
    

    It just sets the plotWidth such that it is equal to plotHeight during the call to drawPoints, giving each cell the same width and height. It ends up showing correctly for some simple demo scenarios, but the chart is shown aligned to the left.

    See this JSFiddle demonstration. It should perhaps be using Math.min for width and height.

    Example 2

    (function (H) {
                    console.log(H);
           H.Series.types.item.prototype.drawPoints = function() {
                    const series = this, options = this.options, renderer = series.chart.renderer, seriesMarkerOptions = options.marker, borderWidth = this.borderWidth, crisp = borderWidth % 2 ? 0.5 : 1, rows = this.getRows(), cols = Math.ceil(this.total / rows);
                    
                    const cellWidth = Math.min(this.chart.plotWidth / cols, this.chart.plotHeight / rows), cellHeight = Math.min(this.chart.plotWidth / cols, this.chart.plotHeight / rows), itemSize = this.itemSize || Math.min(cellWidth, cellHeight);
                    const leftPadding = Math.max(0, (this.chart.plotWidth - (cellWidth * cols)) / 2);
                    
                    let i = 0;
                    /* @todo: remove if not needed
                    this.slots.forEach(slot => {
                        this.chart.renderer.circle(slot.x, slot.y, 6)
                            .attr({
                                fill: 'silver'
                            })
                            .add(this.group);
                    });
                    //*/
                    for (const point of series.points) {
                        const pointMarkerOptions = point.marker || {}, symbol = (pointMarkerOptions.symbol ||
                            seriesMarkerOptions.symbol), r = H.pick(pointMarkerOptions.radius, seriesMarkerOptions.radius), size = H.defined(r) ? 2 * r : itemSize, padding = size * options.itemPadding;
                        let attr, graphics, pointAttr, x, y, width, height;
                        point.graphics = graphics = point.graphics || [];
                        if (!series.chart.styledMode) {
                            pointAttr = series.pointAttribs(point, point.selected && 'select');
                        }
                        if (!point.isNull && point.visible) {
                            if (!point.graphic) {
                                point.graphic = renderer.g('point')
                                    .add(series.group);
                            }
                            for (let val = 0; val < (point.y || 0); ++val) {
                                // Semi-circle
                                if (series.center && series.slots) {
                                    // Fill up the slots from left to right
                                    const slot = series.slots.shift();
                                    x = slot.x - itemSize / 2;
                                    y = slot.y - itemSize / 2;
                                }
                                else if (options.layout === 'horizontal') {
                                    x = cellWidth * (i % cols);
                                    y = cellHeight * Math.floor(i / cols);
                                }
                                else {
                                    x = leftPadding + (cellWidth * Math.floor(i / rows));
                                    y = cellHeight * (i % rows);
                                }
                                x += padding;
                                y += padding;
                                width = Math.round(size - 2 * padding);
                                height = width;
                                if (series.options.crisp) {
                                    x = Math.round(x) - crisp;
                                    y = Math.round(y) + crisp;
                                }
                                attr = {
                                    x: x,
                                    y: y,
                                    width: width,
                                    height: height
                                };
                                if (typeof r !== 'undefined') {
                                    attr.r = r;
                                }
                                // Circles attributes update (#17257)
                                if (pointAttr) {
                                    H.extend(attr, pointAttr);
                                }
                                let graphic = graphics[val];
                                if (graphic) {
                                    graphic.animate(attr);
                                }
                                else {
                                    graphic = renderer
                                        .symbol(symbol, void 0, void 0, void 0, void 0, {
                                        backgroundSize: 'within'
                                    })
                                        .attr(attr)
                                        .add(point.graphic);
                                }
                                graphic.isActive = true;
                                graphics[val] = graphic;
                                ++i;
                            }
                        }
                        for (let j = 0; j < graphics.length; j++) {
                            const graphic = graphics[j];
                            if (!graphic) {
                                return;
                            }
                            if (!graphic.isActive) {
                                graphic.destroy();
                                graphics.splice(j, 1);
                                j--; // Need to substract 1 after splice, #19053
                            }
                            else {
                                graphic.isActive = false;
                            }
                        }
                    }
                };
    }(Highcharts));
    
    

    This is essentially a full overwrite of the drawPoints code, using the original as a source. It defines the cellWidth and cellHeight variables differently from the original, making the cells square. It also defines a leftPadding variable to be able to center the chart in the plot.

    See this JSFiddle demonstration.