Yog.Layout (YogEx v0.99.1)

Copy Markdown View Source

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

AlgorithmFunctionMathematical ModelBest ForTime Complexity
Circularcircular/2Uniform spacing on unit circleSymmetric/small graphs, cycles$O(V)$
Randomrandom/2Uniform distribution in bounding boxInitial states, baseline checks$O(V)$
Springspring/2Fruchterman-Reingold force modelSocial networks, general graphs$O(I \cdot (V^2 + E))$
Tuttetutte/3Gauss-Seidel barycentric relaxationPlanar graphs, routing visual flow$O(I \cdot (V + E))$
Shellshell/3Concentric circles placementHierarchies, core-periphery structures$O(V)$
Multipartitemultipartite/3Parallel rows/columns alignmentBipartite 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.

graph LayoutComparison { bgcolor="transparent"; node [shape=circle, fontname="inherit"]; subgraph cluster_spring { label="Spring (Force-Directed)"; color="#10b981"; s1 -- s2; s2 -- s3; s3 -- s1; s1 -- s4; s4 -- s5; s5 -- s1; } subgraph cluster_circular { label="Circular"; color="#3b82f6"; c1 -- c2 -- c3 -- c4 -- c5 -- c1; } }

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

anchor(rect, direction)

@spec anchor({float(), float(), float(), float()}, atom()) :: {float(), float()}

Returns an anchor point on a bounding rectangle edge.

Delegates to Yog.Layout.Geometry.anchor/2.

bounds(positions)

@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

center(positions, opts \\ [])

@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}}

circular(graph, opts \\ [])

@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.

edge_endpoints(positions, edges, opts \\ [])

@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.

fit(positions, opts \\ [])

@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}

graphviz(graph, opts \\ [])

@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.

grid(graph, opts)

@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.

manual(graph, positions, opts \\ [])

@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 - If true, raises ArgumentError if there are extra coordinates in positions for nodes not in the graph (default: false).
  • :missing - Strategy for placing nodes that are present in the graph but missing in the positions map:
    • :error - Raises ArgumentError if 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 :center and :random missing 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}}

merge_position_maps(position_maps)

@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}}

multipartite(graph, layers, opts \\ [])

@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.

pack(position_maps, opts \\ [])

@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 :horizontal or :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}
}

random(graph, opts \\ [])

@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.

rects(positions, opts \\ [])

@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.

scale(positions, factor)

@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}}

scale(positions, sx, sy)

@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}}

shell(graph, shells, opts \\ [])

@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.

spring(graph, opts \\ [])

@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.

translate(positions, dx, dy)

@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}}

tutte(graph, boundary_nodes, opts \\ [])

@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.