Sizing GraphObjects

The size of a GraphObject is determined by the values of the GraphObject.desiredSize, GraphObject.minSize, GraphObject.maxSize and GraphObject.stretch properties. The actual size of an object after its containing panel measures and arranges it is given by several read-only properties: GraphObject.naturalBounds, GraphObject.measuredBounds, and GraphObject.actualBounds.

The GraphObject.width property is exactly the same as the Size.width component of the GraphObject.desiredSize. Similarly, the GraphObject.height property corresponds to the desiredSize's height. The default value of GraphObject.desiredSize is (NaN, NaN) -- meaning that the size must be computed. One can set the width to a real number and leave the height to be NaN, or vice-versa.

Users can also change the size of an object within a Part via the ResizingTool: Introduction to the ResizingTool.

DesiredSize, MinSize, and MaxSize

When the GraphObject.desiredSize property is set to real numbers it gets that as its natural size. When the desiredSize property is not set but there is a GraphObject.stretch value, it will get the size of the available space. When desiredSize is not set and there is no stretch, an object prefers being its natural size, based on the type of object that it is and the other properties that it has.

But the effective width and effective height, whether given by desiredSize or computed, are each constrained by the GraphObject.maxSize and by the GraphObject.minSize. The minimum size takes precedence over the maximum size in case of conflict.

The size for a GraphObject in a Table Panel may also be constrained by the width of the column and the height of the row that the object is in.


  diagram.add(
    new go.Part()
      .add(
        new go.Panel("Table", { defaultAlignment: go.Spot.Left })
          .addColumnDefinition(0, { width: 200 })
          .addColumnDefinition(1, { width: 15 })
          .add(
            new go.Shape("Rectangle", {
                row: 0, column: 0, fill: "green",
                width: 100, height: 20 }),
            new go.TextBlock({ row: 0, column: 2,
                               text: "desiredSize: 100x20, no minSize, no maxSize" }),
            new go.Shape("Rectangle", {
                row: 1, column: 0, fill: "red",
                width: 100, height: 20,
                minSize: new go.Size(150, 10) }),
            new go.TextBlock({ row: 1, column: 2,
                               text: "desired: 100x20, min: 150x10" }),
            new go.Shape("Rectangle", {
                row: 2, column: 0, fill: "yellow",
                width: 100, height: 20,
                maxSize: new go.Size(50, 300) }),
            new go.TextBlock({ row: 2, column: 2,
                               text: "desired: 100x20, max: 50x300" }),
            new go.Shape("Rectangle", {
                row: 3, column: 0, fill: "red",
                width: 100, height: 20,
                minSize: new go.Size(150, 10), maxSize: new go.Size(50, 300) }),
            new go.TextBlock({ row: 3, column: 2,
                               text: "desired: 100x20, min: 150x10, max: 50x300" })
          )
      ));

Measured and Actual Sizes

Every GraphObject also has a GraphObject.measuredBounds, which describes how big the object seems to be, and a GraphObject.actualBounds, which describes the position and size of an object. These read-only properties take into account any non-zero GraphObject.angle or non-unitary GraphObject.scale. These measurements are in the containing Panel's coordinate system.


  function getSizeString(s) {
    return s.width.toFixed(2) + "x" + s.height.toFixed(2);
  }
  const table =
    new go.Part("Table")
      .add(
        new go.Shape({ name: "A", row: 0, column: 1,
                      figure: "Club", fill: "green", background: "lightgray",
                      width: 40, height: 40,
                      }),  // default angle is zero; default scale is one
        new go.Shape({ name: "B", row: 0, column: 2,
                      figure: "Club", fill: "green", background: "lightgray",
                      width: 40, height: 40,
                      angle: 30 }),
        new go.Shape({ name: "C", row: 0, column: 3,
                      figure: "Club", fill: "green", background: "lightgray",
                      width: 40, height: 40,
                      scale: 1.5 }),
        new go.Shape({ name: "D", row: 0, column: 4,
                      figure: "Club", fill: "green", background: "lightgray",
                      width: 40, height: 40,
                      angle: 30, scale: 1.5 }),
        new go.TextBlock("naturalBounds:", { row: 1, column: 0, alignment: go.Spot.Left }),
        new go.TextBlock("measuredBounds:", { row: 2, column: 0, alignment: go.Spot.Left }),
        new go.TextBlock("actualBounds:", { row: 3, column: 0, alignment: go.Spot.Left }),
        new go.TextBlock({ row: 1, column: 1, margin: 4 })
          .bindObject("text", "naturalBounds", getSizeString, null, "A"),
        new go.TextBlock({ row: 1, column: 2, margin: 4 })
          .bindObject("text", "naturalBounds", getSizeString, null, "B"),
        new go.TextBlock({ row: 1, column: 3, margin: 4 })
          .bindObject("text", "naturalBounds", getSizeString, null, "C"),
        new go.TextBlock({ row: 1, column: 4, margin: 4 })
          .bindObject("text", "naturalBounds", getSizeString, null, "D"),
        new go.TextBlock({ row: 2, column: 1, margin: 4 })
          .bindObject("text", "measuredBounds", getSizeString, null, "A"),
        new go.TextBlock({ row: 2, column: 2, margin: 4 })
          .bindObject("text", "measuredBounds", getSizeString, null, "B"),
        new go.TextBlock({ row: 2, column: 3, margin: 4 })
          .bindObject("text", "measuredBounds", getSizeString, null, "C"),
        new go.TextBlock({ row: 2, column: 4, margin: 4 })
          .bindObject("text", "measuredBounds", getSizeString, null, "D"),
        new go.TextBlock({ row: 3, column: 1, margin: 4 })
          .bindObject("text", "actualBounds", getSizeString, null, "A"),
        new go.TextBlock({ row: 3, column: 2, margin: 4 })
          .bindObject("text", "actualBounds", getSizeString, null, "B"),
        new go.TextBlock({ row: 3, column: 3, margin: 4 })
          .bindObject("text", "actualBounds", getSizeString, null, "C"),
        new go.TextBlock({ row: 3, column: 4, margin: 4 })
          .bindObject("text", "actualBounds", getSizeString, null, "D")
      );
  diagram.add(table);
  setTimeout(() => {
    table.data = {};  // cause bindings to be evaluated after Shapes are measured
  }, 1);

Note that the size of the regular 40x40 shape is 41x41. The additional size is due to the thickness of the pen (Shape.strokeWidth) used to outline the shape. Rotating or increasing the scale causes the 40x40 shape to actually take up significantly more space.

To summarize: the GraphObject.desiredSize (a.k.a. GraphObject.width and GraphObject.height) and the GraphObject.naturalBounds are in the object's local coordinate system. The GraphObject.minSize, GraphObject.maxSize, GraphObject.margin, GraphObject.measuredBounds, and GraphObject.actualBounds are all in the containing Panel's coordinate system, or in document coordinates if there is no such panel because it is a Part.

Stretching of GraphObjects

When you specify a GraphObject.stretch value other than Stretch.None, the object will stretch or contract to fill the available space. However, the GraphObject.maxSize and GraphObject.minSize properties still limit the size.

But setting the GraphObject.desiredSize (or equivalently, the GraphObject.width and/or GraphObject.height) will cause any stretch value to be ignored.

In the following examples the left column is constrained to have a width of 200.


  diagram.add(
    new go.Part()
      .add(
        new go.Panel("Table", { defaultAlignment: go.Spot.Left })
          .addColumnDefinition(0, { width: 200 })
          .addColumnDefinition(1, { width: 15 })
          .add(
            new go.Shape("Rectangle", {
                row: 0, column: 0, fill: "green",
                stretch: go.Stretch.Fill }),
            new go.TextBlock({ row: 0, column: 2,
                               text: "stretch: Fill, no minSize, no maxSize" }),
            new go.Shape("Rectangle", {
                row: 1, column: 0, fill: "red",
                stretch: go.Stretch.Fill,
                minSize: new go.Size(150, 10) }),
            new go.TextBlock({ row: 1, column: 2,
                               text: "stretch: Fill, min: 150x10" }),
            new go.Shape("Rectangle", {
                row: 2, column: 0, fill: "yellow",
                stretch: go.Stretch.Fill,
                maxSize: new go.Size(50, 300) }),
            new go.TextBlock({ row: 2, column: 2,
                               text: "stretch: Fill, max: 50x300" }),
            new go.Shape("Rectangle", {
                row: 3, column: 0, fill: "red",
                stretch: go.Stretch.Fill,
                minSize: new go.Size(150, 10), maxSize: new go.Size(50, 300) }),
            new go.TextBlock({ row: 3, column: 2,
                               text: "stretch: Fill, min: 150x10, max: 50x300" }),
            new go.Shape("Rectangle", {
                row: 4, column: 0, fill: "red",
                width: 100, stretch: go.Stretch.Fill }),
            new go.TextBlock({ row: 4, column: 2,
                               text: "desired width & stretch: ignore stretch" })
          )
      ));

To summarize, if GraphObject.desiredSize is set to a real number, any GraphObject.stretch is ignored. If GraphObject.maxSize conflicts with that value, it takes precedence. And if GraphObject.minSize conflicts with those values, it takes precedence. The width values are constrained independently of the height values.

Stretch and Alignment

The size of a GraphObject in a Panel is determined by many factors. The GraphObject.stretch property specifies whether the width and/or height should take up all of the space given to it by the Panel. When the width and/or height is not stretched to fill the given space, the GraphObject.alignment property controls where the object is placed if it is smaller than available space. One may also stretch the width while aligning vertically, just as one may also stretch vertically while aligning along the X axis.

The alignment value for a GraphObject, if not given by the value of GraphObject.alignment, may be inherited. If the object is in a Table Panel, the value may inherit from the RowColumnDefinitions of the row and of the column that the object is in. Finally the value may be inherited from the Panel.defaultAlignment property.

If you specify a fill stretch (horizontal or vertical or both) and an alignment, the alignment will be ignored. Basically if an object is exactly the size that is available to it, there is only one position for it, so all alignments are the same.

Alignment of Shapes


  diagram.add(
    new go.Part()
      .add(
        new go.Panel("Table", { defaultAlignment: go.Spot.Left })
          .addColumnDefinition(0, { width: 200 })
          .addColumnDefinition(1, { width: 15 })
          .add(
            new go.Shape("Rectangle", {
                row: 0, column: 0, fill: "lightblue",
                width: 100, height: 20, alignment: go.Spot.Left }),
            new go.TextBlock({ row: 0, column: 2, text: "alignment: Left" }),
            new go.Shape("Rectangle", {
                row: 1, column: 0, fill: "lightblue",
                width: 100, height: 20, alignment: go.Spot.Center }),
            new go.TextBlock({ row: 1, column: 2, text: "alignment: Center" }),
            new go.Shape("Rectangle", {
                row: 2, column: 0, fill: "lightblue",
                width: 100, height: 20, alignment: go.Spot.Right }),
            new go.TextBlock({ row: 2, column: 2, text: "alignment: Right" }),
            new go.Shape("Rectangle", {
                row: 3, column: 0, fill: "yellow",
                height: 20, stretch: go.Stretch.Horizontal }),
            new go.TextBlock({ row: 3, column: 2, text: "stretch: Horizontal" }),
            new go.Shape("Rectangle", {
                row: 4, column: 0, fill: "yellow",
                height: 20, stretch: go.Stretch.Horizontal, alignment: go.Spot.Right }),
            new go.TextBlock({ row: 4, column: 2,
                               text: "stretch: Horizontal, ignore alignment" })
          )
      ));

When the element is larger than the available space, the GraphObject.alignment property still controls where the element is positioned. However the element will be clipped to fit.

To make things clearer in the following examples we have made the shape stroke thicker and added a margin to separate the shapes.


  diagram.add(
    new go.Part()
      .add(
        new go.Panel("Table", { defaultAlignment: go.Spot.Left })
          .addColumnDefinition(0, { width: 200 })
          .addColumnDefinition(1, { width: 15 })
          .add(
            new go.Shape("Rectangle", {
                row: 0, column: 0, fill: "lightblue", strokeWidth: 2,
                width: 300, height: 20, margin: 2, alignment: go.Spot.Left }),
            new go.TextBlock({ row: 0, column: 2, text: "big obj alignment: Left" }),
            new go.Shape("Rectangle", {
                row: 1, column: 0, fill: "lightblue", strokeWidth: 2,
                width: 300, height: 20, margin: 2, alignment: go.Spot.Center }),
            new go.TextBlock({ row: 1, column: 2, text: "big obj alignment: Center" }),
            new go.Shape("Rectangle", {
                row: 2, column: 0, fill: "lightblue", strokeWidth: 2,
                width: 300, height: 20, margin: 2, alignment: go.Spot.Right }),
            new go.TextBlock({ row: 2, column: 2, text: "big obj alignment: Right" })
          )
      ));