Merge pull request #317 from ejulio-ts/nested-sequence-diagrams
layout: sequence diagrams
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
309
d2layouts/d2sequence/sequence_diagram.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
22
d2lib/d2.go
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
1472
e2etests/testdata/stable/sequence_diagram_nested_span/dagre/board.exp.json
generated
vendored
Normal file
24
e2etests/testdata/stable/sequence_diagram_nested_span/dagre/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 332 KiB |
1472
e2etests/testdata/stable/sequence_diagram_nested_span/elk/board.exp.json
generated
vendored
Normal file
24
e2etests/testdata/stable/sequence_diagram_nested_span/elk/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 332 KiB |
787
e2etests/testdata/stable/sequence_diagram_simple/dagre/board.exp.json
generated
vendored
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
31
e2etests/testdata/stable/sequence_diagram_simple/dagre/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 474 KiB |
787
e2etests/testdata/stable/sequence_diagram_simple/elk/board.exp.json
generated
vendored
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
31
e2etests/testdata/stable/sequence_diagram_simple/elk/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 474 KiB |
1400
e2etests/testdata/stable/sequence_diagram_span/dagre/board.exp.json
generated
vendored
Normal file
31
e2etests/testdata/stable/sequence_diagram_span/dagre/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 476 KiB |
1400
e2etests/testdata/stable/sequence_diagram_span/elk/board.exp.json
generated
vendored
Normal file
31
e2etests/testdata/stable/sequence_diagram_span/elk/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 476 KiB |
4792
e2etests/testdata/stable/sequence_diagrams/dagre/board.exp.json
generated
vendored
Normal file
31
e2etests/testdata/stable/sequence_diagrams/dagre/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 498 KiB |
4711
e2etests/testdata/stable/sequence_diagrams/elk/board.exp.json
generated
vendored
Normal file
31
e2etests/testdata/stable/sequence_diagrams/elk/sketch.exp.svg
vendored
Normal file
|
After Width: | Height: | Size: 498 KiB |
6
main.go
|
|
@ -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,
|
||||
|
|
|
|||