# Shapes

Use the Shape class to paint a geometrical figure. You can control what kind of shape is drawn and how it is stroked and 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 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.

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.

Here are several of the most often used Shape figures:

      diagram.add(
        $(go.Part, "Horizontal",
          $(go.Shape, "Rectangle",
                      { width: 40, height: 60, margin: 4, fill: null }),
          $(go.Shape, "RoundedRectangle",
                      { width: 40, height: 60, margin: 4, fill: null }),
          $(go.Shape, "Ellipse",
                      { width: 40, height: 60, margin: 4, fill: null }),
          $(go.Shape, "Triangle",
                      { width: 40, height: 60, margin: 4, fill: null }),
          $(go.Shape, "Diamond",
                      { 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(
        $(go.Part, "Horizontal",
          $(go.Shape, { figure: "Club", width: 40, height: 40, margin: 4
                        }),  // default fill and stroke are "black"
          $(go.Shape, { figure: "Club", width: 40, height: 40, margin: 4,
                        fill: "green" }),
          $(go.Shape, { figure: "Club", width: 40, height: 40, margin: 4,
                        fill: "green", stroke: null }),
          $(go.Shape, { figure: "Club", width: 40, height: 40, margin: 4,
                        fill: null, stroke: "green" }),
          $(go.Shape, { figure: "Club", width: 40, height: 40, margin: 4,
                        fill: null, stroke: "green", strokeWidth: 3 }),
          $(go.Shape, { figure: "Club", width: 40, height: 40, margin: 4,
                        fill: null, stroke: "green", strokeWidth: 6 }),
          $(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(
        $(go.Part, "Table",
          $(go.Shape, { row: 0, column: 0, figure: "Club", width: 60, height: 60, margin: 4,
                        fill: "green" }),
          $(go.TextBlock, "green", { row: 1, column: 0 }),
          $(go.Shape, { row: 0, column: 1, figure: "Club", width: 60, height: 60, margin: 4,
                        fill: "white" }),
          $(go.TextBlock, "white", { row: 1, column: 1 }),
          $(go.Shape, { row: 0, column: 2, figure: "Club", width: 60, height: 60, margin: 4,
                        fill: "transparent" }),
          $(go.TextBlock, "transparent", { row: 1, column: 2 }),
          $(go.Shape, { row: 0, column: 3, figure: "Club", width: 60, height: 60, margin: 4,
                        fill: null }),
          $(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.

      var W_geometry = go.Geometry.parse("M 0,0 L 10,50 20,10 30,50 40,0", false);
      diagram.add(
        $(go.Part, "Horizontal",
          $(go.Shape, { geometry: W_geometry, strokeWidth: 2 }),
          $(go.Shape, { geometry: W_geometry, stroke: "blue", strokeWidth: 10,
                        strokeJoin: "miter", strokeCap: "butt" }),
          $(go.Shape, { geometry: W_geometry, stroke: "blue", strokeWidth: 10,
                        strokeJoin: "miter", strokeCap: "round" }),
          $(go.Shape, { geometry: W_geometry, stroke: "blue", strokeWidth: 10,
                        strokeJoin: "miter", strokeCap: "square" }),
          $(go.Shape, { geometry: W_geometry, stroke: "green", strokeWidth: 10,
                        strokeJoin: "bevel", strokeCap: "butt" }),
          $(go.Shape, { geometry: W_geometry, stroke: "green", strokeWidth: 10,
                        strokeJoin: "bevel", strokeCap: "round" }),
          $(go.Shape, { geometry: W_geometry, stroke: "green", strokeWidth: 10,
                        strokeJoin: "bevel", strokeCap: "square" }),
          $(go.Shape, { geometry: W_geometry, stroke: "red", strokeWidth: 10,
                        strokeJoin: "round", strokeCap: "butt" }),
          $(go.Shape, { geometry: W_geometry, stroke: "red", strokeWidth: 10,
                        strokeJoin: "round", strokeCap: "round" }),
          $(go.Shape, { geometry: W_geometry, stroke: "red", strokeWidth: 10,
                        strokeJoin: "round", strokeCap: "square" }),
          $(go.Shape, { geometry: W_geometry, stroke: "purple", strokeWidth: 2,
                        strokeDashArray: [4, 2] }),
          $(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(
        $(go.Part, "Table",
          $(go.Shape, { row: 0, column: 1,
                        figure: "Club", fill: "green", width: 40, height: 40,
                        }),  // default angle is zero; default scale is one
          $(go.Shape, { row: 0, column: 2,
                        figure: "Club", fill: "green", width: 40, height: 40,
                        angle: 30 }),
          $(go.Shape, { row: 0, column: 3,
                        figure: "Club", fill: "green", width: 40, height: 40,
                        scale: 1.5 }),
          $(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 GraphObject.areaBackground is drawn in the containing panel's coordinates, so it is not affected by the object's scale or angle.

The following two shapes each use three separate linear gradient brushes, one for each of the three properties. Note the unrotated shape on the left. Because its GraphObject.background brush is opaque, you cannot see the GraphObject.areaBackground brush that fills the same area behind it.

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

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

## 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.

      go.Shape.defineFigureGenerator("RoundedTopRectangle", function(shape, w, h) {
        // this figure takes one parameter, the size of the corner
        var p1 = 5;  // default corner size
        if (shape !== null) {
          var param1 = shape.parameter1;
          if (!isNaN(param1) && param1 >= 0) p1 = param1;  // can't be negative or NaN
        }
        p1 = Math.min(p1, w / 2);
        p1 = Math.min(p1, h / 2);  // limit by whole height or by half height?
        var geo = new go.Geometry();
        // a single figure consisting of straight lines and quarter-circle arcs
        geo.add(new go.PathFigure(0, p1)
                 .add(new go.PathSegment(go.PathSegment.Arc, 180, 90, p1, p1, p1, p1))
                 .add(new go.PathSegment(go.PathSegment.Line, w - p1, 0))
                 .add(new go.PathSegment(go.PathSegment.Arc, 270, 90, w - p1, p1, p1, p1))
                 .add(new go.PathSegment(go.PathSegment.Line, w, h))
                 .add(new go.PathSegment(go.PathSegment.Line, 0, h).close()));
        // don't intersect with two top corners when used in an "Auto" Panel
        geo.spot1 = new go.Spot(0, 0, 0.3 * p1, 0.3 * p1);
        geo.spot2 = new go.Spot(1, 1, -0.3 * p1, 0);
        return geo;
      });

      go.Shape.defineFigureGenerator("RoundedBottomRectangle", function(shape, w, h) {
        // this figure takes one parameter, the size of the corner
        var p1 = 5;  // default corner size
        if (shape !== null) {
          var param1 = shape.parameter1;
          if (!isNaN(param1) && param1 >= 0) p1 = param1;  // can't be negative or NaN
        }
        p1 = Math.min(p1, w / 2);
        p1 = Math.min(p1, h / 2);  // limit by whole height or by half height?
        var geo = new go.Geometry();
        // a single figure consisting of straight lines and quarter-circle arcs
        geo.add(new go.PathFigure(0, 0)
                 .add(new go.PathSegment(go.PathSegment.Line, w, 0))
                 .add(new go.PathSegment(go.PathSegment.Line, w, h - p1))
                 .add(new go.PathSegment(go.PathSegment.Arc, 0, 90, w - p1, h - p1, p1, p1))
                 .add(new go.PathSegment(go.PathSegment.Line, p1, h))
                 .add(new go.PathSegment(go.PathSegment.Arc, 90, 90, p1, h - p1, p1, p1).close()));
        // don't intersect with two bottom corners when used in an "Auto" Panel
        geo.spot1 = new go.Spot(0, 0, 0.3 * p1, 0);
        geo.spot2 = new go.Spot(1, 1, -0.3 * p1, -0.3 * p1);
        return geo;
      });

      diagram.nodeTemplate =
          $(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
            },
            $(go.Shape,
              {
                name: "SHAPE",
                fill: $(go.Brush, "Linear", { 0.0: "white", 1.0: "gray" }),
                desiredSize: new go.Size(100, 50)
              },
              new go.Binding("figure", "fig"),
              new go.Binding("parameter1", "p1")),
            $(go.Panel, "Vertical",
              $(go.TextBlock,
                new go.Binding("text", "fig")),
              $(go.TextBlock, { stroke: "blue" },
                new go.Binding("text", "parameter1", function(p1) { return p1; }).ofObject("SHAPE"))
            )
          );

      diagram.model = new go.Model([
        { fig: "RoundedTopRectangle" },
        { fig: "RoundedTopRectangle", p1: 0 },
        { fig: "RoundedTopRectangle", p1: 3 },
        { fig: "RoundedTopRectangle", p1: 10 },
        { fig: "RoundedTopRectangle", p1: 50 },
        { fig: "RoundedTopRectangle", p1: 250 },
        { fig: "RoundedBottomRectangle" },
        { fig: "RoundedBottomRectangle", p1: 0 },
        { fig: "RoundedBottomRectangle", p1: 3 },
        { fig: "RoundedBottomRectangle", p1: 10 },
        { fig: "RoundedBottomRectangle", p1: 50 },
        { fig: "RoundedBottomRectangle", p1: 250 }
      ]);

Note how the Shape.parameter1 property, data bound to the "p1" property, controls how rounded the corners are. The definition of each figure limits the roundedness based on the actual size of the geometry. You can see the effects by resizing the last shape -- the curve on the shape with p1==250 can be huge if the shape becomes huge.

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