Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new grid layout with rows/columns #1122

Merged
merged 31 commits into from
Apr 7, 2023
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
5b22382
add teleport_grid test
gavin-ts Apr 1, 2023
d139eea
layout with grids
gavin-ts Apr 1, 2023
67ca089
update test
gavin-ts Apr 1, 2023
f8617d1
refactor
gavin-ts Apr 3, 2023
abe27cd
cleanup
gavin-ts Apr 3, 2023
06fb313
values must be positive
gavin-ts Apr 3, 2023
acd0638
evenly size grid nodes
gavin-ts Apr 3, 2023
13a8ca0
update test
gavin-ts Apr 3, 2023
0ed4bfe
add dagger_grid test
gavin-ts Apr 4, 2023
c958269
more dynamic grid sizing according to node sizes
gavin-ts Apr 4, 2023
bb090be
update tests
gavin-ts Apr 4, 2023
a169247
finish column logic
gavin-ts Apr 4, 2023
7ac7214
fixing rows creation
gavin-ts Apr 4, 2023
a9a7d23
update test
gavin-ts Apr 4, 2023
bcc4ce1
update test
gavin-ts Apr 4, 2023
d7fd7d7
update test
gavin-ts Apr 4, 2023
bce774f
validate edges
gavin-ts Apr 5, 2023
ac0845d
center container if growing to fit label
gavin-ts Apr 5, 2023
108face
layout evenly with rows and columns
gavin-ts Apr 5, 2023
69ceb5b
add grid_tests test
gavin-ts Apr 5, 2023
18e7288
add executive_grid test
gavin-ts Apr 5, 2023
0dc6a80
update test
gavin-ts Apr 5, 2023
04775c8
validate descendants
gavin-ts Apr 5, 2023
44f2d7a
update grid_tests
gavin-ts Apr 5, 2023
06a942c
rename to grid diagram
gavin-ts Apr 5, 2023
292ac05
changelog
gavin-ts Apr 5, 2023
8eb99a4
new method for placing nodes across rows
gavin-ts Apr 5, 2023
8b3ba86
update test
gavin-ts Apr 6, 2023
bab54b4
cleanup
gavin-ts Apr 6, 2023
37fc3ea
cleanup
gavin-ts Apr 6, 2023
a75d4dd
update keywords rows -> grid-rows
gavin-ts Apr 6, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 🚀

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

#### Improvements 🧹

Expand Down
46 changes: 46 additions & 0 deletions d2compiler/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ func (c *compiler) compileBoard(g *d2graph.Graph, ir *d2ir.Map) *d2graph.Graph {
c.validateKeys(g.Root, ir)
}
c.validateNear(g)
c.validateEdges(g)

c.compileBoardsField(g, ir, "layers")
c.compileBoardsField(g, ir, "scenarios")
Expand Down Expand Up @@ -362,6 +363,32 @@ func (c *compiler) compileReserved(attrs *d2graph.Attributes, f *d2ir.Field) {
}
attrs.Constraint.Value = scalar.ScalarString()
attrs.Constraint.MapKey = f.LastPrimaryKey()
case "rows":
v, err := strconv.Atoi(scalar.ScalarString())
if err != nil {
c.errorf(scalar, "non-integer rows %#v: %s", scalar.ScalarString(), err)
return
}
if v <= 0 {
c.errorf(scalar, "rows must be a positive integer: %#v", scalar.ScalarString())
return
}
attrs.Rows = &d2graph.Scalar{}
attrs.Rows.Value = scalar.ScalarString()
attrs.Rows.MapKey = f.LastPrimaryKey()
case "columns":
v, err := strconv.Atoi(scalar.ScalarString())
if err != nil {
c.errorf(scalar, "non-integer columns %#v: %s", scalar.ScalarString(), err)
return
}
if v <= 0 {
c.errorf(scalar, "columns must be a positive integer: %#v", scalar.ScalarString())
return
}
attrs.Columns = &d2graph.Scalar{}
attrs.Columns.Value = scalar.ScalarString()
attrs.Columns.MapKey = f.LastPrimaryKey()
}

if attrs.Link != nil && attrs.Tooltip != nil {
Expand Down Expand Up @@ -678,6 +705,12 @@ 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 "rows", "columns":
for _, child := range obj.ChildrenArray {
if child.IsContainer() {
c.errorf(f.LastPrimaryKey(), fmt.Sprintf(`invalid grid diagram %#v. can only set %#v with no descendants (see %#v)`, obj.AbsID(), keyword, child.ChildrenArray[0].AbsID()))
gavin-ts marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
return
}
Expand Down Expand Up @@ -761,6 +794,19 @@ func (c *compiler) validateNear(g *d2graph.Graph) {
}
}

func (c *compiler) validateEdges(g *d2graph.Graph) {
for _, edge := range g.Edges {
if gd := edge.Src.ClosestGridDiagram(); gd != nil {
c.errorf(edge.GetAstEdge(), "edge %#v cannot enter grid diagram %#v", d2format.Format(edge.GetAstEdge()), gd.AbsID())
gavin-ts marked this conversation as resolved.
Show resolved Hide resolved
continue
}
if gd := edge.Dst.ClosestGridDiagram(); gd != nil {
c.errorf(edge.GetAstEdge(), "edge %#v cannot enter grid diagram %#v", d2format.Format(edge.GetAstEdge()), gd.AbsID())
continue
}
}
}

func (c *compiler) validateBoardLinks(g *d2graph.Graph) {
for _, obj := range g.Objects {
if obj.Attributes.Link == nil {
Expand Down
50 changes: 50 additions & 0 deletions d2compiler/compile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2268,6 +2268,56 @@ obj {
`,
expErr: `d2/testdata/d2compiler/TestCompile/near_near_const.d2:7:8: near keys cannot be set to an object with a constant near key`,
},
{
name: "grid",
text: `hey: {
rows: 200
columns: 230
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
tassert.Equal(t, "200", g.Objects[0].Attributes.Rows.Value)
},
},
{
name: "grid_negative",
text: `hey: {
rows: 200
columns: -200
}
`,
expErr: `d2/testdata/d2compiler/TestCompile/grid_negative.d2:3:11: columns must be a positive integer: "-200"`,
},
{
name: "grid_edge",
text: `hey: {
rows: 1
a -> b
}
c -> hey.b
hey.a -> c

hey -> c: ok
`,
expErr: `d2/testdata/d2compiler/TestCompile/grid_edge.d2:3:2: edge "a -> b" cannot enter grid diagram "hey"
d2/testdata/d2compiler/TestCompile/grid_edge.d2:5:2: edge "c -> hey.b" cannot enter grid diagram "hey"
d2/testdata/d2compiler/TestCompile/grid_edge.d2:6:2: edge "hey.a -> c" cannot enter grid diagram "hey"`,
},
{
name: "grid_nested",
text: `hey: {
rows: 200
columns: 200

a
b
c
d.invalid descendant
}
`,
expErr: `d2/testdata/d2compiler/TestCompile/grid_nested.d2:2:2: invalid grid diagram "hey". can only set "rows" with no descendants (see "hey.d.invalid descendant")
d2/testdata/d2compiler/TestCompile/grid_nested.d2:3:2: invalid grid diagram "hey". can only set "columns" with no descendants (see "hey.d.invalid descendant")`,
},
}

for _, tc := range testCases {
Expand Down
3 changes: 2 additions & 1 deletion d2exporter/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"oss.terrastruct.com/d2/d2compiler"
"oss.terrastruct.com/d2/d2exporter"
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
"oss.terrastruct.com/d2/d2layouts/d2grid"
"oss.terrastruct.com/d2/d2layouts/d2sequence"
"oss.terrastruct.com/d2/d2target"
"oss.terrastruct.com/d2/lib/geo"
Expand Down Expand Up @@ -231,7 +232,7 @@ func run(t *testing.T, tc testCase) {
err = g.SetDimensions(nil, ruler, nil)
assert.JSON(t, nil, err)

err = d2sequence.Layout(ctx, g, d2dagrelayout.DefaultLayout)
err = d2sequence.Layout(ctx, g, d2grid.Layout(ctx, g, d2dagrelayout.DefaultLayout))
if err != nil {
t.Fatal(err)
}
Expand Down
12 changes: 12 additions & 0 deletions d2graph/d2graph.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package d2graph

import (
"context"
"errors"
"fmt"
"math"
Expand Down Expand Up @@ -67,6 +68,8 @@ func (g *Graph) RootBoard() *Graph {
return g
}

type LayoutGraph func(context.Context, *Graph) error

// TODO consider having different Scalar types
// Right now we'll hold any types in Value and just convert, e.g. floats
type Scalar struct {
Expand Down Expand Up @@ -129,6 +132,9 @@ type Attributes struct {

Direction Scalar `json:"direction"`
Constraint Scalar `json:"constraint"`

Rows *Scalar `json:"rows,omitempty"`
Columns *Scalar `json:"columns,omitempty"`
}

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

func (e *Edge) GetAstEdge() *d2ast.Edge {
return e.References[0].Edge
}

func (e *Edge) GetStroke(dashGapSize interface{}) string {
if dashGapSize != 0.0 {
return color.B2
Expand Down Expand Up @@ -1534,6 +1544,8 @@ var SimpleReservedKeywords = map[string]struct{}{
"direction": {},
"top": {},
"left": {},
"rows": {},
"columns": {},
}

// ReservedKeywordHolders are reserved keywords that are meaningless on its own and exist solely to hold a set of reserved keywords
Expand Down
16 changes: 16 additions & 0 deletions d2graph/grid_diagram.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package d2graph

func (obj *Object) IsGridDiagram() bool {
return obj != nil && obj.Attributes != nil &&
(obj.Attributes.Rows != nil || obj.Attributes.Columns != nil)
}

func (obj *Object) ClosestGridDiagram() *Object {
if obj.Parent == nil {
return nil
}
if obj.Parent.IsGridDiagram() {
return obj.Parent
}
return obj.Parent.ClosestGridDiagram()
gavin-ts marked this conversation as resolved.
Show resolved Hide resolved
}
81 changes: 81 additions & 0 deletions d2layouts/d2grid/grid_diagram.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package d2grid

import (
"strconv"

"oss.terrastruct.com/d2/d2graph"
)

type gridDiagram struct {
root *d2graph.Object
nodes []*d2graph.Object
gavin-ts marked this conversation as resolved.
Show resolved Hide resolved
rows int
columns int

rowDominant bool
gavin-ts marked this conversation as resolved.
Show resolved Hide resolved

width float64
height float64
}

func newGridDiagram(root *d2graph.Object) *gridDiagram {
gd := gridDiagram{root: root, nodes: root.ChildrenArray}
if root.Attributes.Rows != nil {
gd.rows, _ = strconv.Atoi(root.Attributes.Rows.Value)
}
if root.Attributes.Columns != nil {
gd.columns, _ = strconv.Atoi(root.Attributes.Columns.Value)
}

// compute exact row/column count based on values entered
if gd.columns == 0 {
gd.rowDominant = true
} else if gd.rows == 0 {
gd.rowDominant = false
gavin-ts marked this conversation as resolved.
Show resolved Hide resolved
} else {
// if keyword rows is first, rows are primary, columns secondary.
if root.Attributes.Rows.MapKey.Range.Before(root.Attributes.Columns.MapKey.Range) {
gd.rowDominant = true
}

// rows and columns specified, but we want to continue naturally if user enters more nodes
// e.g. 2 rows, 3 columns specified + g node added: │ with 3 columns, 2 rows:
// . original add row add column │ original add row add column
// . ┌───────┐ ┌───────┐ ┌─────────┐ │ ┌───────┐ ┌───────┐ ┌─────────┐
// . │ a b c │ │ a b c │ │ a b c d │ │ │ a c e │ │ a d g │ │ a c e g │
// . │ d e f │ │ d e f │ │ e f g │ │ │ b d f │ │ b e │ │ b d f │
// . └───────┘ │ g │ └─────────┘ │ └───────┘ │ c f │ └─────────┘
// . └───────┘ ▲ │ └───────┘ ▲
// . ▲ └─existing nodes modified │ ▲ └─existing nodes preserved
// . └─existing rows preserved │ └─existing rows modified
capacity := gd.rows * gd.columns
for capacity < len(gd.nodes) {
alixander marked this conversation as resolved.
Show resolved Hide resolved
if gd.rowDominant {
gd.rows++
capacity += gd.columns
} else {
gd.columns++
capacity += gd.rows
}
}
}

return &gd
}

func (gd *gridDiagram) shift(dx, dy float64) {
for _, obj := range gd.nodes {
obj.TopLeft.X += dx
obj.TopLeft.Y += dy
}
}

func (gd *gridDiagram) cleanup(obj *d2graph.Object, graph *d2graph.Graph) {
obj.Children = make(map[string]*d2graph.Object)
obj.ChildrenArray = make([]*d2graph.Object, 0)
for _, child := range gd.nodes {
obj.Children[child.ID] = child
gavin-ts marked this conversation as resolved.
Show resolved Hide resolved
obj.ChildrenArray = append(obj.ChildrenArray, child)
}
graph.Objects = append(graph.Objects, gd.nodes...)
}
Loading