forked from NorthwoodsSoftware/GoJS
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsequenceDiagram.html
419 lines (386 loc) · 16.3 KB
/
sequenceDiagram.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
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Sequence Diagram</title>
<meta name="description" content="A sequence diagram editor." />
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Copyright 1998-2019 by Northwoods Software Corporation. -->
<script src="../release/go.js"></script>
<link href='https://fonts.googleapis.com/css?family=Source+Sans+Pro' rel='stylesheet' type='text/css'>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script id="code">
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", // must be the ID or reference to an HTML DIV
{
allowCopy: false,
linkingTool: $(MessagingTool), // defined below
"resizingTool.isGridSnapEnabled": true,
draggingTool: $(MessageDraggingTool), // defined below
"draggingTool.gridSnapCellSize": new go.Size(1, MessageSpacing / 4),
"draggingTool.isGridSnapEnabled": true,
// automatically extend Lifelines as Activities are moved or resized
"SelectionMoved": ensureLifelineHeights,
"PartResized": ensureLifelineHeights,
"undoManager.isEnabled": true
});
// when the document is modified, add a "*" to the title and enable the "Save" button
myDiagram.addDiagramListener("Modified", function(e) {
var button = document.getElementById("SaveButton");
if (button) button.disabled = !myDiagram.isModified;
var idx = document.title.indexOf("*");
if (myDiagram.isModified) {
if (idx < 0) document.title += "*";
} else {
if (idx >= 0) document.title = document.title.substr(0, idx);
}
});
// define the Lifeline Node template.
myDiagram.groupTemplate =
$(go.Group, "Vertical",
{
locationSpot: go.Spot.Bottom,
locationObjectName: "HEADER",
minLocation: new go.Point(0, 0),
maxLocation: new go.Point(9999, 0),
selectionObjectName: "HEADER"
},
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
$(go.Panel, "Auto",
{ name: "HEADER" },
$(go.Shape, "Rectangle",
{
fill: $(go.Brush, "Linear", { 0: "#bbdefb", 1: go.Brush.darkenBy("#bbdefb", 0.1) }),
stroke: null
}),
$(go.TextBlock,
{
margin: 5,
font: "400 10pt Source Sans Pro, sans-serif"
},
new go.Binding("text", "text"))
),
$(go.Shape,
{
figure: "LineV",
fill: null,
stroke: "gray",
strokeDashArray: [3, 3],
width: 1,
alignment: go.Spot.Center,
portId: "",
fromLinkable: true,
fromLinkableDuplicates: true,
toLinkable: true,
toLinkableDuplicates: true,
cursor: "pointer"
},
new go.Binding("height", "duration", computeLifelineHeight))
);
// define the Activity Node template
myDiagram.nodeTemplate =
$(go.Node,
{
locationSpot: go.Spot.Top,
locationObjectName: "SHAPE",
minLocation: new go.Point(NaN, LinePrefix - ActivityStart),
maxLocation: new go.Point(NaN, 19999),
selectionObjectName: "SHAPE",
resizable: true,
resizeObjectName: "SHAPE",
resizeAdornmentTemplate:
$(go.Adornment, "Spot",
$(go.Placeholder),
$(go.Shape, // only a bottom resize handle
{
alignment: go.Spot.Bottom, cursor: "col-resize",
desiredSize: new go.Size(6, 6), fill: "yellow"
})
)
},
new go.Binding("location", "", computeActivityLocation).makeTwoWay(backComputeActivityLocation),
$(go.Shape, "Rectangle",
{
name: "SHAPE",
fill: "white", stroke: "black",
width: ActivityWidth,
// allow Activities to be resized down to 1/4 of a time unit
minSize: new go.Size(ActivityWidth, computeActivityHeight(0.25))
},
new go.Binding("height", "duration", computeActivityHeight).makeTwoWay(backComputeActivityHeight))
);
// define the Message Link template.
myDiagram.linkTemplate =
$(MessageLink, // defined below
{ selectionAdorned: true, curviness: 0 },
$(go.Shape, "Rectangle",
{ stroke: "black" }),
$(go.Shape,
{ toArrow: "OpenTriangle", stroke: "black" }),
$(go.TextBlock,
{
font: "400 9pt Source Sans Pro, sans-serif",
segmentIndex: 0,
segmentOffset: new go.Point(NaN, NaN),
isMultiline: false,
editable: true
},
new go.Binding("text", "text").makeTwoWay())
);
// create the graph by reading the JSON data saved in "mySavedModel" textarea element
load();
}
function ensureLifelineHeights(e) {
// iterate over all Activities (ignore Groups)
var arr = myDiagram.model.nodeDataArray;
var max = -1;
for (var i = 0; i < arr.length; i++) {
var act = arr[i];
if (act.isGroup) continue;
max = Math.max(max, act.start + act.duration);
}
if (max > 0) {
// now iterate over only Groups
for (var i = 0; i < arr.length; i++) {
var gr = arr[i];
if (!gr.isGroup) continue;
if (max > gr.duration) { // this only extends, never shrinks
myDiagram.model.setDataProperty(gr, "duration", max);
}
}
}
}
// some parameters
var LinePrefix = 20; // vertical starting point in document for all Messages and Activations
var LineSuffix = 30; // vertical length beyond the last message time
var MessageSpacing = 20; // vertical distance between Messages at different steps
var ActivityWidth = 10; // width of each vertical activity bar
var ActivityStart = 5; // height before start message time
var ActivityEnd = 5; // height beyond end message time
function computeLifelineHeight(duration) {
return LinePrefix + duration * MessageSpacing + LineSuffix;
}
function computeActivityLocation(act) {
var groupdata = myDiagram.model.findNodeDataForKey(act.group);
if (groupdata === null) return new go.Point();
// get location of Lifeline's starting point
var grouploc = go.Point.parse(groupdata.loc);
return new go.Point(grouploc.x, convertTimeToY(act.start) - ActivityStart);
}
function backComputeActivityLocation(loc, act) {
myDiagram.model.setDataProperty(act, "start", convertYToTime(loc.y + ActivityStart));
}
function computeActivityHeight(duration) {
return ActivityStart + duration * MessageSpacing + ActivityEnd;
}
function backComputeActivityHeight(height) {
return (height - ActivityStart - ActivityEnd) / MessageSpacing;
}
// time is just an abstract small non-negative integer
// here we map between an abstract time and a vertical position
function convertTimeToY(t) {
return t * MessageSpacing + LinePrefix;
}
function convertYToTime(y) {
return (y - LinePrefix) / MessageSpacing;
}
// a custom routed Link
function MessageLink() {
go.Link.call(this);
this.time = 0; // use this "time" value when this is the temporaryLink
}
go.Diagram.inherit(MessageLink, go.Link);
MessageLink.prototype.getLinkPoint = function(node, port, spot, from, ortho, othernode, otherport) {
var p = port.getDocumentPoint(go.Spot.Center);
var r = port.getDocumentBounds();
var op = otherport.getDocumentPoint(go.Spot.Center);
var data = this.data;
var time = data !== null ? data.time : this.time; // if not bound, assume this has its own "time" property
var aw = this.findActivityWidth(node, time);
var x = (op.x > p.x ? p.x + aw / 2 : p.x - aw / 2);
var y = convertTimeToY(time);
return new go.Point(x, y);
};
MessageLink.prototype.findActivityWidth = function(node, time) {
var aw = ActivityWidth;
if (node instanceof go.Group) {
// see if there is an Activity Node at this point -- if not, connect the link directly with the Group's lifeline
if (!node.memberParts.any(function(mem) {
var act = mem.data;
return (act !== null && act.start <= time && time <= act.start + act.duration);
})) {
aw = 0;
}
}
return aw;
};
MessageLink.prototype.getLinkDirection = function(node, port, linkpoint, spot, from, ortho, othernode, otherport) {
var p = port.getDocumentPoint(go.Spot.Center);
var op = otherport.getDocumentPoint(go.Spot.Center);
var right = op.x > p.x;
return right ? 0 : 180;
};
MessageLink.prototype.computePoints = function() {
if (this.fromNode === this.toNode) { // also handle a reflexive link as a simple orthogonal loop
var data = this.data;
var time = data !== null ? data.time : this.time; // if not bound, assume this has its own "time" property
var p = this.fromNode.port.getDocumentPoint(go.Spot.Center);
var aw = this.findActivityWidth(this.fromNode, time);
var x = p.x + aw / 2;
var y = convertTimeToY(time);
this.clearPoints();
this.addPoint(new go.Point(x, y));
this.addPoint(new go.Point(x + 50, y));
this.addPoint(new go.Point(x + 50, y + 5));
this.addPoint(new go.Point(x, y + 5));
return true;
} else {
return go.Link.prototype.computePoints.call(this);
}
}
// end MessageLink
// A custom LinkingTool that fixes the "time" (i.e. the Y coordinate)
// for both the temporaryLink and the actual newly created Link
function MessagingTool() {
go.LinkingTool.call(this);
var $ = go.GraphObject.make;
this.temporaryLink =
$(MessageLink,
$(go.Shape, "Rectangle",
{ stroke: "magenta", strokeWidth: 2 }),
$(go.Shape,
{ toArrow: "OpenTriangle", stroke: "magenta" }));
};
go.Diagram.inherit(MessagingTool, go.LinkingTool);
MessagingTool.prototype.doActivate = function() {
go.LinkingTool.prototype.doActivate.call(this);
var time = convertYToTime(this.diagram.firstInput.documentPoint.y);
this.temporaryLink.time = Math.ceil(time); // round up to an integer value
};
MessagingTool.prototype.insertLink = function(fromnode, fromport, tonode, toport) {
var newlink = go.LinkingTool.prototype.insertLink.call(this, fromnode, fromport, tonode, toport);
if (newlink !== null) {
var model = this.diagram.model;
// specify the time of the message
var start = this.temporaryLink.time;
var duration = 1;
newlink.data.time = start;
model.setDataProperty(newlink.data, "text", "msg");
// and create a new Activity node data in the "to" group data
var newact = {
group: newlink.data.to,
start: start,
duration: duration
};
model.addNodeData(newact);
// now make sure all Lifelines are long enough
ensureLifelineHeights();
}
return newlink;
};
// end MessagingTool
// A custom DraggingTool that supports dragging any number of MessageLinks up and down --
// changing their data.time value.
function MessageDraggingTool() {
go.DraggingTool.call(this);
}
go.Diagram.inherit(MessageDraggingTool, go.DraggingTool);
// override the standard behavior to include all selected Links,
// even if not connected with any selected Nodes
MessageDraggingTool.prototype.computeEffectiveCollection = function(parts, options) {
var result = go.DraggingTool.prototype.computeEffectiveCollection.call(this, parts, options);
// add a dummy Node so that the user can select only Links and move them all
result.add(new go.Node(), new go.DraggingInfo(new go.Point()));
// normally this method removes any links not connected to selected nodes;
// we have to add them back so that they are included in the "parts" argument to moveParts
parts.each(function(part) {
if (part instanceof go.Link) {
result.add(part, new go.DraggingInfo(part.getPoint(0).copy()));
}
})
return result;
}
// override to allow dragging when the selection only includes Links
MessageDraggingTool.prototype.mayMove = function() {
return !this.diagram.isReadOnly && this.diagram.allowMove;
}
// override to move Links (which are all assumed to be MessageLinks) by
// updating their Link.data.time property so that their link routes will
// have the correct vertical position
MessageDraggingTool.prototype.moveParts = function(parts, offset, check) {
go.DraggingTool.prototype.moveParts.call(this, parts, offset, check);
var it = parts.iterator;
while (it.next()) {
if (it.key instanceof go.Link) {
var link = it.key;
var startY = it.value.point.y; // DraggingInfo.point.y
var y = startY + offset.y; // determine new Y coordinate value for this link
var cellY = this.gridSnapCellSize.height;
y = Math.round(y / cellY) * cellY; // snap to multiple of gridSnapCellSize.height
var t = Math.max(0, convertYToTime(y));
link.diagram.model.set(link.data, "time", t);
link.invalidateRoute();
}
}
}
// end MessageDraggingTool
// Show the diagram's model in JSON format
function save() {
document.getElementById("mySavedModel").value = myDiagram.model.toJson();
myDiagram.isModified = false;
}
function load() {
myDiagram.model = go.Model.fromJson(document.getElementById("mySavedModel").value);
}
</script>
</head>
<body onload="init()" >
<div id="sample">
<div id="myDiagramDiv" style="border: solid 1px black; width: 100%; height: 400px"></div>
<p>
A <em>sequence diagram</em> is an interaction diagram that shows how entities operate with one another and in what order.
In this sample, we show the interaction between different people in a restaurant.
</p>
<p>
The diagram uses the <a>Diagram.groupTemplate</a> for "lifelines,"
<a>Diagram.nodeTemplate</a> for "activities," and <a>Diagram.linkTemplate</a> for "messages" between the entities.
Also featured are a custom Link class and custom <a>LinkingTool</a> to draw links
between lifelines and create activities at the end of the new link. Nodes use a binding function on the location
property to ensure they are anchored to their lifeline.
</p>
<div>
<div>
<button id="SaveButton" onclick="save()">Save</button>
<button onclick="load()">Load</button>
Diagram Model saved in JSON format:
</div>
<textarea id="mySavedModel" style="width:100%;height:240px">
{ "class": "go.GraphLinksModel",
"nodeDataArray": [
{"key":"Fred", "text":"Fred: Patron", "isGroup":true, "loc":"0 0", "duration":9},
{"key":"Bob", "text":"Bob: Waiter", "isGroup":true, "loc":"100 0", "duration":9},
{"key":"Hank", "text":"Hank: Cook", "isGroup":true, "loc":"200 0", "duration":9},
{"key":"Renee", "text":"Renee: Cashier", "isGroup":true, "loc":"300 0", "duration":9},
{"group":"Bob", "start":1, "duration":2},
{"group":"Hank", "start":2, "duration":3},
{"group":"Fred", "start":3, "duration":1},
{"group":"Bob", "start":5, "duration":1},
{"group":"Fred", "start":6, "duration":2},
{"group":"Renee", "start":8, "duration":1}
],
"linkDataArray": [
{"from":"Fred", "to":"Bob", "text":"order", "time":1},
{"from":"Bob", "to":"Hank", "text":"order food", "time":2},
{"from":"Bob", "to":"Fred", "text":"serve drinks", "time":3},
{"from":"Hank", "to":"Bob", "text":"finish cooking", "time":5},
{"from":"Bob", "to":"Fred", "text":"serve food", "time":6},
{"from":"Fred", "to":"Renee", "text":"pay", "time":8}
]}
</textarea>
</div>
</div>
</body>
</html>