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
|
// 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
|
// if columns is true, each column aims to have the targetSize height
|
||||||
func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2graph.Object {
|
func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2graph.Object {
|
||||||
|
debug := false
|
||||||
var nCuts int
|
var nCuts int
|
||||||
if columns {
|
if columns {
|
||||||
nCuts = gd.columns - 1
|
nCuts = gd.columns - 1
|
||||||
|
|
@ -476,6 +477,22 @@ func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2gr
|
||||||
return genLayout(gd.objects, nil)
|
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
|
var gap float64
|
||||||
if columns {
|
if columns {
|
||||||
gap = float64(gd.verticalGap)
|
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
|
skipCount := 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
|
// 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 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 {
|
||||||
|
// there may even be too many to skip
|
||||||
|
return true
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 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 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
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
|
// 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:
|
// with 5 objects and 2 cuts we have these options:
|
||||||
// . A B C │ D │ E <- these cuts would produce: ┌A─┐ ┌B─┐ ┌C─┐
|
// . 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++
|
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
|
return count >= attemptLimit || skipCount >= skipLimit
|
||||||
}
|
}
|
||||||
|
|
||||||
// try at least 3 different okThresholds
|
// try number of different okThresholds depending on std deviation of sizes
|
||||||
for i := 0; i < 3 || bestLayout == nil; i++ {
|
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)
|
iterDivisions(gd.objects, nCuts, tryDivision, rowOk)
|
||||||
okThreshold += thresholdStep
|
okThreshold += thresholdStep
|
||||||
if debug {
|
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)
|
startingCache = make(map[int]bool)
|
||||||
count = 0.
|
if skipCount == 0 {
|
||||||
}
|
// threshold isn't skipping anything so increasing it won't help
|
||||||
if debug {
|
break
|
||||||
fmt.Printf("final count %d, skip count %d\n", count, skipCount)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
debt := 0.
|
||||||
fastDivision := make([]int, 0, nCuts)
|
fastDivision := make([]int, 0, nCuts)
|
||||||
rowSize := 0.
|
rowSize := 0.
|
||||||
for i := 0; i < len(gd.objects); i++ {
|
for i := 0; i < len(gd.objects); i++ {
|
||||||
o := 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 rowSize == 0 {
|
||||||
if size > targetSize-debt {
|
if size > targetSize-debt {
|
||||||
fastDivision = append(fastDivision, i-1)
|
fastDivision = append(fastDivision, i-1)
|
||||||
|
|
@ -602,14 +689,10 @@ func (gd *gridDiagram) getBestLayout(targetSize float64, columns bool) [][]*d2gr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(fastDivision) == nCuts {
|
if len(fastDivision) == nCuts {
|
||||||
layout := genLayout(gd.objects, fastDivision)
|
layout = genLayout(gd.objects, fastDivision)
|
||||||
dist := getDistToTarget(layout, targetSize, float64(gd.horizontalGap), float64(gd.verticalGap), columns)
|
|
||||||
if dist < bestDist {
|
|
||||||
bestLayout = layout
|
|
||||||
bestDist = dist
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return bestLayout
|
|
||||||
|
return layout
|
||||||
}
|
}
|
||||||
|
|
||||||
// process current division, return true to stop iterating
|
// process current division, return true to stop iterating
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue