Rows of horizontal bands on a chart's x axis — what was scheduled, on or booked over the stretch a chart above shows. No JavaScript.
A row is one thing (a device, a room, a person, a task); its bands are the
stretches of x it occupies. Bands say nothing about what they mean: each
carries a variant (how it is drawn) and a class (its colour), and the
caller decides which of its own states map to which. Layer several bands in
one row to show, say, a planned window as a dashed outline with the
actually used part filled inside it.
Lining up with a chart
Bands are placed in percentages of the lanes' width with
PhoenixKitWeb.Components.Core.ChartScale — the scale line_chart/1
draws with. Give both the same x_domain and the same width (one wrapper,
no padding between them) and every band sits under the chart's x:
<div class="w-full">
<div class="h-48">
<.line_chart id="price" data={@prices} x_domain={{0, 1440}} step />
</div>
<.chart_lanes id="devices" rows={@device_rows} x_domain={{0, 1440}} marker_x={@now} />
</div>Bands are HTML, not SVG, so dashed borders and rounded ends keep their shape however wide the page is.
Scrolling
With more rows than scroll_after, the list scrolls inside a fixed
height while the chart above stays put. A classic (non-overlay) scrollbar
takes a few pixels from the right edge of the scrolling list; when exact
alignment matters at the right edge, give the chart the same right padding
or set scroll_after={nil} and let the page scroll.
Origin: generalised from NordSwitch's device schedule under its price chart.
Summary
Functions
Renders rows of bands on a shared x axis.
Functions
Renders rows of bands on a shared x axis.
Rows
Each row is a map:
:id— optional stable id; the row's DOM id follows it, so a reordered list keeps each row's element:label— what the row is (rendered over the row unless the:row_labelslot is given):note— optional secondary text beside the label:bands— a list of bands
Each band is a map:
:from,:to— x values, in the chart's units.nilis open-ended and runs to that edge of the domain.from == tois a point (a milestone, an event) and is drawn as a thin mark. A band outside the domain or running backwards is not drawn.:variant—:fill(default),:soft,:outlineor:dashed:class— its colour, as a text colour class ("text-primary"):label— optional text inside the band (a guest name on a booking):title— the native tooltip and the screen-reader text; defaults to the label, then the range throughx_format
Bands are drawn in list order, so later bands sit on top. Any other keys
are kept and handed to the :band slot.
Interactive bands
The :band slot renders inside each band and receives the band's own map,
so a band can hold a real link or button — the accessible way to make it
open a booking or a task:
<.chart_lanes id="bookings" rows={@rows} x_domain={{8, 20}}>
<:band :let={band}>
<button type="button" phx-click="open" phx-value-id={band.id} class="w-full h-full text-left px-1 truncate">
{band.label}
</button>
</:band>
</.chart_lanes>Examples
<.chart_lanes
id="boilers"
x_domain={{0, 1440}}
marker_x={@now_minute}
x_format={&clock_label/1}
rows={[
%{label: "Office boiler", note: "22 °C",
bands: [
%{from: 360, to: 540, variant: :dashed, class: "text-info", title: "Scheduled"},
%{from: 380, to: 470, variant: :fill, class: "text-success", title: "Heating"}
]}
]}
/>
<%!-- a custom label --%>
<.chart_lanes id="rooms" rows={@rooms} x_domain={{8, 20}}>
<:row_label :let={row}><.link navigate={row.path}>{row.label}</.link></:row_label>
</.chart_lanes>Attributes
id(:string) (required)rows(:list) (required) - Rows, each%{id, label, note, bands}(see above).x_domain(:any) (required) -{low, high}— pass the chart's ownx_domainso bands line up with it. Required: bands alone cannot say where the axis starts and ends.marker_x(:any) - x for a dashed vertical line across every row. Defaults tonil.ticks(:list) - x values for faint vertical gridlines (hours, days) across every row. Defaults to[].x_format(:any) - 1-arity fun formatting an x for band titles (e.g. minutes to a clock time). Defaults tonil.row_height(:any) - Row height in rem. Defaults to2.scroll_after(:any) - Rows shown before the list scrolls inside a fixed height;nilnever scrolls. Defaults to12.class(:any) - Classes for the wrapper (set text-* for the marker). Defaults tonil.aria_label(:string) - Defaults tonil.- Global attributes are accepted.
Slots
row_label- Custom content over each row; receives the row map via:let.band- Custom content inside each band; receives the band's map via:let.empty- Shown when there are no rows or the domain is unusable.