create-gojs-kit
Version:
A CLI for downloading GoJS samples, extensions, and docs
723 lines (678 loc) • 27.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"/>
<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="Grid Patterns" />
<meta property="og:title" content="Grid Patterns" />
<meta name="twitter:title" content="Grid Patterns" />
<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/grids.html" />
<meta property="twitter:url" content="https://gojs.net/latest/intro/grids.html" />
<meta name="twitter:card" content="summary_large_image" />
<meta property="og:type" content="website" />
<meta property="twitter:domain" content="gojs.net" />
<title>
Grid Patterns | 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 & 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>Grid Patterns</h1>
<p>
It is common to want to display a grid of lines drawn at regular intervals. You may also want to force dragged parts to be aligned on grid points, and to
resize parts to be multiples of the grid cell size.
</p>
<p>
Grids are implemented using a type of <a>Panel</a>, <a>Panel.Grid</a>. Grid Panels, like most other types of Panels, can be used within <a>Node</a>s or any
other kind of <a>Part</a>. However when they are used as the <a>Diagram.grid</a>, they are effectively infinite in extent.
</p>
<p>
Unlike in other kinds of <a>Panel</a>s, Grid Panel elements must be <a>Shape</a>s that are only used to control how the grid lines or grid bars are drawn.
</p>
<p>
See samples that make use of grids in the <a href="../samples/index.html#grid">samples index</a>.
</p>
<h2 id="DefaultGrid">Default Grid</h2>
<p>
To display a grid pattern in the background of the diagram, you can just make the <a>Diagram.grid</a> visible:
</p>
<pre class="lang-js" id="defaultGrid"><code>
diagram.grid.visible = true;
diagram.nodeTemplate =
new go.Node("Auto")
.add(
new go.Shape("Rectangle", { fill: "lightgray" }),
new go.TextBlock({ margin: 5 })
.bind("text", "key")
);
const nodeDataArray = [
{ key: "Alpha" }, { key: "Beta" }, { key: "Gamma" }
];
diagram.model = new go.GraphLinksModel(nodeDataArray);
</code></pre>
<script>
goCode('defaultGrid', 600, 150);
</script>
<h2 id="GridSnapping">Grid Snapping</h2>
<p>
The <a>DraggingTool</a> and <a>ResizingTool</a> can change their behavior based on the background grid pattern, if you set the
<a>DraggingTool.isGridSnapEnabled</a> and/or <a>ResizingTool.isGridSnapEnabled</a> properties to true.
</p>
<p>
Setting <a>DraggingTool.isGridSnapEnabled</a> to true will not affect disconnected Links, but these can snap if you define a custom
<a>Part.dragComputation</a> to do so on the Link template.
</p>
<pre class="lang-js" id="gridSnapping"><code>
diagram.grid.visible = true;
diagram.toolManager.draggingTool.isGridSnapEnabled = true;
diagram.toolManager.resizingTool.isGridSnapEnabled = true;
diagram.nodeTemplate =
new go.Node("Auto", { resizable: true })
.add(
new go.Shape("Rectangle", { fill: "lightgray" }),
new go.TextBlock({ margin: 5 })
.bind("text", "key")
);
const nodeDataArray = [
{ key: "Alpha" }, { key: "Beta" }, { key: "Gamma" }
];
diagram.model = new go.GraphLinksModel(nodeDataArray);
</code></pre>
<script>
goCode('gridSnapping', 600, 150);
</script>
<h2 id="SimpleGridCustomization">Simple Grid Customization</h2>
<p>
You can change the size of the grid cell by setting <a>Panel.gridCellSize</a>:
</p>
<pre class="lang-js" id="biggerGrid"><code>
diagram.grid.visible = true;
diagram.grid.gridCellSize = new go.Size(30, 20);
diagram.toolManager.draggingTool.isGridSnapEnabled = true;
diagram.toolManager.resizingTool.isGridSnapEnabled = true;
diagram.nodeTemplate =
new go.Node("Auto", { resizable: true })
.add(
new go.Shape("Rectangle", { fill: "lightgray" }),
new go.TextBlock({ margin: 5 })
.bind("text", "key")
);
const nodeDataArray = [
{ key: "Alpha" }, { key: "Beta" }, { key: "Gamma" }
];
diagram.model = new go.GraphLinksModel(nodeDataArray);
</code></pre>
<script>
goCode('biggerGrid', 600, 150);
</script>
<p>
The cell size used when snapping the locations of Parts during a drag need not be exactly the same as the background grid's cell size. The value of
<a>DraggingTool.gridSnapCellSize</a> takes precedence over the <a>Panel.gridCellSize</a>. Note that if <a>DraggingTool.gridSnapCellSize</a> is set but
<a>ResizingTool.cellSize</a> is not, Parts will use the DraggingTool.gridSnapCellSize value when resizing.
</p>
<pre class="lang-js" id="gridSnapping2"><code>
diagram.grid.visible = true;
diagram.toolManager.draggingTool.isGridSnapEnabled = true;
diagram.toolManager.resizingTool.isGridSnapEnabled = true;
// snap to every other point both vertically and horizontally
// (the default background grid has a cell size of 10x10)
diagram.toolManager.draggingTool.gridSnapCellSize = new go.Size(20, 20);
diagram.nodeTemplate =
new go.Node("Auto", { resizable: true })
.add(
new go.Shape("Rectangle", { fill: "lightgray" }),
new go.TextBlock({ margin: 5 })
.bind("text", "key")
);
const nodeDataArray = [
{ key: "Alpha" }, { key: "Beta" }, { key: "Gamma" }
];
diagram.model = new go.GraphLinksModel(nodeDataArray);
</code></pre>
<script>
goCode('gridSnapping2', 600, 150);
</script>
<h2 id="CustomGrids">Custom Grids</h2>
<p>
Grid patterns are implemented by the <a>Panel</a> class when its <a>Panel.type</a> is <a>Panel.Grid</a>. The elements of a Grid Panel must be <a>Shape</a>s
whose <a>Shape.figure</a> is one of a small set of known kinds of figures. The only figures it can accept are: "LineH", "LineV", "BarH", and "BarV". The two
"Line" figures result in stroked lines separating the grid cells; the two "Bar" figures result in filled rectangles in the grid cells.
</p>
<p>
Here is a simple grid consisting of blue horizontal lines and green vertical lines:
</p>
<pre class="lang-js" id="customBackground"><code>
diagram.grid =
new go.Panel("Grid", { gridCellSize: new go.Size(25, 25) })
.add(
new go.Shape("LineH", { stroke: "blue" }),
new go.Shape("LineV", { stroke: "green" })
);
</code></pre>
<script>
goCode('customBackground', 600, 150);
</script>
<p>
The <a>Shape.interval</a> property is also used by a Grid Panel to determine how frequently a line should be drawn. The value should be a positive integer
specifying how many cells there are between drawings of this particular line. So if you wanted darker blue and darker green lines every five cells:
</p>
<pre class="lang-js" id="customBackground2"><code>
diagram.grid =
new go.Panel("Grid", { gridCellSize: new go.Size(10, 10) })
.add(
new go.Shape("LineH", { stroke: "lightblue" }),
new go.Shape("LineV", { stroke: "lightgreen" }),
new go.Shape("LineH", { stroke: "blue", interval: 5 }),
new go.Shape("LineV", { stroke: "green", interval: 5 })
);
diagram.nodeTemplate =
new go.Node("Auto", { resizable: true })
.add(
new go.Shape("Rectangle", { fill: "lightgray" }),
new go.TextBlock({ margin: 5 })
.bind("text", "key")
);
const nodeDataArray = [
{ key: "Alpha" }
];
diagram.model = new go.GraphLinksModel(nodeDataArray);
</code></pre>
<script>
goCode('customBackground2', 600, 150);
</script>
<p>
Note that the Shapes are drawn in the order in which they appear in the Panel, so you can see that the dark blue horizontal lines are drawn in front of the
light green vertical lines, and that the dark green vertical line crosses in front of the dark blue horizontal lines.
</p>
<p>
Here is the definition of the predefined <a>Diagram.grid</a>:
</p>
<pre class="lang-js" id="standardGrid"><code>
diagram.grid =
new go.Panel("Grid", {
name: "GRID",
visible: false,
gridCellSize: new go.Size(10, 10),
gridOrigin: new go.Point(0, 0)
})
.add(
new go.Shape("LineH", { stroke: "lightgray", strokeWidth: 0.5, interval: 1 }),
new go.Shape("LineH", { stroke: "gray", strokeWidth: 0.5, interval: 5 }),
new go.Shape("LineH", { stroke: "gray", strokeWidth: 1.0, interval: 10 }),
new go.Shape("LineV", { stroke: "lightgray", strokeWidth: 0.5, interval: 1 }),
new go.Shape("LineV", { stroke: "gray", strokeWidth: 0.5, interval: 5 }),
new go.Shape("LineV", { stroke: "gray", strokeWidth: 1.0, interval: 10 })
);
diagram.grid.visible = true; // so that this example shows the standard grid
diagram.div.style.background = "white";
</code></pre>
<script>
goCode('standardGrid', 600, 150);
</script>
<p>
You can get a green-bar pattern by using the "BarH" figure. Note the use of <a>Shape.fill</a> instead of <a>Shape.stroke</a> and explicitly setting the
<a>GraphObject.height</a>:
</p>
<pre class="lang-js" id="customBackground3"><code>
diagram.grid =
new go.Panel("Grid", { gridCellSize: new go.Size(50, 50) })
.add(
new go.Shape("BarH", { fill: "lightgreen", interval: 2, height: 50 })
);
diagram.nodeTemplate =
new go.Node("Auto", {
dragComputation: (node, pt, gridpt) => {
pt.y = Math.round(pt.y/100)*100;
return pt;
}
})
.add(
new go.Shape("Rectangle", { fill: "lightgray" }),
new go.TextBlock({ margin: 5 })
.bind("text", "key")
);
const nodeDataArray = [
{ key: "Alpha" }
];
diagram.model = new go.GraphLinksModel(nodeDataArray);
</code></pre>
<script>
goCode('customBackground3', 600, 350);
</script>
<p>
This example also demonstrates how one can use the <a>Part.dragComputation</a> property to customize where the user can drag the node. In this case the
<a>Part.location</a>.y is limited to be multiples of 100, corresponding to the rows of cells filled by the green bars.
</p>
<p>
To get a tablecloth effect, one can use both vertical and horizontal bars with a translucent color:
</p>
<pre class="lang-js" id="customBackground4"><code>
diagram.grid =
new go.Panel("Grid", { gridCellSize: new go.Size(100, 100) })
.add(
new go.Shape("BarV", { fill: "rgba(255,0,0,0.1)", width: 50 }),
new go.Shape("BarH", { fill: "rgba(255,0,0,0.1)", height: 50 })
);
diagram.toolManager.draggingTool.isGridSnapEnabled = true;
diagram.nodeTemplate =
new go.Node("Auto", { width: 50, height: 50 })
.add(
new go.Shape("Rectangle", { fill: "lightgray" }),
new go.TextBlock({ margin: 5 })
.bind("text", "key")
);
const nodeDataArray = [
{ key: "Alpha" }
];
diagram.model = new go.GraphLinksModel(nodeDataArray);
</code></pre>
<script>
goCode('customBackground4', 600, 350);
</script>
<p>
This example limits dragging of all nodes by setting <a>DraggingTool.isGridSnapEnabled</a> to true.
</p>
<p>
Another custom grid: dots
</p>
<pre class="lang-js" id="customBackground5"><code>
diagram.div.style.background = "white";
diagram.grid =
new go.Panel("Grid")
.add(
new go.Shape("LineH", { strokeWidth: 1, strokeDashArray: [0, 9, 1, 0] })
);
diagram.toolManager.draggingTool.isGridSnapEnabled = true;
diagram.nodeTemplate =
new go.Node("Auto")
.add(
new go.Shape("Rectangle", { fill: "white" }),
new go.TextBlock({ margin: 5 })
.bind("text", "key")
);
const nodeDataArray = [
{ key: "Alpha" }
];
diagram.model = new go.GraphLinksModel(nodeDataArray);
</code></pre>
<script>
goCode('customBackground5', 600, 350);
</script>
<p>
Here is an example of using a "Grid" <a>Panel</a> as a regular data bound element in a <a>Node</a>:
</p>
<pre class="lang-js" id="nodeGrid"><code>
diagram.nodeTemplate =
new go.Node("Auto", { resizable: true, resizeObjectName: "GRID" })
.add(
new go.Shape("Rectangle", { fill: "transparent" }),
new go.Panel("Grid", {
name: "GRID",
desiredSize: new go.Size(100, 100),
gridCellSize: new go.Size(20, 20)
})
.bindTwoWay("desiredSize", "size", go.Size.parse, go.Size.stringify)
.bindTwoWay("gridCellSize", "cell", go.Size.parse, go.Size.stringify)
.add(
new go.Shape("LineV")
.bind("stroke"),
new go.Shape("LineH")
.bind("stroke")
)
);
diagram.model = new go.GraphLinksModel([
{ key: "Alpha", cell: "25 25", stroke: "lightgreen" },
{ key: "Beta", size: "150 75", cell: "15 30" }
]);
</code></pre>
<script>
goCode('nodeGrid', 600, 350);
</script>
<h2 id="OtherConsiderations">Other Considerations</h2>
<p>
A Grid Panel should have a non-null <code>background</code> if it needs to be pickable. One cannot set or bind the <a>Panel.itemArray</a> of a Grid Panel.
</p>
<p>
Events on the Shapes will be ignored. Shapes in a Grid Panel must not be scaled or rotated.
</p>
</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>