This commit is contained in:
Gavin Nishizawa 2023-04-06 14:30:45 -07:00
parent 8b3ba86da3
commit bab54b4030
No known key found for this signature in database
GPG key ID: AE3B177777CE55CD
2 changed files with 140 additions and 135 deletions

View file

@ -8,18 +8,20 @@ import (
type gridDiagram struct { type gridDiagram struct {
root *d2graph.Object root *d2graph.Object
nodes []*d2graph.Object objects []*d2graph.Object
rows int rows int
columns int columns int
rowDominant bool // if true, place objects left to right along rows
// if false, place objects top to bottom along columns
rowDirected bool
width float64 width float64
height float64 height float64
} }
func newGridDiagram(root *d2graph.Object) *gridDiagram { func newGridDiagram(root *d2graph.Object) *gridDiagram {
gd := gridDiagram{root: root, nodes: root.ChildrenArray} gd := gridDiagram{root: root, objects: root.ChildrenArray}
if root.Attributes.Rows != nil { if root.Attributes.Rows != nil {
gd.rows, _ = strconv.Atoi(root.Attributes.Rows.Value) gd.rows, _ = strconv.Atoi(root.Attributes.Rows.Value)
} }
@ -27,30 +29,31 @@ func newGridDiagram(root *d2graph.Object) *gridDiagram {
gd.columns, _ = strconv.Atoi(root.Attributes.Columns.Value) gd.columns, _ = strconv.Atoi(root.Attributes.Columns.Value)
} }
// compute exact row/column count based on values entered if gd.rows != 0 && gd.columns != 0 {
if gd.columns == 0 { // . row-directed column-directed
gd.rowDominant = true // . ┌───────┐ ┌───────┐
} else if gd.rows == 0 { // . │ a b c │ │ a d g │
gd.rowDominant = false // . │ d e f │ │ b e h │
} else { // . │ g h i │ │ c f i │
// if keyword rows is first, rows are primary, columns secondary. // . └───────┘ └───────┘
// if keyword rows is first, make it row-directed, if columns is first it is column-directed
if root.Attributes.Rows.MapKey.Range.Before(root.Attributes.Columns.MapKey.Range) { if root.Attributes.Rows.MapKey.Range.Before(root.Attributes.Columns.MapKey.Range) {
gd.rowDominant = true gd.rowDirected = true
} }
// rows and columns specified, but we want to continue naturally if user enters more nodes // rows and columns specified, but we want to continue naturally if user enters more objects
// e.g. 2 rows, 3 columns specified + g node added: │ with 3 columns, 2 rows: // e.g. 2 rows, 3 columns specified + g added: │ with 3 columns, 2 rows:
// . original add row add column │ original add row add column // . 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 │ // . │ 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 │ // . │ d e f │ │ d e f │ │ e f g │ │ │ b d f │ │ b e │ │ b d f │
// . └───────┘ │ g │ └─────────┘ │ └───────┘ │ c f │ └─────────┘ // . └───────┘ │ g │ └─────────┘ │ └───────┘ │ c f │ └─────────┘
// . └───────┘ ▲ │ └───────┘ ▲ // . └───────┘ ▲ │ └───────┘ ▲
// . ▲ └─existing nodes modified │ ▲ └─existing nodes preserved // . ▲ └─existing objects modified│ ▲ └─existing columns preserved
// . └─existing rows preserved │ └─existing rows modified // . └─existing rows preserved │ └─existing objects modified
capacity := gd.rows * gd.columns capacity := gd.rows * gd.columns
for capacity < len(gd.nodes) { for capacity < len(gd.objects) {
if gd.rowDominant { if gd.rowDirected {
gd.rows++ gd.rows++
capacity += gd.columns capacity += gd.columns
} else { } else {
@ -58,13 +61,15 @@ func newGridDiagram(root *d2graph.Object) *gridDiagram {
capacity += gd.rows capacity += gd.rows
} }
} }
} else if gd.columns == 0 {
gd.rowDirected = true
} }
return &gd return &gd
} }
func (gd *gridDiagram) shift(dx, dy float64) { func (gd *gridDiagram) shift(dx, dy float64) {
for _, obj := range gd.nodes { for _, obj := range gd.objects {
obj.TopLeft.X += dx obj.TopLeft.X += dx
obj.TopLeft.Y += dy obj.TopLeft.Y += dy
} }
@ -73,9 +78,9 @@ func (gd *gridDiagram) shift(dx, dy float64) {
func (gd *gridDiagram) cleanup(obj *d2graph.Object, graph *d2graph.Graph) { func (gd *gridDiagram) cleanup(obj *d2graph.Object, graph *d2graph.Graph) {
obj.Children = make(map[string]*d2graph.Object) obj.Children = make(map[string]*d2graph.Object)
obj.ChildrenArray = make([]*d2graph.Object, 0) obj.ChildrenArray = make([]*d2graph.Object, 0)
for _, child := range gd.nodes { for _, child := range gd.objects {
obj.Children[child.ID] = child obj.Children[child.ID] = child
obj.ChildrenArray = append(obj.ChildrenArray, child) obj.ChildrenArray = append(obj.ChildrenArray, child)
} }
graph.Objects = append(graph.Objects, gd.nodes...) graph.Objects = append(graph.Objects, gd.objects...)
} }

View file

@ -94,8 +94,8 @@ func withoutGridDiagrams(ctx context.Context, g *d2graph.Graph) (gridDiagrams ma
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter)) obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
gridDiagrams[obj.AbsID()] = gd gridDiagrams[obj.AbsID()] = gd
for _, node := range gd.nodes { for _, o := range gd.objects {
toRemove[node] = struct{}{} toRemove[o] = struct{}{}
} }
} }
} }
@ -123,12 +123,12 @@ func layoutGrid(g *d2graph.Graph, obj *d2graph.Object) (*gridDiagram, error) {
} }
// position labels and icons // position labels and icons
for _, n := range gd.nodes { for _, o := range gd.objects {
if n.Attributes.Icon != nil { if o.Attributes.Icon != nil {
n.LabelPosition = go2.Pointer(string(label.InsideTopCenter)) o.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
n.IconPosition = go2.Pointer(string(label.InsideMiddleCenter)) o.IconPosition = go2.Pointer(string(label.InsideMiddleCenter))
} else { } else {
n.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter)) o.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
} }
} }
@ -136,19 +136,19 @@ func layoutGrid(g *d2graph.Graph, obj *d2graph.Object) (*gridDiagram, error) {
} }
func (gd *gridDiagram) layoutEvenly(g *d2graph.Graph, obj *d2graph.Object) { func (gd *gridDiagram) layoutEvenly(g *d2graph.Graph, obj *d2graph.Object) {
// layout nodes in a grid with these 2 properties: // layout objects in a grid with these 2 properties:
// all nodes in the same row should have the same height // all objects in the same row should have the same height
// all nodes in the same column should have the same width // all objects in the same column should have the same width
getNode := func(rowIndex, columnIndex int) *d2graph.Object { getObject := func(rowIndex, columnIndex int) *d2graph.Object {
var index int var index int
if gd.rowDominant { if gd.rowDirected {
index = rowIndex*gd.columns + columnIndex index = rowIndex*gd.columns + columnIndex
} else { } else {
index = columnIndex*gd.rows + rowIndex index = columnIndex*gd.rows + rowIndex
} }
if index < len(gd.nodes) { if index < len(gd.objects) {
return gd.nodes[index] return gd.objects[index]
} }
return nil return nil
} }
@ -158,38 +158,38 @@ func (gd *gridDiagram) layoutEvenly(g *d2graph.Graph, obj *d2graph.Object) {
for i := 0; i < gd.rows; i++ { for i := 0; i < gd.rows; i++ {
rowHeight := 0. rowHeight := 0.
for j := 0; j < gd.columns; j++ { for j := 0; j < gd.columns; j++ {
n := getNode(i, j) o := getObject(i, j)
if n == nil { if o == nil {
break break
} }
rowHeight = math.Max(rowHeight, n.Height) rowHeight = math.Max(rowHeight, o.Height)
} }
rowHeights = append(rowHeights, rowHeight) rowHeights = append(rowHeights, rowHeight)
} }
for j := 0; j < gd.columns; j++ { for j := 0; j < gd.columns; j++ {
columnWidth := 0. columnWidth := 0.
for i := 0; i < gd.rows; i++ { for i := 0; i < gd.rows; i++ {
n := getNode(i, j) o := getObject(i, j)
if n == nil { if o == nil {
break break
} }
columnWidth = math.Max(columnWidth, n.Width) columnWidth = math.Max(columnWidth, o.Width)
} }
colWidths = append(colWidths, columnWidth) colWidths = append(colWidths, columnWidth)
} }
cursor := geo.NewPoint(0, 0) cursor := geo.NewPoint(0, 0)
if gd.rowDominant { if gd.rowDirected {
for i := 0; i < gd.rows; i++ { for i := 0; i < gd.rows; i++ {
for j := 0; j < gd.columns; j++ { for j := 0; j < gd.columns; j++ {
n := getNode(i, j) o := getObject(i, j)
if n == nil { if o == nil {
break break
} }
n.Width = colWidths[j] o.Width = colWidths[j]
n.Height = rowHeights[i] o.Height = rowHeights[i]
n.TopLeft = cursor.Copy() o.TopLeft = cursor.Copy()
cursor.X += n.Width + HORIZONTAL_PAD cursor.X += o.Width + HORIZONTAL_PAD
} }
cursor.X = 0 cursor.X = 0
cursor.Y += rowHeights[i] + VERTICAL_PAD cursor.Y += rowHeights[i] + VERTICAL_PAD
@ -197,14 +197,14 @@ func (gd *gridDiagram) layoutEvenly(g *d2graph.Graph, obj *d2graph.Object) {
} else { } else {
for j := 0; j < gd.columns; j++ { for j := 0; j < gd.columns; j++ {
for i := 0; i < gd.rows; i++ { for i := 0; i < gd.rows; i++ {
n := getNode(i, j) o := getObject(i, j)
if n == nil { if o == nil {
break break
} }
n.Width = colWidths[j] o.Width = colWidths[j]
n.Height = rowHeights[i] o.Height = rowHeights[i]
n.TopLeft = cursor.Copy() o.TopLeft = cursor.Copy()
cursor.Y += n.Height + VERTICAL_PAD cursor.Y += o.Height + VERTICAL_PAD
} }
cursor.X += colWidths[j] + HORIZONTAL_PAD cursor.X += colWidths[j] + HORIZONTAL_PAD
cursor.Y = 0 cursor.Y = 0
@ -225,14 +225,14 @@ func (gd *gridDiagram) layoutEvenly(g *d2graph.Graph, obj *d2graph.Object) {
} }
func (gd *gridDiagram) layoutDynamic(g *d2graph.Graph, obj *d2graph.Object) { func (gd *gridDiagram) layoutDynamic(g *d2graph.Graph, obj *d2graph.Object) {
// assume we have the following nodes to layout: // assume we have the following objects to layout:
// . ┌A──────────────┐ ┌B──┐ ┌C─────────┐ ┌D────────┐ ┌E────────────────┐ // . ┌A──────────────┐ ┌B──┐ ┌C─────────┐ ┌D────────┐ ┌E────────────────┐
// . └───────────────┘ │ │ │ │ │ │ │ │ // . └───────────────┘ │ │ │ │ │ │ │ │
// . │ │ └──────────┘ │ │ │ │ // . │ │ └──────────┘ │ │ │ │
// . │ │ │ │ └─────────────────┘ // . │ │ │ │ └─────────────────┘
// . └───┘ │ │ // . └───┘ │ │
// . └─────────┘ // . └─────────┘
// Note: if the grid is row dominant, all nodes should be the same height (same width if column dominant) // Note: if the grid is row dominant, all objects should be the same height (same width if column dominant)
// . ┌A─────────────┐ ┌B──┐ ┌C─────────┐ ┌D────────┐ ┌E────────────────┐ // . ┌A─────────────┐ ┌B──┐ ┌C─────────┐ ┌D────────┐ ┌E────────────────┐
// . ├ ─ ─ ─ ─ ─ ─ ─┤ │ │ │ │ │ │ │ │ // . ├ ─ ─ ─ ─ ─ ─ ─┤ │ │ │ │ │ │ │ │
// . │ │ │ │ ├ ─ ─ ─ ─ ─┤ │ │ │ │ // . │ │ │ │ ├ ─ ─ ─ ─ ─┤ │ │ │ │
@ -242,15 +242,15 @@ func (gd *gridDiagram) layoutDynamic(g *d2graph.Graph, obj *d2graph.Object) {
// we want to split up the total width across the N rows or columns as evenly as possible // we want to split up the total width across the N rows or columns as evenly as possible
var totalWidth, totalHeight float64 var totalWidth, totalHeight float64
for _, n := range gd.nodes { for _, o := range gd.objects {
totalWidth += n.Width totalWidth += o.Width
totalHeight += n.Height totalHeight += o.Height
} }
totalWidth += HORIZONTAL_PAD * float64(len(gd.nodes)-gd.rows) totalWidth += HORIZONTAL_PAD * float64(len(gd.objects)-gd.rows)
totalHeight += VERTICAL_PAD * float64(len(gd.nodes)-gd.columns) totalHeight += VERTICAL_PAD * float64(len(gd.objects)-gd.columns)
var layout [][]*d2graph.Object var layout [][]*d2graph.Object
if gd.rowDominant { if gd.rowDirected {
targetWidth := totalWidth / float64(gd.rows) targetWidth := totalWidth / float64(gd.rows)
layout = gd.getBestLayout(targetWidth, false) layout = gd.getBestLayout(targetWidth, false)
} else { } else {
@ -260,8 +260,8 @@ func (gd *gridDiagram) layoutDynamic(g *d2graph.Graph, obj *d2graph.Object) {
cursor := geo.NewPoint(0, 0) cursor := geo.NewPoint(0, 0)
var maxY, maxX float64 var maxY, maxX float64
if gd.rowDominant { if gd.rowDirected {
// if we have 2 rows, then each row's nodes should have the same height // if we have 2 rows, then each row's objects should have the same height
// . ┌A─────────────┐ ┌B──┐ ┌C─────────┐ ┬ maxHeight(A,B,C) // . ┌A─────────────┐ ┌B──┐ ┌C─────────┐ ┬ maxHeight(A,B,C)
// . ├ ─ ─ ─ ─ ─ ─ ─┤ │ │ │ │ │ // . ├ ─ ─ ─ ─ ─ ─ ─┤ │ │ │ │ │
// . │ │ │ │ ├ ─ ─ ─ ─ ─┤ │ // . │ │ │ │ ├ ─ ─ ─ ─ ─┤ │
@ -276,18 +276,18 @@ func (gd *gridDiagram) layoutDynamic(g *d2graph.Graph, obj *d2graph.Object) {
rowWidths := []float64{} rowWidths := []float64{}
for _, row := range layout { for _, row := range layout {
rowHeight := 0. rowHeight := 0.
for _, n := range row { for _, o := range row {
n.TopLeft = cursor.Copy() o.TopLeft = cursor.Copy()
cursor.X += n.Width + HORIZONTAL_PAD cursor.X += o.Width + HORIZONTAL_PAD
rowHeight = math.Max(rowHeight, n.Height) rowHeight = math.Max(rowHeight, o.Height)
} }
rowWidth := cursor.X - HORIZONTAL_PAD rowWidth := cursor.X - HORIZONTAL_PAD
rowWidths = append(rowWidths, rowWidth) rowWidths = append(rowWidths, rowWidth)
maxX = math.Max(maxX, rowWidth) maxX = math.Max(maxX, rowWidth)
// set all nodes in row to the same height // set all objects in row to the same height
for _, n := range row { for _, o := range row {
n.Height = rowHeight o.Height = rowHeight
} }
// new row // new row
@ -296,7 +296,7 @@ func (gd *gridDiagram) layoutDynamic(g *d2graph.Graph, obj *d2graph.Object) {
} }
maxY = cursor.Y - VERTICAL_PAD maxY = cursor.Y - VERTICAL_PAD
// then expand thinnest nodes to make each row the same width // then expand thinnest objects to make each row the same width
// . ┌A─────────────┐ ┌B──┐ ┌C─────────┐ ┬ maxHeight(A,B,C) // . ┌A─────────────┐ ┌B──┐ ┌C─────────┐ ┬ maxHeight(A,B,C)
// . │ │ │ │ │ │ │ // . │ │ │ │ │ │ │
// . │ │ │ │ │ │ │ // . │ │ │ │ │ │ │
@ -314,28 +314,28 @@ func (gd *gridDiagram) layoutDynamic(g *d2graph.Graph, obj *d2graph.Object) {
continue continue
} }
delta := maxX - rowWidth delta := maxX - rowWidth
nodes := []*d2graph.Object{} objects := []*d2graph.Object{}
var widest float64 var widest float64
for _, n := range row { for _, o := range row {
widest = math.Max(widest, n.Width) widest = math.Max(widest, o.Width)
nodes = append(nodes, n) objects = append(objects, o)
} }
sort.Slice(nodes, func(i, j int) bool { sort.Slice(objects, func(i, j int) bool {
return nodes[i].Width < nodes[j].Width return objects[i].Width < objects[j].Width
}) })
// expand smaller nodes to fill remaining space // expand smaller objects to fill remaining space
for _, n := range nodes { for _, o := range objects {
if n.Width < widest { if o.Width < widest {
var index int var index int
for i, node := range row { for i, rowObj := range row {
if n == node { if o == rowObj {
index = i index = i
break break
} }
} }
grow := math.Min(widest-n.Width, delta) grow := math.Min(widest-o.Width, delta)
n.Width += grow o.Width += grow
// shift following nodes // shift following objects
for i := index + 1; i < len(row); i++ { for i := index + 1; i < len(row); i++ {
row[i].TopLeft.X += grow row[i].TopLeft.X += grow
} }
@ -348,15 +348,15 @@ func (gd *gridDiagram) layoutDynamic(g *d2graph.Graph, obj *d2graph.Object) {
if delta > 0 { if delta > 0 {
grow := delta / float64(len(row)) grow := delta / float64(len(row))
for i := len(row) - 1; i >= 0; i-- { for i := len(row) - 1; i >= 0; i-- {
n := row[i] o := row[i]
n.TopLeft.X += grow * float64(i) o.TopLeft.X += grow * float64(i)
n.Width += grow o.Width += grow
delta -= grow delta -= grow
} }
} }
} }
} else { } else {
// if we have 3 columns, then each column's nodes should have the same width // if we have 3 columns, then each column's objects should have the same width
// . ├maxWidth(A,B)─┤ ├maxW(C,D)─┤ ├maxWidth(E)──────┤ // . ├maxWidth(A,B)─┤ ├maxW(C,D)─┤ ├maxWidth(E)──────┤
// . ┌A─────────────┐ ┌C─────────┐ ┌E────────────────┐ // . ┌A─────────────┐ ┌C─────────┐ ┌E────────────────┐
// . └──────────────┘ │ │ │ │ // . └──────────────┘ │ │ │ │
@ -370,17 +370,17 @@ func (gd *gridDiagram) layoutDynamic(g *d2graph.Graph, obj *d2graph.Object) {
colHeights := []float64{} colHeights := []float64{}
for _, column := range layout { for _, column := range layout {
colWidth := 0. colWidth := 0.
for _, n := range column { for _, o := range column {
n.TopLeft = cursor.Copy() o.TopLeft = cursor.Copy()
cursor.Y += n.Height + VERTICAL_PAD cursor.Y += o.Height + VERTICAL_PAD
colWidth = math.Max(colWidth, n.Width) colWidth = math.Max(colWidth, o.Width)
} }
colHeight := cursor.Y - VERTICAL_PAD colHeight := cursor.Y - VERTICAL_PAD
colHeights = append(colHeights, colHeight) colHeights = append(colHeights, colHeight)
maxY = math.Max(maxY, colHeight) maxY = math.Max(maxY, colHeight)
// set all nodes in column to the same width // set all objects in column to the same width
for _, n := range column { for _, o := range column {
n.Width = colWidth o.Width = colWidth
} }
// new column // new column
@ -388,7 +388,7 @@ func (gd *gridDiagram) layoutDynamic(g *d2graph.Graph, obj *d2graph.Object) {
cursor.X += colWidth + HORIZONTAL_PAD cursor.X += colWidth + HORIZONTAL_PAD
} }
maxX = cursor.X - HORIZONTAL_PAD maxX = cursor.X - HORIZONTAL_PAD
// then expand shortest nodes to make each column the same height // then expand shortest objects to make each column the same height
// . ├maxWidth(A,B)─┤ ├maxW(C,D)─┤ ├maxWidth(E)──────┤ // . ├maxWidth(A,B)─┤ ├maxW(C,D)─┤ ├maxWidth(E)──────┤
// . ┌A─────────────┐ ┌C─────────┐ ┌E────────────────┐ // . ┌A─────────────┐ ┌C─────────┐ ┌E────────────────┐
// . ├ ─ ─ ─ ─ ─ ─ ┤ │ │ │ │ // . ├ ─ ─ ─ ─ ─ ─ ┤ │ │ │ │
@ -405,28 +405,28 @@ func (gd *gridDiagram) layoutDynamic(g *d2graph.Graph, obj *d2graph.Object) {
continue continue
} }
delta := maxY - colHeight delta := maxY - colHeight
nodes := []*d2graph.Object{} objects := []*d2graph.Object{}
var tallest float64 var tallest float64
for _, n := range column { for _, o := range column {
tallest = math.Max(tallest, n.Height) tallest = math.Max(tallest, o.Height)
nodes = append(nodes, n) objects = append(objects, o)
} }
sort.Slice(nodes, func(i, j int) bool { sort.Slice(objects, func(i, j int) bool {
return nodes[i].Height < nodes[j].Height return objects[i].Height < objects[j].Height
}) })
// expand smaller nodes to fill remaining space // expand smaller objects to fill remaining space
for _, n := range nodes { for _, o := range objects {
if n.Height < tallest { if o.Height < tallest {
var index int var index int
for i, node := range column { for i, colObj := range column {
if n == node { if o == colObj {
index = i index = i
break break
} }
} }
grow := math.Min(tallest-n.Height, delta) grow := math.Min(tallest-o.Height, delta)
n.Height += grow o.Height += grow
// shift following nodes // shift following objects
for i := index + 1; i < len(column); i++ { for i := index + 1; i < len(column); i++ {
column[i].TopLeft.Y += grow column[i].TopLeft.Y += grow
} }
@ -439,9 +439,9 @@ func (gd *gridDiagram) layoutDynamic(g *d2graph.Graph, obj *d2graph.Object) {
if delta > 0 { if delta > 0 {
grow := delta / float64(len(column)) grow := delta / float64(len(column))
for i := len(column) - 1; i >= 0; i-- { for i := len(column) - 1; i >= 0; i-- {
n := column[i] o := column[i]
n.TopLeft.Y += grow * float64(i) o.TopLeft.Y += grow * float64(i)
n.Height += grow o.Height += grow
delta -= grow delta -= grow
} }
} }
@ -451,7 +451,7 @@ func (gd *gridDiagram) layoutDynamic(g *d2graph.Graph, obj *d2graph.Object) {
gd.height = maxY gd.height = maxY
} }
// generate the best layout of nodes aiming for each row to be the targetSize width // generate the best layout of objects aiming for each row to be the targetSize width
// if columns is true, each column aims to have the targetSize height // if columns is true, each column aims to have the targetSize height
func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2graph.Object { func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2graph.Object {
var nCuts int var nCuts int
@ -461,24 +461,24 @@ func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2gr
nCuts = gd.rows - 1 nCuts = gd.rows - 1
} }
if nCuts == 0 { if nCuts == 0 {
return genLayout(gd.nodes, nil) return genLayout(gd.objects, nil)
} }
// get all options for where to place these cuts, preferring later cuts over earlier cuts // get all options for where to place these cuts, preferring later cuts over earlier cuts
// with 5 nodes and 2 cuts we have these options: // with 5 objects and 2 cuts we have these options:
// . A B C │ D │ E <- these cuts would produce: ┌A─┐ ┌B─┐ ┌C─┐ // . A B C │ D │ E <- these cuts would produce: ┌A─┐ ┌B─┐ ┌C─┐
// . A B │ C D │ E └──┘ └──┘ └──┘ // . A B │ C D │ E └──┘ └──┘ └──┘
// . A │ B C D │ E ┌D───────────┐ // . A │ B C D │ E ┌D───────────┐
// . A B │ C │ D E └────────────┘ // . A B │ C │ D E └────────────┘
// . A │ B C │ D E ┌E───────────┐ // . A │ B C │ D E ┌E───────────┐
// . A │ B │ C D E └────────────┘ // . A │ B │ C D E └────────────┘
divisions := genDivisions(gd.nodes, nCuts) divisions := genDivisions(gd.objects, nCuts)
var bestLayout [][]*d2graph.Object var bestLayout [][]*d2graph.Object
bestDist := math.MaxFloat64 bestDist := math.MaxFloat64
// of these divisions, find the layout with rows closest to the targetSize // of these divisions, find the layout with rows closest to the targetSize
for _, division := range divisions { for _, division := range divisions {
layout := genLayout(gd.nodes, division) layout := genLayout(gd.objects, division)
dist := getDistToTarget(layout, targetSize, columns) dist := getDistToTarget(layout, targetSize, columns)
if dist < bestDist { if dist < bestDist {
bestLayout = layout bestLayout = layout
@ -489,16 +489,16 @@ func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2gr
return bestLayout return bestLayout
} }
// get all possible divisions of nodes by the number of cuts // get all possible divisions of objects by the number of cuts
func genDivisions(nodes []*d2graph.Object, nCuts int) (divisions [][]int) { func genDivisions(objects []*d2graph.Object, nCuts int) (divisions [][]int) {
if len(nodes) < 2 || nCuts == 0 { if len(objects) < 2 || nCuts == 0 {
return nil return nil
} }
// we go in this order to prefer extra nodes in starting rows rather than later ones // we go in this order to prefer extra objects in starting rows rather than later ones
lastNode := len(nodes) - 1 lastObj := len(objects) - 1
for index := lastNode; index >= nCuts; index-- { for index := lastObj; index >= nCuts; index-- {
if nCuts > 1 { if nCuts > 1 {
for _, inner := range genDivisions(nodes[:index], nCuts-1) { for _, inner := range genDivisions(objects[:index], nCuts-1) {
divisions = append(divisions, append(inner, index-1)) divisions = append(divisions, append(inner, index-1))
} }
} else { } else {
@ -509,19 +509,19 @@ func genDivisions(nodes []*d2graph.Object, nCuts int) (divisions [][]int) {
return divisions return divisions
} }
// generate a grid of nodes from the given cut indices // generate a grid of objects from the given cut indices
func genLayout(nodes []*d2graph.Object, cutIndices []int) [][]*d2graph.Object { func genLayout(objects []*d2graph.Object, cutIndices []int) [][]*d2graph.Object {
layout := make([][]*d2graph.Object, len(cutIndices)+1) layout := make([][]*d2graph.Object, len(cutIndices)+1)
nodeIndex := 0 objIndex := 0
for i := 0; i <= len(cutIndices); i++ { for i := 0; i <= len(cutIndices); i++ {
var stop int var stop int
if i < len(cutIndices) { if i < len(cutIndices) {
stop = cutIndices[i] stop = cutIndices[i]
} else { } else {
stop = len(nodes) - 1 stop = len(objects) - 1
} }
for ; nodeIndex <= stop; nodeIndex++ { for ; objIndex <= stop; objIndex++ {
layout[i] = append(layout[i], nodes[nodeIndex]) layout[i] = append(layout[i], objects[objIndex])
} }
} }
return layout return layout
@ -531,11 +531,11 @@ func getDistToTarget(layout [][]*d2graph.Object, targetSize float64, columns boo
totalDelta := 0. totalDelta := 0.
for _, row := range layout { for _, row := range layout {
rowSize := 0. rowSize := 0.
for _, n := range row { for _, o := range row {
if columns { if columns {
rowSize += n.Height + VERTICAL_PAD rowSize += o.Height + VERTICAL_PAD
} else { } else {
rowSize += n.Width + HORIZONTAL_PAD rowSize += o.Width + HORIZONTAL_PAD
} }
} }
totalDelta += math.Abs(rowSize - targetSize) totalDelta += math.Abs(rowSize - targetSize)