Skip to content

Commit b3511d1

Browse files
authored
Merge pull request #1122 from gavin-ts/grid-layouts
new grid layout with rows/columns
2 parents 72ea1b1 + a75d4dd commit b3511d1

37 files changed

+16695
-54
lines changed

ci/release/changelogs/next.md

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#### Features 🚀
22

33
- Multi-board SVG outputs with internal links go to their output paths [#1116](https://github.com/terrastruct/d2/pull/1116)
4+
- New grid layout to place nodes in rows and columns [#1122](https://github.com/terrastruct/d2/pull/1122)
45

56
#### Improvements 🧹
67

d2compiler/compile.go

+47
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ func (c *compiler) compileBoard(g *d2graph.Graph, ir *d2ir.Map) *d2graph.Graph {
7373
c.validateKeys(g.Root, ir)
7474
}
7575
c.validateNear(g)
76+
c.validateEdges(g)
7677

7778
c.compileBoardsField(g, ir, "layers")
7879
c.compileBoardsField(g, ir, "scenarios")
@@ -362,6 +363,32 @@ func (c *compiler) compileReserved(attrs *d2graph.Attributes, f *d2ir.Field) {
362363
}
363364
attrs.Constraint.Value = scalar.ScalarString()
364365
attrs.Constraint.MapKey = f.LastPrimaryKey()
366+
case "grid-rows":
367+
v, err := strconv.Atoi(scalar.ScalarString())
368+
if err != nil {
369+
c.errorf(scalar, "non-integer grid-rows %#v: %s", scalar.ScalarString(), err)
370+
return
371+
}
372+
if v <= 0 {
373+
c.errorf(scalar, "grid-rows must be a positive integer: %#v", scalar.ScalarString())
374+
return
375+
}
376+
attrs.GridRows = &d2graph.Scalar{}
377+
attrs.GridRows.Value = scalar.ScalarString()
378+
attrs.GridRows.MapKey = f.LastPrimaryKey()
379+
case "grid-columns":
380+
v, err := strconv.Atoi(scalar.ScalarString())
381+
if err != nil {
382+
c.errorf(scalar, "non-integer grid-columns %#v: %s", scalar.ScalarString(), err)
383+
return
384+
}
385+
if v <= 0 {
386+
c.errorf(scalar, "grid-columns must be a positive integer: %#v", scalar.ScalarString())
387+
return
388+
}
389+
attrs.GridColumns = &d2graph.Scalar{}
390+
attrs.GridColumns.Value = scalar.ScalarString()
391+
attrs.GridColumns.MapKey = f.LastPrimaryKey()
365392
}
366393

367394
if attrs.Link != nil && attrs.Tooltip != nil {
@@ -678,6 +705,13 @@ func (c *compiler) validateKey(obj *d2graph.Object, f *d2ir.Field) {
678705
if !in && arrowheadIn {
679706
c.errorf(f.LastPrimaryKey(), fmt.Sprintf(`invalid shape, can only set "%s" for arrowheads`, obj.Attributes.Shape.Value))
680707
}
708+
case "grid-rows", "grid-columns":
709+
for _, child := range obj.ChildrenArray {
710+
if child.IsContainer() {
711+
c.errorf(f.LastPrimaryKey(),
712+
fmt.Sprintf(`%#v can only be used on containers with one level of nesting right now. (%#v has nested %#v)`, keyword, child.AbsID(), child.ChildrenArray[0].ID))
713+
}
714+
}
681715
}
682716
return
683717
}
@@ -761,6 +795,19 @@ func (c *compiler) validateNear(g *d2graph.Graph) {
761795
}
762796
}
763797

798+
func (c *compiler) validateEdges(g *d2graph.Graph) {
799+
for _, edge := range g.Edges {
800+
if gd := edge.Src.Parent.ClosestGridDiagram(); gd != nil {
801+
c.errorf(edge.GetAstEdge(), "edges in grid diagrams are not supported yet")
802+
continue
803+
}
804+
if gd := edge.Dst.Parent.ClosestGridDiagram(); gd != nil {
805+
c.errorf(edge.GetAstEdge(), "edges in grid diagrams are not supported yet")
806+
continue
807+
}
808+
}
809+
}
810+
764811
func (c *compiler) validateBoardLinks(g *d2graph.Graph) {
765812
for _, obj := range g.Objects {
766813
if obj.Attributes.Link == nil {

d2compiler/compile_test.go

+50
Original file line numberDiff line numberDiff line change
@@ -2268,6 +2268,56 @@ obj {
22682268
`,
22692269
expErr: `d2/testdata/d2compiler/TestCompile/near_near_const.d2:7:8: near keys cannot be set to an object with a constant near key`,
22702270
},
2271+
{
2272+
name: "grid",
2273+
text: `hey: {
2274+
grid-rows: 200
2275+
grid-columns: 230
2276+
}
2277+
`,
2278+
assertions: func(t *testing.T, g *d2graph.Graph) {
2279+
tassert.Equal(t, "200", g.Objects[0].Attributes.GridRows.Value)
2280+
},
2281+
},
2282+
{
2283+
name: "grid_negative",
2284+
text: `hey: {
2285+
grid-rows: 200
2286+
grid-columns: -200
2287+
}
2288+
`,
2289+
expErr: `d2/testdata/d2compiler/TestCompile/grid_negative.d2:3:16: grid-columns must be a positive integer: "-200"`,
2290+
},
2291+
{
2292+
name: "grid_edge",
2293+
text: `hey: {
2294+
grid-rows: 1
2295+
a -> b
2296+
}
2297+
c -> hey.b
2298+
hey.a -> c
2299+
2300+
hey -> c: ok
2301+
`,
2302+
expErr: `d2/testdata/d2compiler/TestCompile/grid_edge.d2:3:2: edges in grid diagrams are not supported yet
2303+
d2/testdata/d2compiler/TestCompile/grid_edge.d2:5:2: edges in grid diagrams are not supported yet
2304+
d2/testdata/d2compiler/TestCompile/grid_edge.d2:6:2: edges in grid diagrams are not supported yet`,
2305+
},
2306+
{
2307+
name: "grid_nested",
2308+
text: `hey: {
2309+
grid-rows: 200
2310+
grid-columns: 200
2311+
2312+
a
2313+
b
2314+
c
2315+
d.invalid descendant
2316+
}
2317+
`,
2318+
expErr: `d2/testdata/d2compiler/TestCompile/grid_nested.d2:2:2: "grid-rows" can only be used on containers with one level of nesting right now. ("hey.d" has nested "invalid descendant")
2319+
d2/testdata/d2compiler/TestCompile/grid_nested.d2:3:2: "grid-columns" can only be used on containers with one level of nesting right now. ("hey.d" has nested "invalid descendant")`,
2320+
},
22712321
}
22722322

22732323
for _, tc := range testCases {

d2exporter/export_test.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"oss.terrastruct.com/d2/d2compiler"
1717
"oss.terrastruct.com/d2/d2exporter"
1818
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
19+
"oss.terrastruct.com/d2/d2layouts/d2grid"
1920
"oss.terrastruct.com/d2/d2layouts/d2sequence"
2021
"oss.terrastruct.com/d2/d2target"
2122
"oss.terrastruct.com/d2/lib/geo"
@@ -231,7 +232,7 @@ func run(t *testing.T, tc testCase) {
231232
err = g.SetDimensions(nil, ruler, nil)
232233
assert.JSON(t, nil, err)
233234

234-
err = d2sequence.Layout(ctx, g, d2dagrelayout.DefaultLayout)
235+
err = d2sequence.Layout(ctx, g, d2grid.Layout(ctx, g, d2dagrelayout.DefaultLayout))
235236
if err != nil {
236237
t.Fatal(err)
237238
}

d2graph/d2graph.go

+25-13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package d2graph
22

33
import (
4+
"context"
45
"errors"
56
"fmt"
67
"math"
@@ -67,6 +68,8 @@ func (g *Graph) RootBoard() *Graph {
6768
return g
6869
}
6970

71+
type LayoutGraph func(context.Context, *Graph) error
72+
7073
// TODO consider having different Scalar types
7174
// Right now we'll hold any types in Value and just convert, e.g. floats
7275
type Scalar struct {
@@ -129,6 +132,9 @@ type Attributes struct {
129132

130133
Direction Scalar `json:"direction"`
131134
Constraint Scalar `json:"constraint"`
135+
136+
GridRows *Scalar `json:"gridRows,omitempty"`
137+
GridColumns *Scalar `json:"gridColumns,omitempty"`
132138
}
133139

134140
// TODO references at the root scope should have their Scope set to root graph AST
@@ -1007,6 +1013,10 @@ type EdgeReference struct {
10071013
ScopeObj *Object `json:"-"`
10081014
}
10091015

1016+
func (e *Edge) GetAstEdge() *d2ast.Edge {
1017+
return e.References[0].Edge
1018+
}
1019+
10101020
func (e *Edge) GetStroke(dashGapSize interface{}) string {
10111021
if dashGapSize != 0.0 {
10121022
return color.B2
@@ -1521,19 +1531,21 @@ var ReservedKeywords2 map[string]struct{}
15211531

15221532
// Non Style/Holder keywords.
15231533
var SimpleReservedKeywords = map[string]struct{}{
1524-
"label": {},
1525-
"desc": {},
1526-
"shape": {},
1527-
"icon": {},
1528-
"constraint": {},
1529-
"tooltip": {},
1530-
"link": {},
1531-
"near": {},
1532-
"width": {},
1533-
"height": {},
1534-
"direction": {},
1535-
"top": {},
1536-
"left": {},
1534+
"label": {},
1535+
"desc": {},
1536+
"shape": {},
1537+
"icon": {},
1538+
"constraint": {},
1539+
"tooltip": {},
1540+
"link": {},
1541+
"near": {},
1542+
"width": {},
1543+
"height": {},
1544+
"direction": {},
1545+
"top": {},
1546+
"left": {},
1547+
"grid-rows": {},
1548+
"grid-columns": {},
15371549
}
15381550

15391551
// ReservedKeywordHolders are reserved keywords that are meaningless on its own and exist solely to hold a set of reserved keywords

d2graph/grid_diagram.go

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package d2graph
2+
3+
func (obj *Object) IsGridDiagram() bool {
4+
return obj != nil && obj.Attributes != nil &&
5+
(obj.Attributes.GridRows != nil || obj.Attributes.GridColumns != nil)
6+
}
7+
8+
func (obj *Object) ClosestGridDiagram() *Object {
9+
if obj == nil {
10+
return nil
11+
}
12+
if obj.IsGridDiagram() {
13+
return obj
14+
}
15+
return obj.Parent.ClosestGridDiagram()
16+
}

d2layouts/d2grid/grid_diagram.go

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package d2grid
2+
3+
import (
4+
"strconv"
5+
"strings"
6+
7+
"oss.terrastruct.com/d2/d2graph"
8+
)
9+
10+
type gridDiagram struct {
11+
root *d2graph.Object
12+
objects []*d2graph.Object
13+
rows int
14+
columns int
15+
16+
// if true, place objects left to right along rows
17+
// if false, place objects top to bottom along columns
18+
rowDirected bool
19+
20+
width float64
21+
height float64
22+
}
23+
24+
func newGridDiagram(root *d2graph.Object) *gridDiagram {
25+
gd := gridDiagram{root: root, objects: root.ChildrenArray}
26+
if root.Attributes.GridRows != nil {
27+
gd.rows, _ = strconv.Atoi(root.Attributes.GridRows.Value)
28+
}
29+
if root.Attributes.GridColumns != nil {
30+
gd.columns, _ = strconv.Atoi(root.Attributes.GridColumns.Value)
31+
}
32+
33+
if gd.rows != 0 && gd.columns != 0 {
34+
// . row-directed column-directed
35+
// . ┌───────┐ ┌───────┐
36+
// . │ a b c │ │ a d g │
37+
// . │ d e f │ │ b e h │
38+
// . │ g h i │ │ c f i │
39+
// . └───────┘ └───────┘
40+
// if keyword rows is first, make it row-directed, if columns is first it is column-directed
41+
if root.Attributes.GridRows.MapKey.Range.Before(root.Attributes.GridColumns.MapKey.Range) {
42+
gd.rowDirected = true
43+
}
44+
45+
// rows and columns specified, but we want to continue naturally if user enters more objects
46+
// e.g. 2 rows, 3 columns specified + g added: │ with 3 columns, 2 rows:
47+
// . original add row add column │ original add row add column
48+
// . ┌───────┐ ┌───────┐ ┌─────────┐ │ ┌───────┐ ┌───────┐ ┌─────────┐
49+
// . │ a b c │ │ a b c │ │ a b c d │ │ │ a c e │ │ a d g │ │ a c e g │
50+
// . │ d e f │ │ d e f │ │ e f g │ │ │ b d f │ │ b e │ │ b d f │
51+
// . └───────┘ │ g │ └─────────┘ │ └───────┘ │ c f │ └─────────┘
52+
// . └───────┘ ▲ │ └───────┘ ▲
53+
// . ▲ └─existing objects modified│ ▲ └─existing columns preserved
54+
// . └─existing rows preserved │ └─existing objects modified
55+
capacity := gd.rows * gd.columns
56+
for capacity < len(gd.objects) {
57+
if gd.rowDirected {
58+
gd.rows++
59+
capacity += gd.columns
60+
} else {
61+
gd.columns++
62+
capacity += gd.rows
63+
}
64+
}
65+
} else if gd.columns == 0 {
66+
gd.rowDirected = true
67+
}
68+
69+
return &gd
70+
}
71+
72+
func (gd *gridDiagram) shift(dx, dy float64) {
73+
for _, obj := range gd.objects {
74+
obj.TopLeft.X += dx
75+
obj.TopLeft.Y += dy
76+
}
77+
}
78+
79+
func (gd *gridDiagram) cleanup(obj *d2graph.Object, graph *d2graph.Graph) {
80+
obj.Children = make(map[string]*d2graph.Object)
81+
obj.ChildrenArray = make([]*d2graph.Object, 0)
82+
for _, child := range gd.objects {
83+
obj.Children[strings.ToLower(child.ID)] = child
84+
obj.ChildrenArray = append(obj.ChildrenArray, child)
85+
}
86+
graph.Objects = append(graph.Objects, gd.objects...)
87+
}

0 commit comments

Comments
 (0)