UNPKG

create-gojs-kit

Version:

A CLI for downloading GoJS samples, extensions, and docs

735 lines (693 loc) 31.3 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"/> <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="Transactions" /> <meta property="og:title" content="Transactions" /> <meta name="twitter:title" content="Transactions" /> <meta property="og:image" content="https://gojs.net/latest/assets/images/fp/defaultCard.png" /> <meta itemprop="image" content="https://gojs.net/latest/assets/images/fp/defaultCard.png" /> <meta name="twitter:image" content="https://gojs.net/latest/assets/images/fp/defaultCard.png" /> <meta property="og:url" content="https://gojs.net/latest/intro/transactions.html" /> <meta property="twitter:url" content="https://gojs.net/latest/intro/transactions.html" /> <meta name="twitter:card" content="summary_large_image" /> <meta property="og:type" content="website" /> <meta property="twitter:domain" content="gojs.net" /> <title> Transactions | GoJS </title> <link rel="stylesheet" href="../assets/css/prism.css"/> </head> <script> window.diagrams = []; window.goCode = function (pre, w, h, parentid, animation) { window .diagrams .push([pre, w, h, parentid, animation]); } </script> <body> <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="sticky top-0 left-0 z-10 px-2 w-full bg-white border-b border-b-gray-200 md:hidden"> <button id="sidenavButton" class="flex p-2 text-gray-900 bg-inherit shadow-none items-center text-sm font-semibold hover:!bg-inherit hover:!text-nwoods-accent hover:!shadow-none" aria-label="Navigation"> <svg class="h-7 w-7 block mr-2" 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> <span>Menu</span> </button> </div> <script> window.addEventListener("DOMContentLoaded", function () { // sidenav var sideButton = document.getElementById("sidenavButton"); var sidenav = document.getElementById("sidenav"); if (sideButton && sidenav) { sideButton.addEventListener("click", function (e) { sidenav .classList .toggle("hidden"); e.stopPropagation(); }); document.addEventListener("click", function (e) { // if the clicked element isn't the list, close the list if (!sidenav.classList.contains("hidden") && !e.target.closest("#sidenavList")) { sideButton.click(); } }); } }); </script> <div class="flex flex-row md:min-h-screen w-full max-w-screen-xl mx-auto"> <aside id="sidenav" class="hidden fixed top-0 left-0 z-10 w-full bg-black/10 min-h-screen max-h-screen overflow-x-hidden overflow-y-auto shrink-0 md:block md:sticky md:w-52 md:min-h-0 md:bg-inherit md:border-r md:border-r-gray-200 md:overscroll-auto"> <nav id="sidenavList" class="flex flex-col bg-white w-52 min-h-screen pl-2 pt-4 pb-24 md:w-full md:min-h-0"> <a href="index.html">Basics</a> <a href="buildingObjects.html">Building Parts</a> <a href="usingModels.html">Using Models</a> <a href="dataBinding.html">Data Binding</a> <a href="react.html">GoJS with React</a> <a href="svelte.html">GoJS with Svelte</a> <a href="angular.html">GoJS with Angular</a> <a href="textBlocks.html">TextBlocks</a> <a href="shapes.html">Shapes</a> <a href="pictures.html">Pictures</a> <a href="panels.html">Panels</a> <a href="tablePanels.html">Table Panels</a> <a href="brush.html">Brushes</a> <a href="sizing.html">Sizing Objects</a> <a href="itemArrays.html">Item Arrays</a> <a href="changedEvents.html">Changed Events</a> <a href="transactions.html">Transactions</a> <a href="viewport.html">Coordinates</a> <a href="initialView.html">Initial View</a> <a href="collections.html">Collections</a> <a href="links.html">Links</a> <a href="linkLabels.html">Link Labels</a> <a href="connectionPoints.html">Link Points</a> <a href="ports.html">Ports</a> <a href="nodes.html">Nodes</a> <a href="typings.html">Typings</a> <a href="debugging.html">Debugging</a> <a href="layouts.html">Layouts</a> <a href="routers.html">Routers</a> <a href="trees.html">Trees</a> <a href="subtrees.html">SubTrees</a> <a href="groups.html">Groups</a> <a href="subgraphs.html">SubGraphs</a> <a href="sizedGroups.html">Sized Groups</a> <a href="selection.html">Selection</a> <a href="highlighting.html">Highlighting</a> <a href="theming.html">Theming</a> <a href="tooltips.html">ToolTips</a> <a href="contextMenus.html">Context Menus</a> <a href="events.html">Diagram Events</a> <a href="tools.html">Tools</a> <a href="commands.html">Commands</a> <a href="accessibility.html">Accessibility</a> <a href="buttons.html">Buttons</a> <a href="permissions.html">Permissions</a> <a href="validation.html">Validation</a> <a href="animation.html">Animation</a> <a href="HTMLInteraction.html">HTML Interaction</a> <a href="layers.html">Layers &amp; Z-ordering</a> <a href="palette.html">Palette</a> <a href="overview.html">Overview</a> <a href="replacingDeleting.html">Replacing and Deleting</a> <a href="templateMaps.html">Template Maps</a> <a href="legends.html">Legends and Titles</a> <a href="extensions.html">Extensions</a> <a href="geometry.html">Geometry Strings</a> <a href="grids.html">Grid Patterns</a> <a href="graduatedPanels.html">Graduated Panels</a> <a href="SVGContext.html">Rendering to SVG</a> <a href="makingSVG.html">Snapshot to SVG</a> <a href="makingImages.html">Diagram Images</a> <a href="printing.html">Printing</a> <a href="serverSideImages.html">Server-side Images</a> <a href="nodeScript.html">GoJS in Node.js</a> <a href="testing.html">Testing</a> <a href="performance.html">Performance</a> <a href="platforms.html">Platforms</a> <a href="deployment.html">Deployment</a> </nav> </aside> <script> var navList = document.getElementById('sidenavList'); if (navList !== null) { var url = window.location.href; var lindex = url.lastIndexOf('/'); url = url .slice(lindex + 1) .toLowerCase(); var aTags = navList.getElementsByTagName('a'); var currentindex = -1; for (var i = 0; i < aTags.length; i++) { var lowerhref = aTags[i] .href .toLowerCase(); if (lowerhref.indexOf('/' + url) !== -1) { currentindex = i; aTags[i] .classList .add('active'); break; } } } </script> <div class="px-4 pb-16 w-full overflow-hidden prose"> <h1>Transactions and the UndoManager</h1> <p> <b>GoJS</b> models and diagrams make use of an <a>UndoManager</a> that can record all changes and support undoing and redoing those changes. Each state change is recorded in a <a>ChangedEvent</a>, which includes enough information about both before and after to be able to reproduce the state change in either direction, backward (undo) or forward (redo). Such changes are grouped together into <a>Transaction</a>s so that a user action, which may result in many changes, can be undone and redone as a single operation. </p> <p> Not all state changes result in <a>ChangedEvent</a>s that can be recorded by the UndoManager. Some properties are considered transient, such as <a>Diagram.position</a>, <a>Diagram.scale</a>, <a>Diagram.currentTool</a>, <a>Diagram.currentCursor</a>, or <a>Diagram.isModified</a>. Some changes are structural or considered unchanging, such as <a>Diagram.model</a>, any property of <a>CommandHandler</a>, or any of the tool or layout properties. But most <a>GraphObject</a> and model properties do raise a ChangedEvent on the Diagram or Model, respectively, when a property value has been changed. </p> <h2 id="Transactions">Transactions</h2> <p> Whenever you modify a model or its data programmatically in response to some event, you should wrap the code in a transaction. Call <a>Diagram.startTransaction</a> or <a>Model.startTransaction</a>, make the changes, and then call <a>Diagram.commitTransaction</a> or <a>Model.commitTransaction</a>. Although the primary benefit from using transactions is to group together side-effects for undo/redo, you should use transactions even if your application does not support undo/redo by the user. </p> <p> As with database transactions, you will want to perform transactions that are short and infrequent. Do not leave transactions ongoing between user actions. Consider whether it would be better to have a single transaction surrounding a loop instead of starting and finishing a transaction repeatedly within a loop. Do not execute transactions within a property setter -- such granularity is too small. Instead execute a transaction where the properties are set in response to some user action or external event. </p> <p> However, unlike database transactions, you do not need to conduct a transaction in order to access any state. All JavaScript objects are in memory, so you can look at their properties at any time that it would make sense to do so. But when you want to make state changes to a <a>Diagram</a> or a <a>GraphObject</a> or a <a>Model</a> or a JavaScript object in a model, do so within a transaction. </p> <p> The only exception is that transactions are unnecessary when initializing a model or a diagram before assigning the model to the <a>Diagram.model</a> property. (A Diagram only gets access to an UndoManager via the Model, the <a>Model.undoManager</a> property.) </p> <p> Furthermore many event handlers and listeners are already executed within transactions that are conducted by <a>Tool</a>s or <a>CommandHandler</a> commands, so you often will not need to start and commit a transaction within such functions. Read the API documentation for details about whether a function is called within a transaction. For example, the <a>GraphObject.click</a> event handler to respond to a click on a GraphObject needs to perform a transaction if it wants to modify the model or the diagram. Most custom click event handlers do not change the diagram but instead update some HTML. </p> <p> But implementing an "ExternalObjectsDropped" <a>DiagramEvent</a> listener, which usually does want to modify the just-dropped Parts in the <a>Diagram.selection</a>, is called within the <a>DraggingTool</a>'s transaction, so no additional start/commit transaction calls are needed. </p> <p> Finally, some customizations, such as the <a>Node.linkValidation</a> predicate, should not modify the diagram or model at all. </p> <p> Both model changes and diagram changes are recorded in the <a>UndoManager</a> only if the model's <a>UndoManager.isEnabled</a> has been set to true. If you do not want the user to be able to perform undo or redo and also prevent the recording of any <a>Transaction</a>s, but you still want to get "Transaction"-type <a>ChangedEvent</a>s because you want to update a database, you can set <a>UndoManager.maxHistoryLength</a> to zero. </p> <p> To better understand the relationships between objects and transactions in memory, look at this diagram: </p> <pre class="lang-js" id="transactionsDiagram" style="display: none"><code> diagram.nodeTemplate = $(go.Node, "Auto", { scale : 1.6, isShadowed: true }, new go.Binding("location", "pos", go.Point.parse), { locationSpot: go.Spot.Center, portId: "NODE" }, $(go.Shape, "RoundedRectangle", { fill: "white", portId: "SHAPE" }, new go.Binding("fill", "color"), new go.Binding("strokeWidth", "strokeW")), $(go.TextBlock, { margin: 4, portId: "TEXTBLOCK" }, new go.Binding("text", "txt")) ); // Represents the nodeDataArray for the two nodes diagram.nodeTemplateMap.add("dataNode", $(go.Node, "Auto", { locationSpot: go.Spot.Center, scale: 1.2, selectionAdorned: true, fromSpot: go.Spot.AllSides, toSpot: go.Spot.AllSides, isShadowed: true }, new go.Binding("location", "pos", go.Point.parse), new go.Binding("toSpot", "tSpot"), new go.Binding("fromSpot", "fSpot"), $(go.Shape, "Rectangle", { fill: "white" }), $(go.Panel, "Vertical", { defaultStretch: go.Stretch.Horizontal }, $(go.TextBlock, headerStyle(), // Header: {portId: "HEADER" }, new go.Binding("text", "head")), $(go.Shape, "LineH", { height: 1, stretch: go.Stretch.Fill }), $(go.TextBlock, textStyle(), // Location: { portId: "ROW1" }, new go.Binding("text", "txt1")), $(go.Shape, "LineH", { height: 1, stretch: go.Stretch.Fill }), $(go.TextBlock, textStyle(), // Fill: { portId: "ROW2" }, new go.Binding("text", "txt2")) ) ) ); diagram.nodeTemplateMap.add("dataNodeChanged", $(go.Node, "Auto", { locationSpot: go.Spot.Center, scale: 1.2, selectionAdorned: true, fromSpot: go.Spot.AllSides, toSpot: go.Spot.AllSides, isShadowed: true }, new go.Binding("location", "pos", go.Point.parse), new go.Binding("toSpot", "tSpot"), new go.Binding("fromSpot", "fSpot"), $(go.Shape, "Rectangle", { fill: "white" }), $(go.Panel, "Vertical", { defaultStretch: go.Stretch.Horizontal }, $(go.TextBlock, headerStyle(), // Header: {portId: "HEADER" }, new go.Binding("text", "head")), $(go.Shape, "LineH", { height: 1, stretch: go.Stretch.Fill }), $(go.TextBlock, textStyle(), // Location: { portId: "ROW1" }, new go.Binding("text", "txt1")), $(go.Shape, "LineH", { height: 1, stretch: go.Stretch.Fill }), $(go.TextBlock, textStyle(), // Fill: { portId: "ROW2" }, new go.Binding("text", "txt2")), $(go.Shape, "LineH", { height: 1, stretch: go.Stretch.Fill }), $(go.TextBlock, textStyle(), // Text: { portId: "ROW3" }, new go.Binding("text", "txt3")), ) ) ); diagram.linkTemplateMap.add("dataNode", // Links from dataNode to Nodes $(go.Link, { routing: go.Routing.Orthogonal, corner: 5 }, $(go.Shape, { stroke: "gray", strokeWidth: 2 }), $(go.Shape, { toArrow: "Standard", stroke: "gray", fill: "gray" }) )); diagram.nodeTemplateMap.add("title", $(go.Node, "Auto", new go.Binding("location", "pos", go.Point.parse), $(go.TextBlock, { font: "bold 25pt sans-serif", textAlign: "center"}, new go.Binding("text", "txt")) )); diagram.nodeTemplateMap.add("nodeDataArray", $(go.Node, "Auto", { locationSpot: go.Spot.Center, scale: 1.2, selectionAdorned: true, fromSpot: go.Spot.AllSides, toSpot: go.Spot.AllSides, shadowColor: "#C5C1AA" }, new go.Binding("location", "pos", go.Point.parse), $(go.Shape, "Rectangle", { fill: "lightgray" }), $(go.Panel, "Vertical", { defaultStretch: go.Stretch.Horizontal }, $(go.TextBlock, headerStyle(), { portId: "HEADER", text: "nodeDataArray" }), $(go.Shape, "LineH", { height: 1, stretch: go.Stretch.Fill }), $(go.TextBlock, textStyle(), { portId: "dataNode1", desiredSize: new go.Size(NaN,16) }), $(go.Shape, "LineH", { height: 1, stretch: go.Stretch.Fill }), $(go.TextBlock, textStyle(), { portId: "dataNode2", desiredSize: new go.Size(NaN,16) }) ) )); diagram.linkTemplateMap.add("Data", $(go.Link, { corner: 10, routing: go.Routing.Orthogonal }, new go.Binding("curviness"), $(go.Shape, { stroke: "gray" , strokeWidth: 2 }), $(go.Shape, { toArrow: "Standard", fill: "gray", stroke: "gray", strokeWidth: 2 }), $(go.TextBlock, { font: "bold 12pt Courier", segmentOffset: new go.Point(0, -10) }, new go.Binding("text", "label"), new go.Binding("segmentOffset", "offset")) )); diagram.scale = 0.8; const model = new go.GraphLinksModel(); model.linkFromPortIdProperty = "fPID"; model.linkToPortIdProperty = "tPID" model.nodeDataArray = [ { key: 1, txt: "Diagram", color: "white", pos: "15 305"}, { key: 2, txt: "Model", color: "white", pos: "215 305"}, { key: 3, category: "dataNode", pos: "215 440", head: "Node Data Array", txt1: "nodeDataArray[0]", txt2: "nodeDataArray[1]"}, { key: 4, pos: "215 187", color: "white", txt: "UndoManager"}, { key: 5, pos: "215, 50", category: "dataNode", head: "List of Transactions", txt1: "history[0]", txt2: "history[1]"}, { key: 6, pos: "630, 230", category: "dataNode", head: "List of ChangedEvents", txt1: "changes[0]", txt2: "changes[1]", fSpot: go.Spot.RightSide}, { key: 7, pos: "15, 561", txt: "Alpha", color: "palegreen"}, { key: 8, category: "dataNode", pos: "510 590", head: "Node Data", txt1: "color: \"palegreen\"", txt2: "text: \"Alpha\""}, { key: 9, category: "dataNodeChanged", pos: "630 422", head: "ChangedEvent", txt1: "propertyName: \"color\"", txt2: "newValue: \"palegreen\"", txt3: "oldValue: \"red\""}, { key: 10, category: "dataNodeChanged", pos: "530, 60", head: "Transaction", txt1: "name: \"change color\"", txt2: "isComplete: true", txt3: "changes: . . ."}, ]; model.linkDataArray = [ { from: 1, to: 2, category: "Data", label: ".model", offset: new go.Point(12, 14)}, { from: 2, to: 4, category: "Data", label: ".undoManager", offset: new go.Point(-10, 60)}, { from: 1, to: 4, category: "Data", label: ".undoManager", offset: new go.Point(0, -63)}, { from: 2, tPID: "HEADER", to: 3, category: "Data", label: ".nodeDataArray", offset: new go.Point(0, -72)}, { from: 4, to: 5, category: "Data", label: ".history", offset: new go.Point(0, -45)}, { from: 5, fPID: "ROW1", tPID: "HEADER", to: 10, category: "Data", label: "history[0]", offset: new go.Point(35, 10)}, { from: 1, to: 7, category: "Data", curviness: -70}, { from: 7, tPID: "HEADER", to: 8, category: "Data", curviness: -70, label: ".data", offset: new go.Point(-70, -10)}, { from: 3, tPID: "HEADER", fPID: "ROW1", to: 8, category: "Data", label: "nodeDataArray[0]", offset: new go.Point(-15, -85)}, { from: 6, to: 9, fPID: "ROW2", category: "Data", label: "changes[1]", offset: new go.Point(0, 54)}, { from: 9, to: 8, category: "Data", label: ".object", offset: new go.Point(-10, -13)}, { from: 10, to: 6, fPID: "ROW3", category: "Data", label: ".changes", offset: new go.Point(-10, 13)}, ]; diagram.model = model; // Formatting function headerStyle() { return { margin: 3, font: "bold 12pt sans-serif", minSize: new go.Size(140, 16), maxSize: new go.Size(120, NaN), textAlign: "center" }; } function textStyle() { return { margin: 3, font: "italic 10pt sans-serif", minSize: new go.Size(16, 16), maxSize: new go.Size(160, NaN), textAlign: "left" }; } </code></pre> <script> goCode('transactionsDiagram', 650, 550); </script> <p> A typical case for using transactions is when some command makes a change to the model. </p> <pre class="lang-js" id="transaction"><code> // define a function named "addChild" that is invoked by a button click addChild = () => { const selnode = diagram.selection.first(); if (!(selnode instanceof go.Node)) return; diagram.commit(d => { // have the Model add a new node data const newnode = { key: "N", text: `New ${d.model.nodeDataArray.length}` }; d.model.addNodeData(newnode); // this makes sure the key is unique // and then add a link data connecting the original node with the new one const newlink = { from: selnode.data.key, to: newnode.key }; // add the new link to the model d.model.addLinkData(newlink); }, "add node and link"); }; diagram.nodeTemplate = new go.Node("Auto") .add( new go.Shape("RoundedRectangle", { fill: "whitesmoke" }), new go.TextBlock({ margin: 5 }) .bind("text") ); diagram.layout = new go.TreeLayout(); const nodeDataArray = [ { key: 1, text: "Alpha" }, { key: 2, text: "Beta" } ]; const linkDataArray = [ { from: 1, to: 2 } ]; diagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray); diagram.model.undoManager.isEnabled = true; </code></pre> <p> In the following example, select a node and then click the button. The addChild function adds a link connecting the selected node to a new node. When no Node is selected, nothing happens. </p> <button onclick="addChild()">addChild() to selected Node</button> <script> goCode('transaction', 600, 200); </script> <h2 id="SupportUndoManager">Supporting the UndoManager</h2> <p> Changes to JavaScript data properties do not automatically result in any notifications that can be observed. Thus when you want to change the value of a property in a manner that can be undone and redone, you should call <a>Model.setDataProperty</a> (or <a>Model.set</a>, which is an abbreviation for that method). This will get the previous value for the property, set the property to the new value, and call <a>Model.raiseDataChanged</a>, which will also automatically update any target bindings in the Node corresponding to the data. </p> <pre class="lang-js" id="changingData"><code> diagram.nodeTemplate = new go.Node("Auto") .add( new go.Shape("RoundedRectangle", { fill: "whitesmoke" }), new go.TextBlock({ margin: 5 }) .bind("text", "someValue") // bind to the "someValue" data property ); const nodeDataArray = [ { key: "Alpha", someValue: 1 } ]; diagram.model = new go.GraphLinksModel(nodeDataArray); diagram.model.undoManager.isEnabled = true; // define a function named "incrementData" callable by onclick incrementData = () => { diagram.model.commit(m => { const data = m.nodeDataArray[0]; // get the first node data m.set(data, "someValue", (data.someValue || 0) + 1); }, "increment"); }; </code></pre> <p> Move the node around. Click on the button to increase the value of the "someValue" property on the first node data. Click to focus in the Diagram and then Ctrl-Z and Ctrl-Y to undo and redo the moves and value changes. </p> <button onclick="incrementData()">incrementData()</button> <script> goCode('changingData', 250, 150); </script> </div> </div> <footer class="bg-white text-gray-900 border-t border-t-gray-200"> <div class="w-full max-w-screen-lg mx-auto px-4 py-6"> <p id="version" class="text-xs text-gray-900 m-0"></p> <div class="text-sm px-0 mb-4 grid grid-cols-2 sm:grid-cols-3 gap-y-10"> <div> <h2 class="text-base font-semibold text-nwoods-primary">GoJS</h2> <ul class="list-none space-y-4 md:space-y-1 px-0"> <li> <a href="../samples/index.html">Samples</a> </li> <li> <a href="../learn/index.html">Learn</a> </li> <li> <a href="../intro/index.html">Intro</a> </li> <li> <a href="../api/index.html">API</a> </li> <li> <a href="../changelog.html">Changelog</a> </li> <li> <a href="https://github.com/NorthwoodsSoftware/GoJS" target="_blank" rel="noopener">GitHub</a> </li> </ul> </div> <div> <h2 class="text-base font-semibold text-nwoods-primary">Support</h2> <ul class="list-none space-y-4 md:space-y-1 px-0"> <li> <a href="https://nwoods.com/contact.html" target="_blank" rel="noopener" onclick="getOutboundLink('https://nwoods.com/contact.html', 'contact');">Contact</a> </li> <li> <a href="https://forum.nwoods.com/c/gojs" target="_blank" rel="noopener">Forum</a> </li> <li> <a href="https://nwoods.com/app/activate.aspx?sku=gojs" target="_blank" rel="noopener">Activate</a> </li> <li> <a href="https://nwoods.com/sales/index.html" target="_blank" rel="noopener" onclick="getOutboundLink('https://nwoods.com/sales/index.html', 'buy');">Buy</a> </li> <li> <a href="https://nwoods.com/register.html" target="_blank" rel="noopener">Register</a> </li> </ul> </div> <div> <h2 class="text-base font-semibold text-nwoods-primary">Company</h2> <ul class="list-none space-y-4 md:space-y-1 px-0"> <li> <a target="_blank" href="https://nwoods.com" target="_blank" rel="noopener">Northwoods</a> </li> <li> <a target="_blank" href="https://nwoods.com/about.html" target="_blank" rel="noopener">About Us</a> </li> <li> <a target="_blank" href="https://nwoods.com/contact.html" target="_blank" rel="noopener">Contact Us</a> </li> <li> <a target="_blank" href="https://nwoods.com/consulting.html" target="_blank" rel="noopener">Consulting</a> </li> <li> <a target="_blank" href="https://twitter.com/northwoodsgo" target="_blank" rel="noopener">Twitter</a> </li> </ul> </div> </div> <p class="text-sm text-gray-900 md:mb-6"> Copyright 1998-2025 <a href="https://nwoods.com">Northwoods Software</a> </p> </div> </footer> </body> <script async src="https://www.googletagmanager.com/gtag/js?id=G-S5QK8VSK84"></script> <script> window.dataLayer = window.dataLayer || []; function gtag() { dataLayer.push(arguments); } gtag('js', new Date()); gtag('config', 'G-S5QK8VSK84'); var getOutboundLink = function (url, label) { gtag('event', 'click', { 'event_category': 'outbound', 'event_label': label, 'transport_type': 'beacon' }); } const params = new URL(document.location).searchParams let a = params.get('a'); if (a) localStorage.setItem('a', a); a = localStorage.getItem('a'); if (a) { const links = [...document.body.getElementsByTagName("a")].filter((l) => l.href.includes('nwoods.com')); for (const l of links) { const url = new URL(l.href); url.searchParams.set('a', a); l.href = url; } } </script> <script src="../assets/js/prism.js"></script> <script src="https://cdn.jsdelivr.net/npm/gojs@3.1.0"></script> <script src="../assets/js/goDoc.js"></script> <script> document.addEventListener("DOMContentLoaded", function () { if (window.go) document .getElementById('version') .textContent = "GoJS version " + go.version; if (window.goDoc) window.goDoc(); var d = window.diagrams; for (var i = 0; i < d.length; i++) { var dargs = d[i]; goCodeExecute(dargs[0], dargs[1], dargs[2], dargs[3], dargs[4]); } if (window.extra) window.extra(); } ); </script> </html>