diff --git a/d2layouts/d2sequence/layout.go b/d2layouts/d2sequence/layout.go
index b11df4eaf..3f8650259 100644
--- a/d2layouts/d2sequence/layout.go
+++ b/d2layouts/d2sequence/layout.go
@@ -37,7 +37,10 @@ func Layout(ctx context.Context, g *d2graph.Graph, layout func(ctx context.Conte
continue
}
- sd := layoutSequenceDiagram(g, obj)
+ sd, err := layoutSequenceDiagram(g, obj)
+ if err != nil {
+ return err
+ }
obj.Children = make(map[string]*d2graph.Object)
obj.ChildrenArray = nil
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
-func layoutSequenceDiagram(g *d2graph.Graph, obj *d2graph.Object) *sequenceDiagram {
+func layoutSequenceDiagram(g *d2graph.Graph, obj *d2graph.Object) (*sequenceDiagram, error) {
var edges []*d2graph.Edge
for _, edge := range g.Edges {
// 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.layout()
- return sd
+ err := sd.layout()
+ return sd, err
}
func getLayoutEdges(g *d2graph.Graph, toRemove map[*d2graph.Edge]struct{}) ([]*d2graph.Edge, map[string]int) {
diff --git a/d2layouts/d2sequence/layout_test.go b/d2layouts/d2sequence/layout_test.go
index 4ffce67a9..5f2368c9b 100644
--- a/d2layouts/d2sequence/layout_test.go
+++ b/d2layouts/d2sequence/layout_test.go
@@ -159,6 +159,7 @@ func TestSpansSequenceDiagram(t *testing.T) {
// ├┐──────────────────────►
// t2 ││ │
// ├┘◄─────────────────────┤
+
input := `
shape: sequence_diagram
a: { shape: person }
@@ -182,7 +183,6 @@ b -> a.t2`
a, has := g.Root.HasChild([]string{"a"})
assert.True(t, has)
- a.Box = geo.NewBox(nil, 100, 100)
a_t1, has := a.HasChild([]string{"t1"})
assert.True(t, has)
@@ -197,6 +197,12 @@ b -> a.t2`
b_t1, has := b.HasChild([]string{"t1"})
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 {
// just set some position as if it had been properly placed
for _, obj := range g.Objects {
@@ -304,6 +310,7 @@ container -> c: edge 1
a_t1, has := a.HasChild([]string{"t1"})
assert.True(t, has)
+ a_t1.Box = geo.NewBox(nil, 100, 100)
b, has := container.HasChild([]string{"b"})
assert.True(t, has)
@@ -311,6 +318,7 @@ container -> c: edge 1
b_t1, has := b.HasChild([]string{"t1"})
assert.True(t, has)
+ b_t1.Box = geo.NewBox(nil, 100, 100)
c := g.Root.EnsureChild([]string{"c"})
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")
+ }
+}
diff --git a/d2layouts/d2sequence/sequence_diagram.go b/d2layouts/d2sequence/sequence_diagram.go
index 88f49af3f..3df170756 100644
--- a/d2layouts/d2sequence/sequence_diagram.go
+++ b/d2layouts/d2sequence/sequence_diagram.go
@@ -4,6 +4,7 @@ import (
"fmt"
"math"
"sort"
+ "strings"
"oss.terrastruct.com/util-go/go2"
@@ -28,9 +29,8 @@ type sequenceDiagram struct {
objectRank map[*d2graph.Object]int
// 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
- minMessageRank map[*d2graph.Object]int
- maxMessageRank map[*d2graph.Object]int
+ firstMessage map[*d2graph.Object]*d2graph.Edge
+ lastMessage map[*d2graph.Object]*d2graph.Edge
yStep float64
actorXStep float64
@@ -137,8 +137,8 @@ func newSequenceDiagram(objects []*d2graph.Object, messages []*d2graph.Edge) *se
notes: nil,
lifelines: nil,
objectRank: make(map[*d2graph.Object]int),
- minMessageRank: make(map[*d2graph.Object]int),
- maxMessageRank: make(map[*d2graph.Object]int),
+ firstMessage: make(map[*d2graph.Object]*d2graph.Edge),
+ lastMessage: make(map[*d2graph.Object]*d2graph.Edge),
yStep: MIN_MESSAGE_DISTANCE,
actorXStep: MIN_ACTOR_DISTANCE,
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.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
// by distributing the label length across the actors rank difference
rankDiff := math.Abs(float64(sd.objectRank[message.Src]) - float64(sd.objectRank[message.Dst]))
- distributedLabelWidth := float64(message.LabelDimensions.Width) / rankDiff
- sd.actorXStep = math.Max(sd.actorXStep, distributedLabelWidth+HORIZONTAL_PAD)
+ if rankDiff != 0 {
+ // 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
@@ -208,23 +218,17 @@ func newSequenceDiagram(objects []*d2graph.Object, messages []*d2graph.Edge) *se
return sd
}
-func (sd *sequenceDiagram) setMinMaxMessageRank(actor *d2graph.Object, rank int) {
- 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() {
+func (sd *sequenceDiagram) layout() error {
sd.placeActors()
- sd.placeSpans()
sd.placeNotes()
- sd.routeMessages()
+ if err := sd.routeMessages(); err != nil {
+ return err
+ }
+ sd.placeSpans()
+ sd.adjustRouteEndpoints()
sd.placeGroups()
sd.addLifelineEdges()
+ return nil
}
func (sd *sequenceDiagram) placeGroups() {
@@ -313,11 +317,10 @@ func (sd *sequenceDiagram) placeActors() {
// │
// │
func (sd *sequenceDiagram) addLifelineEdges() {
+ lastRoute := sd.messages[len(sd.messages)-1].Route
endY := 0.
- for _, m := range sd.messages {
- for _, p := range m.Route {
- endY = math.Max(endY, p.Y)
- }
+ for _, p := range lastRoute {
+ endY = math.Max(endY, p.Y)
}
for _, note := range sd.notes {
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
minMessageY := math.Inf(1)
- if minRank, exists := sd.minMessageRank[span]; exists {
- minMessageY = sd.getMessageY(minRank)
+ if firstMessage, exists := sd.firstMessage[span]; exists {
+ // 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)
- if maxRank, exists := sd.maxMessageRank[span]; exists {
- maxMessageY = sd.getMessageY(maxRank)
+ if lastMessage, exists := sd.lastMessage[span]; exists {
+ 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
@@ -442,52 +454,89 @@ func (sd *sequenceDiagram) placeSpans() {
}
}
-// routeMessages routes horizontal edges (messages) from Src to Dst
-func (sd *sequenceDiagram) routeMessages() {
- for rank, message := range sd.messages {
- message.ZIndex = 2
- isLeftToRight := message.Src.TopLeft.X < message.Dst.TopLeft.X
-
- // 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)
-
+// routeMessages routes horizontal edges (messages) from Src to Dst lifeline (actor/span center)
+// in another step, routes are adjusted to spans borders when necessary
+func (sd *sequenceDiagram) routeMessages() error {
+ messageOffset := sd.maxActorHeight + sd.yStep
+ for _, message := range sd.messages {
+ noteOffset := 0.
for _, note := range sd.notes {
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{
- geo.NewPoint(startX, messageY),
- geo.NewPoint(endX, messageY),
+ message.ZIndex = 2
+ var startX, endX float64
+ 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 != "" {
message.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
}
}
+ return nil
}
-func (sd *sequenceDiagram) getMessageY(rank int) float64 {
- // +1 so that the first message has the top padding for its label
- return ((float64(rank) + 1.) * sd.yStep) + sd.maxActorHeight
+func getCenter(obj *d2graph.Object) *geo.Point {
+ if obj == nil {
+ 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 {
@@ -501,8 +550,7 @@ func (sd *sequenceDiagram) getWidth() float64 {
}
func (sd *sequenceDiagram) getHeight() float64 {
- // the layout is always placed starting at 0, so the height is just the last message
- return sd.getMessageY(len(sd.messages))
+ return sd.lifelines[0].Route[1].Y
}
func (sd *sequenceDiagram) shift(tl *geo.Point) {
diff --git a/e2etests/stable_test.go b/e2etests/stable_test.go
index 61cbdff7e..d256a29ed 100644
--- a/e2etests/stable_test.go
+++ b/e2etests/stable_test.go
@@ -1325,6 +1325,16 @@ s -> t`,
z -> y
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",
diff --git a/e2etests/testdata/stable/sequence_diagram_real/dagre/board.exp.json b/e2etests/testdata/stable/sequence_diagram_real/dagre/board.exp.json
index 296963097..4381d3065 100644
--- a/e2etests/testdata/stable/sequence_diagram_real/dagre/board.exp.json
+++ b/e2etests/testdata/stable/sequence_diagram_real/dagre/board.exp.json
@@ -9,7 +9,7 @@
"y": 0
},
"width": 3633,
- "height": 2055,
+ "height": 2311,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
@@ -357,7 +357,7 @@
"type": "rectangle",
"pos": {
"x": 1304,
- "y": 869
+ "y": 1125
},
"width": 20,
"height": 422,
@@ -473,7 +473,7 @@
"type": "rectangle",
"pos": {
"x": 1727,
- "y": 1649
+ "y": 1905
},
"width": 20,
"height": 292,
diff --git a/e2etests/testdata/stable/sequence_diagram_real/dagre/sketch.exp.svg b/e2etests/testdata/stable/sequence_diagram_real/dagre/sketch.exp.svg
index b00bf663b..818b2370e 100644
--- a/e2etests/testdata/stable/sequence_diagram_real/dagre/sketch.exp.svg
+++ b/e2etests/testdata/stable/sequence_diagram_real/dagre/sketch.exp.svg
@@ -14,7 +14,7 @@ width="3833" height="2511" viewBox="-100 -100 3833 2511">How this is renderedCLId2astd2compilerd2layoutd2exporterd2themesd2rendererd2sequencelayoutd2dagrelayoutmeasurements also take place 'How this is rendered: {...}'tokenized ASTcompile ASTobjects and edgesrun layout enginesrun engine on shape: sequence_diagram, temporarily removerun core engine on rest add back in sequence diagramsdiagram with correct positions and dimensionsexport diagram with chosen theme and rendererget theme stylesrender to SVGresulting SVG
+How this is renderedCLId2astd2compilerd2layoutd2exporterd2themesd2rendererd2sequencelayoutd2dagrelayoutmeasurements also take place 'How this is rendered: {...}'tokenized ASTcompile ASTobjects and edgesrun layout enginesrun engine on shape: sequence_diagram, temporarily removerun core engine on rest add back in sequence diagramsdiagram with correct positions and dimensionsexport diagram with chosen theme and rendererget theme stylesrender to SVGresulting SVG
diff --git a/e2etests/testdata/stable/sequence_diagram_real/elk/board.exp.json b/e2etests/testdata/stable/sequence_diagram_real/elk/board.exp.json
index 5c2f76e22..4ca4af78c 100644
--- a/e2etests/testdata/stable/sequence_diagram_real/elk/board.exp.json
+++ b/e2etests/testdata/stable/sequence_diagram_real/elk/board.exp.json
@@ -9,7 +9,7 @@
"y": 12
},
"width": 3633,
- "height": 2055,
+ "height": 2311,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
@@ -357,7 +357,7 @@
"type": "rectangle",
"pos": {
"x": 1316,
- "y": 881
+ "y": 1137
},
"width": 20,
"height": 422,
@@ -473,7 +473,7 @@
"type": "rectangle",
"pos": {
"x": 1739,
- "y": 1661
+ "y": 1917
},
"width": 20,
"height": 292,
diff --git a/e2etests/testdata/stable/sequence_diagram_real/elk/sketch.exp.svg b/e2etests/testdata/stable/sequence_diagram_real/elk/sketch.exp.svg
index 96aba78e2..a9f8aa371 100644
--- a/e2etests/testdata/stable/sequence_diagram_real/elk/sketch.exp.svg
+++ b/e2etests/testdata/stable/sequence_diagram_real/elk/sketch.exp.svg
@@ -14,7 +14,7 @@ width="3833" height="2511" viewBox="-88 -88 3833 2511">How this is renderedCLId2astd2compilerd2layoutd2exporterd2themesd2rendererd2sequencelayoutd2dagrelayoutmeasurements also take place 'How this is rendered: {...}'tokenized ASTcompile ASTobjects and edgesrun layout enginesrun engine on shape: sequence_diagram, temporarily removerun core engine on rest add back in sequence diagramsdiagram with correct positions and dimensionsexport diagram with chosen theme and rendererget theme stylesrender to SVGresulting SVG
+How this is renderedCLId2astd2compilerd2layoutd2exporterd2themesd2rendererd2sequencelayoutd2dagrelayoutmeasurements also take place 'How this is rendered: {...}'tokenized ASTcompile ASTobjects and edgesrun layout enginesrun engine on shape: sequence_diagram, temporarily removerun core engine on rest add back in sequence diagramsdiagram with correct positions and dimensionsexport diagram with chosen theme and rendererget theme stylesrender to SVGresulting SVG
diff --git a/e2etests/testdata/stable/sequence_diagram_self_edges/dagre/board.exp.json b/e2etests/testdata/stable/sequence_diagram_self_edges/dagre/board.exp.json
new file mode 100644
index 000000000..7bb323ba1
--- /dev/null
+++ b/e2etests/testdata/stable/sequence_diagram_self_edges/dagre/board.exp.json
@@ -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
+ }
+ ]
+}
diff --git a/e2etests/testdata/stable/sequence_diagram_self_edges/dagre/sketch.exp.svg b/e2etests/testdata/stable/sequence_diagram_self_edges/dagre/sketch.exp.svg
new file mode 100644
index 000000000..fe2e24cb0
--- /dev/null
+++ b/e2etests/testdata/stable/sequence_diagram_self_edges/dagre/sketch.exp.svg
@@ -0,0 +1,39 @@
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/stable/sequence_diagram_self_edges/elk/board.exp.json b/e2etests/testdata/stable/sequence_diagram_self_edges/elk/board.exp.json
new file mode 100644
index 000000000..7bb323ba1
--- /dev/null
+++ b/e2etests/testdata/stable/sequence_diagram_self_edges/elk/board.exp.json
@@ -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
+ }
+ ]
+}
diff --git a/e2etests/testdata/stable/sequence_diagram_self_edges/elk/sketch.exp.svg b/e2etests/testdata/stable/sequence_diagram_self_edges/elk/sketch.exp.svg
new file mode 100644
index 000000000..fe2e24cb0
--- /dev/null
+++ b/e2etests/testdata/stable/sequence_diagram_self_edges/elk/sketch.exp.svg
@@ -0,0 +1,39 @@
+
+
\ No newline at end of file