Shapes

Use the Shape class to paint a geometrical figure. You can control what kind of shape is drawn and how its outline is stroked and how its interior is filled.

Shapes, like TextBlocks and Pictures, are "atomic" objects -- they cannot contain any other objects. So a Shape will never draw some text or an image.

In these simplistic demonstrations, the code programmatically creates a Part and adds it to the Diagram. Once you learn about models and data binding you will generally not create parts (nodes or links) programmatically.

Figures

You can set the Shape.figure property to commonly named kinds of shapes. When using the constructor or GraphObject.make, you can pass the figure name as a string argument. You may also need to set the GraphObject.desiredSize or GraphObject.width and GraphObject.height properties, although it is also common to have the size determined by the Panel that the shape is in.

Here are several of the most often used Shape figures:


  diagram.add(
    new go.Part("Horizontal")
      .add(
        new go.Shape("Rectangle",        { width: 40, height: 60, margin: 4, fill: null }),
        new go.Shape("RoundedRectangle", { width: 40, height: 60, margin: 4, fill: null }),
        new go.Shape("Capsule",          { width: 40, height: 60, margin: 4, fill: null }),
        new go.Shape("Ellipse",          { width: 40, height: 60, margin: 4, fill: null }),
        new go.Shape("Diamond",          { width: 40, height: 60, margin: 4, fill: null }),
        new go.Shape("TriangleRight",    { width: 40, height: 60, margin: 4, fill: null }),
        new go.Shape("TriangleDown",     { width: 40, height: 60, margin: 4, fill: null }),
        new go.Shape("TriangleLeft",     { width: 40, height: 60, margin: 4, fill: null }),
        new go.Shape("TriangleUp",       { width: 40, height: 60, margin: 4, fill: null }),
        new go.Shape("MinusLine",        { width: 40, height: 60, margin: 4, fill: null }),
        new go.Shape("PlusLine",         { width: 40, height: 60, margin: 4, fill: null }),
        new go.Shape("XLine",            { width: 40, height: 60, margin: 4, fill: null })
      ));

You can see all of the named geometrical figures in the shapes sample. Some of the most commonly used figures are predefined in the GoJS library. But most figures are defined in the Figures.js file in the extensions directory.

Fill and Strokes

The Shape.stroke property specifies the brush used to draw the shape's outline. The Shape.fill property specifies the brush used to fill the shape's outline. Additional "stroke..." properties also control how the shape's outline is drawn. The most common such property is Shape.strokeWidth.


  diagram.add(
    new go.Part("Horizontal")
      .add(
        new go.Shape({ figure: "Club", width: 40, height: 40, margin: 4
                      }),  // default fill and stroke are "black"
        new go.Shape({ figure: "Club", width: 40, height: 40, margin: 4,
                      fill: "green" }),
        new go.Shape({ figure: "Club", width: 40, height: 40, margin: 4,
                      fill: "green", stroke: null }),
        new go.Shape({ figure: "Club", width: 40, height: 40, margin: 4,
                      fill: null, stroke: "green" }),
        new go.Shape({ figure: "Club", width: 40, height: 40, margin: 4,
                      fill: null, stroke: "green", strokeWidth: 3 }),
        new go.Shape({ figure: "Club", width: 40, height: 40, margin: 4,
                      fill: null, stroke: "green", strokeWidth: 6 }),
        new go.Shape({ figure: "Club", width: 40, height: 40, margin: 4,
                      fill: "green", background: "orange" })
      ));

The Shape.stroke and Shape.fill properties take Brushes but most often are given a CSS color string to denote solid color brushes. These two properties default to a solid black brush. However it is common to assign one of them to be either null or "transparent". A null brush means that nothing is drawn for that stroke or fill. A transparent brush produces the same appearance but different hit-testing behavior. A shape with a null Shape.fill produces a "hollow" shape -- clicking inside the shape will not "hit" that shape and thus not select the Node that that shape is in. But a shape with a transparent fill produces a "filled" shape -- a mouse event inside the shape will "hit" that shape.


  diagram.div.style.background = "lightgray";
  diagram.add(
    new go.Part("Table")
      .add(
        new go.Shape({ row: 0, column: 0, figure: "Club", width: 60, height: 60, margin: 4,
                      fill: "green" }),
        new go.TextBlock("green", { row: 1, column: 0 }),
        new go.Shape({ row: 0, column: 1, figure: "Club", width: 60, height: 60, margin: 4,
                      fill: "white" }),
        new go.TextBlock("white", { row: 1, column: 1 }),
        new go.Shape({ row: 0, column: 2, figure: "Club", width: 60, height: 60, margin: 4,
                      fill: "transparent" }),
        new go.TextBlock("transparent", { row: 1, column: 2 }),
        new go.Shape({ row: 0, column: 3, figure: "Club", width: 60, height: 60, margin: 4,
                      fill: null }),
        new go.TextBlock("null", { row: 1, column: 3 })
      ));

Try clicking inside each of the shapes to see which ones will respond to the click and cause the whole panel to be selected. Note that with the "transparent" fill you can see the diagram background, yet when you click in it you "hit" the Shape. Only the last one, with a null fill, is truly "hollow". Clicking in the last shape will only result in a click on the diagram background, unless you click on the stroke outline.

Geometry

Every Shape gets its "shape" from the Geometry that it uses. A Geometry is just a saved description of how to draw some lines given a set of points. Setting Shape.figure uses a named predefined geometry that can be parameterized. In general it is most efficient to give a Shape a Geometry rather than giving it a figure.

If you want something different from all of the predefined figures in GoJS, you can construct your own Geometry and set Shape.geometry. One way of building your own Geometry is by building PathFigures consisting of PathSegments. This is often necessary when building a geometry whose points are computed based on some data.

But an easier way to create constant geometries is by calling Geometry.parse to read a string that has a geometry-defining path expression, or to set Shape.geometryString to such a string. These expressions have commands for moving an imaginary "pen". The syntax for geometry paths is documented in the Geometry Path Strings page.

This example creates a Geometry that looks like the letter "W" and uses it in several Shape objects with different stroke characteristics. Geometry objects may be shared by multiple Shapes. Note that there may be no need to specify the GraphObject.desiredSize or GraphObject.width and GraphObject.height, because the Geometry defines its own size. If the size is set or if it is imposed by the containing Panel, the effective geometry is determined by the Shape.geometryStretch property. Depending on the value of the geometryStretch property, this may result in extra empty space or the clipping of the shape.


  const W_geometry = go.Geometry.parse("M 0,0 L 10,50 20,10 30,50 40,0", false);
  diagram.add(
    new go.Part("Horizontal")
      .add(
        new go.Shape({ geometry: W_geometry, strokeWidth: 2 }),
        new go.Shape({ geometry: W_geometry, stroke: "blue", strokeWidth: 10,
                      strokeJoin: "miter", strokeCap: "butt" }),
        new go.Shape({ geometry: W_geometry, stroke: "blue", strokeWidth: 10,
                      strokeJoin: "miter", strokeCap: "round" }),
        new go.Shape({ geometry: W_geometry, stroke: "blue", strokeWidth: 10,
                      strokeJoin: "miter", strokeCap: "square" }),
        new go.Shape({ geometry: W_geometry, stroke: "green", strokeWidth: 10,
                      strokeJoin: "bevel", strokeCap: "butt" }),
        new go.Shape({ geometry: W_geometry, stroke: "green", strokeWidth: 10,
                      strokeJoin: "bevel", strokeCap: "round" }),
        new go.Shape({ geometry: W_geometry, stroke: "green", strokeWidth: 10,
                      strokeJoin: "bevel", strokeCap: "square" }),
        new go.Shape({ geometry: W_geometry, stroke: "red", strokeWidth: 10,
                      strokeJoin: "round", strokeCap: "butt" }),
        new go.Shape({ geometry: W_geometry, stroke: "red", strokeWidth: 10,
                      strokeJoin: "round", strokeCap: "round" }),
        new go.Shape({ geometry: W_geometry, stroke: "red", strokeWidth: 10,
                      strokeJoin: "round", strokeCap: "square" }),
        new go.Shape({ geometry: W_geometry, stroke: "purple", strokeWidth: 2,
                      strokeDashArray: [4, 2] }),
        new go.Shape({ geometry: W_geometry, stroke: "purple", strokeWidth: 2,
                      strokeDashArray: [6, 6, 2, 2] })
      ));

Angle and Scale

Besides setting the GraphObject.desiredSize or GraphObject.width and GraphObject.height to declare the size of a Shape, you can also set other properties to affect the appearance. For example, you can set the GraphObject.angle and GraphObject.scale properties.


  diagram.add(
    new go.Part("Table")
      .add(
        new go.Shape({ row: 0, column: 1,
                      figure: "Club", fill: "green", width: 40, height: 40,
                      }),  // default angle is zero; default scale is one
        new go.Shape({ row: 0, column: 2,
                      figure: "Club", fill: "green", width: 40, height: 40,
                      angle: 30 }),
        new go.Shape({ row: 0, column: 3,
                      figure: "Club", fill: "green", width: 40, height: 40,
                      scale: 1.5 }),
        new go.Shape({ row: 0, column: 4,
                      figure: "Club", fill: "green", width: 40, height: 40,
                      angle: 30, scale: 1.5 })
      ));

The Shape.fill and GraphObject.background brushes scale and rotate along with the shape.

The following shapes show how three separate linear gradient brushes are drawn for the Shape.fill, Shape.stroke, and GraphObject.background of a Shape whose angle isn't zero.


  const bluered = new go.Brush("Linear", { 0.0: "blue", 1.0: "red" });
  const yellowgreen = new go.Brush("Linear", { 0.0: "yellow", 1.0: "green" });
  const grays = new go.Brush("Linear", { 1.0: "black", 0.0: "lightgray" });

  diagram.add(
    new go.Part("Table")
      .add(
        new go.Shape({ row: 0, column: 0,
                      figure: "Club", width: 40, height: 40, angle: 0, scale: 2,
                      fill: bluered,
                      stroke: grays, strokeWidth: 2,
                      background: yellowgreen
                    }),
        new go.Shape({ row: 0, column: 1, width: 10, fill: null, stroke: null }),
        new go.Shape({ row: 0, column: 2,
                      figure: "Club", width: 40, height: 40, angle: 45, scale: 2,
                      fill: bluered,
                      stroke: grays, strokeWidth: 2,
                      background: yellowgreen
                    })
      ));

Custom Figures

As shown above, one can easily create custom shapes just by setting Shape.geometry or Shape.geometryString. This is particularly convenient when importing SVG. However it is also possible to define additional named figures, which is convenient when you want to be able to easily specify or change the geometry of an existing Shape by setting or data binding the Shape.figure property.

The static function Shape.defineFigureGenerator can be used to define new figure names. The second argument is a function that is called with the Shape and the expected width and height in order to generate and return a Geometry. This permits parameterization of the geometry based on properties of the Shape and the expected size. In particular, the Shape.parameter1 and Shape.parameter2 properties can be considered, in addition to the width and height, while producing the Geometry. To be valid, the generated Geometry bounds must be equal to or less than the supplied width and height.


  go.Shape.defineFigureGenerator('FramedRectangle', (shape, w, h) => {
    let param1 = shape ? shape.parameter1 : NaN;
    let param2 = shape ? shape.parameter2 : NaN;
    if (isNaN(param1))
        param1 = 8; // default values PARAMETER 1 is for WIDTH
    if (isNaN(param2))
        param2 = 8; // default values PARAMETER 2 is for HEIGHT
    const geo = new go.Geometry();
    const fig = new go.PathFigure(0, 0, true);
    geo.add(fig);
    // outer rectangle, clockwise
    fig.add(new go.PathSegment(go.SegmentType.Line, w, 0));
    fig.add(new go.PathSegment(go.SegmentType.Line, w, h));
    fig.add(new go.PathSegment(go.SegmentType.Line, 0, h).close());
    if (param1 < w / 2 && param2 < h / 2) {
        // inner rectangle, counter-clockwise
        fig.add(new go.PathSegment(go.SegmentType.Move, param1, param2)); // subpath
        fig.add(new go.PathSegment(go.SegmentType.Line, param1, h - param2));
        fig.add(new go.PathSegment(go.SegmentType.Line, w - param1, h - param2));
        fig.add(new go.PathSegment(go.SegmentType.Line, w - param1, param2).close());
    }
    geo.setSpots(0, 0, 1, 1, param1, param2, -param1, -param2);
    return geo;
  });

  diagram.nodeTemplate =
    new go.Part("Spot",
        {
          selectionAdorned: false,  // don't show the standard selection handle
          resizable: true, resizeObjectName: "SHAPE",  // user can resize the Shape
          rotatable: true, rotateObjectName: "SHAPE",  // user can rotate the Shape
                                                        // without rotating the label
        })
      .add(
        new go.Shape('FramedRectangle',
            {
              name: "SHAPE",
              fill: new go.Brush("Linear", { 0.0: "white", 1.0: "gray" }),
              desiredSize: new go.Size(100, 50)
            })
          .bind("parameter1", "p1")
          .bind("parameter2", "p2"),
        new go.TextBlock({ stroke: "blue" })
          .bindObject("text", "", s => `${s.parameter1}, ${s.parameter2}`, null, "SHAPE")
      );

  diagram.model = new go.Model([
    { },  // unspecified parameter values treated as 8 by "FramedRectangle" figure
    { p1: 0 },
    { p1: 5 },
    { p1: 15 },
    { p1: 5, p2: 5 },
    { p1: 15, p2: 15 },
  ]);

Note how the Shape.parameter1 property, data bound to the "p1" property, controls how thick the sides are. The Shape.parameter2 property, data bound to the "p2" property, controls how thick the top and bottom are. You can see the effects by resizing a shape. Notice how it doesn't bother drawing the empty area in the middle when there isn't enough width or height.

You can find the definitions for many figures at: Figures.js or Figures.ts