package d2grid
import (
"context"
"math"
"github.com/pancsta/d2/d2graph"
"github.com/pancsta/d2/d2target"
"github.com/pancsta/d2/lib/geo"
"github.com/pancsta/d2/lib/label"
"github.com/d2lang/util-go/go2"
)
const (
CONTAINER_PADDING = 60
DEFAULT_GAP = 40
)
func Layout (ctx context .Context , g *d2graph .Graph ) error {
obj := g .Root
gd , err := layoutGrid (g , obj )
if err != nil {
return err
}
if obj .HasLabel () && obj .LabelPosition == nil {
obj .LabelPosition = go2 .Pointer (label .InsideTopCenter .String ())
}
if obj .Icon != nil && obj .IconPosition == nil {
obj .IconPosition = go2 .Pointer (label .InsideTopLeft .String ())
}
if obj .Box != nil {
horizontalPadding , verticalPadding := CONTAINER_PADDING , CONTAINER_PADDING
if obj .GridGap != nil || obj .HorizontalGap != nil {
horizontalPadding = gd .horizontalGap
}
if obj .GridGap != nil || obj .VerticalGap != nil {
verticalPadding = gd .verticalGap
}
contentWidth , contentHeight := gd .width , gd .height
var labelPosition , iconPosition label .Position
if obj .LabelPosition != nil {
labelPosition = label .FromString (*obj .LabelPosition )
}
if obj .IconPosition != nil {
iconPosition = label .FromString (*obj .IconPosition )
}
_ , padding := obj .Spacing ()
var labelWidth , labelHeight float64
if obj .LabelDimensions .Width > 0 {
labelWidth = float64 (obj .LabelDimensions .Width ) + 2 *label .PADDING
}
if obj .LabelDimensions .Height > 0 {
labelHeight = float64 (obj .LabelDimensions .Height ) + 2 *label .PADDING
}
if labelWidth > 0 {
switch labelPosition {
case label .OutsideTopLeft , label .OutsideTopCenter , label .OutsideTopRight ,
label .InsideTopLeft , label .InsideTopCenter , label .InsideTopRight ,
label .InsideBottomLeft , label .InsideBottomCenter , label .InsideBottomRight ,
label .OutsideBottomLeft , label .OutsideBottomCenter , label .OutsideBottomRight :
overflow := labelWidth - contentWidth
if overflow > 0 {
padding .Left += overflow / 2
padding .Right += overflow / 2
}
}
}
if labelHeight > 0 {
switch labelPosition {
case label .OutsideLeftTop , label .OutsideLeftMiddle , label .OutsideLeftBottom ,
label .InsideMiddleLeft , label .InsideMiddleCenter , label .InsideMiddleRight ,
label .OutsideRightTop , label .OutsideRightMiddle , label .OutsideRightBottom :
overflow := labelHeight - contentHeight
if overflow > 0 {
padding .Top += overflow / 2
padding .Bottom += overflow / 2
}
}
}
if iconPosition == label .InsideTopLeft && labelPosition == label .InsideTopCenter {
iconSize := float64 (d2target .MAX_ICON_SIZE ) + 2 *label .PADDING
padding .Left = math .Max (padding .Left , iconSize )
padding .Right = math .Max (padding .Right , iconSize )
minWidth := 2 *iconSize + float64 (obj .LabelDimensions .Width ) + 2 *label .PADDING
overflow := minWidth - contentWidth
if overflow > 0 {
padding .Left = math .Max (padding .Left , overflow /2 )
padding .Right = math .Max (padding .Right , overflow /2 )
}
}
padding .Top = math .Max (padding .Top , float64 (verticalPadding ))
padding .Bottom = math .Max (padding .Bottom , float64 (verticalPadding ))
padding .Left = math .Max (padding .Left , float64 (horizontalPadding ))
padding .Right = math .Max (padding .Right , float64 (horizontalPadding ))
totalWidth := padding .Left + contentWidth + padding .Right
totalHeight := padding .Top + contentHeight + padding .Bottom
obj .SizeToContent (totalWidth , totalHeight , 0 , 0 )
s := obj .ToShape ()
innerTL := s .GetInsidePlacement (totalWidth , totalHeight , 0 , 0 )
innerBox := s .GetInnerBox ()
var resizeDx , resizeDy float64
if innerBox .Width > totalWidth {
resizeDx = (innerBox .Width - totalWidth ) / 2
}
if innerBox .Height > totalHeight {
resizeDy = (innerBox .Height - totalHeight ) / 2
}
dx := -float64 (horizontalPadding ) + innerTL .X + padding .Left + resizeDx
dy := -float64 (verticalPadding ) + innerTL .Y + padding .Top + resizeDy
if dx != 0 || dy != 0 {
gd .shift (dx , dy )
}
}
for _ , e := range g .Edges {
if !e .Src .Parent .IsDescendantOf (obj ) && !e .Dst .Parent .IsDescendantOf (obj ) {
continue
}
gd .edges = append (gd .edges , e )
if e .Src .Parent != obj || e .Dst .Parent != obj {
continue
}
e .Route = []*geo .Point {e .Src .Center (), e .Dst .Center ()}
e .TraceToShape (e .Route , 0 , 1 )
if e .Label .Value != "" {
e .LabelPosition = go2 .Pointer (label .InsideMiddleCenter .String ())
}
}
if g .Root .IsGridDiagram () && len (g .Root .ChildrenArray ) != 0 {
g .Root .TopLeft = geo .NewPoint (0 , 0 )
}
if g .RootLevel > 0 {
horizontalPadding , verticalPadding := CONTAINER_PADDING , CONTAINER_PADDING
if obj .GridGap != nil || obj .HorizontalGap != nil {
horizontalPadding = gd .horizontalGap
}
if obj .GridGap != nil || obj .VerticalGap != nil {
verticalPadding = gd .verticalGap
}
gd .shift (
obj .TopLeft .X +float64 (horizontalPadding ),
obj .TopLeft .Y +float64 (verticalPadding ),
)
}
return nil
}
func layoutGrid(g *d2graph .Graph , obj *d2graph .Object ) (*gridDiagram , error ) {
gd , err := newGridDiagram (obj )
if err != nil {
return nil , err
}
for _ , o := range gd .objects {
positionedLabel := false
if o .Icon != nil && o .IconPosition == nil {
if len (o .ChildrenArray ) > 0 {
o .IconPosition = go2 .Pointer (label .OutsideTopLeft .String ())
if o .LabelPosition == nil {
o .LabelPosition = go2 .Pointer (label .OutsideTopRight .String ())
positionedLabel = true
}
} else {
o .IconPosition = go2 .Pointer (label .InsideMiddleCenter .String ())
}
}
if !positionedLabel && o .HasLabel () && o .LabelPosition == nil {
if len (o .ChildrenArray ) > 0 {
o .LabelPosition = go2 .Pointer (label .OutsideTopCenter .String ())
} else if o .HasOutsideBottomLabel () {
o .LabelPosition = go2 .Pointer (label .OutsideBottomCenter .String ())
} else if o .Icon != nil {
o .LabelPosition = go2 .Pointer (label .InsideTopCenter .String ())
} else {
o .LabelPosition = go2 .Pointer (label .InsideMiddleCenter .String ())
}
}
}
revertAdjustments := gd .sizeForOutsideLabels ()
if gd .rows != 0 && gd .columns != 0 {
gd .layoutEvenly (g , obj )
} else {
gd .layoutDynamic (g , obj )
}
revertAdjustments ()
return gd , nil
}
func (gd *gridDiagram ) layoutEvenly (g *d2graph .Graph , obj *d2graph .Object ) {
getObject := func (rowIndex , columnIndex int ) *d2graph .Object {
var index int
if gd .rowDirected {
index = rowIndex *gd .columns + columnIndex
} else {
index = columnIndex *gd .rows + rowIndex
}
if index < len (gd .objects ) {
return gd .objects [index ]
}
return nil
}
occupiedRows , occupiedColumns := gd .occupiedDimensions ()
rowHeights := make ([]float64 , 0 , occupiedRows )
colWidths := make ([]float64 , 0 , occupiedColumns )
for i := 0 ; i < occupiedRows ; i ++ {
rowHeight := 0.
for j := 0 ; j < occupiedColumns ; j ++ {
o := getObject (i , j )
if o == nil {
break
}
rowHeight = math .Max (rowHeight , o .Height )
}
rowHeights = append (rowHeights , rowHeight )
}
for j := 0 ; j < occupiedColumns ; j ++ {
columnWidth := 0.
for i := 0 ; i < occupiedRows ; i ++ {
o := getObject (i , j )
if o == nil {
break
}
columnWidth = math .Max (columnWidth , o .Width )
}
colWidths = append (colWidths , columnWidth )
}
horizontalGap := float64 (gd .horizontalGap )
verticalGap := float64 (gd .verticalGap )
cursor := geo .NewPoint (0 , 0 )
if gd .rowDirected {
for i := 0 ; i < occupiedRows ; i ++ {
for j := 0 ; j < occupiedColumns ; j ++ {
o := getObject (i , j )
if o == nil {
break
}
o .Width = colWidths [j ]
o .Height = rowHeights [i ]
o .MoveWithDescendantsTo (cursor .X , cursor .Y )
cursor .X += o .Width + horizontalGap
}
cursor .X = 0
cursor .Y += rowHeights [i ] + verticalGap
}
} else {
for j := 0 ; j < occupiedColumns ; j ++ {
for i := 0 ; i < occupiedRows ; i ++ {
o := getObject (i , j )
if o == nil {
break
}
o .Width = colWidths [j ]
o .Height = rowHeights [i ]
o .MoveWithDescendantsTo (cursor .X , cursor .Y )
cursor .Y += o .Height + verticalGap
}
cursor .X += colWidths [j ] + horizontalGap
cursor .Y = 0
}
}
totalWidth := float64 (gd .columns -1 ) * horizontalGap
totalHeight := float64 (gd .rows -1 ) * verticalGap
for _ , w := range colWidths {
totalWidth += w
}
for _ , h := range rowHeights {
totalHeight += h
}
gd .width = totalWidth
gd .height = totalHeight
}
func (gd *gridDiagram ) occupiedDimensions () (rows , columns int ) {
if len (gd .objects ) == 0 {
return 0 , 0
}
if gd .rowDirected {
return divideRoundUp (len (gd .objects ), gd .columns ), min (len (gd .objects ), gd .columns )
}
return min (len (gd .objects ), gd .rows ), divideRoundUp (len (gd .objects ), gd .rows )
}
func (gd *gridDiagram ) layoutDynamic (g *d2graph .Graph , obj *d2graph .Object ) {
horizontalGap := float64 (gd .horizontalGap )
verticalGap := float64 (gd .verticalGap )
var totalWidth , totalHeight float64
for _ , o := range gd .objects {
totalWidth += o .Width
totalHeight += o .Height
}
totalWidth += horizontalGap * float64 (len (gd .objects )-gd .rows )
totalHeight += verticalGap * float64 (len (gd .objects )-gd .columns )
var layout [][]*d2graph .Object
if gd .rowDirected {
targetWidth := totalWidth / float64 (gd .rows )
layout = gd .getBestLayout (targetWidth , false )
} else {
targetHeight := totalHeight / float64 (gd .columns )
layout = gd .getBestLayout (targetHeight , true )
}
cursor := geo .NewPoint (0 , 0 )
var maxY , maxX float64
if gd .rowDirected {
rowWidths := []float64 {}
for _ , row := range layout {
x := 0.
for _ , o := range row {
x += o .Width + horizontalGap
}
rowWidth := x - horizontalGap
rowWidths = append (rowWidths , rowWidth )
maxX = math .Max (maxX , rowWidth )
}
for i , row := range layout {
rowWidth := rowWidths [i ]
if rowWidth == maxX {
continue
}
delta := maxX - rowWidth
var widest float64
for _ , o := range row {
widest = math .Max (widest , o .Width )
}
diffs := make ([]float64 , len (row ))
totalDiff := 0.
for i , o := range row {
diffs [i ] = widest - o .Width
totalDiff += diffs [i ]
}
if totalDiff > 0 {
for i := range diffs {
diffs [i ] /= totalDiff
}
growth := math .Min (delta , totalDiff )
for i , o := range row {
o .Width += diffs [i ] * growth
}
}
if delta > totalDiff {
growth := (delta - totalDiff ) / float64 (len (row ))
for _ , o := range row {
o .Width += growth
}
}
}
for _ , row := range layout {
rowHeight := 0.
for _ , o := range row {
o .MoveWithDescendantsTo (cursor .X , cursor .Y )
cursor .X += o .Width + horizontalGap
rowHeight = math .Max (rowHeight , o .Height )
}
for _ , o := range row {
o .Height = rowHeight
}
cursor .X = 0
cursor .Y += rowHeight + verticalGap
}
maxY = cursor .Y - verticalGap
} else {
colHeights := []float64 {}
for _ , column := range layout {
y := 0.
for _ , o := range column {
y += o .Height + verticalGap
}
colHeight := y - verticalGap
colHeights = append (colHeights , colHeight )
maxY = math .Max (maxY , colHeight )
}
for i , column := range layout {
colHeight := colHeights [i ]
if colHeight == maxY {
continue
}
delta := maxY - colHeight
var tallest float64
for _ , o := range column {
tallest = math .Max (tallest , o .Height )
}
diffs := make ([]float64 , len (column ))
totalDiff := 0.
for i , o := range column {
diffs [i ] = tallest - o .Height
totalDiff += diffs [i ]
}
if totalDiff > 0 {
for i := range diffs {
diffs [i ] /= totalDiff
}
growth := math .Min (delta , totalDiff )
for i , o := range column {
o .Height += diffs [i ] * growth
}
}
if delta > totalDiff {
growth := (delta - totalDiff ) / float64 (len (column ))
for _ , o := range column {
o .Height += growth
}
}
}
for _ , column := range layout {
colWidth := 0.
for _ , o := range column {
o .MoveWithDescendantsTo (cursor .X , cursor .Y )
cursor .Y += o .Height + verticalGap
colWidth = math .Max (colWidth , o .Width )
}
for _ , o := range column {
o .Width = colWidth
}
cursor .Y = 0
cursor .X += colWidth + horizontalGap
}
maxX = cursor .X - horizontalGap
}
gd .width = maxX
gd .height = maxY
}
func (gd *gridDiagram ) getBestLayout (targetSize float64 , columns bool ) [][]*d2graph .Object {
var nCuts int
if columns {
nCuts = gd .columns - 1
} else {
nCuts = gd .rows - 1
}
if nCuts == 0 {
return GenLayout (gd .objects , nil )
}
var bestLayout [][]*d2graph .Object
bestDist := math .MaxFloat64
fastIsBest := false
if fastLayout := gd .fastLayout (targetSize , nCuts , columns ); fastLayout != nil {
dist := getDistToTarget (fastLayout , targetSize , float64 (gd .horizontalGap ), float64 (gd .verticalGap ), columns )
if dist == 0 {
return fastLayout
}
bestDist = dist
bestLayout = fastLayout
fastIsBest = true
}
var gap float64
if columns {
gap = float64 (gd .verticalGap )
} else {
gap = float64 (gd .horizontalGap )
}
getSize := func (o *d2graph .Object ) float64 {
if columns {
return o .Height
} else {
return o .Width
}
}
sizes := []float64 {}
for _ , obj := range gd .objects {
size := getSize (obj )
sizes = append (sizes , size )
}
sd := stddev (sizes )
skipCount := 0
count := 0
startingCache := make (map [int ]bool )
measurements := newGridRowMeasurements (sizes , gap )
okThreshold := STARTING_THRESHOLD
rowOk := func (start , end int , starting bool ) (ok bool ) {
n := end - start
if starting {
if ok , has := startingCache [n ]; has {
return ok
}
defer func () {
startingCache [n ] = ok
}()
}
rowSize := measurements .get (start , end ).size
if n > 1 {
rowSize += gap * float64 (n -1 )
if rowSize > okThreshold *targetSize {
skipCount ++
return skipCount >= SKIP_LIMIT
}
}
if rowSize < targetSize /okThreshold {
skipCount ++
return skipCount >= SKIP_LIMIT
}
return true
}
tryDivision := func (division []int ) bool {
dist := 0.
start := 0
for i := 0 ; i <= len (division ); i ++ {
end := len (gd .objects )
if i < len (division ) {
end = division [i ] + 1
}
dist += math .Abs (measurements .get (start , end ).withGap - targetSize )
start = end
}
if dist < bestDist {
bestLayout = GenLayout (gd .objects , division )
bestDist = dist
fastIsBest = false
} else if fastIsBest && dist == bestDist {
bestLayout = GenLayout (gd .objects , division )
fastIsBest = false
}
count ++
return count >= ATTEMPT_LIMIT || skipCount >= SKIP_LIMIT
}
thresholdAttempts := int (math .Ceil (sd ))
if thresholdAttempts < MIN_THRESHOLD_ATTEMPTS {
thresholdAttempts = MIN_THRESHOLD_ATTEMPTS
} else if thresholdAttempts > MAX_THRESHOLD_ATTEMPTS {
thresholdAttempts = MAX_THRESHOLD_ATTEMPTS
}
for i := 0 ; i < thresholdAttempts || bestLayout == nil ; i ++ {
count = 0.
skipCount = 0.
iterDivisions (len (gd .objects ), nCuts , tryDivision , rowOk )
okThreshold += THRESHOLD_STEP_SIZE
startingCache = make (map [int ]bool )
if skipCount == 0 {
break
}
if count == 0 && thresholdAttempts < MAX_THRESHOLD_ATTEMPTS {
thresholdAttempts ++
}
}
return bestLayout
}
func sum(values []float64 ) float64 {
s := 0.
for _ , v := range values {
s += v
}
return s
}
func avg(values []float64 ) float64 {
return sum (values ) / float64 (len (values ))
}
func variance(values []float64 ) float64 {
mean := avg (values )
total := 0.
for _ , value := range values {
dev := mean - value
total += dev * dev
}
return total / float64 (len (values ))
}
func stddev(values []float64 ) float64 {
return math .Sqrt (variance (values ))
}
func (gd *gridDiagram ) fastLayout (targetSize float64 , nCuts int , columns bool ) (layout [][]*d2graph .Object ) {
var gap float64
if columns {
gap = float64 (gd .verticalGap )
} else {
gap = float64 (gd .horizontalGap )
}
debt := 0.
fastDivision := make ([]int , 0 , nCuts )
rowSize := 0.
for i := 0 ; i < len (gd .objects ); i ++ {
o := gd .objects [i ]
var size float64
if columns {
size = o .Height
} else {
size = o .Width
}
if rowSize == 0 {
if size > targetSize -debt {
fastDivision = append (fastDivision , i )
newDebt := size - targetSize
debt += newDebt
} else {
rowSize += size
}
continue
}
if rowSize +gap +(size )/2. > targetSize -debt {
fastDivision = append (fastDivision , i -1 )
newDebt := rowSize - targetSize
debt += newDebt
rowSize = size
} else {
rowSize += gap + size
}
}
if len (fastDivision ) == nCuts {
layout = GenLayout (gd .objects , fastDivision )
}
return layout
}
func GenLayout (objects []*d2graph .Object , cutIndices []int ) [][]*d2graph .Object {
layout := make ([][]*d2graph .Object , len (cutIndices )+1 )
objIndex := 0
for i := 0 ; i <= len (cutIndices ); i ++ {
var stop int
if i < len (cutIndices ) {
stop = cutIndices [i ]
} else {
stop = len (objects ) - 1
}
if stop >= objIndex {
layout [i ] = make ([]*d2graph .Object , 0 , stop -objIndex +1 )
}
for ; objIndex <= stop ; objIndex ++ {
layout [i ] = append (layout [i ], objects [objIndex ])
}
}
return layout
}
func getDistToTarget(layout [][]*d2graph .Object , targetSize float64 , horizontalGap , verticalGap float64 , columns bool ) float64 {
totalDelta := 0.
for _ , row := range layout {
rowSize := 0.
for _ , o := range row {
if columns {
rowSize += o .Height + verticalGap
} else {
rowSize += o .Width + horizontalGap
}
}
if len (row ) > 0 {
if columns {
rowSize -= verticalGap
} else {
rowSize -= horizontalGap
}
}
totalDelta += math .Abs (rowSize - targetSize )
}
return totalDelta
}
func (gd *gridDiagram ) sizeForOutsideLabels () (revert func ()) {
margins := make (map [*d2graph .Object ]geo .Spacing )
for _ , o := range gd .objects {
margin := o .GetMargin ()
margins [o ] = margin
o .Height += margin .Top + margin .Bottom
o .Width += margin .Left + margin .Right
}
return func () {
for _ , o := range gd .objects {
m , has := margins [o ]
if !has {
continue
}
dy := m .Top + m .Bottom
dx := m .Left + m .Right
o .Height -= dy
o .Width -= dx
margin := o .GetMargin ()
marginX := margin .Left + margin .Right
marginY := margin .Top + margin .Bottom
if marginX < dx {
o .Width += dx - marginX
}
if marginY < dy {
o .Height += dy - marginY
}
if margin .Left > 0 || margin .Top > 0 {
o .MoveWithDescendants (margin .Left , margin .Top )
}
}
}
}
The pages are generated with Golds v0.8.7 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .