Merge pull request #1178 from gavin-ts/grid-gap-keywords

Grid gap keywords
This commit is contained in:
gavin-ts 2023-04-12 15:23:52 -07:00 committed by GitHub
commit 1a81930876
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 3805 additions and 45 deletions

View file

@ -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 🧹

View file

@ -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":
@ -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(),

View file

@ -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: {

View file

@ -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
@ -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

View file

@ -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)
}
@ -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
}

View file

@ -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
@ -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++ {
@ -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++ {
@ -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
}
@ -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 {
@ -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)
@ -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)
@ -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
@ -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────────────────┐
@ -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
@ -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)

View file

@ -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

View file

@ -2572,6 +2572,7 @@ scenarios: {
loadFromFile(t, "grid_tests"),
loadFromFile(t, "executive_grid"),
loadFromFile(t, "grid_animated"),
loadFromFile(t, "grid_gap"),
}
runa(t, tcs)

66
e2etests/testdata/files/grid_gap.d2 vendored Normal file
View file

@ -0,0 +1,66 @@
vertical-gap 30 horizontal-gap 100: {
grid-rows: 3
grid-columns: 3
vertical-gap: 30
horizontal-gap: 100
a
b
c
d
e
f
g
h
i
}
vertical-gap 100 horizontal-gap 30: {
grid-rows: 3
grid-columns: 3
vertical-gap: 100
horizontal-gap: 30
a
b
c
d
e
f
g
h
i
}
gap 0: {
grid-rows: 3
grid-columns: 3
grid-gap: 0
a
b
c
d
e
f
g
h
i
}
gap 10 vertical-gap 100: {
grid-rows: 3
grid-columns: 3
grid-gap: 10
vertical-gap: 100
a
b
c
d
e
f
g
h
i
}

1688
e2etests/testdata/stable/grid_gap/dagre/board.exp.json generated vendored Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 26 KiB

1688
e2etests/testdata/stable/grid_gap/elk/board.exp.json generated vendored Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 26 KiB

View file

@ -0,0 +1,16 @@
{
"graph": null,
"err": {
"ioerr": null,
"errs": [
{
"range": "d2/testdata/d2compiler/TestCompile/grid_gap_negative.d2,1:17:24-1:21:28",
"errmsg": "d2/testdata/d2compiler/TestCompile/grid_gap_negative.d2:2:18: horizontal-gap must be a non-negative integer: \"-200\""
},
{
"range": "d2/testdata/d2compiler/TestCompile/grid_gap_negative.d2,2:15:44-2:18:47",
"errmsg": "d2/testdata/d2compiler/TestCompile/grid_gap_negative.d2:3:16: vertical-gap must be a non-negative integer: \"-30\""
}
]
}
}