Skip to content

Commit

Permalink
Merge pull request #1178 from gavin-ts/grid-gap-keywords
Browse files Browse the repository at this point in the history
Grid gap keywords
  • Loading branch information
gavin-ts authored Apr 12, 2023
2 parents 4f91c99 + c0e164e commit 1a81930
Show file tree
Hide file tree
Showing 14 changed files with 3,805 additions and 45 deletions.
1 change: 1 addition & 0 deletions ci/release/changelogs/next.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#### Features 🚀

- Export diagrams to `.pptx` (PowerPoint)[#1139](https://github.com/terrastruct/d2/pull/1139)
- Customize gap size in grid diagrams with `grid-gap`, `vertical-gap`, or `horizontal-gap` [#1178](https://github.com/terrastruct/d2/issues/1178)

#### Improvements 🧹

Expand Down
41 changes: 40 additions & 1 deletion d2compiler/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,45 @@ func (c *compiler) compileReserved(attrs *d2graph.Attributes, f *d2ir.Field) {
attrs.GridColumns = &d2graph.Scalar{}
attrs.GridColumns.Value = scalar.ScalarString()
attrs.GridColumns.MapKey = f.LastPrimaryKey()
case "grid-gap":
v, err := strconv.Atoi(scalar.ScalarString())
if err != nil {
c.errorf(scalar, "non-integer grid-gap %#v: %s", scalar.ScalarString(), err)
return
}
if v < 0 {
c.errorf(scalar, "grid-gap must be a non-negative integer: %#v", scalar.ScalarString())
return
}
attrs.GridGap = &d2graph.Scalar{}
attrs.GridGap.Value = scalar.ScalarString()
attrs.GridGap.MapKey = f.LastPrimaryKey()
case "vertical-gap":
v, err := strconv.Atoi(scalar.ScalarString())
if err != nil {
c.errorf(scalar, "non-integer vertical-gap %#v: %s", scalar.ScalarString(), err)
return
}
if v < 0 {
c.errorf(scalar, "vertical-gap must be a non-negative integer: %#v", scalar.ScalarString())
return
}
attrs.VerticalGap = &d2graph.Scalar{}
attrs.VerticalGap.Value = scalar.ScalarString()
attrs.VerticalGap.MapKey = f.LastPrimaryKey()
case "horizontal-gap":
v, err := strconv.Atoi(scalar.ScalarString())
if err != nil {
c.errorf(scalar, "non-integer horizontal-gap %#v: %s", scalar.ScalarString(), err)
return
}
if v < 0 {
c.errorf(scalar, "horizontal-gap must be a non-negative integer: %#v", scalar.ScalarString())
return
}
attrs.HorizontalGap = &d2graph.Scalar{}
attrs.HorizontalGap.Value = scalar.ScalarString()
attrs.HorizontalGap.MapKey = f.LastPrimaryKey()
case "class":
attrs.Classes = append(attrs.Classes, scalar.ScalarString())
case "classes":
Expand Down Expand Up @@ -757,7 +796,7 @@ func (c *compiler) validateKey(obj *d2graph.Object, f *d2ir.Field) {
if !in && arrowheadIn {
c.errorf(f.LastPrimaryKey(), fmt.Sprintf(`invalid shape, can only set "%s" for arrowheads`, obj.Attributes.Shape.Value))
}
case "grid-rows", "grid-columns":
case "grid-rows", "grid-columns", "grid-gap", "vertical-gap", "horizontal-gap":
for _, child := range obj.ChildrenArray {
if child.IsContainer() {
c.errorf(f.LastPrimaryKey(),
Expand Down
10 changes: 10 additions & 0 deletions d2compiler/compile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2301,6 +2301,16 @@ obj {
`,
expErr: `d2/testdata/d2compiler/TestCompile/grid_negative.d2:3:16: grid-columns must be a positive integer: "-200"`,
},
{
name: "grid_gap_negative",
text: `hey: {
horizontal-gap: -200
vertical-gap: -30
}
`,
expErr: `d2/testdata/d2compiler/TestCompile/grid_gap_negative.d2:2:18: horizontal-gap must be a non-negative integer: "-200"
d2/testdata/d2compiler/TestCompile/grid_gap_negative.d2:3:16: vertical-gap must be a non-negative integer: "-30"`,
},
{
name: "grid_edge",
text: `hey: {
Expand Down
44 changes: 25 additions & 19 deletions d2graph/d2graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,11 @@ type Attributes struct {
Direction Scalar `json:"direction"`
Constraint Scalar `json:"constraint"`

GridRows *Scalar `json:"gridRows,omitempty"`
GridColumns *Scalar `json:"gridColumns,omitempty"`
GridRows *Scalar `json:"gridRows,omitempty"`
GridColumns *Scalar `json:"gridColumns,omitempty"`
GridGap *Scalar `json:"gridGap,omitempty"`
VerticalGap *Scalar `json:"verticalGap,omitempty"`
HorizontalGap *Scalar `json:"horizontalGap,omitempty"`

// These names are attached to the rendered elements in SVG
// so that users can target them however they like outside of D2
Expand Down Expand Up @@ -1588,23 +1591,26 @@ var ReservedKeywords2 map[string]struct{}

// Non Style/Holder keywords.
var SimpleReservedKeywords = map[string]struct{}{
"label": {},
"desc": {},
"shape": {},
"icon": {},
"constraint": {},
"tooltip": {},
"link": {},
"near": {},
"width": {},
"height": {},
"direction": {},
"top": {},
"left": {},
"grid-rows": {},
"grid-columns": {},
"class": {},
"classes": {},
"label": {},
"desc": {},
"shape": {},
"icon": {},
"constraint": {},
"tooltip": {},
"link": {},
"near": {},
"width": {},
"height": {},
"direction": {},
"top": {},
"left": {},
"grid-rows": {},
"grid-columns": {},
"grid-gap": {},
"vertical-gap": {},
"horizontal-gap": {},
"class": {},
"classes": {},
}

// ReservedKeywordHolders are reserved keywords that are meaningless on its own and exist solely to hold a set of reserved keywords
Expand Down
23 changes: 22 additions & 1 deletion d2layouts/d2grid/grid_diagram.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,19 @@ type gridDiagram struct {

width float64
height float64

verticalGap int
horizontalGap int
}

func newGridDiagram(root *d2graph.Object) *gridDiagram {
gd := gridDiagram{root: root, objects: root.ChildrenArray}
gd := gridDiagram{
root: root,
objects: root.ChildrenArray,
verticalGap: DEFAULT_GAP,
horizontalGap: DEFAULT_GAP,
}

if root.Attributes.GridRows != nil {
gd.rows, _ = strconv.Atoi(root.Attributes.GridRows.Value)
}
Expand Down Expand Up @@ -74,6 +83,18 @@ func newGridDiagram(root *d2graph.Object) *gridDiagram {
}
}

// grid gap sets both, but can be overridden
if root.Attributes.GridGap != nil {
gd.verticalGap, _ = strconv.Atoi(root.Attributes.GridGap.Value)
gd.horizontalGap = gd.verticalGap
}
if root.Attributes.VerticalGap != nil {
gd.verticalGap, _ = strconv.Atoi(root.Attributes.VerticalGap.Value)
}
if root.Attributes.HorizontalGap != nil {
gd.horizontalGap, _ = strconv.Atoi(root.Attributes.HorizontalGap.Value)
}

return &gd
}

Expand Down
53 changes: 29 additions & 24 deletions d2layouts/d2grid/layout.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ import (

const (
CONTAINER_PADDING = 60
HORIZONTAL_PAD = 40.
VERTICAL_PAD = 40.
DEFAULT_GAP = 40
)

// Layout runs the grid layout on containers with rows/columns
Expand Down Expand Up @@ -178,6 +177,9 @@ func (gd *gridDiagram) layoutEvenly(g *d2graph.Graph, obj *d2graph.Object) {
colWidths = append(colWidths, columnWidth)
}

horizontalGap := float64(gd.horizontalGap)
verticalGap := float64(gd.verticalGap)

cursor := geo.NewPoint(0, 0)
if gd.rowDirected {
for i := 0; i < gd.rows; i++ {
Expand All @@ -189,10 +191,10 @@ func (gd *gridDiagram) layoutEvenly(g *d2graph.Graph, obj *d2graph.Object) {
o.Width = colWidths[j]
o.Height = rowHeights[i]
o.TopLeft = cursor.Copy()
cursor.X += o.Width + HORIZONTAL_PAD
cursor.X += o.Width + horizontalGap
}
cursor.X = 0
cursor.Y += rowHeights[i] + VERTICAL_PAD
cursor.Y += rowHeights[i] + verticalGap
}
} else {
for j := 0; j < gd.columns; j++ {
Expand All @@ -204,22 +206,22 @@ func (gd *gridDiagram) layoutEvenly(g *d2graph.Graph, obj *d2graph.Object) {
o.Width = colWidths[j]
o.Height = rowHeights[i]
o.TopLeft = cursor.Copy()
cursor.Y += o.Height + VERTICAL_PAD
cursor.Y += o.Height + verticalGap
}
cursor.X += colWidths[j] + HORIZONTAL_PAD
cursor.X += colWidths[j] + horizontalGap
cursor.Y = 0
}
}

var totalWidth, totalHeight float64
for _, w := range colWidths {
totalWidth += w + HORIZONTAL_PAD
totalWidth += w + horizontalGap
}
for _, h := range rowHeights {
totalHeight += h + VERTICAL_PAD
totalHeight += h + verticalGap
}
totalWidth -= HORIZONTAL_PAD
totalHeight -= VERTICAL_PAD
totalWidth -= horizontalGap
totalHeight -= verticalGap
gd.width = totalWidth
gd.height = totalHeight
}
Expand All @@ -240,14 +242,17 @@ func (gd *gridDiagram) layoutDynamic(g *d2graph.Graph, obj *d2graph.Object) {
// . │ │ ├ ─ ┤ │ │ │ │ │ │
// . └──────────────┘ └───┘ └──────────┘ └─────────┘ └─────────────────┘

horizontalGap := float64(gd.horizontalGap)
verticalGap := float64(gd.verticalGap)

// we want to split up the total width across the N rows or columns as evenly as possible
var totalWidth, totalHeight float64
for _, o := range gd.objects {
totalWidth += o.Width
totalHeight += o.Height
}
totalWidth += HORIZONTAL_PAD * float64(len(gd.objects)-gd.rows)
totalHeight += VERTICAL_PAD * float64(len(gd.objects)-gd.columns)
totalWidth += horizontalGap * float64(len(gd.objects)-gd.rows)
totalHeight += verticalGap * float64(len(gd.objects)-gd.columns)

var layout [][]*d2graph.Object
if gd.rowDirected {
Expand Down Expand Up @@ -278,10 +283,10 @@ func (gd *gridDiagram) layoutDynamic(g *d2graph.Graph, obj *d2graph.Object) {
rowHeight := 0.
for _, o := range row {
o.TopLeft = cursor.Copy()
cursor.X += o.Width + HORIZONTAL_PAD
cursor.X += o.Width + horizontalGap
rowHeight = math.Max(rowHeight, o.Height)
}
rowWidth := cursor.X - HORIZONTAL_PAD
rowWidth := cursor.X - horizontalGap
rowWidths = append(rowWidths, rowWidth)
maxX = math.Max(maxX, rowWidth)

Expand All @@ -292,9 +297,9 @@ func (gd *gridDiagram) layoutDynamic(g *d2graph.Graph, obj *d2graph.Object) {

// new row
cursor.X = 0
cursor.Y += rowHeight + VERTICAL_PAD
cursor.Y += rowHeight + verticalGap
}
maxY = cursor.Y - VERTICAL_PAD
maxY = cursor.Y - horizontalGap

// then expand thinnest objects to make each row the same width
// . ┌A─────────────┐ ┌B──┐ ┌C─────────┐ ┬ maxHeight(A,B,C)
Expand Down Expand Up @@ -372,10 +377,10 @@ func (gd *gridDiagram) layoutDynamic(g *d2graph.Graph, obj *d2graph.Object) {
colWidth := 0.
for _, o := range column {
o.TopLeft = cursor.Copy()
cursor.Y += o.Height + VERTICAL_PAD
cursor.Y += o.Height + verticalGap
colWidth = math.Max(colWidth, o.Width)
}
colHeight := cursor.Y - VERTICAL_PAD
colHeight := cursor.Y - verticalGap
colHeights = append(colHeights, colHeight)
maxY = math.Max(maxY, colHeight)
// set all objects in column to the same width
Expand All @@ -385,9 +390,9 @@ func (gd *gridDiagram) layoutDynamic(g *d2graph.Graph, obj *d2graph.Object) {

// new column
cursor.Y = 0
cursor.X += colWidth + HORIZONTAL_PAD
cursor.X += colWidth + horizontalGap
}
maxX = cursor.X - HORIZONTAL_PAD
maxX = cursor.X - horizontalGap
// then expand shortest objects to make each column the same height
// . ├maxWidth(A,B)─┤ ├maxW(C,D)─┤ ├maxWidth(E)──────┤
// . ┌A─────────────┐ ┌C─────────┐ ┌E────────────────┐
Expand Down Expand Up @@ -479,7 +484,7 @@ func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2gr
// of these divisions, find the layout with rows closest to the targetSize
for _, division := range divisions {
layout := genLayout(gd.objects, division)
dist := getDistToTarget(layout, targetSize, columns)
dist := getDistToTarget(layout, targetSize, float64(gd.horizontalGap), float64(gd.verticalGap), columns)
if dist < bestDist {
bestLayout = layout
bestDist = dist
Expand Down Expand Up @@ -527,15 +532,15 @@ func genLayout(objects []*d2graph.Object, cutIndices []int) [][]*d2graph.Object
return layout
}

func getDistToTarget(layout [][]*d2graph.Object, targetSize float64, columns bool) float64 {
func getDistToTarget(layout [][]*d2graph.Object, targetSize float64, horizontalGap, verticalGap float64, columns bool) float64 {
totalDelta := 0.
for _, row := range layout {
rowSize := 0.
for _, o := range row {
if columns {
rowSize += o.Height + VERTICAL_PAD
rowSize += o.Height + verticalGap
} else {
rowSize += o.Width + HORIZONTAL_PAD
rowSize += o.Width + horizontalGap
}
}
totalDelta += math.Abs(rowSize - targetSize)
Expand Down
15 changes: 15 additions & 0 deletions d2oracle/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,21 @@ func _set(g *d2graph.Graph, key string, tag, value *string) error {
attrs.GridColumns.MapKey.SetScalar(mk.Value.ScalarBox())
return nil
}
case "grid-gap":
if attrs.GridGap != nil && attrs.GridGap.MapKey != nil {
attrs.GridGap.MapKey.SetScalar(mk.Value.ScalarBox())
return nil
}
case "vertical-gap":
if attrs.VerticalGap != nil && attrs.VerticalGap.MapKey != nil {
attrs.VerticalGap.MapKey.SetScalar(mk.Value.ScalarBox())
return nil
}
case "horizontal-gap":
if attrs.HorizontalGap != nil && attrs.HorizontalGap.MapKey != nil {
attrs.HorizontalGap.MapKey.SetScalar(mk.Value.ScalarBox())
return nil
}
case "source-arrowhead", "target-arrowhead":
if reservedKey == "source-arrowhead" {
attrs = edge.SrcArrowhead
Expand Down
1 change: 1 addition & 0 deletions e2etests/stable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2572,6 +2572,7 @@ scenarios: {
loadFromFile(t, "grid_tests"),
loadFromFile(t, "executive_grid"),
loadFromFile(t, "grid_animated"),
loadFromFile(t, "grid_gap"),
}

runa(t, tcs)
Expand Down
Loading

0 comments on commit 1a81930

Please sign in to comment.