Documentation
¶
Overview ¶
Package transform provides post-layout transformations for tower rendering.
Overview ¶
After computing block positions with layout.Build, this package provides transformations that modify the layout for improved visual output:
- MergeSubdividers: Combines subdivider blocks into continuous vertical columns
- Randomize: Applies random width variation for a natural, hand-drawn look
These transformations are applied after layout computation but before rendering to SVG/JSON output.
Merging Subdividers ¶
When long edges are subdivided by dag/transform.Subdivide, each segment becomes a separate block. MergeSubdividers combines these into single continuous vertical blocks for cleaner visualization:
layout := layout.Build(g, width, height, opts...) layout = transform.MergeSubdividers(layout, g)
Blocks are grouped by their MasterID (the original node they were split from) and their horizontal position. Multiple groups can exist for the same master if the subdivider chain splits across different horizontal positions.
Randomization ¶
Randomize applies controlled random variation to block widths, creating a checkerboard pattern that mimics hand-drawn diagrams:
layout = transform.Randomize(layout, g, seed, nil)
The randomization:
- Shrinks alternating rows to create visual rhythm
- Uses a seed for reproducible "randomness"
- Ensures minimum overlap between connected blocks
- Respects minimum block width and gap constraints
Options ¶
Options configures randomization behavior:
- WidthShrink: Maximum shrink factor (0-1, default 0.85)
- MinBlockWidth: Minimum allowed width (default 30px)
- MinGap: Minimum gap between blocks (default 5px)
- MinOverlap: Minimum overlap for connected blocks (default 10px)
Pipeline Position ¶
These transformations fit in the rendering pipeline:
DAG → dag/transform.Normalize → ordering → layout.Build → [this package] → sink.RenderSVG
layout.Build: github.com/matzehuels/stacktower/pkg/render/tower/layout.Build dag/transform.Subdivide: github.com/matzehuels/stacktower/pkg/dag/transform.Subdivide
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func MergeSubdividers ¶
MergeSubdividers combines subdivider blocks into continuous vertical columns. Subdivider nodes (created by dag/transform.Subdivide to break long edges) are grouped by their MasterID and horizontal position, then merged into single blocks spanning from top to bottom.
This creates cleaner visuals where a package's vertical "column" is rendered as one continuous block rather than separate segments per row.
The returned layout has subdivider nodes removed from RowOrders and replaced with merged blocks keyed by their master ID.
Example ¶
package main
import (
"fmt"
"github.com/matzehuels/stacktower/pkg/dag"
"github.com/matzehuels/stacktower/pkg/render/tower/layout"
"github.com/matzehuels/stacktower/pkg/render/tower/transform"
)
func main() {
// Create a graph with subdivider nodes (from edge subdivision)
g := dag.New(nil)
_ = g.AddNode(dag.Node{ID: "app", Row: 0, Kind: dag.NodeKindRegular})
_ = g.AddNode(dag.Node{ID: "lib", Row: 3, Kind: dag.NodeKindRegular})
// Subdivider nodes break the long edge into segments
_ = g.AddNode(dag.Node{
ID: "lib#1",
Row: 1,
Kind: dag.NodeKindSubdivider,
MasterID: "lib",
})
_ = g.AddNode(dag.Node{
ID: "lib#2",
Row: 2,
Kind: dag.NodeKindSubdivider,
MasterID: "lib",
})
_ = g.AddEdge(dag.Edge{From: "app", To: "lib#1"})
_ = g.AddEdge(dag.Edge{From: "lib#1", To: "lib#2"})
_ = g.AddEdge(dag.Edge{From: "lib#2", To: "lib"})
// Create a layout with separate blocks for each subdivider
l := layout.Layout{
FrameWidth: 800,
FrameHeight: 600,
Blocks: map[string]layout.Block{
"app": {NodeID: "app", Left: 100, Right: 200, Top: 50, Bottom: 0},
"lib#1": {NodeID: "lib#1", Left: 100, Right: 200, Top: 150, Bottom: 100},
"lib#2": {NodeID: "lib#2", Left: 100, Right: 200, Top: 250, Bottom: 200},
"lib": {NodeID: "lib", Left: 100, Right: 200, Top: 350, Bottom: 300},
},
RowOrders: map[int][]string{
0: {"app"},
1: {"lib#1"},
2: {"lib#2"},
3: {"lib"},
},
}
// Merge subdividers into a single continuous block
merged := transform.MergeSubdividers(l, g)
// The subdivider segments are now merged
fmt.Printf("Original blocks: %d\n", len(l.Blocks))
fmt.Printf("Merged blocks: %d\n", len(merged.Blocks))
fmt.Printf("Subdividers removed from row orders: %v\n", len(merged.RowOrders[1]) == 0)
}
Output: Original blocks: 4 Merged blocks: 2 Subdividers removed from row orders: true
func Randomize ¶
Randomize applies controlled random variation to block widths. It creates a checkerboard pattern by shrinking alternating rows, which mimics hand-drawn diagrams and adds visual interest.
The seed ensures reproducible randomness—the same seed produces identical layouts. Pass nil for opts to use defaults.
After shrinking, the function ensures connected blocks maintain minimum overlap so dependency edges remain visually clear.
Example ¶
package main
import (
"fmt"
"github.com/matzehuels/stacktower/pkg/dag"
"github.com/matzehuels/stacktower/pkg/render/tower/layout"
"github.com/matzehuels/stacktower/pkg/render/tower/transform"
)
func main() {
// Create a simple layout
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"})
l := layout.Layout{
FrameWidth: 800,
FrameHeight: 400,
Blocks: map[string]layout.Block{
"app": {NodeID: "app", Left: 100, Right: 300, Top: 100, Bottom: 0},
"lib": {NodeID: "lib", Left: 100, Right: 300, Top: 300, Bottom: 200},
},
RowOrders: map[int][]string{
0: {"app"},
1: {"lib"},
},
}
// Apply randomization with a fixed seed for reproducibility
randomized := transform.Randomize(l, g, 42, nil)
// Block widths are now varied (but deterministic with same seed)
// Note: Row 0 is not randomized (the root), only subsequent rows are
appWidth := randomized.Blocks["app"].Right - randomized.Blocks["app"].Left
libWidth := randomized.Blocks["lib"].Right - randomized.Blocks["lib"].Left
fmt.Printf("Randomization applied\n")
fmt.Printf("App width changed: %v\n", appWidth != 200)
fmt.Printf("Lib width changed: %v\n", libWidth != 200)
}
Output: Randomization applied App width changed: false Lib width changed: true
Example (CustomOptions) ¶
package main
import (
"fmt"
"github.com/matzehuels/stacktower/pkg/dag"
"github.com/matzehuels/stacktower/pkg/render/tower/layout"
"github.com/matzehuels/stacktower/pkg/render/tower/transform"
)
func main() {
// Create a layout
g := dag.New(nil)
_ = g.AddNode(dag.Node{ID: "service", Row: 0})
_ = g.AddNode(dag.Node{ID: "database", Row: 1})
_ = g.AddEdge(dag.Edge{From: "service", To: "database"})
l := layout.Layout{
FrameWidth: 800,
FrameHeight: 400,
Blocks: map[string]layout.Block{
"service": {NodeID: "service", Left: 100, Right: 400, Top: 100, Bottom: 0},
"database": {NodeID: "database", Left: 100, Right: 400, Top: 300, Bottom: 200},
},
RowOrders: map[int][]string{
0: {"service"},
1: {"database"},
},
}
// Use custom options for more aggressive randomization
opts := &transform.Options{
WidthShrink: 0.7, // More shrinking (default 0.85)
MinBlockWidth: 50.0, // Larger minimum (default 30)
MinGap: 10.0, // Larger gaps (default 5)
MinOverlap: 20.0, // More overlap required (default 10)
}
randomized := transform.Randomize(l, g, 123, opts)
fmt.Printf("Custom randomization applied\n")
fmt.Printf("Blocks modified: %d\n", len(randomized.Blocks))
}
Output: Custom randomization applied Blocks modified: 2
Example (DeterministicWithSeed) ¶
package main
import (
"fmt"
"github.com/matzehuels/stacktower/pkg/dag"
"github.com/matzehuels/stacktower/pkg/render/tower/layout"
"github.com/matzehuels/stacktower/pkg/render/tower/transform"
)
func main() {
// Demonstrate that the same seed produces identical results
g := dag.New(nil)
_ = g.AddNode(dag.Node{ID: "a", Row: 0})
_ = g.AddNode(dag.Node{ID: "b", Row: 1})
_ = g.AddEdge(dag.Edge{From: "a", To: "b"})
l := layout.Layout{
FrameWidth: 800,
FrameHeight: 400,
Blocks: map[string]layout.Block{
"a": {NodeID: "a", Left: 100, Right: 300, Top: 100, Bottom: 0},
"b": {NodeID: "b", Left: 100, Right: 300, Top: 300, Bottom: 200},
},
RowOrders: map[int][]string{
0: {"a"},
1: {"b"},
},
}
// Apply randomization twice with the same seed
result1 := transform.Randomize(l, g, 999, nil)
result2 := transform.Randomize(l, g, 999, nil)
// Results are identical
width1 := result1.Blocks["a"].Right - result1.Blocks["a"].Left
width2 := result2.Blocks["a"].Right - result2.Blocks["a"].Left
fmt.Printf("Same seed produces identical results: %v\n", width1 == width2)
}
Output: Same seed produces identical results: true
Types ¶
type Options ¶
type Options struct {
// WidthShrink is the maximum shrink factor applied to blocks (0-1).
// Higher values create more width variation. Default: 0.85.
WidthShrink float64
// MinBlockWidth is the minimum allowed block width in pixels.
// Blocks will not shrink below this size. Default: 30.
MinBlockWidth float64
// MinGap is the minimum gap between adjacent blocks in pixels. Default: 5.
MinGap float64
// MinOverlap is the minimum horizontal overlap required between connected
// blocks (parent-child pairs). Blocks are expanded if needed. Default: 10.
MinOverlap float64
}
Options configures the randomization behavior for Randomize.