UNPKG

create-gojs-kit

Version:

A CLI for downloading GoJS samples, extensions, and docs

539 lines (487 loc) 22.6 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="A simple implementation of a system dynamics editor." /> <meta itemprop="description" content="A simple implementation of a system dynamics editor." /> <meta property="og:description" content="A simple implementation of a system dynamics editor." /> <meta name="twitter:description" content="A simple implementation of a system dynamics editor." /> <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="System Dynamics Diagram Editor: Storage, Flows, Control Factors" /> <meta property="og:title" content="System Dynamics Diagram Editor: Storage, Flows, Control Factors" /> <meta name="twitter:title" content="System Dynamics Diagram Editor: Storage, Flows, Control Factors" /> <meta property="og:image" content="https://gojs.net/latest/assets/images/screenshots/systemdynamics.png" /> <meta itemprop="image" content="https://gojs.net/latest/assets/images/screenshots/systemdynamics.png" /> <meta name="twitter:image" content="https://gojs.net/latest/assets/images/screenshots/systemdynamics.png" /> <meta property="og:url" content="https://gojs.net/latest/samples/systemDynamics.html" /> <meta property="twitter:url" content="https://gojs.net/latest/samples/systemDynamics.html" /> <meta name="twitter:card" content="summary_large_image" /> <meta property="og:type" content="website" /> <meta property="twitter:domain" content="gojs.net" /> <title> System Dynamics Diagram Editor: Storage, Flows, Control Factors | 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/Figures.js"></script> <script src="../extensions/NodeLabelDraggingTool.js"></script> <script id="code"> // SD is a global variable, to avoid polluting global namespace and to make the global // nature of the individual variables obvious. var SD = { mode: 'pointer', // Set to default mode. Alternatives are "node" and "link", for // adding a new node or a new link respectively. itemType: 'pointer', // Set when user clicks on a node or link button. nodeCounter: { stock: 0, cloud: 0, variable: 0, valve: 0 } }; var myDiagram; // Declared as global function init() { myDiagram = new go.Diagram('myDiagramDiv', { 'undoManager.isEnabled': true, allowLink: false, // linking is only started via buttons, not modelessly 'animationManager.isEnabled': false, 'linkingTool.portGravity': 0, // no snapping while drawing new links 'linkingTool.doActivate': function () { // an override must be function, not using an arrow // change the curve of the LinkingTool.temporaryLink this.temporaryLink.curve = SD.itemType === 'flow' ? go.Curve.None : go.Curve.Bezier; this.temporaryLink.path.stroke = SD.itemType === 'flow' ? 'blue' : 'green'; this.temporaryLink.path.strokeWidth = SD.itemType === 'flow' ? 5 : 1; go.LinkingTool.prototype.doActivate.call(this); }, // override the link creation process 'linkingTool.insertLink': function (fromnode, fromport, tonode, toport) { // method override must be function, not => // to control what kind of Link is created, // change the LinkingTool.archetypeLinkData's category myDiagram.model.setCategoryForLinkData(this.archetypeLinkData, SD.itemType); // Whenever a new Link is drawn by the LinkingTool, it also adds a node data object // that acts as the label node for the link, to allow links to be drawn to/from the link. this.archetypeLabelNodeData = SD.itemType === 'flow' ? { category: 'valve' } : null; // also change the text indicating the condition, which the user can edit this.archetypeLinkData.text = SD.itemType; return go.LinkingTool.prototype.insertLink.call(this, fromnode, fromport, tonode, toport); }, 'clickCreatingTool.archetypeNodeData': {}, // enable ClickCreatingTool 'clickCreatingTool.isDoubleClick': false, // operates on a single click in background // but only in "node" creation mode 'clickCreatingTool.canStart': function () { // method override must be function, not => return SD.mode === 'node' && go.ClickCreatingTool.prototype.canStart.call(this); }, // customize the data for the new node 'clickCreatingTool.insertPart': function (loc) { // method override must be function, not => SD.nodeCounter[SD.itemType] += 1; var newNodeId = SD.itemType + SD.nodeCounter[SD.itemType]; this.archetypeNodeData = { key: newNodeId, category: SD.itemType, label: newNodeId }; return go.ClickCreatingTool.prototype.insertPart.call(this, loc); } }); // install the NodeLabelDraggingTool as a "mouse move" tool myDiagram.toolManager.mouseMoveTools.insertAt(0, new NodeLabelDraggingTool()); // when the document is modified, add a "*" to the title and enable the "Save" button myDiagram.addDiagramListener('Modified', e => { const button = document.getElementById('SaveButton'); if (button) button.disabled = !myDiagram.isModified; const idx = document.title.indexOf('*'); if (myDiagram.isModified) { if (idx < 0) document.title += '*'; } else { if (idx >= 0) document.title = document.title.slice(0, idx); } }); // generate unique label for valve on newly-created flow link myDiagram.addDiagramListener('LinkDrawn', e => { var link = e.subject; if (link.category === 'flow') { myDiagram.startTransaction('updateNode'); SD.nodeCounter.valve += 1; var newNodeId = 'flow' + SD.nodeCounter.valve; var labelNode = link.labelNodes.first(); myDiagram.model.setDataProperty(labelNode.data, 'label', newNodeId); myDiagram.commitTransaction('updateNode'); } }); buildTemplates(); load(); setMode('pointer', 'pointer'); } function buildTemplates() { // helper functions for the templates function nodeStyle(node) { node.type = go.Panel.Spot; node.layerName = 'Background'; node.locationObjectName = 'SHAPE'; node.selectionObjectName = 'SHAPE'; node.locationSpot = go.Spot.Center; node.bindTwoWay('location', 'loc', go.Point.parse, go.Point.stringify); } function shapeStyle(shp) { shp.name = 'SHAPE'; shp.stroke = 'black'; shp.fill = '#f0f0f0'; shp.portId = ''; // So a link can be dragged from the Node = see /GraphObject.html#portId shp.fromLinkable = true; shp.toLinkable = true } function textStyle(tb) { tb.font = 'bold 11pt helvetica, bold arial, sans-serif'; tb.margin = 2; tb.editable = true; tb.bindTwoWay('text', 'label'); } function labelStyle(obj) { obj.attach({ _isNodeLabel: true }) // declare draggable by NodeLabelDraggingTool obj.alignment = new go.Spot(0.5, 0.5, 0, 30); // initial value obj.bindTwoWay('alignment', 'label_offset', go.Spot.parse, go.Spot.stringify); } // Node templates myDiagram.nodeTemplateMap.add('stock', new go.Node() .apply(nodeStyle) .add( new go.Shape({ desiredSize: new go.Size(50, 30) }) .apply(shapeStyle), new go.TextBlock() .apply(textStyle) .apply(labelStyle) )); myDiagram.nodeTemplateMap.add('cloud', new go.Node() .apply(nodeStyle) .add( new go.Shape('Cloud', { desiredSize: new go.Size(35, 35) }) .apply(shapeStyle) )); myDiagram.nodeTemplateMap.add('valve', new go.Node() .apply(nodeStyle) .set({ movable: false, layerName: 'Foreground', alignmentFocus: go.Spot.None }) .add( new go.Shape('Ellipse', { desiredSize: new go.Size(20, 20) }) .apply(shapeStyle), new go.TextBlock() .apply(textStyle) .apply(labelStyle) )); myDiagram.nodeTemplateMap.add('variable', new go.Node() .apply(nodeStyle) .set({ type: go.Panel.Auto }) .add( new go.TextBlock({ isMultiline: false }) .apply(textStyle), new go.Shape() .apply(shapeStyle) // the port is in front and transparent, even though it goes around the text; // in "link" mode will support drawing a new link .set({ isPanelMain: true, stroke: null, fill: 'transparent' }) )); // Link templates myDiagram.linkTemplateMap.add('flow', new go.Link({ toShortLength: 8 }) .add( new go.Shape({ stroke: 'blue', strokeWidth: 5 }), new go.Shape({ fill: 'blue', stroke: null, toArrow: 'Standard', scale: 2.5 }) ) ); myDiagram.linkTemplateMap.add('influence', new go.Link({ curve: go.Curve.Bezier, toShortLength: 8 }) .add( new go.Shape({ stroke: 'green', strokeWidth: 1.5 }), new go.Shape({ fill: 'green', stroke: null, toArrow: 'Standard', scale: 1.5 }) ) ); } function setMode(mode, itemType) { myDiagram.startTransaction(); document.getElementById(SD.itemType + '_button').style.filter = 'brightness(100%)'; document.getElementById(itemType + '_button').style.filter = 'brightness(180%)'; SD.mode = mode; SD.itemType = itemType; if (mode === 'pointer') { myDiagram.allowLink = false; myDiagram.nodes.each(n => (n.port.cursor = '')); } else if (mode === 'node') { myDiagram.allowLink = false; myDiagram.nodes.each(n => (n.port.cursor = '')); } else if (mode === 'link') { myDiagram.allowLink = true; myDiagram.nodes.each(n => (n.port.cursor = 'pointer')); } myDiagram.commitTransaction('mode changed'); } // Show the diagram's model in JSON format that the user may edit function save() { document.getElementById('mySavedModel').value = myDiagram.model.toJson(); myDiagram.isModified = false; } function load() { myDiagram.model = go.Model.fromJson(document.getElementById('mySavedModel').value); } window.addEventListener('DOMContentLoaded', init); </script> <div id="sample"> <div class="flex flex-row flex-wrap"> <div id="myDiagramDiv" class="mb-2 mr-2 min-w-[400px]" style="width: 600px; height: 500px; border: solid 1px black"></div> <div class="mb-2 h-fit w-fit flex-none rounded-md border-x border-t border-black" style="background-color: rgb(31, 73, 99)"> <div> <h3 class="text-center text-white">On Click...</h3> </div> <table class="table" style="background-color: white"> <tbody> <th>Do Default</th> <tr> <td> <button id="pointer_button" onclick="setMode('pointer','pointer');">Pointer</button> </td> </tr> <th>Create ______ Node</th> <tr> <td> <button id="stock_button" class="node_normal" onclick="setMode('node','stock');"> Stock </button> <button id="cloud_button" class="node_normal" onclick="setMode('node','cloud');"> Cloud </button> <button id="variable_button" class="node_normal" onclick="setMode('node','variable');"> Variable </button> </td> </tr> <th>Create ______ Link</th> <tr> <td> <button id="flow_button" class="link_normal" onclick="setMode('link','flow');"> Flow </button> <button id="influence_button" class="link_normal" onclick="setMode('link','influence');"> Influence </button> </td> </tr> </tbody> </table> <div class="rounded-b-md border-b border-black" style="background-color: white; height: 0.5rem"></div> </div> </div> <p> A <em>system dynamics diagram</em> shows the storages (stocks) and flows of material in some system, and the factors that influence the rates of flow. It is usually a cosmetic interface for building mathematical models -- you provide values and equations for the stocks and flows, and appropriate software can then simulate the system's behavior. </p> <p> The diagram has two types of link: flow links and influence links. In additon to the node attached to each flow, there are 3 types of node: </p> <ul> <li><b>stocks</b>, the amount of some substance</li> <li><b>clouds</b>, like stocks, but outside the system of interest</li> <li><b>variables</b>, either numeric constants or calculated from other elements</li> </ul> <p> The conventional user interface for building system dynamics diagrams is modal -- you select a tool in the toolbar, then either click in an empty part of the diagram to add a node or drag from one node to another to add a link. That is the approach used in this example, accomplished with the <a>clickCreatingTool</a> and <a>linkingTool</a>. Note that you need to click on the Pointer tool to revert to the normal mode. </p> <p> In addition to the above, the diagram also installs the <a href="../samples/NodeLabelDragging.html">NodeLabelDraggingTool</a> extension into <a>ToolManager.mouseMoveTools</a>. </p> <p>This sample is based on a prototype developed by Robert Muetzelfeldt.</p> <div> <div> <button id="SaveButton" onclick="save()">Save</button> <button onclick="load()">Load</button> Diagram Model saved in JSON format: </div> <textarea id="mySavedModel" style="width: 100%; height: 400px"> { "class": "go.GraphLinksModel", "linkLabelKeysProperty": "labelKeys", "nodeDataArray": [ {"key":"grass", "category":"stock", "label":"Grass", "loc":"30 220", "label_offset":"0.5 0.5 0 30"}, {"key":"cloud1", "category":"cloud", "loc":"200 220"}, {"key":"sheep", "category":"stock", "label":"Sheep", "loc":"30 20","label_offset":"0.5 0.5 0 -30"}, {"key":"cloud2", "category":"cloud", "loc":"200 20"}, {"key":"cloud3", "category":"cloud", "loc":"-150 220"}, {"key":"grass_loss", "category":"valve", "label":"grass_loss","label_offset":"0.5 0.5 0 20" }, {"key":"grazing", "category":"valve", "label":"grazing","label_offset":"0.5 0.5 45 0" }, {"key":"growth", "category":"valve", "label":"growth","label_offset":"0.5 0.5 0 20" }, {"key":"sheep_loss", "category":"valve", "label":"sheep_loss","label_offset":"0.5 0.5 0 20" }, {"key":"k1", "category":"variable", "label":"good weather", "loc": "-80 100"}, {"key":"k2", "category":"variable", "label":"bad weather", "loc": "100 150"}, {"key":"k3", "category":"variable", "label":"wolves", "loc": "150 -40"} ], "linkDataArray": [ {"from":"grass", "to":"cloud1", "category":"flow", "labelKeys":[ "grass_loss" ]}, {"from":"sheep", "to":"cloud2", "category":"flow", "labelKeys":[ "sheep_loss" ]}, {"from":"grass", "to":"sheep", "category":"flow", "labelKeys":[ "grazing" ]}, {"from":"cloud3", "to":"grass", "category":"flow", "labelKeys":[ "growth" ]}, {"from":"grass", "to":"grass_loss", "category":"influence"}, {"from":"sheep", "to":"sheep_loss", "category":"influence"}, {"from":"grass", "to":"growth", "category":"influence"}, {"from":"grass", "to":"grazing", "category":"influence"}, {"from":"sheep", "to":"grazing", "category":"influence"}, {"from":"k1", "to":"growth", "category":"influence"}, {"from":"k1", "to":"grazing", "category":"influence"}, {"from":"k2", "to":"grass_loss", "category":"influence"}, {"from":"k3", "to":"sheep_loss", "category":"influence"} ] } </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 --> <!-- 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>