d2/d2layouts/d2grid/layout.go

578 lines
19 KiB
Go
Raw Normal View History

2023-04-01 00:18:17 +00:00
package d2grid
import (
"context"
"math"
2023-04-01 00:18:17 +00:00
"sort"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/label"
"oss.terrastruct.com/util-go/go2"
)
2023-04-03 18:36:01 +00:00
const (
CONTAINER_PADDING = 60
DEFAULT_GAP = 40
2023-04-03 18:36:01 +00:00
)
2023-04-01 00:18:17 +00:00
// Layout runs the grid layout on containers with rows/columns
// Note: children are not allowed edges or descendants
//
// 1. Traverse graph from root, skip objects with no rows/columns
// 2. Construct a grid with the container children
// 3. Remove the children from the main graph
// 4. Run grid layout
// 5. Set the resulting dimensions to the main graph shape
// 6. Run core layouts (without grid children)
// 7. Put grid children back in correct location
func Layout(ctx context.Context, g *d2graph.Graph, layout d2graph.LayoutGraph) d2graph.LayoutGraph {
return func(ctx context.Context, g *d2graph.Graph) error {
2023-04-05 18:11:31 +00:00
gridDiagrams, objectOrder, err := withoutGridDiagrams(ctx, g)
2023-04-01 00:18:17 +00:00
if err != nil {
return err
}
2023-04-05 18:11:31 +00:00
if g.Root.IsGridDiagram() && len(g.Root.ChildrenArray) != 0 {
2023-04-01 00:18:17 +00:00
g.Root.TopLeft = geo.NewPoint(0, 0)
} else if err := layout(ctx, g); err != nil {
return err
}
2023-04-05 18:11:31 +00:00
cleanup(g, gridDiagrams, objectOrder)
2023-04-01 00:18:17 +00:00
return nil
}
}
2023-04-05 18:11:31 +00:00
func withoutGridDiagrams(ctx context.Context, g *d2graph.Graph) (gridDiagrams map[string]*gridDiagram, objectOrder map[string]int, err error) {
2023-04-01 00:18:17 +00:00
toRemove := make(map[*d2graph.Object]struct{})
2023-04-05 18:11:31 +00:00
gridDiagrams = make(map[string]*gridDiagram)
2023-04-01 00:18:17 +00:00
if len(g.Objects) > 0 {
queue := make([]*d2graph.Object, 1, len(g.Objects))
queue[0] = g.Root
for len(queue) > 0 {
obj := queue[0]
queue = queue[1:]
if len(obj.ChildrenArray) == 0 {
continue
}
2023-04-05 18:11:31 +00:00
if !obj.IsGridDiagram() {
2023-04-01 00:18:17 +00:00
queue = append(queue, obj.ChildrenArray...)
continue
}
2023-04-05 18:11:31 +00:00
gd, err := layoutGrid(g, obj)
2023-04-01 00:18:17 +00:00
if err != nil {
return nil, nil, err
}
obj.Children = make(map[string]*d2graph.Object)
obj.ChildrenArray = nil
var dx, dy float64
2023-04-05 18:11:31 +00:00
width := gd.width + 2*CONTAINER_PADDING
labelWidth := float64(obj.LabelDimensions.Width) + 2*label.PADDING
if labelWidth > width {
dx = (labelWidth - width) / 2
width = labelWidth
}
2023-04-05 18:11:31 +00:00
height := gd.height + 2*CONTAINER_PADDING
labelHeight := float64(obj.LabelDimensions.Height) + 2*label.PADDING
if labelHeight > CONTAINER_PADDING {
// if the label doesn't fit within the padding, we need to add more
grow := labelHeight - CONTAINER_PADDING
dy = grow / 2
height += grow
}
// we need to center children if we have to expand to fit the container label
if dx != 0 || dy != 0 {
2023-04-05 18:11:31 +00:00
gd.shift(dx, dy)
}
obj.Box = geo.NewBox(nil, width, height)
2023-04-01 00:18:17 +00:00
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
2023-04-05 18:11:31 +00:00
gridDiagrams[obj.AbsID()] = gd
2023-04-01 00:18:17 +00:00
2023-04-06 21:30:45 +00:00
for _, o := range gd.objects {
toRemove[o] = struct{}{}
2023-04-01 00:18:17 +00:00
}
}
}
objectOrder = make(map[string]int)
layoutObjects := make([]*d2graph.Object, 0, len(toRemove))
for i, obj := range g.Objects {
objectOrder[obj.AbsID()] = i
if _, exists := toRemove[obj]; !exists {
layoutObjects = append(layoutObjects, obj)
}
}
g.Objects = layoutObjects
2023-04-05 18:11:31 +00:00
return gridDiagrams, objectOrder, nil
2023-04-01 00:18:17 +00:00
}
2023-04-05 18:11:31 +00:00
func layoutGrid(g *d2graph.Graph, obj *d2graph.Object) (*gridDiagram, error) {
gd := newGridDiagram(obj)
2023-04-05 03:02:22 +00:00
2023-04-05 18:11:31 +00:00
if gd.rows != 0 && gd.columns != 0 {
gd.layoutEvenly(g, obj)
2023-04-05 03:02:22 +00:00
} else {
2023-04-05 18:11:31 +00:00
gd.layoutDynamic(g, obj)
2023-04-05 03:02:22 +00:00
}
// position labels and icons
2023-04-06 21:30:45 +00:00
for _, o := range gd.objects {
if o.Attributes.Icon != nil {
o.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
o.IconPosition = go2.Pointer(string(label.InsideMiddleCenter))
2023-04-05 03:02:22 +00:00
} else {
2023-04-06 21:30:45 +00:00
o.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
2023-04-05 03:02:22 +00:00
}
}
2023-04-05 18:11:31 +00:00
return gd, nil
2023-04-05 03:02:22 +00:00
}
2023-04-05 18:11:31 +00:00
func (gd *gridDiagram) layoutEvenly(g *d2graph.Graph, obj *d2graph.Object) {
2023-04-06 21:30:45 +00:00
// layout objects in a grid with these 2 properties:
// all objects in the same row should have the same height
// all objects in the same column should have the same width
2023-04-05 03:02:22 +00:00
2023-04-06 21:30:45 +00:00
getObject := func(rowIndex, columnIndex int) *d2graph.Object {
2023-04-05 03:02:22 +00:00
var index int
2023-04-06 21:30:45 +00:00
if gd.rowDirected {
2023-04-05 18:11:31 +00:00
index = rowIndex*gd.columns + columnIndex
2023-04-05 03:02:22 +00:00
} else {
2023-04-05 18:11:31 +00:00
index = columnIndex*gd.rows + rowIndex
2023-04-05 03:02:22 +00:00
}
2023-04-06 21:30:45 +00:00
if index < len(gd.objects) {
return gd.objects[index]
2023-04-05 03:02:22 +00:00
}
return nil
}
2023-04-05 18:11:31 +00:00
rowHeights := make([]float64, 0, gd.rows)
colWidths := make([]float64, 0, gd.columns)
for i := 0; i < gd.rows; i++ {
2023-04-05 03:02:22 +00:00
rowHeight := 0.
2023-04-05 18:11:31 +00:00
for j := 0; j < gd.columns; j++ {
2023-04-06 21:30:45 +00:00
o := getObject(i, j)
if o == nil {
2023-04-05 03:02:22 +00:00
break
}
2023-04-06 21:30:45 +00:00
rowHeight = math.Max(rowHeight, o.Height)
2023-04-05 03:02:22 +00:00
}
rowHeights = append(rowHeights, rowHeight)
}
2023-04-05 18:11:31 +00:00
for j := 0; j < gd.columns; j++ {
2023-04-05 03:02:22 +00:00
columnWidth := 0.
2023-04-05 18:11:31 +00:00
for i := 0; i < gd.rows; i++ {
2023-04-06 21:30:45 +00:00
o := getObject(i, j)
if o == nil {
2023-04-05 03:02:22 +00:00
break
}
2023-04-06 21:30:45 +00:00
columnWidth = math.Max(columnWidth, o.Width)
2023-04-05 03:02:22 +00:00
}
colWidths = append(colWidths, columnWidth)
}
cursor := geo.NewPoint(0, 0)
2023-04-06 21:30:45 +00:00
if gd.rowDirected {
2023-04-05 18:11:31 +00:00
for i := 0; i < gd.rows; i++ {
for j := 0; j < gd.columns; j++ {
2023-04-06 21:30:45 +00:00
o := getObject(i, j)
if o == nil {
2023-04-05 03:02:22 +00:00
break
}
2023-04-06 21:30:45 +00:00
o.Width = colWidths[j]
o.Height = rowHeights[i]
o.TopLeft = cursor.Copy()
cursor.X += o.Width + float64(gd.gapColumns)
2023-04-05 03:02:22 +00:00
}
cursor.X = 0
cursor.Y += rowHeights[i] + float64(gd.gapRows)
2023-04-05 03:02:22 +00:00
}
} else {
2023-04-05 18:11:31 +00:00
for j := 0; j < gd.columns; j++ {
for i := 0; i < gd.rows; i++ {
2023-04-06 21:30:45 +00:00
o := getObject(i, j)
if o == nil {
2023-04-05 03:02:22 +00:00
break
}
2023-04-06 21:30:45 +00:00
o.Width = colWidths[j]
o.Height = rowHeights[i]
o.TopLeft = cursor.Copy()
cursor.Y += o.Height + float64(gd.gapRows)
2023-04-05 03:02:22 +00:00
}
cursor.X += colWidths[j] + float64(gd.gapColumns)
2023-04-05 03:02:22 +00:00
cursor.Y = 0
}
}
var totalWidth, totalHeight float64
for _, w := range colWidths {
totalWidth += w + float64(gd.gapColumns)
2023-04-05 03:02:22 +00:00
}
for _, h := range rowHeights {
totalHeight += h + float64(gd.gapRows)
2023-04-05 03:02:22 +00:00
}
totalWidth -= float64(gd.gapColumns)
totalHeight -= float64(gd.gapRows)
2023-04-05 18:11:31 +00:00
gd.width = totalWidth
gd.height = totalHeight
2023-04-05 03:02:22 +00:00
}
2023-04-05 18:11:31 +00:00
func (gd *gridDiagram) layoutDynamic(g *d2graph.Graph, obj *d2graph.Object) {
2023-04-06 21:30:45 +00:00
// assume we have the following objects to layout:
// . ┌A──────────────┐ ┌B──┐ ┌C─────────┐ ┌D────────┐ ┌E────────────────┐
// . └───────────────┘ │ │ │ │ │ │ │ │
// . │ │ └──────────┘ │ │ │ │
// . │ │ │ │ └─────────────────┘
// . └───┘ │ │
// . └─────────┘
2023-04-06 21:30:45 +00:00
// Note: if the grid is row dominant, all objects should be the same height (same width if column dominant)
// . ┌A─────────────┐ ┌B──┐ ┌C─────────┐ ┌D────────┐ ┌E────────────────┐
// . ├ ─ ─ ─ ─ ─ ─ ─┤ │ │ │ │ │ │ │ │
// . │ │ │ │ ├ ─ ─ ─ ─ ─┤ │ │ │ │
// . │ │ │ │ │ │ │ │ ├ ─ ─ ─ ─ ─ ─ ─ ─ ┤
// . │ │ ├ ─ ┤ │ │ │ │ │ │
// . └──────────────┘ └───┘ └──────────┘ └─────────┘ └─────────────────┘
// we want to split up the total width across the N rows or columns as evenly as possible
var totalWidth, totalHeight float64
2023-04-06 21:30:45 +00:00
for _, o := range gd.objects {
totalWidth += o.Width
totalHeight += o.Height
}
totalWidth += float64(gd.gapColumns * (len(gd.objects) - gd.rows))
totalHeight += float64(gd.gapRows * (len(gd.objects) - gd.columns))
var layout [][]*d2graph.Object
2023-04-06 21:30:45 +00:00
if gd.rowDirected {
2023-04-05 18:11:31 +00:00
targetWidth := totalWidth / float64(gd.rows)
layout = gd.getBestLayout(targetWidth, false)
} else {
2023-04-05 18:11:31 +00:00
targetHeight := totalHeight / float64(gd.columns)
layout = gd.getBestLayout(targetHeight, true)
}
2023-04-03 18:36:01 +00:00
cursor := geo.NewPoint(0, 0)
var maxY, maxX float64
2023-04-06 21:30:45 +00:00
if gd.rowDirected {
// if we have 2 rows, then each row's objects should have the same height
// . ┌A─────────────┐ ┌B──┐ ┌C─────────┐ ┬ maxHeight(A,B,C)
// . ├ ─ ─ ─ ─ ─ ─ ─┤ │ │ │ │ │
// . │ │ │ │ ├ ─ ─ ─ ─ ─┤ │
// . │ │ │ │ │ │ │
// . └──────────────┘ └───┘ └──────────┘ ┴
// . ┌D────────┐ ┌E────────────────┐ ┬ maxHeight(D,E)
// . │ │ │ │ │
// . │ │ │ │ │
// . │ │ ├ ─ ─ ─ ─ ─ ─ ─ ─ ┤ │
// . │ │ │ │ │
// . └─────────┘ └─────────────────┘ ┴
rowWidths := []float64{}
for _, row := range layout {
rowHeight := 0.
2023-04-06 21:30:45 +00:00
for _, o := range row {
o.TopLeft = cursor.Copy()
cursor.X += o.Width + float64(gd.gapColumns)
2023-04-06 21:30:45 +00:00
rowHeight = math.Max(rowHeight, o.Height)
}
rowWidth := cursor.X - float64(gd.gapColumns)
rowWidths = append(rowWidths, rowWidth)
maxX = math.Max(maxX, rowWidth)
2023-04-06 21:30:45 +00:00
// set all objects in row to the same height
for _, o := range row {
o.Height = rowHeight
}
// new row
cursor.X = 0
cursor.Y += rowHeight + float64(gd.gapRows)
2023-04-03 18:36:01 +00:00
}
maxY = cursor.Y - float64(gd.gapRows)
2023-04-06 21:30:45 +00:00
// then expand thinnest objects to make each row the same width
// . ┌A─────────────┐ ┌B──┐ ┌C─────────┐ ┬ maxHeight(A,B,C)
// . │ │ │ │ │ │ │
// . │ │ │ │ │ │ │
// . │ │ │ │ │ │ │
// . └──────────────┘ └───┘ └──────────┘ ┴
// . ┌D────────┬────┐ ┌E────────────────┐ ┬ maxHeight(D,E)
// . │ │ │ │ │
// . │ │ │ │ │ │
// . │ │ │ │ │
// . │ │ │ │ │ │
// . └─────────┴────┘ └─────────────────┘ ┴
for i, row := range layout {
rowWidth := rowWidths[i]
if rowWidth == maxX {
continue
}
delta := maxX - rowWidth
2023-04-06 21:30:45 +00:00
objects := []*d2graph.Object{}
var widest float64
2023-04-06 21:30:45 +00:00
for _, o := range row {
widest = math.Max(widest, o.Width)
objects = append(objects, o)
}
2023-04-06 21:30:45 +00:00
sort.Slice(objects, func(i, j int) bool {
return objects[i].Width < objects[j].Width
})
2023-04-06 21:30:45 +00:00
// expand smaller objects to fill remaining space
for _, o := range objects {
if o.Width < widest {
var index int
2023-04-06 21:30:45 +00:00
for i, rowObj := range row {
if o == rowObj {
index = i
break
}
}
2023-04-06 21:30:45 +00:00
grow := math.Min(widest-o.Width, delta)
o.Width += grow
// shift following objects
for i := index + 1; i < len(row); i++ {
row[i].TopLeft.X += grow
}
delta -= grow
if delta <= 0 {
break
}
}
}
if delta > 0 {
grow := delta / float64(len(row))
for i := len(row) - 1; i >= 0; i-- {
2023-04-06 21:30:45 +00:00
o := row[i]
o.TopLeft.X += grow * float64(i)
o.Width += grow
delta -= grow
}
}
}
} else {
2023-04-06 21:30:45 +00:00
// if we have 3 columns, then each column's objects should have the same width
// . ├maxWidth(A,B)─┤ ├maxW(C,D)─┤ ├maxWidth(E)──────┤
// . ┌A─────────────┐ ┌C─────────┐ ┌E────────────────┐
// . └──────────────┘ │ │ │ │
// . ┌B──┬──────────┐ └──────────┘ │ │
// . │ │ ┌D────────┬┐ └─────────────────┘
// . │ │ │ │ │
// . │ │ │ ││
// . └───┴──────────┘ │ │
// . │ ││
// . └─────────┴┘
2023-04-04 18:38:33 +00:00
colHeights := []float64{}
for _, column := range layout {
2023-04-04 18:38:33 +00:00
colWidth := 0.
2023-04-06 21:30:45 +00:00
for _, o := range column {
o.TopLeft = cursor.Copy()
cursor.Y += o.Height + float64(gd.gapRows)
2023-04-06 21:30:45 +00:00
colWidth = math.Max(colWidth, o.Width)
}
colHeight := cursor.Y - float64(gd.gapRows)
2023-04-04 18:38:33 +00:00
colHeights = append(colHeights, colHeight)
maxY = math.Max(maxY, colHeight)
2023-04-06 21:30:45 +00:00
// set all objects in column to the same width
for _, o := range column {
o.Width = colWidth
}
// new column
cursor.Y = 0
cursor.X += colWidth + float64(gd.gapColumns)
}
maxX = cursor.X - float64(gd.gapColumns)
2023-04-06 21:30:45 +00:00
// then expand shortest objects to make each column the same height
// . ├maxWidth(A,B)─┤ ├maxW(C,D)─┤ ├maxWidth(E)──────┤
// . ┌A─────────────┐ ┌C─────────┐ ┌E────────────────┐
// . ├ ─ ─ ─ ─ ─ ─ ┤ │ │ │ │
// . │ │ └──────────┘ │ │
// . └──────────────┘ ┌D─────────┐ ├ ─ ─ ─ ─ ─ ─ ─ ─ ┤
// . ┌B─────────────┐ │ │ │ │
// . │ │ │ │ │ │
// . │ │ │ │ │ │
// . │ │ │ │ │ │
// . └──────────────┘ └──────────┘ └─────────────────┘
2023-04-04 18:38:33 +00:00
for i, column := range layout {
colHeight := colHeights[i]
if colHeight == maxY {
continue
}
delta := maxY - colHeight
2023-04-06 21:30:45 +00:00
objects := []*d2graph.Object{}
2023-04-04 18:38:33 +00:00
var tallest float64
2023-04-06 21:30:45 +00:00
for _, o := range column {
tallest = math.Max(tallest, o.Height)
objects = append(objects, o)
2023-04-04 18:38:33 +00:00
}
2023-04-06 21:30:45 +00:00
sort.Slice(objects, func(i, j int) bool {
return objects[i].Height < objects[j].Height
2023-04-04 18:38:33 +00:00
})
2023-04-06 21:30:45 +00:00
// expand smaller objects to fill remaining space
for _, o := range objects {
if o.Height < tallest {
2023-04-04 18:38:33 +00:00
var index int
2023-04-06 21:30:45 +00:00
for i, colObj := range column {
if o == colObj {
2023-04-04 18:38:33 +00:00
index = i
break
}
}
2023-04-06 21:30:45 +00:00
grow := math.Min(tallest-o.Height, delta)
o.Height += grow
// shift following objects
2023-04-04 18:38:33 +00:00
for i := index + 1; i < len(column); i++ {
column[i].TopLeft.Y += grow
2023-04-04 18:38:33 +00:00
}
delta -= grow
if delta <= 0 {
break
}
}
}
if delta > 0 {
grow := delta / float64(len(column))
for i := len(column) - 1; i >= 0; i-- {
2023-04-06 21:30:45 +00:00
o := column[i]
o.TopLeft.Y += grow * float64(i)
o.Height += grow
2023-04-04 18:38:33 +00:00
delta -= grow
}
}
}
2023-04-03 18:36:01 +00:00
}
2023-04-05 18:11:31 +00:00
gd.width = maxX
gd.height = maxY
2023-04-03 18:36:01 +00:00
}
2023-04-06 21:30:45 +00:00
// 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 {
var nCuts int
if columns {
nCuts = gd.columns - 1
} else {
nCuts = gd.rows - 1
}
if nCuts == 0 {
2023-04-06 21:30:45 +00:00
return genLayout(gd.objects, nil)
}
// get all options for where to place these cuts, preferring later cuts over earlier cuts
2023-04-06 21:30:45 +00:00
// 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 └──┘ └──┘ └──┘
// . A │ B C D │ E ┌D───────────┐
// . A B │ C │ D E └────────────┘
// . A │ B C │ D E ┌E───────────┐
// . A │ B │ C D E └────────────┘
2023-04-06 21:30:45 +00:00
divisions := genDivisions(gd.objects, nCuts)
var bestLayout [][]*d2graph.Object
bestDist := math.MaxFloat64
// of these divisions, find the layout with rows closest to the targetSize
for _, division := range divisions {
2023-04-06 21:30:45 +00:00
layout := genLayout(gd.objects, division)
2023-04-11 03:02:54 +00:00
dist := getDistToTarget(layout, targetSize, gd.gapRows, gd.gapColumns, columns)
if dist < bestDist {
bestLayout = layout
bestDist = dist
}
}
return bestLayout
}
2023-04-06 21:30:45 +00:00
// get all possible divisions of objects by the number of cuts
func genDivisions(objects []*d2graph.Object, nCuts int) (divisions [][]int) {
if len(objects) < 2 || nCuts == 0 {
return nil
}
2023-04-06 21:30:45 +00:00
// we go in this order to prefer extra objects in starting rows rather than later ones
lastObj := len(objects) - 1
for index := lastObj; index >= nCuts; index-- {
if nCuts > 1 {
2023-04-06 21:30:45 +00:00
for _, inner := range genDivisions(objects[:index], nCuts-1) {
divisions = append(divisions, append(inner, index-1))
}
} else {
divisions = append(divisions, []int{index - 1})
}
}
return divisions
}
2023-04-06 21:30:45 +00:00
// generate a grid of objects from the given cut indices
func genLayout(objects []*d2graph.Object, cutIndices []int) [][]*d2graph.Object {
layout := make([][]*d2graph.Object, len(cutIndices)+1)
2023-04-06 21:30:45 +00:00
objIndex := 0
for i := 0; i <= len(cutIndices); i++ {
var stop int
if i < len(cutIndices) {
stop = cutIndices[i]
} else {
2023-04-06 21:30:45 +00:00
stop = len(objects) - 1
}
2023-04-06 21:30:45 +00:00
for ; objIndex <= stop; objIndex++ {
layout[i] = append(layout[i], objects[objIndex])
}
}
return layout
}
func getDistToTarget(layout [][]*d2graph.Object, targetSize float64, gapRows, gapColumns int, columns bool) float64 {
totalDelta := 0.
for _, row := range layout {
rowSize := 0.
2023-04-06 21:30:45 +00:00
for _, o := range row {
if columns {
rowSize += o.Height + float64(gapRows)
} else {
rowSize += o.Width + float64(gapColumns)
}
}
totalDelta += math.Abs(rowSize - targetSize)
}
return totalDelta
}
2023-04-01 00:18:17 +00:00
// cleanup restores the graph after the core layout engine finishes
// - translating the grid to its position placed by the core layout engine
// - restore the children of the grid
// - sorts objects to their original graph order
2023-04-05 18:11:31 +00:00
func cleanup(graph *d2graph.Graph, gridDiagrams map[string]*gridDiagram, objectsOrder map[string]int) {
defer func() {
sort.SliceStable(graph.Objects, func(i, j int) bool {
return objectsOrder[graph.Objects[i].AbsID()] < objectsOrder[graph.Objects[j].AbsID()]
})
}()
2023-04-05 18:11:31 +00:00
if graph.Root.IsGridDiagram() {
gd, exists := gridDiagrams[graph.Root.AbsID()]
if exists {
2023-04-05 18:11:31 +00:00
gd.cleanup(graph.Root, graph)
return
}
2023-04-01 00:18:17 +00:00
}
for _, obj := range graph.Objects {
2023-04-05 18:11:31 +00:00
gd, exists := gridDiagrams[obj.AbsID()]
2023-04-03 18:36:01 +00:00
if !exists {
2023-04-01 00:18:17 +00:00
continue
}
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
// shift the grid from (0, 0)
2023-04-05 18:11:31 +00:00
gd.shift(
2023-04-01 00:18:17 +00:00
obj.TopLeft.X+CONTAINER_PADDING,
obj.TopLeft.Y+CONTAINER_PADDING,
)
2023-04-05 18:11:31 +00:00
gd.cleanup(obj, graph)
2023-04-01 00:18:17 +00:00
}
}