improve layoutDynamic performance when evenly sized
This commit is contained in:
parent
87117d2946
commit
595cdd4b0b
1 changed files with 105 additions and 22 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue