English
Resource Load
Resource load / swimlane view (new component, data family): given resources and tasks (tasks carrying assigned resources), it renders resource swimlanes (one row per resource, showing its assigned task bars on a shared timeline) and a load histogram (parallel task count per period by scale, highlighting overload above capacity). It is a pure view layer — no scheduling/leveling engine; leveling is exported as a pure function levelResources(), and the host decides whether to write back.
Basic Usage
Resource swimlanes + load histogram: resources (with capacity) + tasks (with resources assignment)
resources:[{ id, name, capacity?, workdays?, holidays?, capacityOverrides?, skills? }]. Whenworkdaysis absent andholidays/capacityOverridesare empty,capacity= parallel tasks per period (legacy semantics, falls back to thecapacityattribute, ultimately 1). Withworkdays(0=Sunday…6=Saturday;[]means fully unavailable), non-emptyholidays, or non-emptycapacityOverrides, capacity is per day and a period capacity is the sum of its days.capacityOverrides(YYYY-MM-DD → capacity) wins overworkdays/holidaysand may be fractional (such as0.5); task load is counted in whole days, so a 0.5 day cannot hold a whole-day task (reserved for a future effort model).skillsis a skill-tag array.tasks:[{ id, name, start, end, resources?: string[], priority?, requiredSkills?, project? }]—start/endareYYYY-MM-DD(day precision only, inclusiveend; time-bearing input is treated as that day);resourcesis the assigned resource id array; assignments pointing to unknown resources are ignored.priority: lower is more important;requiredSkillsrequires every assigned target resource to have all tags during reassign;projectworks withlevelResources({ projectOrder })for cross-project priority.scale:day(default) /week/month— controls the histogram period granularity and axis labels.- Swimlanes and histogram share column width and horizontal scroll; overload bars get
.over(color via--oas-color-danger); overlapping task bars within a resource are stacked in sub-rows (no mutual occlusion, clicking a bar hits that bar). - The lane-gap token
--oas-resource-lane-gapis derived from row height and bar height (--oas-resource-row-heightparticipates; a multi-segment track's real height = segments × bar height + (segments + 1) × gap). The token is declared on the component host element: only element-level overrides take effect — inline style or a class rule hitting the host element (e2e-verified: a host class setting--oas-resource-lane-gap:4px→ single-segment track height 36 → 28);:root/ ancestor-level declarations do not (the host already declares its own value, so the inherited value is overridden).
Overload & Leveling
Detect overload (oas-overload) → one-click leveling: keep durations and shift (default) or reassign with mode: reassign (dates unchanged); the host writes tasks back
Leveling is a host decision: the component only exports a pure function
ts
import { levelResources } from '@oas-ui/ui'
// Shift (default): changes dates, keeps durations
const moved = levelResources(resources, tasks, { scale: 'day' })
// Reassign: dates unchanged; move tasks to resources with spare capacity for the whole span
const reassigned = levelResources(resources, tasks, { scale: 'day', mode: 'reassign' })
// Both return full copies of the changed tasks; merge by id and write back
el.tasks = tasks.map((t) => [...moved, ...reassigned].find((m) => m.id === t.id) ?? t)levelResources guarantees: deterministic (same input → same output), iterates until no overload or maxIter; it does not mutate the input. The component never calls it automatically.
Capacity calendars, skills, and priority
ts
const resources = [
{ id: 'alice', name: 'Alice', capacity: 1, workdays: [1, 2, 3, 4, 5], holidays: ['2026-03-05'], skills: ['frontend'] },
{ id: 'bob', name: 'Bob', capacity: 1, capacityOverrides: { '2026-03-06': 0.5 }, skills: ['backend'] },
]
const tasks = [
{ id: 't1', name: 'Page work', start: '2026-03-02', end: '2026-03-06', resources: ['alice'], requiredSkills: ['frontend'], priority: 0, project: 'portal' },
]
// Weekly capacity sums each working day; holidays contribute 0; override capacity may be fractional (task load counts whole days)
levelResources(resources, tasks, { scale: 'week', projectOrder: ['portal'] })Capacity calendar: holidays show 0 and half-day capacity shows an overload bar (day scale)
- Without
workdaysand with emptyholidays/capacityOverrides, the legacy per-period capacity semantics are unchanged; with them, period capacity is the sum of daily capacity. Emptyholidays: []/capacityOverrides: {}count as absent and do not switch the legacy semantics. workdaysuses0=Sunday…6=Saturday;workdays: []means zero capacity every day (fully unavailable). A non-empty array with no valid values (strings, out-of-range, 1..7 convention) is treated as absent with a devWarn instead of silently making the resource unschedulable.capacityOverrideswins overholidays; capacity may be fractional, but task load is counted in whole days, so a0.5day cannot hold a whole-day task.reassigndefaults torespectSkills: true; every target must have all of a task'srequiredSkills. No skill data means no behavior change.- Projects earlier in
projectOrderwin; within a project lowerpriorityvalues win. Lower-priority tasks move/reassign first. Project names and skills are compared after trimming. - The algorithm remains deterministic and greedy; it does not claim a global optimum.
shiftskips a resource only whenworkdays: []and future positive overrides cannot hold its tasks (structurally unschedulable), with a devWarn instead of pushing tasks pastmaxIterperiods; consecutive holidays / a single zero override can still be shifted past.reassignreturns no targets when none is feasible.
options.mode (default shift):
shift: each task keeps its duration and only moves later; suitable when there is deadline slack.reassign: dates never change — tasks on an overloaded resource are moved to a resource with spare capacity in every period the task spans (picked by the lowest worst load ratio, ties by resource input order; multi-resource tasks only swap the overloaded assignment). Stops when no feasible target exists — it never degrades to shifting; the right mode when deadlines are hard and only people can move (the pm template's "balance load" button).
In
weekscale the step is one week period, but the pure function has no locale context (week start defaults to Monday); for tasks crossing week boundaries pass{ weekStart: <matching the page locale> }(0=Sunday … 6=Saturday) explicitly to avoid a boundary mismatch with the histogram's week granularity.
Readability (opt-in switches)
Readability switches (opt-in): load values on pillars (show-load-values) + overload badge (show-overload-badge) + bar labels (bar-label)
All three switches on: pillar tops show the parallel task count per period (overloaded in red); Alice's row header shows an "Overloaded 1 period" badge; task bars show their names.
All three readability capabilities are opt-in (off by default, zero extra nodes): show-load-values displays the parallel task count on top of every load > 0 pillar (decorative numbers, aria-hidden; overloaded pillars get .over using --oas-color-danger); show-overload-badge shows an "Overloaded N periods" badge on the swimlane row header of overloaded resources (visible information, not aria-hidden, copy follows the global locale instantly); bar-label shows the task name inside the bar (rendered as plain text against injection, single-line clipped on narrow bars; does not affect aria-label or click hit-testing).
Localization & Scrolling
locale="en-US" (Sunday-based) + scale="week": labels and week start follow en-US; scrollToDate scrolls the timeline to a given date
Instance method scrollToDate(date): horizontally scrolls the timeline to the given date (YYYY-MM-DD, centered in the viewport, minus the sticky resource name column width). Invalid dates / no data are no-ops; RTL layouts use the negative scrollLeft model (same as gantt).
Server-side row window (windowed)
For thousands of resources / hundreds of thousands of tasks: the host supplies only the resource-row slice near the visible window and the component holds no full dataset. The row window is computed from the scroll position and, when not covered by the loaded range,
oas-window-requestis emitted; the host refills and rows render by absolute row index.
Attributes: windowed (enable) + total (full resource row count) + window-start (absolute row index of the slice's first row) + range-start / range-end (host-managed time-axis extent, ISO dates, range-end inclusive). Events:
oas-window-request:detail: { start, end, reason }(half-open, absolute row indices); the same expected window is not emitted twice.oas-range-request: emitted when the visible time window approaches an axis edge;detail: { startMs, endMs, reason }.
html
<oas-resource windowed total="5000" window-start="0" range-start="2026-01-01" range-end="2026-12-31"
scale="day" height="360"></oas-resource>
<script>
const el = document.querySelector('oas-resource[windowed]')
el.addEventListener('oas-window-request', (e) => {
const { start, end } = e.detail
el.setAttribute('window-start', String(start))
el.setAttribute('resources', JSON.stringify(fetchResources(start, end))) // fetch from your backend
})
</script>Conventions in windowed mode: load/overload stats cover the loaded slice only (full-dataset stats are the host's job) with a devWarn; without range-start / range-end the time axis falls back to deriving from loaded tasks with a devWarn; oas-tasks-change carries only the slice in detail.tasks — the host must merge by id into the full dataset, never replace it wholesale.
For the full host recipe (segment fetch, abort in-flight, merge by id, atomic refill) and a runnable example, see Server-side windowed data.
API
oas-resource
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
bar-label | Show the task name inside the bar (boolean, off by default): single line with ellipsis, clipped on narrow bars; aria-label and click/drag hit-testing are unchanged | boolean | — |
capacity | Default capacity when a resource declares none (default 1) | string | 1 |
header-sticky | Lane header sticks to the top while scrolling lanes (on by default): week/day ticks stay visible; header-sticky="false" disables it (the header scrolls with content). The corner name column always stays pinned on horizontal scroll (LTR/RTL) | — | — |
height | Component height in px (default 320; accepts a plain number or Npx): mapped to the custom property --oas-resource-height (the host's own inline height always wins and is never overwritten by the component). While the height attribute is present and valid (>0 plain number or Npx) the component takes over this variable — writing it inline on the host and overriding any --oas-resource-height the host set itself; an empty/whitespace value is treated as unset; an invalid value writes nothing and only clears a value this instance wrote earlier, so a host token is never affected; after the attribute is removed the component-written value is cleared to zero, so the height falls back to the token default (320) or host CSS, and the host's previous inline value is not restored; when the height attribute was never set the component never writes the variable, and a --oas-resource-height set by the host (inline or via class rules) is left completely untouched | string | 320 |
locale | Localization (overrides config-provider injection and global locale) | string | — |
range-end | Host-managed time-axis end (ISO date YYYY-MM-DD, inclusive); pair with range-start. If only one is given, reversed or invalid, the axis is not host-managed and a devWarn is emitted | string | — |
range-start | Host-managed time-axis start (ISO date YYYY-MM-DD): when provided the period range is no longer derived from loaded tasks | string | — |
resources | Resources (JSON [{ id, name, capacity?, workdays?, holidays?, capacityOverrides?, skills? }]). workdays uses 0=Sunday…6=Saturday; [] means fully unavailable. When workdays is absent and holidays / capacityOverrides are empty, capacity = parallel tasks per period (legacy); otherwise period capacity is the sum of daily capacity, non-empty capacityOverrides wins over workdays / holidays, and may be fractional while task load is counted in whole days (a 0.5 day cannot hold a whole-day task); empty holidays / capacityOverrides count as absent. skills is a skill-tag array. A resource may carry an optional color (CSS color or token, e.g. var(--oas-color-success)) used to tint its lane bars and load bars; when absent the unified --oas-resource-bar-bg / --oas-resource-load-bg applies | ResourceItem[] | [] |
scale | Histogram period granularity and axis labels: day (default) / week / month; invalid falls back to day | string | — |
show-load-values | Show the load value above each histogram bar (boolean, off by default): only for load > 0; overload values use text-on-primary (white in light / dark in dark) for contrast on the danger bar. The number is visual redundancy (aria-hidden); screen readers still get the column load description | boolean | — |
show-overload-badge | Show an "Overloaded ×N" badge in the lane header (boolean, off by default): only for resources with overloaded periods; text comes from the i18n key resource.overBadge | boolean | — |
tasks | Resource tasks (JSON [{ id, name, start, end, resources?, priority?, requiredSkills?, project? }]); start/end are day precision YYYY-MM-DD only (end inclusive; time-bearing input is treated as that day), resources is the assigned resource id array (assignments to unknown resources are ignored). priority: lower is more important; requiredSkills: every reassign target must have all tags; project works with levelResources({ projectOrder }) for cross-project priority | ResourceTask[] | [] |
total | Full resource row count in windowed mode; 0 or invalid disables windowed (falls back to local mode) | string | — |
window-start | Absolute row index (within the full dataset) of the first row (resource) in the slice (windowed mode) | string | 0 |
windowed | Server-side row-window mode (boolean, off by default): with total / window-start the host supplies only the visible window slice (rendered by absolute row index); when the expected row window is not covered, oas-window-request is emitted for the host to refill. Load/overload stats cover the loaded slice only (full-dataset stats are the host's job) with a devWarn; when range-start / range-end are absent the time axis falls back to deriving from loaded tasks with a devWarn. Non-windowed behavior unchanged | boolean | — |
Property (JS property; see each row for attribute reflection)
| Property | Description | Type | Default |
|---|---|---|---|
resources | Resources (JSON [{ id, name, capacity?, workdays?, holidays?, capacityOverrides?, skills? }]). workdays uses 0=Sunday…6=Saturday; [] means fully unavailable. When workdays is absent and holidays / capacityOverrides are empty, capacity = parallel tasks per period (legacy); otherwise period capacity is the sum of daily capacity, non-empty capacityOverrides wins over workdays / holidays, and may be fractional while task load is counted in whole days (a 0.5 day cannot hold a whole-day task); empty holidays / capacityOverrides count as absent. skills is a skill-tag array. A resource may carry an optional color (CSS color or token, e.g. var(--oas-color-success)) used to tint its lane bars and load bars; when absent the unified --oas-resource-bar-bg / --oas-resource-load-bg applies; property read is a copy: nested plain objects/arrays are deep-cloned — cycle-safe; shared references stay shared within a single read result; Date is copied by value (not shared); exotic objects such as class instances/Set are shared by reference; writing syncs the attribute + re-renders; if a custom key is not serializable, only that key is dropped from the attribute while the property readback stays complete | ResourceItem[] | [] |
tasks | Resource tasks (JSON [{ id, name, start, end, resources?, priority?, requiredSkills?, project? }]); start/end are day precision YYYY-MM-DD only (end inclusive; time-bearing input is treated as that day), resources is the assigned resource id array (assignments to unknown resources are ignored). priority: lower is more important; requiredSkills: every reassign target must have all tags; project works with levelResources({ projectOrder }) for cross-project priority; property read is a copy: nested plain objects/arrays are deep-cloned — cycle-safe; shared references stay shared within a single read result; Date is copied by value (not shared); exotic objects such as class instances/Set are shared by reference; writing syncs the attribute + re-renders + dispatches oas-tasks-change | ResourceTask[] | [] |
Events
| Event | Description |
|---|---|
oas-overload | Dispatched when the overload state changes (at most once per update; the same overload state is not re-dispatched), detail: { overloads: [{ resourceId, period, load, capacity }] } (period is the period start date); not dispatched when there is no overload |
oas-range-request | Emitted when the time axis is host-managed (range-start/range-end) and the visible time window approaches an axis edge; detail: { startMs, endMs, reason } (the neighbouring range requested). The same expected range is not emitted twice |
oas-task-click | Dispatched when a task bar in a resource swimlane is clicked, detail: |
oas-tasks-change | Emitted when the tasks property is written programmatically; detail: { tasks }. In windowed mode detail.tasks is only the loaded slice: the host must merge by id into the full dataset and must not replace it wholesale |
oas-window-request | Emitted in windowed mode when the expected row window is not covered by the loaded range; detail: { start, end, reason } (half-open, absolute row indices; reason is init / scroll / resize / data). The same expected window is not emitted twice |
CSS Variables
| CSS Variable | Default |
|---|---|
--oas-resource-bar-bg | color-mix(in srgb, var(--oas-color-primary) 22%, var(--oas-color-bg)) |
--oas-resource-bar-height | 20px |
--oas-resource-bar-hover-bg | color-mix(in srgb, var(--oas-color-primary) 34%, var(--oas-color-bg)) |
--oas-resource-header-bg | var(--oas-color-bg) |
--oas-resource-header-height | 32px |
--oas-resource-height | 320px |
--oas-resource-lane-gap | max( 0px, (var(--oas-resource-row-height, 36px) - var(--oas-resource-bar-height, 20px)) / 2 ) |
--oas-resource-load-bg | color-mix(in srgb, var(--oas-color-primary) 45%, var(--oas-color-bg)) |
--oas-resource-load-height | 64px |
--oas-resource-load-over-bg | var(--oas-color-danger) |
--oas-resource-name-width | 160px |
--oas-resource-row-height | 36px |