PhoenixLiveGantt.Layout (PhoenixLiveGantt v0.1.0)

Copy Markdown View Source

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

id()

@type id() :: term()

item()

@type item() :: term()

span()

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

temporal()

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

Functions

sequential(items, opts)

@spec sequential(
  [item()],
  keyword()
) :: %{required(id()) => 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.