Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds basic sequence diagrams behind a layout flag #219

Merged
merged 7 commits into from
Nov 28, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions d2layouts/d2sequence/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package d2sequence

// min horizontal pad for actors, or edge labels, to consider the min distance between them
const MIN_HORIZONTAL_PAD = 50.

const MIN_ACTOR_DISTANCE = 200.

// min vertical distance between edges
const MIN_EDGE_DISTANCE = 100.
86 changes: 38 additions & 48 deletions d2layouts/d2sequence/layout.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,45 +12,33 @@ import (
)

func Layout(ctx context.Context, g *d2graph.Graph) (err error) {
pad := 50. // 2 * 25
edgeYStep := 100.
objectXStep := 200.
maxObjectHeight := 0.
pad := MIN_HORIZONTAL_PAD
edgeYStep := MIN_EDGE_DISTANCE
actorXStep := MIN_ACTOR_DISTANCE
maxActorHeight := 0.

var objectsInOrder []*d2graph.Object
seen := make(map[*d2graph.Object]struct{})
for _, edge := range g.Edges {
if _, exists := seen[edge.Src]; !exists {
seen[edge.Src] = struct{}{}
objectsInOrder = append(objectsInOrder, edge.Src)
}
if _, exists := seen[edge.Dst]; !exists {
seen[edge.Dst] = struct{}{}
objectsInOrder = append(objectsInOrder, edge.Dst)
}

edgeYStep = math.Max(edgeYStep, float64(edge.LabelDimensions.Height)+pad)
objectXStep = math.Max(objectXStep, float64(edge.LabelDimensions.Width)+pad)
maxObjectHeight = math.Max(maxObjectHeight, edge.Src.Height+pad)
maxObjectHeight = math.Max(maxObjectHeight, edge.Dst.Height+pad)
actorXStep = math.Max(actorXStep, float64(edge.LabelDimensions.Width)+pad)
maxActorHeight = math.Max(maxActorHeight, edge.Src.Height+pad)
maxActorHeight = math.Max(maxActorHeight, edge.Dst.Height+pad)
}

placeObjects(objectsInOrder, maxObjectHeight, objectXStep)
// edges are placed in the order users define them
routeEdges(g.Edges, maxObjectHeight, edgeYStep)
addLifelineEdges(g, objectsInOrder, edgeYStep)
placeActors(g.Objects, maxActorHeight, actorXStep)
routeEdges(g.Edges, maxActorHeight, edgeYStep)
addLifelineEdges(g, g.Objects, edgeYStep)

return nil
}

// placeObjects places objects side by side
func placeObjects(objectsInOrder []*d2graph.Object, maxHeight, xStep float64) {
// placeActors places actors bottom aligned, side by side
func placeActors(actors []*d2graph.Object, maxHeight, xStep float64) {
x := 0.
for _, obj := range objectsInOrder {
yDiff := maxHeight - obj.Height
obj.TopLeft = geo.NewPoint(x, yDiff/2.)
x += obj.Width + xStep
obj.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
for _, actors := range actors {
yOffset := maxHeight - actors.Height
actors.TopLeft = geo.NewPoint(x, yOffset)
x += actors.Width + xStep
actors.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
}
}

Expand All @@ -66,45 +54,47 @@ func routeEdges(edgesInOrder []*d2graph.Edge, startY, yStep float64) {
edgeY += yStep

if edge.Attributes.Label.Value != "" {
// TODO: consider label right-to-left
edge.LabelPosition = go2.Pointer(string(label.OutsideTopCenter))
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 object in the graph that represents the
// edge below he object showing its lifespan
// addLifelineEdges adds a new edge for each actor in the graph that represents the
// edge below the actor showing its lifespan
// ┌──────────────┐
// │ object
// │ actor
// └──────┬───────┘
// │
// │ lifeline
// │
// │
func addLifelineEdges(g *d2graph.Graph, objectsInOrder []*d2graph.Object, yStep float64) {
func addLifelineEdges(g *d2graph.Graph, actors []*d2graph.Object, yStep float64) {
endY := g.Edges[len(g.Edges)-1].Route[0].Y + yStep
for _, obj := range objectsInOrder {
objBottom := obj.Center()
objBottom.Y = obj.TopLeft.Y + obj.Height
objLifelineEnd := obj.Center()
objLifelineEnd.Y = endY
for _, actor := range actors {
actorBottom := actor.Center()
actorBottom.Y = actor.TopLeft.Y + actor.Height
actorLifelineEnd := actor.Center()
actorLifelineEnd.Y = endY
g.Edges = append(g.Edges, &d2graph.Edge{
Attributes: d2graph.Attributes{
Style: d2graph.Style{
StrokeDash: &d2graph.Scalar{
Value: "10",
},
Stroke: obj.Attributes.Style.Stroke,
StrokeWidth: obj.Attributes.Style.StrokeWidth,
StrokeDash: &d2graph.Scalar{Value: "10"},
Stroke: actor.Attributes.Style.Stroke,
StrokeWidth: actor.Attributes.Style.StrokeWidth,
},
},
Src: obj,
Src: actor,
SrcArrow: false,
Dst: &d2graph.Object{
ID: obj.ID + fmt.Sprintf("-lifeline-end-%d", go2.StringToIntHash(obj.ID+"-lifeline-end")),
ID: actor.ID + fmt.Sprintf("-lifeline-end-%d", go2.StringToIntHash(actor.ID+"-lifeline-end")),
},
DstArrow: false,
Route: []*geo.Point{objBottom, objLifelineEnd},
Route: []*geo.Point{actorBottom, actorLifelineEnd},
})
}
}
30 changes: 16 additions & 14 deletions d2layouts/d2sequence/layout_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,27 +45,29 @@ func TestLayout(t *testing.T) {
ctx := log.WithTB(context.Background(), t, nil)
Layout(ctx, g)

// asserts that objects were placed in the expected x order and at y=0
objectsOrder := []*d2graph.Object{
// asserts that actors were placed in the expected x order and at y=0
actors := []*d2graph.Object{
g.Objects[0],
g.Objects[1],
}
for i := 1; i < len(objectsOrder); i++ {
if objectsOrder[i].TopLeft.X < objectsOrder[i-1].TopLeft.X {
t.Fatalf("expected object[%d].TopLeft.X > object[%d].TopLeft.X", i, i-1)
for i := 1; i < len(actors); i++ {
if actors[i].TopLeft.X < actors[i-1].TopLeft.X {
t.Fatalf("expected actor[%d].TopLeft.X > actor[%d].TopLeft.X", i, i-1)
}
if objectsOrder[i].Center().Y != objectsOrder[i-1].Center().Y {
t.Fatalf("expected object[%d] and object[%d] to be at the same center y", i, i-1)
actorBottom := actors[i].TopLeft.Y + actors[i].Height
prevActorBottom := actors[i-1].TopLeft.Y + actors[i-1].Height
if actorBottom != prevActorBottom {
t.Fatalf("expected actor[%d] and actor[%d] to be at the same bottom y", i, i-1)
}
}

nExpectedEdges := nEdges + len(objectsOrder)
nExpectedEdges := nEdges + len(actors)
if len(g.Edges) != nExpectedEdges {
t.Fatalf("expected %d edges, got %d", nExpectedEdges, len(g.Edges))
}

// assert that edges were placed in y order and have the endpoints at their objects
// uses `nEdges` because Layout creates some vertical edges to represent the object lifeline
// assert that edges were placed in y order and have the endpoints at their actors
// uses `nEdges` because Layout creates some vertical edges to represent the actor lifeline
for i := 0; i < nEdges; i++ {
edge := g.Edges[i]
if len(edge.Route) != 2 {
Expand All @@ -75,10 +77,10 @@ func TestLayout(t *testing.T) {
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 object", i)
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 object", i)
t.Fatalf("expected edge[%d] target endpoint to be at the middle of the target actor", i)
}
if i > 0 {
prevEdge := g.Edges[i-1]
Expand All @@ -98,10 +100,10 @@ func TestLayout(t *testing.T) {
t.Fatalf("expected 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 object center", i)
t.Fatalf("expected 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 object", i)
t.Fatalf("expected 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)
Expand Down