PhoenixLiveGantt (PhoenixLiveGantt v0.1.0)

Copy Markdown View Source

Waterfall (Gantt) view — horizontal bars on a date-range axis.

Each item is rendered as a row with a horizontal bar whose left edge corresponds to its start date and width corresponds to its duration. Optionally draws SVG connector lines between dependent items using orthogonal (right-angle) routing — the industry-standard approach for Gantt dependency arrows.

Features

  • Multi-zoom: :day, :week, :month granularity
  • Today marker (vertical line)
  • Non-working day shading via day_markers
  • Progress indicator (fill percentage via extra.progress_pct)
  • Milestones (zero-duration items rendered as diamonds)
  • Orthogonal dependency connectors with arrow heads
  • Grouping via extra.group field
  • Custom item rendering via :item slot

Data mapping

Waterfall uses the standard PhoenixLiveGantt.Task struct:

Waterfall conceptEvent field
Task nametitle
Start datestart (Date)
End dateend (Date, exclusive)
DurationComputed from start/end
Status/colorcolor, status
Progressextra.progress_pct (0–100)
Group/phaseextra.group or category
Assigneeextra.assignee
MilestoneWhen start == end (zero duration)

Connectors are passed separately as a list of %{from: event_id, to: event_id} maps.

Coordinate system

Everything is pixel-based against a fixed content width (total_days × day_width_px + 2 × axis_pad, where axis_pad is a fixed 16px margin on each side that gives edge connectors room — see axis_pad_px/0). This keeps bar positions, grid columns, today marker, and connector arrows aligned no matter the flex context.

Summary

Functions

The fixed horizontal margin (px) reserved on EACH side of the time axis so a connector exiting/entering a task at the very edge of the window has room to draw instead of clipping. The natural content width is total_days × day_width_px + 2 × axis_pad_px(). A consumer computing a fit-to-width day_width_px from a measured viewport should subtract 2 × axis_pad_px() first.

The default pixels-per-day for a zoom level — :hour 720, :day 40, :week 24, :month 8. Use it as the floor when computing a fit-to-width day_width_px override, so fitting only ever widens (and a long chart still scrolls at its natural density).

Renders a Gantt / waterfall chart — horizontal task bars on a time axis with orthogonal dependency connectors, milestones, sub-projects, and a built-in popover.

A Phoenix.LiveView.JS command that scrolls a chart's timeline back to its start (leftmost column). Pair it with a "home"/"fit" button whose server handler refits the window — the server can't move the scroll, and the built-in scroll-to-today only fires when the today marker is in view, so a refit that doesn't include today would otherwise leave the timeline scrolled to a stale spot. Requires enable_hooks + the matching id (the LgAutoScroll hook listens for the dispatched lg:scroll-start).

Toggles an id in an expanded set — convenience for on_toggle_expand handlers so consumers don't re-write the member?/put/delete boilerplate.

Functions

axis_pad_px()

@spec axis_pad_px() :: non_neg_integer()

The fixed horizontal margin (px) reserved on EACH side of the time axis so a connector exiting/entering a task at the very edge of the window has room to draw instead of clipping. The natural content width is total_days × day_width_px + 2 × axis_pad_px(). A consumer computing a fit-to-width day_width_px from a measured viewport should subtract 2 × axis_pad_px() first.

default_day_width_px(zoom)

@spec default_day_width_px(atom()) :: pos_integer()

The default pixels-per-day for a zoom level — :hour 720, :day 40, :week 24, :month 8. Use it as the floor when computing a fit-to-width day_width_px override, so fitting only ever widens (and a long chart still scrolls at its natural density).

gantt(assigns)

Renders a Gantt / waterfall chart — horizontal task bars on a time axis with orthogonal dependency connectors, milestones, sub-projects, and a built-in popover.

Pass a list of PhoenixLiveGantt.Task structs as events and a Date.Range as date_range; everything else is optional. Each attribute below documents its own default and behavior. The smallest useful call:

<PhoenixLiveGantt.gantt events={@tasks} date_range={@range} />

Note: no stylesheet ships — your app's Tailwind must scan this library as a content source (see the README), and the JS hooks (priv/static/assets/ phoenix_live_gantt.js) must be registered for the popover / scroll-to-today.

Attributes

  • events (:list) - Defaults to [].

  • date_range (Date.Range) (required)

  • window_start (NaiveDateTime) - Optional sub-day positioning origin. When window_start/window_end are both NaiveDateTimes, the window becomes the axis: it drives the start/end instants, the columns, event partition (in-window vs the ← N earlier / N later → edge counts), and the today marker — instead of date_range's midnight-to-midnight span. Useful at :hour/:min15/:min5 zoom to begin ~1 column before the first task rather than at midnight (a wall of empty pre-task columns). Snap window_start to a column-slot boundary so labels land on round clock times. date_range remains required as the base axis and is used whenever the window is absent or non-positive, so keep it covering the same span. Defaults to nil.

  • window_end (NaiveDateTime) - Sub-day positioning end instant. See window_start. Defaults to nil.

  • zoom (:atom) - Defaults to :week.

  • connectors (:list) - Defaults to [].

  • day_markers (:list) - Non-working / highlighted day ranges, shaded in the grid. Each is a %{start_date: Date.t(), end_date: Date.t() | nil, available: boolean()} (an end_date of nil means a single day; available: false shades it as non-working). Defaults to [].

  • day_width_px (:integer) - Override the per-zoom pixels-per-day. The natural content width is total_days * day_width_px + 2 * axis_pad_px() (the pad reserves room for edge connectors); that is the scroll min-width. The chart is responsive and fills wider containers on its own (horizontal coords are percentages), so use this only to tune density / the scroll threshold. nil uses the zoom default. Defaults to nil.

  • min_bar_px (:integer) - Minimum rendered width (px) for a non-milestone bar. Default 0 — bars reflect their TRUE duration, so a task too short to show at the current zoom is a hairline (or vanishes) until you zoom in. Set e.g. 4 to floor every bar to a visible/clickable sliver at the cost of overstating very short tasks' spans (connectors still attach to the rendered edge). A zero-DURATION task is always a milestone diamond regardless of this value. Defaults to 0.

  • today (:any) - Today's date (a Date), or a DateTime/NaiveDateTime for a precise 'now' line — recommended at :hour zoom, where the marker lands at the exact time and the current-hour column highlights. Defaults to Date.utc_today(). Defaults to nil.

  • row_height (:string) - Defaults to "2.5rem".

  • label_width (:string) - Defaults to "14rem".

  • on_event_click (:any) - Defaults to nil.

  • expanded (:any) - Which sub-projects are expanded (children visible). A MapSet or list of expanded event ids, :all to expand everything, or nil for all collapsed. Collapsed parents' children are hidden and connectors retarget to the visible ancestor. Defaults to nil.

  • on_toggle_expand (:any) - phx-click event name fired when a sub-project chevron is toggled. The handler receives the event id under the "event-id" param key (hyphen). Update your expanded set in response — see PhoenixLiveGantt.toggle_expanded/2. Defaults to nil.

  • show_progress (:boolean) - Defaults to true.

  • show_today (:boolean) - Defaults to true.

  • show_today_edge (:boolean) - Show the floating directional ← Today / Today → pill when today is off-screen. Independent of show_today (which controls the in-range today line), so you can keep the line but drop the off-screen hint. Defaults to true.

  • show_connectors (:boolean) - Defaults to true.

  • tiny_bar_px (:integer) - When a bar renders narrower than this many SCREEN pixels, a small fixed-size down-triangle marker appears at the task's start to signal a too-small-to-see task. The decision is pure CSS — a container query against the bar's rendered width — so it's server-emitted, correct against the responsive fill + zoom, instant on first paint, and re-resolves on resize with no JavaScript. (Clicking the marker opens the same popover, which does need enable_hooks.) Set 0 to disable. Bars themselves stay at their true width — see min_bar_px. Assumes a uniform value across charts sharing a page. Defaults to 5.

  • avoid_collisions (:boolean) - When true, connector trunks are shifted to avoid crossing unrelated bars. Turn off for very large Gantts or when you prefer strict bus alignment. Defaults to true.

  • label_background (:atom) - How connector labels render. :halo (default) paints each glyph with a base-100 outline so the line shows between letters. :rect draws a solid base-100 rectangle behind the text — stronger contrast over bars but can leave visible gaps in the line around narrow words. Defaults to :halo. Must be one of :halo, or :rect.

  • connector_color_class (:string) - Defaults to "text-base-content/50".

  • connector_stroke_width (:float) - Defaults to 1.5.

  • connector_opacity (:float) - Defaults to 1.0.

  • connector_dasharray (:string) - Defaults to "none".

  • critical_color_class (:string) - Defaults to "text-primary".

  • critical_stroke_width (:float) - Defaults to 2.25.

  • invalid_color_class (:string) - Defaults to "text-error".

  • invalid_stroke_width (:float) - Defaults to 2.0.

  • invalid_dasharray (:string) - Defaults to "4 3".

  • connector_elbow_px (:integer) - Defaults to 10.

  • connector_bar_clearance_px (:integer) - Defaults to 10.

  • bus_split_offset_pct (:integer) - Used by bus_attach_mode={:type_zoned}. When a bar side has both incoming and outgoing arrows, this is the % offset from the bar's top edge for outgoing attachment (incoming mirrors). Default 40 → 40%/60% split. Set to 50 to disable the split. Defaults to 40.

  • bus_attach_mode (:atom) - How arrow endpoints attach to a bar edge when multiple arrows touch it:

    • :smart (default) — each arrow's attach y depends on the OTHER end's row. Outgoing arrows going DOWN attach to the bar bottom; outgoing arrows going UP attach to the bar top. Incoming arrows from ABOVE land on the upper-middle of the bar; from BELOW land on the lower-middle. Up to 4 designated y positions per side. If only one of these positions is in use → collapses to bar center.
    • :type_zoned — outgoing always at top, incoming always at bottom (regardless of direction). Uses bus_split_offset_pct.
    • :center — disable splits entirely; everything attaches at the bar center. Per-task override: set extra.bus_attach_mode on an event to one of these atoms.

    Defaults to :smart. Must be one of :smart, :type_zoned, or :center.

  • bus_attach_inner_pct (:integer) - Smart mode only. % offset from bar edge for both attach positions. Default 40 → split at 40%/60% of bar height. Smart mode picks one for outgoing (by majority outgoing direction) and the opposite for incoming. Defaults to 40.

  • bus_stagger_outgoing_px (:integer) - Stagger trunk x by this many px per lane for arrows in the SAME outgoing bus (multiple outgoing from one source on one side). Default 0 = merged (single trunk). Set to 3-5 to fan out each outgoing arrow into its own visible lane. Per-task override via extra.bus_stagger_outgoing_px. Defaults to 0.

  • bus_stagger_incoming_px (:integer) - Stagger trunk x by this many px per lane for arrows in the SAME incoming bus (multiple incoming to one target on one side). Default 0 = merged. Per-task override via extra.bus_stagger_incoming_px. Defaults to 0.

  • bus_stagger_corner_clearance_px (:integer) - When stagger is active and a bar side has multiple arrows, lanes are distributed evenly across the bar's FLAT region (excluding rounded corners) so no arrow emerges from a corner. This sets the corner radius to avoid; default 4 matches Tailwind's rounded (4px) on the default bar_class. Set to 0 if your bar isn't rounded. Defaults to 4.

  • main_header_class (:string) - Defaults to "flex sticky top-0 z-20".

  • label_header_class (:string) - Defaults to "flex-shrink-0 bg-base-100 border-r border-base-content/10 border-b-2 border-b-base-content/15 px-3 py-2 font-semibold text-sm text-base-content".

  • column_header_class (:string) - Defaults to "text-xs text-center py-2 border-r border-base-content/5 font-medium flex-shrink-0".

  • column_header_today_class (:string) - Defaults to "bg-primary/10 font-bold text-primary".

  • column_divider_class (:string) - Defaults to "border-r border-base-content/5 h-full flex-shrink-0".

  • non_working_class (:string) - Defaults to "bg-base-content/[0.04]".

  • row_class (:string) - Defaults to "relative border-b border-base-content/5 hover:bg-base-content/[0.02]".

  • label_col_class (:string) - Defaults to "relative flex-shrink-0 border-r border-base-content/10".

  • label_row_class (:string) - Defaults to "flex items-center px-3 border-b border-base-content/5 overflow-hidden cursor-pointer hover:bg-base-content/[0.02]".

  • group_header_class (:string) - Defaults to "flex items-center bg-base-200/50 border-b border-base-content/10 px-3".

  • group_header_text_class (:string) - Defaults to "text-xs font-bold uppercase tracking-wider text-base-content/60 truncate".

  • group_spacer_class (:string) - Defaults to "bg-base-200/50 border-b border-base-content/10 relative".

  • bar_class (:string) - Defaults to "absolute top-1 bottom-1 rounded cursor-pointer overflow-hidden flex items-center z-10".

  • bar_subproject_class (:string) - Defaults to "ring-1 ring-base-content/30 ring-offset-0 [background-image:repeating-linear-gradient(135deg,transparent_0_6px,rgba(0,0,0,0.08)_6px_7px)]".

  • subproject_frame_color (:any) - Defaults to ["#FEF3C7", "#DBEAFE", "#E0E7FF", "#FCE7F3"].

  • bar_background_class (:string) - Defaults to "absolute inset-0 rounded".

  • bar_default_color_class (:string) - Defaults to "bg-primary".

  • bar_title_class (:string) - Defaults to "relative z-10 text-xs font-medium truncate px-2".

  • bar_title_cancelled_class (:string) - Defaults to "line-through".

  • progress_class (:string) - Defaults to "absolute inset-y-0 left-0 rounded-l".

  • progress_complete_radius_class (:string) - Defaults to "rounded-r".

  • progress_incomplete_class (:string) - Defaults to "bg-base-content/20".

  • progress_complete_class (:string) - Defaults to "bg-success/40".

  • milestone_class (:string) - Defaults to "absolute top-1/2 z-40 cursor-pointer w-4 h-4 border-2".

  • milestone_default_color_class (:string) - Defaults to "bg-primary".

  • milestone_status_cancelled_class (:string) - Defaults to "opacity-50".

  • status_tentative_class (:string) - Defaults to "opacity-60".

  • status_cancelled_class (:string) - Defaults to "opacity-40".

  • status_pending_approval_class (:string) - Defaults to "animate-pulse".

  • status_no_show_class (:string) - Defaults to nil.

  • status_blocked_class (:string) - Defaults to "opacity-60 grayscale".

  • bar_popover_class (:string) - Defaults to "absolute z-[60] max-w-md rounded-md shadow-lg border-2 border-base-content overflow-hidden hidden".

  • bar_popover_title_class (:string) - Defaults to "flex items-center px-2 text-xs font-medium whitespace-normal break-words leading-tight".

  • bar_popover_subtitle_class (:string) - Defaults to "px-2 pb-1 text-[0.65rem] opacity-80 leading-tight".

  • bar_popover_actions_class (:string) - Defaults to "flex gap-1 px-2 py-2".

  • bar_action_button_class (:string) - Defaults to "relative inline-flex items-center justify-center w-7 h-7 rounded hover:bg-base-content/15 cursor-pointer".

  • bar_action_disabled_class (:string) - Defaults to "opacity-50 cursor-not-allowed pointer-events-none".

  • label_popover_class (:string) - Defaults to "absolute left-2 right-2 z-[60] rounded-md shadow-lg border-2 border-base-content overflow-hidden hidden".

  • badge_class (:string) - Defaults to "absolute z-50 inline-flex items-center justify-center px-1 min-w-[1rem] h-4 text-[0.55rem] font-bold rounded-full ring-1 ring-base-100 leading-none pointer-events-none".

  • badge_default_color_class (:string) - Defaults to "bg-error".

  • today_marker_line_class (:string) - Defaults to "absolute top-0 w-0.5 bg-error z-30 pointer-events-none".

  • today_marker_badge_class (:string) - Defaults to "absolute top-0 -translate-x-1/2 bg-error text-error-content text-[0.55rem] px-1 rounded-b font-bold whitespace-nowrap".

  • translations (:map) - Defaults to %{}.

  • class (:string) - Defaults to "".

  • dir (:atom) - Defaults to :ltr.

  • id (:string) - Stable DOM id. Required when show_header is true OR auto_scroll_today is on, so JS dispatches target the right gantt instance when multiple are on the page. Defaults to nil.

  • show_header (:boolean) - Defaults to false.

  • show_zoom_switcher (:boolean) - Defaults to true.

  • show_today_button (:boolean) - Defaults to true.

  • show_navigation (:boolean) - Defaults to true.

  • zooms (:list) - Defaults to [:day, :week, :month].

  • on_zoom_change (:any) - Defaults to nil.

  • on_navigate (:any) - Defaults to nil.

  • on_scroll_today (:any) - Defaults to nil.

  • toolbar_class (:string) - Defaults to "lg-toolbar flex items-center justify-between gap-3 px-3 py-2 border-b border-base-content/15 bg-base-100".

  • show_edge_indicators (:boolean) - Defaults to true.

  • on_show_earlier (:any) - Defaults to nil.

  • on_show_later (:any) - Defaults to nil.

  • on_show_today (:any) - phx-click event for the off-screen Today hint (shown when today is outside date_range). Wire it to widen the range / jump to today; if nil the hint is informational only. Requires show_today. Defaults to nil.

  • edge_indicator_class (:string) - Defaults to "lg-edge px-2 py-1 rounded-full bg-base-200/95 border border-base-content/10 text-[0.65rem] font-medium text-base-content/70 shadow-sm hover:bg-base-200 transition-colors".

  • enable_hooks (:boolean) - When true, attaches BOTH JS hooks: LgAutoScroll on the container (auto-scroll + today button) and LgBarPopover on every bar/milestone/label (the click popover + dependency-tree highlight). Requires the PhoenixLiveGantt JS bundle (priv/static/assets/phoenix_live_gantt.js, registered as window.PhoenixLiveGanttHooks). Leave false if you don't ship the bundle — otherwise the browser logs an "unknown hook" error per element. Defaults to false.

  • auto_scroll_today (:boolean) - On mount, scroll the timeline so today is horizontally centered (if today is in range and hooks are enabled). Defaults to true.

Slots

  • item
  • label
  • toolbar_start - Extra content rendered at the left of the toolbar, after the today/nav buttons.
  • toolbar_end - Extra content rendered at the right of the toolbar, after the zoom switcher.

scroll_to_start(js \\ %JS{}, id)

@spec scroll_to_start(Phoenix.LiveView.JS.t(), String.t()) :: Phoenix.LiveView.JS.t()

A Phoenix.LiveView.JS command that scrolls a chart's timeline back to its start (leftmost column). Pair it with a "home"/"fit" button whose server handler refits the window — the server can't move the scroll, and the built-in scroll-to-today only fires when the today marker is in view, so a refit that doesn't include today would otherwise leave the timeline scrolled to a stale spot. Requires enable_hooks + the matching id (the LgAutoScroll hook listens for the dispatched lg:scroll-start).

<button phx-click={
  JS.push("fit_project") |> PhoenixLiveGantt.scroll_to_start("project-gantt-#{@id}")
}>Project</button>

Composes with an existing JS command (e.g. a JS.push/2); pass it as the first argument, or omit it to start a fresh command.

toggle_expanded(set, id)

@spec toggle_expanded(MapSet.t() | list() | nil, term()) :: MapSet.t()

Toggles an id in an expanded set — convenience for on_toggle_expand handlers so consumers don't re-write the member?/put/delete boilerplate.

Normalizes the first argument to a MapSet (accepts a MapSet, a list, or nil) and returns a MapSet. The id should be the value delivered to your handler under the "event-id" param key.

def handle_event("toggle_subproject", %{"event-id" => id}, socket) do
  {:noreply, update(socket, :expanded, &PhoenixLiveGantt.toggle_expanded(&1, id))}
end