diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md
index 406e245d5..22e973a91 100644
--- a/ci/release/changelogs/next.md
+++ b/ci/release/changelogs/next.md
@@ -11,6 +11,7 @@
- All vars defined in a scope are accessible everywhere in that scope, i.e., an object can use a var defined after itself. [#1695](https://github.com/terrastruct/d2/pull/1695)
- Encoding API switches to standard zlib encoding so that decoding doesn't depend on source. [#1709](https://github.com/terrastruct/d2/pull/1709)
- `currentcolor` is accepted as a color option to inherit parent colors. (ty @hboomsma) [#1700](https://github.com/terrastruct/d2/pull/1700)
+- grid containers can now be sized with `width`/`height` even when using a layout plugin without that feature. [#1731](https://github.com/terrastruct/d2/pull/1731)
#### Bugfixes ⛑️
diff --git a/d2layouts/d2grid/layout.go b/d2layouts/d2grid/layout.go
index f0956d053..fc9b4a91e 100644
--- a/d2layouts/d2grid/layout.go
+++ b/d2layouts/d2grid/layout.go
@@ -5,6 +5,7 @@ import (
"context"
"fmt"
"math"
+ "strconv"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2target"
@@ -47,15 +48,7 @@ func Layout(ctx context.Context, g *d2graph.Graph) error {
verticalPadding = gd.verticalGap
}
- // size shape according to grid
- obj.SizeToContent(gd.width, gd.height, float64(2*horizontalPadding), float64(2*verticalPadding))
-
- // compute where the grid should be placed inside shape
- s := obj.ToShape()
- innerBox := s.GetInnerBox()
- if innerBox.TopLeft.X != 0 || innerBox.TopLeft.Y != 0 {
- gd.shift(innerBox.TopLeft.X, innerBox.TopLeft.Y)
- }
+ contentWidth, contentHeight := gd.width, gd.height
var labelPosition, iconPosition label.Position
if obj.LabelPosition != nil {
@@ -83,7 +76,7 @@ func Layout(ctx context.Context, g *d2graph.Graph) error {
label.InsideTopLeft, label.InsideTopCenter, label.InsideTopRight,
label.InsideBottomLeft, label.InsideBottomCenter, label.InsideBottomRight,
label.OutsideBottomLeft, label.OutsideBottomCenter, label.OutsideBottomRight:
- overflow := labelWidth - (obj.Width - float64(2*horizontalPadding))
+ overflow := labelWidth - contentWidth
if overflow > 0 {
padding.Left += overflow / 2
padding.Right += overflow / 2
@@ -95,7 +88,7 @@ func Layout(ctx context.Context, g *d2graph.Graph) error {
case label.OutsideLeftTop, label.OutsideLeftMiddle, label.OutsideLeftBottom,
label.InsideMiddleLeft, label.InsideMiddleCenter, label.InsideMiddleRight,
label.OutsideRightTop, label.OutsideRightMiddle, label.OutsideRightBottom:
- overflow := labelHeight - (obj.Height - float64(2*verticalPadding))
+ overflow := labelHeight - contentHeight
if overflow > 0 {
padding.Top += overflow / 2
padding.Bottom += overflow / 2
@@ -112,7 +105,7 @@ func Layout(ctx context.Context, g *d2graph.Graph) error {
padding.Left = math.Max(padding.Left, iconSize)
padding.Right = math.Max(padding.Right, iconSize)
minWidth := 2*iconSize + float64(obj.LabelDimensions.Width) + 2*label.PADDING
- overflow := minWidth - (obj.Width - float64(2*horizontalPadding))
+ overflow := minWidth - contentWidth
if overflow > 0 {
padding.Left = math.Max(padding.Left, overflow/2)
padding.Right = math.Max(padding.Right, overflow/2)
@@ -121,24 +114,65 @@ func Layout(ctx context.Context, g *d2graph.Graph) error {
overflowTop := padding.Top - float64(verticalPadding)
if overflowTop > 0 {
- obj.Height += overflowTop
+ contentHeight += overflowTop
dy += overflowTop
}
overflowBottom := padding.Bottom - float64(verticalPadding)
if overflowBottom > 0 {
- obj.Height += overflowBottom
+ contentHeight += overflowBottom
}
overflowLeft := padding.Left - float64(horizontalPadding)
if overflowLeft > 0 {
- obj.Width += overflowLeft
+ contentWidth += overflowLeft
dx += overflowLeft
}
overflowRight := padding.Right - float64(horizontalPadding)
if overflowRight > 0 {
- obj.Width += overflowRight
+ contentWidth += overflowRight
}
- // we need to center children if we have to expand to fit the container label
+ // manually handle desiredWidth/Height so we can center the grid
+ var desiredWidth, desiredHeight int
+ var originalWidthAttr, originalHeightAttr *d2graph.Scalar
+ if obj.WidthAttr != nil {
+ desiredWidth, _ = strconv.Atoi(obj.WidthAttr.Value)
+ // SizeToContent without desired width
+ originalWidthAttr = obj.WidthAttr
+ obj.WidthAttr = nil
+ }
+ if obj.HeightAttr != nil {
+ desiredHeight, _ = strconv.Atoi(obj.HeightAttr.Value)
+ originalHeightAttr = obj.HeightAttr
+ obj.HeightAttr = nil
+ }
+ // size shape according to grid
+ obj.SizeToContent(contentWidth, contentHeight, float64(2*horizontalPadding), float64(2*verticalPadding))
+ if originalWidthAttr != nil {
+ obj.WidthAttr = originalWidthAttr
+ }
+ if originalHeightAttr != nil {
+ obj.HeightAttr = originalHeightAttr
+ }
+
+ if desiredWidth > 0 {
+ ddx := float64(desiredWidth) - obj.Width
+ if ddx > 0 {
+ dx += ddx / 2
+ obj.Width = float64(desiredWidth)
+ }
+ }
+ if desiredHeight > 0 {
+ ddy := float64(desiredHeight) - obj.Height
+ if ddy > 0 {
+ dy += ddy / 2
+ obj.Height = float64(desiredHeight)
+ }
+ }
+
+ // compute where the grid should be placed inside shape
+ innerBox := obj.ToShape().GetInnerBox()
+ dx = innerBox.TopLeft.X + dx
+ dy = innerBox.TopLeft.Y + dy
if dx != 0 || dy != 0 {
gd.shift(dx, dy)
}
diff --git a/d2plugin/plugin_features.go b/d2plugin/plugin_features.go
index 5ca6aef51..b4331234b 100644
--- a/d2plugin/plugin_features.go
+++ b/d2plugin/plugin_features.go
@@ -38,7 +38,8 @@ func FeatureSupportCheck(info *PluginInfo, g *d2graph.Graph) error {
return fmt.Errorf(`Object "%s" has attribute "top" and/or "left" set, but layout engine "%s" does not support locked positions. See https://d2lang.com/tour/layouts/#layout-specific-functionality for more.`, obj.AbsID(), info.Name)
}
}
- if (obj.WidthAttr != nil || obj.HeightAttr != nil) && len(obj.ChildrenArray) > 0 {
+ if (obj.WidthAttr != nil || obj.HeightAttr != nil) &&
+ len(obj.ChildrenArray) > 0 && !obj.IsGridDiagram() {
if _, ok := featureMap[CONTAINER_DIMENSIONS]; !ok {
return fmt.Errorf(`Object "%s" has attribute "width" and/or "height" set, but layout engine "%s" does not support dimensions set on containers. See https://d2lang.com/tour/layouts/#layout-specific-functionality for more.`, obj.AbsID(), info.Name)
}
diff --git a/e2etests/stable_test.go b/e2etests/stable_test.go
index 88d4e3b9d..59a58208d 100644
--- a/e2etests/stable_test.go
+++ b/e2etests/stable_test.go
@@ -2869,6 +2869,7 @@ y: profits {
loadFromFile(t, "grid_edge_across_cell"),
loadFromFile(t, "nesting_power"),
loadFromFile(t, "unfilled_triangle"),
+ loadFromFile(t, "grid_container_dimensions"),
loadFromFile(t, "grid_label_positions"),
}
diff --git a/e2etests/testdata/files/grid_container_dimensions.d2 b/e2etests/testdata/files/grid_container_dimensions.d2
new file mode 100644
index 000000000..a09be08b3
--- /dev/null
+++ b/e2etests/testdata/files/grid_container_dimensions.d2
@@ -0,0 +1,11 @@
+grid: {
+ width: 200
+ height: 200
+ grid-gap: 0
+ grid-rows: 2
+ grid-columns: 2
+ a
+ b
+ c
+ d
+}
diff --git a/e2etests/testdata/stable/grid_container_dimensions/dagre/board.exp.json b/e2etests/testdata/stable/grid_container_dimensions/dagre/board.exp.json
new file mode 100644
index 000000000..a55ca4731
--- /dev/null
+++ b/e2etests/testdata/stable/grid_container_dimensions/dagre/board.exp.json
@@ -0,0 +1,253 @@
+{
+ "name": "",
+ "isFolderOnly": false,
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "grid",
+ "type": "rectangle",
+ "pos": {
+ "x": 0,
+ "y": 0
+ },
+ "width": 200,
+ "height": 200,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B4",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "grid",
+ "fontSize": 28,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 44,
+ "labelHeight": 36,
+ "labelPosition": "INSIDE_TOP_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "grid.a",
+ "type": "rectangle",
+ "pos": {
+ "x": 46,
+ "y": 57
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "a",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 8,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "grid.b",
+ "type": "rectangle",
+ "pos": {
+ "x": 99,
+ "y": 57
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "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": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 8,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "grid.c",
+ "type": "rectangle",
+ "pos": {
+ "x": 46,
+ "y": 123
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "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": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 8,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "grid.d",
+ "type": "rectangle",
+ "pos": {
+ "x": 99,
+ "y": 123
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "d",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 9,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 2
+ }
+ ],
+ "connections": [],
+ "root": {
+ "id": "",
+ "type": "",
+ "pos": {
+ "x": 0,
+ "y": 0
+ },
+ "width": 0,
+ "height": 0,
+ "opacity": 0,
+ "strokeDash": 0,
+ "strokeWidth": 0,
+ "borderRadius": 0,
+ "fill": "N7",
+ "stroke": "",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "",
+ "fontSize": 0,
+ "fontFamily": "",
+ "language": "",
+ "color": "",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "zIndex": 0,
+ "level": 0
+ }
+}
diff --git a/e2etests/testdata/stable/grid_container_dimensions/dagre/sketch.exp.svg b/e2etests/testdata/stable/grid_container_dimensions/dagre/sketch.exp.svg
new file mode 100644
index 000000000..f39bcbbce
--- /dev/null
+++ b/e2etests/testdata/stable/grid_container_dimensions/dagre/sketch.exp.svg
@@ -0,0 +1,106 @@
+
\ No newline at end of file
diff --git a/e2etests/testdata/stable/grid_container_dimensions/elk/board.exp.json b/e2etests/testdata/stable/grid_container_dimensions/elk/board.exp.json
new file mode 100644
index 000000000..f9a8e2442
--- /dev/null
+++ b/e2etests/testdata/stable/grid_container_dimensions/elk/board.exp.json
@@ -0,0 +1,253 @@
+{
+ "name": "",
+ "isFolderOnly": false,
+ "fontFamily": "SourceSansPro",
+ "shapes": [
+ {
+ "id": "grid",
+ "type": "rectangle",
+ "pos": {
+ "x": 12,
+ "y": 12
+ },
+ "width": 200,
+ "height": 200,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B4",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "grid",
+ "fontSize": 28,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 44,
+ "labelHeight": 36,
+ "labelPosition": "INSIDE_TOP_CENTER",
+ "zIndex": 0,
+ "level": 1
+ },
+ {
+ "id": "grid.a",
+ "type": "rectangle",
+ "pos": {
+ "x": 58,
+ "y": 69
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "a",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 8,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "grid.b",
+ "type": "rectangle",
+ "pos": {
+ "x": 111,
+ "y": 69
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "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": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 8,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "grid.c",
+ "type": "rectangle",
+ "pos": {
+ "x": 58,
+ "y": 135
+ },
+ "width": 53,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "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": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 8,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 2
+ },
+ {
+ "id": "grid.d",
+ "type": "rectangle",
+ "pos": {
+ "x": 111,
+ "y": 135
+ },
+ "width": 54,
+ "height": 66,
+ "opacity": 1,
+ "strokeDash": 0,
+ "strokeWidth": 2,
+ "borderRadius": 0,
+ "fill": "B5",
+ "stroke": "B1",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "d",
+ "fontSize": 16,
+ "fontFamily": "DEFAULT",
+ "language": "",
+ "color": "N1",
+ "italic": false,
+ "bold": true,
+ "underline": false,
+ "labelWidth": 9,
+ "labelHeight": 21,
+ "labelPosition": "INSIDE_MIDDLE_CENTER",
+ "zIndex": 0,
+ "level": 2
+ }
+ ],
+ "connections": [],
+ "root": {
+ "id": "",
+ "type": "",
+ "pos": {
+ "x": 0,
+ "y": 0
+ },
+ "width": 0,
+ "height": 0,
+ "opacity": 0,
+ "strokeDash": 0,
+ "strokeWidth": 0,
+ "borderRadius": 0,
+ "fill": "N7",
+ "stroke": "",
+ "shadow": false,
+ "3d": false,
+ "multiple": false,
+ "double-border": false,
+ "tooltip": "",
+ "link": "",
+ "icon": null,
+ "iconPosition": "",
+ "blend": false,
+ "fields": null,
+ "methods": null,
+ "columns": null,
+ "label": "",
+ "fontSize": 0,
+ "fontFamily": "",
+ "language": "",
+ "color": "",
+ "italic": false,
+ "bold": false,
+ "underline": false,
+ "labelWidth": 0,
+ "labelHeight": 0,
+ "zIndex": 0,
+ "level": 0
+ }
+}
diff --git a/e2etests/testdata/stable/grid_container_dimensions/elk/sketch.exp.svg b/e2etests/testdata/stable/grid_container_dimensions/elk/sketch.exp.svg
new file mode 100644
index 000000000..4f85d9a04
--- /dev/null
+++ b/e2etests/testdata/stable/grid_container_dimensions/elk/sketch.exp.svg
@@ -0,0 +1,106 @@
+gridabcd
+
+
+
+
+
+
+
\ No newline at end of file