Merge pull request #1178 from gavin-ts/grid-gap-keywords
Grid gap keywords
This commit is contained in:
commit
1a81930876
14 changed files with 3805 additions and 45 deletions
|
|
@ -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 🧹
|
||||
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -138,6 +138,9 @@ type Attributes struct {
|
|||
|
||||
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
|
||||
|
|
@ -1603,6 +1606,9 @@ var SimpleReservedKeywords = map[string]struct{}{
|
|||
"left": {},
|
||||
"grid-rows": {},
|
||||
"grid-columns": {},
|
||||
"grid-gap": {},
|
||||
"vertical-gap": {},
|
||||
"horizontal-gap": {},
|
||||
"class": {},
|
||||
"classes": {},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
66
e2etests/testdata/files/grid_gap.d2
vendored
Normal 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
1688
e2etests/testdata/stable/grid_gap/dagre/board.exp.json
generated
vendored
Normal file
File diff suppressed because it is too large
Load diff
102
e2etests/testdata/stable/grid_gap/dagre/sketch.exp.svg
vendored
Normal file
102
e2etests/testdata/stable/grid_gap/dagre/sketch.exp.svg
vendored
Normal file
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
1688
e2etests/testdata/stable/grid_gap/elk/board.exp.json
generated
vendored
Normal file
File diff suppressed because it is too large
Load diff
102
e2etests/testdata/stable/grid_gap/elk/sketch.exp.svg
vendored
Normal file
102
e2etests/testdata/stable/grid_gap/elk/sketch.exp.svg
vendored
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 26 KiB |
16
testdata/d2compiler/TestCompile/grid_gap_negative.exp.json
generated
vendored
Normal file
16
testdata/d2compiler/TestCompile/grid_gap_negative.exp.json
generated
vendored
Normal 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\""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue