more dynamic grid sizing according to node sizes
This commit is contained in:
parent
0ed4bfe244
commit
c958269b03
5 changed files with 315 additions and 111 deletions
|
|
@ -1,6 +1,6 @@
|
|||
package d2graph
|
||||
|
||||
func (obj *Object) IsGrid() bool {
|
||||
return obj != nil && obj.Attributes != nil && len(obj.ChildrenArray) != 0 &&
|
||||
return obj != nil && obj.Attributes != nil &&
|
||||
(obj.Attributes.Rows != nil || obj.Attributes.Columns != nil)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package d2grid
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
"oss.terrastruct.com/d2/d2graph"
|
||||
|
|
@ -13,6 +12,8 @@ type grid struct {
|
|||
rows int
|
||||
columns int
|
||||
|
||||
rowDominant bool
|
||||
|
||||
cellWidth float64
|
||||
cellHeight float64
|
||||
width float64
|
||||
|
|
@ -29,64 +30,38 @@ func newGrid(root *d2graph.Object) *grid {
|
|||
}
|
||||
|
||||
// compute exact row/column count based on values entered
|
||||
if g.rows == 0 {
|
||||
// set rows based on number of columns
|
||||
g.rows = len(g.nodes) / g.columns
|
||||
if len(g.nodes)%g.columns != 0 {
|
||||
g.rows++
|
||||
}
|
||||
} else if g.columns == 0 {
|
||||
// set columns based on number of rows
|
||||
g.columns = len(g.nodes) / g.rows
|
||||
if len(g.nodes)%g.rows != 0 {
|
||||
g.columns++
|
||||
}
|
||||
if g.columns == 0 {
|
||||
g.rowDominant = true
|
||||
} else if g.rows == 0 {
|
||||
g.rowDominant = false
|
||||
} else {
|
||||
// rows and columns specified (add more rows if needed)
|
||||
// if keyword rows is first, rows are primary, columns secondary.
|
||||
if root.Attributes.Rows.MapKey.Range.Before(root.Attributes.Columns.MapKey.Range) {
|
||||
g.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 := g.rows * g.columns
|
||||
for capacity < len(g.nodes) {
|
||||
g.rows++
|
||||
capacity += g.columns
|
||||
if g.rowDominant {
|
||||
g.rows++
|
||||
capacity += g.columns
|
||||
} else {
|
||||
g.columns++
|
||||
capacity += g.rows
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if we have the following nodes for a 2 row, 3 column grid
|
||||
// . ┌A──────────────────┐ ┌B─────┐ ┌C────────────┐ ┌D───────────┐ ┌E───────────────────┐
|
||||
// . │ │ │ │ │ │ │ │ │ │
|
||||
// . │ │ │ │ │ │ │ │ │ │
|
||||
// . └───────────────────┘ │ │ │ │ │ │ │ │
|
||||
// . │ │ └─────────────┘ │ │ │ │
|
||||
// . │ │ │ │ └────────────────────┘
|
||||
// . └──────┘ │ │
|
||||
// . └────────────┘
|
||||
// Then we must get the max width and max height to determine the grid cell size
|
||||
// . maxWidth├────────────────────┤
|
||||
// . ┌A───────────────────┐ ┌B───────────────────┐ ┌C───────────────────┐ ┬maxHeight
|
||||
// . │ │ │ │ │ │ │
|
||||
// . │ │ │ │ │ │ │
|
||||
// . │ │ │ │ │ │ │
|
||||
// . │ │ │ │ │ │ │
|
||||
// . │ │ │ │ │ │ │
|
||||
// . │ │ │ │ │ │ │
|
||||
// . └────────────────────┘ └────────────────────┘ └────────────────────┘ ┴
|
||||
// . ┌D───────────────────┐ ┌E───────────────────┐
|
||||
// . │ │ │ │
|
||||
// . │ │ │ │
|
||||
// . │ │ │ │
|
||||
// . │ │ │ │
|
||||
// . │ │ │ │
|
||||
// . │ │ │ │
|
||||
// . └────────────────────┘ └────────────────────┘
|
||||
var maxWidth, maxHeight float64
|
||||
for _, n := range g.nodes {
|
||||
maxWidth = math.Max(maxWidth, n.Width)
|
||||
maxHeight = math.Max(maxHeight, n.Height)
|
||||
}
|
||||
g.cellWidth = maxWidth
|
||||
g.cellHeight = maxHeight
|
||||
g.width = maxWidth + (float64(g.columns)-1)*(maxWidth+HORIZONTAL_PAD)
|
||||
g.height = maxHeight + (float64(g.rows)-1)*(maxHeight+VERTICAL_PAD)
|
||||
|
||||
return &g
|
||||
}
|
||||
|
||||
|
|
@ -96,3 +71,13 @@ func (g *grid) shift(dx, dy float64) {
|
|||
obj.TopLeft.Y += dy
|
||||
}
|
||||
}
|
||||
|
||||
func (g *grid) cleanup(obj *d2graph.Object, graph *d2graph.Graph) {
|
||||
obj.Children = make(map[string]*d2graph.Object)
|
||||
obj.ChildrenArray = make([]*d2graph.Object, 0)
|
||||
for _, child := range g.nodes {
|
||||
obj.Children[child.ID] = child
|
||||
obj.ChildrenArray = append(obj.ChildrenArray, child)
|
||||
}
|
||||
graph.Objects = append(graph.Objects, g.nodes...)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package d2grid
|
|||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"sort"
|
||||
|
||||
"oss.terrastruct.com/d2/d2graph"
|
||||
|
|
@ -33,7 +34,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, layout d2graph.LayoutGraph) d
|
|||
return err
|
||||
}
|
||||
|
||||
if g.Root.IsGrid() {
|
||||
if g.Root.IsGrid() && len(g.Root.ChildrenArray) != 0 {
|
||||
g.Root.TopLeft = geo.NewPoint(0, 0)
|
||||
} else if err := layout(ctx, g); err != nil {
|
||||
return err
|
||||
|
|
@ -93,20 +94,207 @@ func withoutGrids(ctx context.Context, g *d2graph.Graph) (idToGrid map[string]*g
|
|||
|
||||
func layoutGrid(g *d2graph.Graph, obj *d2graph.Object) (*grid, error) {
|
||||
grid := newGrid(obj)
|
||||
// assume we have the following nodes to layout:
|
||||
// . ┌A──────────────┐ ┌B──┐ ┌C─────────┐ ┌D────────┐ ┌E────────────────┐
|
||||
// . └───────────────┘ │ │ │ │ │ │ │ │
|
||||
// . │ │ └──────────┘ │ │ │ │
|
||||
// . │ │ │ │ └─────────────────┘
|
||||
// . └───┘ │ │
|
||||
// . └─────────┘
|
||||
// Note: if the grid is row dominant, all nodes should be the same height (same width if column dominant)
|
||||
// . ┌A─────────────┐ ┌B──┐ ┌C─────────┐ ┌D────────┐ ┌E────────────────┐
|
||||
// . ├ ─ ─ ─ ─ ─ ─ ─┤ │ │ │ │ │ │ │ │
|
||||
// . │ │ │ │ ├ ─ ─ ─ ─ ─┤ │ │ │ │
|
||||
// . │ │ │ │ │ │ │ │ ├ ─ ─ ─ ─ ─ ─ ─ ─ ┤
|
||||
// . │ │ ├ ─ ┤ │ │ │ │ │ │
|
||||
// . └──────────────┘ └───┘ └──────────┘ └─────────┘ └─────────────────┘
|
||||
|
||||
// position nodes
|
||||
cursor := geo.NewPoint(0, 0)
|
||||
for i := 0; i < grid.rows; i++ {
|
||||
for j := 0; j < grid.columns; j++ {
|
||||
n := grid.nodes[i*grid.columns+j]
|
||||
n.Width = grid.cellWidth
|
||||
n.Height = grid.cellHeight
|
||||
n.TopLeft = cursor.Copy()
|
||||
cursor.X += grid.cellWidth + HORIZONTAL_PAD
|
||||
}
|
||||
cursor.X = 0
|
||||
cursor.Y += float64(grid.cellHeight) + VERTICAL_PAD
|
||||
// we want to split up the total width across the N rows or columns as evenly as possible
|
||||
var totalWidth, totalHeight float64
|
||||
for _, n := range grid.nodes {
|
||||
totalWidth += n.Width
|
||||
totalHeight += n.Height
|
||||
}
|
||||
totalWidth += HORIZONTAL_PAD * float64(len(grid.nodes)-1)
|
||||
totalHeight += VERTICAL_PAD * float64(len(grid.nodes)-1)
|
||||
|
||||
layout := [][]int{{}}
|
||||
if grid.rowDominant {
|
||||
targetWidth := totalWidth / float64(grid.rows)
|
||||
rowWidth := 0.
|
||||
rowIndex := 0
|
||||
for i, n := range grid.nodes {
|
||||
layout[rowIndex] = append(layout[rowIndex], i)
|
||||
rowWidth += n.Width + HORIZONTAL_PAD
|
||||
// add a new row if we pass the target width and there are more nodes
|
||||
if rowWidth > targetWidth && i < len(grid.nodes)-1 {
|
||||
layout = append(layout, []int{})
|
||||
rowIndex++
|
||||
rowWidth = 0
|
||||
}
|
||||
}
|
||||
} else {
|
||||
targetHeight := totalHeight / float64(grid.columns)
|
||||
columnHeight := 0.
|
||||
columnIndex := 0
|
||||
for i, n := range grid.nodes {
|
||||
layout[columnIndex] = append(layout[columnIndex], i)
|
||||
columnHeight += n.Height + VERTICAL_PAD
|
||||
if columnHeight > targetHeight && i < len(grid.nodes)-1 {
|
||||
layout = append(layout, []int{})
|
||||
columnIndex++
|
||||
columnHeight = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cursor := geo.NewPoint(0, 0)
|
||||
var maxY, maxX float64
|
||||
if grid.rowDominant {
|
||||
// if we have 2 rows, then each row's nodes should have the same height
|
||||
// . ┌A─────────────┐ ┌B──┐ ┌C─────────┐ ┬ maxHeight(A,B,C)
|
||||
// . ├ ─ ─ ─ ─ ─ ─ ─┤ │ │ │ │ │
|
||||
// . │ │ │ │ ├ ─ ─ ─ ─ ─┤ │
|
||||
// . │ │ │ │ │ │ │
|
||||
// . └──────────────┘ └───┘ └──────────┘ ┴
|
||||
// . ┌D────────┐ ┌E────────────────┐ ┬ maxHeight(D,E)
|
||||
// . │ │ │ │ │
|
||||
// . │ │ │ │ │
|
||||
// . │ │ ├ ─ ─ ─ ─ ─ ─ ─ ─ ┤ │
|
||||
// . │ │ │ │ │
|
||||
// . └─────────┘ └─────────────────┘ ┴
|
||||
rowWidths := []float64{}
|
||||
for _, row := range layout {
|
||||
rowHeight := 0.
|
||||
for _, nodeIndex := range row {
|
||||
n := grid.nodes[nodeIndex]
|
||||
n.TopLeft = cursor.Copy()
|
||||
cursor.X += n.Width + HORIZONTAL_PAD
|
||||
rowHeight = math.Max(rowHeight, n.Height)
|
||||
}
|
||||
rowWidth := cursor.X - HORIZONTAL_PAD
|
||||
rowWidths = append(rowWidths, rowWidth)
|
||||
maxX = math.Max(maxX, rowWidth)
|
||||
|
||||
// set all nodes in row to the same height
|
||||
for _, nodeIndex := range row {
|
||||
n := grid.nodes[nodeIndex]
|
||||
n.Height = rowHeight
|
||||
}
|
||||
|
||||
// new row
|
||||
cursor.X = 0
|
||||
cursor.Y += rowHeight + VERTICAL_PAD
|
||||
}
|
||||
maxY = cursor.Y - VERTICAL_PAD
|
||||
|
||||
// then expand thinnest nodes to make each row the same width
|
||||
// . ┌A─────────────┐ ┌B──┐ ┌C─────────┐ ┬ maxHeight(A,B,C)
|
||||
// . │ │ │ │ │ │ │
|
||||
// . │ │ │ │ │ │ │
|
||||
// . │ │ │ │ │ │ │
|
||||
// . └──────────────┘ └───┘ └──────────┘ ┴
|
||||
// . ┌D────────┬────┐ ┌E────────────────┐ ┬ maxHeight(D,E)
|
||||
// . │ │ │ │ │
|
||||
// . │ │ │ │ │ │
|
||||
// . │ │ │ │ │
|
||||
// . │ │ │ │ │ │
|
||||
// . └─────────┴────┘ └─────────────────┘ ┴
|
||||
for i, row := range layout {
|
||||
rowWidth := rowWidths[i]
|
||||
if rowWidth == maxX {
|
||||
continue
|
||||
}
|
||||
delta := maxX - rowWidth
|
||||
nodes := []*d2graph.Object{}
|
||||
var widest float64
|
||||
for _, nodeIndex := range row {
|
||||
n := grid.nodes[nodeIndex]
|
||||
widest = math.Max(widest, n.Width)
|
||||
nodes = append(nodes, n)
|
||||
}
|
||||
sort.Slice(nodes, func(i, j int) bool {
|
||||
return nodes[i].Width < nodes[j].Width
|
||||
})
|
||||
// expand smaller nodes to fill remaining space
|
||||
for _, n := range nodes {
|
||||
if n.Width < widest {
|
||||
var index int
|
||||
for i, nodeIndex := range row {
|
||||
if n == grid.nodes[nodeIndex] {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
grow := math.Min(widest-n.Width, delta)
|
||||
n.Width += grow
|
||||
// shift following nodes
|
||||
for i := index + 1; i < len(row); i++ {
|
||||
grid.nodes[row[i]].TopLeft.X += grow
|
||||
}
|
||||
delta -= grow
|
||||
if delta <= 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if delta > 0 {
|
||||
grow := delta / float64(len(row))
|
||||
for i := len(row) - 1; i >= 0; i-- {
|
||||
n := grid.nodes[row[i]]
|
||||
n.TopLeft.X += grow * float64(i)
|
||||
n.Width += grow
|
||||
delta -= grow
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// if we have 3 columns, then each column's nodes should have the same width
|
||||
// . ├maxWidth(A,B)─┤ ├maxW(C,D)─┤ ├maxWidth(E)──────┤
|
||||
// . ┌A─────────────┐ ┌C─────────┐ ┌E────────────────┐
|
||||
// . └──────────────┘ │ │ │ │
|
||||
// . ┌B──┬──────────┐ └──────────┘ │ │
|
||||
// . │ │ ┌D────────┬┐ └─────────────────┘
|
||||
// . │ │ │ │ │
|
||||
// . │ │ │ ││
|
||||
// . └───┴──────────┘ │ │
|
||||
// . │ ││
|
||||
// . └─────────┴┘
|
||||
for _, column := range layout {
|
||||
columnWidth := 0.
|
||||
for _, nodeIndex := range column {
|
||||
n := grid.nodes[nodeIndex]
|
||||
n.TopLeft = cursor.Copy()
|
||||
cursor.Y += n.Height + VERTICAL_PAD
|
||||
columnWidth = math.Max(columnWidth, n.Width)
|
||||
}
|
||||
maxY = math.Max(maxY, cursor.Y-VERTICAL_PAD)
|
||||
// set all nodes in column to the same width
|
||||
for _, nodeIndex := range column {
|
||||
n := grid.nodes[nodeIndex]
|
||||
n.Width = columnWidth
|
||||
}
|
||||
|
||||
// new column
|
||||
cursor.Y = 0
|
||||
cursor.X += columnWidth + HORIZONTAL_PAD
|
||||
}
|
||||
maxX = cursor.X - HORIZONTAL_PAD
|
||||
// then expand shortest nodes to make each column the same height
|
||||
// . ├maxWidth(A,B)─┤ ├maxW(C,D)─┤ ├maxWidth(E)──────┤
|
||||
// . ┌A─────────────┐ ┌C─────────┐ ┌E────────────────┐
|
||||
// . ├ ─ ─ ─ ─ ─ ─ ┤ │ │ │ │
|
||||
// . │ │ └──────────┘ │ │
|
||||
// . └──────────────┘ ┌D─────────┐ ├ ─ ─ ─ ─ ─ ─ ─ ─ ┤
|
||||
// . ┌B─────────────┐ │ │ │ │
|
||||
// . │ │ │ │ │ │
|
||||
// . │ │ │ │ │ │
|
||||
// . │ │ │ │ │ │
|
||||
// . └──────────────┘ └──────────┘ └─────────────────┘
|
||||
// TODO see rows
|
||||
}
|
||||
grid.width = maxX
|
||||
grid.height = maxY
|
||||
|
||||
// position labels and icons
|
||||
for _, n := range grid.nodes {
|
||||
|
|
@ -125,37 +313,32 @@ func layoutGrid(g *d2graph.Graph, obj *d2graph.Object) (*grid, error) {
|
|||
// - translating the grid to its position placed by the core layout engine
|
||||
// - restore the children of the grid
|
||||
// - sorts objects to their original graph order
|
||||
func cleanup(g *d2graph.Graph, grids map[string]*grid, objectsOrder map[string]int) {
|
||||
var objects []*d2graph.Object
|
||||
if g.Root.IsGrid() {
|
||||
objects = []*d2graph.Object{g.Root}
|
||||
} else {
|
||||
objects = g.Objects
|
||||
func cleanup(graph *d2graph.Graph, grids map[string]*grid, objectsOrder map[string]int) {
|
||||
defer func() {
|
||||
sort.SliceStable(graph.Objects, func(i, j int) bool {
|
||||
return objectsOrder[graph.Objects[i].AbsID()] < objectsOrder[graph.Objects[j].AbsID()]
|
||||
})
|
||||
}()
|
||||
|
||||
if graph.Root.IsGrid() {
|
||||
grid, exists := grids[graph.Root.AbsID()]
|
||||
if exists {
|
||||
grid.cleanup(graph.Root, graph)
|
||||
return
|
||||
}
|
||||
}
|
||||
for _, obj := range objects {
|
||||
|
||||
for _, obj := range graph.Objects {
|
||||
grid, exists := grids[obj.AbsID()]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
|
||||
|
||||
// shift the grid from (0, 0)
|
||||
grid.shift(
|
||||
obj.TopLeft.X+CONTAINER_PADDING,
|
||||
obj.TopLeft.Y+CONTAINER_PADDING,
|
||||
)
|
||||
|
||||
obj.Children = make(map[string]*d2graph.Object)
|
||||
obj.ChildrenArray = make([]*d2graph.Object, 0)
|
||||
for _, child := range grid.nodes {
|
||||
obj.Children[child.ID] = child
|
||||
obj.ChildrenArray = append(obj.ChildrenArray, child)
|
||||
}
|
||||
|
||||
g.Objects = append(g.Objects, grid.nodes...)
|
||||
grid.cleanup(obj, graph)
|
||||
}
|
||||
|
||||
sort.SliceStable(g.Objects, func(i, j int) bool {
|
||||
return objectsOrder[g.Objects[i].AbsID()] < objectsOrder[g.Objects[j].AbsID()]
|
||||
})
|
||||
}
|
||||
|
|
|
|||
80
e2etests/testdata/files/dagger_grid.d2
vendored
80
e2etests/testdata/files/dagger_grid.d2
vendored
|
|
@ -1,25 +1,61 @@
|
|||
todo root level grid: {
|
||||
rows: 4
|
||||
rows: 4
|
||||
style.fill: black
|
||||
|
||||
flow: {
|
||||
width: 800
|
||||
height: 200
|
||||
flow: "" {
|
||||
width: 800
|
||||
height: 200
|
||||
style: {
|
||||
fill: cornflowerblue
|
||||
}
|
||||
|
||||
dagger engine: {
|
||||
width: 800
|
||||
}
|
||||
|
||||
any docker compatible runtime: {
|
||||
width: 800
|
||||
}
|
||||
|
||||
any ci: {
|
||||
width: 800
|
||||
}
|
||||
|
||||
windows
|
||||
linux
|
||||
macos
|
||||
kubernetes
|
||||
}
|
||||
|
||||
DAGGER ENGINE: {
|
||||
width: 800
|
||||
style: {
|
||||
fill: beige
|
||||
stroke: darkcyan
|
||||
font-color: blue
|
||||
stroke-width: 8
|
||||
}
|
||||
}
|
||||
|
||||
ANY DOCKER COMPATIBLE RUNTIME: {
|
||||
width: 800
|
||||
style: {
|
||||
fill: lightcyan
|
||||
stroke: darkcyan
|
||||
font-color: black
|
||||
stroke-width: 8
|
||||
}
|
||||
icon: https://icons.terrastruct.com/dev%2Fdocker.svg
|
||||
}
|
||||
|
||||
ANY CI: {
|
||||
style: {
|
||||
fill: gold
|
||||
stroke: maroon
|
||||
font-color: maroon
|
||||
stroke-width: 8
|
||||
}
|
||||
}
|
||||
|
||||
WINDOWS.style: {
|
||||
font-color: white
|
||||
fill: darkcyan
|
||||
stroke: black
|
||||
}
|
||||
LINUX.style: {
|
||||
font-color: white
|
||||
fill: darkcyan
|
||||
stroke: black
|
||||
}
|
||||
MACOS.style: {
|
||||
font-color: white
|
||||
fill: darkcyan
|
||||
stroke: black
|
||||
}
|
||||
KUBERNETES.style: {
|
||||
font-color: white
|
||||
fill: darkcyan
|
||||
stroke: black
|
||||
}
|
||||
|
|
|
|||
2
e2etests/testdata/files/teleport_grid.d2
vendored
2
e2etests/testdata/files/teleport_grid.d2
vendored
|
|
@ -9,7 +9,7 @@ teleport -> identity provider
|
|||
teleport <- identity provider
|
||||
|
||||
users: "" {
|
||||
rows: 2
|
||||
columns: 1
|
||||
|
||||
Engineers: {
|
||||
shape: circle
|
||||
|
|
|
|||
Loading…
Reference in a new issue