Merge pull request #317 from ejulio-ts/nested-sequence-diagrams

layout: sequence diagrams
This commit is contained in:
ejulio-ts 2022-12-02 13:03:30 -08:00 committed by GitHub
commit ef757fb532
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 17850 additions and 300 deletions

View file

@ -1,5 +1,7 @@
#### Features 🚀
- Sequence diagrams are now supported. See [docs](https://d2lang.com/tour/sequence-diagrams) for more.
[#99](https://github.com/terrastruct/d2/issues/99)
- Formatting of d2 scripts is supported on the CLI with the `fmt` subcommand.
[#292](https://github.com/terrastruct/d2/pull/292)
- Latex is now supported. See [docs](https://d2lang.com/tour/text) for more.

View file

@ -3,22 +3,22 @@ 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
// leaves at least 25 units of space on the top/bottom when computing the space required between messages
const VERTICAL_PAD = 50.
const MIN_ACTOR_DISTANCE = 200.
// min vertical distance between edges
const MIN_EDGE_DISTANCE = 100.
// min vertical distance between messages
const MIN_MESSAGE_DISTANCE = 100.
// default size
const SPAN_WIDTH = 20.
// small pad so that edges don't touch lifelines and spans
const SPAN_EDGE_PAD = 5.
// small pad so that messages don't touch lifelines and spans
const SPAN_MESSAGE_PAD = 5.
// as the spans start getting nested, their size grows
const SPAN_DEPTH_GROW_FACTOR = 10.
// when a span has a single edge
const MIN_SPAN_HEIGHT = MIN_EDGE_DISTANCE / 2.
// when a span has a single messages
const MIN_SPAN_HEIGHT = MIN_MESSAGE_DISTANCE / 2.

View file

@ -2,280 +2,169 @@ package d2sequence
import (
"context"
"fmt"
"math"
"sort"
"strings"
"oss.terrastruct.com/util-go/go2"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/label"
"oss.terrastruct.com/d2/lib/shape"
)
func Layout(ctx context.Context, g *d2graph.Graph) (err error) {
sd := &sequenceDiagram{
graph: g,
objectRank: make(map[*d2graph.Object]int),
minEdgeRank: make(map[*d2graph.Object]int),
maxEdgeRank: make(map[*d2graph.Object]int),
edgeYStep: MIN_EDGE_DISTANCE,
actorXStep: MIN_ACTOR_DISTANCE,
maxActorHeight: 0.,
}
// Layout runs the sequence diagram layout engine on objects of shape sequence_diagram
//
// 1. Traverse graph from root, skip objects with shape not `sequence_diagram`
// 2. Construct a sequence diagram from all descendant objects and edges
// 3. Remove those objects and edges from the main graph
// 4. Run layout on sequence diagrams
// 5. Set the resulting dimensions to the main graph shape
// 6. Run core layouts (still without sequence diagram innards)
// 7. Put back sequence diagram innards in correct location
func Layout(ctx context.Context, g *d2graph.Graph, layout func(ctx context.Context, g *d2graph.Graph) error) error {
objectsToRemove := make(map[*d2graph.Object]struct{})
edgesToRemove := make(map[*d2graph.Edge]struct{})
sequenceDiagrams := make(map[string]*sequenceDiagram)
sd.init()
sd.placeActors()
sd.placeSpans()
sd.routeEdges()
sd.addLifelineEdges()
return nil
}
type sequenceDiagram struct {
graph *d2graph.Graph
edges []*d2graph.Edge
actors []*d2graph.Object
spans []*d2graph.Object
// 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
// 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
minEdgeRank map[*d2graph.Object]int
maxEdgeRank map[*d2graph.Object]int
edgeYStep float64
actorXStep float64
maxActorHeight float64
}
func (sd *sequenceDiagram) init() {
sd.edges = make([]*d2graph.Edge, len(sd.graph.Edges))
copy(sd.edges, sd.graph.Edges)
queue := make([]*d2graph.Object, len(sd.graph.Root.ChildrenArray))
copy(queue, sd.graph.Root.ChildrenArray)
queue := make([]*d2graph.Object, 1, len(g.Objects))
queue[0] = g.Root
for len(queue) > 0 {
obj := queue[0]
queue = queue[1:]
if sd.isActor(obj) {
sd.actors = append(sd.actors, obj)
sd.objectRank[obj] = len(sd.actors)
sd.maxActorHeight = math.Max(sd.maxActorHeight, obj.Height)
} else {
// 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]
if obj.Attributes.Shape.Value != d2target.ShapeSequenceDiagram {
queue = append(queue, obj.ChildrenArray...)
continue
}
queue = append(queue, obj.ChildrenArray...)
sd := layoutSequenceDiagram(g, obj)
obj.Children = make(map[string]*d2graph.Object)
obj.ChildrenArray = nil
obj.Box = geo.NewBox(nil, sd.getWidth(), sd.getHeight())
sequenceDiagrams[obj.AbsID()] = sd
for _, edge := range sd.messages {
edgesToRemove[edge] = struct{}{}
}
for _, obj := range sd.actors {
objectsToRemove[obj] = struct{}{}
}
for _, obj := range sd.spans {
objectsToRemove[obj] = struct{}{}
}
}
for rank, edge := range sd.edges {
sd.edgeYStep = math.Max(sd.edgeYStep, float64(edge.LabelDimensions.Height))
layoutEdges, edgeOrder := getLayoutEdges(g, edgesToRemove)
g.Edges = layoutEdges
layoutObjects, objectOrder := getLayoutObjects(g, objectsToRemove)
g.Objects = layoutObjects
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)
if isRootSequenceDiagram(g) {
// the sequence diagram is the only layout engine if the whole diagram is
// shape: sequence_diagram
g.Root.TopLeft = geo.NewPoint(0, 0)
} else if err := layout(ctx, g); err != nil {
return err
}
sd.maxActorHeight += VERTICAL_PAD
sd.edgeYStep += VERTICAL_PAD
cleanup(g, sequenceDiagrams, objectOrder, edgeOrder)
return nil
}
func (sd *sequenceDiagram) setMinMaxEdgeRank(actor *d2graph.Object, rank int) {
if minRank, exists := sd.minEdgeRank[actor]; exists {
sd.minEdgeRank[actor] = go2.IntMin(minRank, rank)
func isRootSequenceDiagram(g *d2graph.Graph) bool {
return g.Root.Attributes.Shape.Value == d2target.ShapeSequenceDiagram
}
// 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 {
var edges []*d2graph.Edge
for _, edge := range g.Edges {
// both Src and Dst must be inside the sequence diagram
if strings.HasPrefix(edge.Src.AbsID(), obj.AbsID()) && strings.HasPrefix(edge.Dst.AbsID(), obj.AbsID()) {
edges = append(edges, edge)
}
}
sd := newSequenceDiagram(obj.ChildrenArray, edges)
sd.layout()
return sd
}
func getLayoutEdges(g *d2graph.Graph, toRemove map[*d2graph.Edge]struct{}) ([]*d2graph.Edge, map[string]int) {
edgeOrder := make(map[string]int)
layoutEdges := make([]*d2graph.Edge, 0, len(g.Edges)-len(toRemove))
for i, edge := range g.Edges {
edgeOrder[edge.AbsID()] = i
if _, exists := toRemove[edge]; !exists {
layoutEdges = append(layoutEdges, edge)
}
}
return layoutEdges, edgeOrder
}
func getLayoutObjects(g *d2graph.Graph, toRemove map[*d2graph.Object]struct{}) ([]*d2graph.Object, map[string]int) {
objectOrder := make(map[string]int)
layoutObjects := make([]*d2graph.Object, 0, len(toRemove))
for i, obj := range g.Objects {
objectOrder[obj.AbsID()] = i
if _, exists := toRemove[obj]; !exists {
layoutObjects = append(layoutObjects, obj)
}
}
return layoutObjects, objectOrder
}
// cleanup restores the graph after the core layout engine finishes
// - translating the sequence diagram to its position placed by the core layout engine
// - restore the children of the sequence diagram graph object
// - adds the sequence diagram edges (messages) back to the graph
// - adds the sequence diagram lifelines to the graph edges
// - adds the sequence diagram descendants back to the graph objects
// - sorts edges and objects to their original graph order
func cleanup(g *d2graph.Graph, sequenceDiagrams map[string]*sequenceDiagram, objectsOrder, edgesOrder map[string]int) {
var objects []*d2graph.Object
if isRootSequenceDiagram(g) {
objects = []*d2graph.Object{g.Root}
} else {
sd.minEdgeRank[actor] = rank
objects = g.Objects
}
for _, obj := range objects {
if _, exists := sequenceDiagrams[obj.AbsID()]; !exists {
continue
}
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
sd := sequenceDiagrams[obj.AbsID()]
// shift the sequence diagrams as they are always placed at (0, 0)
sd.shift(obj.TopLeft)
obj.Children = make(map[string]*d2graph.Object)
for _, child := range sd.actors {
obj.Children[child.ID] = child
}
obj.ChildrenArray = sd.actors
g.Edges = append(g.Edges, sequenceDiagrams[obj.AbsID()].messages...)
g.Edges = append(g.Edges, sequenceDiagrams[obj.AbsID()].lifelines...)
g.Objects = append(g.Objects, sequenceDiagrams[obj.AbsID()].actors...)
g.Objects = append(g.Objects, sequenceDiagrams[obj.AbsID()].spans...)
}
sd.maxEdgeRank[actor] = go2.IntMax(sd.maxEdgeRank[actor], rank)
}
// placeActors places actors bottom aligned, side by side
func (sd *sequenceDiagram) placeActors() {
x := 0.
for _, actors := range sd.actors {
yOffset := sd.maxActorHeight - actors.Height
actors.TopLeft = geo.NewPoint(x, yOffset)
x += actors.Width + sd.actorXStep
actors.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
}
}
// addLifelineEdges adds a new edge for each actor in the graph that represents the its lifeline
// ┌──────────────┐
// │ actor │
// └──────┬───────┘
// │
// │ lifeline
// │
// │
func (sd *sequenceDiagram) addLifelineEdges() {
endY := sd.getEdgeY(len(sd.edges))
for _, actor := range sd.actors {
actorBottom := actor.Center()
actorBottom.Y = actor.TopLeft.Y + actor.Height
actorLifelineEnd := actor.Center()
actorLifelineEnd.Y = endY
sd.graph.Edges = append(sd.graph.Edges, &d2graph.Edge{
Attributes: d2graph.Attributes{
Style: d2graph.Style{
StrokeDash: &d2graph.Scalar{Value: "10"},
Stroke: actor.Attributes.Style.Stroke,
StrokeWidth: actor.Attributes.Style.StrokeWidth,
},
},
Src: actor,
SrcArrow: false,
Dst: &d2graph.Object{
ID: actor.ID + fmt.Sprintf("-lifeline-end-%d", go2.StringToIntHash(actor.ID+"-lifeline-end")),
},
DstArrow: false,
Route: []*geo.Point{actorBottom, actorLifelineEnd},
})
}
}
// placeSpans places spans over the object lifeline
// ┌──────────┐
// │ actor │
// └────┬─────┘
// ┌─┴──┐
// │ │
// |span|
// │ │
// └─┬──┘
// │
// lifeline
// │
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 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 spanFromMostNested[i].Level() > spanFromMostNested[j].Level()
// no new objects, so just keep the same position
sort.SliceStable(g.Objects, func(i, j int) bool {
return objectsOrder[g.Objects[i].AbsID()] < objectsOrder[g.Objects[j].AbsID()]
})
for _, span := range spanFromMostNested {
// finds the position based on children
minChildY := math.Inf(1)
maxChildY := math.Inf(-1)
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 span
minEdgeY := math.Inf(1)
if minRank, exists := sd.minEdgeRank[span]; exists {
minEdgeY = sd.getEdgeY(minRank)
// sequence diagrams add lifelines, and they must be the last ones in this slice
sort.SliceStable(g.Edges, func(i, j int) bool {
iOrder, iExists := edgesOrder[g.Edges[i].AbsID()]
jOrder, jExists := edgesOrder[g.Edges[j].AbsID()]
if iExists && jExists {
return iOrder < jOrder
} else if iExists && !jExists {
return true
}
maxEdgeY := math.Inf(-1)
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 -= SPAN_DEPTH_GROW_FACTOR
} else {
minY -= SPAN_EDGE_PAD
}
maxY := math.Max(maxEdgeY, maxChildY)
if maxY == maxChildY {
maxY += SPAN_DEPTH_GROW_FACTOR
} else {
maxY += SPAN_EDGE_PAD
}
height := math.Max(maxY-minY, MIN_SPAN_HEIGHT)
// -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)
span.ZIndex = 1
}
}
// routeEdges routes horizontal edges from Src to Dst
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
} 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
}
if isLeftToRight {
startX += SPAN_EDGE_PAD
endX -= SPAN_EDGE_PAD
} else {
startX -= SPAN_EDGE_PAD
endX += SPAN_EDGE_PAD
}
edgeY := sd.getEdgeY(rank)
edge.Route = []*geo.Point{
geo.NewPoint(startX, edgeY),
geo.NewPoint(endX, edgeY),
}
if edge.Attributes.Label.Value != "" {
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))
}
}
}
}
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
// either both don't exist or i doesn't exist and j exists
return false
})
}

View file

@ -5,6 +5,7 @@ import (
"testing"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/label"
"oss.terrastruct.com/d2/lib/log"
@ -25,6 +26,7 @@ func TestBasicSequenceDiagram(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)
n2 := g.Root.EnsureChild([]string{"n2"})
@ -32,32 +34,46 @@ func TestBasicSequenceDiagram(t *testing.T) {
g.Edges = []*d2graph.Edge{
{
Src: n1,
Dst: n2,
Src: n1,
Dst: n2,
Index: 0,
Attributes: d2graph.Attributes{
Label: d2graph.Scalar{Value: "left to right"},
},
},
{
Src: n2,
Dst: n1,
Src: n2,
Dst: n1,
Index: 0,
Attributes: d2graph.Attributes{
Label: d2graph.Scalar{Value: "right to left"},
},
},
{
Src: n1,
Dst: n2,
Src: n1,
Dst: n2,
Index: 1,
},
{
Src: n2,
Dst: n1,
Src: n2,
Dst: n1,
Index: 1,
},
}
nEdges := len(g.Edges)
ctx := log.WithTB(context.Background(), t, nil)
Layout(ctx, g)
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 {
obj.TopLeft = geo.NewPoint(0, 0)
}
for _, edge := range g.Edges {
edge.Route = []*geo.Point{geo.NewPoint(1, 1)}
}
return nil
})
// asserts that actors were placed in the expected x order and at y=0
actors := []*d2graph.Object{
@ -92,19 +108,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_EDGE_PAD {
if edge.Route[0].X != edge.Src.Center().X+SPAN_MESSAGE_PAD {
t.Fatalf("expected edge[%d] x to be at the actor center", i)
}
if edge.Route[1].X != edge.Dst.Center().X-SPAN_EDGE_PAD {
if edge.Route[1].X != edge.Dst.Center().X-SPAN_MESSAGE_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_EDGE_PAD {
if edge.Route[0].X != edge.Src.Center().X-SPAN_MESSAGE_PAD {
t.Fatalf("expected edge[%d] x to be at the actor center", i)
}
if edge.Route[1].X != edge.Dst.Center().X+SPAN_EDGE_PAD {
if edge.Route[1].X != edge.Dst.Center().X+SPAN_MESSAGE_PAD {
t.Fatalf("expected edge[%d] x to be at the actor center", i)
}
}
@ -157,6 +173,7 @@ func TestSpansSequenceDiagram(t *testing.T) {
// t2 ││ │
// ├┘◄─────────────────────┤
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{
@ -174,22 +191,36 @@ func TestSpansSequenceDiagram(t *testing.T) {
g.Edges = []*d2graph.Edge{
{
Src: a_t1,
Dst: b_t1,
Src: a_t1,
Dst: b_t1,
Index: 0,
}, {
Src: b_t1,
Dst: a_t1,
Src: b_t1,
Dst: a_t1,
Index: 0,
}, {
Src: a_t2,
Dst: b,
Src: a_t2,
Dst: b,
Index: 0,
}, {
Src: b,
Dst: a_t2,
Src: b,
Dst: a_t2,
Index: 0,
},
}
ctx := log.WithTB(context.Background(), t, nil)
Layout(ctx, g)
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 {
obj.TopLeft = geo.NewPoint(0, 0)
}
for _, edge := range g.Edges {
edge.Route = []*geo.Point{geo.NewPoint(1, 1)}
}
return nil
})
// check properties
if a.Attributes.Shape.Value != shape.PERSON_TYPE {
@ -215,7 +246,7 @@ func TestSpansSequenceDiagram(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_EDGE_PAD)
expectedHeight := g.Edges[1].Route[0].Y - g.Edges[0].Route[0].Y + (2 * SPAN_MESSAGE_PAD)
if a_t1.Height != expectedHeight {
t.Fatalf("expected a.t1 height to be %.5f, got %.5f", expectedHeight, a_t1.Height)
}
@ -237,20 +268,118 @@ func TestSpansSequenceDiagram(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_EDGE_PAD {
t.Fatal("expected a.t1 to be placed at the same Y of the first edge")
if a_t1.TopLeft.Y != g.Edges[0].Route[0].Y-SPAN_MESSAGE_PAD {
t.Fatal("expected a.t1 to be placed at the same Y of the first message")
}
// check routes
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[0].X != a_t1.TopLeft.X+a_t1.Width+SPAN_MESSAGE_PAD {
t.Fatal("expected the first message to start on a.t1 top right X")
}
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[0].Route[1].X != b_t1.TopLeft.X-SPAN_MESSAGE_PAD {
t.Fatal("expected the first message to end on b.t1 top left X")
}
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")
if g.Edges[2].Route[1].X != b.Center().X-SPAN_MESSAGE_PAD {
t.Fatal("expected the third message to end on b.t1 center X")
}
}
func TestNestedSequenceDiagrams(t *testing.T) {
// ┌────────────────────────────────────────┐
// | ┌─────┐ container ┌─────┐ |
// | │ a │ │ b │ | ┌─────┐
// | └──┬──┘ └──┬──┘ ├────edge1───┤ c │
// | ├┐───────sdEdge1──────►┌┤ | └─────┘
// | t1 ││ ││ t1 |
// | ├┘◄──────sdEdge2───────└┤ |
// └────────────────────────────────────────┘
g := d2graph.NewGraph(nil)
container := g.Root.EnsureChild([]string{"container"})
container.Attributes.Shape = d2graph.Scalar{Value: d2target.ShapeSequenceDiagram}
container.Box = geo.NewBox(nil, 500, 500)
a := container.EnsureChild([]string{"a"})
a.Box = geo.NewBox(nil, 100, 100)
a.Attributes.Shape = d2graph.Scalar{Value: shape.PERSON_TYPE}
a_t1 := a.EnsureChild([]string{"t1"})
b := container.EnsureChild([]string{"b"})
b.Box = geo.NewBox(nil, 30, 30)
b_t1 := b.EnsureChild([]string{"t1"})
c := g.Root.EnsureChild([]string{"c"})
c.Box = geo.NewBox(nil, 100, 100)
c.Attributes.Shape = d2graph.Scalar{Value: d2target.ShapeSquare}
sdEdge1, err := g.Root.Connect(a_t1.AbsIDArray(), b_t1.AbsIDArray(), false, true, "sequence diagram edge 1")
if err != nil {
t.Fatal(err)
}
sdEdge2, err := g.Root.Connect(b_t1.AbsIDArray(), a_t1.AbsIDArray(), false, true, "sequence diagram edge 2")
if err != nil {
t.Fatal(err)
}
edge1, err := g.Root.Connect(container.AbsIDArray(), c.AbsIDArray(), false, false, "edge 1")
if err != nil {
t.Fatal(err)
}
layoutFn := func(ctx context.Context, g *d2graph.Graph) error {
if len(g.Objects) != 2 {
t.Fatal("expected only diagram objects for layout")
}
for _, obj := range g.Objects {
if obj == a || obj == a_t1 || obj == b || obj == b_t1 {
t.Fatal("expected to have removed all sequence diagram objects")
}
}
if len(container.ChildrenArray) != 0 {
t.Fatalf("expected no `container` children, got %d", len(container.ChildrenArray))
}
if len(container.Children) != len(container.ChildrenArray) {
t.Fatal("container children mismatch")
}
for _, edge := range g.Edges {
if edge == sdEdge1 || edge == sdEdge2 {
t.Fatal("expected to have removed all sequence diagram edges from graph")
}
}
if g.Edges[0] != edge1 {
t.Fatal("expected graph edge to be in the graph")
}
// just set some position as if it had been properly placed
for _, obj := range g.Objects {
obj.TopLeft = geo.NewPoint(0, 0)
}
for _, edge := range g.Edges {
edge.Route = []*geo.Point{geo.NewPoint(1, 1)}
}
return nil
}
ctx := log.WithTB(context.Background(), t, nil)
if err = Layout(ctx, g, layoutFn); err != nil {
t.Fatal(err)
}
if len(g.Edges) != 5 {
t.Fatal("expected graph to have all edges and lifelines after layout")
}
for _, obj := range g.Objects {
if obj.TopLeft == nil {
t.Fatal("expected to have placed all objects")
}
}
for _, edge := range g.Edges {
if len(edge.Route) == 0 {
t.Fatal("expected to have routed all edges")
}
}
}

View file

@ -0,0 +1,309 @@
package d2sequence
import (
"fmt"
"math"
"sort"
"oss.terrastruct.com/util-go/go2"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/label"
"oss.terrastruct.com/d2/lib/shape"
)
type sequenceDiagram struct {
root *d2graph.Object
messages []*d2graph.Edge
lifelines []*d2graph.Edge
actors []*d2graph.Object
spans []*d2graph.Object
// 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
// 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
messageYStep float64
actorXStep float64
maxActorHeight float64
}
func newSequenceDiagram(actors []*d2graph.Object, messages []*d2graph.Edge) *sequenceDiagram {
sd := &sequenceDiagram{
messages: messages,
actors: actors,
spans: nil,
lifelines: nil,
objectRank: make(map[*d2graph.Object]int),
minMessageRank: make(map[*d2graph.Object]int),
maxMessageRank: make(map[*d2graph.Object]int),
messageYStep: MIN_MESSAGE_DISTANCE,
actorXStep: MIN_ACTOR_DISTANCE,
maxActorHeight: 0.,
}
for rank, actor := range actors {
sd.root = actor.Parent
sd.objectRank[actor] = rank
sd.maxActorHeight = math.Max(sd.maxActorHeight, actor.Height)
queue := make([]*d2graph.Object, len(actor.ChildrenArray))
copy(queue, actor.ChildrenArray)
for len(queue) > 0 {
span := queue[0]
queue = queue[1:]
// spans are always rectangles and have no labels
span.Attributes.Label = d2graph.Scalar{Value: ""}
span.Attributes.Shape = d2graph.Scalar{Value: shape.SQUARE_TYPE}
sd.spans = append(sd.spans, span)
sd.objectRank[span] = rank
queue = append(queue, span.ChildrenArray...)
}
}
for rank, message := range sd.messages {
sd.messageYStep = math.Max(sd.messageYStep, 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)
}
sd.maxActorHeight += VERTICAL_PAD
sd.messageYStep += VERTICAL_PAD
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() {
sd.placeActors()
sd.placeSpans()
sd.routeMessages()
sd.addLifelineEdges()
}
// placeActors places actors bottom aligned, side by side
func (sd *sequenceDiagram) placeActors() {
x := 0.
for _, actors := range sd.actors {
yOffset := sd.maxActorHeight - actors.Height
actors.TopLeft = geo.NewPoint(x, yOffset)
x += actors.Width + sd.actorXStep
actors.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
}
}
// addLifelineEdges adds a new edge for each actor in the graph that represents the its lifeline
// ┌──────────────┐
// │ actor │
// └──────┬───────┘
// │
// │ lifeline
// │
// │
func (sd *sequenceDiagram) addLifelineEdges() {
endY := sd.getMessageY(len(sd.messages))
for _, actor := range sd.actors {
actorBottom := actor.Center()
actorBottom.Y = actor.TopLeft.Y + actor.Height
actorLifelineEnd := actor.Center()
actorLifelineEnd.Y = endY
sd.lifelines = append(sd.lifelines, &d2graph.Edge{
Attributes: d2graph.Attributes{
Style: d2graph.Style{
StrokeDash: &d2graph.Scalar{Value: "10"},
Stroke: actor.Attributes.Style.Stroke,
StrokeWidth: actor.Attributes.Style.StrokeWidth,
},
},
Src: actor,
SrcArrow: false,
Dst: &d2graph.Object{
ID: actor.ID + fmt.Sprintf("-lifeline-end-%d", go2.StringToIntHash(actor.ID+"-lifeline-end")),
},
DstArrow: false,
Route: []*geo.Point{actorBottom, actorLifelineEnd},
})
}
}
// placeSpans places spans over the object lifeline
// ┌──────────┐
// │ actor │
// └────┬─────┘
// ┌─┴──┐
// │ │
// |span|
// │ │
// └─┬──┘
// │
// lifeline
// │
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 spans from most to least nested
// the order is important because the only way a child span exists is if there'e an message to it
// however, the parent span might not have a message to it and then its position is based on the child position
// or, there can be a message 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 message
spanFromMostNested := make([]*d2graph.Object, len(sd.spans))
copy(spanFromMostNested, sd.spans)
sort.SliceStable(spanFromMostNested, func(i, j int) bool {
return spanFromMostNested[i].Level() > spanFromMostNested[j].Level()
})
for _, span := range spanFromMostNested {
// finds the position based on children
minChildY := math.Inf(1)
maxChildY := math.Inf(-1)
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 messages to this span
minMessageY := math.Inf(1)
if minRank, exists := sd.minMessageRank[span]; exists {
minMessageY = sd.getMessageY(minRank)
}
maxMessageY := math.Inf(-1)
if maxRank, exists := sd.maxMessageRank[span]; exists {
maxMessageY = sd.getMessageY(maxRank)
}
// if it is the same as the child top left, add some padding
minY := math.Min(minMessageY, minChildY)
if minY == minChildY {
minY -= SPAN_DEPTH_GROW_FACTOR
} else {
minY -= SPAN_MESSAGE_PAD
}
maxY := math.Max(maxMessageY, maxChildY)
if maxY == maxChildY {
maxY += SPAN_DEPTH_GROW_FACTOR
} else {
maxY += SPAN_MESSAGE_PAD
}
height := math.Max(maxY-minY, MIN_SPAN_HEIGHT)
// -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)
span.ZIndex = 1
}
}
// 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
}
if isLeftToRight {
startX += SPAN_MESSAGE_PAD
endX -= SPAN_MESSAGE_PAD
} else {
startX -= SPAN_MESSAGE_PAD
endX += SPAN_MESSAGE_PAD
}
messageY := sd.getMessageY(rank)
message.Route = []*geo.Point{
geo.NewPoint(startX, messageY),
geo.NewPoint(endX, messageY),
}
if message.Attributes.Label.Value != "" {
if isLeftToRight {
message.LabelPosition = go2.Pointer(string(label.OutsideTopCenter))
} else {
// the label will be placed above the message because the orientation is based on the edge normal vector
message.LabelPosition = go2.Pointer(string(label.OutsideBottomCenter))
}
}
}
}
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.messageYStep) + sd.maxActorHeight
}
func (sd *sequenceDiagram) isActor(obj *d2graph.Object) bool {
return obj.Parent == sd.root
}
func (sd *sequenceDiagram) getWidth() float64 {
// the layout is always placed starting at 0, so the width is just the last actor
lastActor := sd.actors[len(sd.actors)-1]
return lastActor.TopLeft.X + lastActor.Width
}
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))
}
func (sd *sequenceDiagram) shift(tl *geo.Point) {
allObjects := append([]*d2graph.Object{}, sd.actors...)
allObjects = append(allObjects, sd.spans...)
for _, obj := range allObjects {
obj.TopLeft.X += tl.X
obj.TopLeft.Y += tl.Y
}
allEdges := append([]*d2graph.Edge{}, sd.messages...)
allEdges = append(allEdges, sd.lifelines...)
for _, edge := range allEdges {
for _, p := range edge.Route {
p.X += tl.X
p.Y += tl.Y
}
}
}

View file

@ -9,6 +9,7 @@ import (
"oss.terrastruct.com/d2/d2compiler"
"oss.terrastruct.com/d2/d2exporter"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2layouts/d2sequence"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/lib/textmeasure"
)
@ -39,14 +40,9 @@ func Compile(ctx context.Context, input string, opts *CompileOptions) (*d2target
return nil, nil, err
}
if opts.Layout != nil {
err = opts.Layout(ctx, g)
} else if os.Getenv("D2_LAYOUT") == "dagre" && dagreLayout != nil {
err = dagreLayout(ctx, g)
} else {
err = errors.New("no available layout")
}
if err != nil {
if layout, err := getLayout(opts); err != nil {
return nil, nil, err
} else if err := d2sequence.Layout(ctx, g, layout); err != nil {
return nil, nil, err
}
@ -54,5 +50,15 @@ func Compile(ctx context.Context, input string, opts *CompileOptions) (*d2target
return diagram, g, err
}
func getLayout(opts *CompileOptions) (func(context.Context, *d2graph.Graph) error, error) {
if opts.Layout != nil {
return opts.Layout, nil
} else if os.Getenv("D2_LAYOUT") == "dagre" && dagreLayout != nil {
return dagreLayout, nil
} else {
return nil, errors.New("no available layout")
}
}
// See c.go
var dagreLayout func(context.Context, *d2graph.Graph) error

View file

@ -1043,6 +1043,172 @@ size XXXL -> custom 64: custom 48 {
style.font-size: 48
}
`,
}, {
name: "sequence_diagram_simple",
script: `shape: sequence_diagram
alice: "Alice\nline\nbreaker" {
shape: person
style.stroke: red
}
bob: "Bob" {
shape: person
style.stroke-width: 5
}
db: {
shape: cylinder
}
queue: {
shape: queue
}
service: "an\nodd\nservice\nwith\na\nname\nin\nmultiple lines"
alice -> bob: "Authentication Request"
bob -> service: "make request for something that is quite far away and requires a really long label to take all the space between the objects"
service -> db: "validate credentials"
db -> service: {
style.stroke-dash: 4
}
service -> bob: {
style.stroke-dash: 4
}
bob -> alice: "Authentication Response"
alice -> bob: "Another authentication Request"
bob -> queue: "do it later"
queue -> bob: "stored" {
style.stroke-dash: 3
style.stroke-width: 5
style.stroke: green
}
bob -> alice: "Another authentication Response"`,
}, {
name: "sequence_diagram_span",
script: `shape: sequence_diagram
scorer.t -> itemResponse.t: getItem()
scorer.t <- itemResponse.t: item
scorer.t -> item.t1: getRubric()
scorer.t <- item.t1: rubric
scorer.t -> essayRubric.t: applyTo(essayResp)
itemResponse -> essayRubric.t.c
essayRubric.t.c -> concept.t: match(essayResponse)
scorer <- essayRubric.t: score
scorer.t -> itemOutcome.t1: new
scorer.t -> item.t2: getNormalMinimum()
scorer.t -> item.t3: getNormalMaximum()
scorer.t -> itemOutcome.t2: setScore(score)
scorer.t -> itemOutcome.t3: setFeedback(missingConcepts)`,
}, {
name: "sequence_diagram_nested_span",
script: `shape: sequence_diagram
scorer: {
stroke: red
stroke-width: 5
}
scorer.abc: {
fill: yellow
stroke-width: 7
}
scorer -> itemResponse.a: {
stroke-width: 10
}
itemResponse.a -> item.a.b
item.a.b -> essayRubric.a.b.c
essayRubric.a.b.c -> concept.a.b.c.d
item.a -> essayRubric.a.b
concept.a.b.c.d -> itemOutcome.a.b.c.d.e
scorer.abc -> item.a
itemOutcome.a.b.c.d.e -> scorer
scorer -> itemResponse.c`,
}, {
name: "sequence_diagrams",
script: `a_shape.shape: circle
a_sequence: {
shape: sequence_diagram
scorer.t -> itemResponse.t: getItem()
scorer.t <- itemResponse.t: item
scorer.t -> item.t1: getRubric()
scorer.t <- item.t1: rubric
scorer.t -> essayRubric.t: applyTo(essayResp)
itemResponse -> essayRubric.t.c
essayRubric.t.c -> concept.t: match(essayResponse)
scorer <- essayRubric.t: score
scorer.t <-> itemOutcome.t1: new
scorer.t <-> item.t2: getNormalMinimum()
scorer.t -> item.t3: getNormalMaximum()
scorer.t -- itemOutcome.t2: setScore(score)
scorer.t -- itemOutcome.t3: setFeedback(missingConcepts)
}
another: {
sequence: {
shape: sequence_diagram
# scoped edges
scorer.t -> itemResponse.t: getItem()
scorer.t <- itemResponse.t: item
scorer.t -> item.t1: getRubric()
scorer.t <- item.t1: rubric
scorer.t -> essayRubric.t: applyTo(essayResp)
itemResponse -> essayRubric.t.c
essayRubric.t.c -> concept.t: match(essayResponse)
scorer <- essayRubric.t: score
scorer.t -> itemOutcome.t1: new
scorer.t <-> item.t2: getNormalMinimum()
scorer.t -> item.t3: getNormalMaximum()
scorer.t -> itemOutcome.t2: setScore(score)
scorer.t -> itemOutcome.t3: setFeedback(missingConcepts)
}
}
a_shape -> a_sequence
a_shape -> another.sequence
a_sequence -> sequence
another.sequence <-> finally.sequence
a_shape -- finally
finally: {
shape: queue
sequence: {
shape: sequence_diagram
# items appear in this order
scorer
concept
essayRubric
item
itemOutcome
itemResponse
}
}
# full path edges
finally.sequence.itemResponse.a -> finally.sequence.item.a.b
finally.sequence.item.a.b -> finally.sequence.essayRubric.a.b.c
finally.sequence.essayRubric.a.b.c -> finally.sequence.concept.a.b.c.d
finally.sequence.item.a -> finally.sequence.essayRubric.a.b
finally.sequence.concept.a.b.c.d -> finally.sequence.itemOutcome.a.b.c.d.e
finally.sequence.scorer.abc -> finally.sequence.item.a
finally.sequence.itemOutcome.a.b.c.d.e -> finally.sequence.scorer
finally.sequence.scorer -> finally.sequence.itemResponse.c`,
},
}

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 332 KiB

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 332 KiB

View file

@ -0,0 +1,787 @@
{
"name": "",
"shapes": [
{
"id": "alice",
"type": "person",
"pos": {
"x": 0,
"y": 130
},
"width": 163,
"height": 158,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "#E3E9FD",
"stroke": "red",
"shadow": false,
"3d": false,
"multiple": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"fields": null,
"methods": null,
"columns": null,
"label": "Alice\nline\nbreaker",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#0A0F25",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 63,
"labelHeight": 58,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
},
{
"id": "bob",
"type": "person",
"pos": {
"x": 480,
"y": 162
},
"width": 132,
"height": 126,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 5,
"borderRadius": 0,
"fill": "#E3E9FD",
"stroke": "#0D32B2",
"shadow": false,
"3d": false,
"multiple": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"fields": null,
"methods": null,
"columns": null,
"label": "Bob",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#0A0F25",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 32,
"labelHeight": 26,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
},
{
"id": "db",
"type": "cylinder",
"pos": {
"x": 929,
"y": 162
},
"width": 124,
"height": 126,
"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": "db",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#0A0F25",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 24,
"labelHeight": 26,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
},
{
"id": "queue",
"type": "queue",
"pos": {
"x": 1370,
"y": 162
},
"width": 149,
"height": 126,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "#F0F3F9",
"stroke": "#0D32B2",
"shadow": false,
"3d": false,
"multiple": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"fields": null,
"methods": null,
"columns": null,
"label": "queue",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#0A0F25",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 49,
"labelHeight": 26,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
},
{
"id": "service",
"type": "",
"pos": {
"x": 1836,
"y": 50
},
"width": 202,
"height": 238,
"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": "an\nodd\nservice\nwith\na\nname\nin\nmultiple lines",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#0A0F25",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 102,
"labelHeight": 138,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
}
],
"connections": [
{
"id": "(alice -> bob)[0]",
"src": "alice",
"srcArrow": "none",
"srcLabel": "",
"dst": "bob",
"dstArrow": "triangle",
"dstLabel": "",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "#0D32B2",
"label": "Authentication Request",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#676C7E",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 155,
"labelHeight": 21,
"labelPosition": "OUTSIDE_TOP_CENTER",
"labelPercentage": 0,
"route": [
{
"x": 86.5,
"y": 438
},
{
"x": 541,
"y": 438
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 2
},
{
"id": "(bob -> service)[0]",
"src": "bob",
"srcArrow": "none",
"srcLabel": "",
"dst": "service",
"dstArrow": "triangle",
"dstLabel": "",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "#0D32B2",
"label": "make request for something that is quite far away and requires a really long label to take all the space between the objects",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#676C7E",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 801,
"labelHeight": 21,
"labelPosition": "OUTSIDE_TOP_CENTER",
"labelPercentage": 0,
"route": [
{
"x": 551,
"y": 588
},
{
"x": 1932,
"y": 588
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 2
},
{
"id": "(service -> db)[0]",
"src": "service",
"srcArrow": "none",
"srcLabel": "",
"dst": "db",
"dstArrow": "triangle",
"dstLabel": "",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "#0D32B2",
"label": "validate credentials",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#676C7E",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 131,
"labelHeight": 21,
"labelPosition": "OUTSIDE_BOTTOM_CENTER",
"labelPercentage": 0,
"route": [
{
"x": 1932,
"y": 738
},
{
"x": 996,
"y": 738
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 2
},
{
"id": "(db -> service)[0]",
"src": "db",
"srcArrow": "none",
"srcLabel": "",
"dst": "service",
"dstArrow": "triangle",
"dstLabel": "",
"opacity": 1,
"strokeDash": 4,
"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": 996,
"y": 888
},
{
"x": 1932,
"y": 888
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 2
},
{
"id": "(service -> bob)[0]",
"src": "service",
"srcArrow": "none",
"srcLabel": "",
"dst": "bob",
"dstArrow": "triangle",
"dstLabel": "",
"opacity": 1,
"strokeDash": 4,
"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": 1932,
"y": 1038
},
{
"x": 551,
"y": 1038
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 2
},
{
"id": "(bob -> alice)[0]",
"src": "bob",
"srcArrow": "none",
"srcLabel": "",
"dst": "alice",
"dstArrow": "triangle",
"dstLabel": "",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "#0D32B2",
"label": "Authentication Response",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#676C7E",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 164,
"labelHeight": 21,
"labelPosition": "OUTSIDE_BOTTOM_CENTER",
"labelPercentage": 0,
"route": [
{
"x": 541,
"y": 1188
},
{
"x": 86.5,
"y": 1188
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 2
},
{
"id": "(alice -> bob)[1]",
"src": "alice",
"srcArrow": "none",
"srcLabel": "",
"dst": "bob",
"dstArrow": "triangle",
"dstLabel": "",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "#0D32B2",
"label": "Another authentication Request",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#676C7E",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 210,
"labelHeight": 21,
"labelPosition": "OUTSIDE_TOP_CENTER",
"labelPercentage": 0,
"route": [
{
"x": 86.5,
"y": 1338
},
{
"x": 541,
"y": 1338
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 2
},
{
"id": "(bob -> queue)[0]",
"src": "bob",
"srcArrow": "none",
"srcLabel": "",
"dst": "queue",
"dstArrow": "triangle",
"dstLabel": "",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "#0D32B2",
"label": "do it later",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#676C7E",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 65,
"labelHeight": 21,
"labelPosition": "OUTSIDE_TOP_CENTER",
"labelPercentage": 0,
"route": [
{
"x": 551,
"y": 1488
},
{
"x": 1439.5,
"y": 1488
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 2
},
{
"id": "(queue -> bob)[0]",
"src": "queue",
"srcArrow": "none",
"srcLabel": "",
"dst": "bob",
"dstArrow": "triangle",
"dstLabel": "",
"opacity": 1,
"strokeDash": 3,
"strokeWidth": 5,
"stroke": "green",
"label": "stored",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#676C7E",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 44,
"labelHeight": 21,
"labelPosition": "OUTSIDE_BOTTOM_CENTER",
"labelPercentage": 0,
"route": [
{
"x": 1439.5,
"y": 1638
},
{
"x": 551,
"y": 1638
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 2
},
{
"id": "(bob -> alice)[1]",
"src": "bob",
"srcArrow": "none",
"srcLabel": "",
"dst": "alice",
"dstArrow": "triangle",
"dstLabel": "",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "#0D32B2",
"label": "Another authentication Response",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#676C7E",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 219,
"labelHeight": 21,
"labelPosition": "OUTSIDE_BOTTOM_CENTER",
"labelPercentage": 0,
"route": [
{
"x": 541,
"y": 1788
},
{
"x": 86.5,
"y": 1788
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 2
},
{
"id": "(alice -- )[0]",
"src": "alice",
"srcArrow": "none",
"srcLabel": "",
"dst": "alice-lifeline-end-3851299086",
"dstArrow": "none",
"dstLabel": "",
"opacity": 1,
"strokeDash": 10,
"strokeWidth": 2,
"stroke": "red",
"label": "",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#676C7E",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 0,
"labelHeight": 0,
"labelPosition": "",
"labelPercentage": 0,
"route": [
{
"x": 81.5,
"y": 288
},
{
"x": 81.5,
"y": 1938
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 0
},
{
"id": "(bob -- )[0]",
"src": "bob",
"srcArrow": "none",
"srcLabel": "",
"dst": "bob-lifeline-end-3036726343",
"dstArrow": "none",
"dstLabel": "",
"opacity": 1,
"strokeDash": 10,
"strokeWidth": 5,
"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": 546,
"y": 288
},
{
"x": 546,
"y": 1938
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 0
},
{
"id": "(db -- )[0]",
"src": "db",
"srcArrow": "none",
"srcLabel": "",
"dst": "db-lifeline-end-2675250554",
"dstArrow": "none",
"dstLabel": "",
"opacity": 1,
"strokeDash": 10,
"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": 991,
"y": 288
},
{
"x": 991,
"y": 1938
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 0
},
{
"id": "(queue -- )[0]",
"src": "queue",
"srcArrow": "none",
"srcLabel": "",
"dst": "queue-lifeline-end-1097346683",
"dstArrow": "none",
"dstLabel": "",
"opacity": 1,
"strokeDash": 10,
"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": 1444.5,
"y": 288
},
{
"x": 1444.5,
"y": 1938
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 0
},
{
"id": "(service -- )[0]",
"src": "service",
"srcArrow": "none",
"srcLabel": "",
"dst": "service-lifeline-end-22863415",
"dstArrow": "none",
"dstLabel": "",
"opacity": 1,
"strokeDash": 10,
"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": 1937,
"y": 288
},
{
"x": 1937,
"y": 1938
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 0
}
]
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 474 KiB

View file

@ -0,0 +1,787 @@
{
"name": "",
"shapes": [
{
"id": "alice",
"type": "person",
"pos": {
"x": 0,
"y": 130
},
"width": 163,
"height": 158,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "#E3E9FD",
"stroke": "red",
"shadow": false,
"3d": false,
"multiple": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"fields": null,
"methods": null,
"columns": null,
"label": "Alice\nline\nbreaker",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#0A0F25",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 63,
"labelHeight": 58,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
},
{
"id": "bob",
"type": "person",
"pos": {
"x": 480,
"y": 162
},
"width": 132,
"height": 126,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 5,
"borderRadius": 0,
"fill": "#E3E9FD",
"stroke": "#0D32B2",
"shadow": false,
"3d": false,
"multiple": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"fields": null,
"methods": null,
"columns": null,
"label": "Bob",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#0A0F25",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 32,
"labelHeight": 26,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
},
{
"id": "db",
"type": "cylinder",
"pos": {
"x": 929,
"y": 162
},
"width": 124,
"height": 126,
"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": "db",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#0A0F25",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 24,
"labelHeight": 26,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
},
{
"id": "queue",
"type": "queue",
"pos": {
"x": 1370,
"y": 162
},
"width": 149,
"height": 126,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"borderRadius": 0,
"fill": "#F0F3F9",
"stroke": "#0D32B2",
"shadow": false,
"3d": false,
"multiple": false,
"tooltip": "",
"link": "",
"icon": null,
"iconPosition": "",
"fields": null,
"methods": null,
"columns": null,
"label": "queue",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#0A0F25",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 49,
"labelHeight": 26,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
},
{
"id": "service",
"type": "",
"pos": {
"x": 1836,
"y": 50
},
"width": 202,
"height": 238,
"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": "an\nodd\nservice\nwith\na\nname\nin\nmultiple lines",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#0A0F25",
"italic": false,
"bold": true,
"underline": false,
"labelWidth": 102,
"labelHeight": 138,
"labelPosition": "INSIDE_MIDDLE_CENTER",
"zIndex": 0,
"level": 1
}
],
"connections": [
{
"id": "(alice -> bob)[0]",
"src": "alice",
"srcArrow": "none",
"srcLabel": "",
"dst": "bob",
"dstArrow": "triangle",
"dstLabel": "",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "#0D32B2",
"label": "Authentication Request",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#676C7E",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 155,
"labelHeight": 21,
"labelPosition": "OUTSIDE_TOP_CENTER",
"labelPercentage": 0,
"route": [
{
"x": 86.5,
"y": 438
},
{
"x": 541,
"y": 438
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 2
},
{
"id": "(bob -> service)[0]",
"src": "bob",
"srcArrow": "none",
"srcLabel": "",
"dst": "service",
"dstArrow": "triangle",
"dstLabel": "",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "#0D32B2",
"label": "make request for something that is quite far away and requires a really long label to take all the space between the objects",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#676C7E",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 801,
"labelHeight": 21,
"labelPosition": "OUTSIDE_TOP_CENTER",
"labelPercentage": 0,
"route": [
{
"x": 551,
"y": 588
},
{
"x": 1932,
"y": 588
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 2
},
{
"id": "(service -> db)[0]",
"src": "service",
"srcArrow": "none",
"srcLabel": "",
"dst": "db",
"dstArrow": "triangle",
"dstLabel": "",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "#0D32B2",
"label": "validate credentials",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#676C7E",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 131,
"labelHeight": 21,
"labelPosition": "OUTSIDE_BOTTOM_CENTER",
"labelPercentage": 0,
"route": [
{
"x": 1932,
"y": 738
},
{
"x": 996,
"y": 738
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 2
},
{
"id": "(db -> service)[0]",
"src": "db",
"srcArrow": "none",
"srcLabel": "",
"dst": "service",
"dstArrow": "triangle",
"dstLabel": "",
"opacity": 1,
"strokeDash": 4,
"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": 996,
"y": 888
},
{
"x": 1932,
"y": 888
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 2
},
{
"id": "(service -> bob)[0]",
"src": "service",
"srcArrow": "none",
"srcLabel": "",
"dst": "bob",
"dstArrow": "triangle",
"dstLabel": "",
"opacity": 1,
"strokeDash": 4,
"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": 1932,
"y": 1038
},
{
"x": 551,
"y": 1038
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 2
},
{
"id": "(bob -> alice)[0]",
"src": "bob",
"srcArrow": "none",
"srcLabel": "",
"dst": "alice",
"dstArrow": "triangle",
"dstLabel": "",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "#0D32B2",
"label": "Authentication Response",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#676C7E",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 164,
"labelHeight": 21,
"labelPosition": "OUTSIDE_BOTTOM_CENTER",
"labelPercentage": 0,
"route": [
{
"x": 541,
"y": 1188
},
{
"x": 86.5,
"y": 1188
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 2
},
{
"id": "(alice -> bob)[1]",
"src": "alice",
"srcArrow": "none",
"srcLabel": "",
"dst": "bob",
"dstArrow": "triangle",
"dstLabel": "",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "#0D32B2",
"label": "Another authentication Request",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#676C7E",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 210,
"labelHeight": 21,
"labelPosition": "OUTSIDE_TOP_CENTER",
"labelPercentage": 0,
"route": [
{
"x": 86.5,
"y": 1338
},
{
"x": 541,
"y": 1338
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 2
},
{
"id": "(bob -> queue)[0]",
"src": "bob",
"srcArrow": "none",
"srcLabel": "",
"dst": "queue",
"dstArrow": "triangle",
"dstLabel": "",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "#0D32B2",
"label": "do it later",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#676C7E",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 65,
"labelHeight": 21,
"labelPosition": "OUTSIDE_TOP_CENTER",
"labelPercentage": 0,
"route": [
{
"x": 551,
"y": 1488
},
{
"x": 1439.5,
"y": 1488
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 2
},
{
"id": "(queue -> bob)[0]",
"src": "queue",
"srcArrow": "none",
"srcLabel": "",
"dst": "bob",
"dstArrow": "triangle",
"dstLabel": "",
"opacity": 1,
"strokeDash": 3,
"strokeWidth": 5,
"stroke": "green",
"label": "stored",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#676C7E",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 44,
"labelHeight": 21,
"labelPosition": "OUTSIDE_BOTTOM_CENTER",
"labelPercentage": 0,
"route": [
{
"x": 1439.5,
"y": 1638
},
{
"x": 551,
"y": 1638
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 2
},
{
"id": "(bob -> alice)[1]",
"src": "bob",
"srcArrow": "none",
"srcLabel": "",
"dst": "alice",
"dstArrow": "triangle",
"dstLabel": "",
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
"stroke": "#0D32B2",
"label": "Another authentication Response",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#676C7E",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 219,
"labelHeight": 21,
"labelPosition": "OUTSIDE_BOTTOM_CENTER",
"labelPercentage": 0,
"route": [
{
"x": 541,
"y": 1788
},
{
"x": 86.5,
"y": 1788
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 2
},
{
"id": "(alice -- )[0]",
"src": "alice",
"srcArrow": "none",
"srcLabel": "",
"dst": "alice-lifeline-end-3851299086",
"dstArrow": "none",
"dstLabel": "",
"opacity": 1,
"strokeDash": 10,
"strokeWidth": 2,
"stroke": "red",
"label": "",
"fontSize": 16,
"fontFamily": "DEFAULT",
"language": "",
"color": "#676C7E",
"italic": true,
"bold": false,
"underline": false,
"labelWidth": 0,
"labelHeight": 0,
"labelPosition": "",
"labelPercentage": 0,
"route": [
{
"x": 81.5,
"y": 288
},
{
"x": 81.5,
"y": 1938
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 0
},
{
"id": "(bob -- )[0]",
"src": "bob",
"srcArrow": "none",
"srcLabel": "",
"dst": "bob-lifeline-end-3036726343",
"dstArrow": "none",
"dstLabel": "",
"opacity": 1,
"strokeDash": 10,
"strokeWidth": 5,
"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": 546,
"y": 288
},
{
"x": 546,
"y": 1938
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 0
},
{
"id": "(db -- )[0]",
"src": "db",
"srcArrow": "none",
"srcLabel": "",
"dst": "db-lifeline-end-2675250554",
"dstArrow": "none",
"dstLabel": "",
"opacity": 1,
"strokeDash": 10,
"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": 991,
"y": 288
},
{
"x": 991,
"y": 1938
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 0
},
{
"id": "(queue -- )[0]",
"src": "queue",
"srcArrow": "none",
"srcLabel": "",
"dst": "queue-lifeline-end-1097346683",
"dstArrow": "none",
"dstLabel": "",
"opacity": 1,
"strokeDash": 10,
"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": 1444.5,
"y": 288
},
{
"x": 1444.5,
"y": 1938
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 0
},
{
"id": "(service -- )[0]",
"src": "service",
"srcArrow": "none",
"srcLabel": "",
"dst": "service-lifeline-end-22863415",
"dstArrow": "none",
"dstLabel": "",
"opacity": 1,
"strokeDash": 10,
"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": 1937,
"y": 288
},
{
"x": 1937,
"y": 1938
}
],
"animated": false,
"tooltip": "",
"icon": null,
"zIndex": 0
}
]
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 474 KiB

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 476 KiB

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 476 KiB

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 498 KiB

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 498 KiB

View file

@ -5,7 +5,6 @@ import (
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
@ -17,7 +16,6 @@ import (
"oss.terrastruct.com/util-go/xmain"
"oss.terrastruct.com/d2/d2layouts/d2sequence"
"oss.terrastruct.com/d2/d2lib"
"oss.terrastruct.com/d2/d2plugin"
"oss.terrastruct.com/d2/d2renderers/d2svg"
@ -202,10 +200,6 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, theme
}
layout := plugin.Layout
// TODO: remove, this is just a feature flag to test sequence diagrams as we work on them
if os.Getenv("D2_SEQUENCE") == "1" {
layout = d2sequence.Layout
}
diagram, _, err := d2lib.Compile(ctx, string(input), &d2lib.CompileOptions{
Layout: layout,
Ruler: ruler,