# `PhoenixLiveGantt.Layout`
[🔗](https://github.com/mdon/phoenix_live_gantt/blob/v0.4.0/lib/phoenix_live_gantt/layout.ex#L1)

Optional layout helper for the common "I have durations, not dates" case.

`PhoenixLiveGantt.gantt/1` is render-only — it draws bars from `start`/`end` **dates**.
Many domains (project tasks, production steps, itineraries) instead have a
*duration* + an *order* + maybe *nested sub-projects*, and no per-item dates.
`sequential/2` does that translation once, correctly, so every consumer
doesn't re-implement (and re-bug) it:

  * each item starts where the previous sibling ended (a waterfall),
  * a sub-project spans its laid-out children (its bar/frame always contains
    them — no spill),
  * every bar is at least `:min_span` long, so short items stay visible and
    siblings never overlap.

The calendar math — how a duration advances a date/time across weekends,
working hours, or holidays — is **yours**, supplied via the `:advance`
callback. That keeps PhoenixLiveGantt domain-agnostic: it never assumes what a
"duration" means.

Works at whatever resolution your `:start`/`:advance` use: pass `Date`s for a
day-level waterfall, or `NaiveDateTime`/`DateTime` (with `min_span: {:hour, 1}`)
for hour-precise layout that pairs with `:hour` zoom.

## Deliberately out of scope

This is layout, not a scheduler. It does NOT do dependency-driven scheduling,
critical path, resource leveling, "start no earlier than" / "must finish by"
constraints, lag/lead, or working-hour models. If you need those, compute your
own dates and pass them to `PhoenixLiveGantt.gantt/1` directly.

## Example

    layout =
      PhoenixLiveGantt.Layout.sequential(assignments,
        start: ~D[2026-06-01],
        id: & &1.uuid,
        parent_id: & &1.child_parent_uuid,
        duration: & &1.estimated_hours,
        order: & &1.position,
        advance: fn start_date, hours, assignment ->
          # your weekend/working-calendar math
          MyCalendar.add_working(start_date, hours, assignment.counts_weekends)
        end
      )

    events =
      Enum.map(assignments, fn a ->
        %{start: s, end: e} = layout[a.uuid]
        %PhoenixLiveGantt.Task{id: a.uuid, title: a.title, start: s, end: e, extra: %{parent_id: a.child_parent_uuid}}
      end)

# `id`

```elixir
@type id() :: term()
```

# `item`

```elixir
@type item() :: term()
```

# `span`

```elixir
@type span() :: %{start: temporal(), end: temporal()}
```

# `temporal`

```elixir
@type temporal() :: Date.t() | NaiveDateTime.t() | DateTime.t()
```

# `sequential`

```elixir
@spec sequential(
  [item()],
  keyword()
) :: %{required(id()) =&gt; span()}
```

Lays `items` out into `%{id => %{start: Date.t(), end: Date.t()}}`.

`end` is exclusive, matching `PhoenixLiveGantt.gantt/1` (a one-day bar is
`start..start+1`). Every id in `items` appears in the result, including
sub-project parents (sized to span their children). Any item the tree walk
can't reach from a root — one whose `parent_id` forms a cycle, points at
itself, or nests past the internal depth cap, **plus every descendant of such
an item** (their chain to a root runs through the unreachable one) — is laid
out flat after the main chain rather than dropped, so `result[id]` is always
safe.

## Options

  * `:start` (required) — the first top-level item's start. A `Date` for day
    layouts; a `NaiveDateTime`/`DateTime` for hour/minute precision (the
    output `start`/`end` preserve the type your `:advance` returns).
  * `:id` — `(item -> id)`. Default `& &1.id`.
  * `:parent_id` — `(item -> id | nil)`. Default `fn _ -> nil end`. An item
    whose parent id is `nil` (or not present among `items`) is a top-level
    root; an item that others point at via this is a sub-project.
  * `:duration` — `(item -> term)`. Default `& &1.duration`. The value is
    opaque to PhoenixLiveGantt and handed straight to `:advance`.
  * `:order` — `(item -> Enum.sort_by key)`. Default keeps input order
    (siblings are otherwise laid out in the order given).
  * `:advance` — `(start, duration, item -> end)` (a 2-arity
    `(start, duration)` function is also accepted). Default treats the
    duration as a whole number of calendar days (`Date.add/2`). For hour
    precision, return a `NaiveDateTime`/`DateTime`. Plug your
    weekend/working-calendar here.
  * `:min_span` — minimum bar length as `{:day | :hour | :minute | :second, n}`
    (or a bare integer = days). Default `{:day, 1}`. Use `{:hour, 1}` for
    hour layouts so a zero-duration item still gets a one-hour bar.
  * `:min_span_days` — shorthand for `min_span: {:day, n}`. Default `1`.

---

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