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 all 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
47 changes: 47 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 "grid-rows":
v, err := strconv.Atoi(scalar.ScalarString())
if err != nil {
c.errorf(scalar, "non-integer grid-rows %#v: %s", scalar.ScalarString(), err)
return
}
if v <= 0 {
c.errorf(scalar, "grid-rows must be a positive integer: %#v", scalar.ScalarString())
return
}
attrs.GridRows = &d2graph.Scalar{}
attrs.GridRows.Value = scalar.ScalarString()
attrs.GridRows.MapKey = f.LastPrimaryKey()
case "grid-columns":
v, err := strconv.Atoi(scalar.ScalarString())
if err != nil {
c.errorf(scalar, "non-integer grid-columns %#v: %s", scalar.ScalarString(), err)
return
}
if v <= 0 {
c.errorf(scalar, "grid-columns must be a positive integer: %#v", scalar.ScalarString())
return
}
attrs.GridColumns = &d2graph.Scalar{}
attrs.GridColumns.Value = scalar.ScalarString()
attrs.GridColumns.MapKey = f.LastPrimaryKey()
}

if attrs.Link != nil && attrs.Tooltip != nil {
Expand Down Expand Up @@ -678,6 +705,13 @@ 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":
for _, child := range obj.ChildrenArray {
if child.IsContainer() {
c.errorf(f.LastPrimaryKey(),
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))
}
}
}
return
}
Expand Down Expand Up @@ -761,6 +795,19 @@ func (c *compiler) validateNear(g *d2graph.Graph) {
}
}

func (c *compiler) validateEdges(g *d2graph.Graph) {
for _, edge := range g.Edges {
if gd := edge.Src.Parent.ClosestGridDiagram(); gd != nil {
c.errorf(edge.GetAstEdge(), "edges in grid diagrams are not supported yet")
continue
}
if gd := edge.Dst.Parent.ClosestGridDiagram(); gd != nil {
c.errorf(edge.GetAstEdge(), "edges in grid diagrams are not supported yet")
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: {
grid-rows: 200
grid-columns: 230
}
`,
assertions: func(t *testing.T, g *d2graph.Graph) {
tassert.Equal(t, "200", g.Objects[0].Attributes.GridRows.Value)
},
},
{
name: "grid_negative",
text: `hey: {
grid-rows: 200
grid-columns: -200
}
`,
expErr: `d2/testdata/d2compiler/TestCompile/grid_negative.d2:3:16: grid-columns must be a positive integer: "-200"`,
},
{
name: "grid_edge",
text: `hey: {
grid-rows: 1
a -> b
}
c -> hey.b
hey.a -> c

hey -> c: ok
`,
expErr: `d2/testdata/d2compiler/TestCompile/grid_edge.d2:3:2: edges in grid diagrams are not supported yet
d2/testdata/d2compiler/TestCompile/grid_edge.d2:5:2: edges in grid diagrams are not supported yet
d2/testdata/d2compiler/TestCompile/grid_edge.d2:6:2: edges in grid diagrams are not supported yet`,
},
{
name: "grid_nested",
text: `hey: {
grid-rows: 200
grid-columns: 200

a
b
c
d.invalid descendant
}
`,
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")
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")`,
},
}

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
38 changes: 25 additions & 13 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"`

GridRows *Scalar `json:"gridRows,omitempty"`
GridColumns *Scalar `json:"gridColumns,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 @@ -1521,19 +1531,21 @@ 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": {},
"label": {},
"desc": {},
"shape": {},
"icon": {},
"constraint": {},
"tooltip": {},
"link": {},
"near": {},
"width": {},
"height": {},
"direction": {},
"top": {},
"left": {},
"grid-rows": {},
"grid-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.GridRows != nil || obj.Attributes.GridColumns != nil)
}

func (obj *Object) ClosestGridDiagram() *Object {
if obj == nil {
return nil
}
if obj.IsGridDiagram() {
return obj
}
return obj.Parent.ClosestGridDiagram()
}
87 changes: 87 additions & 0 deletions d2layouts/d2grid/grid_diagram.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package d2grid

import (
"strconv"
"strings"

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

type gridDiagram struct {
root *d2graph.Object
objects []*d2graph.Object
rows int
columns int

// if true, place objects left to right along rows
// if false, place objects top to bottom along columns
rowDirected bool

width float64
height float64
}

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

if gd.rows != 0 && gd.columns != 0 {
// . row-directed column-directed
// . ┌───────┐ ┌───────┐
// . │ a b c │ │ a d g │
// . │ d e f │ │ b e h │
// . │ g h i │ │ c f i │
// . └───────┘ └───────┘
// if keyword rows is first, make it row-directed, if columns is first it is column-directed
if root.Attributes.GridRows.MapKey.Range.Before(root.Attributes.GridColumns.MapKey.Range) {
gd.rowDirected = true
}

// rows and columns specified, but we want to continue naturally if user enters more objects
// e.g. 2 rows, 3 columns specified + g 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 objects modified│ ▲ └─existing columns preserved
// . └─existing rows preserved │ └─existing objects modified
capacity := gd.rows * gd.columns
for capacity < len(gd.objects) {
if gd.rowDirected {
gd.rows++
capacity += gd.columns
} else {
gd.columns++
capacity += gd.rows
}
}
} else if gd.columns == 0 {
gd.rowDirected = true
}

return &gd
}

func (gd *gridDiagram) shift(dx, dy float64) {
for _, obj := range gd.objects {
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.objects {
obj.Children[strings.ToLower(child.ID)] = child
obj.ChildrenArray = append(obj.ChildrenArray, child)
}
graph.Objects = append(graph.Objects, gd.objects...)
}
Loading