create-gojs-kit
Version:
A CLI for downloading GoJS samples, extensions, and docs
699 lines (657 loc) • 28.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="Pictures" />
<meta property="og:title" content="Pictures" />
<meta name="twitter:title" content="Pictures" />
<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/pictures.html" />
<meta property="twitter:url" content="https://gojs.net/latest/intro/pictures.html" />
<meta name="twitter:card" content="summary_large_image" />
<meta property="og:type" content="website" />
<meta property="twitter:domain" content="gojs.net" />
<title>
Pictures | 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>Pictures</h1>
<p>
Use the <a>Picture</a> class to display images. The most common usage is to set the <a>Picture.source</a> property with a URL string, along with the
<a>GraphObject.desiredSize</a> or the <a>GraphObject.width</a> and <a>GraphObject.height</a>.
</p>
<p>
If the URL is just a simple constant string, you can pass the string directly as an argument, rather than assign the "source:" property. Both techniques have
the same effect.
</p>
<p>
In these simplistic demonstrations, the code programmatically creates a Part and adds it to the Diagram.
Once you learn about models and data binding you will generally not create parts (nodes or links) programmatically.
</p>
<pre class="lang-js" id="source"><code>
diagram.add(
new go.Part()
.add(
new go.Picture("images/100x65.png")
));
</code></pre>
<script>
goCode('source', 600, 160);
</script>
<p>However for more sophisticated control you can set the <a>Picture.element</a> to an <b>HTMLImageElement</b> or an <b>HTMLCanvasElement</b>.</p>
<h3 id="SimpleIconsUsingFonts">Simple Icons Using Fonts</h3>
<p>
Note: for showing simple icons you may want to use an icon font.
See the example using a <a>TextBlock</a> rather than a <a>Picture</a> at:
<a href="textBlocks.html#IconFonts">Icon Fonts</a>
</p>
<h2 id="Sizing">Sizing</h2>
<p>
If you do not set the <a>GraphObject.desiredSize</a> of a <a>Picture</a>, it will get the picture's natural size. But when you set the desiredSize to be
something different than the natural size, the picture may be stretched or compressed to fit.
</p>
<p>The following pictures all show a picture of kittens that is 100x65 pixels.</p>
<ul>
<li>The first picture shows the image at its natural size.</li>
<li>The second picture also shows the image at its natural size, but has its desiredSize set explicitly.</li>
<li>The third picture increases the size of the Picture, causing the image to be expanded evenly.</li>
<li>The fourth picture squeezes the 100x65 image into a 50x32.5 space -- half size. This also maintains the original aspect ratio of the image.</li>
<li>The last picture sets the picture size to be 50x70, which changes the aspect ratio to be taller and thinner than the original.</li>
</ul>
<pre class="lang-js" id="sizedPictures"><code>
const settings = { source: "images/100x65.png", margin: 2 };
diagram.add(
new go.Part("Table")
.add(
new go.Picture(settings)
.set({ column: 0, /* natural width and height */ }),
new go.TextBlock("natural", { row: 1, column: 0 }),
new go.Picture(settings)
.set({ column: 1, width: 100, height: 65 }),
new go.TextBlock("same size", { row: 1, column: 1 }),
new go.Picture(settings)
.set({ column: 2, width: 200, height: 130 }),
new go.TextBlock("bigger", { row: 1, column: 2 }),
new go.Picture(settings)
.set({ column: 3, width: 50, height: 32.5 }),
new go.TextBlock("smaller", { row: 1, column: 3 }),
new go.Picture(settings)
.set({ column: 4, width: 50, height: 70 }),
new go.TextBlock("stretched", { row: 1, column: 4 })
));
</code></pre>
<script>
goCode('sizedPictures', 600, 160);
</script>
<p>
Note that it may take a while for the media to load. Until the time that the media has loaded sufficiently to know its natural size, the Picture may have the
wrong size, such as 0x0. We recommend that you specify the desiredSize (or width and height) so that the Panel(s) holding the Picture will not have to
rearrange themselves once the media has loaded.
</p>
<p>However for the times when you cannot know the natural size ahead of time, there are alternative ways of stretching images to fit in a given space.</p>
<h2 id="ImageStretch">Image Stretch</h2>
<p>
Instead of always stretching or compressing to fill the desiredSize, you can set the <a>Picture.imageStretch</a> property to control the size and aspect ratio
of the drawn image.
</p>
<p>
The following pictures demonstrate the four possible values for Picture.imageStretch. All four Pictures here have the size 60x80 and show the same 100x65 PNG
file. The Pictures also have a light green background, to show the space available that may be left unused, but is still part of the Picture's bounds.
</p>
<ul>
<li>
The first picture demonstrates the default behavior, to stretch in both directions. Note how the image is distorted to be narrower than it should be.
However, all of the image is shown. Because the image fills the whole area and the image is not translucent, the background color does not show anywhere.
</li>
<li>
You can see in the second picture, using an imageStretch of <a>ImageStretch.None</a>, how it only shows a fraction of the whole kitten image. Because the
desiredSize is smaller than the natural size of the image, parts of the image are clipped.
</li>
<li>
The third picture shows how a <a>ImageStretch.Uniform</a> imageStretch will make sure that all of the image is shown, at the expense of reducing the scale
and leaving some empty space at the sides or at the top and bottom. In this case, because the natural image aspect ratio is wider than the available 60x80
aspect ratio, the empty space will be at the top and bottom.
</li>
<li>
The fourth picture shows how a <a>ImageStretch.UniformToFill</a> imageStretch will ensure that the whole area is occupied with image, but that not all of
the image is shown, since some may be clipped at the sides or at the top and bottom. Such images normally have a larger scale than when using Uniform
imageStretch. In this case what must be clipped is at the sides of the image.
</li>
<li>Finally there is a separate Part containing the original image, sized naturally, for comparison.</li>
</ul>
<pre class="lang-js" id="stretchedPictures"><code>
const settings = { width: 60, height: 80, margin: 2, background: "lime" };
diagram.add(
new go.Part("Table")
.add(
new go.Picture("images/100x65.png", {
row: 0, column: 0, imageStretch: go.ImageStretch.Fill /* the default value */
})
.set(settings),
new go.TextBlock("Fill", { row: 1, column: 0 }),
new go.Picture("images/100x65.png", {
row: 0, column: 1, imageStretch: go.ImageStretch.None
})
.set(settings),
new go.TextBlock("None", { row: 1, column: 1 }),
new go.Picture("images/100x65.png", {
row: 0, column: 2, imageStretch: go.ImageStretch.Uniform
})
.set(settings),
new go.TextBlock("Uniform", { row: 1, column: 2 }),
new go.Picture("images/100x65.png", {
row: 0, column: 3, imageStretch: go.ImageStretch.UniformToFill
})
.set(settings),
new go.TextBlock("UniformToFill", { row: 1, column: 3 })
));
// The original image sized naturally, for comparison
diagram.add(
new go.Part("Vertical")
.add(
new go.Picture("images/100x65.png"),
new go.TextBlock("Original image,\nsized naturally")
));
</code></pre>
<script>
goCode('stretchedPictures', 600, 120);
</script>
<p>When images are clipped you can control what part of the image is drawn by using the <a>Picture.imageAlignment</a> property.</p>
<h2 id="Clipping">Clipping</h2>
<p>
If you have a Picture that must be clipped to a geometry, such as to produce a circular image, there are two options.
The first is to use a "frame" geometry to hide part of the image.
Typically this frame is the same color as the Diagram background or the background of the Node.
This method does not change the area of the Picture, does not allow for true transparency,
and clicking anywhere in the bounds will always pick the picture.
</p>
<p>
A second method uses <a>Panel.isClipping</a>.
This property on a "Spot" Panel allows the filled area of the main Shape to serve as a clipping region instead
of a drawn shape. This method does not change the area of the Picture, but does allow for transparency.
It affects object picking so that only the resultant drawn area is pickable;
areas of the image that are not drawn cannot be "hit".
</p>
<p>Examples of both follow:</p>
<pre class="lang-js" id="clipPictures"><code>
diagram.layout = new go.GridLayout();
// Using a black "frame" geometry to hide part of the image.
// Typically this frame is the same color as the Diagram background or the background of the Node.
diagram.add(
new go.Part("Spot", { scale: 2 })
.add(
new go.Picture("../samples/images/55x55.png", {
width: 55, height: 55,
background: 'red'
}),
new go.Shape({
width: 55, height: 55,
geometryString: "f M0 0 L100 0 L100 100 L0 100 z M5,50a45,45 0 1,0 90,0a45,45 0 1,0 -90,0 z",
fill: 'black', strokeWidth: 0
})
)
);
// Using Panel.isClipping
diagram.add(
new go.Part("Spot", { scale: 2, isClipping: true })
.add(
new go.Shape("Circle", { width: 55, strokeWidth: 0 } ),
new go.Picture("../samples/images/55x55.png",
{ width: 55, height: 55 })
)
);
// Using Panel.isClipping and also having a surrounding border behind it
diagram.add(
new go.Part("Spot", { scale: 2 })
.add(
// the background border
new go.Shape("Circle", { width: 65, strokeWidth: 0, fill: 'red' } ),
// the same clipping panel as the second example, above
new go.Panel("Spot", { isClipping: true })
.add(
new go.Shape("Circle", { width: 55, strokeWidth: 0 } ),
new go.Picture("../samples/images/55x55.png",
{ width: 55, height: 55 })
)
)
);
</code></pre>
<script>
goCode('clipPictures', 500, 200);
</script>
<h2 id="Flipping">Flipping</h2>
<p>You can flip image sources horizontally and vertically with the <a>Picture.flip</a> property:</p>
<pre class="lang-js" id="flipPictures"><code>
const settings = { source: "images/100x65.png", margin: 2 };
diagram.add(
new go.Part("Table")
.add(
new go.Picture(settings)
.set({ column: 0, flip: go.Flip.None /* the default value */ }),
new go.TextBlock("None (default)", { row: 1, column: 0 }),
new go.Picture(settings)
.set({ column: 1, flip: go.Flip.Horizontal }),
new go.TextBlock("FlipHorizontal", { row: 1, column: 1 }),
new go.Picture(settings)
.set({ column: 2, flip: go.Flip.Vertical }),
new go.TextBlock("FlipVertical", { row: 1, column: 2 }),
new go.Picture(settings)
.set({ column: 3, flip: go.Flip.Both }),
new go.TextBlock("FlipBoth", { row: 1, column: 3 })
));
</code></pre>
<script>
goCode('flipPictures', 600, 160);
</script>
<h2 id="CrossOriginPictures">Cross Origin Pictures</h2>
<p>
Since Pictures are backed by HTMLImageElements, they must abide by the same Cross-origin (CORS) rules that apply to Images. If you are using images that apply
to CORS rules, you may need to set the <a>Picture.sourceCrossOrigin</a> property to a function that returns an appropriate value. If
<code>sourceCrossOrigin</code> is supplied, the value returned by the function is used as the value of any constructed <code>image.crossOrigin</code>.
Example:
</p>
<pre class="lang-js"><code>
new go.Picture({
width: 64, height: 64,
sourceCrossOrigin: pict => "use-credentials"
})
.bind("source", "path")
</code></pre>
<p>
Common values to return are "use-credentials" and "anonymous", but other situations may call for other values or conditional values. We suggest researching
<a href="https://enable-cors.org/">cross-origin resource sharing</a> to determine what is right for your situation.
</p>
<p>
If you are using <a>Diagram.makeImage</a>, <a>Diagram.makeImageData</a>, or <a>Diagram.makeSvg</a>, and you are seeing blank or missing images, CORS-related
problems are the first thing to investigate.
</p>
<h2 id="UsingSVGAsPictureSource">Using SVG as a Picture source</h2>
<p>
Almost all browsers accept SVG files as a Picture source, but in many browsers you <strong>must</strong>
assign width and height attributes to the SVG element. These values should be integers.
</p>
<p>
This first SVG file has a width and height specified in its SVG element, and also has its desired size set.
It should display in most browsers:
</p>
<pre class="lang-html"><code>
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="580" height="580">
...
</code></pre>
<pre class="lang-js" id="svg1"><code>
diagram.add(
new go.Part()
.add(
new go.Picture({ width: 580, height: 580, source: "images/tiger.svg" })
));
diagram.scale = 0.5;
</code></pre>
<script>
goCode('svg1', 300, 300);
</script>
<p style="color: red">
<strong> The following SVG file does not specify width and height attributes in its SVG element,
and as a result some browsers may not render it correctly: </strong>
</p>
<pre class="lang-html"><code>
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
...
</code></pre>
<pre class="lang-js" id="svg2"><code>
diagram.add(
new go.Part()
.add(
// missing width and height:
new go.Picture({ source: "images/tiger-noWidthHeightSpecified.svg" })
));
diagram.scale = 0.5;
</code></pre>
<script>
goCode('svg2', 300, 300);
</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>