diff --git a/d2layouts/d2grid/constants.go b/d2layouts/d2grid/constants.go new file mode 100644 index 000000000..5542d90ac --- /dev/null +++ b/d2layouts/d2grid/constants.go @@ -0,0 +1,13 @@ +package d2grid + +const ( + // don't consider layouts with rows longer than targetSize*1.2 or shorter than targetSize/1.2 + STARTING_THRESHOLD = 1.2 + // next try layouts with a 25% larger threshold + THRESHOLD_STEP_SIZE = 0.25 + MIN_THRESHOLD_ATTEMPTS = 1 + MAX_THRESHOLD_ATTEMPTS = 3 + + ATTEMPT_LIMIT = 100_000 + SKIP_LIMIT = 10_000_000 +) diff --git a/d2layouts/d2grid/layout.go b/d2layouts/d2grid/layout.go index 2927a3c23..5566bff49 100644 --- a/d2layouts/d2grid/layout.go +++ b/d2layouts/d2grid/layout.go @@ -1,6 +1,7 @@ package d2grid import ( + "bytes" "context" "fmt" "math" @@ -479,18 +480,19 @@ func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2gr var bestLayout [][]*d2graph.Object bestDist := math.MaxFloat64 - + fastIsBest := false // 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 fastLayout := gd.fastLayout(targetSize, nCuts, columns); fastLayout != nil { + dist := getDistToTarget(fastLayout, 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 + return fastLayout } bestDist = dist + bestLayout = fastLayout + fastIsBest = true } var gap float64 @@ -515,22 +517,16 @@ func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2gr sd := stddev(sizes) if debug { fmt.Printf("sizes (%d): %v\n", len(sizes), sizes) - fmt.Printf("std dev: %v\n", sd) + fmt.Printf("std dev: %v; targetSize %v\n", sd, targetSize) } 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 - // skip options with a row that is 1.2*longer or shorter // Note: we want a low threshold to explore good options within attemptLimit, // but the best option may require a few rows that are far from the target size. - okThreshold := 1.2 - // if we don't find a layout try 25% larger threshold - thresholdStep := 0.25 + okThreshold := STARTING_THRESHOLD rowOk := func(row []*d2graph.Object, starting bool) (ok bool) { if starting { // we can cache results from starting positions since they repeat and don't change @@ -553,7 +549,7 @@ 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 { + if skipCount >= SKIP_LIMIT { // there may even be too many to skip return true } @@ -563,7 +559,7 @@ func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2gr // row is too small to be good overall if rowSize < targetSize/okThreshold { skipCount++ - if skipCount >= skipLimit { + if skipCount >= SKIP_LIMIT { return true } return false @@ -586,19 +582,29 @@ func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2gr if dist < bestDist { bestLayout = layout bestDist = dist + fastIsBest = false + } else if fastIsBest && dist == bestDist { + // prefer ordered search solution to fast layout solution + bestLayout = layout + fastIsBest = false } 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 || skipCount >= skipLimit + return count >= ATTEMPT_LIMIT || skipCount >= SKIP_LIMIT } // 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++ { + 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(gd.objects, nCuts, tryDivision, rowOk) - okThreshold += thresholdStep + okThreshold += THRESHOLD_STEP_SIZE if debug { fmt.Printf("count %d, skip count %d, bestDist %v increasing ok threshold to %v\n", count, skipCount, bestDist, okThreshold) } @@ -607,17 +613,14 @@ func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2gr // threshold isn't skipping anything so increasing it won't help break } + // okThreshold isn't high enough yet, we skipped every option so don't count it + if count == 0 && thresholdAttempts < MAX_THRESHOLD_ATTEMPTS { + thresholdAttempts++ + } } 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") + fmt.Printf("best layout: %v\n", layoutString(bestLayout, sizes)) } return bestLayout } @@ -695,6 +698,19 @@ func (gd *gridDiagram) fastLayout(targetSize float64, nCuts int, columns bool) ( return layout } +func layoutString(layout [][]*d2graph.Object, sizes []float64) string { + buf := &bytes.Buffer{} + i := 0 + fmt.Fprintf(buf, "[\n") + for _, r := range layout { + vals := sizes[i : i+len(r)] + fmt.Fprintf(buf, "%v:\t%v\n", sum(vals), vals) + i += len(r) + } + fmt.Fprintf(buf, "]\n") + return buf.String() +} + // process current division, return true to stop iterating type iterDivision func(division []int) (done bool) type checkCut func(objects []*d2graph.Object, starting bool) (ok bool)