ANSI/ISA-5.1-2009 sample

Hello,
I’m looking if it already’s already exist an sample taking the normalized instrumentation representation ?

GoJS seem really a powerfull tool to can produce some great diagram in this industrial way to describe equipment.

ANSI/ISA-5.1-2009, bring a lot of normalized symbol, it’s more an sample, an library to can chose only accepted symbol.

I’m not talking about simulation nor responsibility in dimensioning but as tool to help doing dynamic presentation for a client.

Are you looking for a sample that can display all possible ANSI/ISA-5.1-2009 diagrams? I don’t think we have such a sample already. Really, the issue is finding SVG definitions for all of those symbols, or better yet parameterized geometries. Hmm, Annex B has a better description of the possibilities for most of the symbols.

Perhaps you not only want an editor that can support designing such diagrams, but also enforce the expected standards regarding things like loops. Tell me more about what you want to build.

For the moment i’m more looking for sample to easily design most ANSI/ISA-5.1-2009 diagrams.
The idea is not to bring design calculation but more use it for more efficient presentation.
It’s tracing design first, for easy engineering design and calculation by other tools.
Because GoJS is full of other tools it can be interesting to keep in mind standard in defining equipment number.
But yes, i’m tinking import an SVG library with all definitions is an first start.

This norm is as complexe than an complet informatical language and in industrial design rapidely error in calculation can bring big financial lose so, it’s not an easy job.

And i think it’s important to not bring in GoJS more than easy coloring and active of already write design.

OK, that sounds reasonable. What’s the schema for the designs that you will need to import and display? I assume you have already seen these samples:

In addition, we probably have the start of other kinds of diagrams that might demonstrate some features:

For the moment i think about some recycling facility, an plastic pyrolyse system to produce fuel with floating plastic on ocean for example.
The Hard part it’s involve an complexe filtration, and ocean water bring some really important chimical and mechanical technology for system preservation.
i will explore the sample you showing me.
I’m thinking bring my development in github on an open model.

I think also, be able to draw the first two page of this PDF can be enough to allow an new industrial sample

If you can find or write SVG for each of those symbols corresponding to GoJS Nodes, I think that will be straightforward to implement.

You haven’t said anything about the schema of existing diagrams that you want to load. If there is an ANSI or other standards body standard for such files, reading and writing such data will be another task.

If you look on the file i show in my previous message, you will see it’s an free to use template from aiche.org.

i find it trying to find some SVG library.
My idea is to do some developpement to can pratice this american norm. I’m French and europeen notation are different but similar.
I’m thinking draw them if i can’t find them.
there this project : Making sure you're not a bot! but it’s not enought to draw completely what i want.

Here’s a demonstration for how to define figures (shapes) and node templates and link templates.

<!DOCTYPE html>
<html>
<head>
  <title>Minimal GoJS Sample</title>
  <!-- Copyright 1998-2025 by Northwoods Software Corporation. -->
</head>
<body>
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:400px"></div>
  <input type="checkbox" onclick="allowLinking(event.target.checked)">Allow Draw Link</input>
  <textarea id="mySavedModel" style="width:100%;height:250px"></textarea>

  <script src="https://cdn.jsdelivr.net/npm/gojs/release/go-debug.js"></script>
  <script id="code">
    
// Produce a Geometry that includes an arrowhead at the end of each segment.
// This only works with orthogonal non-Bezier routing.
class MultiArrowLink extends go.Link {
  constructor(init) {
    super();
    this.routing = go.Routing.Orthogonal;
    if (init) Object.assign(this, init);
  }

  // produce a Geometry from the Link's route
  makeGeometry() {
    // get the Geometry created by the standard behavior
    const geo = super.makeGeometry();
    if (geo.type !== go.GeometryType.Path || geo.figures.length === 0) return geo;
    const mainfig = geo.figures.elt(0); // assume there's just one PathFigure
    const mainsegs = mainfig.segments;

    const arrowLen = 8; // length for each arrowhead
    const arrowWid = 3; // actually half-width of each arrowhead
    let fx = mainfig.startX;
    let fy = mainfig.startY;
    for (let i = 0; i < mainsegs.length; i++) {
      const a = mainsegs.elt(i);
      // assume each arrowhead is a simple triangle
      const ax = a.endX;
      const ay = a.endY;
      let bx = ax;
      let by = ay;
      let cx = ax;
      let cy = ay;
      if (fx < ax - arrowLen) {
        bx -= arrowLen;
        by += arrowWid;
        cx -= arrowLen;
        cy -= arrowWid;
      } else if (fx > ax + arrowLen) {
        bx += arrowLen;
        by += arrowWid;
        cx += arrowLen;
        cy -= arrowWid;
      } else if (fy < ay - arrowLen) {
        bx -= arrowWid;
        by -= arrowLen;
        cx += arrowWid;
        cy -= arrowLen;
      } else if (fy > ay + arrowLen) {
        bx -= arrowWid;
        by += arrowLen;
        cx += arrowWid;
        cy += arrowLen;
      }
      geo.add(
        new go.PathFigure(ax, ay, true)
          .add(new go.PathSegment(go.SegmentType.Line, bx, by))
          .add(new go.PathSegment(go.SegmentType.Line, cx, cy).close())
      );
      fx = ax;
      fy = ay;
    }

    return geo;
  }
}
// end of MultiArrowLink class

const myDiagram =
  new go.Diagram("myDiagramDiv", {
      "draggingTool.isGridSnapEnabled": true,
      "draggingTool.gridSnapCellSize": new go.Size(0.5, 0.5),
      "rotatingTool.snapAngleMultiple": 90,
      "rotatingTool.snapAngleEpsilon": 45,
      allowLink: false,
      allowRelink: false,
      "undoManager.isEnabled": true,
      "ModelChanged": e => {     // just for demonstration purposes,
        if (e.isTransactionFinished) {  // show the model data in the page's TextArea
          document.getElementById("mySavedModel").textContent = e.model.toJson();
        }
      }
    });

function allowLinking(allow) {
  myDiagram.commit(diag => {
    diag.allowLink = allow;
    diag.allowRelink = allow;
    diag.nodes.each(n => n.port.cursor = allow ? "pointer" : "");
  });
}

function commonNodeStyle(node) {
  node.type = go.Panel.Spot;
  node.locationSpot = go.Spot.Center;
  node.locationObjectName = "ICON";
  node.bindTwoWay("location", "loc", go.Point.parse, go.Point.stringify);
}

function commonPortStyle(elt) {
  elt.portId = "";
  elt.fromLinkable = true;
  elt.toLinkable = true;
  //elt.cursor = "pointer";
}

go.Shape.defineFigureGenerator("SharedDisplay", (shp, w, h) => {
  return new go.Geometry()
    .add(new go.PathFigure(0, 0)
        .add(new go.PathSegment(go.SegmentType.Line, w, 0))
        .add(new go.PathSegment(go.SegmentType.Line, w, h))
        .add(new go.PathSegment(go.SegmentType.Line, 0, h).close()))
    .add(new go.PathFigure(w, h/2)
        .add(new go.PathSegment(go.SegmentType.Arc, 0, 360, w/2, h/2, w/2, h/2)))
    .add(new go.PathFigure(0, h/2)
        .add(new go.PathSegment(go.SegmentType.Line, w, h/2)));
});

myDiagram.nodeTemplateMap.add("Instrument",
  new go.Node()
    .apply(commonNodeStyle)
    .set({ rotatable: false })  // override commonNodeStyle
    .add(
      new go.Shape("Circle", {
          name: "ICON",
          width: 40, height: 40, fill: "white",
        })
        .apply(commonPortStyle)
        .bind("figure", "symbol"),
      new go.TextBlock({ alignment: new go.Spot(0.5, 0.33) })
        .bind("text", "function"),
      new go.TextBlock({ alignment: new go.Spot(0.5, 0.7) })
        .bind("text", "tag"),
      new go.TextBlock({ alignment: go.Spot.TopRight, visible: false })
        .bind("text", "quantity")
        .bind("visible", "quantity", q => !!q)
    ));

go.Shape.defineFigureGenerator("Butterfly", (shp, w, h) => {
  return new go.Geometry()
    .add(new go.PathFigure(0, 0)
        .add(new go.PathSegment(go.SegmentType.Line, 0, h))
        .add(new go.PathSegment(go.SegmentType.Move, w, 0))
        .add(new go.PathSegment(go.SegmentType.Line, w, h)))
    .add(new go.PathFigure(w*5/6, h/6)
        .add(new go.PathSegment(go.SegmentType.Line, w/6, h*5/6)))
    .add(new go.PathFigure(w/2, h/2)
        .add(new go.PathSegment(go.SegmentType.Arc, 0, 360, w/2, h/2, 2, 2)))
});

myDiagram.nodeTemplateMap.add("Valve",
  new go.Node({ rotatable: true })
    .apply(commonNodeStyle)
    .bindTwoWay("angle")
    .add(
      new go.Shape({
          name: "ICON",
          width: 30, height: 30, background: "white",
          fromSpot: go.Spot.LeftRightSides, toSpot: go.Spot.LeftRightSides
        })
        .apply(commonPortStyle)
        .bind("figure", "symbol"),
      new go.TextBlock({ alignment: go.Spot.Top, alignmentFocus: go.Spot.Bottom })
        .bind("text", "size")
        .bind("angle", "angle", a => a === 180 ? 180 : 0),
      new go.Panel("Vertical", { alignment: go.Spot.Bottom, alignmentFocus: go.Spot.Top })
        .add(
          new go.TextBlock()
            .bind("text", "spec")
            .bind("angle", "angle", a => a === 180 ? 180 : 0),
          new go.TextBlock()
            .bind("text", "tag")
            .bind("angle", "angle", a => a === 180 ? 180 : 0)
        )
    ));

myDiagram.nodeTemplateMap.add("I/O",
  new go.Node({ rotatable: true })
    .apply(commonNodeStyle)
    .bindTwoWay("angle")
    .add(
      new go.Shape("TriangleRight", {
          name: "ICON", width: 30, height: 30, fill: "white",
          fromSpot: go.Spot.Right, toSpot: go.Spot.Left
        })
        .apply(commonPortStyle),
      new go.TextBlock({ alignment: new go.Spot(0.3, 0.53) })
        .bind("text", "type")
        .bind("angle", "angle", a => a === 180 ? 180 : 0)
    )
)

myDiagram.linkTemplate =
  new MultiArrowLink({
      routing: go.Link.Orthogonal,
      relinkableFrom: true, relinkableTo: true,
      reshapable: true, resegmentable: true
    })
    .add(
      new go.Shape()
    );

myDiagram.model = new go.GraphLinksModel(
[
  { key: 1, category: "Instrument", function: "LE", tag: "123A", quantity: 3, loc: "20.5 27.5" },
  { key: 2, category: "Instrument", symbol: "SharedDisplay", function: "ZI", tag: "101-", loc: "143 27.5" },
  { key: 3, category: "Valve", symbol: "Butterfly", size: "100", spec: "N4", tag: "HV0001", loc: "77 108" },
  { key: 4, category: "I/O", type: "DI", loc: "189 149" },
],
[
  { from: 1, to: 2 },
  { from: 1, to: 3 },
  { from: 3, to: 4 },
]);
  </script>
</body>
</html>

Woh So clear and efficient !
Thanks with this sample.
I think i have the tips i needed to can express myself clearly.
I will be able to think about process, precision, efficiency and other engineering stuff using GoJS and american Industrial Drawing normative.

The main thing visual thing in https://www.aiche.org/sites/default/files/chenected/2010/08/ChEnected-Example-PIDs-and-Lead-Sheets.pdf that is not straight-forward to implement in GoJS are some of the Line Symbols. I’ll work on a demonstration of that – it’s just a variation on the MultiArrowLink class that I included in the code above.

More features:

Complete code:

<!DOCTYPE html>
<html>
<head>
  <title>Minimal Piping & Instrumentation Diagram</title>
  <!-- Copyright 1998-2025 by Northwoods Software Corporation. -->
</head>
<body>
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:400px"></div>
  Use the context menu on a line/Link to change its line symbol.<br>
  <input type="checkbox" onclick="allowLinking(event.target.checked)">Allow Draw Link</input><br>
  <textarea id="mySavedModel" style="width:100%;height:250px"></textarea>
  Design taken from <a href="https://www.aiche.org/sites/default/files/chenected/2010/08/ChEnected-Example-PIDs-and-Lead-Sheets.pdf">AIChE example diagram</a>

  <script src="https://cdn.jsdelivr.net/npm/gojs/release/go-debug.js"></script>
  <script id="code">
    
// Produce a Geometry that includes an arrowhead at the end of each segment.
// This only works with orthogonal non-Bezier routing.
class MultiArrowLink extends go.Link {
  constructor(init) {
    super();
    this.routing = go.Routing.Orthogonal;
    if (init) Object.assign(this, init);
  }

  static FigureBuilders = {
    "capillary":
        (geo, x, y, a) =>  // independent of angle
          geo.add(
            new go.PathFigure(x-3, y-3, false)
              .add(new go.PathSegment(go.SegmentType.Line, x+3, y+3))
              .add(new go.PathSegment(go.SegmentType.Move, x+3, y-3))
              .add(new go.PathSegment(go.SegmentType.Line, x-3, y+3))
          ),
      "software":
        (geo, x, y, a) =>  // independent of angle
          geo.add(
            new go.PathFigure(x+3, y, true)
              .add(new go.PathSegment(go.SegmentType.Arc, 0, 360, x, y, 3, 3))
          ),
      "mechanical":
        (geo, x, y, a) =>  // independent of angle
          geo.add(
            new go.PathFigure(x+3, y, true)
              .add(new go.PathSegment(go.SegmentType.Arc, 0, 360, x, y, 3, 3))
          ),
      "pneumatic":
        (geo, x, y, a) => {
          if (a === 90 || a === 270) {  // distinguish horizontal and vertical
            geo.add(
              new go.PathFigure(x-3, y-3, false)
                .add(new go.PathSegment(go.SegmentType.Line, x+3, y))
                .add(new go.PathSegment(go.SegmentType.Move, x-3, y))
                .add(new go.PathSegment(go.SegmentType.Line, x+3, y+3))
            );
          } else {
            geo.add(
              new go.PathFigure(x, y-3, false)
                .add(new go.PathSegment(go.SegmentType.Line, x-3, y+3))
                .add(new go.PathSegment(go.SegmentType.Move, x+3, y-3))
                .add(new go.PathSegment(go.SegmentType.Line, x, y+3))
            );
          }
        },
      "hydraulic":
        (geo, x, y, a) => {
          if (a === 90) {  // L-shape must be different for each angle
            geo.add(
              new go.PathFigure(x+3, y-1, false)
                .add(new go.PathSegment(go.SegmentType.Line, x-3, y-1))
                .add(new go.PathSegment(go.SegmentType.Line, x-3, y+3))
            );
          } else if (a === 270) {
            geo.add(
              new go.PathFigure(x+3, y-3, false)
                .add(new go.PathSegment(go.SegmentType.Line, x+3, y+1))
                .add(new go.PathSegment(go.SegmentType.Line, x-3, y+1))
            );
          } else if (a === 180) {
            geo.add(
              new go.PathFigure(x+2, y+3, false)
                .add(new go.PathSegment(go.SegmentType.Line, x+2, y-3))
                .add(new go.PathSegment(go.SegmentType.Line, x-1, y-3))
            );
          } else {
            geo.add(
              new go.PathFigure(x-1, y-3, false)
                .add(new go.PathSegment(go.SegmentType.Line, x-1, y+3))
                .add(new go.PathSegment(go.SegmentType.Line, x+2, y+3))
            );
          }
        },
      "guided":
        (geo, x, y, a) => {
          if (a === 90 || a === 270) {  // distinguish horizontal and vertical
            geo.add(
              new go.PathFigure(x, y-6, false)
                .add(new go.PathSegment(go.SegmentType.SvgArc, x, y, 3, 3, 0, true, true))
                .add(new go.PathSegment(go.SegmentType.SvgArc, x, y+6, 3, 3, 0, true, false))
            );
          } else {
            geo.add(
              new go.PathFigure(x-6, y, false)
                .add(new go.PathSegment(go.SegmentType.SvgArc, x, y, 3, 3, 0, true, true))
                .add(new go.PathSegment(go.SegmentType.SvgArc, x+6, y, 3, 3, 0, true, false))
            );
          }
        },
      "unguided":
        (geo, x, y, a) => {
          if (a === 90 || a === 270) {  // distinguish horizontal and vertical
            geo.add(
              new go.PathFigure(x, y-6, false)
                .add(new go.PathSegment(go.SegmentType.SvgArc, x, y, 3, 3, 0, true, true))
                .add(new go.PathSegment(go.SegmentType.SvgArc, x, y+6, 3, 3, 0, true, false))
            );
          } else {
            geo.add(
              new go.PathFigure(x-6, y, false)
                .add(new go.PathSegment(go.SegmentType.SvgArc, x, y, 3, 3, 0, true, true))
                .add(new go.PathSegment(go.SegmentType.SvgArc, x+6, y, 3, 3, 0, true, false))
            );
          }
        }
    };

  // produce a Geometry from the Link's route
  makeGeometry() {
    // get the Geometry created by the standard behavior
    const geo = super.makeGeometry();
    if (geo.type !== go.GeometryType.Path || geo.figures.length === 0) return geo;

    const mainfig = geo.figures.elt(0); // assume there's just one PathFigure
    const mainsegs = mainfig.segments;
    const mainnumsegs = mainsegs.length;

    const hasArrowheads = this.data?.arrowheads;
    if (hasArrowheads) {
      const arrowLen = 8; // length for each arrowhead
      const arrowWid = 3; // actually half-width of each arrowhead
      let fx = mainfig.startX; let fy = mainfig.startY;
      for (let i = 0; i < mainnumsegs; i++) {
        const e = mainsegs.elt(i);
        // assume each arrowhead is a simple triangle
        const ex = e.endX; const ey = e.endY;
        let bx = ex; let by = ey;
        let cx = ex; let cy = ey;
        if (fx < ex - arrowLen) {
          bx -= arrowLen; by += arrowWid;
          cx -= arrowLen; cy -= arrowWid;
        } else if (fx > ex + arrowLen) {
          bx += arrowLen; by += arrowWid;
          cx += arrowLen; cy -= arrowWid;
        } else if (fy < ey - arrowLen) {
          bx -= arrowWid; by -= arrowLen;
          cx += arrowWid; cy -= arrowLen;
        } else if (fy > ey + arrowLen) {
          bx -= arrowWid; by += arrowLen;
          cx += arrowWid; cy += arrowLen;
        }
        geo.add(
          new go.PathFigure(ex, ey, true)
            .add(new go.PathSegment(go.SegmentType.Line, bx, by))
            .add(new go.PathSegment(go.SegmentType.Line, cx, cy).close())
        );
        fx = ex; fy = ey;
      }
    }

    const type = this.data?.type;
    // maybe a function taking (geo,x,y,angle) that adds geometry for a mark at (x,y)
    const mark = MultiArrowLink.FigureBuilders[type];
    if (mark) {
      const minLength = 20;  // don't draw anything if segment is less than 20 document units long
      const interval = 30;  // draw every 30 document units
      const offset = 10;  // leave at 10 document units space between the first or last marks with the ends of the segment
      let fx = mainfig.startX; let fy = mainfig.startY;
      for (let i = 0; i < mainnumsegs; i++) {
        const e = mainsegs.elt(i);
        const ex = e.endX; const ey = e.endY;
        if (fx < ex - minLength) {
          const len = ex - fx;
          for (let x = fx + offset + ((len - 2*offset)%interval)/2; x < ex - offset; x += interval) mark(geo, x, fy, 0);
        } else if (fx > ex + minLength) {
          const len = fx - ex;
          for (let x = ex + offset + ((len - 2*offset)%interval)/2; x < fx - offset; x += interval) mark(geo, x, fy, 180);
        } else if (fy < ey - minLength) {
          const len = ey - fy;
          for (let y = fy + offset + ((len - 2*offset)%interval)/2; y < ey - offset; y += interval) mark(geo, fx, y, 90);
        } else if (fy > ey + minLength) {
          const len = fy - ey;
          for (let y = ey + offset + ((len - 2*offset)%interval)/2; y < fy - offset; y += interval) mark(geo, fx, y, 90);
        }
        fx = ex;
        fy = ey;
      }
    }

    return geo;
  }
}
// end of MultiArrowLink class

const myDiagram =
  new go.Diagram("myDiagramDiv", {
      "draggingTool.isGridSnapEnabled": true,
      "draggingTool.gridSnapCellSize": new go.Size(0.5, 0.5),
      "rotatingTool.snapAngleMultiple": 90,
      "rotatingTool.snapAngleEpsilon": 45,
      allowLink: false,
      allowRelink: false,
      "undoManager.isEnabled": true,
      "ModelChanged": e => {     // just for demonstration purposes,
        if (e.isTransactionFinished) {  // show the model data in the page's TextArea
          document.getElementById("mySavedModel").textContent = e.model.toJson();
        }
      }
    });

function allowLinking(allow) {
  myDiagram.commit(diag => {
    diag.allowLink = allow;
    diag.allowRelink = allow;
    diag.nodes.each(n => n.port.cursor = allow ? "pointer" : "");
  });
}


function commonNodeStyle(node) {
  node.type = go.Panel.Spot;
  node.locationSpot = go.Spot.Center;
  node.locationObjectName = "ICON";
  node.bindTwoWay("location", "loc", go.Point.parse, go.Point.stringify);
}

function commonPortStyle(elt) {
  elt.portId = "";
  elt.fromLinkable = true;
  elt.toLinkable = true;
  //elt.cursor = "pointer";
}

go.Shape.defineFigureGenerator("SharedDisplay", (shp, w, h) => {
  return new go.Geometry()
    .add(new go.PathFigure(0, 0)
        .add(new go.PathSegment(go.SegmentType.Line, w, 0))
        .add(new go.PathSegment(go.SegmentType.Line, w, h))
        .add(new go.PathSegment(go.SegmentType.Line, 0, h).close()))
    .add(new go.PathFigure(w, h/2)
        .add(new go.PathSegment(go.SegmentType.Arc, 0, 360, w/2, h/2, w/2, h/2)))
    .add(new go.PathFigure(0, h/2)
        .add(new go.PathSegment(go.SegmentType.Line, w, h/2)));
});

myDiagram.nodeTemplateMap.add("Instrument",
  new go.Node()
    .apply(commonNodeStyle)
    .set({ rotatable: false })  // override commonNodeStyle
    .add(
      new go.Shape("Circle", {
          name: "ICON",
          width: 40, height: 40, fill: "white",
        })
        .apply(commonPortStyle)
        .bind("figure", "symbol"),
      new go.TextBlock({ alignment: new go.Spot(0.5, 0.33) })
        .bind("text", "function"),
      new go.TextBlock({ alignment: new go.Spot(0.5, 0.7) })
        .bind("text", "tag"),
      new go.TextBlock({ alignment: go.Spot.TopRight, visible: false })
        .bind("text", "quantity")
        .bind("visible", "quantity", q => !!q)
    ));

go.Shape.defineFigureGenerator("Butterfly", (shp, w, h) => {
  return new go.Geometry()
    .add(new go.PathFigure(0, 0)
        .add(new go.PathSegment(go.SegmentType.Line, 0, h))
        .add(new go.PathSegment(go.SegmentType.Move, w, 0))
        .add(new go.PathSegment(go.SegmentType.Line, w, h)))
    .add(new go.PathFigure(w*5/6, h/6)
        .add(new go.PathSegment(go.SegmentType.Line, w/6, h*5/6)))
    .add(new go.PathFigure(w/2, h/2)
        .add(new go.PathSegment(go.SegmentType.Arc, 0, 360, w/2, h/2, 2, 2)))
});

myDiagram.nodeTemplateMap.add("Valve",
  new go.Node({ rotatable: true })
    .apply(commonNodeStyle)
    .bindTwoWay("angle")
    .add(
      new go.Shape({
          name: "ICON",
          width: 30, height: 30, background: "white",
          fromSpot: go.Spot.LeftRightSides, toSpot: go.Spot.LeftRightSides
        })
        .apply(commonPortStyle)
        .bind("figure", "symbol")
        .bind("stroke", "open", open => open === true ? "green" : (open === false ? "red" : "black")),
      new go.TextBlock({ alignment: go.Spot.Top, alignmentFocus: go.Spot.Bottom })
        .bind("text", "size")
        .bind("angle", "angle", a => a === 180 ? 180 : 0),
      new go.Panel("Vertical", { alignment: go.Spot.Bottom, alignmentFocus: go.Spot.Top })
        .add(
          new go.TextBlock()
            .bind("text", "spec")
            .bind("angle", "angle", a => a === 180 ? 180 : 0),
          new go.TextBlock()
            .bind("text", "tag")
            .bind("angle", "angle", a => a === 180 ? 180 : 0)
        )
    ));

myDiagram.nodeTemplateMap.add("I/O",
  new go.Node({ rotatable: true })
    .apply(commonNodeStyle)
    .bindTwoWay("angle")
    .add(
      new go.Shape("TriangleRight", {
          name: "ICON", width: 30, height: 30, fill: "white",
          fromSpot: go.Spot.Right, toSpot: go.Spot.Left
        })
        .apply(commonPortStyle),
      new go.TextBlock({ alignment: new go.Spot(0.3, 0.53) })
        .bind("text", "type")
        .bind("angle", "angle", a => a === 180 ? 180 : 0)
    ));


function defineLinkCMbutton(name, type) {
  return go.GraphObject.build("ContextMenuButton", {
      click: (e, but) => {
        const link = but.part.adornedPart;
        e.diagram.model.commit(m => {
          m.set(link.data, "type", type);
          if (link.invalidateGeometry) {
            link.invalidateGeometry();
          } else {
            const oldskips = m.skipsUndoManager;
            m.skipsUndoManager = true;
            link.smoothness = 1;
            link.smoothness = 0.5;
            m.skipsUndoManager = oldskips;
          }
        });
      }
    })
    .add(new go.TextBlock(name))
}

myDiagram.linkTemplate =
  new MultiArrowLink({
      routing: go.Link.Orthogonal,
      relinkableFrom: true, relinkableTo: true,
      reshapable: true, resegmentable: true,
      contextMenu:
        go.GraphObject.build("ContextMenu")
          .add(
            defineLinkCMbutton("major process piping", "major"),
            defineLinkCMbutton("minor/instrument piping", "minor"),
            defineLinkCMbutton("existing piping", "existing"),
            defineLinkCMbutton("electical signal", "electrical"),
            defineLinkCMbutton("capillary tubing", "capillary"),
            defineLinkCMbutton("software or data link", "software"),
            defineLinkCMbutton("mechanical link", "mechanical"),
            defineLinkCMbutton("pneumatic signal/piping", "pneumatic"),
            defineLinkCMbutton("hydraulic signal", "hydraulic"),
            defineLinkCMbutton("guided wave", "guided"),
            defineLinkCMbutton("unguided wave", "unguided")
          )
    })
    .add(
      new go.Shape({ strokeWidth: 1.5 })
        .bind("fill", "type", t => t === "software" ? "white" : "black")
        .bind("strokeWidth", "type", t => t === "major" ? 3 : 1.5)
        .bind("strokeDashArray", "type", t => t === "existing" ? [20, 10] : (t === "electrical" ? [10, 3] : null))
    );

myDiagram.model = new go.GraphLinksModel(
[
  { key: 1, category: "Instrument", function: "LE", tag: "123A", quantity: 3, loc: "20.5 27.5" },
  { key: 2, category: "Instrument", symbol: "SharedDisplay", function: "ZI", tag: "101-", loc: "143 27.5" },
  { key: 3, category: "Valve", symbol: "Butterfly", size: "100", spec: "N4", tag: "HV0001", loc: "77 108", open: true },
  { key: 4, category: "I/O", type: "DI", loc: "189 149" },
],
[
  { from: 1, to: 2, type: "hydraulic" },
  { from: 1, to: 3, type: "guided" },
  { from: 3, to: 4, type: "electrical", arrowheads: true },
]);
  </script>
</body>
</html>

Can you code an Hide/Show labelling linkDataArray and nodeDataArray ?

I think it will be rapidely needed to can give them an Id

??? what do you mean? Some examples would be useful.

Sorry, i write a mistake talking about Id and Label my choice of word was not enough precise for an professional use.

  • It’s more an comment for exemple : “Meter proving”
  • or characteristic : 50°F Electric / 4G 120mm² etc…

What behaviors might a “characteristic” have? Can the user select it? If they can select it, can they move it or copy it or delete it? If they delete it, does it delete the line/Link that it is connected with? Or is its lifetime necessarily the same as the line/Link that it’s on?

Basically are “characteristics” independent Nodes, or are they merely labels owned by the line/Link?

Comments are easy:

<!DOCTYPE html>
<html>
<head>
  <title>Minimal Piping & Instrumentation Diagram</title>
  <!-- Copyright 1998-2025 by Northwoods Software Corporation. -->
</head>
<body>
  <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:400px"></div>
  Use the context menu on a line/Link to change its line symbol.<br>
  <input type="checkbox" onclick="allowLinking(event.target.checked)">Allow Draw Link</input><br>
  <textarea id="mySavedModel" style="width:100%;height:250px"></textarea>
  Design taken from <a href="https://www.aiche.org/sites/default/files/chenected/2010/08/ChEnected-Example-PIDs-and-Lead-Sheets.pdf">AIChE example diagram</a>

  <script src="https://cdn.jsdelivr.net/npm/gojs/release/go-debug.js"></script>
  <script id="code">
    
// Produce a Geometry that includes an arrowhead at the end of each segment.
// This only works with orthogonal non-Bezier routing.
class MultiArrowLink extends go.Link {
  constructor(init) {
    super();
    this.routing = go.Routing.Orthogonal;
    if (init) Object.assign(this, init);
  }

  static FigureBuilders = {
    "capillary":
        (geo, x, y, a) =>  // independent of angle
          geo.add(
            new go.PathFigure(x-3, y-3, false)
              .add(new go.PathSegment(go.SegmentType.Line, x+3, y+3))
              .add(new go.PathSegment(go.SegmentType.Move, x+3, y-3))
              .add(new go.PathSegment(go.SegmentType.Line, x-3, y+3))
          ),
      "software":
        (geo, x, y, a) =>  // independent of angle
          geo.add(
            new go.PathFigure(x+3, y, true)
              .add(new go.PathSegment(go.SegmentType.Arc, 0, 360, x, y, 3, 3))
          ),
      "mechanical":
        (geo, x, y, a) =>  // independent of angle
          geo.add(
            new go.PathFigure(x+3, y, true)
              .add(new go.PathSegment(go.SegmentType.Arc, 0, 360, x, y, 3, 3))
          ),
      "pneumatic":
        (geo, x, y, a) => {
          if (a === 90 || a === 270) {  // distinguish horizontal and vertical
            geo.add(
              new go.PathFigure(x-3, y-3, false)
                .add(new go.PathSegment(go.SegmentType.Line, x+3, y))
                .add(new go.PathSegment(go.SegmentType.Move, x-3, y))
                .add(new go.PathSegment(go.SegmentType.Line, x+3, y+3))
            );
          } else {
            geo.add(
              new go.PathFigure(x, y-3, false)
                .add(new go.PathSegment(go.SegmentType.Line, x-3, y+3))
                .add(new go.PathSegment(go.SegmentType.Move, x+3, y-3))
                .add(new go.PathSegment(go.SegmentType.Line, x, y+3))
            );
          }
        },
      "hydraulic":
        (geo, x, y, a) => {
          if (a === 90) {  // L-shape must be different for each angle
            geo.add(
              new go.PathFigure(x+3, y-1, false)
                .add(new go.PathSegment(go.SegmentType.Line, x-3, y-1))
                .add(new go.PathSegment(go.SegmentType.Line, x-3, y+3))
            );
          } else if (a === 270) {
            geo.add(
              new go.PathFigure(x-3, y+1, false)
                .add(new go.PathSegment(go.SegmentType.Line, x+3, y+1))
                .add(new go.PathSegment(go.SegmentType.Line, x+3, y-3))
            );
          } else if (a === 180) {
            geo.add(
              new go.PathFigure(x+2, y+3, false)
                .add(new go.PathSegment(go.SegmentType.Line, x+2, y-3))
                .add(new go.PathSegment(go.SegmentType.Line, x-1, y-3))
            );
          } else {
            geo.add(
              new go.PathFigure(x-1, y-3, false)
                .add(new go.PathSegment(go.SegmentType.Line, x-1, y+3))
                .add(new go.PathSegment(go.SegmentType.Line, x+2, y+3))
            );
          }
        },
      "guided":
        (geo, x, y, a) => {
          if (a === 90 || a === 270) {  // distinguish horizontal and vertical
            geo.add(
              new go.PathFigure(x, y-6, false)
                .add(new go.PathSegment(go.SegmentType.SvgArc, x, y, 3, 3, 0, true, true))
                .add(new go.PathSegment(go.SegmentType.SvgArc, x, y+6, 3, 3, 0, true, false))
            );
          } else {
            geo.add(
              new go.PathFigure(x-6, y, false)
                .add(new go.PathSegment(go.SegmentType.SvgArc, x, y, 3, 3, 0, true, true))
                .add(new go.PathSegment(go.SegmentType.SvgArc, x+6, y, 3, 3, 0, true, false))
            );
          }
        },
      "unguided":
        (geo, x, y, a) => {
          if (a === 90 || a === 270) {  // distinguish horizontal and vertical
            geo.add(
              new go.PathFigure(x, y-6, false)
                .add(new go.PathSegment(go.SegmentType.SvgArc, x, y, 3, 3, 0, true, true))
                .add(new go.PathSegment(go.SegmentType.SvgArc, x, y+6, 3, 3, 0, true, false))
            );
          } else {
            geo.add(
              new go.PathFigure(x-6, y, false)
                .add(new go.PathSegment(go.SegmentType.SvgArc, x, y, 3, 3, 0, true, true))
                .add(new go.PathSegment(go.SegmentType.SvgArc, x+6, y, 3, 3, 0, true, false))
            );
          }
        }
    };

  // produce a Geometry from the Link's route
  makeGeometry() {
    // get the Geometry created by the standard behavior
    const geo = super.makeGeometry();
    if (geo.type !== go.GeometryType.Path || geo.figures.length === 0) return geo;

    const mainfig = geo.figures.elt(0); // assume there's just one PathFigure
    const mainsegs = mainfig.segments;
    const mainnumsegs = mainsegs.length;

    const hasArrowheads = this.data?.arrowheads;
    if (hasArrowheads) {
      const arrowLen = 8; // length for each arrowhead
      const arrowWid = 3; // actually half-width of each arrowhead
      let fx = mainfig.startX; let fy = mainfig.startY;
      for (let i = 0; i < mainnumsegs; i++) {
        const e = mainsegs.elt(i);
        // assume each arrowhead is a simple triangle
        const ex = e.endX; const ey = e.endY;
        let bx = ex; let by = ey;
        let cx = ex; let cy = ey;
        if (fx < ex - arrowLen) {
          bx -= arrowLen; by += arrowWid;
          cx -= arrowLen; cy -= arrowWid;
        } else if (fx > ex + arrowLen) {
          bx += arrowLen; by += arrowWid;
          cx += arrowLen; cy -= arrowWid;
        } else if (fy < ey - arrowLen) {
          bx -= arrowWid; by -= arrowLen;
          cx += arrowWid; cy -= arrowLen;
        } else if (fy > ey + arrowLen) {
          bx -= arrowWid; by += arrowLen;
          cx += arrowWid; cy += arrowLen;
        }
        geo.add(
          new go.PathFigure(ex, ey, true)
            .add(new go.PathSegment(go.SegmentType.Line, bx, by))
            .add(new go.PathSegment(go.SegmentType.Line, cx, cy).close())
        );
        fx = ex; fy = ey;
      }
    }

    const type = this.data?.type;
    // maybe a function taking (geo,x,y,angle) that adds geometry for a mark at (x,y)
    const mark = MultiArrowLink.FigureBuilders[type];
    if (mark) {
      const minLength = 20;  // don't draw anything if segment is less than 20 document units long
      const interval = 30;  // draw every 30 document units
      const offset = 10;  // leave at 10 document units space between the first or last marks with the ends of the segment
      let fx = mainfig.startX; let fy = mainfig.startY;
      for (let i = 0; i < mainnumsegs; i++) {
        const e = mainsegs.elt(i);
        const ex = e.endX; const ey = e.endY;
        if (fx < ex - minLength) {
          const len = ex - fx;
          for (let x = fx + offset + ((len - 2*offset)%interval)/2; x < ex - offset; x += interval) mark(geo, x, fy, 0);
        } else if (fx > ex + minLength) {
          const len = fx - ex;
          for (let x = ex + offset + ((len - 2*offset)%interval)/2; x < fx - offset; x += interval) mark(geo, x, fy, 180);
        } else if (fy < ey - minLength) {
          const len = ey - fy;
          for (let y = fy + offset + ((len - 2*offset)%interval)/2; y < ey - offset; y += interval) mark(geo, fx, y, 90);
        } else if (fy > ey + minLength) {
          const len = fy - ey;
          for (let y = ey + offset + ((len - 2*offset)%interval)/2; y < fy - offset; y += interval) mark(geo, fx, y, 90);
        }
        fx = ex;
        fy = ey;
      }
    }

    return geo;
  }
}
// end of MultiArrowLink class


// DIAGRAM

const myDiagram =
  new go.Diagram("myDiagramDiv", {
      "draggingTool.isGridSnapEnabled": true,
      "draggingTool.gridSnapCellSize": new go.Size(0.5, 0.5),
      "rotatingTool.snapAngleMultiple": 90,
      "rotatingTool.snapAngleEpsilon": 45,
      allowLink: false,
      allowRelink: false,
      "undoManager.isEnabled": true,
      "ModelChanged": e => {     // just for demonstration purposes,
        if (e.isTransactionFinished) {  // show the model data in the page's TextArea
          document.getElementById("mySavedModel").textContent = e.model.toJson();
        }
      }
    });

function allowLinking(allow) {
  myDiagram.commit(diag => {
    diag.allowLink = allow;
    diag.allowRelink = allow;
    diag.nodes.each(n => n.port.cursor = allow ? "pointer" : "");
  });
}


// NODES

function commonNodeStyle(node) {
  node.type = go.Panel.Spot;
  node.locationSpot = go.Spot.Center;
  node.locationObjectName = "ICON";
  node.bindTwoWay("location", "loc", go.Point.parse, go.Point.stringify);
}

function commonPortStyle(elt) {
  elt.portId = "";
  elt.fromLinkable = true;
  elt.toLinkable = true;
  //elt.cursor = "pointer";
}

go.Shape.defineFigureGenerator("SharedDisplay", (shp, w, h) => {
  return new go.Geometry()
    .add(new go.PathFigure(0, 0)
        .add(new go.PathSegment(go.SegmentType.Line, w, 0))
        .add(new go.PathSegment(go.SegmentType.Line, w, h))
        .add(new go.PathSegment(go.SegmentType.Line, 0, h).close()))
    .add(new go.PathFigure(w, h/2)
        .add(new go.PathSegment(go.SegmentType.Arc, 0, 360, w/2, h/2, w/2, h/2)))
    .add(new go.PathFigure(0, h/2)
        .add(new go.PathSegment(go.SegmentType.Line, w, h/2)));
});

myDiagram.nodeTemplateMap.add("Instrument",
  new go.Node()
    .apply(commonNodeStyle)
    .set({ rotatable: false })  // override commonNodeStyle
    .add(
      new go.Shape("Circle", {
          name: "ICON",
          width: 40, height: 40, fill: "white",
        })
        .apply(commonPortStyle)
        .bind("figure", "symbol"),
      new go.TextBlock({ alignment: new go.Spot(0.5, 0.33) })
        .bind("text", "function"),
      new go.TextBlock({ alignment: new go.Spot(0.5, 0.7) })
        .bind("text", "tag"),
      new go.TextBlock({ alignment: go.Spot.TopRight, visible: false })
        .bind("text", "quantity")
        .bind("visible", "quantity", q => !!q)
    ));

go.Shape.defineFigureGenerator("Butterfly", (shp, w, h) => {
  return new go.Geometry()
    .add(new go.PathFigure(0, 0)
        .add(new go.PathSegment(go.SegmentType.Line, 0, h))
        .add(new go.PathSegment(go.SegmentType.Move, w, 0))
        .add(new go.PathSegment(go.SegmentType.Line, w, h)))
    .add(new go.PathFigure(w*5/6, h/6)
        .add(new go.PathSegment(go.SegmentType.Line, w/6, h*5/6)))
    .add(new go.PathFigure(w/2, h/2)
        .add(new go.PathSegment(go.SegmentType.Arc, 0, 360, w/2, h/2, 2, 2)))
});

myDiagram.nodeTemplateMap.add("Valve",
  new go.Node({ rotatable: true })
    .apply(commonNodeStyle)
    .bindTwoWay("angle")
    .add(
      new go.Shape({
          name: "ICON",
          width: 30, height: 30, background: "white",
          fromSpot: go.Spot.LeftRightSides, toSpot: go.Spot.LeftRightSides
        })
        .apply(commonPortStyle)
        .bind("figure", "symbol")
        .bind("stroke", "open", open => open === true ? "green" : (open === false ? "red" : "black")),
      new go.TextBlock({ alignment: go.Spot.Top, alignmentFocus: go.Spot.Bottom })
        .bind("text", "size")
        .bind("angle", "angle", a => a === 180 ? 180 : 0),
      new go.Panel("Vertical", { alignment: go.Spot.Bottom, alignmentFocus: go.Spot.Top })
        .add(
          new go.TextBlock()
            .bind("text", "spec")
            .bind("angle", "angle", a => a === 180 ? 180 : 0),
          new go.TextBlock()
            .bind("text", "tag")
            .bind("angle", "angle", a => a === 180 ? 180 : 0)
        )
    ));

myDiagram.nodeTemplateMap.add("I/O",
  new go.Node({ rotatable: true })
    .apply(commonNodeStyle)
    .bindTwoWay("angle")
    .add(
      new go.Shape("TriangleRight", {
          name: "ICON", width: 30, height: 30, fill: "white",
          fromSpot: go.Spot.Right, toSpot: go.Spot.Left
        })
        .apply(commonPortStyle),
      new go.TextBlock({ alignment: new go.Spot(0.3, 0.53) })
        .bind("text", "type")
        .bind("angle", "angle", a => a === 180 ? 180 : 0)
    ));

myDiagram.nodeTemplateMap.add("Comment",
  new go.Node({ })
    .bindTwoWay("location", "loc", go.Point.parse, go.Point.stringify)
    .add(
      new go.TextBlock({ stroke: "brown" })
        .bind("text")
    ));


// LINKS

function defineLinkCMbutton(name, type) {
  return go.GraphObject.build("ContextMenuButton", {
      click: (e, but) => {
        const link = but.part.adornedPart;
        e.diagram.model.commit(m => {
          m.set(link.data, "type", type);
          if (link.invalidateGeometry) {
            link.invalidateGeometry();
          } else {
            const oldskips = m.skipsUndoManager;
            m.skipsUndoManager = true;
            link.smoothness = 1;
            link.smoothness = 0.5;
            m.skipsUndoManager = oldskips;
          }
        });
      }
    })
    .add(new go.TextBlock(name))
}

myDiagram.linkTemplate =
  new MultiArrowLink({
      routing: go.Link.Orthogonal,
      relinkableFrom: true, relinkableTo: true,
      reshapable: true, resegmentable: true,
      contextMenu:
        go.GraphObject.build("ContextMenu")
          .add(
            defineLinkCMbutton("major process piping", "major"),
            defineLinkCMbutton("minor/instrument piping", "minor"),
            defineLinkCMbutton("existing piping", "existing"),
            defineLinkCMbutton("electical signal", "electrical"),
            defineLinkCMbutton("capillary tubing", "capillary"),
            defineLinkCMbutton("software or data link", "software"),
            defineLinkCMbutton("mechanical link", "mechanical"),
            defineLinkCMbutton("pneumatic signal/piping", "pneumatic"),
            defineLinkCMbutton("hydraulic signal", "hydraulic"),
            defineLinkCMbutton("guided wave", "guided"),
            defineLinkCMbutton("unguided wave", "unguided")
          )
    })
    .add(
      new go.Shape({ strokeWidth: 1.5 })
        .bind("fill", "type", t => t === "software" ? "white" : "black")
        .bind("strokeWidth", "type", t => t === "major" ? 3 : 1.5)
        .bind("strokeDashArray", "type", t => t === "existing" ? [20, 10] : (t === "electrical" ? [10, 3] : null))
    );

myDiagram.linkTemplateMap.add("Comment",
  new go.Link({ fromSpot: go.Spot.LeftRightSides, toSpot: go.Spot.None })
    .add(
      new go.Shape({ stroke: "brown", strokeWidth: 1.5 })
    ));


// MODEL

myDiagram.model = new go.GraphLinksModel(
[
  { key: 1, category: "Instrument", function: "LE", tag: "123A", quantity: 3, loc: "20.5 27.5" },
  { key: 2, category: "Instrument", symbol: "SharedDisplay", function: "ZI", tag: "101-", loc: "143 27.5" },
  { key: 3, category: "Valve", symbol: "Butterfly", size: "100", spec: "N4", tag: "HV0001", loc: "77 108", open: true },
  { key: 4, category: "I/O", type: "DI", loc: "189 149" },
  { key: 5, category: "Comment", text: "this is a comment", loc: "-101.5 133.5" }
],
[
  { from: 1, to: 2, type: "hydraulic" },
  { from: 1, to: 3, type: "guided" },
  { from: 3, to: 4, type: "electrical", arrowheads: true },
  { from: 5, to: 3, category: "Comment" }
]);
  </script>
</body>
</html>

It’s important “characteristics” are owned by link.
In this normalised drawing, it’s not needed to add to much details.
Have the ability to can easily bring some important characteristics ( Pipe Diam, Line name, etc…) can be an important feature.
It’s not only comment in plan but really linked to the object.
I was thinking about an Hide/Show, because they can be seen has a “verbose mode”

For labels on links, please read Link Labels | GoJS

If you want to show and hide "Comment"s, that is easy to implement. Or just the “Comment” Nodes connected with a particular Node. Or any other action you can think of.