diff --git a/d2layouts/d2grid/layout.go b/d2layouts/d2grid/layout.go index d46475518..2927a3c23 100644 --- a/d2layouts/d2grid/layout.go +++ b/d2layouts/d2grid/layout.go @@ -466,6 +466,7 @@ func (gd *gridDiagram) layoutDynamic(g *d2graph.Graph, obj *d2graph.Object) { // 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 func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2graph.Object { + debug := false var nCuts int if columns { nCuts = gd.columns - 1 @@ -476,6 +477,22 @@ func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2gr return genLayout(gd.objects, nil) } + var bestLayout [][]*d2graph.Object + bestDist := math.MaxFloat64 + + // try fast layout algorithm as a baseline + bestLayout = gd.fastLayout(targetSize, nCuts, columns) + if bestLayout != nil { + dist := getDistToTarget(bestLayout, targetSize, float64(gd.horizontalGap), float64(gd.verticalGap), columns) + if debug { + fmt.Printf("fast dist %v dist per row %v\n", dist, dist/(float64(nCuts)+1)) + } + if dist == 0 { + return bestLayout + } + bestDist = dist + } + var gap float64 if columns { gap = float64(gd.verticalGap) @@ -490,8 +507,21 @@ func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2gr } } - debug := false + sizes := []float64{} + for _, obj := range gd.objects { + size := getSize(obj) + sizes = append(sizes, size) + } + sd := stddev(sizes) + if debug { + fmt.Printf("sizes (%d): %v\n", len(sizes), sizes) + fmt.Printf("std dev: %v\n", sd) + } + skipCount := 0 + count := 0 + attemptLimit := 100_000 + skipLimit := 100 * attemptLimit // quickly eliminate bad row groupings startingCache := make(map[int]bool) // try to find a layout with all rows within 1.2*targetSize @@ -523,21 +553,24 @@ func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2gr // if multiple nodes are too big, it isn't ok. but a single node can't shrink so only check here if rowSize > okThreshold*targetSize { skipCount++ + if skipCount >= skipLimit { + // there may even be too many to skip + return true + } return false } } // row is too small to be good overall if rowSize < targetSize/okThreshold { skipCount++ + if skipCount >= skipLimit { + return true + } return false } return true } - var bestLayout [][]*d2graph.Object - bestDist := math.MaxFloat64 - count := 0 - attemptLimit := 100_000 // get all options for where to place these cuts, preferring later cuts over earlier cuts // with 5 objects and 2 cuts we have these options: // . A B C │ D │ E <- these cuts would produce: ┌A─┐ ┌B─┐ ┌C─┐ @@ -556,30 +589,84 @@ func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2gr } count++ // with few objects we can try all options to get best result but this won't scale, so only try up to 100k options - return count >= attemptLimit + return count >= attemptLimit || skipCount >= skipLimit } - // try at least 3 different okThresholds - for i := 0; i < 3 || bestLayout == nil; i++ { + // try number of different okThresholds depending on std deviation of sizes + thresholds := int(math.Min(math.Max(1, math.Ceil(sd)), 3)) + for i := 0; i < thresholds || bestLayout == nil; i++ { + count = 0. + skipCount = 0. iterDivisions(gd.objects, nCuts, tryDivision, rowOk) okThreshold += thresholdStep if debug { - fmt.Printf("increasing ok threshold to %v\n", okThreshold) + fmt.Printf("count %d, skip count %d, bestDist %v increasing ok threshold to %v\n", count, skipCount, bestDist, okThreshold) } startingCache = make(map[int]bool) - count = 0. - } - if debug { - fmt.Printf("final count %d, skip count %d\n", count, skipCount) + if skipCount == 0 { + // threshold isn't skipping anything so increasing it won't help + break + } + } + + if debug { + i := 0 + fmt.Printf("best layout: [\n") + for _, r := range bestLayout { + vals := sizes[i : i+len(r)] + fmt.Printf("%v \t=%v\n", vals, sum(vals)) + i += len(r) + } + fmt.Printf("]\n") + } + 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) } - // try fast layout algorithm, see if it is better than first 1mil attempts debt := 0. fastDivision := make([]int, 0, nCuts) rowSize := 0. for i := 0; i < len(gd.objects); i++ { o := gd.objects[i] - size := getSize(o) + var size float64 + if columns { + size = o.Height + } else { + size = o.Width + } if rowSize == 0 { if size > targetSize-debt { fastDivision = append(fastDivision, i-1) @@ -602,14 +689,10 @@ func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2gr } } if len(fastDivision) == nCuts { - layout := genLayout(gd.objects, fastDivision) - dist := getDistToTarget(layout, targetSize, float64(gd.horizontalGap), float64(gd.verticalGap), columns) - if dist < bestDist { - bestLayout = layout - bestDist = dist - } + layout = genLayout(gd.objects, fastDivision) } - return bestLayout + + return layout } // process current division, return true to stop iterating