Here’s what I’ve got so far:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Scrolling Groups</title>
<!-- Copyright 1998-2018 by Northwoods Software Corporation. -->
<meta charset="UTF-8">
<script src="go.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script src="../extensions/ScrollingTable.js"></script> <!-- to define AutoRepeatButton -->
<script id="code">
function ScrollingGroupLayout() {
go.Layout.call(this);
this._topIndex = 0;
this._spacing = 10;
this._count = 0;
this._lastIndex = 0;
this._padding = 30;
}
go.Diagram.inherit(ScrollingGroupLayout, go.Layout);
ScrollingGroupLayout.prototype.cloneProtected = function(copy) {
go.Layout.prototype.cloneProtected.call(this, copy);
copy._topIndex = this._topIndex;
copy._spacing = this._spacing;
};
Object.defineProperty(ScrollingGroupLayout.prototype, "topIndex", {
get: function() { return this._topIndex; },
set: function(val) {
if (typeof val !== "number") throw new Error("new value for ScrollingGroupLayout.topIndex is not a number: " + val);
if (this._topIndex !== val) {
this._topIndex = val;
this.invalidateLayout();
}
}
});
Object.defineProperty(ScrollingGroupLayout.prototype, "spacing", {
get: function() { return this._spacing; },
set: function(val) {
if (typeof val !== "number") throw new Error("new value for ScrollingGroupLayout.spacing is not a number: " + val);
if (this._spacing !== val) {
this._spacing = val;
this.invalidateLayout();
}
}
});
Object.defineProperty(ScrollingGroupLayout.prototype, "count", {
get: function() { return this._count; }
});
Object.defineProperty(ScrollingGroupLayout.prototype, "lastIndex", {
get: function() { return this._lastIndex; }
});
ScrollingGroupLayout.prototype.doLayout = function(coll) {
var diagram = this.diagram;
var group = this.group;
if (group === null) throw new Error("ScrollingGroupLayout must be a Group.layout, not a Diagram.layout");
if (diagram !== null) diagram.startTransaction("Scrolling Group Layout");
this.arrangementOrigin = this.initialOrigin(this.arrangementOrigin);
var arr = [];
// can't use Layout.collectParts here, because we're intentionally making some
// member nodes not visible, which would normally prevent them from being laid out
var it = group.memberParts.iterator;
while (it.next()) {
var part = it.value;
if (part instanceof go.Link) continue;
part.ensureBounds();
arr.push(part);
}
this._count = arr.length;
// sort by their initial Y position, allowing users to drag nodes for re-ordering them
var comparer = function(a, b) {
return a.actualBounds.y - b.actualBounds.y;
};
arr.sort(comparer);
var body = group.findObject("BODY");
if (!body) body = group.placeholder;
if (!body) throw new Error("no BODY element or Placeholder in Group");
var x = this.arrangementOrigin.x + body.actualBounds.width/2;
var y = this.arrangementOrigin.y + this._padding;
var maxy = y + body.height - this._padding;
var i = 0;
var last = -1;
while (i < arr.length && i < this.topIndex) {
var part = arr[i++];
part.visible = false;
part.location = new go.Point(x, y);
}
while (i < arr.length && y < maxy) {
var part = arr[i++];
part.location = new go.Point(x, y + part.actualBounds.height/2);
if (y + part.actualBounds.height < maxy) {
part.visible = true;
y += part.actualBounds.height + this.spacing;
last = i - 1;
} else {
part.visible = false;
break;
}
}
while (i < arr.length) {
var part = arr[i++];
part.visible = false;
part.location = new go.Point(x, y);
}
this._lastIndex = last;
var up = group.findObject("UP");
if (up !== null) up.visible = this.lastIndex < this.count - 1;
var down = group.findObject("DOWN");
if (down !== null) down.visible = this.topIndex > 0;
if (diagram !== null) diagram.commitTransaction("Scrolling Group Layout");
};
// end of ScrollingGroupLayout
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make;
myDiagram =
$(go.Diagram, "myDiagramDiv", // create a Diagram for the DIV HTML element
{
layout: $(go.GridLayout, { wrappingWidth: Infinity, spacing: new go.Size(0, 0), cellSize: new go.Size(1, 1) }),
"SelectionMoved": function(e) {
e.subject.each(function(p) {
if (p.containingGroup !== null) p.containingGroup.layout.invalidateLayout();
});
},
"PartResized": function(e) {
var part = e.subject.part;
if (part instanceof go.Group && part.layout instanceof ScrollingGroupLayout) {
part.layout.invalidateLayout();
}
},
"toolManager.doMouseWheel": function() {
var grp = null;
this.diagram.findTopLevelGroups().each(function(g) {
if (g.actualBounds.containsPoint(g.diagram.lastInput.documentPoint)) grp = g;
});
if (grp !== null) {
var lay = grp.layout;
if (lay instanceof ScrollingGroupLayout) {
if (this.diagram.lastInput.delta > 0) {
if (lay.lastIndex < lay.count - 1) {
lay.topIndex++;
}
} else {
if (lay.topIndex > 0) {
lay.topIndex--;
}
}
}
} else {
this.standardMouseWheel();
}
this.diagram.lastInput.bubbles = false;
}
});
myDiagram.nodeTemplate =
$(go.Node, go.Panel.Auto,
{
locationSpot: go.Spot.Center,
minLocation: new go.Point(NaN, -Infinity),
maxLocation: new go.Point(NaN, Infinity),
containingGroupChanged: function(n) {
if (n.containingGroup.key === "GROUP 3") {
n.minLocation = new go.Point(-Infinity, -Infinity);
n.maxLocation = new go.Point(Infinity, Infinity);
n.layerName = "Background";
} else {
n.minLocation = new go.Point(NaN, -Infinity);
n.maxLocation = new go.Point(NaN, Infinity);
n.layerName = "";
}
}
},
new go.Binding("location", "loc", go.Point.parse),
$(go.Shape, { fill: "white" },
new go.Binding("fill", "color")),
$(go.TextBlock, { margin: 6 },
new go.Binding("text", "key")));
myDiagram.groupTemplate =
$(go.Group, "Vertical",
{
selectable: false,
resizeObjectName: "BODY", // also used by ScrollingGroupLayout
computesBoundsIncludingLocation: true,
layout: $(ScrollingGroupLayout)
},
$(go.Panel, "Auto",
{ name: "BODY", height: 310, width: 120 },
$(go.Shape, { fill: "lightyellow", stroke: "yellow" }),
$("AutoRepeatButton",
{ name: "UP", alignment: go.Spot.TopRight },
$(go.Shape, "TriangleUp", { width: 6, height: 6 }),
{
click: function(e, button) {
var group = button.part;
var lay = group.layout;
if (lay.lastIndex < lay.count - 1) {
lay.topIndex++;
}
}
}),
$("AutoRepeatButton",
{ name: "DOWN", alignment: go.Spot.BottomRight },
$(go.Shape, "TriangleDown", { width: 6, height: 6 }),
{
click: function(e, button) {
var group = button.part;
var lay = group.layout;
if (lay.topIndex > 0) {
lay.topIndex--;
}
}
})
),
$(go.TextBlock, new go.Binding("text", "key"))
);
myDiagram.groupTemplateMap.add("Main",
$(go.Group, "Vertical",
{
selectable: false,
computesBoundsIncludingLocation: true,
layerName: "Background"
},
$(go.Shape, { name: "BODY", height: 310, width: 300, fill: "lightpink", strokeWidth: 0.5 }),
$(go.TextBlock, new go.Binding("text", "key"))
));
myDiagram.linkTemplate =
$(go.Link,
{
layerName: "Foreground",
routing: go.Link.Orthogonal, corner: 10,
fromSpot: go.Spot.Right, toSpot: go.Spot.Left
},
$(go.Shape),
$(go.Shape, { toArrow: "Standard"})
);
var model = new go.GraphLinksModel();
model.nodeDataArray = [
{ key: "GROUP", isGroup: true },
{ key: "Alpha", color: "coral", group: "GROUP" },
{ key: "Beta", color: "tomato", group: "GROUP" },
{ key: "Gamma", color: "goldenrod", group: "GROUP" },
{ key: "Delta", color: "orange", group: "GROUP" },
{ key: "Epsilon", color: "coral", group: "GROUP" },
{ key: "Zeta", color: "tomato", group: "GROUP" },
{ key: "Eta", color: "goldenrod", group: "GROUP" },
{ key: "Theta", color: "orange", group: "GROUP" },
{ key: "Iota", color: "coral", group: "GROUP" },
{ key: "Kappa", color: "tomato", group: "GROUP" },
{ key: "Lambda", color: "goldenrod", group: "GROUP" },
{ key: "Mu", color: "orange", group: "GROUP" },
{ key: "Nu", color: "coral", group: "GROUP" },
{ key: "GROUP 2", isGroup: true },
{ key: "Xi", color: "tomato", group: "GROUP 2" },
{ key: "Omicron", color: "goldenrod", group: "GROUP 2" },
{ key: "Pi", color: "orange", group: "GROUP 2" },
{ key: "Rho", color: "coral", group: "GROUP 2" },
{ key: "Sigma", color: "tomato", group: "GROUP 2" },
{ key: "Tau", color: "goldenrod", group: "GROUP 2" },
{ key: "Upsilon", color: "orange", group: "GROUP 2" },
{ key: "Phi", color: "coral", group: "GROUP 2" },
{ key: "Chi", color: "tomato", group: "GROUP 2" },
{ key: "Psi", color: "goldenrod", group: "GROUP 2" },
{ key: "Omega", color: "orange", group: "GROUP 2" },
{ key: "GROUP 3", isGroup: true, category: "Main" },
{ key: "Main 1", color: "lightgreen", group: "GROUP 3", loc: "140 50" },
{ key: "Main 2", color: "lightgreen", group: "GROUP 3", loc: "70 150" },
{ key: "Main 3", color: "lightgreen", group: "GROUP 3", loc: "200 250" },
{ key: "Main 4", color: "lightgreen", group: "GROUP 3", loc: "160 100" },
{ key: "GROUP 4", isGroup: true },
{ key: "Alpha", color: "coral", group: "GROUP 4" },
{ key: "Beta", color: "tomato", group: "GROUP 4" },
{ key: "Gamma", color: "goldenrod", group: "GROUP 4" },
{ key: "Delta", color: "orange", group: "GROUP 4" },
{ key: "Epsilon", color: "coral", group: "GROUP 4" },
{ key: "Zeta", color: "tomato", group: "GROUP 4" },
{ key: "Eta", color: "goldenrod", group: "GROUP 4" },
{ key: "Theta", color: "orange", group: "GROUP 4" },
{ key: "Iota", color: "coral", group: "GROUP 4" },
{ key: "Kappa", color: "tomato", group: "GROUP 4" },
{ key: "Lambda", color: "goldenrod", group: "GROUP 4" },
{ key: "Mu", color: "orange", group: "GROUP 4" },
{ key: "Nu", color: "coral", group: "GROUP 4" },
{ key: "GROUP 5", isGroup: true },
{ key: "Xi", color: "tomato", group: "GROUP 5" },
{ key: "Omicron", color: "goldenrod", group: "GROUP 5" },
{ key: "Pi", color: "orange", group: "GROUP 5" },
{ key: "Rho", color: "coral", group: "GROUP 5" },
{ key: "Sigma", color: "tomato", group: "GROUP 5" },
{ key: "Tau", color: "goldenrod", group: "GROUP 5" },
{ key: "Upsilon", color: "orange", group: "GROUP 5" },
{ key: "Phi", color: "coral", group: "GROUP 5" },
{ key: "Chi", color: "tomato", group: "GROUP 5" },
{ key: "Psi", color: "goldenrod", group: "GROUP 5" },
{ key: "Omega", color: "orange", group: "GROUP 2" }
];
model.linkDataArray = [
{ from: "Beta", to: "Pi" },
{ from: "Gamma", to: "Psi" },
{ from: "Psi", to: "Main 2" },
{ from: "Main 2", to: "Main 3" },
{ from: "Main 3", to: "Kappa2" },
{ from: "Kappa2", to: "Rho2" }
];
myDiagram.model = model;
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:500px; min-width: 200px"></div>
</div>
</body>
</html>
The user can scroll each of the four "ScrollingGroup"s on the sides (i.e. not the middle group) either via the "AutoRepeatButton"s or by scrolling the mouse wheel while the mouse pointer is within the group.
The layout of those four groups is implemented by the custom ScrollingGroupLayout
, which is like GridLayout in putting everything in a single column, but it acts like a view onto a list by keeping track of the first visible item and how many items fit into the “BODY” element of the Group. It hides the rest of the items. It also allows the user to change the order by drag-and-drop.
The whole Diagram will need a new custom replacement PanningTool that supports scrolling individually in those "ScrollingGroup"s.
There also needs to be a “ViewportBoundsChanged” DiagramEvent listener that adjusts the height of each of the groups so that it matches the height of the viewport. The details for what you want to do as the viewport changes size or aspect ratio weren’t clear to me, but that is something you can do, along with customizing the templates.