Preview leveling#

Show the user what leveling would change before it touches the project, then keep the leveled plan or throw it away.

Project#level commits. It writes levelingDelay and pauses into the project and accepts the leveled dates as the new baseline. To get a proposal instead, level a copy. Project#copy is cheap, because the data stays shared until one copy is edited.

Prerequisites#

  • A solved Project: solve(data, options) has run.
  • The data has resources and assignments, so there's overallocation to level.
  • The project is forward-scheduled. level refuses config.scheduleFromEnd = true with an InputException.

Steps#

  1. Copy the solved project:

    Project draft = project.copy();
    
  2. Level the copy. The original keeps its dates, its baseline and its overallocation:

    LevelingResult proposal = draft.level(new LevelingOptions());
    
  3. Show the proposal. proposal.schedule.updates lists the tasks leveling moved, with their new dates, levelingDelay and pauses. The diff is relative to the state the call started from, and that state matches the original. Apply the entries to the model you already hold.

    for (Mutation update : proposal.schedule.updates) {
        System.out.println(update.id + ": start " + update.startDate
            + ", delay " + update.levelingDelay + ", pauses " + update.pauses);
    }
    
  4. Show what leveling couldn't fix. proposal.schedule.overallocations holds the windows left after leveling. It's null when the project has no assignments. Each window has a reason, such as fixed-task or priority-1000:

    if (proposal.schedule.overallocations != null) {
        for (ResourceOverallocation over : proposal.schedule.overallocations) {
            for (OverallocationWindow window : over.windows) {
                System.out.println(over.resource + ": " + window.from + " .. " + window.to
                    + ", " + window.reason);
            }
        }
    }
    
  5. Apply the user's choice:

    • Accept: replace the original with the copy, project = draft;. The project now holds the leveled baseline.
    • Reject: drop draft. Nothing else needs undoing.

    Replace the original only if nobody edited it after you copied it. Otherwise the proposal comes from an older state, so level a new copy.

The full example levels two tasks that book the same developer full time on Monday and Tuesday:

import java.util.List;
import org.dhtmlx.plen.*;

public class PreviewLeveling {
  static TaskData task(String id, Moment start, double duration) {
    return new TaskData(Id.str(id)).withStartDate(start).withDuration(duration);
  }

  static AssignmentData booking(String task, String resource) {
    return new AssignmentData()
        .withTask(Id.str(task))
        .withResource(Id.str(resource))
        .withValue(1.0);
  }

  public static void main(String[] args) throws Exception {
    // Monday, August 10, 2026, on the default calendar.
    var monday = Moment.fromYmdHm(2026, 8, 10, 8, 0);
    var data = new ProjectData()
        .withTasks(List.of(task("A", monday, 2.0), task("B", monday, 2.0)))
        .withResources(List.of(new ResourceData().withId(Id.str("dev1"))))
        .withAssignments(List.of(booking("A", "dev1"), booking("B", "dev1")));

    var project = new Project();
    project.solve(data, new SolveOptions());

    Project draft = project.copy();
    LevelingResult proposal = draft.level(new LevelingOptions());

    for (Mutation update : proposal.schedule.updates) {
      if (update.startDate != null && update.endDate != null) {
        System.out.println(update.id + ": " + update.startDate + " .. " + update.endDate
            + ", delay " + update.levelingDelay);
      }
    }
    var over = proposal.schedule.overallocations;
    System.out.println("overallocated resources left: " + (over == null ? 0 : over.size()));

    var original = project.solve(null, new SolveOptions());
    System.out.println("original still overallocated: " + original.overallocations.size());

    boolean userAccepts = true;
    if (userAccepts) {
      project = draft;
    }
    var plan = project.solve(null, new SolveOptions());
    System.out.println("baseline matches proposal: "
        + plan.baselineId.equals(proposal.schedule.baselineId));
  }
}

Result#

The example prints the one task leveling moved. The proposal has no overallocation left. The original still has one:

B: 2026-08-12 08:00 .. 2026-08-14 08:00, delay 2.0
overallocated resources left: 0
original still overallocated: 1
baseline matches proposal: true

B waits two days for A, so its levelingDelay is 2.0. Until you replace project with draft, project still reports the Monday-Tuesday window on dev1. After the swap, baselineId is the token from the leveling result. Baseline tokens are opaque, so compare them only within one process.

The copy is a full solved project, so you can also call draft.loadProfile(...) on it to draw the leveled load. See Resource load. To compare leveling across several alternative change sets with scores, use evaluate with EvaluateOptions.level. See Evaluating alternatives. For options, reasons and splits, see Leveling resources.