forked from NorthwoodsSoftware/GoJS
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathvirtualizedTree.html
339 lines (302 loc) · 12.8 KB
/
virtualizedTree.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Virtualized Tree with custom layout</title>
<meta name="description" content="An example of virtualization where a very simple tree layout sets the bounds for each node data." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Copyright 1998-2019 by Northwoods Software Corporation. -->
<script src="../release/go.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script id="code">
// this controls whether the tree grows deeper towards the right or downwards:
var HORIZONTAL = true;
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this
var $ = go.GraphObject.make; // for conciseness in defining templates
// The Diagram just shows what should be visible in the viewport.
// Its model does NOT include node data for the whole graph, but only that
// which might be visible in the viewport.
myDiagram =
$(go.Diagram, "myDiagramDiv",
{
contentAlignment: go.Spot.Center,
initialDocumentSpot: go.Spot.Center,
initialViewportSpot: go.Spot.Center,
// Do manual layout in the layoutTree function below, rather than automatic layout using a
// TreeLayout which would require the Nodes and Links to exist first for an accurate layout.
//layout: $(go.TreeLayout,
// { nodeSpacing: 4, compaction: go.TreeLayout.CompactionNone }),
// Assume there's no Layout -- all data.bounds are calculated in layoutTree
layout: $(go.Layout, { isInitial: false, isOngoing: false }), // never invalidates
// Define the template for Nodes, used by virtualization.
nodeTemplate:
$(go.Node, "Auto",
{ isLayoutPositioned: false }, // optimization
new go.Binding("position", "bounds", function(b) { return b.position; })
.makeTwoWay(function(p, d) { return new go.Rect(p.x, p.y, d.bounds.width, d.bounds.height); }),
{ width: 70, height: 20 }, // in cooperation with the load function, below
$(go.Shape, "Rectangle",
new go.Binding("fill", "color")),
$(go.TextBlock,
{ margin: 2 },
new go.Binding("text", "color")),
{
toolTip:
$("ToolTip",
$(go.TextBlock, { margin: 3 },
new go.Binding("text", "",
function(d) { return "key: " + d.key + "\nbounds: " + d.bounds.toString(); }))
)
}
),
// Define the template for Links
linkTemplate:
$(go.Link,
{
isLayoutPositioned: false, // optimization
fromSpot: (HORIZONTAL ? go.Spot.Right : go.Spot.Bottom),
toSpot: (HORIZONTAL ? go.Spot.Left : go.Spot.Top)
},
$(go.Shape)
),
"animationManager.isEnabled": false
});
// This model includes all of the data
myWholeModel =
$(go.TreeModel); // must match the TreeModel used by the Diagram, below
// Do not set myDiagram.model = myWholeModel -- that would create a zillion Nodes and Links!
// In the future Diagram may have built-in support for virtualization.
// For now, we have to implement virtualization ourselves by having the Diagram's model
// be different than the "real" model.
myDiagram.model = // this only holds nodes that should be in the viewport
$(go.TreeModel); // must match the model, above
// for now, we have to implement virtualization ourselves
myDiagram.isVirtualized = true;
myDiagram.addDiagramListener("ViewportBoundsChanged", onViewportChanged);
// This is a status message
myLoading =
$(go.Part, // this has to set the location or position explicitly
{ location: new go.Point(0, 0) },
$(go.TextBlock, "loading...",
{ stroke: "red", font: "20pt sans-serif" }));
// temporarily add the status indicator
myDiagram.add(myLoading);
// Allow the myLoading indicator to be shown now,
// but allow objects added in load to also be considered part of the initial Diagram.
// If you are not going to add temporary initial Parts, don't call delayInitialization.
myDiagram.delayInitialization(load);
}
function load() {
// create a lot of data for the myWholeModel
var total = 123456;
var treedata = [];
for (var i = 0; i < total; i++) {
var d = {
key: i, // this node data's key
color: go.Brush.randomColor(), // the node's color
parent: (i > 0 ? Math.floor(Math.random() * i / 2) : undefined) // the random parent's key
};
//!!!???@@@ this needs to be customized to account for your chosen Node template
d.bounds = new go.Rect(0, 0, 70, 20);
treedata.push(d);
}
myWholeModel.nodeDataArray = treedata;
// make it faster to get from a model parent data object to all of the children data
improveNavigation(myWholeModel);
// this sets the data.bounds on each node data
// and Diagram.fixedBounds on the diagram, so the diagram knows how far it can scroll
layoutTree(myWholeModel);
// remove the status indicator
myDiagram.remove(myLoading);
}
// this adds ._parent and ._children properties on each node data
function improveNavigation(model) { // this must be a TreeModel
var ndata = model.nodeDataArray;
// create an Array of child data references for each parent data
for (var i = 0; i < ndata.length; i++) {
var child = ndata[i];
var parentkey = model.getParentKeyForNodeData(child);
var parent = model.findNodeDataForKey(parentkey);
if (parent) {
child._parent = parent;
var childarr = parent._children;
if (childarr) {
childarr.push(child);
} else {
parent._children = [child];
}
}
}
}
// The following functions implement virtualization of the Diagram
// Assume data.bounds is a Rect of the area occupied by the Node in document coordinates.
// The normal mechanism for determining the size of the document depends on all of the
// Nodes and Links existing, so we need to use a function that depends only on the model data.
function computeDocumentBounds(model) {
var b = new go.Rect();
var ndata = model.nodeDataArray;
for (var i = 0; i < ndata.length; i++) {
var d = ndata[i];
if (!d.bounds) continue;
if (i === 0) {
b.set(d.bounds);
} else {
b.unionRect(d.bounds);
}
}
return b;
}
// As the user scrolls or zooms, make sure the Parts (Nodes and Links) exist in the viewport.
function onViewportChanged(e) {
var diagram = e.diagram;
// make sure there are Nodes for each node data that is in the viewport
// or that is connected to such a Node
var viewb = diagram.viewportBounds; // the new viewportBounds
var model = diagram.model;
var oldskips = diagram.skipsUndoManager;
diagram.skipsUndoManager = true;
var b = new go.Rect();
var ndata = myWholeModel.nodeDataArray;
for (var i = 0; i < ndata.length; i++) {
var n = ndata[i];
if (!n.bounds) continue;
if (n.bounds.intersectsRect(viewb)) {
model.addNodeData(n);
}
// make sure links to all parent nodes appear
var parentkey = myWholeModel.getParentKeyForNodeData(n);
var parent = myWholeModel.findNodeDataForKey(parentkey);
if (parent !== null) {
if (n.bounds.intersectsRect(viewb)) { // N is inside viewport
model.addNodeData(parent); // so that link to parent appears
} else { // N is outside of viewport
// see if there's a parent that is in the viewport,
// or if the link might cross over the viewport
b.set(n.bounds);
b.unionRect(parent.bounds);
if (b.intersectsRect(viewb)) {
model.addNodeData(n); // add N so that link to parent appears
}
}
}
}
diagram.skipsUndoManager = oldskips;
if (myRemoveTimer === null) {
// only remove offscreen nodes after a delay
myRemoveTimer = setTimeout(function() { removeOffscreen(diagram); }, 3000);
}
updateCounts(); // only for this sample
}
// occasionally remove Parts that are offscreen from the Diagram
var myRemoveTimer = null;
function removeOffscreen(diagram) {
myRemoveTimer = null;
var viewb = diagram.viewportBounds;
var model = diagram.model;
var remove = []; // collect for later removal
var it = diagram.nodes;
while (it.next()) {
var n = it.value;
var d = n.data;
if (d === null) continue;
if (!n.actualBounds.intersectsRect(viewb) && !n.isSelected) {
// even if the node is out of the viewport, keep it if it is selected or
// if any link connecting with the node is still in the viewport
if (!n.linksConnected.any(function(l) { return l.actualBounds.intersectsRect(viewb); })) {
remove.push(d);
}
}
}
if (remove.length > 0) {
var oldskips = diagram.skipsUndoManager;
diagram.skipsUndoManager = true;
model.removeNodeDataCollection(remove);
diagram.skipsUndoManager = oldskips;
}
updateCounts(); // only for this sample
}
// end of virtualized Diagram
// A very simple tree layout.
// Basic tree layout parameters
var nodeSpacing = 4;
var layerSpacing = 50;
// Layout the whole tree just using the model, not any Nodes or Links.
function layoutTree(model) {
var ndata = model.nodeDataArray;
// layout each tree root
if (HORIZONTAL) {
var y = 0;
for (var i = 0; i < ndata.length; i++) {
var d = ndata[i];
// is this a root node?
if (!d._parent) {
y = walkTreeH(d, 0, y) + d.bounds.height + nodeSpacing;
}
}
} else { // !HORIZONTAL
var x = 0;
for (var i = 0; i < ndata.length; i++) {
var d = ndata[i];
// is this a root node?
if (!d._parent) {
x = walkTreeV(d, x, 0) + d.bounds.width + nodeSpacing;
}
}
}
// can't depend on regular bounds computation that depends on all Nodes existing
myDiagram.fixedBounds = computeDocumentBounds(model);
}
// Walk subtrees from each root node, positioning as we go.
function walkTreeH(parent, oldx, oldy) { // HORIZONTAL
var origy = oldy;
var newy = oldy;
var childarr = parent._children;
if (childarr) {
for (var i = 0; i < childarr.length; i++) {
var child = childarr[i];
newy = walkTreeH(child, oldx + child.bounds.width + layerSpacing, oldy);
oldy = newy + child.bounds.height + nodeSpacing;
}
}
parent.bounds.x = oldx;
parent.bounds.y = (origy + newy) / 2;
return newy;
}
function walkTreeV(parent, oldx, oldy) { // !HORIZONTAL
var origx = oldx;
var newx = oldx;
var childarr = parent._children;
if (childarr) {
for (var i = 0; i < childarr.length; i++) {
var child = childarr[i];
newx = walkTreeV(child, oldx, oldy + child.bounds.height + layerSpacing);
oldx = newx + child.bounds.width + nodeSpacing;
}
}
parent.bounds.x = (origx + newx) / 2;
parent.bounds.y = oldy;
return newx;
}
// end of layoutTree functionality
// This function is only used in this sample to demonstrate the effects of the virtualization.
// In a real application you would delete this function and all calls to it.
function updateCounts() {
document.getElementById("myMessage1").textContent = myWholeModel.nodeDataArray.length;
document.getElementById("myMessage2").textContent = myDiagram.nodes.count;
document.getElementById("myMessage4").textContent = myDiagram.links.count;
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagramDiv" style="background-color: white; border: solid 1px black; width: 100%; height: 600px"></div>
<div id="description">
<p>This uses a <a>TreeModel</a> but not <a>TreeLayout</a>.</p>
Node data in Model: <span id="myMessage1"></span>.
Actual Nodes in Diagram: <span id="myMessage2"></span>.
Actual Links in Diagram: <span id="myMessage4"></span>.
</div>
</div>
</body>
</html>