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_spanlong, 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 Dates 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)
Summary
Functions
Lays items out into %{id => %{start: Date.t(), end: Date.t()}}.
Types
@type id() :: term()
@type item() :: term()
@type temporal() :: Date.t() | NaiveDateTime.t() | DateTime.t()
Functions
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. ADatefor day layouts; aNaiveDateTime/DateTimefor hour/minute precision (the outputstart/endpreserve the type your:advancereturns).:id—(item -> id). Default& &1.id.:parent_id—(item -> id | nil). Defaultfn _ -> nil end. An item whose parent id isnil(or not present amongitems) 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 aNaiveDateTime/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 formin_span: {:day, n}. Default1.