# `Yog.Layout`
[🔗](https://github.com/code-shoily/yog_ex/blob/v1.0.0/lib/yog/layout.ex#L1)

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.

<div class="graphviz">
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;
  }
}
</div>

## 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]

# `anchor`

```elixir
@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`

```elixir
@spec bounds(%{required(Yog.Graph.node_id()) =&gt; {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`

```elixir
@spec center(
  %{required(Yog.Graph.node_id()) =&gt; {float(), float()}},
  keyword()
) :: %{required(Yog.Graph.node_id()) =&gt; {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`

```elixir
@spec circular(
  Yog.Graph.t() | Yog.DAG.t(),
  keyword()
) :: %{required(Yog.Graph.node_id()) =&gt; {float(), float()}}
```

Positions nodes uniformly spaced on a circle.

Delegates to `Yog.Layout.Circular.layout/2`.

# `edge_endpoints`

```elixir
@spec edge_endpoints(
  %{required(any()) =&gt; {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`

```elixir
@spec fit(
  %{required(Yog.Graph.node_id()) =&gt; {float(), float()}},
  keyword()
) :: %{required(Yog.Graph.node_id()) =&gt; {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`

```elixir
@spec graphviz(
  Yog.Graph.t() | Yog.DAG.t() | Yog.Multi.Graph.t(),
  keyword()
) :: %{required(any()) =&gt; {float(), float()}}
```

Positions nodes using an external GraphViz layout engine.

Delegates to `Yog.Layout.GraphViz.layout/2`.

# `grid`

```elixir
@spec grid(
  Yog.Graph.t() | Yog.DAG.t(),
  keyword()
) :: %{required(Yog.Graph.node_id()) =&gt; {float(), float()}}
```

Positions nodes deterministically on a 2D grid based on user-supplied rows or columns.

Delegates to `Yog.Layout.Grid.layout/2`.

# `manual`

```elixir
@spec manual(
  Yog.Graph.t() | Yog.DAG.t(),
  %{required(Yog.Graph.node_id()) =&gt; {float(), float()}},
  keyword()
) :: %{required(Yog.Graph.node_id()) =&gt; {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`

```elixir
@spec merge_position_maps([%{required(Yog.Graph.node_id()) =&gt; {float(), float()}}]) ::
  %{
    required(Yog.Graph.node_id()) =&gt; {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`

```elixir
@spec multipartite(Yog.Graph.t() | Yog.DAG.t(), [[Yog.Graph.node_id()]], keyword()) ::
  %{
    required(Yog.Graph.node_id()) =&gt; {float(), float()}
  }
```

Positions nodes in parallel layers (columns or rows).

Delegates to `Yog.Layout.Multipartite.layout/3`.

# `pack`

```elixir
@spec pack(
  [%{required(Yog.Graph.node_id()) =&gt; {float(), float()}}],
  keyword()
) :: %{required(Yog.Graph.node_id()) =&gt; {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`

```elixir
@spec random(
  Yog.Graph.t() | Yog.DAG.t(),
  keyword()
) :: %{required(Yog.Graph.node_id()) =&gt; {float(), float()}}
```

Positions nodes randomly within a specified bounding box.

Delegates to `Yog.Layout.Random.layout/2`.

# `rects`

```elixir
@spec rects(
  %{required(any()) =&gt; {float(), float()}},
  keyword()
) :: %{required(any()) =&gt; {float(), float(), float(), float()}}
```

Converts a center-based position map to bounding rectangle maps.

Delegates to `Yog.Layout.Geometry.rects/2`.

# `scale`

```elixir
@spec scale(%{required(Yog.Graph.node_id()) =&gt; {float(), float()}}, float()) :: %{
  required(Yog.Graph.node_id()) =&gt; {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`

```elixir
@spec scale(%{required(Yog.Graph.node_id()) =&gt; {float(), float()}}, float(), float()) ::
  %{
    required(Yog.Graph.node_id()) =&gt; {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`

```elixir
@spec shell(Yog.Graph.t() | Yog.DAG.t(), [[Yog.Graph.node_id()]], keyword()) :: %{
  required(Yog.Graph.node_id()) =&gt; {float(), float()}
}
```

Positions nodes in concentric circles (shells).

Delegates to `Yog.Layout.Shell.layout/3`.

# `spring`

```elixir
@spec spring(
  Yog.Graph.t() | Yog.DAG.t(),
  keyword()
) :: %{required(Yog.Graph.node_id()) =&gt; {float(), float()}}
```

Positions nodes using a spring/force-directed model (Fruchterman-Reingold).

Delegates to `Yog.Layout.Spring.layout/2`.

# `translate`

```elixir
@spec translate(
  %{required(Yog.Graph.node_id()) =&gt; {float(), float()}},
  float(),
  float()
) :: %{
  required(Yog.Graph.node_id()) =&gt; {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`

```elixir
@spec tutte(Yog.Graph.t() | Yog.DAG.t(), [Yog.Graph.node_id()], keyword()) :: %{
  required(Yog.Graph.node_id()) =&gt; {float(), float()}
}
```

Positions nodes using Tutte's barycentric embedding.

Delegates to `Yog.Layout.Tutte.layout/3`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
