create-gojs-kit
Version:
A CLI for downloading GoJS samples, extensions, and docs
539 lines (487 loc) • 22.6 kB
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>