Skip to main content
Back to examples
Popular features/Baselines and recovery
Loading live demo…from dhtmlxcode.com
Popular features

Baselines and recovery

Keep one approved baseline and switch the live schedule between the contract plan, a delayed forecast, and a recovery plan.

gantt.plugins({ tooltip: true });

gantt.message({
	text: "Blue tasks are still on baseline. Red tasks are late. Green tasks show the recovery forecast.",
	expire: -1
});

gantt.config.date_format = "%Y-%m-%d";
gantt.config.open_tree_initially = true;
gantt.config.row_height = 40;
gantt.config.bar_height = 24;
gantt.config.scale_height = 50;
gantt.config.grid_width = 500;
gantt.config.baselines.render_mode = "separateRow";
gantt.config.baselines.row_height = 16;
gantt.config.baselines.bar_height = 9;

gantt.config.scales = [
	{ unit: "month", step: 1, format: "%F %Y" },
	{ unit: "week", step: 1, format: "%d %M" }
];

let currentScenario = "current";

let scenarios = {
	contract: { label: "Contract plan", shifts: { 11: 0, 12: 0, 13: 0, 14: 0, 21: 0, 23: 0, 22: 0, 24: 0, 31: 0, 33: 0, 32: 0, 34: 0, 41: 0, 43: 0, 42: 0, 44: 0, 50: 0 } },
	current: { label: "Current delay", shifts: { 11: 0, 12: 2, 13: 3, 14: 4, 21: 4, 23: 6, 22: 7, 24: 7, 31: 8, 33: 9, 32: 9, 34: 10, 41: 11, 43: 11, 42: 11, 44: 12, 50: 12 } },
	recovery: { label: "Recovery plan", shifts: { 11: 0, 12: 2, 13: 3, 14: 3, 21: 4, 23: 5, 22: 5, 24: 4, 31: 5, 33: 5, 32: 4, 34: 4, 41: 4, 43: 3, 42: 3, 44: 3, 50: 3 } }
};

let contractTasks = [
	{ id: 1, text: "Hotel opening program", type: "project", open: true },
	{ id: 10, text: "Guest rooms", type: "project", parent: 1, open: true },
	{ id: 11, text: "Model room approval", owner: "Client", parent: 10, start: "2026-04-06", duration: 5, progress: 1 },
	{ id: 12, text: "Room furniture install", owner: "Vendor", parent: 10, start: "2026-04-13", duration: 10, progress: .75 },
	{ id: 13, text: "Bathroom fixtures install", owner: "Vendor", parent: 10, start: "2026-04-16", duration: 8, progress: .6 },
	{ id: 14, text: "Corridor carpet and paint", owner: "Field", parent: 10, start: "2026-04-24", duration: 6, progress: .3 },
	{ id: 20, text: "Public areas", type: "project", parent: 1, open: true },
	{ id: 21, text: "Lobby stone delivery", owner: "Supply", parent: 20, start: "2026-04-27", duration: 7, progress: .55 },
	{ id: 23, text: "Lobby lighting install", owner: "Field", parent: 20, start: "2026-05-04", duration: 6, progress: .4 },
	{ id: 22, text: "Restaurant fit-out", owner: "Field", parent: 20, start: "2026-05-06", duration: 12, progress: .25 },
	{ id: 24, text: "Signage and wayfinding", owner: "Vendor", parent: 20, start: "2026-05-13", duration: 5, progress: .15 },
	{ id: 30, text: "Systems", type: "project", parent: 1, open: true },
	{ id: 31, text: "Fire alarm commissioning", owner: "MEP", parent: 30, start: "2026-05-18", duration: 6, progress: .2 },
	{ id: 33, text: "HVAC balancing", owner: "MEP", parent: 30, start: "2026-05-20", duration: 8, progress: .1 },
	{ id: 32, text: "Elevator inspection", owner: "Authority", parent: 30, start: "2026-05-27", duration: 4, progress: 0 },
	{ id: 34, text: "Life-safety systems test", owner: "Authority", parent: 30, start: "2026-05-29", duration: 5, progress: 0 },
	{ id: 40, text: "Opening readiness", type: "project", parent: 1, open: true },
	{ id: 41, text: "Staff training", owner: "Ops", parent: 40, start: "2026-06-02", duration: 5, progress: 0 },
	{ id: 43, text: "Punch list walkthrough", owner: "Client", parent: 40, start: "2026-06-05", duration: 4, progress: 0 },
	{ id: 42, text: "Soft opening", owner: "Ops", parent: 40, start: "2026-06-10", duration: 3, progress: 0 },
	{ id: 44, text: "Marketing photography", owner: "Ops", parent: 40, start: "2026-06-08", duration: 3, progress: 0 },
	{ id: 50, text: "Hotel opens", owner: "GM", parent: 1, start: "2026-06-15", type: "milestone", progress: 0 }
];

let links = [
	{ id: 1, source: 11, target: 12, type: "0" },
	{ id: 2, source: 12, target: 21, type: "0" },
	{ id: 3, source: 21, target: 22, type: "0" },
	{ id: 4, source: 22, target: 31, type: "0" },
	{ id: 5, source: 31, target: 32, type: "0" },
	{ id: 6, source: 32, target: 41, type: "0" },
	{ id: 7, source: 41, target: 42, type: "0" },
	{ id: 8, source: 42, target: 50, type: "0" },
	{ id: 9, source: 12, target: 13, type: "0" },
	{ id: 10, source: 13, target: 14, type: "0" },
	{ id: 11, source: 21, target: 23, type: "0" },
	{ id: 12, source: 22, target: 24, type: "0" },
	{ id: 13, source: 31, target: 33, type: "0" },
	{ id: 14, source: 32, target: 34, type: "0" },
	{ id: 15, source: 41, target: 43, type: "0" },
	{ id: 16, source: 42, target: 44, type: "0" }
];


const monthDayToStr = gantt.date.date_to_str("%d %M");
const dateToStr = gantt.date.date_to_str("%Y-%m-%d");
const strToDate = gantt.date.str_to_date("%Y-%m-%d");

function addDays(date, days) {
	let next = strToDate(date);
	return dateToStr(gantt.date.add(next, days, "day"));
}

function makeData(mode) {
	let shifts = scenarios[mode].shifts;
	let data = contractTasks.map(function (task) {
		let copy = {};
		for (let key in task) copy[key] = task[key];
		if (task.start) {
			copy.start_date = addDays(task.start, shifts[task.id] || 0);
			copy.baseline_start = task.start;
		}
		delete copy.start;
		return copy;
	});
	let baselines = contractTasks
		.filter(function (task) { return task.start; })
		.map(function (task, index) {
			let start = gantt.date.parseDate(task.start, "%Y-%m-%d");
			let end = task.type === "milestone" ? new Date(start) : gantt.calculateEndDate({ start_date: start, duration: task.duration });
			return { id: index + 1, task_id: task.id, start_date: task.start, end_date: dateToStr(end) };
		});
	return { tasks: data, baselines, links };
}

function baselineFor(task) {
	let list = task.baselines || [];
	return list.length ? list[0] : null;
}

function slipDays(task) {
	let bl = baselineFor(task);
	if (!bl) return null;

	const minDate = new Date(Math.min(task.end_date.valueOf(), bl.end_date.valueOf()));
	const maxDate = new Date(Math.max(task.end_date.valueOf(), bl.end_date.valueOf()));
	const sign = task.end_date > bl.end_date ? 1 : -1;

	return sign * gantt.calculateDuration({ start_date: minDate, end_date: maxDate, task });
}

gantt.templates.task_class = function (start, end, task) {
	if (gantt.isSummaryTask(task)) return "";
	let slip = slipDays(task);
	if (slip <= 0) return "on_baseline";
	return currentScenario === "recovery" ? "recovery_forecast" : "late_forecast";
};

gantt.templates.tooltip_text = function (start, end, task) {
	let html = `<b>${task.text}</b><br>${scenarios[currentScenario].label}: ${monthDayToStr(start)} &ndash; ${monthDayToStr(end)}`;
	let bl = baselineFor(task);
	if (bl) {
		let slip = slipDays(task);
		html += `<br>Baseline: ${monthDayToStr(bl.start_date)} &ndash; ${monthDayToStr(bl.end_date)}`;
		html += slip > 0 ? `<br>Forecast slip +${slip}d` : "<br>On baseline";
	}
	return html;
};

gantt.config.columns = [
	{ name: "text", label: "Work package", tree: true, width: 220, resize: true },
	{ name: "owner", label: "Owner", width: 86, align: "center", resize: true },
	{ name: "end_date", label: "Forecast finish", width: 110, align: "center", resize: true,
		template: function (task) { return gantt.isSummaryTask(task) ? "" : monthDayToStr(task.end_date); } },
	{ name: "delta", label: "vs baseline", width: 92, align: "center", resize: true, template: function (task) {
		if (gantt.isSummaryTask(task)) return "";
		let slip = slipDays(task);
		let label = slip <= 0 ? "on plan" : `+${slip}d`;
		return `<span class='delta_chip ${slip > 0 ? "bad" : "good"}'>${label}</span>`;
	}}
];

function setScenario(mode) {
	currentScenario = mode;
	DHX.ui.setActiveByAttribute("#scenario_seg .dhx_btn", "data-mode", mode, "active");
	gantt.clearAll();
	gantt.parse(makeData(mode));
	gantt.batchUpdate(function () {
		gantt.eachTask(function (task) { gantt.adjustTaskHeightForBaselines(task); });
	});
	updateSummary();
	gantt.showDate(new Date(2026, 3, 27));
}

function updateSummary() {
	let finish = gantt.getTask(50);
	let finishSlip = slipDays(finish);
	let recovered = scenarios.current.shifts[50] - scenarios.recovery.shifts[50];
	let text = `${scenarios[currentScenario].label} \u00b7 opening ${monthDayToStr(finish.start_date)}`;
	text += finishSlip > 0 ? ` (+${finishSlip}d vs baseline)` : " (on baseline)";
	if (currentScenario === "recovery") text += ` \u00b7 ${recovered} days recovered`;
	DHX.ui.setText("footer_var", text);
}

gantt.init("gantt_here");
setScenario("current");
<!DOCTYPE html>
<html lang="en">
<head>
	<meta http-equiv="Content-type" content="text/html; charset=utf-8">
	<title>Baseline recovery plan: dhtmlxGantt</title>
	<script src="../../codebase/dhtmlxgantt.js?v=10.0.0"></script>
	<link rel="stylesheet" href="../../codebase/dhtmlxgantt.css?v=10.0.0">
	<link rel="stylesheet" href="../common/demo-controls/dhx_controls.css">
	<script src="../common/demo-controls/dhx_controls.js"></script>
	<link rel="preconnect" href="https://fonts.googleapis.com">
	<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
	<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
	<style>
		body { display: flex; flex-direction: column; }
		#gantt_here { width: 100%; flex: 1 1 auto; min-height: 0; }
		.gantt_task_line { border: none; border-radius: 6px; }
		.gantt_task_baseline {
			background: rgba(100,116,139,.22) !important;
			border: 1.5px dashed rgba(71,85,105,.75) !important;
			border-radius: 4px !important;
			opacity: 1 !important;
		}
		:root[data-gantt-theme="dark"] .gantt_task_baseline {
			background: #5a6475 !important;
			border: none !important;
		}
		.gantt_task_line.on_baseline { background: #2563eb; }
		.gantt_task_line.late_forecast { background: #d94b4b; }
		.gantt_task_line.recovery_forecast { background: #14a076; }
		.delta_chip { display: inline-block; padding: 1px 9px; border-radius: 10px; font: 600 11px var(--dhx-font); font-variant-numeric: tabular-nums; }
		.delta_chip.good { background: var(--dhx-ok-bg); color: var(--dhx-ok-ink); }
		.delta_chip.bad { background: var(--dhx-danger-bg); color: var(--dhx-danger-ink); }
		.footer_legend { display: inline-flex; gap: 16px; flex-wrap: wrap; }
		.footer_legend .lg { display: inline-flex; align-items: center; gap: 6px; }
		.footer_legend .sw { width: 16px; height: 10px; border-radius: 2px; display: inline-block; }
		.footer_legend .baseline_task { background: #2563eb; }
		.footer_legend .current { background: #d94b4b; }
		.footer_legend .recovery { background: #14a076; }
		.footer_legend .baseline { background: rgba(100,116,139,.22); border: 1.5px dashed rgba(71,85,105,.75); }
		:root[data-gantt-theme="dark"] .footer_legend .baseline { background: #5a6475; border: none; }
		.gantt_task_cell.week_end {
			background-color: var(--dhx-gantt-base-colors-background-alt);
		}

		.gantt_task_row.gantt_selected .gantt_task_cell.week_end {
			background-color: var(--dhx-gantt-base-colors-select);
		}
	</style>
</head>
<body>

<div class="dhx_header dhx_header--centered">
	<div class="dhx_seg" id="scenario_seg">
		<button class="dhx_btn" data-mode="contract" onclick="setScenario('contract')">Contract</button>
		<button class="dhx_btn active" data-mode="current" onclick="setScenario('current')">Current delay</button>
		<button class="dhx_btn" data-mode="recovery" onclick="setScenario('recovery')">Recovery plan</button>
	</div>
</div>

<div id="gantt_here"></div>

<div class="dhx_footer">
	<div class="footer_legend">
		<span class="lg"><i class="sw baseline_task"></i> On baseline</span>
		<span class="lg"><i class="sw current"></i> Late forecast</span>
		<span class="lg"><i class="sw recovery"></i> Recovery forecast</span>
		<span class="lg"><i class="sw baseline"></i> Saved baseline</span>
	</div>
	<div class="dhx_footer__right"><span id="footer_var"></span></div>
</div>


</body>
</html>