Algorithms for calculating 2D coordinates for graph nodes.
Calculates coordinates mapping node IDs to {x, y} float coordinate tuples.
These coordinates can be used for rendering graphs visually via custom SVG elements,
exporting data for web dashboards (using Cytoscape.js or D3.js), or generating layouts.
Overview
| Algorithm | Function | Mathematical Model | Best For | Time Complexity |
|---|---|---|---|---|
| Circular | circular/2 | Uniform spacing on unit circle | Symmetric/small graphs, cycles | $O(V)$ |
| Random | random/2 | Uniform distribution in bounding box | Initial states, baseline checks | $O(V)$ |
| Spring | spring/2 | Fruchterman-Reingold force model | Social networks, general graphs | $O(I \cdot (V^2 + E))$ |
| Tutte | tutte/3 | Gauss-Seidel barycentric relaxation | Planar graphs, routing visual flow | $O(I \cdot (V + E))$ |
| Shell | shell/3 | Concentric circles placement | Hierarchies, core-periphery structures | $O(V)$ |
| Multipartite | multipartite/3 | Parallel rows/columns alignment | Bipartite graphs, neural nets, flow nets | $O(V)$ |
Graph Layout Visualization (Spring vs. Circular)
The layout determines the structural aesthetic. Spring layout cluster connected nodes together, whereas Circular layout focuses purely on ordering.
Usage Example
Below is an example showing how layout coordinates can be mapped directly to generate a visual representation:
iex> graph = Yog.from_unweighted_edges(:undirected, [{1, 2}, {2, 3}])
iex> pos = Yog.Layout.circular(graph, radius: 10.0)
iex> Map.keys(pos) |> Enum.sort()
[1, 2, 3]
Summary
Functions
Returns an anchor point on a bounding rectangle edge.
Calculates the bounding box {min_x, max_x, min_y, max_y} of the positions map.
Centers the layout at the specified coordinates.
Positions nodes uniformly spaced on a circle.
Computes connector endpoints between nodes for a list of edges.
Fits the layout coordinates inside a target bounding box, preserving aspect ratio by default.
Positions nodes using an external GraphViz layout engine.
Positions nodes deterministically on a 2D grid based on user-supplied rows or columns.
Positions nodes manually based on a supplied map of coordinates, with options to validate and fill in missing nodes.
Merges a list of position maps into a single position map.
Positions nodes in parallel layers (columns or rows).
Packs multiple position maps sequentially either horizontally or vertically with a gap.
Positions nodes randomly within a specified bounding box.
Converts a center-based position map to bounding rectangle maps.
Scales all coordinates by a single factor.
Scales all coordinates by separate sx and sy factors.
Positions nodes in concentric circles (shells).
Positions nodes using a spring/force-directed model (Fruchterman-Reingold).
Translates all coordinates by dx and dy.
Positions nodes using Tutte's barycentric embedding.
Functions
Returns an anchor point on a bounding rectangle edge.
Delegates to Yog.Layout.Geometry.anchor/2.
@spec bounds(%{required(Yog.Graph.node_id()) => {float(), float()}}) :: {float(), float(), float(), float()} | nil
Calculates the bounding box {min_x, max_x, min_y, max_y} of the positions map.
Returns nil if the positions map is empty.
Examples
iex> Yog.Layout.bounds(%{1 => {1.0, 2.0}, 2 => {5.0, -3.0}})
{1.0, 5.0, -3.0, 2.0}
iex> Yog.Layout.bounds(%{})
nil
@spec center( %{required(Yog.Graph.node_id()) => {float(), float()}}, keyword() ) :: %{required(Yog.Graph.node_id()) => {float(), float()}}
Centers the layout at the specified coordinates.
Options
:at- Bounding box center coordinates{cx, cy}(default:{0.0, 0.0}).
Examples
iex> Yog.Layout.center(%{1 => {0.0, 0.0}, 2 => {4.0, 2.0}}, at: {1.0, 1.0})
%{1 => {-1.0, 0.0}, 2 => {3.0, 2.0}}
@spec circular( Yog.Graph.t(), keyword() ) :: %{required(Yog.Graph.node_id()) => {float(), float()}}
Positions nodes uniformly spaced on a circle.
Delegates to Yog.Layout.Circular.layout/2.
@spec edge_endpoints( %{required(any()) => {float(), float()}}, [{any(), any()}], keyword() ) :: [ {{float(), float()}, {float(), float()}} ]
Computes connector endpoints between nodes for a list of edges.
Delegates to Yog.Layout.Geometry.edge_endpoints/3.
@spec fit( %{required(Yog.Graph.node_id()) => {float(), float()}}, keyword() ) :: %{required(Yog.Graph.node_id()) => {float(), float()}}
Fits the layout coordinates inside a target bounding box, preserving aspect ratio by default.
Options
:width- Target bounding box width (default:1.0).:height- Target bounding box height (default:1.0).:padding- Bounding box padding (default:0.0).:preserve_aspect- Preserve aspect ratio of coordinate box (default:true).:center- Center of layout space (default:{width / 2.0, height / 2.0}).
Examples
iex> pos = %{1 => {0.0, 0.0}, 2 => {10.0, 5.0}}
iex> fitted = Yog.Layout.fit(pos, width: 100.0, height: 100.0, padding: 10.0, preserve_aspect: false)
iex> Yog.Layout.bounds(fitted)
{10.0, 90.0, 10.0, 90.0}
@spec graphviz( Yog.Graph.t() | Yog.Multi.Graph.t(), keyword() ) :: %{required(any()) => {float(), float()}}
Positions nodes using an external GraphViz layout engine.
Delegates to Yog.Layout.GraphViz.layout/2.
@spec grid( Yog.Graph.t(), keyword() ) :: %{required(Yog.Graph.node_id()) => {float(), float()}}
Positions nodes deterministically on a 2D grid based on user-supplied rows or columns.
Delegates to Yog.Layout.Grid.layout/2.
@spec manual( Yog.Graph.t(), %{required(Yog.Graph.node_id()) => {float(), float()}}, keyword() ) :: %{required(Yog.Graph.node_id()) => {float(), float()}}
Positions nodes manually based on a supplied map of coordinates, with options to validate and fill in missing nodes.
Options
:strict- Iftrue, raisesArgumentErrorif there are extra coordinates inpositionsfor nodes not in the graph (default:false).:missing- Strategy for placing nodes that are present in the graph but missing in thepositionsmap::error- RaisesArgumentErrorif any nodes are missing coordinates.:center- Places missing nodes at:center.:ignore- Omit missing nodes from the returned coordinate map.:random- Places missing nodes randomly.{:random, random_opts}- Places missing nodes randomly with custom bounds (e.g.[width: 10.0, height: 10.0]).function- A 1-arity function(node_id -> {x, y})that generates coordinates for each missing node.- Default:
{:random, []}.
:center- The{x, y}coordinates of the center, used as default center for:centerand:randommissing placement (default:{0.0, 0.0}).:seed- Seed for random positioning generator.
Examples
iex> graph = Yog.undirected() |> Yog.add_nodes_from([1, 2, 3])
iex> pos = %{1 => {1.0, 2.0}, 2 => {3.0, 4.0}}
iex> manual_pos = Yog.Layout.manual(graph, pos, missing: :center, center: {0.0, 0.0})
iex> manual_pos
%{1 => {1.0, 2.0}, 2 => {3.0, 4.0}, 3 => {0.0, 0.0}}
@spec merge_position_maps([%{required(Yog.Graph.node_id()) => {float(), float()}}]) :: %{ required(Yog.Graph.node_id()) => {float(), float()} }
Merges a list of position maps into a single position map.
Raises ArgumentError if there are duplicate node IDs across the maps.
Examples
iex> map_a = %{a: {1.0, 2.0}}
iex> map_b = %{b: {3.0, 4.0}}
iex> Yog.Layout.merge_position_maps([map_a, map_b])
%{a: {1.0, 2.0}, b: {3.0, 4.0}}
@spec multipartite(Yog.Graph.t(), [[Yog.Graph.node_id()]], keyword()) :: %{ required(Yog.Graph.node_id()) => {float(), float()} }
Positions nodes in parallel layers (columns or rows).
Delegates to Yog.Layout.Multipartite.layout/3.
@spec pack( [%{required(Yog.Graph.node_id()) => {float(), float()}}], keyword() ) :: %{required(Yog.Graph.node_id()) => {float(), float()}}
Packs multiple position maps sequentially either horizontally or vertically with a gap.
Useful for placing independent subgraphs side-by-side or stacked without overlapping.
Options
:direction- Packing direction, either:horizontalor:vertical(default::horizontal).:gap- Gap size between consecutive bounding boxes (default:0.0).
Examples
iex> map_a = %{a: {0.0, 0.0}, b: {10.0, 10.0}}
iex> map_b = %{c: {0.0, 0.0}, d: {5.0, 5.0}}
iex> Yog.Layout.pack([map_a, map_b], direction: :horizontal, gap: 5.0)
%{
a: {0.0, 0.0},
b: {10.0, 10.0},
c: {15.0, 0.0},
d: {20.0, 5.0}
}
@spec random( Yog.Graph.t(), keyword() ) :: %{required(Yog.Graph.node_id()) => {float(), float()}}
Positions nodes randomly within a specified bounding box.
Delegates to Yog.Layout.Random.layout/2.
@spec rects( %{required(any()) => {float(), float()}}, keyword() ) :: %{required(any()) => {float(), float(), float(), float()}}
Converts a center-based position map to bounding rectangle maps.
Delegates to Yog.Layout.Geometry.rects/2.
@spec scale(%{required(Yog.Graph.node_id()) => {float(), float()}}, float()) :: %{ required(Yog.Graph.node_id()) => {float(), float()} }
Scales all coordinates by a single factor.
Examples
iex> Yog.Layout.scale(%{1 => {1.0, 2.0}}, 3.0)
%{1 => {3.0, 6.0}}
@spec scale(%{required(Yog.Graph.node_id()) => {float(), float()}}, float(), float()) :: %{ required(Yog.Graph.node_id()) => {float(), float()} }
Scales all coordinates by separate sx and sy factors.
Examples
iex> Yog.Layout.scale(%{1 => {1.0, 2.0}}, 3.0, -1.0)
%{1 => {3.0, -2.0}}
@spec shell(Yog.Graph.t(), [[Yog.Graph.node_id()]], keyword()) :: %{ required(Yog.Graph.node_id()) => {float(), float()} }
Positions nodes in concentric circles (shells).
Delegates to Yog.Layout.Shell.layout/3.
@spec spring( Yog.Graph.t(), keyword() ) :: %{required(Yog.Graph.node_id()) => {float(), float()}}
Positions nodes using a spring/force-directed model (Fruchterman-Reingold).
Delegates to Yog.Layout.Spring.layout/2.
@spec translate( %{required(Yog.Graph.node_id()) => {float(), float()}}, float(), float() ) :: %{ required(Yog.Graph.node_id()) => {float(), float()} }
Translates all coordinates by dx and dy.
Examples
iex> Yog.Layout.translate(%{1 => {1.0, 2.0}}, 2.0, -1.0)
%{1 => {3.0, 1.0}}
@spec tutte(Yog.Graph.t(), [Yog.Graph.node_id()], keyword()) :: %{ required(Yog.Graph.node_id()) => {float(), float()} }
Positions nodes using Tutte's barycentric embedding.
Delegates to Yog.Layout.Tutte.layout/3.