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

170 Views Asked by At

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!

3

There are 3 best solutions below

0
Halvor Holsten Strand On BEST ANSWER

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.

0
GotStu On

From the docs it looks like you can set a width ONLY on image markers.

width: number Image markers only. Set the image width explicitly. When using this option, a height must also be set.

https://api.highcharts.com/highcharts/plotOptions.series.marker.symbol

Might be only way to get what you want.

1
Halvor Holsten Strand On

There appears to be a relationship between the plot width and the spacing that is used. I see from the code that e.g.:

cellWidth = this.chart.plotWidth / cols, cellHeight = this.chart.plotHeight / rows

So if the plot is very wide then it will utilize that space. Although not ideal, you could lock down either chart.width or #container { max-width: 320px; ... } to a size that gives you very little padding between columns.

See e.g. how I've used 320px for this JSFiddle demonstration.