[0.4.0] — 2026-06-26
Configurable bar labels (inside / outside / fit / watermark), a sticky sidebar, and a batch of label/marker fixes.
Added
- Configurable bar labels via
label_position—:none(clean bars),:inside(overlaid, the default),:outside(in the empty track beside the bar, never clipped),:fit(overlaid but hidden on bars too narrow to showlabel_fit_ratioof the label), and:watermark(a big, soft, italic copy beside the bar). Tuned withlabel_side,label_overflow,label_fit_ratio, andlabel_watermark_opacity.:fitis decided in the browser via a pure-CSS container query against each bar's rendered width, so it tracks the responsive fill at every zoom with no JavaScript. - Sticky sidebar label column — the task-name column stays pinned during horizontal scroll.
Changed
:autooutside/watermark labels dodge dependency arrows. When one side of the bar is free of connector arrows, an:autolabel picks that side, falling back to the fit-based after→before flip otherwise.- The too-small-task marker is centered on the task span (
tiny_bar_px), instead of sitting at the task's start. - The default bar title is now a sibling overlay in every mode, so an
:outside/:visiblelabel is never clipped by the bar'soverflow-hidden;bar_title_classis now typography-only.
Fixed
- Connector labels are no longer horizontally stretched. They render as an
HTML overlay beside the arrow instead of SVG
<text>inside the fill-stretched connector layer (which distorted the glyphs); they still track the arrow as the chart fills and keep a readable halo. :insidemilestones no longer render a label overlapping the diamond. A diamond has no interior, so it shows no:inside/:fitlabel — use:outside/:watermarkto label milestones beside the diamond.- The too-small-task popover opens at the correct position at week/month zoom — the triangle marker now re-anchors the popover to its bar.
- The milestone
:outsidelabel gap no longer grows with the responsive fill — it's anchored at the diamond center with fixed-px clearance.
[0.3.0] — 2026-06-22
Followable connector routing for dense charts, a relocated Today badge, and theme-aware sub-project frames.
Added
- Outer-gutter routing for long dependency skips. When a forward arrow skips down a tight "staircase" of consecutive bars with no clear channel anywhere (e.g. a packed waterfall layout), the trunk now descends a clear column to the LEFT of the staircase and crosses straight to the target, instead of piercing or hugging an intervening task. It shares that descent with sibling arrows from the same source — each branches off toward its own target — so they read as one line rather than crossing strokes.
Changed
- Connector trunks keep real clearance from unrelated bars. A forward trunk aims for a comfortable gap from any bar it crosses, tightening toward a 1px floor only when forced, and routes via a detour when no clear channel exists — instead of running flush along a bar's edge (where it reads as part of the bar) or straight through it.
- The "Today" badge moved to the date-header row. It sits flush on the marker line (no border seam) rather than at the top of the body, where it overlapped bars and too-small-task markers.
- Sub-project frame colors are theme-aware. The expanded-sub-project band now
uses translucent daisyUI tints (
color-mix+ the--color-*vars) per nesting depth, so it adapts to light/dark themes and no longer washes out the label text — previously an opaque light hex that looked harsh on dark themes. Overridesubproject_frame_colorwith any CSS color to customize.
Fixed
- No dialyzer warning on the sub-project date roll-up. The parent-span
roll-up uses a map-update (
%{ev | ...}) rather than a named struct-update that a genericEnum.mapbinding can't narrow toTask— a harmless but noisy success-typing note.
[0.2.0] — 2026-06-22
Week/month axis legibility + a solid arrowhead.
Changed
- Week/month axis snaps to whole columns. At
:week/:monthgranularity the date axis now rounds OUTWARD to whole-week (Mon–Sun) / whole-month boundaries, so every column is a complete, boundary-aligned week/month instead of a ragged partial stub (e.g. a 2-day "Sat–Sun" sliver). Bars keep their true dates within the widened axis. Pass a tight, task-fitted range and the chart rounds it out on its own. Finer granularities are unaffected. - Week columns are labeled with their date span ("Apr 27 – May 3", or "May 4 – 10" within a month) instead of the ISO ordinal ("W18") — a range reads without mapping a week number back to dates.
Fixed
- A week straddling New Year is one column, not two. Week chunking now keys on
the full ISO week (
:calendar.iso_week_number/1) rather than{calendar_year, week}, so ISO week 53 (Mon 2026-12-28 → Sun 2027-01-03) no longer splits into two mislabeled stubs across the year boundary. - Connector arrowheads render solid. The head no longer inherits the line's
alpha (the default
text-base-content/50), so the shaft can't show through a half-transparent triangle. The line stays subtle; only the head is made opaque, and it works for any custom connector color (e.g.text-primary/30→ a subtle line with a solidtext-primaryhead).
[0.1.1] — 2026-06-13
Docs + accessibility. No API changes.
Added
- Much-expanded README: "Making it interactive" (hooks, the built-in toolbar,
the
on_*callback table, and theextra.actions/extra.badgesshapes), "Translations" (the chrometranslationsmap vs. consumer-resolved content — works with gettext, Cldr, or a JSONB multilang column), "Live updates", and an "Accessibility" section. New Gotchas: nil/duplicate id raises,todaydefaults to UTC, andwindow_start/window_endis all-or-nothing. :docfor thetranslationsattr.
Accessibility
- Sub-project chevrons now expose
aria-expanded(and anaria-label), so screen readers announce expand/collapse state. - The decorative connector + arrowhead SVGs are
aria-hidden, so a screen reader walks the bars rather than the path geometry.
[0.1.0] — 2026-06-13
Initial release. Extracted from live_calendar's waterfall view into a
standalone package.
Features
PhoenixLiveGantt.gantt/1component — horizontal bars on a time axis with orthogonal connector routing (FS/SS/FF/SF), bar-edge attach modes, bus stagger, smart trunk consolidation.PhoenixLiveGantt.Taskstruct — Gantt-focused (no calendar/recurrence baggage).- Sub-projects: any task with
extra.parent_idbecomes a child; parents roll up over descendants, expand/collapse via chevron or popover button, with framed timeline + sidebar treatment. - Per-bar popover (click to open) with title, assignee/progress subtitle, optional custom action buttons (icon + tooltip + phx-click + per-event badges).
- Corner badges (notification-style pills with stacking + flash).
- Built-in
PhoenixLiveGantt.Inspectorfor HTML → geometry parsing andPhoenixLiveGantt.TestHelpersfor property assertions. mix phoenix_live_gantt.dumpfor offline geometry inspection.- JS hooks
LgBarPopover+LgAutoScroll. PhoenixLiveGantt.scroll_to_start/2— scroll the timeline back to its start. APhoenix.LiveView.JScommand (composes withJS.push/2) that theLgAutoScrollhook consumes (lg:scroll-start) to scroll the chart to its 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 parked at a stale spot. A pending-flag in the hook makes the scroll authoritative across the refit patch even when it moves the today marker (which would otherwise re-center on today).window_start/window_endattrs — sub-day positioning window. The positioning axis is normallydate_range's whole-day, midnight-to-midnight span. A consumer can now override the ORIGIN and SPAN with a pair ofNaiveDateTimes so the axis starts/ends partway through a day — e.g. ~1 column before the first task at:hour/:min15/:min5zoom, instead of a wall of empty pre-task columns from midnight. Positioning threads aview = {origin, span_days}(origin is the whole-dayrange.firstDate in the default path, aNaiveDateTimewhen overridden) through bars, connector endpoints, the today marker, sub-project frames, obstacles, and a newwindow_columns/5column builder that walks fixed slot-minute steps from the origin (labels: the date on each midnight slot, a bare hour on:hour, the:15clock boundaries on sub-hour zooms).date_rangestill drives event partition / edge counts, so keep it covering the same window. Behavior is byte-identical when the override is absent (origin =range.first, span =total_days). Snapwindow_startto a slot boundary so column labels land on round clock times.tiny_bar_pxattr (default5) — "too small to see" marker. A bar whose TRUE width renders narrower than this many SCREEN pixels gets a small fixed-size down-triangle at the task's start, signalling a task that's there but too short to see. The decision is pure CSS — each marker lives inside a per-taskcontainer-type: inline-sizeelement whose width tracks the bar's rendered width, and an injected container query (@container (max-width: {tiny_bar_px}px)) reveals it. So it's server-emitted and browser-resolved against true screen pixels: correct under the responsive fill + zoom, instant on first paint (no socket/hook/measurement), and re-resolved on resize by the browser with zero JavaScript. The marker is clickable (opens the same popover — that part needsenable_hooks). Set0to disable. Pairs withmin_bar_px: 0(the default) so bars stay honest while hairline tasks remain discoverable. Assumes a uniformtiny_bar_pxacross charts sharing a page.min_bar_pxattr (default0) — bars reflect their TRUE duration. Previously every non-milestone bar was floored to a 4px minimum so a short task stayed a visible sliver. That made the bar overstate the task's span and diverge from the connector geometry (arrows attached to a phantom edge). The floor is now opt-in: by default a bar is exactly as wide as its duration (a task too short to show at the current zoom is a hairline / vanishes until you zoom in), so the chart is honest and connectors attach to the real edge. Setmin_bar_pxto e.g.4to restore the always-visible-sliver behavior. (A zero-DURATION task is still a milestone diamond regardless.) Connector endpoints are DRAWN from the RENDERED bar edges (so a non-zeromin_bar_pxstays consistent with where arrows attach), but the backward/invalid ("time-travel") decision is JUDGED from the NATURAL temporal edges — otherwise a zero-gap FS dependency (B starting exactly when A finishes) would be falsely flagged backward by A's min-width sliver poking past B's start.:hourzoom + continuous coordinates. The positioning axis is now a continuous "fractional days from range start" used uniformly by bars, the today marker, connector endpoints, and columns — so a:hourzoom (and DateTime/NaiveDateTimestart/end) renders intra-day detail (a 2h and a 6h task differ in width/position).Dateinputs at day/week/month zoom are byte-identical to before.todayaccepts aDateTime/NaiveDateTimefor a precise "now" line + current-hour column highlight; positioning uses wall-clock time (DST-safe).PhoenixLiveGantt.Layout.sequential/2gained a:min_span{unit, n}option and emits sub-day temporals when its:start/:advancedo.- Responsive fit-to-width (pure CSS, no round-trip). Horizontal geometry
(bars, columns, today marker, sub-project frames, badges, popovers) now
renders as PERCENTAGES of the content width, and the timeline uses
width: 100%; min-width: {content_px}pxinsideoverflow-x-auto. A short chart fills the container exactly (no gap); a long one scrolls at its natural density — instantly, on first paint, with zero measurement or server round-trip. The connector SHAFT SVG keeps a pixel viewBox but renderswidth: 100%withpreserveAspectRatio="none"+vector-effect="non-scaling-stroke", so the lines scale in lockstep with the bars and stay aligned at any width (the connector router is unchanged). Arrowheads are drawn in a SEPARATE, non-stretched overlay (positioned by%so the tip tracks the bar-aligned shaft end, but sized in fixed px so the triangle never distorts) — a stretched line is still a correct line, but a stretched triangle is not an arrowhead.day_width_pxnow sets the natural content width (scroll threshold / density);default_day_width_px/1exposes the per-zoom defaults. - Off-screen Today hint. When
todayfalls outsidedate_range, a directional pill (← Today/Today →) now pins to the edge pointing toward today, instead of the consumer having to widen the axis to keep the marker on screen. Optionalon_show_todaymakes it clickable (e.g. to jump to today); otherwise it's informational. The vertical marker line still renders only when today is in range.
Fixes
- Connectors to/from a task at the very edge of the window no longer clip off the
chart. A task flush against the left/right edge had no room for its connector's
exit/entry stub (it bulges ~
@elbow_pxpast the bar), so the stub — and sometimes the arrowhead — drew pastcontent_widthand got clipped by the chart'soverflow-x-auto. The time axis now reserves a fixed@axis_pad_px(16px) of horizontal breathing room on each side: every x coordinate shifts in by the pad,content_widthgrows by 2×, and transparent spacer columns hold the margin so bars still exactly cover their time columns. (Absolute %s move, but every layer shares the padded denominator, so alignment is unchanged.) - Arrowheads into a milestone no longer detach from the shaft at a low fill
factor. The head is nudged
@milestone_edge_px(12px) out to the diamond's edge — a fixed SCREEN px — but it rides the connector's final approach segment, which was only@elbow_px(10px) of VIEWBOX. When the chart scrolls rather than fills (e.g.:min5, where that segment renders ~1:1), 12px of nudge overshot the 10px segment and the head floated off the trunk, disconnected. A milestone target now gets an approach stem a hair longer than the nudge (@milestone_edge_px + 2), so the head always lands ON the shaft at every zoom. (Connectors are still the normal horizontal zigzag — longer at a high fill is fine.) - An open bar/label popover now sits above everything else in the chart
(
z-[60]). It wasz-40— tying with milestone diamonds — and since rows areposition: relativewith no z-index (one shared stacking context), a popover that overhung the row below lost to that row's diamond by DOM paint order and got clipped. Clicking a milestone in a stack of same-date diamonds now shows an un-occluded popover. (Above bars z-10, today line z-30, diamonds z-40, and badges z-50.) - Milestone diamonds are now clickable. They rendered with
cursor-pointer(the defaultmilestone_class) but carried no popover wiring — only the optionalon_event_click— so a consumer that relied on the built-in popover (as bars do) got a diamond that looked clickable but did nothing. Diamonds now get the sameLgBarPopoverhook + popover sibling as bars, so clicking one opens its title/actions popover AND highlights its dependency tree (fading unrelated tasks) — the tool for tracing arrows through a cluster of same-date milestones. (The dependency highlight walks ancestors only, by design; an inline comment claiming it walked "both directions" was stale and has been corrected.) - Column-header "today" highlight now honors the
todayattribute instead of always computing againstDate.utc_today(), so it agrees with the today-marker line when a consumer passes an explicittoday. - The built-in toolbar's Today button is now disabled (with an
explanatory tooltip) when it can't actually scroll — i.e.
enable_hooksis off and noon_scroll_todayis wired — instead of rendering enabled and silently doing nothing. (Default scroll-to-today needsid+enable_hooksso theLgAutoScrollhook has a target + listener.) - Popover action buttons now always expose the event id as
phx-value-event-id(hyphen), even when the action sets aphx_valuemap — previously a map made itphx-value-event_id(underscore), disagreeing with the no-value path and the chevron. Handlers now read%{"event-id" => id}consistently. LgBarPopoverre-anchors a bar popover to the bar's CURRENT geometry on open, instead of trusting its (frozen,phx-update="ignore") server- rendered position. Fixes popovers opening far from their bar after the chart re-rendered with new geometry — e.g. switching zoom.- Connector arrowheads no longer distort under the responsive fill. They were
SVG
<marker>s inside thepreserveAspectRatio="none"shaft SVG, so at high fill factors they stretched into thin, disconnected-looking triangles. They now render in a fixed-px overlay anchored by%to the shaft's true terminal point (PhoenixLiveGantt.PathFormat.terminal/1), so the head stays on the shaft end even whenconsolidate_piercing_trunksre-routes a forward path to end at a different y (the old marker rode the path, the overlay must re-derive it). NewInspectorarrowhead extraction +TestHelpers.assert_arrowheads_at_path_ends/2(wired intofind_geometry_issues/2) lock the head-meets-shaft invariant. - Sub-day tasks are no longer mis-routed as milestones.
milestone?/1(connector routing) testedDate.diff(end, start) <= 0, so any task shorter than a full day — common at:hourzoom — started and ended on the same DATE and was treated as a zero-duration milestone, even thoughbar_geometry/3(which uses fractional days) rendered it as a thin bar. The router then applied milestone endpoint offsets + the 10px diamond gap and frequently mis-flagged the dependency as backward (dashed), so arrows routed to/from a phantom diamond and looked disconnected.milestone?/1now uses the same fractional-day duration test asbar_geometry/3(identical to the old behavior for pure-Dateevents). - Arrow tips now land ON the target bar's edge (gap 0) instead of a 4px natural gap. Under the responsive fill the shaft SVG stretches with the bars, so a natural-px gap was magnified into a visible disconnect (4px → ~15px at a 3.8× fill); the fixed-px arrowhead overlay now supplies the visual separation, so arrows read as connected at any fill factor. (Milestone targets keep their diamond-clearance gap.)
Ergonomics & docs
PhoenixLiveGantt.Layout.sequential/2— optional, domain-agnostic helper that lays items with durations + order + sub-projects out intostart/enddates (sequential waterfall, sub-project span, day-aligned:min_span_days) with a pluggable:advancecalendar callback. Keepsgantt/1render-only while giving consumers the durations→dates layout they'd otherwise hand-roll. Does not do dependency-driven scheduling / critical path / resource leveling.PhoenixLiveGantt.toggle_expanded/2— convenience foron_toggle_expandhandlers (accepts a MapSet/list/nil, returns a MapSet).- README rewritten with the steps/gotchas that bite first-time consumers: the
required Tailwind content source (no stylesheet ships),
endbeing exclusive, the sub-project rules (always include descendants; give parentsnildates so they roll up), and theon_toggle_expand"event-id"param key. expanded/on_toggle_expandnow carry attr docs (the:allshortcut, the hyphenated param key).
Naming
CSS class prefix: lg-. JS hooks: LgBarPopover, LgAutoScroll.
Events dispatched: lg:scroll-today.