From b8355c7fa9c5a710e2ce1dda74837335e8925f82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Mon, 28 Nov 2022 14:01:22 -0800 Subject: [PATCH 01/18] Add sequenceDiagram to hold values required to layout the diagram --- d2layouts/d2sequence/layout.go | 55 ++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/d2layouts/d2sequence/layout.go b/d2layouts/d2sequence/layout.go index 5d1107a39..870eaa622 100644 --- a/d2layouts/d2sequence/layout.go +++ b/d2layouts/d2sequence/layout.go @@ -12,53 +12,64 @@ import ( ) func Layout(ctx context.Context, g *d2graph.Graph) (err error) { - edgeYStep := MIN_EDGE_DISTANCE - actorXStep := MIN_ACTOR_DISTANCE - maxActorHeight := 0. + sd := &sequenceDiagram{ + graph: g, + edgeYStep: MIN_EDGE_DISTANCE, + actorXStep: MIN_ACTOR_DISTANCE, + maxActorHeight: 0., + } actorRank := make(map[*d2graph.Object]int) for rank, actor := range g.Objects { actorRank[actor] = rank } for _, edge := range g.Edges { - edgeYStep = math.Max(edgeYStep, float64(edge.LabelDimensions.Height)+HORIZONTAL_PAD) - maxActorHeight = math.Max(maxActorHeight, edge.Src.Height+HORIZONTAL_PAD) - maxActorHeight = math.Max(maxActorHeight, edge.Dst.Height+HORIZONTAL_PAD) + sd.edgeYStep = math.Max(sd.edgeYStep, float64(edge.LabelDimensions.Height)+HORIZONTAL_PAD) + sd.maxActorHeight = math.Max(sd.maxActorHeight, edge.Src.Height+HORIZONTAL_PAD) + sd.maxActorHeight = math.Max(sd.maxActorHeight, edge.Dst.Height+HORIZONTAL_PAD) // 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(actorRank[edge.Src]) - float64(actorRank[edge.Dst])) distributedLabelWidth := float64(edge.LabelDimensions.Width) / rankDiff - actorXStep = math.Max(actorXStep, distributedLabelWidth+HORIZONTAL_PAD) + sd.actorXStep = math.Max(sd.actorXStep, distributedLabelWidth+HORIZONTAL_PAD) } - placeActors(g.Objects, maxActorHeight, actorXStep) - routeEdges(g.Edges, maxActorHeight, edgeYStep) - addLifelineEdges(g, g.Objects, edgeYStep) + sd.placeActors() + sd.routeEdges() + sd.addLifelineEdges() return nil } +type sequenceDiagram struct { + graph *d2graph.Graph + + edgeYStep float64 + actorXStep float64 + maxActorHeight float64 +} + // placeActors places actors bottom aligned, side by side -func placeActors(actors []*d2graph.Object, maxHeight, xStep float64) { +func (sd *sequenceDiagram) placeActors() { x := 0. - for _, actors := range actors { - yOffset := maxHeight - actors.Height + for _, actors := range sd.graph.Objects { + yOffset := sd.maxActorHeight - actors.Height actors.TopLeft = geo.NewPoint(x, yOffset) - x += actors.Width + xStep + x += actors.Width + sd.actorXStep actors.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter)) } } // routeEdges routes horizontal edges from Src to Dst -func routeEdges(edgesInOrder []*d2graph.Edge, startY, yStep float64) { - edgeY := startY + yStep // in case the first edge has a tall label - for _, edge := range edgesInOrder { +func (sd *sequenceDiagram) routeEdges() { + edgeY := sd.maxActorHeight + sd.edgeYStep // in case the first edge has a tall label + for _, edge := range sd.graph.Edges { start := edge.Src.Center() start.Y = edgeY end := edge.Dst.Center() end.Y = edgeY edge.Route = []*geo.Point{start, end} - edgeY += yStep + edgeY += sd.edgeYStep if edge.Attributes.Label.Value != "" { isLeftToRight := edge.Src.TopLeft.X < edge.Dst.TopLeft.X @@ -80,14 +91,14 @@ func routeEdges(edgesInOrder []*d2graph.Edge, startY, yStep float64) { // │ lifeline // │ // │ -func addLifelineEdges(g *d2graph.Graph, actors []*d2graph.Object, yStep float64) { - endY := g.Edges[len(g.Edges)-1].Route[0].Y + yStep - for _, actor := range actors { +func (sd *sequenceDiagram) addLifelineEdges() { + endY := sd.graph.Edges[len(sd.graph.Edges)-1].Route[0].Y + sd.edgeYStep + for _, actor := range sd.graph.Objects { actorBottom := actor.Center() actorBottom.Y = actor.TopLeft.Y + actor.Height actorLifelineEnd := actor.Center() actorLifelineEnd.Y = endY - g.Edges = append(g.Edges, &d2graph.Edge{ + sd.graph.Edges = append(sd.graph.Edges, &d2graph.Edge{ Attributes: d2graph.Attributes{ Style: d2graph.Style{ StrokeDash: &d2graph.Scalar{Value: "10"}, From b3dfd8548da33c3a5525c536da7833bc9d9373a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Mon, 28 Nov 2022 17:06:37 -0800 Subject: [PATCH 02/18] Add lifespan --- d2layouts/d2sequence/constants.go | 2 + d2layouts/d2sequence/layout.go | 165 +++++++++++++++++++++------- d2layouts/d2sequence/layout_test.go | 93 ++++++++++++---- 3 files changed, 200 insertions(+), 60 deletions(-) diff --git a/d2layouts/d2sequence/constants.go b/d2layouts/d2sequence/constants.go index bdffd77f8..1b8eccc5a 100644 --- a/d2layouts/d2sequence/constants.go +++ b/d2layouts/d2sequence/constants.go @@ -7,3 +7,5 @@ const MIN_ACTOR_DISTANCE = 200. // min vertical distance between edges const MIN_EDGE_DISTANCE = 100. + +const LIFESPAN_BOX_WIDTH = 20. diff --git a/d2layouts/d2sequence/layout.go b/d2layouts/d2sequence/layout.go index 870eaa622..3da7ce999 100644 --- a/d2layouts/d2sequence/layout.go +++ b/d2layouts/d2sequence/layout.go @@ -14,29 +14,20 @@ import ( func Layout(ctx context.Context, g *d2graph.Graph) (err error) { sd := &sequenceDiagram{ graph: g, + objectRank: make(map[*d2graph.Object]int), + edgeRank: make(map[*d2graph.Edge]int), + minEdgeRank: make(map[*d2graph.Object]int), + maxEdgeRank: make(map[*d2graph.Object]int), edgeYStep: MIN_EDGE_DISTANCE, actorXStep: MIN_ACTOR_DISTANCE, maxActorHeight: 0., } - actorRank := make(map[*d2graph.Object]int) - for rank, actor := range g.Objects { - actorRank[actor] = rank - } - for _, edge := range g.Edges { - sd.edgeYStep = math.Max(sd.edgeYStep, float64(edge.LabelDimensions.Height)+HORIZONTAL_PAD) - sd.maxActorHeight = math.Max(sd.maxActorHeight, edge.Src.Height+HORIZONTAL_PAD) - sd.maxActorHeight = math.Max(sd.maxActorHeight, edge.Dst.Height+HORIZONTAL_PAD) - // 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(actorRank[edge.Src]) - float64(actorRank[edge.Dst])) - distributedLabelWidth := float64(edge.LabelDimensions.Width) / rankDiff - sd.actorXStep = math.Max(sd.actorXStep, distributedLabelWidth+HORIZONTAL_PAD) - } - + sd.init() sd.placeActors() - sd.routeEdges() sd.addLifelineEdges() + sd.placeLifespan() + sd.routeEdges() return nil } @@ -44,15 +35,88 @@ func Layout(ctx context.Context, g *d2graph.Graph) (err error) { type sequenceDiagram struct { graph *d2graph.Graph + edges []*d2graph.Edge + actors []*d2graph.Object + lifespans []*d2graph.Object + + // can be either actors or lifespans + objectRank map[*d2graph.Object]int + edgeRank map[*d2graph.Edge]int + + // keep track of the first and last edge of a given actor + // needed for lifespan + minEdgeRank map[*d2graph.Object]int + maxEdgeRank map[*d2graph.Object]int + edgeYStep float64 actorXStep float64 maxActorHeight float64 } +func intMin(a, b int) int { + return int(math.Min(float64(a), float64(b))) +} + +func intMax(a, b int) int { + return int(math.Max(float64(a), float64(b))) +} + +func (sd *sequenceDiagram) init() { + sd.edges = make([]*d2graph.Edge, len(sd.graph.Edges)) + copy(sd.edges, sd.graph.Edges) + + for rank, actor := range sd.graph.Root.ChildrenArray { + sd.assignRank(actor, rank) + } + for _, obj := range sd.graph.Objects { + if obj.Parent == sd.graph.Root { + sd.actors = append(sd.actors, obj) + } else if obj != sd.graph.Root { + sd.lifespans = append(sd.lifespans, obj) + } + } + for rank, edge := range sd.edges { + sd.edgeRank[edge] = rank + if edge.Src.Parent == sd.graph.Root { + sd.maxActorHeight = math.Max(sd.maxActorHeight, edge.Src.Height+HORIZONTAL_PAD) + } + if edge.Dst.Parent == sd.graph.Root { + sd.maxActorHeight = math.Max(sd.maxActorHeight, edge.Dst.Height+HORIZONTAL_PAD) + } + sd.edgeYStep = math.Max(sd.edgeYStep, float64(edge.LabelDimensions.Height)+HORIZONTAL_PAD) + + sd.setMinMaxEdgeRank(edge.Src, rank) + sd.setMinMaxEdgeRank(edge.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[edge.Src]) - float64(sd.objectRank[edge.Dst])) + distributedLabelWidth := float64(edge.LabelDimensions.Width) / rankDiff + sd.actorXStep = math.Max(sd.actorXStep, distributedLabelWidth+HORIZONTAL_PAD) + } +} + +func (sd *sequenceDiagram) assignRank(actor *d2graph.Object, rank int) { + sd.objectRank[actor] = rank + for _, child := range actor.Children { + sd.assignRank(child, rank) + } +} + +func (sd *sequenceDiagram) setMinMaxEdgeRank(actor *d2graph.Object, rank int) { + if minRank, exists := sd.minEdgeRank[actor]; exists { + sd.minEdgeRank[actor] = intMin(minRank, rank) + } else { + sd.minEdgeRank[actor] = rank + } + + sd.maxEdgeRank[actor] = intMax(sd.maxEdgeRank[actor], rank) +} + // placeActors places actors bottom aligned, side by side func (sd *sequenceDiagram) placeActors() { x := 0. - for _, actors := range sd.graph.Objects { + for _, actors := range sd.actors { yOffset := sd.maxActorHeight - actors.Height actors.TopLeft = geo.NewPoint(x, yOffset) x += actors.Width + sd.actorXStep @@ -60,28 +124,6 @@ func (sd *sequenceDiagram) placeActors() { } } -// routeEdges routes horizontal edges from Src to Dst -func (sd *sequenceDiagram) routeEdges() { - edgeY := sd.maxActorHeight + sd.edgeYStep // in case the first edge has a tall label - for _, edge := range sd.graph.Edges { - start := edge.Src.Center() - start.Y = edgeY - end := edge.Dst.Center() - end.Y = edgeY - edge.Route = []*geo.Point{start, end} - edgeY += sd.edgeYStep - - if edge.Attributes.Label.Value != "" { - isLeftToRight := edge.Src.TopLeft.X < edge.Dst.TopLeft.X - if isLeftToRight { - edge.LabelPosition = go2.Pointer(string(label.OutsideTopCenter)) - } else { - edge.LabelPosition = go2.Pointer(string(label.OutsideBottomCenter)) - } - } - } -} - // addLifelineEdges adds a new edge for each actor in the graph that represents the // edge below the actor showing its lifespan // ┌──────────────┐ @@ -92,8 +134,8 @@ func (sd *sequenceDiagram) routeEdges() { // │ // │ func (sd *sequenceDiagram) addLifelineEdges() { - endY := sd.graph.Edges[len(sd.graph.Edges)-1].Route[0].Y + sd.edgeYStep - for _, actor := range sd.graph.Objects { + endY := sd.getEdgeY(len(sd.edges)) + for _, actor := range sd.actors { actorBottom := actor.Center() actorBottom.Y = actor.TopLeft.Y + actor.Height actorLifelineEnd := actor.Center() @@ -116,3 +158,44 @@ func (sd *sequenceDiagram) addLifelineEdges() { }) } } + +func (sd *sequenceDiagram) placeLifespan() { + rankToX := make(map[int]float64) + for _, actor := range sd.actors { + rankToX[sd.objectRank[actor]] = actor.Center().X + } + for _, lifespan := range sd.lifespans { + minRank := sd.minEdgeRank[lifespan] + maxRank := sd.maxEdgeRank[lifespan] + + minY := sd.getEdgeY(minRank) + maxY := sd.getEdgeY(maxRank) + height := maxY - minY + x := rankToX[sd.objectRank[lifespan]] - (LIFESPAN_BOX_WIDTH / 2.) + lifespan.Box = geo.NewBox(geo.NewPoint(x, minY), LIFESPAN_BOX_WIDTH, height) + } +} + +// routeEdges routes horizontal edges from Src to Dst +func (sd *sequenceDiagram) routeEdges() { + for rank, edge := range sd.edges { + start := edge.Src.Center() + start.Y = sd.getEdgeY(rank) + end := edge.Dst.Center() + end.Y = start.Y + edge.Route = []*geo.Point{start, end} + + if edge.Attributes.Label.Value != "" { + isLeftToRight := edge.Src.TopLeft.X < edge.Dst.TopLeft.X + if isLeftToRight { + edge.LabelPosition = go2.Pointer(string(label.OutsideTopCenter)) + } else { + edge.LabelPosition = go2.Pointer(string(label.OutsideBottomCenter)) + } + } + } +} + +func (sd *sequenceDiagram) getEdgeY(rank int) float64 { + return ((float64(rank) + 1.) * sd.edgeYStep) + sd.maxActorHeight +} diff --git a/d2layouts/d2sequence/layout_test.go b/d2layouts/d2sequence/layout_test.go index f01575292..107cfef49 100644 --- a/d2layouts/d2sequence/layout_test.go +++ b/d2layouts/d2sequence/layout_test.go @@ -9,35 +9,41 @@ import ( "oss.terrastruct.com/d2/lib/log" ) -func TestLayout(t *testing.T) { +func TestBasicSequenceDiagram(t *testing.T) { + // ┌────────┐ ┌────────┐ + // │ n1 │ │ n2 │ + // └────┬───┘ └────┬───┘ + // │ │ + // ├───────────────────────► + // │ │ + // ◄───────────────────────┤ + // │ │ + // ├───────────────────────► + // │ │ + // ◄───────────────────────┤ + // │ │ g := d2graph.NewGraph(nil) - g.Objects = []*d2graph.Object{ - { - ID: "Alice", - Box: geo.NewBox(nil, 100, 100), - }, - { - ID: "Bob", - Box: geo.NewBox(nil, 30, 30), - }, - } + n1 := g.Root.EnsureChild([]string{"n1"}) + n1.Box = geo.NewBox(nil, 100, 100) + n2 := g.Root.EnsureChild([]string{"n2"}) + n2.Box = geo.NewBox(nil, 30, 30) g.Edges = []*d2graph.Edge{ { - Src: g.Objects[0], - Dst: g.Objects[1], + Src: n1, + Dst: n2, }, { - Src: g.Objects[1], - Dst: g.Objects[0], + Src: n2, + Dst: n1, }, { - Src: g.Objects[0], - Dst: g.Objects[1], + Src: n1, + Dst: n2, }, { - Src: g.Objects[1], - Dst: g.Objects[0], + Src: n2, + Dst: n1, }, } nEdges := len(g.Edges) @@ -110,3 +116,52 @@ func TestLayout(t *testing.T) { } } } + +func TestLifespanSequenceDiagram(t *testing.T) { + // ┌─────┐ ┌─────┐ + // │ a │ │ b │ + // └──┬──┘ └──┬──┘ + // ├┐────────────────────►┌┤ + // t1 ││ ││ t1 + // ├┘◄────────────────────└┤ + // ├┐──────────────────────► + // t2 ││ │ + // ├┘◄─────────────────────┤ + g := d2graph.NewGraph(nil) + a := g.Root.EnsureChild([]string{"a"}) + a.Box = geo.NewBox(nil, 100, 100) + a_t1 := a.EnsureChild([]string{"t1"}) + a_t2 := a.EnsureChild([]string{"t2"}) + b := g.Root.EnsureChild([]string{"b"}) + b.Box = geo.NewBox(nil, 30, 30) + b_t1 := b.EnsureChild([]string{"t1"}) + + g.Edges = []*d2graph.Edge{ + { + Src: a_t1, + Dst: b_t1, + }, { + Src: b_t1, + Dst: a_t1, + }, { + Src: a_t2, + Dst: b, + }, { + Src: b, + Dst: a_t2, + }, + } + + ctx := log.WithTB(context.Background(), t, nil) + Layout(ctx, g) + + if a.Center().X != a_t1.Center().X { + t.Fatal("expected a_t1.X = a.X") + } + if a.Center().X != a_t2.Center().X { + t.Fatal("expected a_t2.X = a.X") + } + if b.Center().X != b_t1.Center().X { + t.Fatal("expected b_t1.X = b.X") + } +} From 341db70e9d8783575080c44afdf2573ca71e7a5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Mon, 28 Nov 2022 17:09:34 -0800 Subject: [PATCH 03/18] remove labels from lifespan --- d2layouts/d2sequence/layout.go | 1 + 1 file changed, 1 insertion(+) diff --git a/d2layouts/d2sequence/layout.go b/d2layouts/d2sequence/layout.go index 3da7ce999..8ff507b2b 100644 --- a/d2layouts/d2sequence/layout.go +++ b/d2layouts/d2sequence/layout.go @@ -165,6 +165,7 @@ func (sd *sequenceDiagram) placeLifespan() { rankToX[sd.objectRank[actor]] = actor.Center().X } for _, lifespan := range sd.lifespans { + lifespan.Attributes.Label = d2graph.Scalar{Value: ""} minRank := sd.minEdgeRank[lifespan] maxRank := sd.maxEdgeRank[lifespan] From f22580d9ed91c7bd83a846b9f55c4b85c1e36a57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Mon, 28 Nov 2022 18:23:51 -0800 Subject: [PATCH 04/18] Route to proper lifespan box sides --- d2layouts/d2sequence/constants.go | 3 ++ d2layouts/d2sequence/layout.go | 49 ++++++++++++++++++++++++++----- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/d2layouts/d2sequence/constants.go b/d2layouts/d2sequence/constants.go index 1b8eccc5a..745b6d05c 100644 --- a/d2layouts/d2sequence/constants.go +++ b/d2layouts/d2sequence/constants.go @@ -9,3 +9,6 @@ const MIN_ACTOR_DISTANCE = 200. const MIN_EDGE_DISTANCE = 100. const LIFESPAN_BOX_WIDTH = 20. + +// +const DEFAULT_LIFESPAN_BOX_HEIGHT = MIN_EDGE_DISTANCE / 2. diff --git a/d2layouts/d2sequence/layout.go b/d2layouts/d2sequence/layout.go index 8ff507b2b..928cb0a61 100644 --- a/d2layouts/d2sequence/layout.go +++ b/d2layouts/d2sequence/layout.go @@ -25,9 +25,9 @@ func Layout(ctx context.Context, g *d2graph.Graph) (err error) { sd.init() sd.placeActors() - sd.addLifelineEdges() sd.placeLifespan() sd.routeEdges() + sd.addLifelineEdges() return nil } @@ -159,6 +159,18 @@ func (sd *sequenceDiagram) addLifelineEdges() { } } +// placeLifespan places lifespan boxes over the object lifeline +// ┌──────────┐ +// │ actor │ +// └────┬─────┘ +// ┌─┴──┐ +// │ │ +// lifespan +// │ │ +// └─┬──┘ +// │ +// lifeline +// │ func (sd *sequenceDiagram) placeLifespan() { rankToX := make(map[int]float64) for _, actor := range sd.actors { @@ -172,6 +184,7 @@ func (sd *sequenceDiagram) placeLifespan() { minY := sd.getEdgeY(minRank) maxY := sd.getEdgeY(maxRank) height := maxY - minY + height = math.Max(height, DEFAULT_LIFESPAN_BOX_HEIGHT) x := rankToX[sd.objectRank[lifespan]] - (LIFESPAN_BOX_WIDTH / 2.) lifespan.Box = geo.NewBox(geo.NewPoint(x, minY), LIFESPAN_BOX_WIDTH, height) } @@ -180,14 +193,31 @@ func (sd *sequenceDiagram) placeLifespan() { // routeEdges routes horizontal edges from Src to Dst func (sd *sequenceDiagram) routeEdges() { for rank, edge := range sd.edges { - start := edge.Src.Center() - start.Y = sd.getEdgeY(rank) - end := edge.Dst.Center() - end.Y = start.Y - edge.Route = []*geo.Point{start, end} + isLeftToRight := edge.Src.TopLeft.X < edge.Dst.TopLeft.X + + var startX, endX float64 + if sd.isActor(edge.Src) { + startX = edge.Src.Center().X + } else if isLeftToRight { + startX = edge.Src.TopLeft.X + edge.Src.Width + } else { + startX = edge.Src.TopLeft.X + } + + if sd.isActor(edge.Dst) { + endX = edge.Dst.Center().X + } else if isLeftToRight { + endX = edge.Dst.TopLeft.X + } else { + endX = edge.Dst.TopLeft.X + edge.Dst.Width + } + edgeY := sd.getEdgeY(rank) + edge.Route = []*geo.Point{ + geo.NewPoint(startX, edgeY), + geo.NewPoint(endX, edgeY), + } if edge.Attributes.Label.Value != "" { - isLeftToRight := edge.Src.TopLeft.X < edge.Dst.TopLeft.X if isLeftToRight { edge.LabelPosition = go2.Pointer(string(label.OutsideTopCenter)) } else { @@ -198,5 +228,10 @@ func (sd *sequenceDiagram) routeEdges() { } func (sd *sequenceDiagram) getEdgeY(rank int) float64 { + // +1 so that the first edge has the top padding for its label return ((float64(rank) + 1.) * sd.edgeYStep) + sd.maxActorHeight } + +func (sd *sequenceDiagram) isActor(obj *d2graph.Object) bool { + return obj.Parent == sd.graph.Root +} From 77b1166abdae1e070275f265ba5ad5acbaa734bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Mon, 28 Nov 2022 19:20:53 -0800 Subject: [PATCH 05/18] Handle nested lifespan --- d2layouts/d2sequence/constants.go | 6 ++- d2layouts/d2sequence/layout.go | 77 ++++++++++++++++++++++--------- 2 files changed, 59 insertions(+), 24 deletions(-) diff --git a/d2layouts/d2sequence/constants.go b/d2layouts/d2sequence/constants.go index 745b6d05c..5a5e1ed2c 100644 --- a/d2layouts/d2sequence/constants.go +++ b/d2layouts/d2sequence/constants.go @@ -8,7 +8,11 @@ const MIN_ACTOR_DISTANCE = 200. // min vertical distance between edges const MIN_EDGE_DISTANCE = 100. +// default size const LIFESPAN_BOX_WIDTH = 20. -// +// as the lifespan boxes start getting nested, their size grows +const LIFESPAN_DEPTH_GROW_FACTOR = 10. + +// when a lifespan box has a single edge const DEFAULT_LIFESPAN_BOX_HEIGHT = MIN_EDGE_DISTANCE / 2. diff --git a/d2layouts/d2sequence/layout.go b/d2layouts/d2sequence/layout.go index 928cb0a61..d1d2f845f 100644 --- a/d2layouts/d2sequence/layout.go +++ b/d2layouts/d2sequence/layout.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "math" + "sort" "oss.terrastruct.com/d2/d2graph" "oss.terrastruct.com/d2/lib/geo" @@ -15,7 +16,7 @@ func Layout(ctx context.Context, g *d2graph.Graph) (err error) { sd := &sequenceDiagram{ graph: g, objectRank: make(map[*d2graph.Object]int), - edgeRank: make(map[*d2graph.Edge]int), + objectDepth: make(map[*d2graph.Object]int), minEdgeRank: make(map[*d2graph.Object]int), maxEdgeRank: make(map[*d2graph.Object]int), edgeYStep: MIN_EDGE_DISTANCE, @@ -40,8 +41,8 @@ type sequenceDiagram struct { lifespans []*d2graph.Object // can be either actors or lifespans - objectRank map[*d2graph.Object]int - edgeRank map[*d2graph.Edge]int + objectRank map[*d2graph.Object]int + objectDepth map[*d2graph.Object]int // keep track of the first and last edge of a given actor // needed for lifespan @@ -65,18 +66,27 @@ func (sd *sequenceDiagram) init() { sd.edges = make([]*d2graph.Edge, len(sd.graph.Edges)) copy(sd.edges, sd.graph.Edges) - for rank, actor := range sd.graph.Root.ChildrenArray { - sd.assignRank(actor, rank) - } - for _, obj := range sd.graph.Objects { + queue := make([]*d2graph.Object, len(sd.graph.Root.ChildrenArray)) + copy(queue, sd.graph.Root.ChildrenArray) + for len(queue) > 0 { + obj := queue[0] + queue = queue[1:] + if obj.Parent == sd.graph.Root { sd.actors = append(sd.actors, obj) + sd.objectRank[obj] = len(sd.actors) + sd.objectDepth[obj] = 0 } else if obj != sd.graph.Root { + obj.Attributes.Label = d2graph.Scalar{Value: ""} sd.lifespans = append(sd.lifespans, obj) + sd.objectRank[obj] = sd.objectRank[obj.Parent] + sd.objectDepth[obj] = sd.objectDepth[obj.Parent] + 1 } + + queue = append(queue, obj.ChildrenArray...) } + for rank, edge := range sd.edges { - sd.edgeRank[edge] = rank if edge.Src.Parent == sd.graph.Root { sd.maxActorHeight = math.Max(sd.maxActorHeight, edge.Src.Height+HORIZONTAL_PAD) } @@ -96,13 +106,6 @@ func (sd *sequenceDiagram) init() { } } -func (sd *sequenceDiagram) assignRank(actor *d2graph.Object, rank int) { - sd.objectRank[actor] = rank - for _, child := range actor.Children { - sd.assignRank(child, rank) - } -} - func (sd *sequenceDiagram) setMinMaxEdgeRank(actor *d2graph.Object, rank int) { if minRank, exists := sd.minEdgeRank[actor]; exists { sd.minEdgeRank[actor] = intMin(minRank, rank) @@ -176,17 +179,45 @@ func (sd *sequenceDiagram) placeLifespan() { for _, actor := range sd.actors { rankToX[sd.objectRank[actor]] = actor.Center().X } - for _, lifespan := range sd.lifespans { - lifespan.Attributes.Label = d2graph.Scalar{Value: ""} - minRank := sd.minEdgeRank[lifespan] - maxRank := sd.maxEdgeRank[lifespan] - minY := sd.getEdgeY(minRank) - maxY := sd.getEdgeY(maxRank) + lifespanFromMostNested := make([]*d2graph.Object, len(sd.lifespans)) + copy(lifespanFromMostNested, sd.lifespans) + sort.SliceStable(lifespanFromMostNested, func(i, j int) bool { + return sd.objectDepth[lifespanFromMostNested[i]] > sd.objectDepth[lifespanFromMostNested[j]] + }) + for _, lifespan := range lifespanFromMostNested { + minChildY := math.Inf(1) + maxChildY := math.Inf(-1) + for _, child := range lifespan.ChildrenArray { + minChildY = math.Min(minChildY, child.TopLeft.Y) + maxChildY = math.Max(maxChildY, child.TopLeft.Y+child.Height) + } + + minEdgeY := math.Inf(1) + if minRank, exists := sd.minEdgeRank[lifespan]; exists { + minEdgeY = sd.getEdgeY(minRank) + } + maxEdgeY := math.Inf(-1) + if maxRank, exists := sd.maxEdgeRank[lifespan]; exists { + maxEdgeY = sd.getEdgeY(maxRank) + } + + minY := math.Min(minEdgeY, minChildY) + if minY == minChildY { + minY -= LIFESPAN_DEPTH_GROW_FACTOR + } + maxY := math.Max(maxEdgeY, maxChildY) + if maxY == maxChildY { + maxY += LIFESPAN_DEPTH_GROW_FACTOR + } + height := maxY - minY height = math.Max(height, DEFAULT_LIFESPAN_BOX_HEIGHT) - x := rankToX[sd.objectRank[lifespan]] - (LIFESPAN_BOX_WIDTH / 2.) - lifespan.Box = geo.NewBox(geo.NewPoint(x, minY), LIFESPAN_BOX_WIDTH, height) + + width := LIFESPAN_BOX_WIDTH + (float64(sd.objectDepth[lifespan]-1) * LIFESPAN_DEPTH_GROW_FACTOR) + + x := rankToX[sd.objectRank[lifespan]] - (width / 2.) + lifespan.Box = geo.NewBox(geo.NewPoint(x, minY), width, height) } } From a79da2db56528cf9737d61d21ca53be4d8c20c31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Tue, 29 Nov 2022 10:08:11 -0800 Subject: [PATCH 06/18] rename lifespan to activation box --- d2layouts/d2sequence/constants.go | 10 +++--- d2layouts/d2sequence/layout.go | 51 ++++++++++++++--------------- d2layouts/d2sequence/layout_test.go | 2 +- 3 files changed, 31 insertions(+), 32 deletions(-) diff --git a/d2layouts/d2sequence/constants.go b/d2layouts/d2sequence/constants.go index 5a5e1ed2c..4c59ea427 100644 --- a/d2layouts/d2sequence/constants.go +++ b/d2layouts/d2sequence/constants.go @@ -9,10 +9,10 @@ const MIN_ACTOR_DISTANCE = 200. const MIN_EDGE_DISTANCE = 100. // default size -const LIFESPAN_BOX_WIDTH = 20. +const ACTIvATION_BOX_WIDTH = 20. -// as the lifespan boxes start getting nested, their size grows -const LIFESPAN_DEPTH_GROW_FACTOR = 10. +// as the activation boxes start getting nested, their size grows +const ACTIVATION_BOX_DEPTH_GROW_FACTOR = 10. -// when a lifespan box has a single edge -const DEFAULT_LIFESPAN_BOX_HEIGHT = MIN_EDGE_DISTANCE / 2. +// when a activation box has a single edge +const DEFAULT_ACTIVATION_BOX_HEIGHT = MIN_EDGE_DISTANCE / 2. diff --git a/d2layouts/d2sequence/layout.go b/d2layouts/d2sequence/layout.go index d1d2f845f..45e113b89 100644 --- a/d2layouts/d2sequence/layout.go +++ b/d2layouts/d2sequence/layout.go @@ -26,7 +26,7 @@ func Layout(ctx context.Context, g *d2graph.Graph) (err error) { sd.init() sd.placeActors() - sd.placeLifespan() + sd.placeActivationBoxes() sd.routeEdges() sd.addLifelineEdges() @@ -36,16 +36,16 @@ func Layout(ctx context.Context, g *d2graph.Graph) (err error) { type sequenceDiagram struct { graph *d2graph.Graph - edges []*d2graph.Edge - actors []*d2graph.Object - lifespans []*d2graph.Object + edges []*d2graph.Edge + actors []*d2graph.Object + activations []*d2graph.Object - // can be either actors or lifespans + // can be either actors or activation boxes objectRank map[*d2graph.Object]int objectDepth map[*d2graph.Object]int // keep track of the first and last edge of a given actor - // needed for lifespan + // needed for activation boxes minEdgeRank map[*d2graph.Object]int maxEdgeRank map[*d2graph.Object]int @@ -78,7 +78,7 @@ func (sd *sequenceDiagram) init() { sd.objectDepth[obj] = 0 } else if obj != sd.graph.Root { obj.Attributes.Label = d2graph.Scalar{Value: ""} - sd.lifespans = append(sd.lifespans, obj) + sd.activations = append(sd.activations, obj) sd.objectRank[obj] = sd.objectRank[obj.Parent] sd.objectDepth[obj] = sd.objectDepth[obj.Parent] + 1 } @@ -127,8 +127,7 @@ func (sd *sequenceDiagram) placeActors() { } } -// addLifelineEdges adds a new edge for each actor in the graph that represents the -// edge below the actor showing its lifespan +// addLifelineEdges adds a new edge for each actor in the graph that represents the its lifeline // ┌──────────────┐ // │ actor │ // └──────┬───────┘ @@ -162,62 +161,62 @@ func (sd *sequenceDiagram) addLifelineEdges() { } } -// placeLifespan places lifespan boxes over the object lifeline +// placeActivationBoxes places activation boxes over the object lifeline // ┌──────────┐ // │ actor │ // └────┬─────┘ // ┌─┴──┐ // │ │ -// lifespan +// activation // │ │ // └─┬──┘ // │ // lifeline // │ -func (sd *sequenceDiagram) placeLifespan() { +func (sd *sequenceDiagram) placeActivationBoxes() { rankToX := make(map[int]float64) for _, actor := range sd.actors { rankToX[sd.objectRank[actor]] = actor.Center().X } - lifespanFromMostNested := make([]*d2graph.Object, len(sd.lifespans)) - copy(lifespanFromMostNested, sd.lifespans) - sort.SliceStable(lifespanFromMostNested, func(i, j int) bool { - return sd.objectDepth[lifespanFromMostNested[i]] > sd.objectDepth[lifespanFromMostNested[j]] + activationFromMostNested := make([]*d2graph.Object, len(sd.activations)) + copy(activationFromMostNested, sd.activations) + sort.SliceStable(activationFromMostNested, func(i, j int) bool { + return sd.objectDepth[activationFromMostNested[i]] > sd.objectDepth[activationFromMostNested[j]] }) - for _, lifespan := range lifespanFromMostNested { + for _, activation := range activationFromMostNested { minChildY := math.Inf(1) maxChildY := math.Inf(-1) - for _, child := range lifespan.ChildrenArray { + for _, child := range activation.ChildrenArray { minChildY = math.Min(minChildY, child.TopLeft.Y) maxChildY = math.Max(maxChildY, child.TopLeft.Y+child.Height) } minEdgeY := math.Inf(1) - if minRank, exists := sd.minEdgeRank[lifespan]; exists { + if minRank, exists := sd.minEdgeRank[activation]; exists { minEdgeY = sd.getEdgeY(minRank) } maxEdgeY := math.Inf(-1) - if maxRank, exists := sd.maxEdgeRank[lifespan]; exists { + if maxRank, exists := sd.maxEdgeRank[activation]; exists { maxEdgeY = sd.getEdgeY(maxRank) } minY := math.Min(minEdgeY, minChildY) if minY == minChildY { - minY -= LIFESPAN_DEPTH_GROW_FACTOR + minY -= ACTIVATION_BOX_DEPTH_GROW_FACTOR } maxY := math.Max(maxEdgeY, maxChildY) if maxY == maxChildY { - maxY += LIFESPAN_DEPTH_GROW_FACTOR + maxY += ACTIVATION_BOX_DEPTH_GROW_FACTOR } height := maxY - minY - height = math.Max(height, DEFAULT_LIFESPAN_BOX_HEIGHT) + height = math.Max(height, DEFAULT_ACTIVATION_BOX_HEIGHT) - width := LIFESPAN_BOX_WIDTH + (float64(sd.objectDepth[lifespan]-1) * LIFESPAN_DEPTH_GROW_FACTOR) + width := ACTIvATION_BOX_WIDTH + (float64(sd.objectDepth[activation]-1) * ACTIVATION_BOX_DEPTH_GROW_FACTOR) - x := rankToX[sd.objectRank[lifespan]] - (width / 2.) - lifespan.Box = geo.NewBox(geo.NewPoint(x, minY), width, height) + x := rankToX[sd.objectRank[activation]] - (width / 2.) + activation.Box = geo.NewBox(geo.NewPoint(x, minY), width, height) } } diff --git a/d2layouts/d2sequence/layout_test.go b/d2layouts/d2sequence/layout_test.go index 107cfef49..797c2aa6e 100644 --- a/d2layouts/d2sequence/layout_test.go +++ b/d2layouts/d2sequence/layout_test.go @@ -117,7 +117,7 @@ func TestBasicSequenceDiagram(t *testing.T) { } } -func TestLifespanSequenceDiagram(t *testing.T) { +func TestActivationBoxesSequenceDiagram(t *testing.T) { // ┌─────┐ ┌─────┐ // │ a │ │ b │ // └──┬──┘ └──┬──┘ From ebac99e3dbfb18b669c8c820bdf171ab814859e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Tue, 29 Nov 2022 10:11:42 -0800 Subject: [PATCH 07/18] Minor refactor --- d2layouts/d2sequence/constants.go | 2 +- d2layouts/d2sequence/layout.go | 28 +++++++++------------------- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/d2layouts/d2sequence/constants.go b/d2layouts/d2sequence/constants.go index 4c59ea427..884305852 100644 --- a/d2layouts/d2sequence/constants.go +++ b/d2layouts/d2sequence/constants.go @@ -9,7 +9,7 @@ const MIN_ACTOR_DISTANCE = 200. const MIN_EDGE_DISTANCE = 100. // default size -const ACTIvATION_BOX_WIDTH = 20. +const ACTIVATION_BOX_WIDTH = 20. // as the activation boxes start getting nested, their size grows const ACTIVATION_BOX_DEPTH_GROW_FACTOR = 10. diff --git a/d2layouts/d2sequence/layout.go b/d2layouts/d2sequence/layout.go index 45e113b89..26c3335d9 100644 --- a/d2layouts/d2sequence/layout.go +++ b/d2layouts/d2sequence/layout.go @@ -10,6 +10,7 @@ import ( "oss.terrastruct.com/d2/lib/geo" "oss.terrastruct.com/d2/lib/go2" "oss.terrastruct.com/d2/lib/label" + "oss.terrastruct.com/d2/lib/shape" ) func Layout(ctx context.Context, g *d2graph.Graph) (err error) { @@ -54,14 +55,6 @@ type sequenceDiagram struct { maxActorHeight float64 } -func intMin(a, b int) int { - return int(math.Min(float64(a), float64(b))) -} - -func intMax(a, b int) int { - return int(math.Max(float64(a), float64(b))) -} - func (sd *sequenceDiagram) init() { sd.edges = make([]*d2graph.Edge, len(sd.graph.Edges)) copy(sd.edges, sd.graph.Edges) @@ -72,12 +65,15 @@ func (sd *sequenceDiagram) init() { obj := queue[0] queue = queue[1:] - if obj.Parent == sd.graph.Root { + if sd.isActor(obj) { sd.actors = append(sd.actors, obj) sd.objectRank[obj] = len(sd.actors) sd.objectDepth[obj] = 0 - } else if obj != sd.graph.Root { + sd.maxActorHeight = math.Max(sd.maxActorHeight, obj.Height+HORIZONTAL_PAD) + } else { + // activations boxes are always rectangles and have no labels obj.Attributes.Label = d2graph.Scalar{Value: ""} + obj.Attributes.Shape = d2graph.Scalar{Value: shape.SQUARE_TYPE} sd.activations = append(sd.activations, obj) sd.objectRank[obj] = sd.objectRank[obj.Parent] sd.objectDepth[obj] = sd.objectDepth[obj.Parent] + 1 @@ -87,12 +83,6 @@ func (sd *sequenceDiagram) init() { } for rank, edge := range sd.edges { - if edge.Src.Parent == sd.graph.Root { - sd.maxActorHeight = math.Max(sd.maxActorHeight, edge.Src.Height+HORIZONTAL_PAD) - } - if edge.Dst.Parent == sd.graph.Root { - sd.maxActorHeight = math.Max(sd.maxActorHeight, edge.Dst.Height+HORIZONTAL_PAD) - } sd.edgeYStep = math.Max(sd.edgeYStep, float64(edge.LabelDimensions.Height)+HORIZONTAL_PAD) sd.setMinMaxEdgeRank(edge.Src, rank) @@ -108,12 +98,12 @@ func (sd *sequenceDiagram) init() { func (sd *sequenceDiagram) setMinMaxEdgeRank(actor *d2graph.Object, rank int) { if minRank, exists := sd.minEdgeRank[actor]; exists { - sd.minEdgeRank[actor] = intMin(minRank, rank) + sd.minEdgeRank[actor] = go2.IntMin(minRank, rank) } else { sd.minEdgeRank[actor] = rank } - sd.maxEdgeRank[actor] = intMax(sd.maxEdgeRank[actor], rank) + sd.maxEdgeRank[actor] = go2.IntMax(sd.maxEdgeRank[actor], rank) } // placeActors places actors bottom aligned, side by side @@ -213,7 +203,7 @@ func (sd *sequenceDiagram) placeActivationBoxes() { height := maxY - minY height = math.Max(height, DEFAULT_ACTIVATION_BOX_HEIGHT) - width := ACTIvATION_BOX_WIDTH + (float64(sd.objectDepth[activation]-1) * ACTIVATION_BOX_DEPTH_GROW_FACTOR) + width := ACTIVATION_BOX_WIDTH + (float64(sd.objectDepth[activation]-1) * ACTIVATION_BOX_DEPTH_GROW_FACTOR) x := rankToX[sd.objectRank[activation]] - (width / 2.) activation.Box = geo.NewBox(geo.NewPoint(x, minY), width, height) From a9b8b3c0b1778b349427a0af7fc90e1b5e326614 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Tue, 29 Nov 2022 10:18:32 -0800 Subject: [PATCH 08/18] Vertical pad --- d2layouts/d2sequence/constants.go | 3 +++ d2layouts/d2sequence/layout.go | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/d2layouts/d2sequence/constants.go b/d2layouts/d2sequence/constants.go index 884305852..3f184e92f 100644 --- a/d2layouts/d2sequence/constants.go +++ b/d2layouts/d2sequence/constants.go @@ -3,6 +3,9 @@ package d2sequence // leaves at least 25 units of space on the left/right when computing the space required between actors const HORIZONTAL_PAD = 50. +// leaves at least 25 units of space on the top/bottom when computing the space required between edges +const VERTICAL_PAD = 50. + const MIN_ACTOR_DISTANCE = 200. // min vertical distance between edges diff --git a/d2layouts/d2sequence/layout.go b/d2layouts/d2sequence/layout.go index 26c3335d9..5ca1a5388 100644 --- a/d2layouts/d2sequence/layout.go +++ b/d2layouts/d2sequence/layout.go @@ -69,7 +69,7 @@ func (sd *sequenceDiagram) init() { sd.actors = append(sd.actors, obj) sd.objectRank[obj] = len(sd.actors) sd.objectDepth[obj] = 0 - sd.maxActorHeight = math.Max(sd.maxActorHeight, obj.Height+HORIZONTAL_PAD) + sd.maxActorHeight = math.Max(sd.maxActorHeight, obj.Height+VERTICAL_PAD) } else { // activations boxes are always rectangles and have no labels obj.Attributes.Label = d2graph.Scalar{Value: ""} @@ -83,7 +83,7 @@ func (sd *sequenceDiagram) init() { } for rank, edge := range sd.edges { - sd.edgeYStep = math.Max(sd.edgeYStep, float64(edge.LabelDimensions.Height)+HORIZONTAL_PAD) + sd.edgeYStep = math.Max(sd.edgeYStep, float64(edge.LabelDimensions.Height)+VERTICAL_PAD) sd.setMinMaxEdgeRank(edge.Src, rank) sd.setMinMaxEdgeRank(edge.Dst, rank) From 94d74d4ddf502f4d0a29f5f7be27c55da2cb2d3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Tue, 29 Nov 2022 10:29:15 -0800 Subject: [PATCH 09/18] Add some comments --- d2layouts/d2sequence/layout.go | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/d2layouts/d2sequence/layout.go b/d2layouts/d2sequence/layout.go index 5ca1a5388..a26a02628 100644 --- a/d2layouts/d2sequence/layout.go +++ b/d2layouts/d2sequence/layout.go @@ -164,17 +164,24 @@ func (sd *sequenceDiagram) addLifelineEdges() { // lifeline // │ func (sd *sequenceDiagram) placeActivationBoxes() { + // quickly find the activation box center X rankToX := make(map[int]float64) for _, actor := range sd.actors { rankToX[sd.objectRank[actor]] = actor.Center().X } + // places activation boxes from most to least nested + // the order is important because the only way a child activation box exists is if there'e an edge to it + // however, the parent activation might not have an edge to it and then its position is based on the child position + // or, there can be edge to it, but it comes after the child one meaning the top left position is still based on the child + // and not on its own edge activationFromMostNested := make([]*d2graph.Object, len(sd.activations)) copy(activationFromMostNested, sd.activations) sort.SliceStable(activationFromMostNested, func(i, j int) bool { return sd.objectDepth[activationFromMostNested[i]] > sd.objectDepth[activationFromMostNested[j]] }) for _, activation := range activationFromMostNested { + // finds the position based on children minChildY := math.Inf(1) maxChildY := math.Inf(-1) for _, child := range activation.ChildrenArray { @@ -182,6 +189,7 @@ func (sd *sequenceDiagram) placeActivationBoxes() { maxChildY = math.Max(maxChildY, child.TopLeft.Y+child.Height) } + // finds the position if there are edges to this activation box minEdgeY := math.Inf(1) if minRank, exists := sd.minEdgeRank[activation]; exists { minEdgeY = sd.getEdgeY(minRank) @@ -191,6 +199,7 @@ func (sd *sequenceDiagram) placeActivationBoxes() { maxEdgeY = sd.getEdgeY(maxRank) } + // if it is the same as the child top left, add some padding minY := math.Min(minEdgeY, minChildY) if minY == minChildY { minY -= ACTIVATION_BOX_DEPTH_GROW_FACTOR @@ -200,11 +209,8 @@ func (sd *sequenceDiagram) placeActivationBoxes() { maxY += ACTIVATION_BOX_DEPTH_GROW_FACTOR } - height := maxY - minY - height = math.Max(height, DEFAULT_ACTIVATION_BOX_HEIGHT) - + height := math.Max(maxY-minY, DEFAULT_ACTIVATION_BOX_HEIGHT) width := ACTIVATION_BOX_WIDTH + (float64(sd.objectDepth[activation]-1) * ACTIVATION_BOX_DEPTH_GROW_FACTOR) - x := rankToX[sd.objectRank[activation]] - (width / 2.) activation.Box = geo.NewBox(geo.NewPoint(x, minY), width, height) } @@ -215,6 +221,7 @@ func (sd *sequenceDiagram) routeEdges() { for rank, edge := range sd.edges { isLeftToRight := edge.Src.TopLeft.X < edge.Dst.TopLeft.X + // finds the proper anchor point based on the edge direction var startX, endX float64 if sd.isActor(edge.Src) { startX = edge.Src.Center().X @@ -241,6 +248,7 @@ func (sd *sequenceDiagram) routeEdges() { if isLeftToRight { edge.LabelPosition = go2.Pointer(string(label.OutsideTopCenter)) } else { + // the label will be placed above the edge because the orientation is based on the edge normal vector edge.LabelPosition = go2.Pointer(string(label.OutsideBottomCenter)) } } From 9c0df4ebdbb218e2bcb1ee62cd6e41d96d303fd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Tue, 29 Nov 2022 10:54:55 -0800 Subject: [PATCH 10/18] tests --- d2layouts/d2sequence/layout_test.go | 71 +++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/d2layouts/d2sequence/layout_test.go b/d2layouts/d2sequence/layout_test.go index 797c2aa6e..28cc8570f 100644 --- a/d2layouts/d2sequence/layout_test.go +++ b/d2layouts/d2sequence/layout_test.go @@ -6,7 +6,9 @@ import ( "oss.terrastruct.com/d2/d2graph" "oss.terrastruct.com/d2/lib/geo" + "oss.terrastruct.com/d2/lib/label" "oss.terrastruct.com/d2/lib/log" + "oss.terrastruct.com/d2/lib/shape" ) func TestBasicSequenceDiagram(t *testing.T) { @@ -32,10 +34,16 @@ func TestBasicSequenceDiagram(t *testing.T) { { Src: n1, Dst: n2, + Attributes: d2graph.Attributes{ + Label: d2graph.Scalar{Value: "left to right"}, + }, }, { Src: n2, Dst: n1, + Attributes: d2graph.Attributes{ + Label: d2graph.Scalar{Value: "right to left"}, + }, }, { Src: n1, @@ -115,6 +123,15 @@ func TestBasicSequenceDiagram(t *testing.T) { t.Fatalf("expected edge[%d] to end after the last sequence edge", i) } } + + // check label positions + if *g.Edges[0].LabelPosition != string(label.OutsideTopCenter) { + t.Fatalf("expected edge label to be placed on %s, got %s", string(label.OutsideTopCenter), *g.Edges[0].LabelPosition) + } + + if *g.Edges[1].LabelPosition != string(label.OutsideBottomCenter) { + t.Fatalf("expected edge label to be placed on %s, got %s", string(label.OutsideBottomCenter), *g.Edges[0].LabelPosition) + } } func TestActivationBoxesSequenceDiagram(t *testing.T) { @@ -130,7 +147,14 @@ func TestActivationBoxesSequenceDiagram(t *testing.T) { g := d2graph.NewGraph(nil) 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.Attributes = d2graph.Attributes{ + Shape: d2graph.Scalar{Value: shape.DIAMOND_TYPE}, + Label: d2graph.Scalar{Value: "label"}, + } a_t2 := a.EnsureChild([]string{"t2"}) b := g.Root.EnsureChild([]string{"b"}) b.Box = geo.NewBox(nil, 30, 30) @@ -155,6 +179,34 @@ func TestActivationBoxesSequenceDiagram(t *testing.T) { ctx := log.WithTB(context.Background(), t, nil) Layout(ctx, g) + // check properties + if a.Attributes.Shape.Value != shape.PERSON_TYPE { + t.Fatal("actor a shape changed") + } + + if a_t1.Attributes.Label.Value != "" { + t.Fatalf("expected no label for activation box, got %s", a_t1.Attributes.Label.Value) + } + + if a_t1.Attributes.Shape.Value != shape.SQUARE_TYPE { + t.Fatalf("expected square shape for activation box, got %s", a_t1.Attributes.Shape.Value) + } + + if a_t1.Height != b_t1.Height { + t.Fatalf("expected a.t1 and b.t1 to have the same height, got %.5f and %.5f", a_t1.Height, b_t1.Height) + } + + // Y diff of the 2 first edges + expectedHeight := g.Edges[1].Route[0].Y - g.Edges[0].Route[0].Y + if a_t1.Height != expectedHeight { + t.Fatalf("expected a.t1 height to be %.5f, got %.5f", expectedHeight, a_t1.Height) + } + + if a_t1.Width != ACTIVATION_BOX_WIDTH { + t.Fatalf("expected activation box width to be %.5f, got %.5f", ACTIVATION_BOX_WIDTH, a_t1.Width) + } + + // check positions if a.Center().X != a_t1.Center().X { t.Fatal("expected a_t1.X = a.X") } @@ -164,4 +216,23 @@ func TestActivationBoxesSequenceDiagram(t *testing.T) { if b.Center().X != b_t1.Center().X { t.Fatal("expected b_t1.X = b.X") } + if a_t1.TopLeft.Y != b_t1.TopLeft.Y { + t.Fatal("expected a.t1 and b.t1 to be placed at the same Y") + } + if a_t1.TopLeft.Y != g.Edges[0].Route[0].Y { + t.Fatal("expected a.t1 to be placed at the same Y of the first edge") + } + + // check routes + if g.Edges[0].Route[0].X != a_t1.TopLeft.X+a_t1.Width { + t.Fatal("expected the first edge to start on a.t1 top right X") + } + + if g.Edges[0].Route[1].X != b_t1.TopLeft.X { + t.Fatal("expected the first edge to end on b.t1 top left X") + } + + if g.Edges[2].Route[1].X != b.Center().X { + t.Fatal("expected the third edge to end on b.t1 center X") + } } From a0c0546059cf8dd01758e3e2c89c46a8ef776d8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Tue, 29 Nov 2022 11:53:30 -0800 Subject: [PATCH 11/18] Add padding to edges --- d2layouts/d2sequence/constants.go | 3 +++ d2layouts/d2sequence/layout.go | 9 +++++++ d2layouts/d2sequence/layout_test.go | 38 +++++++++++++++++++---------- 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/d2layouts/d2sequence/constants.go b/d2layouts/d2sequence/constants.go index 3f184e92f..9f2dc2e5b 100644 --- a/d2layouts/d2sequence/constants.go +++ b/d2layouts/d2sequence/constants.go @@ -14,6 +14,9 @@ const MIN_EDGE_DISTANCE = 100. // default size const ACTIVATION_BOX_WIDTH = 20. +// small pad so that edges don't touch lifelines and activation boxes +const ACTIVATION_BOX_EDGE_PAD = 2. + // as the activation boxes start getting nested, their size grows const ACTIVATION_BOX_DEPTH_GROW_FACTOR = 10. diff --git a/d2layouts/d2sequence/layout.go b/d2layouts/d2sequence/layout.go index a26a02628..2eca23f95 100644 --- a/d2layouts/d2sequence/layout.go +++ b/d2layouts/d2sequence/layout.go @@ -238,6 +238,15 @@ func (sd *sequenceDiagram) routeEdges() { } else { endX = edge.Dst.TopLeft.X + edge.Dst.Width } + + if isLeftToRight { + startX += ACTIVATION_BOX_EDGE_PAD + endX -= ACTIVATION_BOX_EDGE_PAD + } else { + startX -= ACTIVATION_BOX_EDGE_PAD + endX += ACTIVATION_BOX_EDGE_PAD + } + edgeY := sd.getEdgeY(rank) edge.Route = []*geo.Point{ geo.NewPoint(startX, edgeY), diff --git a/d2layouts/d2sequence/layout_test.go b/d2layouts/d2sequence/layout_test.go index 28cc8570f..fcdd12352 100644 --- a/d2layouts/d2sequence/layout_test.go +++ b/d2layouts/d2sequence/layout_test.go @@ -90,11 +90,23 @@ func TestBasicSequenceDiagram(t *testing.T) { if edge.Route[0].Y != edge.Route[1].Y { t.Fatalf("expected edge[%d] to be a horizontal line", i) } - if edge.Route[0].X != edge.Src.Center().X { - t.Fatalf("expected edge[%d] source endpoint to be at the middle of the source actor", i) - } - if edge.Route[1].X != edge.Dst.Center().X { - t.Fatalf("expected edge[%d] target endpoint to be at the middle of the target actor", i) + if edge.Src.TopLeft.X < edge.Dst.TopLeft.X { + // left to right + if edge.Route[0].X != edge.Src.Center().X+ACTIVATION_BOX_EDGE_PAD { + t.Fatalf("expected edge[%d] x to be at the actor center", i) + } + + if edge.Route[1].X != edge.Dst.Center().X-ACTIVATION_BOX_EDGE_PAD { + t.Fatalf("expected edge[%d] x to be at the actor center", i) + } + } else { + if edge.Route[0].X != edge.Src.Center().X-ACTIVATION_BOX_EDGE_PAD { + t.Fatalf("expected edge[%d] x to be at the actor center", i) + } + + if edge.Route[1].X != edge.Dst.Center().X+ACTIVATION_BOX_EDGE_PAD { + t.Fatalf("expected edge[%d] x to be at the actor center", i) + } } if i > 0 { prevEdge := g.Edges[i-1] @@ -108,19 +120,19 @@ func TestBasicSequenceDiagram(t *testing.T) { for i := nEdges; i < nExpectedEdges; i++ { edge := g.Edges[i] if len(edge.Route) != 2 { - t.Fatalf("expected edge[%d] to have only 2 points", i) + t.Fatalf("expected lifeline edge[%d] to have only 2 points", i) } if edge.Route[0].X != edge.Route[1].X { - t.Fatalf("expected edge[%d] to be a vertical line", i) + t.Fatalf("expected lifeline edge[%d] to be a vertical line", i) } if edge.Route[0].X != edge.Src.Center().X { - t.Fatalf("expected edge[%d] x to be at the actor center", i) + t.Fatalf("expected lifeline edge[%d] x to be at the actor center", i) } if edge.Route[0].Y != edge.Src.Height+edge.Src.TopLeft.Y { - t.Fatalf("expected edge[%d] to start at the bottom of the source actor", i) + t.Fatalf("expected lifeline edge[%d] to start at the bottom of the source actor", i) } if edge.Route[1].Y < lastSequenceEdge.Route[0].Y { - t.Fatalf("expected edge[%d] to end after the last sequence edge", i) + t.Fatalf("expected lifeline edge[%d] to end after the last sequence edge", i) } } @@ -224,15 +236,15 @@ func TestActivationBoxesSequenceDiagram(t *testing.T) { } // check routes - if g.Edges[0].Route[0].X != a_t1.TopLeft.X+a_t1.Width { + if g.Edges[0].Route[0].X != a_t1.TopLeft.X+a_t1.Width+ACTIVATION_BOX_EDGE_PAD { t.Fatal("expected the first edge to start on a.t1 top right X") } - if g.Edges[0].Route[1].X != b_t1.TopLeft.X { + if g.Edges[0].Route[1].X != b_t1.TopLeft.X-ACTIVATION_BOX_EDGE_PAD { t.Fatal("expected the first edge to end on b.t1 top left X") } - if g.Edges[2].Route[1].X != b.Center().X { + if g.Edges[2].Route[1].X != b.Center().X-ACTIVATION_BOX_EDGE_PAD { t.Fatal("expected the third edge to end on b.t1 center X") } } From d002531768bbd6f5c2b932672e5b7cf5247b041c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Tue, 29 Nov 2022 12:57:07 -0800 Subject: [PATCH 12/18] Add vertical pad to activation boxes --- d2layouts/d2sequence/constants.go | 2 +- d2layouts/d2sequence/layout.go | 4 ++++ d2layouts/d2sequence/layout_test.go | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/d2layouts/d2sequence/constants.go b/d2layouts/d2sequence/constants.go index 9f2dc2e5b..0dfb3f2a7 100644 --- a/d2layouts/d2sequence/constants.go +++ b/d2layouts/d2sequence/constants.go @@ -15,7 +15,7 @@ const MIN_EDGE_DISTANCE = 100. const ACTIVATION_BOX_WIDTH = 20. // small pad so that edges don't touch lifelines and activation boxes -const ACTIVATION_BOX_EDGE_PAD = 2. +const ACTIVATION_BOX_EDGE_PAD = 5. // as the activation boxes start getting nested, their size grows const ACTIVATION_BOX_DEPTH_GROW_FACTOR = 10. diff --git a/d2layouts/d2sequence/layout.go b/d2layouts/d2sequence/layout.go index 2eca23f95..99ba69d63 100644 --- a/d2layouts/d2sequence/layout.go +++ b/d2layouts/d2sequence/layout.go @@ -203,10 +203,14 @@ func (sd *sequenceDiagram) placeActivationBoxes() { minY := math.Min(minEdgeY, minChildY) if minY == minChildY { minY -= ACTIVATION_BOX_DEPTH_GROW_FACTOR + } else { + minY -= ACTIVATION_BOX_EDGE_PAD } maxY := math.Max(maxEdgeY, maxChildY) if maxY == maxChildY { maxY += ACTIVATION_BOX_DEPTH_GROW_FACTOR + } else { + maxY += ACTIVATION_BOX_EDGE_PAD } height := math.Max(maxY-minY, DEFAULT_ACTIVATION_BOX_HEIGHT) diff --git a/d2layouts/d2sequence/layout_test.go b/d2layouts/d2sequence/layout_test.go index fcdd12352..90320a464 100644 --- a/d2layouts/d2sequence/layout_test.go +++ b/d2layouts/d2sequence/layout_test.go @@ -209,7 +209,7 @@ func TestActivationBoxesSequenceDiagram(t *testing.T) { } // Y diff of the 2 first edges - expectedHeight := g.Edges[1].Route[0].Y - g.Edges[0].Route[0].Y + expectedHeight := g.Edges[1].Route[0].Y - g.Edges[0].Route[0].Y + (2 * ACTIVATION_BOX_EDGE_PAD) if a_t1.Height != expectedHeight { t.Fatalf("expected a.t1 height to be %.5f, got %.5f", expectedHeight, a_t1.Height) } @@ -231,7 +231,7 @@ func TestActivationBoxesSequenceDiagram(t *testing.T) { if a_t1.TopLeft.Y != b_t1.TopLeft.Y { t.Fatal("expected a.t1 and b.t1 to be placed at the same Y") } - if a_t1.TopLeft.Y != g.Edges[0].Route[0].Y { + if a_t1.TopLeft.Y != g.Edges[0].Route[0].Y-ACTIVATION_BOX_EDGE_PAD { t.Fatal("expected a.t1 to be placed at the same Y of the first edge") } From 8c84ed037849ff43da992e86114dcc2d50473059 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Tue, 29 Nov 2022 15:14:36 -0800 Subject: [PATCH 13/18] Refactor --- d2layouts/d2sequence/constants.go | 2 +- d2layouts/d2sequence/layout.go | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/d2layouts/d2sequence/constants.go b/d2layouts/d2sequence/constants.go index 0dfb3f2a7..b042db3d9 100644 --- a/d2layouts/d2sequence/constants.go +++ b/d2layouts/d2sequence/constants.go @@ -21,4 +21,4 @@ const ACTIVATION_BOX_EDGE_PAD = 5. const ACTIVATION_BOX_DEPTH_GROW_FACTOR = 10. // when a activation box has a single edge -const DEFAULT_ACTIVATION_BOX_HEIGHT = MIN_EDGE_DISTANCE / 2. +const MIN_ACTIVATION_BOX_HEIGHT = MIN_EDGE_DISTANCE / 2. diff --git a/d2layouts/d2sequence/layout.go b/d2layouts/d2sequence/layout.go index 99ba69d63..1411f1cf9 100644 --- a/d2layouts/d2sequence/layout.go +++ b/d2layouts/d2sequence/layout.go @@ -42,11 +42,13 @@ type sequenceDiagram struct { activations []*d2graph.Object // can be either actors or activation boxes - objectRank map[*d2graph.Object]int + // rank: left to right position of actors/activations + objectRank map[*d2graph.Object]int + // depth: the nested levels of a given actor/activation objectDepth map[*d2graph.Object]int // keep track of the first and last edge of a given actor - // needed for activation boxes + // the edge rank is the order in which it appears from top to bottom minEdgeRank map[*d2graph.Object]int maxEdgeRank map[*d2graph.Object]int @@ -69,7 +71,7 @@ func (sd *sequenceDiagram) init() { sd.actors = append(sd.actors, obj) sd.objectRank[obj] = len(sd.actors) sd.objectDepth[obj] = 0 - sd.maxActorHeight = math.Max(sd.maxActorHeight, obj.Height+VERTICAL_PAD) + sd.maxActorHeight = math.Max(sd.maxActorHeight, obj.Height) } else { // activations boxes are always rectangles and have no labels obj.Attributes.Label = d2graph.Scalar{Value: ""} @@ -83,7 +85,7 @@ func (sd *sequenceDiagram) init() { } for rank, edge := range sd.edges { - sd.edgeYStep = math.Max(sd.edgeYStep, float64(edge.LabelDimensions.Height)+VERTICAL_PAD) + sd.edgeYStep = math.Max(sd.edgeYStep, float64(edge.LabelDimensions.Height)) sd.setMinMaxEdgeRank(edge.Src, rank) sd.setMinMaxEdgeRank(edge.Dst, rank) @@ -94,6 +96,9 @@ func (sd *sequenceDiagram) init() { distributedLabelWidth := float64(edge.LabelDimensions.Width) / rankDiff sd.actorXStep = math.Max(sd.actorXStep, distributedLabelWidth+HORIZONTAL_PAD) } + + sd.maxActorHeight += VERTICAL_PAD + sd.edgeYStep += VERTICAL_PAD } func (sd *sequenceDiagram) setMinMaxEdgeRank(actor *d2graph.Object, rank int) { @@ -213,7 +218,7 @@ func (sd *sequenceDiagram) placeActivationBoxes() { maxY += ACTIVATION_BOX_EDGE_PAD } - height := math.Max(maxY-minY, DEFAULT_ACTIVATION_BOX_HEIGHT) + height := math.Max(maxY-minY, MIN_ACTIVATION_BOX_HEIGHT) width := ACTIVATION_BOX_WIDTH + (float64(sd.objectDepth[activation]-1) * ACTIVATION_BOX_DEPTH_GROW_FACTOR) x := rankToX[sd.objectRank[activation]] - (width / 2.) activation.Box = geo.NewBox(geo.NewPoint(x, minY), width, height) From 82f2cd37f5a91af5c6b1d3e446524e59c7f71dd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Tue, 29 Nov 2022 15:17:47 -0800 Subject: [PATCH 14/18] Rename activation box to span box --- d2layouts/d2sequence/constants.go | 14 +++--- d2layouts/d2sequence/layout.go | 74 ++++++++++++++--------------- d2layouts/d2sequence/layout_test.go | 28 +++++------ 3 files changed, 58 insertions(+), 58 deletions(-) diff --git a/d2layouts/d2sequence/constants.go b/d2layouts/d2sequence/constants.go index b042db3d9..16b5baf48 100644 --- a/d2layouts/d2sequence/constants.go +++ b/d2layouts/d2sequence/constants.go @@ -12,13 +12,13 @@ const MIN_ACTOR_DISTANCE = 200. const MIN_EDGE_DISTANCE = 100. // default size -const ACTIVATION_BOX_WIDTH = 20. +const SPAN_BOX_WIDTH = 20. -// small pad so that edges don't touch lifelines and activation boxes -const ACTIVATION_BOX_EDGE_PAD = 5. +// small pad so that edges don't touch lifelines and span boxes +const SPAN_BOX_EDGE_PAD = 5. -// as the activation boxes start getting nested, their size grows -const ACTIVATION_BOX_DEPTH_GROW_FACTOR = 10. +// as the span boxes start getting nested, their size grows +const SPAN_BOX_DEPTH_GROW_FACTOR = 10. -// when a activation box has a single edge -const MIN_ACTIVATION_BOX_HEIGHT = MIN_EDGE_DISTANCE / 2. +// when a span box has a single edge +const MIN_SPAN_BOX_HEIGHT = MIN_EDGE_DISTANCE / 2. diff --git a/d2layouts/d2sequence/layout.go b/d2layouts/d2sequence/layout.go index 1411f1cf9..f08ba6c21 100644 --- a/d2layouts/d2sequence/layout.go +++ b/d2layouts/d2sequence/layout.go @@ -27,7 +27,7 @@ func Layout(ctx context.Context, g *d2graph.Graph) (err error) { sd.init() sd.placeActors() - sd.placeActivationBoxes() + sd.placeSpanBoxes() sd.routeEdges() sd.addLifelineEdges() @@ -37,14 +37,14 @@ func Layout(ctx context.Context, g *d2graph.Graph) (err error) { type sequenceDiagram struct { graph *d2graph.Graph - edges []*d2graph.Edge - actors []*d2graph.Object - activations []*d2graph.Object + edges []*d2graph.Edge + actors []*d2graph.Object + spans []*d2graph.Object - // can be either actors or activation boxes - // rank: left to right position of actors/activations + // can be either actors or span boxes + // rank: left to right position of actors/span boxes objectRank map[*d2graph.Object]int - // depth: the nested levels of a given actor/activation + // depth: the nested levels of a given actor/span objectDepth map[*d2graph.Object]int // keep track of the first and last edge of a given actor @@ -73,10 +73,10 @@ func (sd *sequenceDiagram) init() { sd.objectDepth[obj] = 0 sd.maxActorHeight = math.Max(sd.maxActorHeight, obj.Height) } else { - // activations boxes are always rectangles and have no labels + // span boxes are always rectangles and have no labels obj.Attributes.Label = d2graph.Scalar{Value: ""} obj.Attributes.Shape = d2graph.Scalar{Value: shape.SQUARE_TYPE} - sd.activations = append(sd.activations, obj) + sd.spans = append(sd.spans, obj) sd.objectRank[obj] = sd.objectRank[obj.Parent] sd.objectDepth[obj] = sd.objectDepth[obj.Parent] + 1 } @@ -156,72 +156,72 @@ func (sd *sequenceDiagram) addLifelineEdges() { } } -// placeActivationBoxes places activation boxes over the object lifeline +// placeSpanBoxes places span boxes over the object lifeline // ┌──────────┐ // │ actor │ // └────┬─────┘ // ┌─┴──┐ // │ │ -// activation +// |span| // │ │ // └─┬──┘ // │ // lifeline // │ -func (sd *sequenceDiagram) placeActivationBoxes() { - // quickly find the activation box center X +func (sd *sequenceDiagram) placeSpanBoxes() { + // quickly find the span box center X rankToX := make(map[int]float64) for _, actor := range sd.actors { rankToX[sd.objectRank[actor]] = actor.Center().X } - // places activation boxes from most to least nested - // the order is important because the only way a child activation box exists is if there'e an edge to it - // however, the parent activation might not have an edge to it and then its position is based on the child position + // places span boxes from most to least nested + // the order is important because the only way a child span box exists is if there'e an edge to it + // however, the parent span might not have an edge to it and then its position is based on the child position // or, there can be edge to it, but it comes after the child one meaning the top left position is still based on the child // and not on its own edge - activationFromMostNested := make([]*d2graph.Object, len(sd.activations)) - copy(activationFromMostNested, sd.activations) - sort.SliceStable(activationFromMostNested, func(i, j int) bool { - return sd.objectDepth[activationFromMostNested[i]] > sd.objectDepth[activationFromMostNested[j]] + spanFromMostNested := make([]*d2graph.Object, len(sd.spans)) + copy(spanFromMostNested, sd.spans) + sort.SliceStable(spanFromMostNested, func(i, j int) bool { + return sd.objectDepth[spanFromMostNested[i]] > sd.objectDepth[spanFromMostNested[j]] }) - for _, activation := range activationFromMostNested { + for _, span := range spanFromMostNested { // finds the position based on children minChildY := math.Inf(1) maxChildY := math.Inf(-1) - for _, child := range activation.ChildrenArray { + for _, child := range span.ChildrenArray { minChildY = math.Min(minChildY, child.TopLeft.Y) maxChildY = math.Max(maxChildY, child.TopLeft.Y+child.Height) } - // finds the position if there are edges to this activation box + // finds the position if there are edges to this span box minEdgeY := math.Inf(1) - if minRank, exists := sd.minEdgeRank[activation]; exists { + if minRank, exists := sd.minEdgeRank[span]; exists { minEdgeY = sd.getEdgeY(minRank) } maxEdgeY := math.Inf(-1) - if maxRank, exists := sd.maxEdgeRank[activation]; exists { + if maxRank, exists := sd.maxEdgeRank[span]; exists { maxEdgeY = sd.getEdgeY(maxRank) } // if it is the same as the child top left, add some padding minY := math.Min(minEdgeY, minChildY) if minY == minChildY { - minY -= ACTIVATION_BOX_DEPTH_GROW_FACTOR + minY -= SPAN_BOX_DEPTH_GROW_FACTOR } else { - minY -= ACTIVATION_BOX_EDGE_PAD + minY -= SPAN_BOX_EDGE_PAD } maxY := math.Max(maxEdgeY, maxChildY) if maxY == maxChildY { - maxY += ACTIVATION_BOX_DEPTH_GROW_FACTOR + maxY += SPAN_BOX_DEPTH_GROW_FACTOR } else { - maxY += ACTIVATION_BOX_EDGE_PAD + maxY += SPAN_BOX_EDGE_PAD } - height := math.Max(maxY-minY, MIN_ACTIVATION_BOX_HEIGHT) - width := ACTIVATION_BOX_WIDTH + (float64(sd.objectDepth[activation]-1) * ACTIVATION_BOX_DEPTH_GROW_FACTOR) - x := rankToX[sd.objectRank[activation]] - (width / 2.) - activation.Box = geo.NewBox(geo.NewPoint(x, minY), width, height) + height := math.Max(maxY-minY, MIN_SPAN_BOX_HEIGHT) + width := SPAN_BOX_WIDTH + (float64(sd.objectDepth[span]-1) * SPAN_BOX_DEPTH_GROW_FACTOR) + x := rankToX[sd.objectRank[span]] - (width / 2.) + span.Box = geo.NewBox(geo.NewPoint(x, minY), width, height) } } @@ -249,11 +249,11 @@ func (sd *sequenceDiagram) routeEdges() { } if isLeftToRight { - startX += ACTIVATION_BOX_EDGE_PAD - endX -= ACTIVATION_BOX_EDGE_PAD + startX += SPAN_BOX_EDGE_PAD + endX -= SPAN_BOX_EDGE_PAD } else { - startX -= ACTIVATION_BOX_EDGE_PAD - endX += ACTIVATION_BOX_EDGE_PAD + startX -= SPAN_BOX_EDGE_PAD + endX += SPAN_BOX_EDGE_PAD } edgeY := sd.getEdgeY(rank) diff --git a/d2layouts/d2sequence/layout_test.go b/d2layouts/d2sequence/layout_test.go index 90320a464..be2e83d85 100644 --- a/d2layouts/d2sequence/layout_test.go +++ b/d2layouts/d2sequence/layout_test.go @@ -92,19 +92,19 @@ func TestBasicSequenceDiagram(t *testing.T) { } if edge.Src.TopLeft.X < edge.Dst.TopLeft.X { // left to right - if edge.Route[0].X != edge.Src.Center().X+ACTIVATION_BOX_EDGE_PAD { + if edge.Route[0].X != edge.Src.Center().X+SPAN_BOX_EDGE_PAD { t.Fatalf("expected edge[%d] x to be at the actor center", i) } - if edge.Route[1].X != edge.Dst.Center().X-ACTIVATION_BOX_EDGE_PAD { + if edge.Route[1].X != edge.Dst.Center().X-SPAN_BOX_EDGE_PAD { t.Fatalf("expected edge[%d] x to be at the actor center", i) } } else { - if edge.Route[0].X != edge.Src.Center().X-ACTIVATION_BOX_EDGE_PAD { + if edge.Route[0].X != edge.Src.Center().X-SPAN_BOX_EDGE_PAD { t.Fatalf("expected edge[%d] x to be at the actor center", i) } - if edge.Route[1].X != edge.Dst.Center().X+ACTIVATION_BOX_EDGE_PAD { + if edge.Route[1].X != edge.Dst.Center().X+SPAN_BOX_EDGE_PAD { t.Fatalf("expected edge[%d] x to be at the actor center", i) } } @@ -146,7 +146,7 @@ func TestBasicSequenceDiagram(t *testing.T) { } } -func TestActivationBoxesSequenceDiagram(t *testing.T) { +func TestSpanBoxesSequenceDiagram(t *testing.T) { // ┌─────┐ ┌─────┐ // │ a │ │ b │ // └──┬──┘ └──┬──┘ @@ -197,11 +197,11 @@ func TestActivationBoxesSequenceDiagram(t *testing.T) { } if a_t1.Attributes.Label.Value != "" { - t.Fatalf("expected no label for activation box, got %s", a_t1.Attributes.Label.Value) + t.Fatalf("expected no label for span box, got %s", a_t1.Attributes.Label.Value) } if a_t1.Attributes.Shape.Value != shape.SQUARE_TYPE { - t.Fatalf("expected square shape for activation box, got %s", a_t1.Attributes.Shape.Value) + t.Fatalf("expected square shape for span box, got %s", a_t1.Attributes.Shape.Value) } if a_t1.Height != b_t1.Height { @@ -209,13 +209,13 @@ func TestActivationBoxesSequenceDiagram(t *testing.T) { } // Y diff of the 2 first edges - expectedHeight := g.Edges[1].Route[0].Y - g.Edges[0].Route[0].Y + (2 * ACTIVATION_BOX_EDGE_PAD) + expectedHeight := g.Edges[1].Route[0].Y - g.Edges[0].Route[0].Y + (2 * SPAN_BOX_EDGE_PAD) if a_t1.Height != expectedHeight { t.Fatalf("expected a.t1 height to be %.5f, got %.5f", expectedHeight, a_t1.Height) } - if a_t1.Width != ACTIVATION_BOX_WIDTH { - t.Fatalf("expected activation box width to be %.5f, got %.5f", ACTIVATION_BOX_WIDTH, a_t1.Width) + if a_t1.Width != SPAN_BOX_WIDTH { + t.Fatalf("expected span box width to be %.5f, got %.5f", SPAN_BOX_WIDTH, a_t1.Width) } // check positions @@ -231,20 +231,20 @@ func TestActivationBoxesSequenceDiagram(t *testing.T) { if a_t1.TopLeft.Y != b_t1.TopLeft.Y { t.Fatal("expected a.t1 and b.t1 to be placed at the same Y") } - if a_t1.TopLeft.Y != g.Edges[0].Route[0].Y-ACTIVATION_BOX_EDGE_PAD { + if a_t1.TopLeft.Y != g.Edges[0].Route[0].Y-SPAN_BOX_EDGE_PAD { t.Fatal("expected a.t1 to be placed at the same Y of the first edge") } // check routes - if g.Edges[0].Route[0].X != a_t1.TopLeft.X+a_t1.Width+ACTIVATION_BOX_EDGE_PAD { + if g.Edges[0].Route[0].X != a_t1.TopLeft.X+a_t1.Width+SPAN_BOX_EDGE_PAD { t.Fatal("expected the first edge to start on a.t1 top right X") } - if g.Edges[0].Route[1].X != b_t1.TopLeft.X-ACTIVATION_BOX_EDGE_PAD { + if g.Edges[0].Route[1].X != b_t1.TopLeft.X-SPAN_BOX_EDGE_PAD { t.Fatal("expected the first edge to end on b.t1 top left X") } - if g.Edges[2].Route[1].X != b.Center().X-ACTIVATION_BOX_EDGE_PAD { + if g.Edges[2].Route[1].X != b.Center().X-SPAN_BOX_EDGE_PAD { t.Fatal("expected the third edge to end on b.t1 center X") } } From 68638899bfe5a1cbbc42492afb96cb8dd0454982 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Tue, 29 Nov 2022 15:40:43 -0800 Subject: [PATCH 15/18] Span boxes -> spans --- d2layouts/d2sequence/constants.go | 12 ++++---- d2layouts/d2sequence/layout.go | 48 ++++++++++++++--------------- d2layouts/d2sequence/layout_test.go | 28 ++++++++--------- 3 files changed, 44 insertions(+), 44 deletions(-) diff --git a/d2layouts/d2sequence/constants.go b/d2layouts/d2sequence/constants.go index 16b5baf48..4c36086b2 100644 --- a/d2layouts/d2sequence/constants.go +++ b/d2layouts/d2sequence/constants.go @@ -12,13 +12,13 @@ const MIN_ACTOR_DISTANCE = 200. const MIN_EDGE_DISTANCE = 100. // default size -const SPAN_BOX_WIDTH = 20. +const SPAN_WIDTH = 20. -// small pad so that edges don't touch lifelines and span boxes -const SPAN_BOX_EDGE_PAD = 5. +// small pad so that edges don't touch lifelines and spans +const SPAN_EDGE_PAD = 5. -// as the span boxes start getting nested, their size grows +// as the spans start getting nested, their size grows const SPAN_BOX_DEPTH_GROW_FACTOR = 10. -// when a span box has a single edge -const MIN_SPAN_BOX_HEIGHT = MIN_EDGE_DISTANCE / 2. +// when a span has a single edge +const MIN_SPAN_HEIGHT = MIN_EDGE_DISTANCE / 2. diff --git a/d2layouts/d2sequence/layout.go b/d2layouts/d2sequence/layout.go index f08ba6c21..dbba8328e 100644 --- a/d2layouts/d2sequence/layout.go +++ b/d2layouts/d2sequence/layout.go @@ -17,7 +17,7 @@ func Layout(ctx context.Context, g *d2graph.Graph) (err error) { sd := &sequenceDiagram{ graph: g, objectRank: make(map[*d2graph.Object]int), - objectDepth: make(map[*d2graph.Object]int), + objectLevel: make(map[*d2graph.Object]int), minEdgeRank: make(map[*d2graph.Object]int), maxEdgeRank: make(map[*d2graph.Object]int), edgeYStep: MIN_EDGE_DISTANCE, @@ -27,7 +27,7 @@ func Layout(ctx context.Context, g *d2graph.Graph) (err error) { sd.init() sd.placeActors() - sd.placeSpanBoxes() + sd.placeSpans() sd.routeEdges() sd.addLifelineEdges() @@ -41,11 +41,11 @@ type sequenceDiagram struct { actors []*d2graph.Object spans []*d2graph.Object - // can be either actors or span boxes - // rank: left to right position of actors/span boxes + // can be either actors or spans + // rank: left to right position of actors/spans (spans have the same rank as their parents) objectRank map[*d2graph.Object]int - // depth: the nested levels of a given actor/span - objectDepth map[*d2graph.Object]int + // similar to d2graph.Object.Level() just don't make the recursive calls + objectLevel map[*d2graph.Object]int // keep track of the first and last edge of a given actor // the edge rank is the order in which it appears from top to bottom @@ -70,15 +70,15 @@ func (sd *sequenceDiagram) init() { if sd.isActor(obj) { sd.actors = append(sd.actors, obj) sd.objectRank[obj] = len(sd.actors) - sd.objectDepth[obj] = 0 + sd.objectLevel[obj] = 0 sd.maxActorHeight = math.Max(sd.maxActorHeight, obj.Height) } else { - // span boxes are always rectangles and have no labels + // spans are always rectangles and have no labels obj.Attributes.Label = d2graph.Scalar{Value: ""} obj.Attributes.Shape = d2graph.Scalar{Value: shape.SQUARE_TYPE} sd.spans = append(sd.spans, obj) sd.objectRank[obj] = sd.objectRank[obj.Parent] - sd.objectDepth[obj] = sd.objectDepth[obj.Parent] + 1 + sd.objectLevel[obj] = sd.objectLevel[obj.Parent] + 1 } queue = append(queue, obj.ChildrenArray...) @@ -156,7 +156,7 @@ func (sd *sequenceDiagram) addLifelineEdges() { } } -// placeSpanBoxes places span boxes over the object lifeline +// placeSpans places spans over the object lifeline // ┌──────────┐ // │ actor │ // └────┬─────┘ @@ -168,22 +168,22 @@ func (sd *sequenceDiagram) addLifelineEdges() { // │ // lifeline // │ -func (sd *sequenceDiagram) placeSpanBoxes() { - // quickly find the span box center X +func (sd *sequenceDiagram) placeSpans() { + // quickly find the span center X rankToX := make(map[int]float64) for _, actor := range sd.actors { rankToX[sd.objectRank[actor]] = actor.Center().X } - // places span boxes from most to least nested - // the order is important because the only way a child span box exists is if there'e an edge to it + // places spans from most to least nested + // the order is important because the only way a child span exists is if there'e an edge to it // however, the parent span might not have an edge to it and then its position is based on the child position // or, there can be edge to it, but it comes after the child one meaning the top left position is still based on the child // and not on its own edge spanFromMostNested := make([]*d2graph.Object, len(sd.spans)) copy(spanFromMostNested, sd.spans) sort.SliceStable(spanFromMostNested, func(i, j int) bool { - return sd.objectDepth[spanFromMostNested[i]] > sd.objectDepth[spanFromMostNested[j]] + return sd.objectLevel[spanFromMostNested[i]] > sd.objectLevel[spanFromMostNested[j]] }) for _, span := range spanFromMostNested { // finds the position based on children @@ -194,7 +194,7 @@ func (sd *sequenceDiagram) placeSpanBoxes() { maxChildY = math.Max(maxChildY, child.TopLeft.Y+child.Height) } - // finds the position if there are edges to this span box + // finds the position if there are edges to this span minEdgeY := math.Inf(1) if minRank, exists := sd.minEdgeRank[span]; exists { minEdgeY = sd.getEdgeY(minRank) @@ -209,17 +209,17 @@ func (sd *sequenceDiagram) placeSpanBoxes() { if minY == minChildY { minY -= SPAN_BOX_DEPTH_GROW_FACTOR } else { - minY -= SPAN_BOX_EDGE_PAD + minY -= SPAN_EDGE_PAD } maxY := math.Max(maxEdgeY, maxChildY) if maxY == maxChildY { maxY += SPAN_BOX_DEPTH_GROW_FACTOR } else { - maxY += SPAN_BOX_EDGE_PAD + maxY += SPAN_EDGE_PAD } - height := math.Max(maxY-minY, MIN_SPAN_BOX_HEIGHT) - width := SPAN_BOX_WIDTH + (float64(sd.objectDepth[span]-1) * SPAN_BOX_DEPTH_GROW_FACTOR) + height := math.Max(maxY-minY, MIN_SPAN_HEIGHT) + width := SPAN_WIDTH + (float64(sd.objectLevel[span]-1) * SPAN_BOX_DEPTH_GROW_FACTOR) x := rankToX[sd.objectRank[span]] - (width / 2.) span.Box = geo.NewBox(geo.NewPoint(x, minY), width, height) } @@ -249,11 +249,11 @@ func (sd *sequenceDiagram) routeEdges() { } if isLeftToRight { - startX += SPAN_BOX_EDGE_PAD - endX -= SPAN_BOX_EDGE_PAD + startX += SPAN_EDGE_PAD + endX -= SPAN_EDGE_PAD } else { - startX -= SPAN_BOX_EDGE_PAD - endX += SPAN_BOX_EDGE_PAD + startX -= SPAN_EDGE_PAD + endX += SPAN_EDGE_PAD } edgeY := sd.getEdgeY(rank) diff --git a/d2layouts/d2sequence/layout_test.go b/d2layouts/d2sequence/layout_test.go index be2e83d85..26d88f9fe 100644 --- a/d2layouts/d2sequence/layout_test.go +++ b/d2layouts/d2sequence/layout_test.go @@ -92,19 +92,19 @@ func TestBasicSequenceDiagram(t *testing.T) { } if edge.Src.TopLeft.X < edge.Dst.TopLeft.X { // left to right - if edge.Route[0].X != edge.Src.Center().X+SPAN_BOX_EDGE_PAD { + if edge.Route[0].X != edge.Src.Center().X+SPAN_EDGE_PAD { t.Fatalf("expected edge[%d] x to be at the actor center", i) } - if edge.Route[1].X != edge.Dst.Center().X-SPAN_BOX_EDGE_PAD { + if edge.Route[1].X != edge.Dst.Center().X-SPAN_EDGE_PAD { t.Fatalf("expected edge[%d] x to be at the actor center", i) } } else { - if edge.Route[0].X != edge.Src.Center().X-SPAN_BOX_EDGE_PAD { + if edge.Route[0].X != edge.Src.Center().X-SPAN_EDGE_PAD { t.Fatalf("expected edge[%d] x to be at the actor center", i) } - if edge.Route[1].X != edge.Dst.Center().X+SPAN_BOX_EDGE_PAD { + if edge.Route[1].X != edge.Dst.Center().X+SPAN_EDGE_PAD { t.Fatalf("expected edge[%d] x to be at the actor center", i) } } @@ -146,7 +146,7 @@ func TestBasicSequenceDiagram(t *testing.T) { } } -func TestSpanBoxesSequenceDiagram(t *testing.T) { +func TestSpansSequenceDiagram(t *testing.T) { // ┌─────┐ ┌─────┐ // │ a │ │ b │ // └──┬──┘ └──┬──┘ @@ -197,11 +197,11 @@ func TestSpanBoxesSequenceDiagram(t *testing.T) { } if a_t1.Attributes.Label.Value != "" { - t.Fatalf("expected no label for span box, got %s", a_t1.Attributes.Label.Value) + t.Fatalf("expected no label for span, got %s", a_t1.Attributes.Label.Value) } if a_t1.Attributes.Shape.Value != shape.SQUARE_TYPE { - t.Fatalf("expected square shape for span box, got %s", a_t1.Attributes.Shape.Value) + t.Fatalf("expected square shape for span, got %s", a_t1.Attributes.Shape.Value) } if a_t1.Height != b_t1.Height { @@ -209,13 +209,13 @@ func TestSpanBoxesSequenceDiagram(t *testing.T) { } // Y diff of the 2 first edges - expectedHeight := g.Edges[1].Route[0].Y - g.Edges[0].Route[0].Y + (2 * SPAN_BOX_EDGE_PAD) + expectedHeight := g.Edges[1].Route[0].Y - g.Edges[0].Route[0].Y + (2 * SPAN_EDGE_PAD) if a_t1.Height != expectedHeight { t.Fatalf("expected a.t1 height to be %.5f, got %.5f", expectedHeight, a_t1.Height) } - if a_t1.Width != SPAN_BOX_WIDTH { - t.Fatalf("expected span box width to be %.5f, got %.5f", SPAN_BOX_WIDTH, a_t1.Width) + if a_t1.Width != SPAN_WIDTH { + t.Fatalf("expected span width to be %.5f, got %.5f", SPAN_WIDTH, a_t1.Width) } // check positions @@ -231,20 +231,20 @@ func TestSpanBoxesSequenceDiagram(t *testing.T) { if a_t1.TopLeft.Y != b_t1.TopLeft.Y { t.Fatal("expected a.t1 and b.t1 to be placed at the same Y") } - if a_t1.TopLeft.Y != g.Edges[0].Route[0].Y-SPAN_BOX_EDGE_PAD { + if a_t1.TopLeft.Y != g.Edges[0].Route[0].Y-SPAN_EDGE_PAD { t.Fatal("expected a.t1 to be placed at the same Y of the first edge") } // check routes - if g.Edges[0].Route[0].X != a_t1.TopLeft.X+a_t1.Width+SPAN_BOX_EDGE_PAD { + if g.Edges[0].Route[0].X != a_t1.TopLeft.X+a_t1.Width+SPAN_EDGE_PAD { t.Fatal("expected the first edge to start on a.t1 top right X") } - if g.Edges[0].Route[1].X != b_t1.TopLeft.X-SPAN_BOX_EDGE_PAD { + if g.Edges[0].Route[1].X != b_t1.TopLeft.X-SPAN_EDGE_PAD { t.Fatal("expected the first edge to end on b.t1 top left X") } - if g.Edges[2].Route[1].X != b.Center().X-SPAN_BOX_EDGE_PAD { + if g.Edges[2].Route[1].X != b.Center().X-SPAN_EDGE_PAD { t.Fatal("expected the third edge to end on b.t1 center X") } } From de58624f06898ecffb0b3613ab416455b57b661e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Tue, 29 Nov 2022 16:16:41 -0800 Subject: [PATCH 16/18] Rename span box --- d2layouts/d2sequence/constants.go | 2 +- d2layouts/d2sequence/layout.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/d2layouts/d2sequence/constants.go b/d2layouts/d2sequence/constants.go index 4c36086b2..7dc2e8798 100644 --- a/d2layouts/d2sequence/constants.go +++ b/d2layouts/d2sequence/constants.go @@ -18,7 +18,7 @@ const SPAN_WIDTH = 20. const SPAN_EDGE_PAD = 5. // as the spans start getting nested, their size grows -const SPAN_BOX_DEPTH_GROW_FACTOR = 10. +const SPAN_DEPTH_GROW_FACTOR = 10. // when a span has a single edge const MIN_SPAN_HEIGHT = MIN_EDGE_DISTANCE / 2. diff --git a/d2layouts/d2sequence/layout.go b/d2layouts/d2sequence/layout.go index dbba8328e..d0d91de7a 100644 --- a/d2layouts/d2sequence/layout.go +++ b/d2layouts/d2sequence/layout.go @@ -207,19 +207,19 @@ func (sd *sequenceDiagram) placeSpans() { // if it is the same as the child top left, add some padding minY := math.Min(minEdgeY, minChildY) if minY == minChildY { - minY -= SPAN_BOX_DEPTH_GROW_FACTOR + minY -= SPAN_DEPTH_GROW_FACTOR } else { minY -= SPAN_EDGE_PAD } maxY := math.Max(maxEdgeY, maxChildY) if maxY == maxChildY { - maxY += SPAN_BOX_DEPTH_GROW_FACTOR + maxY += SPAN_DEPTH_GROW_FACTOR } else { maxY += SPAN_EDGE_PAD } height := math.Max(maxY-minY, MIN_SPAN_HEIGHT) - width := SPAN_WIDTH + (float64(sd.objectLevel[span]-1) * SPAN_BOX_DEPTH_GROW_FACTOR) + width := SPAN_WIDTH + (float64(sd.objectLevel[span]-1) * SPAN_DEPTH_GROW_FACTOR) x := rankToX[sd.objectRank[span]] - (width / 2.) span.Box = geo.NewBox(geo.NewPoint(x, minY), width, height) } From 5a5240b73b693d0f985a8b418ce255ab546cdc49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20C=C3=A9sar=20Batista?= Date: Tue, 29 Nov 2022 21:34:14 -0800 Subject: [PATCH 17/18] Remove objectDepth --- d2layouts/d2sequence/layout.go | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/d2layouts/d2sequence/layout.go b/d2layouts/d2sequence/layout.go index d0d91de7a..b77037d60 100644 --- a/d2layouts/d2sequence/layout.go +++ b/d2layouts/d2sequence/layout.go @@ -17,7 +17,6 @@ func Layout(ctx context.Context, g *d2graph.Graph) (err error) { sd := &sequenceDiagram{ graph: g, objectRank: make(map[*d2graph.Object]int), - objectLevel: make(map[*d2graph.Object]int), minEdgeRank: make(map[*d2graph.Object]int), maxEdgeRank: make(map[*d2graph.Object]int), edgeYStep: MIN_EDGE_DISTANCE, @@ -44,8 +43,6 @@ type sequenceDiagram struct { // can be either actors or spans // rank: left to right position of actors/spans (spans have the same rank as their parents) objectRank map[*d2graph.Object]int - // similar to d2graph.Object.Level() just don't make the recursive calls - objectLevel map[*d2graph.Object]int // keep track of the first and last edge of a given actor // the edge rank is the order in which it appears from top to bottom @@ -70,7 +67,6 @@ func (sd *sequenceDiagram) init() { if sd.isActor(obj) { sd.actors = append(sd.actors, obj) sd.objectRank[obj] = len(sd.actors) - sd.objectLevel[obj] = 0 sd.maxActorHeight = math.Max(sd.maxActorHeight, obj.Height) } else { // spans are always rectangles and have no labels @@ -78,7 +74,6 @@ func (sd *sequenceDiagram) init() { obj.Attributes.Shape = d2graph.Scalar{Value: shape.SQUARE_TYPE} sd.spans = append(sd.spans, obj) sd.objectRank[obj] = sd.objectRank[obj.Parent] - sd.objectLevel[obj] = sd.objectLevel[obj.Parent] + 1 } queue = append(queue, obj.ChildrenArray...) @@ -183,7 +178,7 @@ func (sd *sequenceDiagram) placeSpans() { spanFromMostNested := make([]*d2graph.Object, len(sd.spans)) copy(spanFromMostNested, sd.spans) sort.SliceStable(spanFromMostNested, func(i, j int) bool { - return sd.objectLevel[spanFromMostNested[i]] > sd.objectLevel[spanFromMostNested[j]] + return spanFromMostNested[i].Level() > spanFromMostNested[j].Level() }) for _, span := range spanFromMostNested { // finds the position based on children @@ -219,7 +214,8 @@ func (sd *sequenceDiagram) placeSpans() { } height := math.Max(maxY-minY, MIN_SPAN_HEIGHT) - width := SPAN_WIDTH + (float64(sd.objectLevel[span]-1) * SPAN_DEPTH_GROW_FACTOR) + // -2 because the actors count as level 1 making the first level span getting 2*SPAN_DEPTH_GROW_FACTOR + width := SPAN_WIDTH + (float64(span.Level()-2) * SPAN_DEPTH_GROW_FACTOR) x := rankToX[sd.objectRank[span]] - (width / 2.) span.Box = geo.NewBox(geo.NewPoint(x, minY), width, height) } From 7f7977eb8b2f7594e72391f2f068715160835536 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 30 Nov 2022 03:52:52 -0800 Subject: [PATCH 18/18] ci: Fix d2chaos upload --- .github/workflows/ci.yml | 2 +- .github/workflows/daily.yml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4e90106c..384cfa3f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: - uses: actions/upload-artifact@v3 if: always() with: - name: d2chaos-test + name: d2chaos path: ./d2chaos/out nofixups: runs-on: ubuntu-latest diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml index 9fc372532..83cf42d57 100644 --- a/.github/workflows/daily.yml +++ b/.github/workflows/daily.yml @@ -22,3 +22,8 @@ jobs: env: GITHUB_TOKEN: ${{ secrets._GITHUB_TOKEN }} DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} + - uses: actions/upload-artifact@v3 + if: always() + with: + name: d2chaos + path: ./d2chaos/out