One task, several people, each for a part
A task is not one person's block. On Build, Ana hands the design over on the first day, Ben and Dee build from the second, and Cy joins at half time for the last week to test. Each of them is an assignment that books only its slice of the task's work.
The lanes under a bar are the assignments. Drag a lane sideways to change where the part starts, or drag its right edge to change how long it lasts. The editor beside the chart changes the same fields by number. The task bar does not move: assignments are data, the task keeps its dates. What moves is the load: the lanes, and the overload strip under the chart.
A slice is two numbers
offset is how much of the task's work passes before the part starts, andduration is how much work it books. Both count working days of the task, so a weekend or a pause added by leveling moves every part with the work. A part past the task end is clipped; one that starts after the end books nothing and stays in the editor so you can bring it back.
// Build is one 10-day task with four assignments, each for a slice of its work.
// offset and duration are working days of the task's work; no duration = to the task end.
assignments: [
{ id: "2-p1", task: 2, resource: "ana", value: 1, offset: 0, duration: 1 }, // design hand-off
{ id: "2-p2", task: 2, resource: "ben", value: 1, offset: 1 }, // dev 1
{ id: "2-p3", task: 2, resource: "dee", value: 1, offset: 1 }, // dev 2
{ id: "2-p4", task: 2, resource: "cy", value: 0.5, offset: 5 }, // QA, half time
]
// One edit is one assignment op. Task dates do not change; the load does.
project.apply({ assignments: { update: [{ id: "2-p4", offset: 7 }] } });
project.solve(null);
const profile = project.loadProfile({ from, to, basis: "day", tasks: "all" });The lanes are drawn from loadProfile: a day belongs to a part when the person's bucket for that day lists the task. The page does no calendar math. While you drag, it askspreview for the profile of the changed plan, so the lanes and the overload strip update live and nothing is applied until you let go. To turn a drag into working days, it previews the part once without a slice at the start of the drag and snaps to the days that preview books.
Who should take this part?
Click the person on a lane. The engine scores every person who is not already on the task, plus the current one, on a leveled copy of the plan. Each candidate changes one assignment, so the other parts stay where they are.
// Who should take the QA part? Each candidate changes only that assignment.
// The current person is a candidate too: shifts are measured against it.
project.evaluate([
{ id: "cy", changes: { assignments: { update: [{ id: "2-p4", resource: "cy" }] } } },
{ id: "eli", changes: { assignments: { update: [{ id: "2-p4", resource: "eli" }] } } },
], { level: { allowSplits: true }, focus: 2 });Leveling sees the slices
Cy's QA part overlaps a Design QA task in Build's second week. Overallocation windows, leveling andevaluate all see sliced load, so only the overlapping days count. PressLevel resources and the log names the slice that collided.
// Cy's QA part (0.5) overlaps Design QA (1.0) on Thu 20 – Fri 21 Aug: peak 1.5 of 1.
project.level({ allowSplits: true });
// updates (the project uses new Project({ dates: "date" }), so dates are Date values):
// Design QA works its first day, pauses while Cy is on Build, then finishes
{ id: 4, kind: "task", end_date: new Date(2026, 7, 29), pauses: [{ at: 1, length: 7, by: "leveling" }] }What this demo leaves out
One person has at most one part of a task. The load profile lists task ids, not assignment ids, so two parts of one person on the same task could not be told apart in the lanes; the editor refuses a second part for the same person. And the task end does not follow its parts: an effort-driven task that ends when its last part ends is not in the engine.
The assign demo scores whole tasks the same way, one person per task.