UNPKG

gojs

Version:

Interactive diagrams, charts, and graphs, such as trees, flowcharts, orgcharts, UML, BPMN, or business diagrams

375 lines (345 loc) 15.4 kB
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>System Dynamics</title> <meta name="description" content="A simple implementation of a system dynamics editor." /> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- Copyright 1998-2020 by Northwoods Software Corporation. --> <style> button { } .pointer_normal { font-weight: normal; background: #f0f0f0; } .pointer_selected { font-weight: bold; background: yellow; } .node_normal { font-weight: normal; background: #fff0f0; } .node_selected { font-weight: bold; background: #ff8080; } .link_normal { font-weight: normal; background: #f0fff0; } .link_selected { font-weight: bold; background: #80ff80; } </style> <script src="../release/go.js"></script> <script src="../extensions/Figures.js"></script> <script src="../extensions/NodeLabelDraggingTool.js"></script> <script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework --> <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() { if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this var $ = go.GraphObject.make; myDiagram = $(go.Diagram, "myDiagram", { "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() { // change the curve of the LinkingTool.temporaryLink this.temporaryLink.curve = (SD.itemType === "flow") ? go.Link.Normal : go.Link.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) { // 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 drawng 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 "clickCreatingTool.canStart": function() { // but only in "node" creation mode return SD.mode === "node" && go.ClickCreatingTool.prototype.canStart.call(this); }, "clickCreatingTool.insertPart": function(loc) { // customize the data for the new node 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", function(e) { var button = document.getElementById("SaveButton"); if (button) button.disabled = !myDiagram.isModified; var idx = document.title.indexOf("*"); if (myDiagram.isModified) { if (idx < 0) document.title += "*"; } else { if (idx >= 0) document.title = document.title.substr(0, idx); } }); // generate unique label for valve on newly-created flow link myDiagram.addDiagramListener("LinkDrawn", function(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(); } function buildTemplates() { var $ = go.GraphObject.make; // helper functions for the templates function nodeStyle() { return [ { type: go.Panel.Spot, layerName: "Background", locationObjectName: "SHAPE", selectionObjectName: "SHAPE", locationSpot: go.Spot.Center }, new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify) ]; } function shapeStyle() { return { name: "SHAPE", stroke: "black", fill: "#f0f0f0", portId: "", // So a link can be dragged from the Node: see /GraphObject.html#portId fromLinkable: true, toLinkable: true }; } function textStyle() { return [ { font: "bold 11pt helvetica, bold arial, sans-serif", margin: 2, editable: true }, new go.Binding("text", "label").makeTwoWay() ]; } // Node templates myDiagram.nodeTemplateMap.add("stock", $(go.Node, nodeStyle(), $(go.Shape, shapeStyle(), { desiredSize: new go.Size(50, 30) }), $(go.TextBlock, textStyle(), { _isNodeLabel: true, // declare draggable by NodeLabelDraggingTool alignment: new go.Spot(0.5, 0.5, 0, 30) // initial value }, new go.Binding("alignment", "label_offset", go.Spot.parse).makeTwoWay(go.Spot.stringify)) )); myDiagram.nodeTemplateMap.add("cloud", $(go.Node, nodeStyle(), $(go.Shape, shapeStyle(), { figure: "Cloud", desiredSize: new go.Size(35, 35) }) )); myDiagram.nodeTemplateMap.add("valve", $(go.Node, nodeStyle(), { movable: false, layerName: "Foreground", alignmentFocus: go.Spot.None }, $(go.Shape, shapeStyle(), { figure: "Ellipse", desiredSize: new go.Size(20, 20) }), $(go.TextBlock, textStyle(), { _isNodeLabel: true, // declare draggable by NodeLabelDraggingTool alignment: new go.Spot(0.5, 0.5, 0, 20) // initial value }, new go.Binding("alignment", "label_offset", go.Spot.parse).makeTwoWay(go.Spot.stringify)) )); myDiagram.nodeTemplateMap.add("variable", $(go.Node, nodeStyle(), { type: go.Panel.Auto }, $(go.TextBlock, textStyle(), { isMultiline: false }), $(go.Shape, shapeStyle(), // the port is in front and transparent, even though it goes around the text; // in "link" mode will support drawing a new link { isPanelMain: true, stroke: null, fill: "transparent" }) )); // Link templates myDiagram.linkTemplateMap.add("flow", $(go.Link, { toShortLength: 8 }, $(go.Shape, { stroke: "blue", strokeWidth: 5 }), $(go.Shape, { fill: "blue", stroke: null, toArrow: "Standard", scale: 2.5 }) )); myDiagram.linkTemplateMap.add("influence", $(go.Link, { curve: go.Link.Bezier, toShortLength: 8 }, $(go.Shape, { stroke: "green", strokeWidth: 1.5 }), $(go.Shape, { fill: "green", stroke: null, toArrow: "Standard", scale: 1.5 }) )); } function setMode(mode, itemType) { myDiagram.startTransaction(); document.getElementById(SD.itemType + "_button").className = SD.mode + "_normal"; document.getElementById(itemType + "_button").className = mode + "_selected"; SD.mode = mode; SD.itemType = itemType; if (mode === "pointer") { myDiagram.allowLink = false; myDiagram.nodes.each(function(n) { n.port.cursor = ""; }); } else if (mode === "node") { myDiagram.allowLink = false; myDiagram.nodes.each(function(n) { n.port.cursor = ""; }); } else if (mode === "link") { myDiagram.allowLink = true; myDiagram.nodes.each(function(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); } </script> </head> <body onload="init()"> <div id="sample"> <div id="myDiagram" style="width:600px; height:500px; border:solid 1px black"></div> <button id="pointer_button" class="pointer_selected" onclick="setMode('pointer','pointer');">Pointer</button> <button id="stock_button" class="node_normal" onclick="setMode('node','stock');" style="margin-left:20px;">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> <button id="flow_button" class="link_normal" onclick="setMode('link','flow');" style="margin-left:20px;">Flow</button> <button id="influence_button" class="link_normal" onclick="setMode('link','influence');">Influence</button> <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 behaiour. </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: <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> <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="../extensions/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> </body> </html>