UNPKG

create-gojs-kit

Version:

A CLI for downloading GoJS samples, extensions, and docs

1,503 lines 79.8 kB
<!DOCTYPE html>
<html lang="en">
  <head>
    
    <meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, viewport-fit=cover"/>

<meta name="description" content="Interactive circuit schematic implemented by GoJS in JavaScript for HTML." />
<meta itemprop="description" content="Interactive circuit schematic implemented by GoJS in JavaScript for HTML." />
<meta property="og:description" content="Interactive circuit schematic implemented by GoJS in JavaScript for HTML." />
<meta name="twitter:description" content="Interactive circuit schematic implemented by GoJS in JavaScript for HTML." />


<link rel="preconnect" href="https://rsms.me/">
<link rel="stylesheet" href="../assets/css/style.css">
<!-- Copyright 1998-2025 by Northwoods Software Corporation. -->


<meta itemprop="name" content="Interactive Diagram for Building Circuit Schematics" />
<meta property="og:title" content="Interactive Diagram for Building Circuit Schematics" />
<meta name="twitter:title" content="Interactive Diagram for Building Circuit Schematics" />


<meta property="og:image" content="https://gojs.net/latest/assets/images/screenshots/circuit.png" />
<meta itemprop="image" content="https://gojs.net/latest/assets/images/screenshots/circuit.png" />
<meta name="twitter:image" content="https://gojs.net/latest/assets/images/screenshots/circuit.png" />

<meta property="og:url" content="https://gojs.net/latest/samples/circuitDesigner.html" />
<meta property="twitter:url" content="https://gojs.net/latest/samples/circuitDesigner.html" />

<meta name="twitter:card" content="summary_large_image" />
<meta property="og:type" content="website" />
<meta property="twitter:domain" content="gojs.net" />

    <title>
      Interactive Diagram for Building Circuit Schematics | GoJS Diagramming Library
    </title>
  </head>

  <body>
    <!-- This top nav is not part of the sample code -->
    <nav id="navTop" class=" w-full h-[var(--topnav-h)] z-30 bg-white border-b border-b-gray-200">
  <div class="max-w-screen-xl mx-auto flex flex-wrap items-start justify-between px-4">
    <a class="text-white bg-nwoods-primary font-bold !leading-[calc(var(--topnav-h)_-_1px)] my-0 px-2 text-4xl lg:text-5xl logo"
       href="../">
      GoJS
    </a>
    <div class="relative">
      <button id="topnavButton" class="h-[calc(var(--topnav-h)_-_1px)] px-2 m-0 text-gray-900 bg-inherit shadow-none md:hidden hover:!bg-inherit hover:!text-nwoods-accent hover:!shadow-none" aria-label="Navigation">
        <svg class="h-7 w-7 block" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
          <path d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" stroke-linecap="round" stroke-linejoin="round"/>
        </svg>
      </button>
      <div id="topnavList" class="hidden md:block">
        <div class="absolute right-0 z-30 flex flex-col items-end rounded border border-gray-200 p-4 pl-12 shadow bg-white text-gray-900 font-semibold
                    md:flex-row md:space-x-4 md:items-start md:border-0 md:p-0 md:shadow-none md:bg-inherit">
          <a href="../learn/">Learn</a>
          <a href="../samples/">Samples</a>
          <a href="../intro/">Intro</a>
          <a href="../api/">API</a>
          <a href="../download.html">Download</a>
          <a href="https://forum.nwoods.com/c/gojs/11" target="_blank" rel="noopener">Forum</a>
          <a id="tc" href="https://nwoods.com/contact.html"
            target="_blank" rel="noopener" onclick="getOutboundLink('https://nwoods.com/contact.html', 'contact');">Contact</a>
          <a id="tb" href="https://nwoods.com/sales/index.html"
            target="_blank" rel="noopener" onclick="getOutboundLink('https://nwoods.com/sales/index.html', 'buy');">Buy</a>
        </div>
      </div>
    </div>
  </div>
</nav>
<script>
  window.addEventListener("DOMContentLoaded", function () {
    // topnav
    var topButton = document.getElementById("topnavButton");
    var topnavList = document.getElementById("topnavList");
    if (topButton && topnavList) {
      topButton.addEventListener("click", function (e) {
        topnavList
          .classList
          .toggle("hidden");
        e.stopPropagation();
      });
      document.addEventListener("click", function (e) {
        // if the clicked element isn't the list, close the list
        if (!topnavList.classList.contains("hidden") && !e.target.closest("#topnavList")) {
          topButton.click();
        }
      });

      // set active <a> element
      var url = window
        .location
        .href
        .toLowerCase();
      var aTags = topnavList.getElementsByTagName('a');
      for (var i = 0; i < aTags.length; i++) {
        var lowerhref = aTags[i]
          .href
          .toLowerCase();
        if (lowerhref.endsWith('.html')) 
          lowerhref = lowerhref.slice(0, -5);
        if (url.startsWith(lowerhref)) {
          aTags[i]
            .classList
            .add('active');
          break;
        }
      }
    }
  });
</script>
    <div class="flex flex-col prose">
      <div class="w-full max-w-screen-xl mx-auto">
        <!-- * * * * * * * * * * * * * -->
        <!-- Start of GoJS sample code -->
        <script src="https://cdn.jsdelivr.net/npm/gojs@3.1.0"></script>

        

        <div id="allSampleContent" class="p-4 w-full">
          
            
            

<script src="../extensions/OrthogonalLinkReshapingTool.js"></script>
<script src="../extensions/PolylineLinkingTool.js"></script>

<script id="code">

  // Size of the grid snapping cells
  const CELLSIZE = 10;

  function init() {
    myDiagram = new go.Diagram(
      'myDiagramDiv',
      {
        'undoManager.isEnabled': true,
        // Lets links be reshaped and moved around
        linkReshapingTool: new OrthogonalLinkReshapingTool(),
        // enables the grid snapping tool for dragging nodes, default is 10x10
        'draggingTool.isGridSnapEnabled': true,
        'draggingTool.gridSnapCellSize': new go.Size(CELLSIZE, CELLSIZE),
        // allow Ctrl-G to call groupSelection()
        'commandHandler.archetypeGroupData': { text: 'Group', color: '#029ffa10', isGroup: true },
        'toolManager.hoverDelay': 3000,
        click: (e) => {
          e.diagram.commit((d) => d.clearHighlighteds(), 'no highlighteds');
        },
        ExternalObjectsDropped: e => {
          // handle drops from the Palette
          if (markingPorts) {
            var newnode = e.diagram.selection.first();
            markNode(newnode);
          }
        },
        initialAutoScale: go.AutoScale.Uniform
      }
    );

    // Enables the polyline linking tool
    myDiagram.toolManager.linkingTool = new PolylineLinkingTool();
    myDiagram.toolManager.linkingTool.temporaryLink.routing = go.Routing.Orthogonal;

    // Checks if node is still in the diagram, adds it to the set
    function stillExists(nodeKey, theSet) {
      if (myDiagram.findNodeForKey(nodeKey)) {
        theSet.add(nodeKey);
      }
    }

    // Listens for changes to the link data array to update port markings if marks unused ports is on
    myDiagram.addModelChangedListener(e => {
      if (!markingPorts) return; // Don't run if not marking unused ports
      if (myDiagram.skipsUndoManager) return; // We mark ports in skipped transactions - don't run for those either
      if (e.isTransactionFinished) {
        const nodesToUpdate = new Set();
        const tx = e.object;
        if (tx instanceof go.Transaction) {
          tx.changes.each(c => {
            // If a relink occurs, add involved nodes to the set
            if (c.modelChange === 'linkFromKey' || c.modelChange === 'linkToKey') {
              stillExists(c.oldValue, nodesToUpdate);
              stillExists(c.newValue, nodesToUpdate);
              stillExists(c.object.from, nodesToUpdate);
              stillExists(c.object.to, nodesToUpdate);
            }
            // Handles relinks between the same two nodes or between a node and itself
            if (c.modelChange === 'linkFromPortId' || c.modelChange === 'linkToPortId') {
              stillExists(c.object.from, nodesToUpdate);
              stillExists(c.object.to, nodesToUpdate);
            }
            if (c.modelChange === 'linkDataArray') {
              // If a link is added or removed, add its from and to node keys to the set
              if (c.oldValue) {
                stillExists(c.oldValue['from'], nodesToUpdate);
                stillExists(c.oldValue['to'], nodesToUpdate);
              }
              if (c.newValue) {
                stillExists(c.newValue['from'], nodesToUpdate);
                stillExists(c.newValue['to'], nodesToUpdate);
              }
            }
          });
        }
        for (x of nodesToUpdate) {
          const n = myDiagram.findNodeForKey(x);
          markNode(n);
        }
      }
    });

    myPalette = new go.Palette(
      'myPaletteDiv',
      {
        'animationManager.isEnabled': false,
        'toolManager.hoverDelay': 600,
        layout: new go.GridLayout({ wrappingColumn: 2 })
      }
    );

    // Don't show context menus in the palette
    myPalette.toolManager.contextMenuTool.isEnabled = false;

    // A helper function for defining tooltips for buttons
    function makeToolTip(str) {
      return go.GraphObject.build('ToolTip').add(new go.TextBlock(str));
    }

    // Returns a simple context menu button with a given function and text
    function createContextMenuButton(text, func) {
      return go.GraphObject.build('ContextMenuButton', {
          // Passes the node instead of the adornment
          click: (e, button) => func(e, button.part.adornedObject),
          'ButtonBorder.fill': 'white',
          "_buttonFillOver": '#1E90FF20',
        })
          .add(new go.TextBlock(text))
    }

    // Context menus for different nodes
    const ICContextMenu = go.GraphObject.build('ContextMenu')
      .add(
        createContextMenuButton('Rotate Node', flipHV),
        createContextMenuButton('Swap Pins', swapPins)
      );

    const switchContextMenu = go.GraphObject.build('ContextMenu')
      .add(
        createContextMenuButton('Rotate Node', flipHV),
        createContextMenuButton('Open Switch', openSwitch)
      );

    const nodeContextMenu = go.GraphObject.build('ContextMenu')
      .add(
        createContextMenuButton('Rotate Node', flipCircle)
      )

    const groupContextMenu = go.GraphObject.build('ContextMenu')
      .add(
        createContextMenuButton('Change Group Color', changeColor),
        createContextMenuButton('Ungroup', (e, obj) => e.diagram.commandHandler.ungroupSelection())
      );

    // Template for integrated circuit pins, serve as the ports for each IC
    const pinTemplate = new go.Panel('Auto', {
      // Allows user selection and link drawing
      fromLinkable: true,
      toLinkable: true,
      click: (e, pin) => highlightLinksFromPin(e, pin),
      mouseEnter: (e, port) => lightPort(e, port),
      mouseLeave: (e, port) => unlightPort(e, port),
      cursor: 'pointer'
    })
      .bind('portId', 'key', (v) => String(v))
      .bind('fromSpot', 'side', (v) => (v === 'left' ? go.Spot.Left : go.Spot.Right))
      .bind('toSpot', 'side', (v) => (v === 'left' ? go.Spot.Left : go.Spot.Right))
      .add(
        new go.Shape('Rectangle', {
          name: 'SHAPE',
          width: 25,
          height: 20,
          stroke: 'black',
          strokeWidth: 1,
          fill: 'white',
          margin: new go.Margin(4.5, 0)
        }),
        new go.TextBlock({
          font: '12px sans-serif'
        })
          .bind('angle', '', (dataObj, textBlock) => {
            return textBlock.part.data.orientation === 'horizontal' ? 90 : 0;
          })
          .bind('text', 'key')
      );

    // Template for the whole IC, stores pins in vertical panels on sides of main shape
    function createIntegratedCircuitNodeTemplate() {
      return new go.Node('Auto', {
        locationSpot: new go.Spot(0.5, 0.5, 0, 5),
        toolTip: makeToolTip('Integrated Circuit'),
        cursor: 'pointer',
        contextMenu: ICContextMenu
      })
        .bind('angle', 'orientation', (v) => (v === 'horizontal' ? -90 : 0))
        .bindTwoWay('location', 'loc', go.Point.parse, go.Point.stringify)
        .add(
          new go.Panel('Horizontal').add(
            new go.Panel('Vertical', {
              itemTemplate: pinTemplate
            }).bind('itemArray', 'pinArrays', (v) => v[0]),
            new go.Panel('Spot').add(
              new go.Shape({
                stroke: 'black',
                fill: 'white',
                width: 120,
                click: (e, node) => highlightLinksFromIC(e, node),
                doubleClick: flipHV
              })
                // Sets height of the rectangle part based on the number of nodes on one side
                .bind('geometryString', 'pinArrays',
                  (v) =>
                    'F M 0 0 H 8 A 1 4 0 0 0 12 0 H 20 V' + // added F at beginning to fill shape
                    String(v[0].length * 30 + 16) + // height + margin per pin, plus fixed end height
                    'H 0 V 0'
                ),
              new go.Shape('Circle', {
                stroke: 'black',
                fill: 'transparent',
                width: 8,
                alignment: new go.Spot(0, 0, 12, 10)
              }),
              new go.TextBlock({
                alignment: go.Spot.Center,
                editable: true
              })
                .bindTwoWay('text', 'name')
                .bind('angle', 'orientation', (v) => (v === 'horizontal' ? 90 : 0))
            ),
            new go.Panel('Vertical', {
              itemTemplate: pinTemplate
            }).bind('itemArray', 'pinArrays', (v) => v[1])
          )
        );
    }

    // Creates separate template for palette that is scaled down
    const integratedCircuitNodeTemplate = createIntegratedCircuitNodeTemplate();
    const paletteICNodeTemplate = createIntegratedCircuitNodeTemplate();
    paletteICNodeTemplate.scale = 0.4;
    paletteICNodeTemplate.locationSpot = new go.Spot(0.2, 0.2);

    const commonPortProperties = {
      fill: 'transparent',
      stroke: 'transparent',
      desiredSize: new go.Size(10, 10),
      cursor: 'pointer',
      fromLinkable: true,
      toLinkable: true,
      mouseEnter: (e, port) => lightPort(e, port),
      mouseLeave: (e, port) => unlightPort(e, port)
    }

    // Creates circle ports
    function createCirclePort(align, linkSpot, id) {
      return new go.Shape('Circle', commonPortProperties)
        .set({
          fromSpot: linkSpot,
          toSpot: linkSpot,
          alignment: align,
          portId: id
        })
    }

    // Item array template for ports
    const circlePortTemplate = new go.Panel()
      .bind('alignment', 'align')
      .add(
        new go.Shape('Circle', commonPortProperties)
          .bind('fromSpot', 'linkSpot') // Required
          .bind('toSpot', 'linkSpot')
          .bind('portId', 'id') // Required
          .bind('toLinkableSelfNode', 'linksSelf')
          .bind('fromLinkableSelfNode', 'linksSelf')
          .bind('stroke')
          .bind('fill')
      );

    // Template for creating parts with a text block and a geometry string
    // Has partType property that automatically sets geometry, size, and ports
    // Bindings for additional optional parameters like text, tool tips, stroke, etc.
    const partNodeTemplate = new go.Node('Auto', {
      locationObjectName: 'MAIN',
      locationSpot: go.Spot.Center,
      toolTip: makeToolTip('Part'),
      click: (e, node) => highlightLinks(e, node),
      doubleClick: flipCircle,
      contextMenu: nodeContextMenu,
      cursor: 'pointer',
      background: 'transparent'
    })
      .bindTwoWay('location', 'loc', go.Point.parse, go.Point.stringify)
      .bind('toolTip', 'toolTip', (str) => makeToolTip(str))
      .bind('locationSpot', 'partType', (type) => !!partLocSpots[type]
        ? partLocSpots[type] : go.Spot.Center) // Adjusts spot on grid if needed
      .add(
        new go.Panel('Vertical')
          .bind('angle', 'orientation', (v) => getAngle(v))
          .add(
            new go.TextBlock({
              textAlign: 'center',
              editable: true,
              margin: 4
            })
              .bindTwoWay('text')
              .bind('visible', 'text', (text) => text ? true : false)
              .bind('angle', 'orientation', (v) => getTextAngle(v)),
            new go.Panel('Spot', {
              itemTemplate: circlePortTemplate
            })
              .bind('itemArray', 'partType', (type) => partPorts[type]) // Required
              .add(
                new go.Panel('Spot').add(
                  new go.Shape({
                    name: 'MAIN',
                    stroke: 'black',
                    fill: 'transparent',
                    strokeWidth: 2
                  })
                    .bind('geometryString', 'partType', (type) => partGeometries[type])
                    .bind('desiredSize', 'partType', (type) => !!partSizes[type] ? partSizes[type] : null)
                    .bind('strokeWidth')
                    .bind('fill'),
                  new go.Shape({
                    visible: false,
                    strokeWidth: 1
                  })
                    .bind('geometryString', 'partType', (type) => !!partGeometries2[type]
                      ? partGeometries2[type]: '')
                    .bind('alignment', 'partType', (type) => !!secondGeometryAlignments[type]
                      ? secondGeometryAlignments[type] : go.Spot.Center)
                    .bind('visible', 'partType', (type) => !!partGeometries2[type])
                  )
              )
          )
      );

    // Data for part geometry strings
    const partGeometries = {
      resistor: 'M 0 3 L 3 3 L 4 1 L 6 5 L 8 1 L 10 5 L 12 1 L 14 5 L 15 3 L 18 3',
      potentiometer: 'M 0 3 L 3 3 L 4 1 L 6 5 L 8 1 L 10 5 L 12 1 L 14 5 L 15 3 L 18 3 M 7 8 L 9 6 L 11 8 M 9 12 V 6',
      capacitor: 'M 0 2 L 2 2 M 2 0 L 2 4 M 4 2 L 6 2 M 4 0 L 4 4',
      capacitorPolarized: 'M 0 2 L 2 2 M 2 0 L 2 4 M 4 2 L 6 2 M 5 0 A 1 2 0 0 0 5 4',
      diodeStandard: 'F M 0 2 L 2 2 M 2 0 L 5 2 L 2 4 M 5 2 L 7 2 M 2 0 L 2 4 M 5 0 L 5 4',
      diodeZener: 'F M 0 2 L 2 2 M 2 0 L 5 2 L 2 4 M 5 2 L 7 2 M 2 0 L 2 4 z M 5 2 A 1 10 0 0 0 6 2 M 5 2 A 1 10 0 0 0 4 2',
      diodeSchottky: 'F M 0 2 L 2 2 M 2 0 L 5 2 L 2 4 M 5 2 L 7 2 M 2 0 L 2 4 z M 5 2 L 5 3 L 6 4 M 5 2 V 1 L 4 0',
      ground: 'M 4 0 L 4 4 M 0 4 H 8 M 1.5 6 H 7 M 2.7 8 H 5.6',
      pChannelMOSFET: 'X M 6 6 L 6 30 H 0 M 9 6 V 12 M 9 15 V 21 M 9 24 V 30 M 9 9 H 24 V 0 M 9 18 H 24 M 9 27 H 24 V 36 X F M 24 18 L 18 15 V 21 L 24 18',
      nChannelMOSFET: 'X M 6 6 L 6 30 H 0 M 9 6 V 12 M 9 15 V 21 M 9 24 V 30 M 9 9 H 24 V 0 M 9 18 H 24 M 9 27 H 24 V 36 X F M 9 18 L 15 15 V 21 L 9 18',
      bjtNPN: 'M 18 21 L 18 9 M 28.5 9 L 7.5 9 M 12 9 L 6 0 L 0 0 M 24 9 L 30 0 L 36 0 M 30 0 L 30 0 L 25.5 1.5 L 30 4.5 L 30 0 M 12 9',
      bjtPNP: 'M 12 14 L 12 6 M 19 6 L 5 6 M 8 6 L 4 0 L 0 0 M 16 6 L 20 0 L 24 0 M 20 0 L 20 0 L 20 0 M 8 6 L 8 3 L 5 5 L 8 6',
      speaker: 'F M 6 18 H 18 V 48 H 6 V 18 M 6 18 M 18 18 L 30 0 V 66 L 18 48 M 6 17.5 H 0 M 6 18.5 H 0 M 6 47.5 H 0 M 6 48.5 H 0',
      pushSwitch: 'F M 0 7.5 H 40 M 16 7.5 V 0 H 24 V 7.5 M 0 8.5 H 40'
    };
    // Spots for grid alignment
    const partLocSpots = {
      pushSwitch: new go.Spot(0.5, 1, 0, 8),
      potentiometer: new go.Spot(0.5, 0.5, 0, -7.65),
      pChannelMOSFET: new go.Spot(1, 0.5, -11, 2),
      nChannelMOSFET: new go.Spot(1, 0.5, -11, 2),
      bjtNPN: new go.Spot(0.5, 0.5, 0, -10.5),
      bjtPNP: new go.Spot(0.5, 0.5, 0, -10.5),
      speaker: new go.Spot(0.5, 0.5, 0, 5)
    }
    // Optional second shape geometries and alignments
    const partGeometries2 = {
      bjtNPN: 'M 21 3 A 1 1 0 0 0 21 33 A 1 1 0 0 0 21 3',
      bjtPNP: 'M 21 3 A 1 1 0 0 0 21 33 A 1 1 0 0 0 21 3'
    }
    const secondGeometryAlignments = {
      bjtNPN: new go.Spot(0.5, 0.5, 0, -10.5),
      bjtPNP: new go.Spot(0.5, 0.5, 0, -10.5)
    }
    // Data for desiredSize property of each part
    const partSizes = {
      resistor: new go.Size(36, 8),
      potentiometer: new go.Size(36, 24),
      capacitor: new go.Size(24, 16),
      capacitorPolarized: new go.Size(24, 16),
      diodeStandard: new go.Size(24, 16),
      diodeZener: new go.Size(24, 16),
      diodeSchottky: new go.Size(24, 16),
      ground: new go.Size(20, 20),
      pChannelMOSFET: new go.Size(24, 36),
      nChannelMOSFET: new go.Size(24, 36),
      bjtNPN: new go.Size(36, 21),
      bjtPNP: new go.Size(36, 21),
      speaker: new go.Size(30, 66),
      pushSwitch: new go.Size(40, 6.5)
    };
    // Data for item arrays of ports on each part
    const partPorts = {
      resistor: [
        { align: new go.Spot(0, 0.5, 3, 0), linkSpot: new go.Spot(0, 0.5, 5, 0), id: 'in' },
        { align: new go.Spot(1, 0.5, -3, 0), linkSpot: new go.Spot(1, 0.5, -5, 0), id: 'out' }
      ],
      capacitor: [
        { align: new go.Spot(0, 0.5, 3, 0), linkSpot: new go.Spot(0, 0.5, 5, 0), id: 'in' },
        { align: new go.Spot(1, 0.5, -3, 0), linkSpot: new go.Spot(1, 0.5, -5, 0), id: 'out' }
      ],
      capacitorPolarized: [
        { align: new go.Spot(0, 0.5, 3, 0), linkSpot: new go.Spot(0, 0.5, 5, 0), id: 'in' },
        { align: new go.Spot(1, 0.5, -3, 0), linkSpot: new go.Spot(1, 0.5, -5, 0), id: 'out' }
      ],
      diodeStandard: [
        { align: new go.Spot(0, 0.5, 3, 0), linkSpot: new go.Spot(0, 0.5, 5, 0), id: 'in' },
        { align: new go.Spot(1, 0.5, -3, 0), linkSpot: new go.Spot(1, 0.5, -5, 0), id: 'out' }
      ],
      diodeZener: [
        { align: new go.Spot(0, 0.5, 3, 0), linkSpot: new go.Spot(0, 0.5, 5, 0), id: 'in' },
        { align: new go.Spot(1, 0.5, -3, 0), linkSpot: new go.Spot(1, 0.5, -5, 0), id: 'out' }
      ],
      diodeSchottky: [
        { align: new go.Spot(0, 0.5, 3, 0), linkSpot: new go.Spot(0, 0.5, 5, 0), id: 'in' },
        { align: new go.Spot(1, 0.5, -3, 0), linkSpot: new go.Spot(1, 0.5, -5, 0), id: 'out' }
      ],
      ground: [
        { align: new go.Spot(0.5, 0, 0, 3), linkSpot: new go.Spot(0.5, 0, 0, 5), id: '' }
      ],
      pChannelMOSFET: [
        { align: new go.Spot(0, 0.8, 0, 0.5), linkSpot: new go.Spot(0, 0.5, 7, 0), id: 'gate' },
        { align: new go.Spot(1, 0, -1, 0), linkSpot: new go.Spot(0.5, 0, 0, 7), id: 'drain' },
        { align: new go.Spot(1, 1, -1, 0), linkSpot: new go.Spot(0.5, 1, 0, -7), id: 'source' }
      ],
      nChannelMOSFET: [
        { align: new go.Spot(0, 0.8, 0, 0.5), linkSpot: new go.Spot(0, 0.5, 7, 0), id: 'gate' },
        { align: new go.Spot(1, 0, -1, 0), linkSpot: new go.Spot(0.5, 0, 0, 7), id: 'drain' },
        { align: new go.Spot(1, 1, -1, 0), linkSpot: new go.Spot(0.5, 1, 0, -7), id: 'source' }
      ],
      speaker: [
        { align: new go.Spot(0, 0.5, 3, -15), linkSpot: new go.Spot(0, 0.5, 5, 0), id: 'in' },
        { align: new go.Spot(0, 0.5, 3, 15), linkSpot: new go.Spot(0, 0.5, 5, 0), id: 'out' }
      ],
      potentiometer: [
        { align: new go.Spot(0, 0.5, 3, -7.65), linkSpot: new go.Spot(0, 0.5, 5, 0), id: 'in', linksSelf: true },
        { align: new go.Spot(1, 0.5, -3, -7.65), linkSpot: new go.Spot(1, 0.5, -5, 0), id: 'out', linksSelf: true },
        { align: new go.Spot(.5, 1, 0, -3), linkSpot: new go.Spot(0.5, 1, 0, -5), id: 'wiper', linksSelf: true }
      ],
      bjtNPN: [
        { align: new go.Spot(0.5, 1, 0, -3), linkSpot: new go.Spot(0.5, 1, 0, -5), id: 'base' },
        { align: new go.Spot(0, 0.5, 3, -3.25), linkSpot: new go.Spot(0, 0.5, 5, 0), id: 'collector' },
        { align: new go.Spot(1, 0.5, -3, -3.25), linkSpot: new go.Spot(1, 0.5, -5, 0), id: 'emitter' }
      ],
      bjtPNP: [
        { align: new go.Spot(0.5, 1, 0, -3), linkSpot: new go.Spot(0.5, 1, 0, -5), id: 'base' },
        { align: new go.Spot(0, 0.5, 3, -3.25), linkSpot: new go.Spot(0, 0.5, 5, 0), id: 'collector' },
        { align: new go.Spot(1, 0.5, -3, -3.25), linkSpot: new go.Spot(1, 0.5, -5, 0), id: 'emitter' }
      ],
      pushSwitch: [
        { align: new go.Spot(0, 1, 5, 8), linkSpot: new go.Spot(0, 0.5, 1, 0), stroke: 'black', fill: 'white', id: 'in' },
        { align: new go.Spot(1, 1, -5, 8), linkSpot: new go.Spot(1, 0.5, -1, 0), stroke: 'black', fill: 'white', id: 'out' }
      ]
    };

    // Node for adding wire junctions
    const junctionNodeTemplate = new go.Node('Auto', {
      locationSpot: go.Spot.Center,
      toolTip: makeToolTip('Junction'),
      click: (e, node) => highlightLinks(e, node),
      cursor: 'pointer'
    })
      .bindTwoWay('location', 'loc', go.Point.parse, go.Point.stringify)
      .add(
        new go.Panel('Spot').add(
          // Transparent outer circle for easier selection and port location
          new go.Shape('Circle', {
            width: 40,
            fill: 'transparent',
            stroke: null
          }),
          // Visible inner circle
          new go.Shape('Circle', {
            fill: 'black',
            width: 10,
            alignment: go.Spot.Center
          }).bind('fill', 'isHollow', (v) => (v ? 'white' : 'black')),
          // Ports on all sides for easier routing
          createCirclePort(new go.Spot(0.5, 0, 0, 10), new go.Spot(0.5, 0, 0, 11), 'top'),
          createCirclePort(new go.Spot(0.5, 1, 0, -10), new go.Spot(0.5, 1, 0, -11), 'bottom'),
          createCirclePort(new go.Spot(0, 0.5, 10, 0), new go.Spot(0, 0.5, 11, 0), 'left'),
          createCirclePort(new go.Spot(1, 0.5, -10, 0), new go.Spot(1, 0.5, -11, 0), 'right')
        )
      );

    // Template for a simple switch, see Logic Circuit sample
    const switchNodeTemplate = new go.Node('Spot', {
      locationObjectName: 'MAIN',
      locationSpot: go.Spot.Center,
      toolTip: makeToolTip('Switch'),
      click: (e, node) => highlightLinks(e, node),
      doubleClick: openSwitch,
      contextMenu: switchContextMenu,
      cursor: 'pointer'
    })
      .bindTwoWay('location', 'loc', go.Point.parse, go.Point.stringify)
      .bind('angle', 'orientation', (v) => (v === 'horizontal' ? 0 : 90))
      .add(
        // Expands clickable region for opening switch
        new go.Shape({
          fill: 'transparent',
          stroke: null,
          width: 40,
          height: 40
        }),
        // Transparent shape in place of closed switch to anchor on grid when opened
        new go.Shape({
          name: 'MAIN',
          desiredSize: new go.Size(40, 40),
          stroke: null,
          fill: 'transparent',
          width: 40,
          height: 2,
          position: new go.Point(40, 0)
        }),
        new go.Panel('Horizontal', {
          // This prevents the ports from moving when the shape rotates
          minSize: new go.Size(42, 42)
        }).add(
          new go.Panel('Spot', {
            isClipping: true
          }).add(
            new go.Shape({ fill: 'blue', strokeWidth: 0 }),
            new go.Panel({
              alignment: go.Spot.Left,
              alignmentFocus: go.Spot.Center,
              angle: 359.99 // Rotate counter clock wise
            })
              .add(
                new go.Shape({ width: 1, height: 1 }),
                new go.Shape({
                  desiredSize: new go.Size(40, 40),
                  strokeWidth: 0,
                  fill: 'black',
                  width: 40,
                  height: 2,
                  position: new go.Point(40, 0)
                })
              )
              .bind('angle', 'isOpen', (isOpen) => (!isOpen ? 359.99 : 359.99 - 30))
          )
        ),
        // Ports
        createCirclePort(new go.Spot(1, 0.5), new go.Spot(1, 0.5, -1, 0), 'out')
          .set({ fill: 'white', stroke: 'black' }),
        createCirclePort(new go.Spot(0, 0.5), new go.Spot(0, 0.5, 1, 0), 'in')
          .set({ fill: 'white', stroke: 'black' })
      );

    // Node for simple user created labels
    const labelNodeTemplate = new go.Node('Auto', {
      locationSpot: go.Spot.Center,
      toolTip: makeToolTip('Label'),
      click: (e, node) => highlightLinks(e, node),
      cursor: 'pointer'
    })
      .bindTwoWay('location', 'loc', go.Point.parse, go.Point.stringify)
      .add(
        new go.TextBlock('Label', {
          editable: true,
          textAlign: 'center',
          verticalAlignment: go.Spot.Center,
          alignment: go.Spot.Center,
          minSize: new go.Size(40, 30),
        }).bindTwoWay('text')
      );

    // Node for a voltage divider chip
    const voltDivNodeTemplate = new go.Node('Spot', {
      locationObjectName: 'MAIN',
      locationSpot: go.Spot.Center,
      toolTip: makeToolTip('Voltage Divider'),
      click: (e, node) => highlightLinks(e, node),
      contextMenu: nodeContextMenu,
      cursor: 'pointer'
    })
      .bindTwoWay('location', 'loc', go.Point.parse, go.Point.stringify)
      .bind('angle', 'orientation', (v) => getAngle(v))
      .add(
        new go.Shape('Rectangle', {
          fill: 'transparent',
          stroke: 'black',
          desiredSize: new go.Size(40, 30),
          doubleClick: flipCircle
        }),
        new go.TextBlock({
          alignment: go.Spot.Center,
          editable: true
        })
          .bindTwoWay('text', 'name')
          .bind('angle', 'orientation', (v) => getTextAngle(v)),
        // Anchors node on grid
        new go.Shape('Rectangle', {
          name: 'MAIN',
          fill: null,
          stroke: null,
          desiredSize: new go.Size(46, 2),
          alignment: go.Spot.Center
        }),
        // Ports, rectangles are visible, transparent circles are the actual ports
        new go.Shape('Rectangle', {
          fill: 'black',
          stroke: null,
          desiredSize: new go.Size(6, 2),
          alignment: new go.Spot(0, 0.5, -3, 0)
        }),
        createCirclePort(new go.Spot(0, 0.5, -3, 0), new go.Spot(0, 0.5, 5, 0), 'in'),
        new go.Shape('Rectangle', {
          fill: 'black',
          stroke: null,
          desiredSize: new go.Size(6, 2),
          alignment: new go.Spot(1, 0.5, 3, 0)
        }),
        createCirclePort(new go.Spot(1, 0.5, 3, 0), new go.Spot(1, 0.5, -5, 0), 'out'),
        new go.Shape('Rectangle', {
          fill: 'black',
          stroke: null,
          desiredSize: new go.Size(2, 6),
          alignment: new go.Spot(0.5, 1, 0, 3)
        }),
        createCirclePort(new go.Spot(0.5, 1, 0, 3), new go.Spot(0.5, 1, 0, -5), 'ground')
      );

    // Adds node templates to a template map for the diagram and palette
    const nodeTemplateMap = new go.Map();
    nodeTemplateMap.add('IC', integratedCircuitNodeTemplate);
    nodeTemplateMap.add('junction', junctionNodeTemplate);
    nodeTemplateMap.add('switch', switchNodeTemplate);
    nodeTemplateMap.add('label', labelNodeTemplate);
    nodeTemplateMap.add('voltDiv', voltDivNodeTemplate);
    // General part template
    nodeTemplateMap.add('part', partNodeTemplate);
    myDiagram.nodeTemplateMap = nodeTemplateMap;
    myPalette.nodeTemplateMap = nodeTemplateMap.copy();
    // Changes to the scaled down/centered versions
    myPalette.nodeTemplateMap.delete('IC');
    myPalette.nodeTemplateMap.add('IC', paletteICNodeTemplate);

    // Link template for drawing wires
    myDiagram.linkTemplate = new go.Link({
      reshapable: true,
      resegmentable: true,
      routing: go.Routing.Orthogonal,
      adjusting: go.LinkAdjusting.End,
      corner: 3,
      relinkableFrom: true,
      relinkableTo: true,
      selectionAdorned: false,
      cursor: 'pointer'
    })
      .bindTwoWay('points')
      .add(
        new go.Shape({ name: 'SHAPE', strokeWidth: 2, stroke: 'black' })
          // Color for link highlighting
          .bindObject('stroke', 'isHighlighted', (h) => (h ? '#42b6f5AA' : 'black'))
          .bindObject('strokeWidth', 'isHighlighted', (h) => (h ? 4 : 2))
      );

    // Template for created groups
    myDiagram.groupTemplate = new go.Group('Auto', {
      ungroupable: true,
      contextMenu: groupContextMenu,
      dragComputation: groupDragComputation
    })
      .add(
        new go.Panel('Vertical').add(
          new go.TextBlock({
            font: '14px sans-serif',
            editable: true,
            margin: 4
          }).bindTwoWay('text'),
          new go.Panel('Auto').add(
            new go.Shape('Rectangle', {
              selectable: false, // Allows nodes and links in a group to still be selected
              fill: '#029ffa10',
              stroke: 'black',
              strokeWidth: 0.5,
              strokeDashArray: [10, 5],
              doubleClick: changeColor
            }).bind('fill', 'color'),
            new go.Placeholder({ padding: 5 })
          )
        )
      );

    // Customizes selection adornment
    myDiagram.nodeSelectionAdornmentTemplate = new go.Adornment('Spot')
      .add(
        new go.Panel('Auto').add(
          new go.Shape('RoundedRectangle', {
            fill: null,
            stroke: '#42b6f5AA',
            strokeWidth: 3,
            strokeDashArray: [6, 8]
          }),
          new go.Placeholder({ padding: 3 }),
        )
      )
    myDiagram.groupSelectionAdornmentTemplate = myDiagram.nodeSelectionAdornmentTemplate;
    myPalette.nodeSelectionAdornmentTemplate = myDiagram.groupSelectionAdornmentTemplate;

    // Starting elements in the palette
    myPalette.model.nodeDataArray = [
      { key: 'label', category: 'label', text: 'Label' },
      { key: 'junction', category: 'junction' },
      { key: 'openPoint', category: 'junction', isHollow: true },
      { key: 'ground', category: 'part', partType: 'ground', toolTip: 'Ground', text: null, orientation: 'horizontal' },
      { key: 'switch', category: 'switch', isOpen: false, orientation: 'horizontal' },
      { key: 'pushSwitch', category: 'part', partType: 'pushSwitch', toolTip: 'Push Switch', text: null, strokeWidth: 1, orientation: 'horizontal' },
      { key: 'resistor', category: 'part', partType: 'resistor', toolTip: 'Resistor', text: '4.7 k', orientation: 'horizontal'},
      { key: 'potentiometer', category: 'part', partType: 'potentiometer', toolTip: 'Potentiometer', text: '1M', orientation: 'horizontal' },
      { key: 'capacitor', category: 'part', partType: 'capacitor', toolTip: 'Capacitor', text: '47 μF', orientation: 'horizontal' },
      { key: 'polarizedCapacitor', category: 'part', partType: 'capacitorPolarized', toolTip: 'Polarized Capacitor', text: '100 μF', orientation: 'horizontal' },
      { key: 'standardDiode', category: 'part', partType: 'diodeStandard', toolTip: 'Standard Diode', text: '1N4001', fill: 'black', orientation: 'horizontal' },
      { key: 'zenerDiode', category: 'part', partType: 'diodeZener', toolTip: 'Zener Diode', text: '1N4001', fill: 'black', orientation: 'horizontal' },
      { key: 'schottkyDiode', category: 'part', partType: 'diodeSchottky', toolTip: 'Schottky Diode', text: '1N4001', fill: 'black', orientation: 'horizontal' },
      { key: 'P-ChannelMOSFET', category: 'part', partType: 'pChannelMOSFET', toolTip: 'P-Channel MOSFET', text: null, fill: 'black', orientation: 'horizontal' },
      { key: 'N-ChannelMOSFET', category: 'part', partType: 'nChannelMOSFET', toolTip: 'N-Channel MOSFET', text: null, fill: 'black', orientation: 'horizontal' },
      { key: 'voltageDivider', category: 'voltDiv', name: '7805', orientation: 'horizontal' },
      { key: 'bjtNPN', category: 'part', partType: 'bjtNPN', toolTip: 'NPN Bipolar Junction Transistor', text: '2N3904', orientation: 'horizontal' },
      { key: 'bjtPNP', category: 'part', partType: 'bjtPNP', toolTip: 'PNP Bipolar Junction Transistor', text: '2N3906', orientation: 'horizontal' },
      { key: 'speaker', category: 'part', partType: 'speaker', toolTip: 'Speaker', text: null, strokeWidth: 1, orientation: 'horizontal'}
    ];

    // Add primary IC's to palette
    addICtoPalette(8, '555');
    addICtoPalette(28, 'SN76477');

    // Starting diagram
    myDiagram.model = new go.GraphLinksModel({
      linkFromPortIdProperty: 'fromPort',
      linkToPortIdProperty: 'toPort'
    });

    load();
  } // end init

  // Keeps track of number of ICs in the panel
  var numICinPalette = 0;

  function addICtoPalette(num, ICname) {
    leftPinArr = [];
    rightPinArr = [];

    for (let i = 0; i < num / 2; i++) {
      leftPinArr.push({ key: i + 1, side: 'left' });
      rightPinArr.push({ key: num - i, side: 'right' });
    }

    numICinPalette++;

    myPalette.model.addNodeData({
      key: 'IC' + String(numICinPalette),
      category: 'IC',
      name: ICname,
      pinArrays: [leftPinArr, rightPinArr],
      orientation: 'vertical'
    });
  }

  // Handles button input and adding IC's
  function addICHandler() {
    let num = Number(document.getElementById('pinNumInput').value);
    let name = document.getElementById('ICNameInput').value;
    if (Number.isFinite(num) && num > 3 && num < 99 && num % 2 === 0) {
      addICtoPalette(num, name);
      document.getElementById('pinNumInput').value = '';
      document.getElementById('ICNameInput').value = '';
    } else {
      alert('Enter an even number of pins between 4 and 98');
    }
  }

  // Swaps the pins on an IC from one side to the other
  function swapPins(e, obj) {
    if (e.diagram instanceof go.Palette) return;
    e.diagram.commit(() => {
      while (obj.part && obj.part !== obj) obj = obj.part;
      const newArrays = [obj.data.pinArrays[1], obj.data.pinArrays[0]];
      myDiagram.model.set(obj.data, 'pinArrays', newArrays)
    })
  }

  // Flips node orientation back and forth between horizontal and vertical on double click
  function flipHV(e, obj) {
    if (e.diagram instanceof go.Palette) return;
    e.diagram.commit(() => {
      while (obj.part && obj.part !== obj) obj = obj.part; // Moves up the tree until it finds the node
      const flip = obj.data.orientation === 'horizontal' ? 'vertical' : 'horizontal';
      myDiagram.model.set(obj.data, 'orientation', flip);
    })
  }

  // Rotates node 90 degrees on double click
  const orientationArray = ['horizontal', 'vertical', 'horizontalFlipped', 'verticalFlipped'];
  function flipCircle(e, obj) {
    if (e.diagram instanceof go.Palette) return;
    e.diagram.commit(() => {
      while (obj.part && obj.part !== obj) obj = obj.part;
      let index = orientationArray.indexOf(obj.data.orientation);
      let next = orientationArray[(index+1) % 4];
      myDiagram.model.set(obj.data, 'orientation', next);
    });
  }

  // Returns the angle for a given string orientation
  const angleMap = {
    'horizontal': 0,
    'vertical': 90,
    'horizontalFlipped': 180,
    'verticalFlipped': 270
  };
  function getAngle(orientation) {
    return angleMap[orientation];
  }

  // Gets angles to keep text blocks upright
  const textAngleMap = {
    'horizontal': 0,
    'vertical': -90,
    'horizontalFlipped': 180,
    'verticalFlipped': 90
  };
  function getTextAngle(orientation) {
    return textAngleMap[orientation];
  }

  // Opens switch
  function openSwitch(e, obj) {
    if (e.diagram instanceof go.Palette) return;
    e.diagram.commit(() => {
      while (obj.part && obj.part !== obj) obj = obj.part;
      const isOpen = !obj.data.isOpen;
      myDiagram.model.setDataProperty(obj.data, 'isOpen', isOpen);
    })
  }

  // Finds links connected to the selected node and highlights them
  function highlightLinks(e, node, pid) {
    myDiagram.commit(() => {
      myDiagram.clearHighlighteds();
      if (pid) { // Case for selection of a single IC pin
        node.findLinksConnected(pid).each((l) => (l.isHighlighted = true));
      } else {
        node.findLinksConnected().each((l) => (l.isHighlighted = true));
      }
    }, null)
  }

  // Finds links out of a single pin
  function highlightLinksFromPin(e, pin) {
    const pid = pin.portId; // Saves portId
    while (pin.part && pin.part !== pin) pin = pin.part; // Finds parent node
    highlightLinks(e, pin, pid);
  }

  // Finds links out of the whole IC
  function highlightLinksFromIC(e, node) {
    while (node.part && node.part !== node) node = node.part; // Finds parent node
    highlightLinks(e, node);
  }

  // Global flag for checking if unused ports are to be highlighted
  var markingPorts = false;

  // Loops through nodes in the diagram and highlights ports that have no links connected to them
  function unusedPorts() {
    myDiagram.nodes.each((node) => {
      if (node.category !== 'junction') { // Ignores junctions
        markNode(node);
      }
    });
    document.getElementById('markPortsButton').style.display = 'none';
    document.getElementById('unmarkPortsButton').style.display = '';
    markingPorts = true;
  }

  function markNode(node) {
    if (node.category === 'junction') return;
    node.ports.each((port) => markPort(node, port));
  }

  function markPort(node, port) {
    // The second argument to commit() is null to skip the undo manager
    myDiagram.commit(() => {
      const isIC = node.category === 'IC';
      const isSwitch = node.category === 'switch' || node.data.partType === 'pushSwitch';
      const shape = isIC ? port.findObject('SHAPE') : port;

      if (node.findLinksConnected(port.portId).count === 0 && node.category !== 'junction') {
        shape.stroke = 'red';
      } else {
        shape.stroke = isIC || isSwitch ? 'black' : 'transparent';
      }
      shape.fill = isIC || isSwitch ? 'white' : 'transparent'; // For un-lighting
    }, null)
  }

  // Does the opposite of above
  function unmarkPorts() {
    myDiagram.nodes.each((node) => {
      if (node.category !== 'junction') {
        unmarkNode(node);
      }
    });
    document.getElementById('markPortsButton').style.display = '';
    document.getElementById('unmarkPortsButton').style.display = 'none';
    markingPorts = false;
  }

  function unmarkNode(node) {
    node.ports.each((port) => unmarkPort(node, port));
  }

  function unmarkPort(node, port) {
    myDiagram.commit(() => {
      const shape = node.category === 'IC' ? port.findObject('SHAPE') : port;
      const isICorSwitch = node.category === 'IC' ||
        node.category === 'switch' || node.data.partType === 'pushSwitch';

      if (isICorSwitch) {
        shape.stroke = 'black';
        shape.fill = 'white'; // for un-lighting
      } else {
        shape.stroke = 'transparent';
        shape.fill = 'transparent';
      }
    }, null)
  }

  // Highlights ports when hovered over
  function lightPort(e, port) {
    if (port.diagram instanceof go.Palette) return; // Doesn't light palette ports
    e.diagram.commit(() => {
      if (port instanceof go.Panel) { // Finds port through panel if needed pin
        port.findObject('SHAPE').stroke = '#1E90FF';
        port.findObject('SHAPE').fill = '#1E90FF20';
      } else {
        port.stroke = '#1E90FF';
        port.fill = '#1E90FF20';
      }
    }, null)
  }

  function unlightPort(e, port) {
    if (port.diagram instanceof go.Palette) return;
      let node = port;
      while (node.part && node.part !== node) node = node.part;
      if (markingPorts) {
        markPort(node, port);
      } else {
        unmarkPort(node, port);
      }
  }

  // Changes color of groups
  const colorArray = ['#029ffa10', '#02fa7610', '#f2fa0210', '#fa1f0210', '#ffffff10'];
  function changeColor(e, node) {
    while (node.part && node.part !== node) node = node.part;
    let index = colorArray.indexOf(node.data.color);
    let color = colorArray[(index+1) % colorArray.length];
    e.diagram.commit(() => {myDiagram.model.set(node.data, 'color', color)});
  }

  // Accounts for groups not locking to the grid when created
  function groupDragComputation(part, pt, gridpt) {
    const loc = part.location;
    const bounds = part.naturalBounds;
    const offx = loc.x % CELLSIZE;
    const offy = loc.y % CELLSIZE;
    return new go.Point(gridpt.x + offx, gridpt.y + offy);
  }

  // Save a model to and load a model from JSON text, displayed below the Diagram
  function save() {
    document.getElementById('mySavedModel').value = myDiagram.model.toJson();
    myDiagram.isModified = false;
  }
  function load() {
    myDiagram.model = go.Model.fromJson(document.getElementById('mySavedModel').value);
  }

  document.addEventListener('DOMContentLoaded', init);
</script>


  <div id="sample">
    <div class="sampleWrapper">
      <div style="width: 100%; height: fit-content; display: flex; flex: 2">
        <div
          id="myPaletteDiv"
          style="border: solid 1px black; width: 300px; height: 700px"></div>
        <div
          id="myDiagramDiv"
          style="border: solid 1px black; width: 1400px; height: 700px"></div>
      </div>
      <div
        id="menuDiv"
        style="display: flex; justify-content: space-between; margin: 10px 0px">
        <div style="display: flex; flex-direction: column; gap: 4px; border: solid 1px black; padding: .5rem; border-radius: .25rem;">
          <div style="display: flex; gap: 8px; margin-top: 4px">
            <label for="pinNumInput" style="text-wrap: nowrap">Number of Pins: </label>
            <input
              id="pinNumInput"
              type="number"
              min="4"
              max="98"
              step="2"
              style="text-align: center; width: 100%" />
          </div>
          <div style="display: flex; gap: 8px; margin-bottom: 4px">
            <label for="ICNameInput" style="text-wrap: nowrap">Model Name: </label>
            <input id="ICNameInput" style="text-align: center; width: 100%" />
          </div>
          <button onclick="addICHandler()" style="align-self: self-end;">Add Integrated Circuit</button>
        </div>
        <div style="display: flex">
          <button onclick="myDiagram.commandHandler.groupSelection()" style="height: fit-content;">
            Group Selected
          </button>
          <button onclick="myDiagram.commandHandler.ungroupSelection()" style="height: fit-content;">
            Ungroup Selected
          </button>
          <button id="markPortsButton" onclick="unusedPorts()" style="height: fit-content;">Mark Unused Ports</button>
          <button id="unmarkPortsButton" onclick="unmarkPorts()" style="display: none; height: fit-content;">Unmark Unused Ports</button>
        </div>
      </div>
      <div id="description">
        <p>
          This GoJS diagram shows an interactive circuit schematic designer. The sample shown
          is an application circuit of a steam train sound with a whistle using the
          Texas Instruments SN76477 sound chip (see
          <a href="https://en.wikipedia.org/wiki/Texas_Instruments_SN76477">Wikipedia</a>
          for more information). The chip was used primarily for arcade game sound
          effects, and it is shown here used with a 555 Timer and a speaker to
          create a steam train sound effect for circuit production.
        </p>
        <p>
          This sample allows users to construct descriptive circuit schematics using integrated
          circuits with between 4 and 98 pins and a variety of standard circuit components
          of various types. Users can add customized integrated circuits with the menu
          below the diagram, group components using <a>Group</a> (double click to change colors),
          and mark ports that are unused. Users can also edit the text on the components to change
          their names or values. Developing new parts uses a reusable <a>Diagram.nodeTemplate</a>
          with text and one, or optionally two, geometries that takes data from a partType binding
          in the model data and constructs a part node. It has several optional bindings for
          further customization as well.
        </p>
        <p>
          A <a>Palette</a> to the left of the diagram allows the user to drag and drop new
          circuit component nodes. Click and drag between the ports on each node to draw new
          links, and double click the nodes to rotate them (or right click them to use the
          <a>GraphObject.contextMenu</a>). The switch can also be opened by right clicking it.
          Selecting any of the nodes or individual pins on the integrated circuits
          will highlight links going into and out of that node's ports with the
          <a>Node.findLinksConnected</a> method.
        </p>
        <p>
          The wire paths are created manually by either dragging between ports or clicking along
          the desired path with the
          <a href="../extensions/PolylineLinkingTool.js">PolylineLinkingTool.js</a> extension.
          They can be reshaped and adjusted with <a>LinkReshapingTool</a> and the
          <a href="../extensions/OrthogonalLinkReshapingTool.js">OrthogonalLinkReshapingTool.js</a>
          extension. The paths are saved in the JSON model by saving the <a>Link.points</a>
          along each wire. Hover over any component to see a description with
          <a>GraphObject.toolTip</a>.
        </p>
      </div>
      <div id="JSONsection">
        <button onclick="save()">Save</button>
        <button onclick="load()">Load</button>
        Diagram model saved in JSON format:
      </div>
      <textarea id="mySavedModel" style="width: 100%; height: 200px">
{ "class": "GraphLinksModel",
  "linkFromPortIdProperty": "fromPort",
  "linkToPortIdProperty": "toPort",
  "nodeDataArray": [
{"key":-1,"category":"IC","name":"555","pinArrays":[[{"key":1,"side":"left"},{"key":2,"side":"left"},{"key":3,"side":"left"},{"key":4,"side":"left"}],[{"key":8,"side":"right"},{"key":7,"side":"right"},{"key":6,"side":"right"},{"key":5,"side":"right"}]],"orientation":"vertical","loc":"-294.5 -110","group":-77},
{"key":-2,"category":"IC","name":"SN76477","pinArrays":[[{"key":1,"side":"left"},{"key":2,"side":"left"},{"key":3,"side":"left"},{"key":4,"side":"left"},{"key":5,"side":"left"},{"key":6,"side":"left"},{"key":7,"side":"left"},{"key":8,"side":"left"},{"key":9,"side":"left"},{"key":10,"side":"left"},{"key":11,"side":"left"},{"key":12,"side":"left"},{"key":13,"side":"left"},{"key":14,"side":"left"}],[{"key":28,"side":"right"},{"key":27,"side":"right"},{"key":26,"side":"right"},{"key":25,"side":"right"},{"key":24,"side":"right"},{"key":23,"side":"right"},{"key":22,"side":"right"},{"key":21,"side":"right"},{"key":20,"side":"right"},{"key":19,"side":"right"},{"key":18,"side":"right"},{"key":17,"side":"right"},{"key":16,"side":"right"},{"key":15,"side":"right"}]],"orientation":"horizontal","loc":"90 -120"},
{"key":-3,"category":"part","partType":"resistor","toolTip":"Resistor","text":"100 k","orientation":"vertical","loc":"-514.5 -240","group":-77},
{"key":-4,"category":"junction","loc":"-514.5 -200","group":-77},
{"key":-5,"category":"part","partType":"resistor","toolTip":"Resistor","text":"100 k","orientation":"vertical","loc":"-514.5 -160","group":-77},
{"key":-6,"category":"junction","loc":"-514.5 -100","group":-77},
{"key":-7,"category":"part","partType":"capacitorPolarized","toolTip":"Polarized Capacitor","text":"220 pF","orientation":"vertical","loc":"-514.5 -50","group":-77},
{"key":-8,"category":"junction","loc":"-514.5 0","group":-77},
{"key":-9,"category":"junction","loc":"-424.5 -30","group":-77},
{"key":-10,"category":"part","partType":"pushSwitch","toolTip":"Push Switch","text":null,"strokeWidth":1,"orientation":"horizontal","loc":"-234.5 -290","group":-77},
{"key":-11,"category":"junction","loc":"-414.5 -270","group":-77},
{"key":-12,"category":"junction","loc":"-184.5 -250","group":-77},
{"key":-13,"category":"junction","loc":"-110 -250"},
{"key":-14,"category":"junction","loc":"-80 -290"},
{"key":-15,"category":"part","partType":"resistor","toolTip":"Resistor","text":"5 k","orientation":"verticalFlipped","loc":"-80 -320"},
{"key":-16,"category":"junction","loc":"-50 -360"},
{"key":-17,"category":"junction","loc":"-20 -360"},
{"key":-18,"category":"part","partType":"capacitorPolarized","toolTip":"Polarized Capacitor","text":".1 \u03bcF","orientation":"verticalFlipped","loc":"100 -320"},
{"key":-19,"category":"junction","loc":"100 -360"},
{"key":-20,"category":"junction","loc":"150 -360"},
{"key":-21,"category":"part","partType":"potentiometer","toolTip":"Potentiometer","text":"1M","orientation":"verticalFlipped","loc":"150 -320"},
{"key":-22,"category":"part","partType":"resistor","toolTip":"Resistor","text":"100 k","orientation":"vertical","loc":"200 -320"},
{"key":-23,"category":"junction","loc":"160 -250"},
{"key":-24,"category":"part","partType":"resistor","toolTip":"Resistor","text":"47 k","orientation":"vertical","loc":"270 -320"},
{"key":-25,"category":"junction","loc":"270 -360"},
{"key":-26,"category":"part","partType":"capacitorPolarized","toolTip":"Polarized Capacitor","text":".1 \u03bcF","orientation":"verticalFlipped","loc":"370 -320"},
{"key":-27,"category":"junction","loc":"370 -360"},
{"key":-28,"category":"junction","loc":"310 -250"},
{"key":-29,"category":"label","text":"5 V","loc":"330 -260"},
{"key":-30,"category":"junction","loc":"570 -320"},
{"key":-31,"category":"part","partType":"resistor","toolTip":"Resistor","text":"77 k","orientation":"horizontal","loc":"510 -320"},
{"key":-32,"category":"part","partType":"resistor","toolTip":"Resistor","text":"68 k","orientation":"horizontal","loc":"510 -280"},
{"key":-33,"category":"junction","loc":"450 -280"},
{"key":-35,"category":"junction","loc":"450 -250"},
{"key":-36,"category":"voltDiv","name":"7805","orientation":"verticalFlipped","loc":"450 -150"},
{"key":-37,"category":"junction","loc":"570 -150"},
{"key":-38,"category":"junction","loc":"450 0"},
{"key":-39,"category":"label","text":"9 V","loc":"430 -10"},
{"key":-40,"category":"part","partType":"bjtNPN","toolTip":"NPN Bipolar Junction Transistor","text":"2N3904","orientation":"vertical","loc":"450 60"},
{"key":-41,"category":"junction","loc":"390 60"},
{"key":-42,"category":"part","partType":"diodeStandard","toolTip":"Standard Diode","text":"1N4148","fill":"black","orientation":"vertical","loc":"390 120"},
{"key":-43,"category":"part","partType":"diodeStandard","toolTip":"Standard Diode","text":"1N4148","fill":"black","orientation":"vertical","loc":"390 200"},
{"key":-44,"category":"part","partType":"resistor","toolTip":"Resistor","text":"1.5\nOhm","orientation":"vertical","loc":"510 120"},
{"key":-45,"category":"part","partType":"resistor","toolTip":"Resistor","text":"1.5\nOhm","orientation":"vertical","loc":"510 200"},
{"key":-46,"category":"junction","loc":"510 160"},
{"key":-47,"category":"part","partType":"bjtPNP","toolTip":"PNP Bipolar Junction Transistor","text":"2N3906","orientation":"vertical","loc":"450 260"},
{"key":-48,"category":"junction","loc":"390 260"},
{"key":-49,"category":"junction","loc":"450 340"},
{"key":-50,"category":"part","partType":"resistor","toolTip":"Resistor","text":"10 k","orientation":"vertical","loc":"330 290"},
{"key":-51,"category":"junction","loc":"330 340"},
{"key":-52,"category":"part","partType":"resistor","toolTip":"Resistor","text":"330 k","orientation":"vertical","loc":"10 290"},
{"key":-53,"category":"part","partType":"resistor","toolTip":"Resistor","text":"47 k","orientation":"verticalFlipped","loc":"-20 290"},
{"key":-54,"category":"junction","loc":"10 340"},
{"key":-55,"category":"junction","loc":"-20 340"},
{"key":-56,"category":"junction","loc":"-110 20"},
{"key":-57,"category":"junction","loc":"-80 20"},
{"key":-58,"category":"junction","loc":"-140 20"},
{"key":-59,"category":"part","partType":"capacitorPolarized","toolTip":"Polarized Capacitor","text":"100 \u03bcF","orientation":"vertical","loc":"90 290"},
{"key":-60,"category":"junction","loc":"90 340"},
{"key":-61,"category":"junction","loc":"230 340"},
{"key":-62,"category":"part","partType":"resistor","toolTip":"Resistor","text":"100 k","orientation":"vertical","loc":"260 290"},
{"key":-63,"category":"part","partType":"resistor","toolTip":"Resistor","text":"100 k","orientation":"verticalFlipped","loc":"230 290"},
{"key":-64,"category":"junction","loc":"260 340"},
{"key":-65,"category":"part","partType":"resistor","toolTip":"Resistor","text":"47 k","orientation":"vertical","loc":"320 90"},
{"key":-66,"category":"junction","loc":"170 340"},
{"key":-67,"category":"junction","isHollow":true,"loc":"680 0.13522086576601566","group":-76},
{"key":-68,"category":"junction","isHollow":true,"loc":"680 340.1352208657661","group":-76},
{"key":-69,"category":"label","text":"Plus\n9 V\nInput","loc":"710 0.13522086576601566","group":-76},
{"key":-70,"category":"label","text":"Minus","loc":"710 340.1352208657661","group":-76},
{"key":-71,"category":"part","partType":"speaker","toolTip":"Speaker","text":null,"strokeWidth":1,"orientation":"horizontal","loc":"690 180.135220865766","group":-76},
{"key":-72,"category":"junction","loc":"570 340"},
{"key":-73,"category":"junction","loc":"640 340"},
{"key":-74,"category":"label","text":"Speaker","loc":"700 120.13522086576603","group":-76},
{"key":-75,"category":"label","text":"Whistle","loc":"-284.5 -310","group":-77},
{"text":"555 Timer Section","color":"#029ffa10","isGroup":true,"key":-77},
{"text":"Sound/Input","color":"#02fa7610","isGroup":true,"key":-76}
],
  "linkDataArray": [
{"from":-4,"to":-3,"fromPort":"top","toPort":"out","points":[-514.5,-205,-514.5,-215,-514.5,-139.25,-514.5,-139.25,-514.5,-213.5,-514.5,-223.5]},
{"from":-4,"to":-5,"fromPort":"bottom","toPort":"in","points":[-514.5,-195,-514.5,-185,-514.5,-75.75,-514.5,-75.75,-514.5,-186.5,-514.5,-176.5]},
{"from":-5,"to":-6,"fromPort":"out","toPort":"top","points":[-514.5,-143.5,-514.5,-133.5,-514.5,-104.25,-514.5,-104.25,-514.5,-115,-514.5,-105]},
{"from":-6,"to":-7,"fromPort":"bottom","toPort":"in","points":[-514.5,-95,-514.5,-85,-514.5,-77.75,-514.5,-77.75,-514.5,-70.5,-514.5,-60.5]},
{"from":-7,"to":-8,"fromPort":"out","toPort":"top","points":[-514.5,-39.5,-514.5,-29.5,-514.5,-27.25,-514.5,-27.25,-514.5,-15,-514.5,-5]},
{"from":-8,"to":-1,"fromPort":"right","toPort":"1","points":[-509.5,0,-499.5,0,-453.80859375,0,-453.80859375,-160,-391,-160,-381,-160]},
{"from":-9,"to":-6,"fromPort":"left","toPort":"right","points":[-429.5,-30,-439.5,-30,-437.5546875,-30,-437.5546875,-100,-499.5,-100,-509.5,-100]},
{"from":-9,"to":-1,"fromPort":"top","toPort":"2","points":[-424.5,-35,-424.5,-45,-424.5,-130,-391,-130,-391,-130,-381,-130]},
{"from":-11,"to":-1,"fromPort":"bottom","toPort":"4","points":[-414.5,-265,-414.5,-255,-414.5,-70,-391,-70,-391,-70,-381,-70]},
{"from":-1,"to":-10,"fromPort":"3","toPort":"in","points":[-381,-100,-391,-100,-391,-226.6484375,-264.5,-226.6484375,-264.5,-290,-254.5,-290]},
{"from":-1,"to":-12,"fromPort":"8","toPort":"bottom","points":[-208,-160,-198,-160,-184.5,-160,-184.5,-235,-184.5,-235,-184.5,-245]},
{"from":-11,"to":-12,"fromPort":"right","toPort":"left","points":[-409.5,-270,-399.5,-270,-299.5,-270,-299.5,-250,-199.5,-250,-189.5,-250]},
{"from":-2,"to":-13,"fromPort":"28","toPort":"bottom","points":[-110,-206.5,-110,-216.5,-110,-225.75,-110,-225.75,-110,-235,-110,-245]},
{"from":-10,"to":-14,"fromPort":"out","toPort":"left","points":[-214.5,-290,-204.5,-290,-142.5,-290,-142.5,-290,-95,-290,-85,-290]},
{"from":-14,"to":-2,"fromPort":"bottom","toPort":"27","points":[-80,-285,-80,-275,-80,-240.75,-80,-240.75,-80,-216.5,-80,-206.5]},
{"from":-2,"to":-16,"fromPort":"26","toPort":"bottom","points":[-50,-206.5,-50,-216.5,-50,-275.75,-50,-275.75,-50,-345,-50,-355]},
{"from":-2,"to":-17,"fromPort":"25","toPort":"bottom","points":[-20,-206.5,-20,-216.5,-20,-275.75,-20,-275.75,-20,-345,-20,-355]},
{"from":-14,"to":-15,"fromPort":"top","toPort":"in","points":[-80,-295,-80,-305,-80,-305,-80,-293.5,-80,-293.5,-80,-303.5]},
{"from":-16,"to":-17,"fromPort":"right","toPort":"left","points":[-45,-360,-35,-360,-35,-360,-35,-360,-35,-360,-25,-360]},
{"from":-2,"to":-18,"fromPort":"21","toPort":"in","points":[100,-206.5,100,-216.5,100,-258,100,-258,100,-299.5,100,-309.5]},
{"from":-19,"to":-18,"fromPort":"bottom","toPort":"out","points":[100,-355,100,-345,100,-345,100,-340.5,100,-340.5,100,-330.5]},
{"from":-15,"to":-16,"fromPort":"out","toPort":"left","points":[-80,-336.5,-80,-346.5,-80,-360,-72.5,-360,-65,-360,-55,-360]},
{"from":-17,"to":-19,"fromPort":"right","toPort":"left","points":[-15,-360,-5,-360,40,-360,40,-360,85,-360,95,-360]},
{"from":-21,"to":-20,"fromPort":"out","toPort":"bottom","points":[150,-336.5,150,-346.5,150,-346.5,150,-345,150,-345,150,-355]},
{"from":-19,"to":-20,"fromPort":"right","toPort":"left","points":[105,-360,115,-360,125,-360,125,-360,135,-360,145,-360]},
{"from":-21,"to":-21,"fromPort":"in","toPort":"wiper","points":[150,-303.5,150,-293.5,150,-290.5,178.15,-290.5,178.15,-320,168.15,-320]},
{"from":-22,"to":-21,"fromPort":"in","toPort":"wiper","points":[200,-336.5,200,-346.5,178.1875,-346.5,178.1875,-320,178.15,-320,168.15,-320]},
{"from":-22,"to":-2,"fromPort":"out","toPort":"20","points":[200,-303.5,200,-293.5,200,-276.515625,130,-276.515625,130,-216.5,130,-206.5]},
{"from":-2,"to":-23,"fromPort":"19","toPort":"bottom","points":[160,-206.5,160,-216.5,160,-225.75,160,-225.75,160,-235,160,-245]},
{"from":-13,"to":-23,"fromPort":"right","toPort":"left","points":[-105,-250,-95,-250,25,-250,25,-250,145,-250,155,-250]},
{"from":-11,"to":-3,"fromPort":"left","toPort":"in","points":[-419.5,-270,-429.5,-270,-514.5,-270,-514.5,-266.5,-514.5,-266.5,-514.5,-256.5]},
{"from":-2,"to":-24,"fromPort":"18","toPort":"out","points":[190,-206.5,190,-216.5,190,-267.45703125,270,-267.45703125,270,-293.5,270,-303.5]},
{"from":-24,"to":-25,"fromPort":"in","toPort":"bottom","points":[270,-336.5,270,-346.5,270,-346.5,270,-345,270,-345,270,-355]},
{"from":-20,"to":-25,"fromPort":"right","toPort":"left","points":[155,-360,165,-360,210,-360,210,-360,255,-360,265,-360]},
{"from":-25,"to":-27,"fromPort":"right","toPort":"left","points":[275,-360,285,-360,330,-360,330,-360,355,-360,365,-360]},
{"from":-26,"to":-27,"fromPort":"out","toPort":"bottom","points":[370,-330.5,370,-340.5,370,-342.75,370,-342.75,370,-345,370,-355]},
{"from":-23,"to":-28,"fromPort":"right","toPort":"left","points":[165,-250,175,-250,240,-250,240,-250,295,-250,305,-250]},
{"from":-31,"to":-30,"fromPort":"out","toPort":"left","points":[526.5,-320,536.5,-320,545.75,-320,545.75,-320,555,-320,565,-320]},
{"from":-33,"to":-32,"fromPort":"right","toPort":"in","points":[455,-280,465,-280,474.25,-280,474.25,-280,483.5,-280,493.5,-280]},
{"from":-2,"to":-33,"fromPort":"16","toPort":"left","points":[250,-206.5,250,-216.5,250,-234.421875,404.3203125,-234.421875,404.3203125,-280,435,-280,445,-280]},
{"from":-2,"to":-26,"fromPort":"17","toPort":"in","points":[220,-206.5,220,-216.5,220,-222.6640625,370,-222.6640625,370,-299.5,370,-309.5]},
{"from":-28,"to":-35,"fromPort":"right","toPort":"left","points":[315,-250,325,-250,405,-250,405,-250,435,-250,445,-250]},
{"from":-32,"to":-35,"fromPort":"out","toPort":"right","points":[526.5,-280,536.5,-280,536.5,-250,500.75,-250,465,-250,455,-250]},
{"from":-30,"to":-37,"fromPort":"bottom","toPort":"top","points":[570,-315,570,-305,570,-240,570,-240,570,-165,570,-155]},
{"from":-36,"to":-37,"fromPort":"ground","toPort":"left","points":[469,-150,479,-150,517,-150,517,-150,555,-150,565,-150]},
{"from":-35,"to":-36,"fromPort":"bottom","toPort":"out","points":[450,-245,450,-235,450,-209.5,450,-209.5,450,-184,450,-174]},
{"from":-31,"to":-33,"fromPort":"in","toPort":"top","points":[493.5,-320,483.5,-320,450,-320,450,-307.5,450,-295,450,-285]},
{"from":-36,"to":-38,"fromPort":"in","toPort":"top","points":[450,-126,450,-116,450,-65.5,450,-65.5,450,-15,450,-5]},
{"from":-2,"to":-38,"fromPort":"14","toPort":"left","points":[280,-33.5,280,-23.5,280,0,435,0,435,0,445,0]},
{"from":-38,"to":-40,"fromPort":"bottom","toPort":"collector","points":[450,5,450,15,450,24.25,450,24.25,450,33.5,450,43.5]},
{"from":-41,"to":-40,"fromPort":"right","toPort":"base","points":[395,60,405,60,417.75,60,417.75,60,420.5,60,430.5,60]},
{"from":-2,"to":-41,"fromPort":"13","toPort":"top","points":[250,-33.5,250,-23.5,250,20.625,390,20.625,390,45,390,55]},
{"from":-41,"to":-42,"fromPort":"bottom","toPort":"in","points":[390,65,390,75,390,87.25,390,87.25,390,99.5,390,109.5]},
{"from":-40,"to":-44,"fromPort":"emitter","toPort":"in","points":[450,76.5,450,86.5,450,90,510,90,510,93.5,510,103.5]},
{"from":-44,"to":-46,"fromPort":"out","toPort":"top","points":[510,136.5,510,146.5,510,146.5,510,145,510,145,510,155]},
{"from":-45,"to":-46,"fromPort":"in","toPort":"bottom","points":[510,183.5,510,173.5,510,173.5,510,175,510,175,510,165]},
{"from":-42,"to":-43,"fromPort":"out","toPort":"in","points":[390,130.5,390,140.5,390,160,390,160,390,179.5,390,189.5]},
{"from":-45,"to":-47,"fromPort":"out","toPort":"collector","points":[510,216.5,510,226.5,510,235,450,235,450,233.5,450,243.5]},
{"from":-43,"to":-48,"fromPort":"out","toPort":"top","points":[390,210.5,390,220.5,390,237.75,390,237.75,390,245,390,255]},
{"from":-48,"to":-47,"fromPort":"right","toPort":"base","points":[395,260,405,260,412.75,260,412.75,260,420.5,260,430.5,260]},
{"from":-47,"to":-49,"fromPort":"emitter","toPort":"top","points":[450,276.5,450,286.5,450,310.75,450,310.75,450,325,450,335]},
{"from":-51,"to":-49,"fromPort":"right","toPort":"left","points":[335,340,345,340,385,340,385,340,435,340,445,340]},
{"from":-50,"to":-51,"fromPort":"out","toPort":"top","points":[330,306.5,330,316.5,330,316.5,330,325,330,325,330,335]},
{"from":-2,"to":-53,"fromPort":"4","toPort":"out","points":[-20,-33.5,-20,-23.5,-20,115,-20,115,-20,263.5,-20,273.5]},
{"from":-2,"to":-52,"fromPort":"5","toPort":"in","points":[10,-33.5,10,-23.5,10,115,10,115,10,263.5,10,273.5]},
{"from":-52,"to":-54,"fromPort":"out","toPort":"top","points":[10,306.5,10,316.5,10,320.75,10,320.75,10,325,10,335]},
{"from":-53,"to":-55,"fromPort":"in","toPort":"top","points":[-20,306.5,-20,316.5,-20,320.75,-20,320.75,-20,325,-20,335]},
{"from":-9,"to":-1,"fromPort":"right","toPort":"6","points":[-419.5,-30,-409.5,-30,-198,-30,-198,-65,-198,-100,-208,-100]},
{"from":-1,"to":-4,"fromPort":"7","toPort":"right","points":[-208,-130,-198,-130,-198,-200,-348.75,-200,-499.5,-200,-509.5,-200]},
{"from":-12,"to":-13,"fromPort":"right","toPort":"left","points":[-179.5,-250,-169.5,-250,-140,-250,-140,-250,-125,-250,-115,-250]},
{"from":-2,"to":-56,"fromPort":"1","toPort":"top","points":[-110,-33.5,-110,-23.5,-110,-9.25,-110,-9.25,-110,5,-110,15]},
{"from":-2,"to":-57,"fromPort":"2","toPort":"top","points":[-80,-33.5,-80,-23.5,-80,-9.25,-80,-9.25,-80,5,-80,15]},
{"from":-57,"to":-2,"fromPort":"right","toPort":"3","points":[-75,20,-65,20,-50,20,-50,-1.75,-50,-23.5,-50,-33.5]},
{"from":-58,"to":-56,"fromPort":"right","toPort":"left","points":[-135,20,-125,20,-125,20,-125,20,-125,20,-115,20]},
{"from":-56,"to":-57,"fromPort":"right","toPort":"left","points":[-105,20,-95,20,-95,20,-95,20,-95,20,-85,20]},
{"from":-8,"to":-58,"fromPort":"bottom","toPort":"left","points":[-514.5,5,-514.5,15,-514.5,20,-155,20,-155,20,-145,20]},
{"from":-58,"to":-55,"fromPort":"bottom","toPort":"left","points":[-140,25,-140,35,-140,340,-87.5,340,-35,340,-25,340]},
{"from":-59,"to":-2,"fromPort":"in","toPort":"6","points":[90,279.5,90,269.5,90,123,40,123,40,-23.5,40,-33.5]},
{"from":-54,"to":-60,"fromPort":"right","toPort":"left","points":[15,340,25,340,50,340,50,340,75,340,85,340]},
{"from":-59,"to":-60,"fromPort":"out","toPort":"top","points":[90,300.5,90,310.5,90,317.75,90,317.75,90,325,90,335]},
{"from":-63,"to":-61,"fromPort":"in","toPort":"top","points":[230,306.5,230,316.5,230,320.75,230,320.75,230,325,230,335]},
{"from":-2,"to":-63,"fromPort":"10","toPort":"out","points":[160,-33.5,160,-23.5,160,122.42968377976197,230,122.42968377976197,230,263.5,230,273.5]},
{"from":-2,"to":-62,"fromPort":"11","toPort":"in","points":[190,-33.5,190,-23.5,190,96.93006463913697,260,96.93006463913697,260,263.5,260,273.5]},
{"from":-62,"to":-64,"fromPort":"out","toPort":"top","points":[260,306.5,260,316.5,260,320.75,260,320.75,260,325,260,335]},
{"from":-2,"to":-65,"fromPort":"12","toPort":"in","points":[220,-33.5,220,-23.5,220,50.79302362351197,320,50.79302362351197,320,63.5,320,73.5]},
{"from":-2,"to":-66,"fromPort":"9","toPort":"top","points":[130,-33.5,130,-23.5,130,150.75,170,150.75,170,325,170,335]},
{"from":-66,"to":-61,"fromPort":"right","toPort":"left","points":[175,340,185,340,175,340,175,340,215,340,225,340]},
{"from":-60,"to":-66,"fromPort":"right","toPort":"left","points":[95,340,105,340,100,340,100,340,155,340,165,340]},
{"from":-55,"to":-54,"fromPort":"right","toPort":"left","points":[-15,340,-5,340,-5,340,-5,340,-5,340,5,340]},
{"from":-61,"to":-64,"fromPort":"right","toPort":"left","points":[235,340,245,340,235,340,235,340,245,340,255,340]},
{"from":-64,"to":-51,"fromPort":"right","toPort":"left","points":[265,340,275,340,310,340,310,340,315,340,325,340]},
{"from":-48,"to":-50,"fromPort":"left","toPort":"in","points":[385,260,375,260,330,260,330,261.75,330,263.5,330,273.5]},
{"from":-38,"to":-67,"fromPort":"right","toPort":"left","points":[455,0,465,0,640,0,640,0.13522086576601566,665,0.13522086576601566,675,0.13522086576601566]},
{"from":-46,"to":-71,"fromPort":"right","toPort":"in","points":[515,160,525,160,626,160,626,160.135220865766,667,160.135220865766,677,160.135220865766]},
{"from":-37,"to":-72,"fromPort":"bottom","toPort":"top","points":[570,-145,570,-135,570,95,570,95,570,325,570,335]},
{"from":-49,"to":-72,"fromPort":"right","toPort":"left","points":[455,340,465,340,510,340,510,340,555,340,565,340]},
{"from":-72,"to":-73,"fromPort":"right","toPort":"left","points":[575,340,585,340,620,340,620,340,625,340,635,340]},
{"from":-65,"to":-46,"fromPort":"out","toPort":"left","points":[320,106.5,320,116.5,320,160,495,160,495,160,505,160]},
{"from":-27,"to":-30,"fromPort":"right","toPort":"top","points":[375,-360,385,-360,570,-360,570,-347.5,570,-335,570,-325]},
{"from":-73,"to":-71,"fromPort":"top","toPort":"out","points":[640,335,640,325,640,190.135220865766,653.5,190.135220865766,667,190.135220865766,677,190.135220865766]},
{"from":-73,"to":-68,"fromPort":"right","toPort":"left","points":[645,340,655,340,660,340,660,340.1352208657661,665,340.1352208657661,675,340.1352208657661]}
]}
      </textarea>
    </div>
  </div>

          
        </div>
        <!-- * * * * * * * * * * * * * -->
        <!--  End of GoJS sample code  -->
      </div>

      <div id="allTagDescriptions" class="p-4 w-full max-w-screen-xl mx-auto">
        <hr/>
        <h3 class="text-xl">GoJS Features in this sample</h3>
        <!-- blacklist tags that do not correspond to a specific GoJS feature -->
          
            <h4>Palette</h4>
<p>
A <a href="../api/symbols/Palette.html" target="api">Palette</a> is a subclass of <a href="../api/symbols/Diagram.html" target="api">Diagram</a> that is used to display a number of <a href="../api/symbols/Part.html" target="api">Part</a>s that
can be dragged into the diagram that is being modified by the user.
The initialization of a <a href="../api/symbols/Palette.html" target="api">Palette</a> is just like the initialization of any <a href="../api/symbols/Diagram.html" target="api">Diagram</a>.
Like Diagrams, you can have more than one Palette on the page at the same time.
</p>
<p>
More information can be found in the <a href="../intro/palette.html">GoJS Intro</a>.
</p>
            <p>
              <a href="../samples/index.html#palette">Related samples</a>
            </p>
            <hr>
          
        <!-- blacklist tags that do not correspond to a specific GoJS feature -->
          
            <h4>Geometry Path Strings</h4>
<p>
The <b>GoJS</b> <a href="../api/symbols/Geometry.html" target="api">Geometry</a> class controls the "shape" of a <a href="../api/symbols/Shape.html" target="api">Shape</a>,
whereas the <a href="../api/symbols/Shape.html#fill" target="api">Shape.fill</a> and <a href="../api/symbols/Shape.html#stroke" target="api">Shape.stroke</a> and other shape properties control the colors and appearance of the shape.
For common shape figures, there are predefined geometries that can be used by setting <a href="../api/symbols/Shape.html#figure" target="api">Shape.figure</a>.
However one can also define custom geometries.
</p>
<p>
One can construct any Geometry by allocating and initializing a <a href="../api/symbols/Geometry.html" target="api">Geometry</a> of at least one <a href="../api/symbols/PathFigure.html" target="api">PathFigure</a> holding some <a href="../api/symbols/PathSegment.html" target="api">PathSegment</a>s.
But you may find that using the string representation of a Geometry is easier to write and save in a database.
Use the static method <a href="../api/symbols/Geometry.html#parse" target="api">Geometry.parse</a> or the <a href="../api/symbols/Shape.html#geometryString" target="api">Shape.geometryString</a> property to transform a geometry path string into a <a href="../api/symbols/Geometry.html" target="api">Geometry</a> object.
</p>
<p>
More information can be found in the <a href="../intro/geometry.html">GoJS Intro</a>.
</p>
            <p>
              <a href="../samples/index.html#geometries">Related samples</a>
            </p>
            <hr>
          
        <!-- blacklist tags that do not correspond to a specific GoJS feature -->
          
            <h4>Links</h4>
<p>
The <a href="../api/symbols/Link.html" target="api">Link</a> class is used to implement a visual relationship between nodes.
Links are normally created by the presence of link data objects in the <a href="../api/symbols/GraphLinksModel.html#linkDataArray" target="api">GraphLinksModel.linkDataArray</a>
or by a parent key reference as the value of the <a href="../api/symbols/TreeModel.html#nodeParentKeyProperty" target="api">TreeModel.nodeParentKeyProperty</a> of a node data object
in a <a href="../api/symbols/TreeModel.html" target="api">TreeModel</a>.
More information can be found in the <a href="../intro/links.html">GoJS Intro</a>.
</p>
            <p>
              <a href="../samples/index.html#links">Related samples</a>
            </p>
            <hr>
          
        <!-- blacklist tags that do not correspond to a specific GoJS feature -->
          
            <h4>Ports in Nodes</h4>
<p>
Specific elements of a Node at which links may connect are called <i>ports</i>. 
There may be any number of ports in a node. By default there is just one port, the whole node,
which results in the effect of having the whole node act as the port.
Port-like GraphObjects can only be in <a href="../api/symbols/Node.html" target="api">Node</a>s or <a href="../api/symbols/Group.html" target="api">Group</a>s, not in <a href="../api/symbols/Link.html" target="api">Link</a>s or <a href="../api/symbols/Adornment.html" target="api">Adornment</a>s or simple <a href="../api/symbols/Part.html" target="api">Part</a>s.
</p>
<p>
More information can be found in the <a href="../intro/ports.html">GoJS Intro</a>.
</p>
            <p>
              <a href="../samples/index.html#ports">Related samples</a>
            </p>
            <hr>
          
        <!-- blacklist tags that do not correspond to a specific GoJS feature -->
          
            <h4>Groups</h4>
<p>
The <a href="../api/symbols/Group.html" target="api">Group</a> class is used to treat a collection of <a href="../api/symbols/Node.html" target="api">Node</a>s and <a href="../api/symbols/Link.html" target="api">Link</a>s as if they were a single <a href="../api/symbols/Node.html" target="api">Node</a>.
Those nodes and links are members of the group; together they constitute a subgraph.
</p>
<p>
A subgraph is <em>not</em> another <a href="../api/symbols/Diagram.html" target="api">Diagram</a>, so there is no separate HTML Div element for the subgraph of a group.
All of the <a href="../api/symbols/Part.html" target="api">Part</a>s that are members of a <a href="../api/symbols/Group.html" target="api">Group</a> belong to the same Diagram as the Group.
There can be links between member nodes and nodes outside of the group as well as links between the group itself and other nodes.
There can even be links between member nodes and the containing group itself.
</p>
<p>
More information can be found in the <a href="../intro/groups.html">GoJS Intro</a>.
</p>
            <p>
              <a href="../samples/index.html#groups">Related samples</a>
            </p>
            <hr>
          
        <!-- blacklist tags that do not correspond to a specific GoJS feature -->
          
            <h4>Tools</h4>
<p>
<a href="../api/symbols/Tool.html" target="api">Tool</a>s handle all input events, such as mouse and keyboard interactions, in a Diagram.
There are many kinds of predefined Tool classes that implement all of the common operations that users do.
</p>
<p>
For flexibility and simplicity, all input events are canonicalized as <a href="../api/symbols/InputEvent.html" target="api">InputEvent</a>s and
redirected by the diagram to go to the <a href="../api/symbols/Diagram.html#currentTool" target="api">Diagram.currentTool</a>.
By default the Diagram.currentTool is an instance of <a href="../api/symbols/ToolManager.html" target="api">ToolManager</a> held as the <a href="../api/symbols/Diagram.html#toolManager" target="api">Diagram.toolManager</a>.
The ToolManager implements support for all mode-less tools.
The ToolManager is responsible for finding another tool that is ready to run and then making it the new current tool.
This causes the new tool to process all of the input events (mouse, keyboard, and touch) until the tool decides that it is finished,
at which time the diagram's current tool reverts back to the <a href="../api/symbols/Diagram.html#defaultTool" target="api">Diagram.defaultTool</a>, which is normally the ToolManager, again.
</p>
<p>
More information can be found in the <a href="../intro/tools.html">GoJS Intro</a>.
</p>
            <p>
              <a href="../samples/index.html#tools">Related samples</a>
            </p>
            <hr>
          
        </div>
    </div>
  </body>
  <!--  This script is part of the gojs.net website, and is not needed to run the sample -->
  <script src="../assets/js/goSamples.js"></script>
</html>