improve layoutDynamic performance when evenly sized

This commit is contained in:
Gavin Nishizawa 2023-05-09 15:05:56 -07:00
parent 87117d2946
commit 595cdd4b0b
No known key found for this signature in database
GPG key ID: AE3B177777CE55CD

View file

@ -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 skipCount == 0 {
// threshold isn't skipping anything so increasing it won't help
break
}
if debug {
fmt.Printf("final count %d, skip count %d\n", count, skipCount)
}
// try fast layout algorithm, see if it is better than first 1mil attempts
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)
}
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