diff --git a/d2layouts/d2dagrelayout/layout.go b/d2layouts/d2dagrelayout/layout.go index 7ac2a44e4..7004496cd 100644 --- a/d2layouts/d2dagrelayout/layout.go +++ b/d2layouts/d2dagrelayout/layout.go @@ -7,6 +7,7 @@ import ( "fmt" "math" "regexp" + "sort" "strings" "cdr.dev/slog" @@ -104,6 +105,16 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err rootAttrs.rankdir = "TB" } + maxContainerLabelHeight := 0 + for _, obj := range g.Objects { + if len(obj.ChildrenArray) == 0 { + continue + } + if obj.LabelHeight != nil { + maxContainerLabelHeight = go2.Max(maxContainerLabelHeight, *obj.LabelHeight) + } + } + maxLabelSize := 0 for _, edge := range g.Edges { size := edge.LabelDimensions.Width @@ -112,7 +123,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err } maxLabelSize = go2.Max(maxLabelSize, size) } - rootAttrs.ranksep = go2.Max(100, maxLabelSize+40) + rootAttrs.ranksep = go2.Max(go2.Max(100, maxLabelSize+40), maxContainerLabelHeight) configJS := setGraphAttrs(rootAttrs) if _, err := vm.RunString(configJS); err != nil { @@ -130,6 +141,9 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err if obj.Attributes.Shape.Value == d2target.ShapeImage || obj.Attributes.Icon != nil { height += float64(*obj.LabelHeight) + label.PADDING } + if len(obj.ChildrenArray) > 0 { + obj.Height += float64(*obj.LabelHeight) + } } loadScript += generateAddNodeLine(id, int(obj.Width), int(height)) if obj.Parent != g.Root { @@ -191,7 +205,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err if obj.LabelWidth != nil && obj.LabelHeight != nil { if len(obj.ChildrenArray) > 0 { - obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter)) + obj.LabelPosition = go2.Pointer(string(label.OutsideTopCenter)) } else if obj.Attributes.Shape.Value == d2target.ShapeImage { obj.LabelPosition = go2.Pointer(string(label.OutsideBottomCenter)) // remove the extra height we added to the node when passing to dagre @@ -327,6 +341,85 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err } } + // TODO probably don't need + byLevels := make([]*d2graph.Object, len(g.Objects)) + copy(byLevels, g.Objects) + sort.SliceStable(byLevels, func(i, j int) bool { + return byLevels[i].Level() > byLevels[j].Level() + }) + + for _, obj := range byLevels { + if obj.LabelHeight == nil || len(obj.ChildrenArray) <= 0 { + continue + } + + // This was artifically added to make dagre consider label height + obj.Height -= float64(*obj.LabelHeight) + + movedEdges := make(map[*d2graph.Edge]struct{}) + for _, e := range g.Edges { + currSrc := e.Src + currDst := e.Dst + + isSrcDesc := false + isDstDesc := false + + for currSrc != nil { + if currSrc == obj { + isSrcDesc = true + break + } + currSrc = currSrc.Parent + } + for currDst != nil { + if currDst == obj { + isDstDesc = true + break + } + currDst = currDst.Parent + } + if isSrcDesc && isDstDesc { + stepSize := float64(*obj.LabelHeight) + if e.Src != obj || e.Dst != obj { + stepSize /= 2. + } + movedEdges[e] = struct{}{} + for _, p := range e.Route { + p.Y += stepSize + } + } + } + + // Downshift descendents + q := []*d2graph.Object{obj} + for len(q) > 0 { + curr := q[0] + q = q[1:] + + stepSize := float64(*obj.LabelHeight) + if curr != obj { + stepSize /= 2. + } + curr.TopLeft.Y += stepSize + if curr != obj { + for _, e := range g.Edges { + if _, ok := movedEdges[e]; ok { + continue + } + if e.Src == curr { + e.Route[0].Y += stepSize + } + if e.Dst == curr { + e.Route[len(e.Route)-1].Y += stepSize + } + } + } + for _, c := range curr.ChildrenArray { + q = append(q, c) + } + } + } + return nil } diff --git a/d2renderers/d2sketch/testdata/animated/sketch.exp.svg b/d2renderers/d2sketch/testdata/animated/sketch.exp.svg index 51a2b7396..4ea57e1cb 100644 --- a/d2renderers/d2sketch/testdata/animated/sketch.exp.svg +++ b/d2renderers/d2sketch/testdata/animated/sketch.exp.svg @@ -3,7 +3,7 @@ id="d2-svg" style="background: white;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" -width="486" height="802" viewBox="-102 -102 486 802">aabbllmmnnoocciikkddgghhjjeeff1122 334455667788 - - - - - - - - - +aabbllmmnnoocciikkddgghhjjeeff1122 334455667788 + + + + + + + + + If we were meant to fly, we wouldn't keep losing our luggagebc + + + \ No newline at end of file diff --git a/e2etests/testdata/stable/container_label_loop/elk/board.exp.json b/e2etests/testdata/stable/container_label_loop/elk/board.exp.json new file mode 100644 index 000000000..9e87e945d --- /dev/null +++ b/e2etests/testdata/stable/container_label_loop/elk/board.exp.json @@ -0,0 +1,217 @@ +{ + "name": "", + "fontFamily": "SourceSansPro", + "shapes": [ + { + "id": "a", + "type": "rectangle", + "pos": { + "x": 62, + "y": 12 + }, + "width": 203, + "height": 382, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#E3E9FD", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "If we were meant to fly, we wouldn't keep losing our luggage", + "fontSize": 28, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 702, + "labelHeight": 36, + "labelPosition": "INSIDE_TOP_CENTER", + "zIndex": 0, + "level": 1 + }, + { + "id": "a.b", + "type": "rectangle", + "pos": { + "x": 137, + "y": 87 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#EDF0FD", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "b", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + }, + { + "id": "a.c", + "type": "rectangle", + "pos": { + "x": 137, + "y": 253 + }, + "width": 53, + "height": 66, + "opacity": 1, + "strokeDash": 0, + "strokeWidth": 2, + "borderRadius": 0, + "fill": "#EDF0FD", + "stroke": "#0D32B2", + "shadow": false, + "3d": false, + "multiple": false, + "double-border": false, + "tooltip": "", + "link": "", + "icon": null, + "iconPosition": "", + "blend": false, + "fields": null, + "methods": null, + "columns": null, + "label": "c", + "fontSize": 16, + "fontFamily": "DEFAULT", + "language": "", + "color": "#0A0F25", + "italic": false, + "bold": true, + "underline": false, + "labelWidth": 8, + "labelHeight": 21, + "labelPosition": "INSIDE_MIDDLE_CENTER", + "zIndex": 0, + "level": 2 + } + ], + "connections": [ + { + "id": "a.(b -> c)[0]", + "src": "a.b", + "srcArrow": "none", + "srcLabel": "", + "dst": "a.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": 163.5, + "y": 153 + }, + { + "x": 163.5, + "y": 253 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + }, + { + "id": "(a -> a)[0]", + "src": "a", + "srcArrow": "none", + "srcLabel": "", + "dst": "a", + "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": 62, + "y": 139.33333333333331 + }, + { + "x": 12, + "y": 139.33333333333331 + }, + { + "x": 12, + "y": 266.66666666666663 + }, + { + "x": 62, + "y": 266.66666666666663 + } + ], + "animated": false, + "tooltip": "", + "icon": null, + "zIndex": 0 + } + ] +} diff --git a/e2etests/testdata/stable/container_label_loop/elk/sketch.exp.svg b/e2etests/testdata/stable/container_label_loop/elk/sketch.exp.svg new file mode 100644 index 000000000..95bc08d82 --- /dev/null +++ b/e2etests/testdata/stable/container_label_loop/elk/sketch.exp.svg @@ -0,0 +1,59 @@ + +If we were meant to fly, we wouldn't keep losing our luggagebc + + + \ No newline at end of file diff --git a/e2etests/testdata/stable/different_subgraphs/dagre/board.exp.json b/e2etests/testdata/stable/different_subgraphs/dagre/board.exp.json index a0924a33c..3e1e67cc4 100644 --- a/e2etests/testdata/stable/different_subgraphs/dagre/board.exp.json +++ b/e2etests/testdata/stable/different_subgraphs/dagre/board.exp.json @@ -499,10 +499,10 @@ "type": "rectangle", "pos": { "x": 821, - "y": 0 + "y": 36 }, "width": 368, - "height": 664, + "height": 628, "opacity": 1, "strokeDash": 0, "strokeWidth": 2, @@ -531,7 +531,7 @@ "underline": false, "labelWidth": 72, "labelHeight": 36, - "labelPosition": "INSIDE_TOP_CENTER", + "labelPosition": "OUTSIDE_TOP_CENTER", "zIndex": 0, "level": 1 }, @@ -704,7 +704,7 @@ "type": "rectangle", "pos": { "x": 1056, - "y": 216 + "y": 234 }, "width": 53, "height": 66, @@ -745,7 +745,7 @@ "type": "rectangle", "pos": { "x": 891, - "y": 382 + "y": 400 }, "width": 74, "height": 66, @@ -786,7 +786,7 @@ "type": "rectangle", "pos": { "x": 1038, - "y": 50 + "y": 68 }, "width": 88, "height": 66, @@ -827,7 +827,7 @@ "type": "rectangle", "pos": { "x": 871, - "y": 548 + "y": 566 }, "width": 113, "height": 66, @@ -868,7 +868,7 @@ "type": "rectangle", "pos": { "x": 1025, - "y": 382 + "y": 400 }, "width": 75, "height": 66, @@ -1587,19 +1587,19 @@ "route": [ { "x": 1055.5, - "y": 263.23624595469255 + "y": 281.23624595469255 }, { "x": 953.1, - "y": 318.24724919093853 + "y": 336.24724919093853 }, { "x": 927.5, - "y": 342 + "y": 360 }, { "x": 927.5, - "y": 382 + "y": 400 } ], "isCurve": true, @@ -1635,19 +1635,19 @@ "route": [ { "x": 1082, - "y": 116 + "y": 134 }, { "x": 1082, - "y": 156 + "y": 174 }, { "x": 1082, - "y": 176 + "y": 194 }, { "x": 1082, - "y": 216 + "y": 234 } ], "isCurve": true, @@ -1683,19 +1683,19 @@ "route": [ { "x": 927.5, - "y": 448 + "y": 466 }, { "x": 927.5, - "y": 488 + "y": 506 }, { "x": 927.5, - "y": 508 + "y": 526 }, { "x": 927.5, - "y": 548 + "y": 566 } ], "isCurve": true, @@ -1731,19 +1731,19 @@ "route": [ { "x": 1074.0481927710844, - "y": 282 + "y": 300 }, { "x": 1064.4096385542168, - "y": 322 + "y": 340 }, { "x": 1062, - "y": 342 + "y": 360 }, { "x": 1062, - "y": 382 + "y": 400 } ], "isCurve": true, diff --git a/e2etests/testdata/stable/different_subgraphs/dagre/sketch.exp.svg b/e2etests/testdata/stable/different_subgraphs/dagre/sketch.exp.svg index cfccd3094..f0c7957ea 100644 --- a/e2etests/testdata/stable/different_subgraphs/dagre/sketch.exp.svg +++ b/e2etests/testdata/stable/different_subgraphs/dagre/sketch.exp.svg @@ -3,7 +3,7 @@ id="d2-svg" style="background: white;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" -width="1696" height="868" viewBox="-102 -102 1696 868">container

they did it in style

-

a header

+container

they did it in style

+

a header

a line of text and an

{
 	indented: "block",
@@ -805,8 +805,8 @@ width="516" height="686" viewBox="-102 -102 516 686">markdown

Lorem ipsum dolor sit amet, consectetur adipiscing elit,
+markdown

Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

-
- +
+ markdown

Lorem ipsum dolor sit amet, consectetur adipiscing elit,
+markdown

Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

-
- +
+ containerscloudtall cylinderclass- +containerscloudtall cylinderclass- num int- timeout @@ -823,8 +823,8 @@ width="2482" height="2672" viewBox="-102 -102 2482 2672">