Optimise layout to smallest square required

I have an ordered list of short pieces of text, usually just one or a few words. I would like to lay them out in a square. I can lay them out horizontally or vertically and I can set the column count to make them wrap. What I’d like to do is not have to set the column count but the wrapping to be done automatically so the square is filled up rather than made wider or taller.

It’s similar to resizing a rectangle manually so the text elements wrap and then stopping the resizing when I can visually see the rectangle is as square as it can be.

For example, the following
|First phrase-Second phrase-Third-Fourth-Fifth|
would become
|First phrase------|
|Second phrase-----|
|Third-Fourth-Fifth|

I can either fix the shape as the square and somehow optimise the layout to minimise size of the square so everything fits or I can make it a rectangle and somehow change the column count until the width and height are as close as possible for everything to still fit.

Any suggestions to help me use GoJS in this way?

I’ve found a way to do it but I’m keen to hear if this is going to cause problems such as hitting performance badly.

const initialWidth = obj.part.actualBounds.width
const initialHeight = obj.part.actualBounds.height
model.startTransaction("initialChangeLayout")
obj.part.layout.wrappingColumn = 1
model.commitTransaction("initialChangeLayout")
let guard = 1
let wrappingColumn = 1
while(guard++ <= 100 && obj.part.actualBounds.width < obj.part.actualBounds.height) {
model.startTransaction("changeLayout")
obj.part.layout.wrappingColumn = ++wrappingColumn
model.commitTransaction("changeLayout")
}

One possibility is to do repeated layouts with different values for GridLayout.wrappingWidth until the aspect ratio of the results is close to 1.0. Each time increase or decrease the wrappingWidth with ever smaller increments, so as to make sure that the layout converges on a solution.

Try something like this custom GridLayout:

  function SquareLayout() {
    go.GridLayout.call(this);
    this.isViewportSized = false;
  }
  go.Diagram.inherit(SquareLayout, go.GridLayout);

  SquareLayout.prototype.doLayout = function(coll) {
    var ecoll = this.collectParts(coll);
    // estimate minimum space needed
    var tot = 0;
    var spac = this.spacing;
    ecoll.each(function(p) {
      if (p instanceof go.Link) return;
      tot += (p.actualBounds.width + spac.width) * (p.actualBounds.height + spac.height);
    });
    if (tot === 0) return;
    this.wrappingWidth = Math.sqrt(tot);

    var diff = 30.0;
    var increasing = false;
    var count = 0;
    while (diff >= 1.0) {
      count++;
      go.GridLayout.prototype.doLayout.call(this, ecoll);
      var bnds = this.diagram.computePartsBounds(ecoll);
      if (bnds.height < 1) break;
      var ratio = bnds.width / bnds.height;
      if (ratio < 0.99) {
        if (!increasing) { increasing = true; diff *= 0.8; }
        this.wrappingWidth += diff;
      } else if (ratio > 1.01) {
        if (increasing) { increasing = false; diff *= 0.8; }
        if (this.wrappingWidth - diff < 1) break;
        this.wrappingWidth -= diff;
      } else {
        break;
      }
    }

    this.isValidLayout = true;  // in case it's been invalidated by setting wrappingWidth
  };

In your case you’ll want to set GridLayout.cellSize to new go.Size(1, 1).

Note that there is no guarantee that the best possible layout will have an aspect ratio close to 1.0. Imagine that there is only one node and it is really short and wide – no layout choices there!