layout

package
v1.6.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 13, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

Documentation

Overview

Package layout computes block positions for tower visualizations.

Overview

Once a DAG has been normalized and its rows ordered, this package computes the exact pixel coordinates for each block. The layout algorithm produces a complete Layout containing all information needed for rendering:

  • Block positions (left, right, top, bottom coordinates)
  • Row orderings (the left-to-right sequence within each layer)
  • Frame dimensions and margins

Width Allocation

Block widths are computed based on support relationships—blocks that carry more weight (support more nodes above) receive more width. This creates a visual hierarchy that reinforces the tower metaphor.

Two width-flow directions are available:

  • Bottom-up (default): Width flows from sinks (foundations) upward. Foundational packages appear wider, supporting narrower blocks above.

  • Top-down: Width flows from roots downward. The application at the top is widest, with dependencies progressively narrower below.

Height Calculation

Row heights are uniform for regular nodes, with auxiliary rows (containing only separator beams) receiving reduced height based on WithAuxiliaryRatio.

Building a Layout

Use Build with a normalized DAG and frame dimensions:

l := layout.Build(g, 800, 600,
    layout.WithMarginRatio(0.05),
)

The default orderer is ordering.OptimalSearch with a 60-second timeout. For faster but potentially suboptimal layouts, use ordering.Barycentric:

l := layout.Build(g, 800, 600,
    layout.WithOrderer(ordering.Barycentric{}),
)

The returned Layout contains a Block for each node with computed coordinates ready for rendering.

Options

Block Coordinates

Each Block provides:

  • NodeID: The node this block represents
  • Left, Right: Horizontal bounds
  • Bottom, Top: Vertical bounds (origin at top-left, Y increases downward)
  • MidX, MidY: Center coordinates (for text placement)

Integration

The layout package sits between ordering and rendering in the pipeline:

DAG → transform.Normalize → ordering.OrderRows → layout.Build → sink.RenderSVG

Sinks in render/tower/sink consume the Layout to produce final output in various formats (SVG, JSON, PDF, PNG).

render/tower/sink: github.com/stacktower-io/stacktower/pkg/core/render/tower/sink

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func ComputeWidths

func ComputeWidths(g *dag.DAG, orders map[int][]string, frameWidth float64) map[string]float64

ComputeWidths assigns horizontal widths to nodes by distributing the frame width among top-level nodes and propagating that width down to children. This results in "top-heavy" towers where the root nodes are wide.

func ComputeWidthsBottomUp

func ComputeWidthsBottomUp(g *dag.DAG, orders map[int][]string, frameWidth float64) map[string]float64

ComputeWidthsBottomUp assigns horizontal widths to nodes by distributing the frame width among bottom-level nodes and propagating that width up to parents. This results in "bottom-heavy" towers where the leaf nodes provide a wide base.

func EnsureLayered

func EnsureLayered(g *dag.DAG)

EnsureLayered ensures the graph has row assignments for tower layout. If the graph has no rows assigned (MaxRow == 0), this assigns layers. This modifies the graph in place.

Types

type Block

type Block struct {
	NodeID      string
	Left, Right float64
	Bottom, Top float64
}

Block represents a single rectangular element in the tower layout. All coordinates are in user units (typically pixels in SVG).

Example
package main

import (
	"fmt"

	"github.com/stacktower-io/stacktower/pkg/core/dag"
	"github.com/stacktower-io/stacktower/pkg/core/render/tower/layout"
)

func main() {
	g := dag.New(nil)
	_ = g.AddNode(dag.Node{ID: "fastapi", Row: 0, Meta: dag.Metadata{"version": "0.100.0"}})

	l := layout.Build(g, 400, 300)
	block := l.Blocks["fastapi"]

	// Block contains all rendering information
	fmt.Println("NodeID:", block.NodeID)
	fmt.Println("Has dimensions:", block.Right > block.Left && block.Top > block.Bottom)
	fmt.Println("Has center:", block.CenterX() > 0 && block.CenterY() > 0)
}
Output:
NodeID: fastapi
Has dimensions: true
Has center: true

func (Block) CenterX

func (b Block) CenterX() float64

CenterX returns the horizontal center point of the block.

func (Block) CenterY

func (b Block) CenterY() float64

CenterY returns the vertical center point of the block.

func (Block) Height

func (b Block) Height() float64

Height returns the vertical span of the block.

func (Block) Width

func (b Block) Width() float64

Width returns the horizontal span of the block.

type Layout

type Layout struct {
	FrameWidth  float64
	FrameHeight float64
	Blocks      map[string]Block
	RowOrders   map[int][]string
	MarginX     float64
	MarginY     float64

	// Metadata fields for rendering configuration
	Style     string // Render style: "simple", "handdrawn"
	Seed      uint64 // Random seed for reproducible rendering
	Randomize bool   // Whether block widths were randomized
	Merged    bool   // Whether subdividers were merged

	// Nebraska contains maintainer ranking data (computed during layout)
	Nebraska []feature.NebraskaRanking
}

Layout represents the computed physical positions and dimensions of all blocks in a tower visualization, along with rendering metadata.

This is the internal representation used during layout computation and rendering. For serialization (JSON files, API responses, caching), convert to graph.Layout using the Export() method. Use Parse() to convert back from serialized form.

The key difference from graph.Layout:

  • This type: optimized for computation (map-based block lookup, computed values)
  • graph.Layout: optimized for serialization (slice-based, JSON-friendly)

func Build

func Build(g *dag.DAG, width, height float64, opts ...Option) Layout

Build computes a physical layout for the given DAG within the specified width and height constraints. It applies row ordering, width computation, and coordinate assignment.

Build requires that the graph has row assignments. If the graph was loaded from a file without normalization, call EnsureLayered first, or the caller should handle layer assignment.

Example
package main

import (
	"fmt"

	"github.com/stacktower-io/stacktower/pkg/core/dag"
	"github.com/stacktower-io/stacktower/pkg/core/render/tower/layout"
)

func main() {
	// Create a simple dependency graph
	g := dag.New(nil)
	_ = g.AddNode(dag.Node{ID: "app", Row: 0})
	_ = g.AddNode(dag.Node{ID: "lib", Row: 1})
	_ = g.AddNode(dag.Node{ID: "core", Row: 2})
	_ = g.AddEdge(dag.Edge{From: "app", To: "lib"})
	_ = g.AddEdge(dag.Edge{From: "lib", To: "core"})

	// Build layout with 800x600 frame
	l := layout.Build(g, 800, 600)

	fmt.Println("Frame:", l.FrameWidth, "x", l.FrameHeight)
	fmt.Println("Block count:", len(l.Blocks))
	fmt.Println("Row count:", len(l.RowOrders))
}
Output:
Frame: 800 x 600
Block count: 3
Row count: 3
Example (TopDownWidths)
package main

import (
	"fmt"

	"github.com/stacktower-io/stacktower/pkg/core/dag"
	"github.com/stacktower-io/stacktower/pkg/core/render/tower/layout"
)

func main() {
	g := dag.New(nil)
	_ = g.AddNode(dag.Node{ID: "app", Row: 0})
	_ = g.AddNode(dag.Node{ID: "auth", Row: 1})
	_ = g.AddNode(dag.Node{ID: "cache", Row: 1})
	_ = g.AddNode(dag.Node{ID: "db", Row: 2})
	_ = g.AddEdge(dag.Edge{From: "app", To: "auth"})
	_ = g.AddEdge(dag.Edge{From: "app", To: "cache"})
	_ = g.AddEdge(dag.Edge{From: "auth", To: "db"})
	_ = g.AddEdge(dag.Edge{From: "cache", To: "db"})

	// Top-down: width flows from roots downward
	// Db is shared by both auth and cache, so it receives combined width
	l := layout.Build(g, 800, 600, layout.WithTopDownWidths())

	// All nodes in this balanced graph have reasonable widths
	appBlock := l.Blocks["app"]
	dbBlock := l.Blocks["db"]

	fmt.Println("App has width:", appBlock.Width() > 0)
	fmt.Println("Db has width:", dbBlock.Width() > 0)
}
Output:
App has width: true
Db has width: true
Example (WithOptions)
package main

import (
	"fmt"

	"github.com/stacktower-io/stacktower/pkg/core/dag"
	"github.com/stacktower-io/stacktower/pkg/core/render/tower/layout"
	"github.com/stacktower-io/stacktower/pkg/core/render/tower/ordering"
)

func main() {
	g := dag.New(nil)
	_ = g.AddNode(dag.Node{ID: "app", Row: 0})
	_ = g.AddNode(dag.Node{ID: "lib", Row: 1})
	_ = g.AddEdge(dag.Edge{From: "app", To: "lib"})

	// Build with custom options
	l := layout.Build(g, 800, 600,
		layout.WithOrderer(ordering.Barycentric{Passes: 24}),
		layout.WithMarginRatio(0.1),     // 10% margins
		layout.WithAuxiliaryRatio(0.15), // Aux rows at 15% height
	)

	fmt.Println("Margin X:", l.MarginX)
	fmt.Println("Margin Y:", l.MarginY)
}
Output:
Margin X: 80
Margin Y: 60

func Parse

func Parse(layout graph.Layout) (Layout, error)

Parse converts a serialized layout to an internal tower layout.

Use this when you need to render from a previously serialized layout:

  • Loading from JSON file (via graph.ReadLayoutFile)
  • Receiving from API/cache

Returns an error if the layout is not a tower type (VizType must be "tower" or empty).

func (Layout) Export

func (l Layout) Export(g *dag.DAG) (graph.Layout, error)

Export converts an internal tower layout to the serialization format.

Use this when you need to serialize the layout for:

  • JSON file output (via graph.WriteLayoutFile)
  • API responses
  • Caching

The DAG is optional but recommended for metadata enrichment (URLs, brittle flags, etc.).

type Option

type Option func(*config)

Option configures the layout generation process.

func WithAuxiliaryRatio

func WithAuxiliaryRatio(r float64) Option

WithAuxiliaryRatio sets the height of auxiliary rows (separator beams) relative to regular rows. Defaults to 0.2.

func WithMarginRatio

func WithMarginRatio(r float64) Option

WithMarginRatio sets the outer margin of the tower relative to the total frame size. Defaults to 0.05.

func WithOrderer

func WithOrderer(o ordering.Orderer) Option

WithOrderer sets the algorithm used to determine the horizontal ordering of blocks in each row. Defaults to ordering.OptimalSearch with a 60-second timeout.

func WithTopDownWidths

func WithTopDownWidths() Option

WithTopDownWidths configures width computation to flow from parents to children (top-down). The default is bottom-up, where blocks are sized to support what is above them.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL