create-gojs-kit
Version:
A CLI for downloading GoJS samples, extensions, and docs
395 lines (345 loc) • 16.3 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="Zooming only changes the distance between nodes, not the apparent size of each node." />
<meta itemprop="description" content="Zooming only changes the distance between nodes, not the apparent size of each node." />
<meta property="og:description" content="Zooming only changes the distance between nodes, not the apparent size of each node." />
<meta name="twitter:description" content="Zooming only changes the distance between nodes, not the apparent size of each node." />
<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="Zooming Changes Spaces Between Nodes without Changing Scale" />
<meta property="og:title" content="Zooming Changes Spaces Between Nodes without Changing Scale" />
<meta name="twitter:title" content="Zooming Changes Spaces Between Nodes without Changing Scale" />
<meta property="og:image" content="https://gojs.net/latest/assets/images/screenshots/minimal.png" />
<meta itemprop="image" content="https://gojs.net/latest/assets/images/screenshots/minimal.png" />
<meta name="twitter:image" content="https://gojs.net/latest/assets/images/screenshots/minimal.png" />
<meta property="og:url" content="https://gojs.net/latest/samples/spacingZoom.html" />
<meta property="twitter:url" content="https://gojs.net/latest/samples/spacingZoom.html" />
<meta name="twitter:card" content="summary_large_image" />
<meta property="og:type" content="website" />
<meta property="twitter:domain" content="gojs.net" />
<title>
Zooming Changes Spaces Between Nodes without Changing Scale | 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 id="code">
// The custom CommandHandler that avoids changing the Diagram.scale
class SpacingCommandHandler extends go.CommandHandler {
constructor(init) {
super();
this._space = 1.0; // replaces Diagram.scale; also copied to/from Model.modelData.space
this._spaceCenter = new go.Point(0, 0); // not currently used -- should this be saved on modelData too?
this._isYSpaced = true; // scale Y along with X? This option is just for demonstration purposes.
this._isUpdating = false;
if (init) Object.assign(this, init);
}
// Overrides of commands that scale the diagram -- change the space instead
decreaseZoom(factor) {
if (factor === undefined /*notpresent*/) factor = 1.0 / this.zoomFactor;
this.setSpace(this.space * factor);
}
canDecreaseZoom(factor) {
if (factor === undefined /*notpresent*/) factor = 1.0 / this.zoomFactor;
return this.checkSpace(this.space * factor);
}
increaseZoom(factor) {
if (factor === undefined /*notpresent*/) factor = 1.0 / this.zoomFactor;
this.setSpace(this.space / factor);
}
canIncreaseZoom(factor) {
if (factor === undefined /*notpresent*/) factor = 1.0 / this.zoomFactor;
return this.checkSpace(this.space / factor);
}
resetZoom(newspace) {
if (newspace === undefined /*notpresent*/) newspace = 1.0;
this.setSpace(newspace);
}
canResetZoom(newspace) {
return this.checkSpace(newspace);
}
// actually set a new value for SPACE
setSpace(s) {
this.space = Math.max(0.1, Math.min(10.0, s));
}
// validity check for a new value for SPACE
checkSpace(s) {
return 0.1 <= s && s <= 10.0;
}
// Properties for SpacingCommandHandler
get space() {
return this._space;
}
set space(val) {
if (val !== this._space) {
this._space = val;
const diagram = this.diagram;
if (diagram !== null) {
// store in model too, and support undo
diagram.model.setDataProperty(diagram.model.modelData, 'space', val);
}
this.updateAllLocations();
// update the page showing the current value
document.getElementById('SPACE').textContent = val.toString();
}
}
get spaceCenter() {
return this._spaceCenter;
}
set spaceCenter(val) {
if (!val.equals(this._spaceCenter)) {
this._spaceCenter = val.copy();
}
}
get isYSpaced() {
return this._isYSpaced;
}
set isYSpaced(val) {
if (val !== this._isYSpaced) {
this._isYSpaced = val;
this.updateAllLocations();
}
}
// If the spacing or isYSpaced properties change value,
// we need to update the effective locations of all nodes.
// Assume Node.location is data bound to "loc" property.
updateAllLocations() {
const diagram = this.diagram;
if (diagram === null) return;
this._isUpdating = true;
diagram.skipsUndoManager = true;
diagram.startTransaction('respace nodes');
diagram.parts.each(p => p.updateTargetBindings('loc'));
diagram.nodes.each(n => n.updateTargetBindings('loc'));
diagram.commitTransaction('respace nodes');
diagram.skipsUndoManager = false;
this._isUpdating = false;
}
}
// end SpacingCommandHandler class
function init() {
myDiagram = new go.Diagram('myDiagramDiv', {
// create a Diagram for the DIV HTML element
commandHandler: new SpacingCommandHandler(),
// update the SpacingCommandHandler.space from the model at the end of each transaction
ModelChanged: e => {
if (e.isTransactionFinished) {
myDiagram.commandHandler.space = myDiagram.model.modelData.space;
}
},
'undoManager.isEnabled': true // enable undo & redo
});
// Define a simple Node template that cannot be shared with other Diagrams,
// because of the use of the Node.location Binding conversion functions.
// The SpacingCommandHandler also assumes the Node.location is bound to the data property named "loc".
myDiagram.nodeTemplate =
new go.Node('Auto') // the Shape will go around the TextBlock
.bindTwoWay('location', 'loc', spacedLocationParse, spacedLocationStringify)
.add(
new go.Shape('RoundedRectangle', { strokeWidth: 0 })
// Shape.fill is bound to Node.data.color
.bind('fill', 'color'),
new go.TextBlock({ margin: 8 }) // some room around the text
// TextBlock.text is bound to Node.data.text
.bind('text')
);
// but use the default Link template, by not setting Diagram.linkTemplate
// create the model data that will be represented by Nodes and Links
myDiagram.model = new go.GraphLinksModel(
[
{ key: 1, text: 'Alpha', color: 'lightblue', loc: '0 0' },
{ key: 2, text: 'Beta', color: 'orange', loc: '100 0' },
{ key: 3, text: 'Gamma', color: 'lightgreen', loc: '0 100' },
{ key: 4, text: 'Delta', color: 'pink', loc: '100 100' }
],
[
{ from: 1, to: 2 },
{ from: 1, to: 3 },
{ from: 2, to: 2 },
{ from: 3, to: 4 },
{ from: 4, to: 1 }
]
);
// the "space" property is kept on the Model.modelData too
myDiagram.model.modelData.space = 1.0;
}
// Conversion functions -- these only work with myDiagram, assuming it uses a SpacingCommandHandler
function spacedLocationParse(str) {
const cmd = myDiagram.commandHandler;
if (!(cmd instanceof SpacingCommandHandler)) throw new Error('not using SpacingCommandHandler');
const pt = go.Point.parse(str);
pt.x = (pt.x - cmd.spaceCenter.x) * cmd.space + cmd.spaceCenter.x;
if (cmd.isYSpaced) {
pt.y = (pt.y - cmd.spaceCenter.y) * cmd.space + cmd.spaceCenter.y;
}
return pt;
}
function spacedLocationStringify(pt, data) {
const cmd = myDiagram.commandHandler;
if (!cmd._isUpdating) {
pt = pt.copy();
pt.x = (pt.x - cmd.spaceCenter.x) / cmd.space + cmd.spaceCenter.x;
if (cmd.isYSpaced) {
pt.y = (pt.y - cmd.spaceCenter.y) / cmd.space + cmd.spaceCenter.y;
}
return go.Point.stringify(pt);
} else {
return data.loc;
}
}
function onIsYSpacedToggled() {
myDiagram.commandHandler.isYSpaced = !myDiagram.commandHandler.isYSpaced;
}
window.addEventListener('DOMContentLoaded', init);
</script>
<div id="sample">
<div id="myDiagramDiv" style="border: solid 1px black; width: 400px; height: 400px"></div>
Spacing factor: <span id="SPACE">1.0</span>
<br />
<label><input type="checkbox" onclick="onIsYSpacedToggled()" checked="checked" />Is Y Axis Spaced?</label>
<br />
<p>
Click in the diagram and then try zooming in and out of the diagram by using control-mouse-wheel. If you don't want to hold down the control key, click the
mouse wheel button in the diagram to toggle between mouse wheel events zooming instead of scrolling.
</p>
<p>
This diagram uses a custom <a>CommandHandler</a> to replace the standard zooming behavior. The <a>CommandHandler.decreaseZoom</a>,
<a>CommandHandler.increaseZoom</a>, and <a>CommandHandler.resetZoom</a> commands no longer change the <a>Diagram.scale</a>. Instead they change the
effective <a>Part.location</a> for all of the <a>Node</a>s by changing the value of the conversion function that is getting the location from the "loc"
property on the node data.
</p>
<p>
As the value of SpacingCommandHandler.space changes, all of the Bindings on "loc" are re-evaluated, causing the nodes to get new locations. The value of the
"loc" data property remains unchanged by the Binding. However the TwoWay Binding does cause the "loc" data property to be modified when the user drags a
node.
</p>
<p>
The conversion functions also depend on the SpacingCommandHandler.isYSpaced property, which can be toggled by the checkbox. When false the conversion
functions do not space along the Y axis, but only along the X axis.
</p>
<p>
Because the conversion functions are dependent on the particular Diagram, and because the node template depends on the conversion functions, the template
cannot be used by other Diagrams.
</p>
<p>
The SpacingCommandHandler.space property is duplicated on the <a>Model.modelData</a> object, both so that the information is saved in the model as well as
to support undo/redo.
</p>
</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 -->
<h4>Commands</h4>
<p>
A <a href="../api/symbols/CommandHandler.html" target="api">CommandHandler</a> handles all default keyboard input events in a Diagram.
There are many predefined methods on <a>CommandHandler</a> that implement common commands to operate on the Diagram or the current <a>Diagram.selection></a>.
</p>
<p>
You can override <a>CommandHandler.doKeyDown</a> to handle additional keyboard shortcuts or to change which commands are invoked via the keyboard.
<p>
Your code can invoke a command by calling the appropriate method on the <a>Diagram.commandHandler</a>.
Each command method has a corresponding <b>can...</b> predicate that your code can use to enable or disable any buttons that invoke the command.
Your code can customize the behavior of a command by overriding the method on <a>CommandHandler</a>,
or by setting properties on the <a>CommandHandler</a> or <a>Diagram</a> or <a>Part</a>s --
see <a href="../intro/permissions.html">GoJS Permissions</a>.
</p>
<p>
There are several CommandHandler extensions that provide additional functionality.
<p>
More information can be found in the <a href="../intro/commands.html">GoJS Intro</a>.
</p>
<p>
<a href="../samples/index.html#commands">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>