This commit is contained in:
Gavin Nishizawa 2023-05-09 17:19:33 -07:00
parent 595cdd4b0b
commit f64588ff89
No known key found for this signature in database
GPG key ID: AE3B177777CE55CD
2 changed files with 56 additions and 27 deletions

View file

@ -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
)

View file

@ -1,6 +1,7 @@
package d2grid package d2grid
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"math" "math"
@ -479,18 +480,19 @@ func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2gr
var bestLayout [][]*d2graph.Object var bestLayout [][]*d2graph.Object
bestDist := math.MaxFloat64 bestDist := math.MaxFloat64
fastIsBest := false
// try fast layout algorithm as a baseline // try fast layout algorithm as a baseline
bestLayout = gd.fastLayout(targetSize, nCuts, columns) if fastLayout := gd.fastLayout(targetSize, nCuts, columns); fastLayout != nil {
if bestLayout != nil { dist := getDistToTarget(fastLayout, targetSize, float64(gd.horizontalGap), float64(gd.verticalGap), columns)
dist := getDistToTarget(bestLayout, targetSize, float64(gd.horizontalGap), float64(gd.verticalGap), columns)
if debug { if debug {
fmt.Printf("fast dist %v dist per row %v\n", dist, dist/(float64(nCuts)+1)) fmt.Printf("fast dist %v dist per row %v\n", dist, dist/(float64(nCuts)+1))
} }
if dist == 0 { if dist == 0 {
return bestLayout return fastLayout
} }
bestDist = dist bestDist = dist
bestLayout = fastLayout
fastIsBest = true
} }
var gap float64 var gap float64
@ -515,22 +517,16 @@ func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2gr
sd := stddev(sizes) sd := stddev(sizes)
if debug { if debug {
fmt.Printf("sizes (%d): %v\n", len(sizes), sizes) 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 skipCount := 0
count := 0 count := 0
attemptLimit := 100_000
skipLimit := 100 * attemptLimit
// quickly eliminate bad row groupings // quickly eliminate bad row groupings
startingCache := make(map[int]bool) 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, // 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. // but the best option may require a few rows that are far from the target size.
okThreshold := 1.2 okThreshold := STARTING_THRESHOLD
// if we don't find a layout try 25% larger threshold
thresholdStep := 0.25
rowOk := func(row []*d2graph.Object, starting bool) (ok bool) { rowOk := func(row []*d2graph.Object, starting bool) (ok bool) {
if starting { if starting {
// we can cache results from starting positions since they repeat and don't change // 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 multiple nodes are too big, it isn't ok. but a single node can't shrink so only check here
if rowSize > okThreshold*targetSize { if rowSize > okThreshold*targetSize {
skipCount++ skipCount++
if skipCount >= skipLimit { if skipCount >= SKIP_LIMIT {
// there may even be too many to skip // there may even be too many to skip
return true return true
} }
@ -563,7 +559,7 @@ func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2gr
// row is too small to be good overall // row is too small to be good overall
if rowSize < targetSize/okThreshold { if rowSize < targetSize/okThreshold {
skipCount++ skipCount++
if skipCount >= skipLimit { if skipCount >= SKIP_LIMIT {
return true return true
} }
return false return false
@ -586,19 +582,29 @@ func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2gr
if dist < bestDist { if dist < bestDist {
bestLayout = layout bestLayout = layout
bestDist = dist bestDist = dist
fastIsBest = false
} else if fastIsBest && dist == bestDist {
// prefer ordered search solution to fast layout solution
bestLayout = layout
fastIsBest = false
} }
count++ 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 // 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 // try number of different okThresholds depending on std deviation of sizes
thresholds := int(math.Min(math.Max(1, math.Ceil(sd)), 3)) thresholdAttempts := int(math.Ceil(sd))
for i := 0; i < thresholds || bestLayout == nil; i++ { 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. count = 0.
skipCount = 0. skipCount = 0.
iterDivisions(gd.objects, nCuts, tryDivision, rowOk) iterDivisions(gd.objects, nCuts, tryDivision, rowOk)
okThreshold += thresholdStep okThreshold += THRESHOLD_STEP_SIZE
if debug { if debug {
fmt.Printf("count %d, skip count %d, bestDist %v increasing ok threshold to %v\n", count, skipCount, bestDist, okThreshold) 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 // threshold isn't skipping anything so increasing it won't help
break 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 { if debug {
i := 0 fmt.Printf("best layout: %v\n", layoutString(bestLayout, sizes))
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 return bestLayout
} }
@ -695,6 +698,19 @@ func (gd *gridDiagram) fastLayout(targetSize float64, nCuts int, columns bool) (
return layout 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 // process current division, return true to stop iterating
type iterDivision func(division []int) (done bool) type iterDivision func(division []int) (done bool)
type checkCut func(objects []*d2graph.Object, starting bool) (ok bool) type checkCut func(objects []*d2graph.Object, starting bool) (ok bool)