From 105e9ad08343c1a6cae325317e8c02e7f313f6f7 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Sun, 4 Dec 2022 00:11:03 -0800 Subject: [PATCH 1/2] implement notes in sequence diagrams --- d2layouts/d2sequence/sequence_diagram.go | 126 +++- e2etests/stable_test.go | 13 + .../dagre/board.exp.json | 592 ++++++++++++++++++ .../dagre/sketch.exp.svg | 34 + .../sequence_diagram_note/elk/board.exp.json | 592 ++++++++++++++++++ .../sequence_diagram_note/elk/sketch.exp.svg | 34 + 6 files changed, 1374 insertions(+), 17 deletions(-) create mode 100644 e2etests/testdata/stable/sequence_diagram_note/dagre/board.exp.json create mode 100644 e2etests/testdata/stable/sequence_diagram_note/dagre/sketch.exp.svg create mode 100644 e2etests/testdata/stable/sequence_diagram_note/elk/board.exp.json create mode 100644 e2etests/testdata/stable/sequence_diagram_note/elk/sketch.exp.svg diff --git a/d2layouts/d2sequence/sequence_diagram.go b/d2layouts/d2sequence/sequence_diagram.go index c0f3727ee..88a368989 100644 --- a/d2layouts/d2sequence/sequence_diagram.go +++ b/d2layouts/d2sequence/sequence_diagram.go @@ -20,6 +20,7 @@ type sequenceDiagram struct { lifelines []*d2graph.Edge actors []*d2graph.Object spans []*d2graph.Object + notes []*d2graph.Object // can be either actors or spans // rank: left to right position of actors/spans (spans have the same rank as their parents) @@ -27,26 +28,63 @@ type sequenceDiagram struct { // 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 + // TODO rank is used for y coordinate here and x coordinate above?? minMessageRank map[*d2graph.Object]int maxMessageRank map[*d2graph.Object]int messageYStep float64 actorXStep float64 maxActorHeight float64 + + verticalIndices map[string]int +} + +func getObjEarliestLineNum(o *d2graph.Object) int { + min := int(math.MaxInt64) + for _, ref := range o.References { + if ref.MapKey == nil { + continue + } + min = go2.IntMin(min, ref.MapKey.Range.Start.Line) + } + return min +} + +func getEdgeEarliestLineNum(e *d2graph.Edge) int { + min := int(math.MaxInt64) + for _, ref := range e.References { + if ref.MapKey == nil { + continue + } + min = go2.IntMin(min, ref.MapKey.Range.Start.Line) + } + return min +} + +func hasEdge(o *d2graph.Object) bool { + for _, ref := range o.References { + if ref.MapKey != nil && len(ref.MapKey.Edges) > 0 { + return true + } + } + + return false } 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., + messages: messages, + actors: actors, + spans: nil, + notes: 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., + verticalIndices: make(map[string]int), } for rank, actor := range actors { @@ -63,20 +101,33 @@ func newSequenceDiagram(actors []*d2graph.Object, messages []*d2graph.Edge) *seq queue := make([]*d2graph.Object, len(actor.ChildrenArray)) copy(queue, actor.ChildrenArray) for len(queue) > 0 { - span := queue[0] + child := 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 + // spans are children of actors that have edges + // notes are children of actors with no edges + if hasEdge(child) { + // spans have no labels + // TODO why not? Spans should be able to + child.Attributes.Label = d2graph.Scalar{Value: ""} + child.Attributes.Shape = d2graph.Scalar{Value: shape.SQUARE_TYPE} + sd.spans = append(sd.spans, child) + sd.objectRank[child] = rank + } else { + sd.verticalIndices[child.AbsID()] = getObjEarliestLineNum(child) + // TODO change to page type when it doesn't look deformed + child.Attributes.Shape = d2graph.Scalar{Value: shape.SQUARE_TYPE} + sd.notes = append(sd.notes, child) + sd.objectRank[child] = rank + child.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter)) + } - queue = append(queue, span.ChildrenArray...) + queue = append(queue, child.ChildrenArray...) } } for rank, message := range sd.messages { + sd.verticalIndices[message.AbsID()] = getEdgeEarliestLineNum(message) sd.messageYStep = math.Max(sd.messageYStep, float64(message.LabelDimensions.Height)) sd.setMinMaxMessageRank(message.Src, rank) @@ -111,6 +162,7 @@ func (sd *sequenceDiagram) setMinMaxMessageRank(actor *d2graph.Object, rank int) func (sd *sequenceDiagram) layout() { sd.placeActors() sd.placeSpans() + sd.placeNotes() sd.routeMessages() sd.addLifelineEdges() } @@ -143,6 +195,11 @@ func (sd *sequenceDiagram) placeActors() { // │ func (sd *sequenceDiagram) addLifelineEdges() { endY := sd.getMessageY(len(sd.messages)) + + for _, note := range sd.notes { + endY += note.Height + sd.messageYStep + } + for _, actor := range sd.actors { actorBottom := actor.Center() actorBottom.Y = actor.TopLeft.Y + actor.Height @@ -169,6 +226,34 @@ func (sd *sequenceDiagram) addLifelineEdges() { } } +func (sd *sequenceDiagram) placeNotes() { + rankToX := make(map[int]float64) + for _, actor := range sd.actors { + rankToX[sd.objectRank[actor]] = actor.Center().X + } + + for _, note := range sd.notes { + verticalIndex := sd.verticalIndices[note.AbsID()] + y := sd.maxActorHeight + sd.messageYStep + + for _, msg := range sd.messages { + if sd.verticalIndices[msg.AbsID()] < verticalIndex { + y += sd.messageYStep + } + } + for _, otherNote := range sd.notes { + if sd.verticalIndices[otherNote.AbsID()] < verticalIndex { + y += otherNote.Height + sd.messageYStep + } + } + + x := rankToX[sd.objectRank[note]] - (note.Width / 2.) + // note.Box = geo.NewBox(geo.NewPoint(x, y), width, height) + note.Box.TopLeft = geo.NewPoint(x, y) + note.ZIndex = 1 + } +} + // placeSpans places spans over the object lifeline // ┌──────────┐ // │ actor │ @@ -261,6 +346,13 @@ func (sd *sequenceDiagram) routeMessages() { } messageY := sd.getMessageY(rank) + + for _, note := range sd.notes { + if sd.verticalIndices[note.AbsID()] < sd.verticalIndices[message.AbsID()] { + messageY += note.Height + sd.messageYStep + } + } + message.Route = []*geo.Point{ geo.NewPoint(startX, messageY), geo.NewPoint(endX, messageY), diff --git a/e2etests/stable_test.go b/e2etests/stable_test.go index 306838bd4..6492bfe9c 100644 --- a/e2etests/stable_test.go +++ b/e2etests/stable_test.go @@ -1332,6 +1332,19 @@ z -> z: hello label: hello icon: https://icons.terrastruct.com/essentials/time.svg } +`, + }, + { + name: "sequence_diagram_note", + script: `shape: sequence_diagram +a; b; c; d +a -> b +a.explanation +a.another explanation +b -> c +b."Some one who believes imaginary things\n appear right before your i's." +c -> b: okay +d."The earth is like a tiny grain of sand, only much, much heavier" `, }, } diff --git a/e2etests/testdata/stable/sequence_diagram_note/dagre/board.exp.json b/e2etests/testdata/stable/sequence_diagram_note/dagre/board.exp.json new file mode 100644 index 000000000..426562e61 --- /dev/null +++ b/e2etests/testdata/stable/sequence_diagram_note/dagre/board.exp.json @@ -0,0 +1,592 @@ +{ + "name": "", + "shapes": [ + { + "id": "a", + "type": "", + "pos": { + "x": 0, + "y": 50 + }, + "width": 150, + "height": 169, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#E3E9FD", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "fields": null, + "methods": null, + "columns": null, + "label": "a", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 12, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "b", + "type": "", + "pos": { + "x": 400, + "y": 52 + }, + "width": 150, + "height": 167, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#E3E9FD", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "fields": null, + "methods": null, + "columns": null, + "label": "b", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 13, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "c", + "type": "", + "pos": { + "x": 800, + "y": 52 + }, + "width": 150, + "height": 167, + "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": "c", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 13, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "d", + "type": "", + "pos": { + "x": 1200, + "y": 52 + }, + "width": 150, + "height": 167, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#E3E9FD", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "fields": null, + "methods": null, + "columns": null, + "label": "d", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 13, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "a.explanation", + "type": "rectangle", + "pos": { + "x": -20, + "y": 479 + }, + "width": 190, + "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": "explanation", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 90, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 1, + "level": 2 + }, + { + "id": "a.another explanation", + "type": "rectangle", + "pos": { + "x": -50, + "y": 735 + }, + "width": 250, + "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": "another explanation", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 150, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 1, + "level": 2 + }, + { + "id": "b.\"Some one who believes imaginary things\\n appear right before your i's.\"", + "type": "rectangle", + "pos": { + "x": 278, + "y": 1121 + }, + "width": 393, + "height": 142, + "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": "Some one who believes imaginary things\n appear right before your i's.", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 293, + "labelHeight": 42, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 1, + "level": 2 + }, + { + "id": "d.The earth is like a tiny grain of sand, only much, much heavier", + "type": "rectangle", + "pos": { + "x": 1004, + "y": 1523 + }, + "width": 541, + "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": "The earth is like a tiny grain of sand, only much, much heavier", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 441, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 1, + "level": 2 + } + ], + "connections": [ + { + "id": "(a -> b)[0]", + "src": "a", + "srcArrow": "none", + "srcLabel": "", + "dst": "b", + "dstArrow": "triangle", + "dstLabel": "", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 75, + "y": 349 + }, + { + "x": 475, + "y": 349 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 2 + }, + { + "id": "(b -> c)[0]", + "src": "b", + "srcArrow": "none", + "srcLabel": "", + "dst": "c", + "dstArrow": "triangle", + "dstLabel": "", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 475, + "y": 991 + }, + { + "x": 875, + "y": 991 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 2 + }, + { + "id": "(c -> b)[0]", + "src": "c", + "srcArrow": "none", + "srcLabel": "", + "dst": "b", + "dstArrow": "triangle", + "dstLabel": "", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "okay", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 33, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "labelPercentage": 0, + "route": [ + { + "x": 875, + "y": 1393 + }, + { + "x": 475, + "y": 1393 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 2 + }, + { + "id": "(a -- )[0]", + "src": "a", + "srcArrow": "none", + "srcLabel": "", + "dst": "a-lifeline-end-2251863791", + "dstArrow": "none", + "dstLabel": "", + "opacity": 1, + "strokeDash": 8, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 75, + "y": 219 + }, + { + "x": 75, + "y": 1779 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "(b -- )[0]", + "src": "b", + "srcArrow": "none", + "srcLabel": "", + "dst": "b-lifeline-end-668380428", + "dstArrow": "none", + "dstLabel": "", + "opacity": 1, + "strokeDash": 8, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 475, + "y": 219 + }, + { + "x": 475, + "y": 1779 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "(c -- )[0]", + "src": "c", + "srcArrow": "none", + "srcLabel": "", + "dst": "c-lifeline-end-955173837", + "dstArrow": "none", + "dstLabel": "", + "opacity": 1, + "strokeDash": 8, + "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": 875, + "y": 219 + }, + { + "x": 875, + "y": 1779 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "(d -- )[0]", + "src": "d", + "srcArrow": "none", + "srcLabel": "", + "dst": "d-lifeline-end-2106864010", + "dstArrow": "none", + "dstLabel": "", + "opacity": 1, + "strokeDash": 8, + "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": 1275, + "y": 219 + }, + { + "x": 1275, + "y": 1779 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + } + ] +} diff --git a/e2etests/testdata/stable/sequence_diagram_note/dagre/sketch.exp.svg b/e2etests/testdata/stable/sequence_diagram_note/dagre/sketch.exp.svg new file mode 100644 index 000000000..c407a5874 --- /dev/null +++ b/e2etests/testdata/stable/sequence_diagram_note/dagre/sketch.exp.svg @@ -0,0 +1,34 @@ + +abcdexplanationanother explanationSome one who believes imaginary things appear right before your i's.The earth is like a tiny grain of sand, only much, much heavier okay + + + \ No newline at end of file diff --git a/e2etests/testdata/stable/sequence_diagram_note/elk/board.exp.json b/e2etests/testdata/stable/sequence_diagram_note/elk/board.exp.json new file mode 100644 index 000000000..426562e61 --- /dev/null +++ b/e2etests/testdata/stable/sequence_diagram_note/elk/board.exp.json @@ -0,0 +1,592 @@ +{ + "name": "", + "shapes": [ + { + "id": "a", + "type": "", + "pos": { + "x": 0, + "y": 50 + }, + "width": 150, + "height": 169, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#E3E9FD", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "fields": null, + "methods": null, + "columns": null, + "label": "a", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 12, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "b", + "type": "", + "pos": { + "x": 400, + "y": 52 + }, + "width": 150, + "height": 167, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#E3E9FD", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "fields": null, + "methods": null, + "columns": null, + "label": "b", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 13, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "c", + "type": "", + "pos": { + "x": 800, + "y": 52 + }, + "width": 150, + "height": 167, + "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": "c", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 13, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "d", + "type": "", + "pos": { + "x": 1200, + "y": 52 + }, + "width": 150, + "height": 167, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#E3E9FD", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "fields": null, + "methods": null, + "columns": null, + "label": "d", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 13, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "a.explanation", + "type": "rectangle", + "pos": { + "x": -20, + "y": 479 + }, + "width": 190, + "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": "explanation", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 90, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 1, + "level": 2 + }, + { + "id": "a.another explanation", + "type": "rectangle", + "pos": { + "x": -50, + "y": 735 + }, + "width": 250, + "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": "another explanation", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 150, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 1, + "level": 2 + }, + { + "id": "b.\"Some one who believes imaginary things\\n appear right before your i's.\"", + "type": "rectangle", + "pos": { + "x": 278, + "y": 1121 + }, + "width": 393, + "height": 142, + "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": "Some one who believes imaginary things\n appear right before your i's.", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 293, + "labelHeight": 42, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 1, + "level": 2 + }, + { + "id": "d.The earth is like a tiny grain of sand, only much, much heavier", + "type": "rectangle", + "pos": { + "x": 1004, + "y": 1523 + }, + "width": 541, + "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": "The earth is like a tiny grain of sand, only much, much heavier", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 441, + "labelHeight": 26, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 1, + "level": 2 + } + ], + "connections": [ + { + "id": "(a -> b)[0]", + "src": "a", + "srcArrow": "none", + "srcLabel": "", + "dst": "b", + "dstArrow": "triangle", + "dstLabel": "", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 75, + "y": 349 + }, + { + "x": 475, + "y": 349 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 2 + }, + { + "id": "(b -> c)[0]", + "src": "b", + "srcArrow": "none", + "srcLabel": "", + "dst": "c", + "dstArrow": "triangle", + "dstLabel": "", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 475, + "y": 991 + }, + { + "x": 875, + "y": 991 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 2 + }, + { + "id": "(c -> b)[0]", + "src": "c", + "srcArrow": "none", + "srcLabel": "", + "dst": "b", + "dstArrow": "triangle", + "dstLabel": "", + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "okay", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 33, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "labelPercentage": 0, + "route": [ + { + "x": 875, + "y": 1393 + }, + { + "x": 475, + "y": 1393 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 2 + }, + { + "id": "(a -- )[0]", + "src": "a", + "srcArrow": "none", + "srcLabel": "", + "dst": "a-lifeline-end-2251863791", + "dstArrow": "none", + "dstLabel": "", + "opacity": 1, + "strokeDash": 8, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 75, + "y": 219 + }, + { + "x": 75, + "y": 1779 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "(b -- )[0]", + "src": "b", + "srcArrow": "none", + "srcLabel": "", + "dst": "b-lifeline-end-668380428", + "dstArrow": "none", + "dstLabel": "", + "opacity": 1, + "strokeDash": 8, + "strokeWidth": 2, + "stroke": "#0D32B2", + "label": "", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#676C7E", + "italic": true, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0, + "labelPosition": "", + "labelPercentage": 0, + "route": [ + { + "x": 475, + "y": 219 + }, + { + "x": 475, + "y": 1779 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "(c -- )[0]", + "src": "c", + "srcArrow": "none", + "srcLabel": "", + "dst": "c-lifeline-end-955173837", + "dstArrow": "none", + "dstLabel": "", + "opacity": 1, + "strokeDash": 8, + "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": 875, + "y": 219 + }, + { + "x": 875, + "y": 1779 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "(d -- )[0]", + "src": "d", + "srcArrow": "none", + "srcLabel": "", + "dst": "d-lifeline-end-2106864010", + "dstArrow": "none", + "dstLabel": "", + "opacity": 1, + "strokeDash": 8, + "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": 1275, + "y": 219 + }, + { + "x": 1275, + "y": 1779 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + } + ] +} diff --git a/e2etests/testdata/stable/sequence_diagram_note/elk/sketch.exp.svg b/e2etests/testdata/stable/sequence_diagram_note/elk/sketch.exp.svg new file mode 100644 index 000000000..c407a5874 --- /dev/null +++ b/e2etests/testdata/stable/sequence_diagram_note/elk/sketch.exp.svg @@ -0,0 +1,34 @@ + +abcdexplanationanother explanationSome one who believes imaginary things appear right before your i's.The earth is like a tiny grain of sand, only much, much heavier okay + + + \ No newline at end of file From a86de8694c898956537f3ec3a41b03dd10eeb54c Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Sun, 4 Dec 2022 00:11:38 -0800 Subject: [PATCH 2/2] messageYStep -> yStep --- d2layouts/d2sequence/sequence_diagram.go | 39 ++++++++++++------------ 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/d2layouts/d2sequence/sequence_diagram.go b/d2layouts/d2sequence/sequence_diagram.go index 88a368989..bf7622a33 100644 --- a/d2layouts/d2sequence/sequence_diagram.go +++ b/d2layouts/d2sequence/sequence_diagram.go @@ -28,11 +28,10 @@ type sequenceDiagram struct { // 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 - // TODO rank is used for y coordinate here and x coordinate above?? minMessageRank map[*d2graph.Object]int maxMessageRank map[*d2graph.Object]int - messageYStep float64 + yStep float64 actorXStep float64 maxActorHeight float64 @@ -81,7 +80,7 @@ func newSequenceDiagram(actors []*d2graph.Object, messages []*d2graph.Edge) *seq objectRank: make(map[*d2graph.Object]int), minMessageRank: make(map[*d2graph.Object]int), maxMessageRank: make(map[*d2graph.Object]int), - messageYStep: MIN_MESSAGE_DISTANCE, + yStep: MIN_MESSAGE_DISTANCE, actorXStep: MIN_ACTOR_DISTANCE, maxActorHeight: 0., verticalIndices: make(map[string]int), @@ -128,7 +127,7 @@ func newSequenceDiagram(actors []*d2graph.Object, messages []*d2graph.Edge) *seq for rank, message := range sd.messages { sd.verticalIndices[message.AbsID()] = getEdgeEarliestLineNum(message) - sd.messageYStep = math.Max(sd.messageYStep, float64(message.LabelDimensions.Height)) + sd.yStep = math.Max(sd.yStep, float64(message.LabelDimensions.Height)) sd.setMinMaxMessageRank(message.Src, rank) sd.setMinMaxMessageRank(message.Dst, rank) @@ -140,7 +139,7 @@ func newSequenceDiagram(actors []*d2graph.Object, messages []*d2graph.Edge) *seq sd.actorXStep = math.Max(sd.actorXStep, distributedLabelWidth+HORIZONTAL_PAD) } - sd.messageYStep += VERTICAL_PAD + sd.yStep += VERTICAL_PAD sd.maxActorHeight += VERTICAL_PAD if sd.root.LabelHeight != nil { sd.maxActorHeight += float64(*sd.root.LabelHeight) @@ -194,11 +193,16 @@ func (sd *sequenceDiagram) placeActors() { // │ // │ func (sd *sequenceDiagram) addLifelineEdges() { - endY := sd.getMessageY(len(sd.messages)) - - for _, note := range sd.notes { - endY += note.Height + sd.messageYStep + endY := 0. + for _, m := range sd.messages { + for _, p := range m.Route { + endY = math.Max(endY, p.Y) + } } + for _, note := range sd.notes { + endY = math.Max(endY, note.TopLeft.Y+note.Height) + } + endY += sd.yStep for _, actor := range sd.actors { actorBottom := actor.Center() @@ -232,23 +236,20 @@ func (sd *sequenceDiagram) placeNotes() { rankToX[sd.objectRank[actor]] = actor.Center().X } - for _, note := range sd.notes { + for i, note := range sd.notes { verticalIndex := sd.verticalIndices[note.AbsID()] - y := sd.maxActorHeight + sd.messageYStep + y := sd.maxActorHeight + sd.yStep for _, msg := range sd.messages { if sd.verticalIndices[msg.AbsID()] < verticalIndex { - y += sd.messageYStep + y += sd.yStep } } - for _, otherNote := range sd.notes { - if sd.verticalIndices[otherNote.AbsID()] < verticalIndex { - y += otherNote.Height + sd.messageYStep - } + for _, otherNote := range sd.notes[:i] { + y += otherNote.Height + sd.yStep } x := rankToX[sd.objectRank[note]] - (note.Width / 2.) - // note.Box = geo.NewBox(geo.NewPoint(x, y), width, height) note.Box.TopLeft = geo.NewPoint(x, y) note.ZIndex = 1 } @@ -349,7 +350,7 @@ func (sd *sequenceDiagram) routeMessages() { for _, note := range sd.notes { if sd.verticalIndices[note.AbsID()] < sd.verticalIndices[message.AbsID()] { - messageY += note.Height + sd.messageYStep + messageY += note.Height + sd.yStep } } @@ -366,7 +367,7 @@ func (sd *sequenceDiagram) routeMessages() { 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 + return ((float64(rank) + 1.) * sd.yStep) + sd.maxActorHeight } func (sd *sequenceDiagram) isActor(obj *d2graph.Object) bool {