Is it possible to modify this layout here to work with a LayeredDigraphLayout?
Well, in this forum there’s:
The basic idea is to override LayeredDigraphLayout.commitLayers to arrange the layers the way that you want.
LayeredDigraphLayout | GoJS API
Here’s the basic code:
<!DOCTYPE html>
<html>
<head>
<title>Layer Bands using a Background Part</title>
<!-- Copyright 1998-2021 by Northwoods Software Corporation. -->
<meta name="description" content="Showing bands for the layers in a diagram.">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="go.js"></script>
<script id="code">
// this controls whether the layout is horizontal and the layer bands are vertical, or vice-versa:
var HORIZONTAL = true; // this constant parameter can only be set here, not dynamically
// Perform a LayeredDigraphLayout where commitLayers is overridden to modify the background Part whose key is "_BANDS".
function BandedLDLayout() {
go.LayeredDigraphLayout.call(this);
}
go.Diagram.inherit(BandedLDLayout, go.LayeredDigraphLayout);
BandedLDLayout.prototype.assignLayers = function() {
go.LayeredDigraphLayout.prototype.assignLayers.call(this);
var maxlayer = this.maxLayer;
// now assign specific layers
var it = this.network.vertexes.iterator;
while (it.next()) {
var v = it.value;
if (v.node !== null) {
var lay = v.node.data.layer;
if (typeof lay === "number" && lay >= 0 && lay <= maxlayer) {
v.layer = lay;
}
}
}
};
BandedLDLayout.prototype.commitLayers = function(layerRects, offset) {
// update the background object holding the visual "bands"
var bands = this.diagram.findPartForKey("_BANDS");
if (bands) {
var model = this.diagram.model;
bands.location = this.arrangementOrigin.copy().add(offset);
// make each band visible or not, depending on whether there is a layer for it
for (var it = bands.elements; it.next(); ) {
var idx = it.key;
var elt = it.value; // the item panel representing a band
elt.visible = idx < layerRects.length;
}
// set the bounds of each band via data binding of the "bounds" property
var arr = bands.data.itemArray;
for (var i = 0; i < layerRects.length; i++) {
var itemdata = arr[i];
if (itemdata) {
model.setDataProperty(itemdata, "bounds", layerRects[i]);
}
}
}
};
// end BandedLDLayout
function init() {
var $ = go.GraphObject.make;
myDiagram = $(go.Diagram, "myDiagramDiv",
{
layout: $(BandedLDLayout,
{
direction: HORIZONTAL ? 0 : 90
}), // custom layout is defined above
"undoManager.isEnabled": true
});
myDiagram.nodeTemplate =
$(go.Node, go.Panel.Auto,
$(go.Shape, "Rectangle",
{ fill: "white" }),
$(go.TextBlock, { margin: 5 },
new go.Binding("text", "key")));
// There should be at most a single object of this category.
// This Part will be modified by BandedLDLayout.commitLayers to display visual "bands"
// where each "layer" is a layer of the tree.
// This template is parameterized at load time by the HORIZONTAL parameter.
// You also have the option of showing rectangles for the layer bands or
// of showing separator lines between the layers, but not both at the same time,
// by commenting in/out the indicated code.
myDiagram.nodeTemplateMap.add("Bands",
$(go.Part, "Position",
new go.Binding("itemArray"),
{
isLayoutPositioned: false, // but still in document bounds
locationSpot: new go.Spot(0, 0, HORIZONTAL ? 0 : 16, HORIZONTAL ? 16 : 0), // account for header height
layerName: "Background",
pickable: false,
selectable: false,
itemTemplate:
$(go.Panel, HORIZONTAL ? "Vertical" : "Horizontal",
new go.Binding("position", "bounds", function(b) { return b.position; }),
$(go.TextBlock,
{
angle: HORIZONTAL ? 0 : 270,
textAlign: "center",
wrap: go.TextBlock.None,
font: "bold 11pt sans-serif",
background: $(go.Brush, "Linear", { 0: "aqua", 1: go.Brush.darken("aqua") })
},
new go.Binding("text"),
// always bind "width" because the angle does the rotation
new go.Binding("width", "bounds", function(r) { return HORIZONTAL ? r.width : r.height; })
),
// option 1: rectangular bands:
$(go.Shape,
{ stroke: null, strokeWidth: 0 },
new go.Binding("desiredSize", "bounds", function(r) { return r.size; }),
new go.Binding("fill", "itemIndex", function(i) { return i % 2 == 0 ? "whitesmoke" : go.Brush.darken("whitesmoke"); }).ofObject())
// option 2: separator lines:
//(HORIZONTAL
// ? $(go.Shape, "LineV",
// { stroke: "gray", alignment: go.Spot.TopLeft, width: 1 },
// new go.Binding("height", "bounds", function(r) { return r.height; }),
// new go.Binding("visible", "itemIndex", function(i) { return i > 0; }).ofObject())
// : $(go.Shape, "LineH",
// { stroke: "gray", alignment: go.Spot.TopLeft, height: 1 },
// new go.Binding("width", "bounds", function(r) { return r.width; }),
// new go.Binding("visible", "itemIndex", function(i) { return i > 0; }).ofObject())
//)
)
}
));
myDiagram.linkTemplate =
$(go.Link,
$(go.Shape)); // simple black line, no arrowhead needed
// define the tree node data
var nodearray = [
{ // this is the information needed for the headers of the bands
key: "_BANDS",
category: "Bands",
itemArray: [
{ text: "Zero" },
{ text: "One" },
{ text: "Two" },
{ text: "Three" },
{ text: "Four" },
{ text: "Five" }
]
},
// these are the regular nodes in the TreeModel
{ key: "root" },
{ key: "oneB", parent: "root" },
{ key: "twoA", parent: "oneB", layer: 0 },
{ key: "twoC", parent: "root" },
{ key: "threeC", parent: "twoC" },
{ key: "threeD", parent: "twoC" },
{ key: "fourB", parent: "threeD", layer: 2 },
{ key: "fourC", parent: "twoC", layer: 0 },
{ key: "fourD", parent: "fourB" },
{ key: "twoD", parent: "root", layer: 1 }
];
myDiagram.model = new go.TreeModel(nodearray);
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px;"></div>
</div>
</body>
</html>
Thanks for the links. I’m looking here at the example you send and not 100% sure that I have it (New to creating a custom layout).
I didn’t make any changes to the code yet, just using the example you posted here.
When I specify the layer on my data the item never shows up in the correct layer. Example here, Node3 should be on layer three and root3 should be on layer One
Also is there a way to always display all the band even if they don’t have any nodes on it?
Here is how I have my data setup.
nodeDataArray = [
{
key: '_BANDS',
category: 'VerticalBands',
itemArray: [
{ text: "One" },
{ text: "Two" },
{ text: "Three" }
]
},
{ key: "root", layer: 0 },
{ key: "root2", layer: 0 },
{ key: "root3", layer: 0 },
{ key: "node", layer: 1 },
{ key: "node2", layer: 1 },
{ key: "node3", layer: 3 }
];
linkDataArray = [
{from: 'root', to: 'node'},
{from: 'root', to: 'node2'},
{from: 'root2', to: 'node'},
];
and when I look at the diagram I see this.
my end goal is to make a diagram like this. Not worried about the styles now, just trying to get the rough diagram working and later I will work on the styles.
@walter I think I was able to achieve what I’m looking for using the previous example you had here Setting nodes to specific bands - #14
Here is how my data is structure.
// define the tree node data
nodeDataArray = [
{
key: "_BANDS",
category: "VerticalBands",
itemArray: [
{ visible: false },
{ text: "One" },
{ text: "Two" },
{ text: "Three" }
]
},
{ band: 1, category: "simple", key: "Q0" },
{ band: 2, category: "simple", key: "Q1" },
{ band: 2, category: "simple", key: "Q2" },
{ band: 3, category: "simple", key: "Q3" },
{ band: 3, category: "simple", key: "Q4" },
{ band: 1, category: "simple", key: "Q5" }
];
linkDataArray = [
{from: 'Q0', to: 'Q2'},
{from: 'Q0', to: 'Q1'},
{from: 'Q1', to: 'Q3'},
{from: 'Q5', to: 'Q4'},
];
// this.myDiagram.model = new go.TreeModel(nodearray);
this.myDiagram.model =
$(go.GraphLinksModel,
{
nodeDataArray,
linkDataArray
}
);
I just need to work on the linkTemplate
and style the diagram.
How about this?
The complete source code:
<!DOCTYPE html>
<html>
<head>
<title>Layer Bands using a Background Part</title>
<!-- Copyright 1998-2021 by Northwoods Software Corporation. -->
<meta name="description" content="Showing bands for the layers in a diagram.">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://unpkg.com/gojs"></script>
<script id="code">
// this controls whether the layout is horizontal and the layer bands are vertical, or vice-versa:
var HORIZONTAL = false; // this constant parameter can only be set here, not dynamically
// Perform a Layout where commitLayers is overridden to modify the background Part whose key is "_BANDS".
function BandedLDLayout() {
go.LayeredDigraphLayout.call(this);
}
go.Diagram.inherit(BandedLDLayout, go.LayeredDigraphLayout);
BandedLDLayout.prototype.assignLayers = function() {
go.LayeredDigraphLayout.prototype.assignLayers.call(this);
var maxlayer = this.maxLayer;
// now assign specific layers
var it = this.network.vertexes.iterator;
while (it.next()) {
var v = it.value;
if (v.node !== null) {
var lay = v.node.data.layer;
if (typeof lay === "number" && lay >= 0 && lay <= maxlayer) {
v.layer = lay;
}
}
}
};
BandedLDLayout.prototype.commitLayers = function(layerRects, offset) {
// update the background object holding the visual "bands"
var bands = this.diagram.findPartForKey("_BANDS");
if (bands) {
var model = this.diagram.model;
bands.location = this.arrangementOrigin.copy().add(offset);
// make each band visible or not, depending on whether there is a layer for it
for (var it = bands.elements; it.next(); ) {
var idx = it.key;
var elt = it.value; // the item panel representing a band
elt.visible = idx < layerRects.length;
}
// set the bounds of each band via data binding of the "bounds" property
var arr = bands.data.itemArray;
for (var i = 0; i < layerRects.length; i++) {
var itemdata = arr[i];
if (itemdata) {
model.setDataProperty(itemdata, "bounds", layerRects[i]);
}
}
}
};
// end BandedLDLayout
function init() {
var $ = go.GraphObject.make;
myDiagram = $(go.Diagram, "myDiagramDiv",
{
layout: $(BandedLDLayout, // custom layout is defined above
{
direction: HORIZONTAL ? 0 : 90,
columnSpacing: 5,
layeringOption: go.LayeredDigraphLayout.LayerLongestPathSource,
setsPortSpots: false
}),
"undoManager.isEnabled": true
});
// There should be at most a single object of this category.
// This Part will be modified by BandedLDLayout.commitLayers to display visual "bands"
// where each "layer" is a layer of the tree.
// This template is parameterized at load time by the HORIZONTAL parameter.
// You also have the option of showing rectangles for the layer bands or
// of showing separator lines between the layers, but not both at the same time,
// by commenting in/out the indicated code.
myDiagram.nodeTemplateMap.add("Bands",
$(go.Part, "Position",
new go.Binding("itemArray"),
{
isLayoutPositioned: false, // but still in document bounds
locationSpot: new go.Spot(0, 0, HORIZONTAL ? 0 : 80, HORIZONTAL ? 80 : 0), // account for header
layerName: "Background",
pickable: false,
selectable: false,
itemTemplate:
$(go.Panel, HORIZONTAL ? "Vertical" : "Horizontal",
new go.Binding("position", "bounds", function(b) { return b.position; }),
// the header (on top for HORIZONTAL, on the left side for !HORIZONTAL)
$(go.Panel, HORIZONTAL ? "Vertical" : "Horizontal",
{ width: 80 },
new go.Binding("opacity", "text", t => t ? 1 : 0),
new go.Binding("pickable", "text", t => !!t),
$("Button",
{ "ButtonBorder.fill": "gold" },
$(go.Shape, "MinusLine", { width: 8, height: 8 }),
{
click: (e, button) => { }
}),
$(go.TextBlock,
{
//wrap: go.TextBlock.None,
font: "11pt sans-serif",
margin: new go.Margin(0, 0, 0, 4)
},
new go.Binding("text"))
),
// option 1: rectangular bands:
// $(go.Shape,
// { stroke: null, strokeWidth: 0 },
// new go.Binding("desiredSize", "bounds", function(r) { return r.size; }),
// new go.Binding("fill", "itemIndex", function(i) { return i % 2 == 0 ? "whitesmoke" : go.Brush.darken("whitesmoke"); }).ofObject())
// option 2: separator lines:
(HORIZONTAL
? $(go.Shape, "LineV",
{ stroke: "gray", strokeDashArray: [4, 4], alignment: go.Spot.TopLeft, width: 1 },
new go.Binding("height", "bounds", function(r) { return r.height; }),
new go.Binding("visible", "itemIndex", function(i) { return i > 0; }).ofObject())
: $(go.Shape, "LineH",
{ stroke: "gray", strokeDashArray: [4, 4], alignment: go.Spot.TopLeft, height: 1 },
new go.Binding("width", "bounds", function(r) { return r.width; }),
new go.Binding("visible", "itemIndex", function(i) { return i > 0; }).ofObject())
)
)
}
));
myDiagram.nodeTemplate =
$(go.Node, go.Panel.Auto,
{ width: 60, height: 40 },
$(go.Shape, "Rectangle",
{ fill: "cornflowerblue", strokeWidth: 0 }),
$(go.TextBlock,
new go.Binding("text", "key")));
myDiagram.nodeTemplateMap.add("X",
$(go.Node, "Spot",
{
linkConnected: (node, link, port) => { // color the link paths
if (node.category === "X") link.path.stroke = node.data.color;
}
},
$(go.Panel, "Auto",
{ width: 80, height: 40 },
$(go.Shape, "Ellipse",
{ fill: "white" },
new go.Binding("fill", "color"))
),
$(go.Panel, "Spot",
{ alignment: new go.Spot(0, 0, 6, 6) },
$(go.Shape, "Circle",
{ width: 14, height: 14, stroke: "white", fill: "gray" }),
$(go.TextBlock,
{ stroke: "white", font: "10px sans-serif", alignment: new go.Spot(0.5, 0.5, 1, 0.5) },
new go.Binding("text", "tag"))
)
))
myDiagram.linkTemplate =
$(go.Link,
{ layerName: "Background" },
$(go.Shape, { strokeWidth: 1.5 }));
var nodearray = [
{ // this is the information needed for the headers of the bands
key: "_BANDS",
category: "Bands",
itemArray: [
{ text: "One" },
{ text: "" },
{ text: "Two" },
{ text: "" },
{ text: "Three" }
]
},
{ key: "A", category: "X", tag: 3, color: "green", layer: 3 },
{ key: "B", category: "X", tag: 2, color: "red", layer: 3 },
{ key: "C", category: "X", tag: 2, color: "magenta", layer: 1 },
{ key: "D", category: "X", tag: 1, color: "cornflowerblue", layer: 1 },
{ key: 1, layer: 4 },
{ key: 2, layer: 2 },
{ key: 3, layer: 2 },
{ key: 4, layer: 2 },
{ key: 5, layer: 2 },
{ key: 6, layer: 2 },
{ key: 7, layer: 2 },
{ key: 8, layer: 0 },
{ key: 9, layer: 0 },
{ key: 10, layer: 0 },
];
var linkarray = [
{ from: 1, to: "A" },
{ from: 1, to: "B" },
{ from: "A", to: 2 },
{ from: "A", to: 3 },
{ from: "A", to: 4 },
{ from: "A", to: 5 },
{ from: "A", to: 6 },
{ from: "A", to: 7 },
{ from: "B", to: 4 },
{ from: "B", to: 5 },
{ from: 4, to: "D" },
{ from: 5, to: "C" },
{ from: 7, to: "D" },
{ from: "C", to: 8 },
{ from: "C", to: 9 },
{ from: "D", to: 10 },
];
myDiagram.model =
$(go.GraphLinksModel,
{
nodeDataArray: nodearray,
linkDataArray: linkarray
});
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px;"></div>
<p>
Unlike swim lane diagrams where the nodes are supposed to stay in their lanes,
layer bands run perpendicular to the growth direction of the layout.
</p>
<p>
This sample uses a custom <a>LayeredDigraphLayout</a> that overrides the <a>LayeredDigraphLayout.commitLayers</a> method
in order to specify the position and size of each "band" that surrounds a layer of the graph.
The "bands" are held in a single Part that is bound to a particular node data object whose key is "_BANDS".
The headers, and potentially any other information that you might want to display in the headers,
are stored in this "_BANDS" object in an Array.
</p>
<p>
This sample can be adapted to use a <a>TreeModel</a> instead of a <a>GraphLinksModel</a>
and a <a>TreeLayout</a> instead of a <a>LayeredDigraphLayout</a>:
<a href="../samples/swimBands.html">Swim Bands</a>.
</p>
</div>
</body>
</html>
Thank you for the help. this works the way I needed too.
You can probably adapt the commitLayers override to do what you want, but I haven’t tried it.
I will play with it here see if I can make it work. When I look at the layerRects inside of the commitLayer why I don’t see some links on it.
example if I use the following data on the code you posted above
nodeDataArray = [
{ // this is the information needed for the headers of the bands
key: "_BANDS",
category: "Bands",
itemArray: [
{ text: "One" },
{ text: "" },
{ text: "Two" },
{ text: "" },
{ text: "Three" }
]
},
{ key: "Parent", category: "X", tag: 3, color: "green", layer: 2 },
{ key: 1, layer: 1 },
{ key: 2, layer: 0 }
];
linkDataArray = [
{ from: 'Parent', to: 1 },
{ from: 'Parent', to: 2 }
];
My diagram looks like this
and when I look at the layerRects I only see this 2 entries and not 3. But if I change the **linkDataArray ** to look like this.
linkDataArray = [
{ from: 'Parent', to: 1 },
{ from: 1, to: 2 }
];
now I see this.
Is it possible on the first example to place the node 2 on band two, but I need the link to come from the parent and not from the node 1
Yes, it does by default. But you could force there to be empty layers by putting in dummy vertexes and edges. You can fiddle with the 25 for the height of the dummy node to control the minimum height of layers, which normally would be zero if there were no nodes in them.
// Perform a Layout where commitLayers is overridden to modify the background Part whose key is "_BANDS".
function BandedLDLayout() {
go.LayeredDigraphLayout.call(this);
}
go.Diagram.inherit(BandedLDLayout, go.LayeredDigraphLayout);
BandedLDLayout.prototype.makeNetwork = function(coll) {
var net = go.LayeredDigraphLayout.prototype.makeNetwork.call(this, coll);
var max = 0;
net.vertexes.each(function(v) {
if (v.node && typeof v.node.data.layer === "number") {
max = Math.max(max, v.node.data.layer);
}
});
if (max > 0) {
var v = net.createVertex(); // a dummy vertex
v.height = 25; // with a minimum height, in case there are no nodes in this layer
net.addVertex(v);
while (max > 0) {
var next = net.createVertex();
next.height = v.height;
net.addVertex(next);
net.linkVertexes(v, next, null); // dummy edge too
v = next;
max--;
}
}
return net;
}
BandedLDLayout.prototype.nodeMinLayerSpace = function(v, topleft) {
if (!v.node && v.height > 0) return v.height/2;
return go.LayeredDigraphLayout.prototype.nodeMinLayerSpace.call(this, v, topleft);
}
BandedLDLayout.prototype.assignLayers = function() {
go.LayeredDigraphLayout.prototype.assignLayers.call(this);
var maxlayer = this.maxLayer;
// now assign specific layers
var it = this.network.vertexes.iterator;
while (it.next()) {
var v = it.value;
if (v.node !== null) {
var lay = v.node.data.layer;
if (typeof lay === "number" && lay >= 0 && lay <= maxlayer) {
v.layer = lay;
}
}
}
};
BandedLDLayout.prototype.commitLayers = function(layerRects, offset) {
// update the background object holding the visual "bands"
var bands = this.diagram.findPartForKey("_BANDS");
if (bands) {
var model = this.diagram.model;
bands.location = this.arrangementOrigin.copy().add(offset);
// make each band visible or not, depending on whether there is a layer for it
for (var it = bands.elements; it.next(); ) {
var idx = it.key;
var elt = it.value; // the item panel representing a band
elt.visible = idx < layerRects.length;
}
// set the bounds of each band via data binding of the "bounds" property
var arr = bands.data.itemArray;
for (var i = 0; i < layerRects.length; i++) {
var itemdata = arr[i];
if (itemdata) {
model.setDataProperty(itemdata, "bounds", layerRects[i]);
}
}
}
};
// end BandedLDLayout