Merge pull request #336 from ejulio-ts/sequence-diagram-self-edges

sequence diagram: self edges
This commit is contained in:
Alexander Wang 2022-12-04 19:42:33 -08:00 committed by GitHub
commit b07fe2b956
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 1630 additions and 80 deletions

View file

@ -37,7 +37,10 @@ func Layout(ctx context.Context, g *d2graph.Graph, layout func(ctx context.Conte
continue continue
} }
sd := layoutSequenceDiagram(g, obj) sd, err := layoutSequenceDiagram(g, obj)
if err != nil {
return err
}
obj.Children = make(map[string]*d2graph.Object) obj.Children = make(map[string]*d2graph.Object)
obj.ChildrenArray = nil obj.ChildrenArray = nil
obj.Box = geo.NewBox(nil, sd.getWidth(), sd.getHeight()) obj.Box = geo.NewBox(nil, sd.getWidth(), sd.getHeight())
@ -76,7 +79,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, layout func(ctx context.Conte
} }
// layoutSequenceDiagram finds the edges inside the sequence diagram and performs the layout on the object descendants // layoutSequenceDiagram finds the edges inside the sequence diagram and performs the layout on the object descendants
func layoutSequenceDiagram(g *d2graph.Graph, obj *d2graph.Object) *sequenceDiagram { func layoutSequenceDiagram(g *d2graph.Graph, obj *d2graph.Object) (*sequenceDiagram, error) {
var edges []*d2graph.Edge var edges []*d2graph.Edge
for _, edge := range g.Edges { for _, edge := range g.Edges {
// both Src and Dst must be inside the sequence diagram // both Src and Dst must be inside the sequence diagram
@ -86,8 +89,8 @@ func layoutSequenceDiagram(g *d2graph.Graph, obj *d2graph.Object) *sequenceDiagr
} }
sd := newSequenceDiagram(obj.ChildrenArray, edges) sd := newSequenceDiagram(obj.ChildrenArray, edges)
sd.layout() err := sd.layout()
return sd return sd, err
} }
func getLayoutEdges(g *d2graph.Graph, toRemove map[*d2graph.Edge]struct{}) ([]*d2graph.Edge, map[string]int) { func getLayoutEdges(g *d2graph.Graph, toRemove map[*d2graph.Edge]struct{}) ([]*d2graph.Edge, map[string]int) {

View file

@ -159,6 +159,7 @@ func TestSpansSequenceDiagram(t *testing.T) {
// ├┐──────────────────────► // ├┐──────────────────────►
// t2 ││ │ // t2 ││ │
// ├┘◄─────────────────────┤ // ├┘◄─────────────────────┤
input := ` input := `
shape: sequence_diagram shape: sequence_diagram
a: { shape: person } a: { shape: person }
@ -182,7 +183,6 @@ b -> a.t2`
a, has := g.Root.HasChild([]string{"a"}) a, has := g.Root.HasChild([]string{"a"})
assert.True(t, has) assert.True(t, has)
a.Box = geo.NewBox(nil, 100, 100)
a_t1, has := a.HasChild([]string{"t1"}) a_t1, has := a.HasChild([]string{"t1"})
assert.True(t, has) assert.True(t, has)
@ -197,6 +197,12 @@ b -> a.t2`
b_t1, has := b.HasChild([]string{"t1"}) b_t1, has := b.HasChild([]string{"t1"})
assert.True(t, has) assert.True(t, has)
a.Box = geo.NewBox(nil, 100, 100)
a_t1.Box = geo.NewBox(nil, 100, 100)
a_t2.Box = geo.NewBox(nil, 100, 100)
b.Box = geo.NewBox(nil, 30, 30)
b_t1.Box = geo.NewBox(nil, 100, 100)
d2sequence.Layout(ctx, g, func(ctx context.Context, g *d2graph.Graph) error { d2sequence.Layout(ctx, g, func(ctx context.Context, g *d2graph.Graph) error {
// just set some position as if it had been properly placed // just set some position as if it had been properly placed
for _, obj := range g.Objects { for _, obj := range g.Objects {
@ -304,6 +310,7 @@ container -> c: edge 1
a_t1, has := a.HasChild([]string{"t1"}) a_t1, has := a.HasChild([]string{"t1"})
assert.True(t, has) assert.True(t, has)
a_t1.Box = geo.NewBox(nil, 100, 100)
b, has := container.HasChild([]string{"b"}) b, has := container.HasChild([]string{"b"})
assert.True(t, has) assert.True(t, has)
@ -311,6 +318,7 @@ container -> c: edge 1
b_t1, has := b.HasChild([]string{"t1"}) b_t1, has := b.HasChild([]string{"t1"})
assert.True(t, has) assert.True(t, has)
b_t1.Box = geo.NewBox(nil, 100, 100)
c := g.Root.EnsureChild([]string{"c"}) c := g.Root.EnsureChild([]string{"c"})
c.Box = geo.NewBox(nil, 100, 100) c.Box = geo.NewBox(nil, 100, 100)
@ -366,3 +374,90 @@ container -> c: edge 1
} }
} }
} }
func TestSelfEdges(t *testing.T) {
g := d2graph.NewGraph(nil)
g.Root.Attributes.Shape = d2graph.Scalar{Value: d2target.ShapeSequenceDiagram}
n1 := g.Root.EnsureChild([]string{"n1"})
n1.Box = geo.NewBox(nil, 100, 100)
g.Edges = []*d2graph.Edge{
{
Src: n1,
Dst: n1,
Index: 0,
Attributes: d2graph.Attributes{
Label: d2graph.Scalar{Value: "left to right"},
},
},
}
ctx := log.WithTB(context.Background(), t, nil)
Layout(ctx, g, func(ctx context.Context, g *d2graph.Graph) error {
return nil
})
route := g.Edges[0].Route
if len(route) != 4 {
t.Fatalf("expected route to have 4 points, got %d", len(route))
}
if route[0].X != route[3].X {
t.Fatalf("route does not end at the same actor, start at %.5f, end at %.5f", route[0].X, route[3].X)
}
if route[3].Y-route[0].Y != MIN_MESSAGE_DISTANCE {
t.Fatalf("expected route height to be %.f5, got %.5f", MIN_MESSAGE_DISTANCE, route[3].Y-route[0].Y)
}
}
func TestSequenceToDescendant(t *testing.T) {
g := d2graph.NewGraph(nil)
g.Root.Attributes.Shape = d2graph.Scalar{Value: d2target.ShapeSequenceDiagram}
a := g.Root.EnsureChild([]string{"a"})
a.Box = geo.NewBox(nil, 100, 100)
a.Attributes = d2graph.Attributes{
Shape: d2graph.Scalar{Value: shape.PERSON_TYPE},
}
a_t1 := a.EnsureChild([]string{"t1"})
a_t1.Box = geo.NewBox(nil, 16, 80)
g.Edges = []*d2graph.Edge{
{
Src: a,
Dst: a_t1,
Index: 0,
}, {
Src: a_t1,
Dst: a,
Index: 0,
},
}
ctx := log.WithTB(context.Background(), t, nil)
Layout(ctx, g, func(ctx context.Context, g *d2graph.Graph) error {
return nil
})
route1 := g.Edges[0].Route
if len(route1) != 4 {
t.Fatal("expected route with 4 points")
}
if route1[0].X != a.Center().X {
t.Fatal("expected route to start at `a` lifeline")
}
if route1[3].X != a_t1.TopLeft.X+a_t1.Width {
t.Fatal("expected route to end at `a.t1` right side")
}
route2 := g.Edges[1].Route
if len(route2) != 4 {
t.Fatal("expected route with 4 points")
}
if route2[0].X != a_t1.TopLeft.X+a_t1.Width {
t.Fatal("expected route to start at `a.t1` right side")
}
if route2[3].X != a.Center().X {
t.Fatal("expected route to end at `a` lifeline")
}
}

View file

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"math" "math"
"sort" "sort"
"strings"
"oss.terrastruct.com/util-go/go2" "oss.terrastruct.com/util-go/go2"
@ -28,9 +29,8 @@ type sequenceDiagram struct {
objectRank map[*d2graph.Object]int objectRank map[*d2graph.Object]int
// keep track of the first and last message of a given actor/span // keep track of the first and last message of a given actor/span
// the message rank is the order in which it appears from top to bottom firstMessage map[*d2graph.Object]*d2graph.Edge
minMessageRank map[*d2graph.Object]int lastMessage map[*d2graph.Object]*d2graph.Edge
maxMessageRank map[*d2graph.Object]int
yStep float64 yStep float64
actorXStep float64 actorXStep float64
@ -137,8 +137,8 @@ func newSequenceDiagram(objects []*d2graph.Object, messages []*d2graph.Edge) *se
notes: nil, notes: nil,
lifelines: nil, lifelines: nil,
objectRank: make(map[*d2graph.Object]int), objectRank: make(map[*d2graph.Object]int),
minMessageRank: make(map[*d2graph.Object]int), firstMessage: make(map[*d2graph.Object]*d2graph.Edge),
maxMessageRank: make(map[*d2graph.Object]int), lastMessage: make(map[*d2graph.Object]*d2graph.Edge),
yStep: MIN_MESSAGE_DISTANCE, yStep: MIN_MESSAGE_DISTANCE,
actorXStep: MIN_ACTOR_DISTANCE, actorXStep: MIN_ACTOR_DISTANCE,
maxActorHeight: 0., maxActorHeight: 0.,
@ -185,18 +185,28 @@ func newSequenceDiagram(objects []*d2graph.Object, messages []*d2graph.Edge) *se
} }
} }
for rank, message := range sd.messages { for _, message := range sd.messages {
sd.verticalIndices[message.AbsID()] = getEdgeEarliestLineNum(message) sd.verticalIndices[message.AbsID()] = getEdgeEarliestLineNum(message)
sd.yStep = math.Max(sd.yStep, float64(message.LabelDimensions.Height)) sd.yStep = math.Max(sd.yStep, float64(message.LabelDimensions.Height))
sd.setMinMaxMessageRank(message.Src, rank)
sd.setMinMaxMessageRank(message.Dst, rank)
// ensures that long labels, spanning over multiple actors, don't make for large gaps between actors // ensures that long labels, spanning over multiple actors, don't make for large gaps between actors
// by distributing the label length across the actors rank difference // by distributing the label length across the actors rank difference
rankDiff := math.Abs(float64(sd.objectRank[message.Src]) - float64(sd.objectRank[message.Dst])) rankDiff := math.Abs(float64(sd.objectRank[message.Src]) - float64(sd.objectRank[message.Dst]))
distributedLabelWidth := float64(message.LabelDimensions.Width) / rankDiff if rankDiff != 0 {
sd.actorXStep = math.Max(sd.actorXStep, distributedLabelWidth+HORIZONTAL_PAD) // rankDiff = 0 for self edges
distributedLabelWidth := float64(message.LabelDimensions.Width) / rankDiff
sd.actorXStep = math.Max(sd.actorXStep, distributedLabelWidth+HORIZONTAL_PAD)
}
sd.lastMessage[message.Src] = message
if _, exists := sd.firstMessage[message.Src]; !exists {
sd.firstMessage[message.Src] = message
}
sd.lastMessage[message.Dst] = message
if _, exists := sd.firstMessage[message.Dst]; !exists {
sd.firstMessage[message.Dst] = message
}
} }
sd.yStep += VERTICAL_PAD sd.yStep += VERTICAL_PAD
@ -208,23 +218,17 @@ func newSequenceDiagram(objects []*d2graph.Object, messages []*d2graph.Edge) *se
return sd return sd
} }
func (sd *sequenceDiagram) setMinMaxMessageRank(actor *d2graph.Object, rank int) { func (sd *sequenceDiagram) layout() error {
if minRank, exists := sd.minMessageRank[actor]; exists {
sd.minMessageRank[actor] = go2.IntMin(minRank, rank)
} else {
sd.minMessageRank[actor] = rank
}
sd.maxMessageRank[actor] = go2.IntMax(sd.maxMessageRank[actor], rank)
}
func (sd *sequenceDiagram) layout() {
sd.placeActors() sd.placeActors()
sd.placeSpans()
sd.placeNotes() sd.placeNotes()
sd.routeMessages() if err := sd.routeMessages(); err != nil {
return err
}
sd.placeSpans()
sd.adjustRouteEndpoints()
sd.placeGroups() sd.placeGroups()
sd.addLifelineEdges() sd.addLifelineEdges()
return nil
} }
func (sd *sequenceDiagram) placeGroups() { func (sd *sequenceDiagram) placeGroups() {
@ -313,11 +317,10 @@ func (sd *sequenceDiagram) placeActors() {
// │ // │
// │ // │
func (sd *sequenceDiagram) addLifelineEdges() { func (sd *sequenceDiagram) addLifelineEdges() {
lastRoute := sd.messages[len(sd.messages)-1].Route
endY := 0. endY := 0.
for _, m := range sd.messages { for _, p := range lastRoute {
for _, p := range m.Route { endY = math.Max(endY, p.Y)
endY = math.Max(endY, p.Y)
}
} }
for _, note := range sd.notes { for _, note := range sd.notes {
endY = math.Max(endY, note.TopLeft.Y+note.Height) endY = math.Max(endY, note.TopLeft.Y+note.Height)
@ -415,12 +418,21 @@ func (sd *sequenceDiagram) placeSpans() {
// finds the position if there are messages to this span // finds the position if there are messages to this span
minMessageY := math.Inf(1) minMessageY := math.Inf(1)
if minRank, exists := sd.minMessageRank[span]; exists { if firstMessage, exists := sd.firstMessage[span]; exists {
minMessageY = sd.getMessageY(minRank) // needs to check Src/Dst because of self-edges or edges to/from descendants
if span == firstMessage.Src {
minMessageY = firstMessage.Route[0].Y
} else {
minMessageY = firstMessage.Route[len(firstMessage.Route)-1].Y
}
} }
maxMessageY := math.Inf(-1) maxMessageY := math.Inf(-1)
if maxRank, exists := sd.maxMessageRank[span]; exists { if lastMessage, exists := sd.lastMessage[span]; exists {
maxMessageY = sd.getMessageY(maxRank) if span == lastMessage.Src {
maxMessageY = lastMessage.Route[0].Y
} else {
maxMessageY = lastMessage.Route[len(lastMessage.Route)-1].Y
}
} }
// if it is the same as the child top left, add some padding // if it is the same as the child top left, add some padding
@ -442,52 +454,89 @@ func (sd *sequenceDiagram) placeSpans() {
} }
} }
// routeMessages routes horizontal edges (messages) from Src to Dst // routeMessages routes horizontal edges (messages) from Src to Dst lifeline (actor/span center)
func (sd *sequenceDiagram) routeMessages() { // in another step, routes are adjusted to spans borders when necessary
for rank, message := range sd.messages { func (sd *sequenceDiagram) routeMessages() error {
message.ZIndex = 2 messageOffset := sd.maxActorHeight + sd.yStep
isLeftToRight := message.Src.TopLeft.X < message.Dst.TopLeft.X for _, message := range sd.messages {
noteOffset := 0.
// finds the proper anchor point based on the message direction
var startX, endX float64
if sd.isActor(message.Src) {
startX = message.Src.Center().X
} else if isLeftToRight {
startX = message.Src.TopLeft.X + message.Src.Width
} else {
startX = message.Src.TopLeft.X
}
if sd.isActor(message.Dst) {
endX = message.Dst.Center().X
} else if isLeftToRight {
endX = message.Dst.TopLeft.X
} else {
endX = message.Dst.TopLeft.X + message.Dst.Width
}
messageY := sd.getMessageY(rank)
for _, note := range sd.notes { for _, note := range sd.notes {
if sd.verticalIndices[note.AbsID()] < sd.verticalIndices[message.AbsID()] { if sd.verticalIndices[note.AbsID()] < sd.verticalIndices[message.AbsID()] {
messageY += note.Height + sd.yStep noteOffset += note.Height + sd.yStep
} }
} }
startY := messageOffset + noteOffset
message.Route = []*geo.Point{ message.ZIndex = 2
geo.NewPoint(startX, messageY), var startX, endX float64
geo.NewPoint(endX, messageY), if startCenter := getCenter(message.Src); startCenter != nil {
startX = startCenter.X
} else {
return fmt.Errorf("could not find center of %s", message.Src.AbsID())
} }
if endCenter := getCenter(message.Dst); endCenter != nil {
endX = endCenter.X
} else {
return fmt.Errorf("could not find center of %s", message.Dst.AbsID())
}
isToDescendant := strings.HasPrefix(message.Dst.AbsID(), message.Src.AbsID())
isFromDescendant := strings.HasPrefix(message.Src.AbsID(), message.Dst.AbsID())
isSelfMessage := message.Src == message.Dst
if isSelfMessage || isToDescendant || isFromDescendant {
midX := startX + MIN_MESSAGE_DISTANCE
endY := startY + MIN_MESSAGE_DISTANCE
message.Route = []*geo.Point{
geo.NewPoint(startX, startY),
geo.NewPoint(midX, startY),
geo.NewPoint(midX, endY),
geo.NewPoint(endX, endY),
}
} else {
message.Route = []*geo.Point{
geo.NewPoint(startX, startY),
geo.NewPoint(endX, startY),
}
}
messageOffset += sd.yStep
if message.Attributes.Label.Value != "" { if message.Attributes.Label.Value != "" {
message.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter)) message.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
} }
} }
return nil
} }
func (sd *sequenceDiagram) getMessageY(rank int) float64 { func getCenter(obj *d2graph.Object) *geo.Point {
// +1 so that the first message has the top padding for its label if obj == nil {
return ((float64(rank) + 1.) * sd.yStep) + sd.maxActorHeight return nil
} else if obj.TopLeft != nil {
return obj.Center()
}
return getCenter(obj.Parent)
}
// adjustRouteEndpoints adjust the first and last points of message routes when they are spans
// routeMessages() will route to the actor lifelife as a reference point and this function
// adjust to span width when necessary
func (sd *sequenceDiagram) adjustRouteEndpoints() {
for _, message := range sd.messages {
route := message.Route
if !sd.isActor(message.Src) {
if sd.objectRank[message.Src] <= sd.objectRank[message.Dst] {
route[0].X += message.Src.Width / 2.
} else {
route[0].X -= message.Src.Width / 2.
}
}
if !sd.isActor(message.Dst) {
if sd.objectRank[message.Src] < sd.objectRank[message.Dst] {
route[len(route)-1].X -= message.Dst.Width / 2.
} else {
route[len(route)-1].X += message.Dst.Width / 2.
}
}
}
} }
func (sd *sequenceDiagram) isActor(obj *d2graph.Object) bool { func (sd *sequenceDiagram) isActor(obj *d2graph.Object) bool {
@ -501,8 +550,7 @@ func (sd *sequenceDiagram) getWidth() float64 {
} }
func (sd *sequenceDiagram) getHeight() float64 { func (sd *sequenceDiagram) getHeight() float64 {
// the layout is always placed starting at 0, so the height is just the last message return sd.lifelines[0].Route[1].Y
return sd.getMessageY(len(sd.messages))
} }
func (sd *sequenceDiagram) shift(tl *geo.Point) { func (sd *sequenceDiagram) shift(tl *geo.Point) {

View file

@ -1325,6 +1325,16 @@ s -> t`,
z -> y z -> y
z -> z: hello z -> z: hello
`, `,
}, {
name: "sequence_diagram_self_edges",
script: `shape: sequence_diagram
a -> a: a self edge here
a -> b: between actors
b -> b.1: to descendant
b.1 -> b.1.2: to deeper descendant
b.1.2 -> b: to parent
b -> a.1.2: actor
a.1 -> b.3`,
}, },
{ {
name: "icon-label", name: "icon-label",

View file

@ -9,7 +9,7 @@
"y": 0 "y": 0
}, },
"width": 3633, "width": 3633,
"height": 2055, "height": 2311,
"opacity": 1, "opacity": 1,
"strokeDash": 0, "strokeDash": 0,
"strokeWidth": 2, "strokeWidth": 2,
@ -357,7 +357,7 @@
"type": "rectangle", "type": "rectangle",
"pos": { "pos": {
"x": 1304, "x": 1304,
"y": 869 "y": 1125
}, },
"width": 20, "width": 20,
"height": 422, "height": 422,
@ -473,7 +473,7 @@
"type": "rectangle", "type": "rectangle",
"pos": { "pos": {
"x": 1727, "x": 1727,
"y": 1649 "y": 1905
}, },
"width": 20, "width": 20,
"height": 292, "height": 292,

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 479 KiB

After

Width:  |  Height:  |  Size: 479 KiB

View file

@ -9,7 +9,7 @@
"y": 12 "y": 12
}, },
"width": 3633, "width": 3633,
"height": 2055, "height": 2311,
"opacity": 1, "opacity": 1,
"strokeDash": 0, "strokeDash": 0,
"strokeWidth": 2, "strokeWidth": 2,
@ -357,7 +357,7 @@
"type": "rectangle", "type": "rectangle",
"pos": { "pos": {
"x": 1316, "x": 1316,
"y": 881 "y": 1137
}, },
"width": 20, "width": 20,
"height": 422, "height": 422,
@ -473,7 +473,7 @@
"type": "rectangle", "type": "rectangle",
"pos": { "pos": {
"x": 1739, "x": 1739,
"y": 1661 "y": 1917
}, },
"width": 20, "width": 20,
"height": 292, "height": 292,

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 479 KiB

After

Width:  |  Height:  |  Size: 479 KiB

View file

@ -0,0 +1,658 @@
{
"name": "",
"shapes": [
{
"id": "a",
"type": "",
"pos": {
"x": 0,
"y": 50
},
"width": 150,
"height": 169,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "#E3E9FD",
"stroke": "#0D32B2",
"shadow": false,
"3d": false,
"multiple": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"fields": null,
"methods": null,
"columns": null,
"label": "a",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#0A0F25",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 12,
"labelHeight": 26,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
},
{
"id": "b",
"type": "",
"pos": {
"x": 400,
"y": 52
},
"width": 150,
"height": 167,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "#E3E9FD",
"stroke": "#0D32B2",
"shadow": false,
"3d": false,
"multiple": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"fields": null,
"methods": null,
"columns": null,
"label": "b",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#0A0F25",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 13,
"labelHeight": 26,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
},
{
"id": "b.1",
"type": "rectangle",
"pos": {
"x": 469,
"y": 673
},
"width": 12,
"height": 228,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "#EDF0FD",
"stroke": "#0D32B2",
"shadow": false,
"3d": false,
"multiple": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"fields": null,
"methods": null,
"columns": null,
"label": "",
"fontSize": 24,
"fontFamily": "DEFAULT",
"language": "",
"color": "#0A0F25",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 15,
"labelHeight": 36,
"zIndex": 1,
"level": 2
},
{
"id": "b.1.2",
"type": "rectangle",
"pos": {
"x": 465,
"y": 803
},
"width": 20,
"height": 82,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "#F7F8FE",
"stroke": "#0D32B2",
"shadow": false,
"3d": false,
"multiple": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"fields": null,
"methods": null,
"columns": null,
"label": "",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#0A0F25",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 13,
"labelHeight": 26,
"zIndex": 1,
"level": 3
},
{
"id": "a.1",
"type": "rectangle",
"pos": {
"x": 69,
"y": 967
},
"width": 12,
"height": 178,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "#EDF0FD",
"stroke": "#0D32B2",
"shadow": false,
"3d": false,
"multiple": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"fields": null,
"methods": null,
"columns": null,
"label": "",
"fontSize": 24,
"fontFamily": "DEFAULT",
"language": "",
"color": "#0A0F25",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 15,
"labelHeight": 36,
"zIndex": 1,
"level": 2
},
{
"id": "a.1.2",
"type": "rectangle",
"pos": {
"x": 65,
"y": 983
},
"width": 20,
"height": 80,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "#F7F8FE",
"stroke": "#0D32B2",
"shadow": false,
"3d": false,
"multiple": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"fields": null,
"methods": null,
"columns": null,
"label": "",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#0A0F25",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 13,
"labelHeight": 26,
"zIndex": 1,
"level": 3
},
{
"id": "b.3",
"type": "rectangle",
"pos": {
"x": 469,
"y": 1113
},
"width": 12,
"height": 80,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "#EDF0FD",
"stroke": "#0D32B2",
"shadow": false,
"3d": false,
"multiple": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"fields": null,
"methods": null,
"columns": null,
"label": "",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#0A0F25",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 13,
"labelHeight": 26,
"zIndex": 1,
"level": 2
}
],
"connections": [
{
"id": "(a -> a)[0]",
"src": "a",
"srcArrow": "none",
"srcLabel": "",
"dst": "a",
"dstArrow": "triangle",
"dstLabel": "",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "#0D32B2",
"label": "a self edge here",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#676C7E",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 103,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"labelPercentage": 0,
"route": [
{
"x": 75,
"y": 349
},
{
"x": 155,
"y": 349
},
{
"x": 155,
"y": 429
},
{
"x": 75,
"y": 429
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 2
},
{
"id": "(a -> b)[0]",
"src": "a",
"srcArrow": "none",
"srcLabel": "",
"dst": "b",
"dstArrow": "triangle",
"dstLabel": "",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "#0D32B2",
"label": "between actors",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#676C7E",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 102,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"labelPercentage": 0,
"route": [
{
"x": 75,
"y": 479
},
{
"x": 475,
"y": 479
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 2
},
{
"id": "(b -> b.1)[0]",
"src": "b",
"srcArrow": "none",
"srcLabel": "",
"dst": "b.1",
"dstArrow": "triangle",
"dstLabel": "",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "#0D32B2",
"label": "to descendant",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#676C7E",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 94,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"labelPercentage": 0,
"route": [
{
"x": 475,
"y": 609
},
{
"x": 555,
"y": 609
},
{
"x": 555,
"y": 689
},
{
"x": 481,
"y": 689
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 2
},
{
"id": "b.(1 -> 1.2)[0]",
"src": "b.1",
"srcArrow": "none",
"srcLabel": "",
"dst": "b.1.2",
"dstArrow": "triangle",
"dstLabel": "",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "#0D32B2",
"label": "to deeper descendant",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#676C7E",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 143,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"labelPercentage": 0,
"route": [
{
"x": 481,
"y": 739
},
{
"x": 555,
"y": 739
},
{
"x": 555,
"y": 819
},
{
"x": 485,
"y": 819
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 2
},
{
"id": "(b.1.2 -> b)[0]",
"src": "b.1.2",
"srcArrow": "none",
"srcLabel": "",
"dst": "b",
"dstArrow": "triangle",
"dstLabel": "",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "#0D32B2",
"label": "to parent",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#676C7E",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 62,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"labelPercentage": 0,
"route": [
{
"x": 485,
"y": 869
},
{
"x": 555,
"y": 869
},
{
"x": 555,
"y": 949
},
{
"x": 475,
"y": 949
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 2
},
{
"id": "(b -> a.1.2)[0]",
"src": "b",
"srcArrow": "none",
"srcLabel": "",
"dst": "a.1.2",
"dstArrow": "triangle",
"dstLabel": "",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "#0D32B2",
"label": "actor",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#676C7E",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 36,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"labelPercentage": 0,
"route": [
{
"x": 475,
"y": 999
},
{
"x": 85,
"y": 999
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 2
},
{
"id": "(a.1 -> b.3)[0]",
"src": "a.1",
"srcArrow": "none",
"srcLabel": "",
"dst": "b.3",
"dstArrow": "triangle",
"dstLabel": "",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "#0D32B2",
"label": "",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#676C7E",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 0,
"labelHeight": 0,
"labelPosition": "",
"labelPercentage": 0,
"route": [
{
"x": 81,
"y": 1129
},
{
"x": 469,
"y": 1129
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 2
},
{
"id": "(a -- )[0]",
"src": "a",
"srcArrow": "none",
"srcLabel": "",
"dst": "a-lifeline-end-2251863791",
"dstArrow": "none",
"dstLabel": "",
"opacity": 1,
"strokeDash": 6,
"strokeWidth": 2,
"stroke": "#0D32B2",
"label": "",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#676C7E",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 0,
"labelHeight": 0,
"labelPosition": "",
"labelPercentage": 0,
"route": [
{
"x": 75,
"y": 219
},
{
"x": 75,
"y": 1259
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 0
},
{
"id": "(b -- )[0]",
"src": "b",
"srcArrow": "none",
"srcLabel": "",
"dst": "b-lifeline-end-668380428",
"dstArrow": "none",
"dstLabel": "",
"opacity": 1,
"strokeDash": 6,
"strokeWidth": 2,
"stroke": "#0D32B2",
"label": "",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#676C7E",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 0,
"labelHeight": 0,
"labelPosition": "",
"labelPercentage": 0,
"route": [
{
"x": 475,
"y": 219
},
{
"x": 475,
"y": 1259
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 0
}
]
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 472 KiB

View file

@ -0,0 +1,658 @@
{
"name": "",
"shapes": [
{
"id": "a",
"type": "",
"pos": {
"x": 0,
"y": 50
},
"width": 150,
"height": 169,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "#E3E9FD",
"stroke": "#0D32B2",
"shadow": false,
"3d": false,
"multiple": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"fields": null,
"methods": null,
"columns": null,
"label": "a",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#0A0F25",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 12,
"labelHeight": 26,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
},
{
"id": "b",
"type": "",
"pos": {
"x": 400,
"y": 52
},
"width": 150,
"height": 167,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "#E3E9FD",
"stroke": "#0D32B2",
"shadow": false,
"3d": false,
"multiple": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"fields": null,
"methods": null,
"columns": null,
"label": "b",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#0A0F25",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 13,
"labelHeight": 26,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
},
{
"id": "b.1",
"type": "rectangle",
"pos": {
"x": 469,
"y": 673
},
"width": 12,
"height": 228,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "#EDF0FD",
"stroke": "#0D32B2",
"shadow": false,
"3d": false,
"multiple": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"fields": null,
"methods": null,
"columns": null,
"label": "",
"fontSize": 24,
"fontFamily": "DEFAULT",
"language": "",
"color": "#0A0F25",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 15,
"labelHeight": 36,
"zIndex": 1,
"level": 2
},
{
"id": "b.1.2",
"type": "rectangle",
"pos": {
"x": 465,
"y": 803
},
"width": 20,
"height": 82,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "#F7F8FE",
"stroke": "#0D32B2",
"shadow": false,
"3d": false,
"multiple": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"fields": null,
"methods": null,
"columns": null,
"label": "",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#0A0F25",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 13,
"labelHeight": 26,
"zIndex": 1,
"level": 3
},
{
"id": "a.1",
"type": "rectangle",
"pos": {
"x": 69,
"y": 967
},
"width": 12,
"height": 178,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "#EDF0FD",
"stroke": "#0D32B2",
"shadow": false,
"3d": false,
"multiple": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"fields": null,
"methods": null,
"columns": null,
"label": "",
"fontSize": 24,
"fontFamily": "DEFAULT",
"language": "",
"color": "#0A0F25",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 15,
"labelHeight": 36,
"zIndex": 1,
"level": 2
},
{
"id": "a.1.2",
"type": "rectangle",
"pos": {
"x": 65,
"y": 983
},
"width": 20,
"height": 80,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "#F7F8FE",
"stroke": "#0D32B2",
"shadow": false,
"3d": false,
"multiple": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"fields": null,
"methods": null,
"columns": null,
"label": "",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#0A0F25",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 13,
"labelHeight": 26,
"zIndex": 1,
"level": 3
},
{
"id": "b.3",
"type": "rectangle",
"pos": {
"x": 469,
"y": 1113
},
"width": 12,
"height": 80,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "#EDF0FD",
"stroke": "#0D32B2",
"shadow": false,
"3d": false,
"multiple": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"fields": null,
"methods": null,
"columns": null,
"label": "",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#0A0F25",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 13,
"labelHeight": 26,
"zIndex": 1,
"level": 2
}
],
"connections": [
{
"id": "(a -> a)[0]",
"src": "a",
"srcArrow": "none",
"srcLabel": "",
"dst": "a",
"dstArrow": "triangle",
"dstLabel": "",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "#0D32B2",
"label": "a self edge here",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#676C7E",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 103,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"labelPercentage": 0,
"route": [
{
"x": 75,
"y": 349
},
{
"x": 155,
"y": 349
},
{
"x": 155,
"y": 429
},
{
"x": 75,
"y": 429
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 2
},
{
"id": "(a -> b)[0]",
"src": "a",
"srcArrow": "none",
"srcLabel": "",
"dst": "b",
"dstArrow": "triangle",
"dstLabel": "",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "#0D32B2",
"label": "between actors",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#676C7E",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 102,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"labelPercentage": 0,
"route": [
{
"x": 75,
"y": 479
},
{
"x": 475,
"y": 479
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 2
},
{
"id": "(b -> b.1)[0]",
"src": "b",
"srcArrow": "none",
"srcLabel": "",
"dst": "b.1",
"dstArrow": "triangle",
"dstLabel": "",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "#0D32B2",
"label": "to descendant",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#676C7E",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 94,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"labelPercentage": 0,
"route": [
{
"x": 475,
"y": 609
},
{
"x": 555,
"y": 609
},
{
"x": 555,
"y": 689
},
{
"x": 481,
"y": 689
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 2
},
{
"id": "b.(1 -> 1.2)[0]",
"src": "b.1",
"srcArrow": "none",
"srcLabel": "",
"dst": "b.1.2",
"dstArrow": "triangle",
"dstLabel": "",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "#0D32B2",
"label": "to deeper descendant",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#676C7E",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 143,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"labelPercentage": 0,
"route": [
{
"x": 481,
"y": 739
},
{
"x": 555,
"y": 739
},
{
"x": 555,
"y": 819
},
{
"x": 485,
"y": 819
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 2
},
{
"id": "(b.1.2 -> b)[0]",
"src": "b.1.2",
"srcArrow": "none",
"srcLabel": "",
"dst": "b",
"dstArrow": "triangle",
"dstLabel": "",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "#0D32B2",
"label": "to parent",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#676C7E",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 62,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"labelPercentage": 0,
"route": [
{
"x": 485,
"y": 869
},
{
"x": 555,
"y": 869
},
{
"x": 555,
"y": 949
},
{
"x": 475,
"y": 949
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 2
},
{
"id": "(b -> a.1.2)[0]",
"src": "b",
"srcArrow": "none",
"srcLabel": "",
"dst": "a.1.2",
"dstArrow": "triangle",
"dstLabel": "",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "#0D32B2",
"label": "actor",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#676C7E",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 36,
"labelHeight": 21,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"labelPercentage": 0,
"route": [
{
"x": 475,
"y": 999
},
{
"x": 85,
"y": 999
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 2
},
{
"id": "(a.1 -> b.3)[0]",
"src": "a.1",
"srcArrow": "none",
"srcLabel": "",
"dst": "b.3",
"dstArrow": "triangle",
"dstLabel": "",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "#0D32B2",
"label": "",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#676C7E",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 0,
"labelHeight": 0,
"labelPosition": "",
"labelPercentage": 0,
"route": [
{
"x": 81,
"y": 1129
},
{
"x": 469,
"y": 1129
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 2
},
{
"id": "(a -- )[0]",
"src": "a",
"srcArrow": "none",
"srcLabel": "",
"dst": "a-lifeline-end-2251863791",
"dstArrow": "none",
"dstLabel": "",
"opacity": 1,
"strokeDash": 6,
"strokeWidth": 2,
"stroke": "#0D32B2",
"label": "",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#676C7E",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 0,
"labelHeight": 0,
"labelPosition": "",
"labelPercentage": 0,
"route": [
{
"x": 75,
"y": 219
},
{
"x": 75,
"y": 1259
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 0
},
{
"id": "(b -- )[0]",
"src": "b",
"srcArrow": "none",
"srcLabel": "",
"dst": "b-lifeline-end-668380428",
"dstArrow": "none",
"dstLabel": "",
"opacity": 1,
"strokeDash": 6,
"strokeWidth": 2,
"stroke": "#0D32B2",
"label": "",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#676C7E",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 0,
"labelHeight": 0,
"labelPosition": "",
"labelPercentage": 0,
"route": [
{
"x": 475,
"y": 219
},
{
"x": 475,
"y": 1259
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 0
}
]
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 472 KiB