diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md
index 2ca488754..35c26ac0e 100644
--- a/ci/release/changelogs/next.md
+++ b/ci/release/changelogs/next.md
@@ -18,6 +18,7 @@ There's also been a major compiler rewrite. It's fixed many minor compiler bugs,
- Reduces default padding of shapes. [#702](https://github.com/terrastruct/d2/pull/702)
- Ensures labels fit inside shapes with shape-specific inner bounding boxes. [#702](https://github.com/terrastruct/d2/pull/702)
- dagre container labels changed positions to outside the shape. Many previously obscured container labels are now legible. [#788](https://github.com/terrastruct/d2/pull/788)
+- Container icons are placed top-left instead of center, to ensure no collisions with children. [#806](https://github.com/terrastruct/d2/pull/806)
- Code snippets use bold and italic font styles as determined by highlighter [#710](https://github.com/terrastruct/d2/issues/710), [#741](https://github.com/terrastruct/d2/issues/741)
- Improves package shape dimensions with short height. [#702](https://github.com/terrastruct/d2/pull/702)
- Sequence diagrams are rendered more compacted, both vertically and horizontally. [#796](https://github.com/terrastruct/d2/pull/796)
diff --git a/d2layouts/d2dagrelayout/layout.go b/d2layouts/d2dagrelayout/layout.go
index a3d943200..d1c91f2ed 100644
--- a/d2layouts/d2dagrelayout/layout.go
+++ b/d2layouts/d2dagrelayout/layout.go
@@ -109,12 +109,21 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
maxContainerLabelHeight := 0
for _, obj := range g.Objects {
- if len(obj.ChildrenArray) == 0 {
+ if len(obj.ChildrenArray) == 0 || obj.Parent == g.Root {
continue
}
if obj.LabelHeight != nil {
maxContainerLabelHeight = go2.Max(maxContainerLabelHeight, *obj.LabelHeight+label.PADDING)
}
+
+ if obj.Attributes.Icon != nil && obj.Attributes.Shape.Value != d2target.ShapeImage {
+ contentBox := geo.NewBox(geo.NewPoint(0, 0), float64(obj.Width), float64(obj.Height))
+ shapeType := d2target.DSL_SHAPE_TO_SHAPE_TYPE[obj.Attributes.Shape.Value]
+ s := shape.NewShape(shapeType, contentBox)
+ iconSize := d2target.GetIconSize(s.GetInnerBox(), string(label.InsideTopLeft))
+ // Since dagre container labels are pushed up, we don't want a child container to collide
+ maxContainerLabelHeight = go2.Max(maxContainerLabelHeight, (iconSize+label.PADDING*2)*2)
+ }
}
maxLabelSize := 0
@@ -219,7 +228,12 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
}
}
if obj.Attributes.Icon != nil {
- obj.IconPosition = go2.Pointer(string(label.InsideMiddleCenter))
+ if len(obj.ChildrenArray) > 0 {
+ obj.IconPosition = go2.Pointer(string(label.OutsideTopLeft))
+ obj.LabelPosition = go2.Pointer(string(label.OutsideTopRight))
+ } else {
+ obj.IconPosition = go2.Pointer(string(label.InsideMiddleCenter))
+ }
}
}
diff --git a/d2layouts/d2elklayout/layout.go b/d2layouts/d2elklayout/layout.go
index 886d3424a..7e1703b05 100644
--- a/d2layouts/d2elklayout/layout.go
+++ b/d2layouts/d2elklayout/layout.go
@@ -199,6 +199,24 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
Padding: opts.Padding,
},
}
+
+ if n.LayoutOptions.Padding == DefaultOpts.Padding {
+ // Default
+ paddingTop := 50
+ if obj.LabelHeight != nil {
+ paddingTop = go2.Max(paddingTop, *obj.LabelHeight+label.PADDING)
+ }
+ if obj.Attributes.Icon != nil && obj.Attributes.Shape.Value != d2target.ShapeImage {
+ contentBox := geo.NewBox(geo.NewPoint(0, 0), float64(n.Width), float64(n.Height))
+ shapeType := d2target.DSL_SHAPE_TO_SHAPE_TYPE[obj.Attributes.Shape.Value]
+ s := shape.NewShape(shapeType, contentBox)
+ iconSize := d2target.GetIconSize(s.GetInnerBox(), string(label.InsideTopLeft))
+ paddingTop = go2.Max(paddingTop, iconSize+label.PADDING*2)
+ }
+ n.LayoutOptions.Padding = fmt.Sprintf("[top=%d,left=50,bottom=50,right=50]",
+ paddingTop,
+ )
+ }
}
if obj.LabelWidth != nil && obj.LabelHeight != nil {
@@ -310,7 +328,12 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
}
}
if obj.Attributes.Icon != nil {
- obj.IconPosition = go2.Pointer(string(label.InsideMiddleCenter))
+ if len(obj.ChildrenArray) > 0 {
+ obj.IconPosition = go2.Pointer(string(label.InsideTopLeft))
+ obj.LabelPosition = go2.Pointer(string(label.InsideTopRight))
+ } else {
+ obj.IconPosition = go2.Pointer(string(label.InsideMiddleCenter))
+ }
}
byID[obj.AbsID()] = obj
diff --git a/d2renderers/d2svg/d2svg.go b/d2renderers/d2svg/d2svg.go
index 25c5dd88e..0b5192b7c 100644
--- a/d2renderers/d2svg/d2svg.go
+++ b/d2renderers/d2svg/d2svg.go
@@ -883,7 +883,7 @@ func drawShape(writer io.Writer, targetShape d2target.Shape, sketchRunner *d2ske
} else {
box = s.GetInnerBox()
}
- iconSize := targetShape.GetIconSize(box)
+ iconSize := d2target.GetIconSize(box, targetShape.IconPosition)
tl := iconPosition.GetPointOnBox(box, label.PADDING, float64(iconSize), float64(iconSize))
diff --git a/d2target/d2target.go b/d2target/d2target.go
index e5cf20cdb..20207c121 100644
--- a/d2target/d2target.go
+++ b/d2target/d2target.go
@@ -538,8 +538,8 @@ func init() {
}
}
-func (s *Shape) GetIconSize(box *geo.Box) int {
- iconPosition := label.Position(s.IconPosition)
+func GetIconSize(box *geo.Box, position string) int {
+ iconPosition := label.Position(position)
minDimension := int(math.Min(box.Width, box.Height))
halfMinDimension := int(math.Ceil(0.5 * float64(minDimension)))
diff --git a/e2etests/stable_test.go b/e2etests/stable_test.go
index bca51d2e7..7f9666e9e 100644
--- a/e2etests/stable_test.go
+++ b/e2etests/stable_test.go
@@ -895,6 +895,37 @@ b: {
icon: https://icons.terrastruct.com/essentials/004-picture.svg
}
a -> b
+`,
+ },
+ {
+ name: "icon-containers",
+ script: `vpc: VPC 1 10.1.0.0./16 {
+ icon: https://icons.terrastruct.com/aws%2F_Group%20Icons%2FVirtual-private-cloud-VPC_light-bg.svg
+ style: {
+ stroke: green
+ font-color: green
+ fill: white
+ }
+ az: Availability Zone A {
+ style: {
+ stroke: blue
+ font-color: blue
+ stroke-dash: 3
+ fill: white
+ }
+ firewall: Firewall Subnet A {
+ icon: https://icons.terrastruct.com/aws%2FNetworking%20&%20Content%20Delivery%2FAmazon-Route-53_Hosted-Zone_light-bg.svg
+ style: {
+ stroke: purple
+ font-color: purple
+ fill: "#e1d5e7"
+ }
+ ec2: EC2 Instance {
+ icon: https://icons.terrastruct.com/aws%2FCompute%2F_Instance%2FAmazon-EC2_C4-Instance_light-bg.svg
+ }
+ }
+ }
+}
`,
},
{
diff --git a/e2etests/testdata/stable/font_sizes_containers_large/dagre/board.exp.json b/e2etests/testdata/stable/font_sizes_containers_large/dagre/board.exp.json
index 74443f63c..22a8aacd9 100644
--- a/e2etests/testdata/stable/font_sizes_containers_large/dagre/board.exp.json
+++ b/e2etests/testdata/stable/font_sizes_containers_large/dagre/board.exp.json
@@ -7,10 +7,10 @@
"type": "rectangle",
"pos": {
"x": 0,
- "y": 65
+ "y": 50
},
"width": 264,
- "height": 511,
+ "height": 406,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
@@ -48,10 +48,10 @@
"type": "rectangle",
"pos": {
"x": 20,
- "y": 162
+ "y": 125
},
"width": 224,
- "height": 381,
+ "height": 306,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
@@ -89,10 +89,10 @@
"type": "rectangle",
"pos": {
"x": 40,
- "y": 241
+ "y": 196
},
"width": 184,
- "height": 270,
+ "height": 210,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
@@ -130,10 +130,10 @@
"type": "rectangle",
"pos": {
"x": 60,
- "y": 309
+ "y": 249
},
"width": 144,
- "height": 160,
+ "height": 130,
"opacity": 1,
"strokeDash": 0,
"strokeWidth": 2,
@@ -171,7 +171,7 @@
"type": "rectangle",
"pos": {
"x": 100,
- "y": 361
+ "y": 286
},
"width": 64,
"height": 56,
diff --git a/e2etests/testdata/stable/font_sizes_containers_large/dagre/sketch.exp.svg b/e2etests/testdata/stable/font_sizes_containers_large/dagre/sketch.exp.svg
index 61db3ed3b..167c42957 100644
--- a/e2etests/testdata/stable/font_sizes_containers_large/dagre/sketch.exp.svg
+++ b/e2etests/testdata/stable/font_sizes_containers_large/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="652" height="843" viewBox="-194 -165 652 843">VPC 1 10.1.0.0./16Availability Zone AFirewall Subnet AEC2 Instance
+
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/stable/icon-containers/elk/board.exp.json b/e2etests/testdata/stable/icon-containers/elk/board.exp.json
new file mode 100644
index 000000000..2b4d6a338
--- /dev/null
+++ b/e2etests/testdata/stable/icon-containers/elk/board.exp.json
@@ -0,0 +1,204 @@
+{
+ "name": "",
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "vpc",
+ "type": "rectangle",
+ "pos": {
+ "x": 12,
+ "y": 12
+ },
+ "width": 460,
+ "height": 466,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "white",
+ "stroke": "green",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": {
+ "Scheme": "https",
+ "Opaque": "",
+ "User": null,
+ "Host": "icons.terrastruct.com",
+ "Path": "/aws/_Group Icons/Virtual-private-cloud-VPC_light-bg.svg",
+ "RawPath": "/aws%2F_Group%20Icons%2FVirtual-private-cloud-VPC_light-bg.svg",
+ "ForceQuery": false,
+ "RawQuery": "",
+ "Fragment": "",
+ "RawFragment": ""
+ },
+ "iconPosition": "INSIDE_TOP_LEFT",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "VPC 1 10.1.0.0./16",
+ "fontSize": 28,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "green",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 206,
+ "labelHeight": 36,
+ "labelPosition": "INSIDE_TOP_RIGHT",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "vpc.az",
+ "type": "rectangle",
+ "pos": {
+ "x": 62,
+ "y": 86
+ },
+ "width": 360,
+ "height": 342,
+ "opacity": 1,
+ "strokeDash": 3,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "white",
+ "stroke": "blue",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "Availability Zone A",
+ "fontSize": 24,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "blue",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 185,
+ "labelHeight": 31,
+ "labelPosition": "INSIDE_TOP_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "vpc.az.firewall",
+ "type": "rectangle",
+ "pos": {
+ "x": 112,
+ "y": 136
+ },
+ "width": 260,
+ "height": 242,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#e1d5e7",
+ "stroke": "purple",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": {
+ "Scheme": "https",
+ "Opaque": "",
+ "User": null,
+ "Host": "icons.terrastruct.com",
+ "Path": "/aws/Networking & Content Delivery/Amazon-Route-53_Hosted-Zone_light-bg.svg",
+ "RawPath": "/aws%2FNetworking%20&%20Content%20Delivery%2FAmazon-Route-53_Hosted-Zone_light-bg.svg",
+ "ForceQuery": false,
+ "RawQuery": "",
+ "Fragment": "",
+ "RawFragment": ""
+ },
+ "iconPosition": "INSIDE_TOP_LEFT",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "Firewall Subnet A",
+ "fontSize": 20,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "purple",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 145,
+ "labelHeight": 26,
+ "labelPosition": "INSIDE_TOP_RIGHT",
+ "zIndex": 0,
+ "level": 3
+ },
+ {
+ "id": "vpc.az.firewall.ec2",
+ "type": "rectangle",
+ "pos": {
+ "x": 162,
+ "y": 210
+ },
+ "width": 160,
+ "height": 118,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "#FFFFFF",
+ "stroke": "#0D32B2",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": {
+ "Scheme": "https",
+ "Opaque": "",
+ "User": null,
+ "Host": "icons.terrastruct.com",
+ "Path": "/aws/Compute/_Instance/Amazon-EC2_C4-Instance_light-bg.svg",
+ "RawPath": "/aws%2FCompute%2F_Instance%2FAmazon-EC2_C4-Instance_light-bg.svg",
+ "ForceQuery": false,
+ "RawQuery": "",
+ "Fragment": "",
+ "RawFragment": ""
+ },
+ "iconPosition": "INSIDE_MIDDLE_CENTER",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "EC2 Instance",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "#0A0F25",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 89,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_TOP_CENTER",
+ "zIndex": 0,
+ "level": 4
+ }
+ ],
+ "connections": []
+}
diff --git a/e2etests/testdata/stable/icon-containers/elk/sketch.exp.svg b/e2etests/testdata/stable/icon-containers/elk/sketch.exp.svg
new file mode 100644
index 000000000..8fb5bdd7f
--- /dev/null
+++ b/e2etests/testdata/stable/icon-containers/elk/sketch.exp.svg
@@ -0,0 +1,59 @@
+
+
\ No newline at end of file
diff --git a/e2etests/testdata/todo/container_icon_label/dagre/board.exp.json b/e2etests/testdata/todo/container_icon_label/dagre/board.exp.json
index 46d342706..9e7c7e58c 100644
--- a/e2etests/testdata/todo/container_icon_label/dagre/board.exp.json
+++ b/e2etests/testdata/todo/container_icon_label/dagre/board.exp.json
@@ -35,7 +35,7 @@
"Fragment": "",
"RawFragment": ""
},
- "iconPosition": "INSIDE_MIDDLE_CENTER",
+ "iconPosition": "OUTSIDE_TOP_LEFT",
"blend": false,
"fields": null,
"methods": null,
@@ -50,7 +50,7 @@
"underline": false,
"labelWidth": 96,
"labelHeight": 38,
- "labelPosition": "OUTSIDE_TOP_CENTER",
+ "labelPosition": "OUTSIDE_TOP_RIGHT",
"zIndex": 0,
"level": 1
},
diff --git a/e2etests/testdata/todo/container_icon_label/dagre/sketch.exp.svg b/e2etests/testdata/todo/container_icon_label/dagre/sketch.exp.svg
index a515d2ca5..469647c61 100644
--- a/e2etests/testdata/todo/container_icon_label/dagre/sketch.exp.svg
+++ b/e2etests/testdata/todo/container_icon_label/dagre/sketch.exp.svg
@@ -39,7 +39,7 @@ width="377" height="800" viewBox="-102 -100 377 800">