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 ¶
- WithOrderer: Algorithm for determining row orderings (default: ordering.OptimalSearch)
- WithAuxiliaryRatio: Height ratio for auxiliary-only rows (default 0.2)
- WithMarginRatio: Frame margin as fraction of dimensions (default 0.05)
- WithTopDownWidths: Use top-down instead of bottom-up width flow
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 ¶
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 ¶
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 ¶
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
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 ¶
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 ¶
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 ¶
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 ¶
WithAuxiliaryRatio sets the height of auxiliary rows (separator beams) relative to regular rows. Defaults to 0.2.
func WithMarginRatio ¶
WithMarginRatio sets the outer margin of the tower relative to the total frame size. Defaults to 0.05.
func WithOrderer ¶
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.