Skip to main content
Back to examples
Popular features/On-demand branch loading
Loading live demo…from dhtmlxcode.com
Popular features

On-demand branch loading

Keep subtasks on the server and fetch a branch only when its parent row is expanded, with a shimmering bar marking requests still in flight.

gantt.plugins({ tooltip: true });

gantt.config.date_format = "%Y-%m-%d";
gantt.config.work_time   = true;
gantt.config.static_background = true;

gantt.config.row_height   = 32;
gantt.config.bar_height   = 22;
gantt.config.scale_height = 54;
gantt.config.min_column_width = 26;

/* Anchor to the next Monday so the demo never looks stale. */
function mondayOnOrAfter(d) {
	d = gantt.date.day_start(new Date(d));
	while (d.getDay() !== 1) d = gantt.date.add(d, 1, "day");
	return d;
}
let ANCHOR = mondayOnOrAfter(new Date());

gantt.templates.scale_cell_class    = function (date)       { return gantt.isWorkTime(date) ? "" : "weekend"; };
gantt.templates.timeline_cell_class = function (item, date) { return gantt.isWorkTime({ date, task: item }) ? "" : "weekend"; };

/* ---------------------------------------------------------------------------
 *  ZOOM (Day / Week / Month / Quarter)
 *  config.scales above is just the "week" level's own scales, so the chart
 *  still renders correctly before gantt.ext.zoom.init runs.
 * ------------------------------------------------------------------------ */
let zoomNames = ["day", "week", "month", "quarter"];
gantt.ext.zoom.init({
	levels: [
		{ name: "day", scale_height: 54, scales: [
			{ unit: "week", step: 1, format: function (d) { return "Week of " + gantt.date.date_to_str("%d %M")(d); } },
			{ unit: "day",  step: 1, format: "%D" }
		] },
		{ name: "week", scale_height: 54, scales: [
			{ unit: "month", step: 1, format: "%M %Y" },
			{ unit: "week",  step: 1, format: "%d" }
		] },
		{ name: "month", scale_height: 54, scales: [
			{ unit: "year",  step: 1, format: "%Y" },
			{ unit: "month", step: 1, format: "%M" }
		] },
		{ name: "quarter", scale_height: 54, scales: [
			{ unit: "year",    step: 1, format: "%Y" },
			{ unit: "quarter", step: 1, format: function (d) { return "Q" + (Math.floor(d.getMonth() / 3) + 1); } }
		] }
	],
	element: function () { return gantt.$root.querySelector(".gantt_task"); }
});
DHX.zoom.init(gantt, zoomNames, "zoom_seg", null);
function setZoom(name) { DHX.zoom.set(name); }

const formatDate = gantt.date.date_to_str("%Y-%m-%d");
/* ---------------------------------------------------------------------------
 *  DETERMINISTIC PRNG (no Math.random -- reruns must look identical)
 * ------------------------------------------------------------------------ */
function makeRng(seed) {
	let state = seed >>> 0;
	return function () {
		state = (1664525 * state + 1013904223) >>> 0;
		return state / 4294967296;
	};
}
function rnd(rand, min, max) {
	return min + Math.floor(rand() * (max - min));
}

/* ---------------------------------------------------------------------------
 *  PORTFOLIO: 40 programs, staggered across ~1 year, sized 80-300 tasks each
 * ------------------------------------------------------------------------ */
let PROGRAM_NAMES = [
	"Core Banking Platform Modernization", "Customer 360 Data Platform",
	"Omnichannel Commerce Rollout", "Cloud Migration - Phase 2",
	"Enterprise Security Hardening", "Global Payroll Consolidation",
	"Supply Chain Visibility Program", "B2B Partner Portal Rebuild",
	"Regulatory Reporting Overhaul", "Mobile Banking App Relaunch",
	"Data Warehouse Modernization", "Contact Center Transformation",
	"Vendor Risk Management Program", "Digital Identity and SSO Rollout",
	"Warehouse Automation Initiative", "Claims Processing Redesign",
	"HR Systems Consolidation", "Marketing Cloud Migration",
	"Fraud Detection Platform", "API Gateway Modernization",
	"Retail POS Refresh", "Sustainability Reporting Program",
	"Procurement Platform Upgrade", "Employee Experience Portal",
	"Network Infrastructure Refresh", "Product Catalog Unification",
	"Customer Support Knowledge Base", "Loyalty Program Relaunch",
	"ERP Migration - EMEA", "ERP Migration - APAC",
	"Treasury Management Upgrade", "Data Privacy Compliance Program",
	"DevOps Toolchain Standardization", "Legacy System Decommissioning",
	"Subscription Billing Platform", "Field Service Mobility Program",
	"Analytics Self-Service Rollout", "Vendor Onboarding Automation",
	"Disaster Recovery Modernization", "Enterprise Search Upgrade"
];

let PROGRAM_BASE = 100000;
let PROGRAMS = [];
let PROGRAMS_BY_ID = {};
let TOTAL_TASKS = 0;

for (let pi = 0; pi < PROGRAM_NAMES.length; pi++) {
	let prand = makeRng(2000 + pi * 97);

	let startOffset = Math.min(220, Math.max(0, Math.round(pi * 5.5) + rnd(prand, -8, 9)));
	let childCount  = rnd(prand, 80, 301);
	let spanDays    = Math.min(140, Math.max(45, Math.round(childCount * 0.4 + 25)));

	let start = gantt.date.add(ANCHOR, startOffset, "day");
	let end   = gantt.date.add(start, spanDays, "day");

	let program = {
		id: PROGRAM_BASE + pi,
		name: PROGRAM_NAMES[pi],
		start: start,
		end: end,
		spanDays: spanDays,
		childCount: childCount
	};
	PROGRAMS.push(program);
	PROGRAMS_BY_ID[program.id] = program;
	TOTAL_TASKS += childCount;
}

/* ---------------------------------------------------------------------------
 *  GRID
 * ------------------------------------------------------------------------ */
function scopeTemplate(task) {
	if (!task.is_program) return "";
	let loaded = loadedPrograms.has(task.id);
	return '<span class="scope_badge' + (loaded ? ' scope_badge--loaded' : '') + '">' +
		task.total_children + (loaded ? " tasks loaded" : " tasks") + '</span>';
}

gantt.config.columns = [
	{ name: "text",       label: "Program / Task", tree: true, width: 300, resize: true },
	{ name: "scope",      label: "Scope",  align: "center", width: 120, resize: true, template: scopeTemplate },
	{ name: "start_date", label: "Start",  align: "center", width: 100, resize: true, template: function (task) {
		return gantt.templates.date_grid(task.start_date, task);
	} },
	{ name: "duration",   label: "Days",   align: "center", width: 56, resize: true },
	{ name: "add", width: 40, resize: true }
];

/* Skeleton styling lives on task_class / grid_row_class -- two separate
 * functions with two different signatures, never shared (task_class gets
 * (start,end,task); grid_row_class gets (id,task)). */
gantt.templates.task_class = function (start, end, task) {
	return task.is_placeholder ? "skeleton_bar" : "";
};
gantt.templates.grid_row_class = function (id, task) {
	return task.is_placeholder ? "skeleton_row" : "";
};

let tooltipDate = gantt.date.date_to_str("%d %M %Y");
gantt.templates.tooltip_text = function (start, end, task) {
	if (task.is_placeholder) return "Loading from server...";
	if (task.is_program) {
		let loaded = loadedPrograms.has(task.id);
		return "<b>" + DHX.ui.escape(task.text) + "</b><br/>" +
			task.total_children + " tasks " + (loaded ? "loaded" : "on the server - expand to fetch");
	}
	return "<b>" + DHX.ui.escape(task.text) + "</b><br/>" + tooltipDate(start) + " - " + tooltipDate(end);
};

/* ---------------------------------------------------------------------------
 *  MOCK SERVER: deterministic children for one program, on demand
 * ------------------------------------------------------------------------ */
let PHASE_NAMES = ["Discovery and Planning", "Design and Architecture", "Build and Development",
	"Testing and Hardening", "Rollout and Adoption"];
let TASK_VERBS = ["Implement", "Design", "Review", "Migrate", "Configure", "Test", "Document",
	"Automate", "Refactor", "Validate", "Deploy", "Integrate", "Audit", "Optimize", "Provision"];
let TASK_OBJECTS = ["API gateway", "data model", "onboarding flow", "billing module",
	"access controls", "reporting dashboard", "notification service", "search index",
	"checkout flow", "inventory sync", "vendor integration", "mobile client", "admin console",
	"audit log", "backup strategy", "load balancer", "CI pipeline", "staging environment",
	"translation strings", "SLA monitors"];

function buildTaskText(rand) {
	return TASK_VERBS[rnd(rand, 0, TASK_VERBS.length)] + " " + TASK_OBJECTS[rnd(rand, 0, TASK_OBJECTS.length)];
}

/* Deterministic per program: same program id always yields the same 3-5
 * phases and the same leaf tasks, dates, and progress values. */
function generateChildren(program) {
	let rand = makeRng(program.id * 7919 + 13);

	let phaseCount = 3 + rnd(rand, 0, 3); // 3..5
	let totalSpan  = program.spanDays;

	/* split childCount across phases, at least 1 task per phase */
	let counts = [], remainingCount = program.childCount;
	for (let k = 0; k < phaseCount; k++) {
		let isLast = (k === phaseCount - 1);
		let share = isLast ? remainingCount : Math.max(1, Math.round(program.childCount / phaseCount + rnd(rand, -6, 7)));
		share = Math.min(share, remainingCount - (phaseCount - k - 1));
		share = Math.max(1, share);
		counts.push(share);
		remainingCount -= share;
	}

	/* split the program's date window across phases, proportional to load */
	let spans = [], remainingSpan = totalSpan;
	for (let s = 0; s < phaseCount; s++) {
		let isLastSpan = (s === phaseCount - 1);
		let span = isLastSpan ? remainingSpan : Math.max(5, Math.round(totalSpan * counts[s] / program.childCount));
		span = Math.min(span, remainingSpan - (phaseCount - s - 1) * 3);
		span = Math.max(3, span);
		spans.push(span);
		remainingSpan -= span;
	}

	let data = [], links = [];
	let cursorOffset = 0;
	let base = childBase(program.id);
	/* Chains every leaf to the one generated before it (finish-to-start),
	 * phase boundaries included, so the whole program lands as one
	 * dependency spine. Link ids live at base+7000.., well clear of the
	 * leaf range (base+2000..6999) and the placeholder (base+9999). */
	let prevLeafId = null, linkSeq = 0;
	for (let p = 0; p < phaseCount; p++) {
		let phaseId    = base + p;                 // 0-4
		let phaseStart = gantt.date.add(program.start, cursorOffset, "day");
		let leafCursor = new Date(phaseStart);

		for (let t = 0; t < counts[p]; t++) {
			let dur = rnd(rand, 2, 9);
			let leafId = base + 2000 + p * 1000 + t; // 2000-6999, 1000-wide per phase
			data.push({
				id: leafId,
				text: buildTaskText(rand),
				start_date: formatDate(leafCursor),
				duration: dur,
				parent: phaseId,
				progress: Math.round(rand() * 10) / 10
			});
			if (prevLeafId !== null) {
				links.push({ id: base + 7000 + (linkSeq++), source: prevLeafId, target: leafId, type: "0" });
			}
			prevLeafId = leafId;

			let step = rand() > 0.3 ? rnd(rand, Math.ceil(dur * 0.6), dur + 2) : rnd(rand, 1, 3);
			leafCursor = gantt.date.add(leafCursor, step, "day");
		}

		data.push({ id: phaseId, text: PHASE_NAMES[p % PHASE_NAMES.length], type: "project",
			parent: program.id, open: false });

		cursorOffset += spans[p];
	}

	return { tasks: data, links };
}

/* ---------------------------------------------------------------------------
 *  FETCH SIMULATION
 * ------------------------------------------------------------------------ */
let loadedPrograms  = new Set();
let loadedTaskCount = 0;
let FETCH_DELAY_MS  = 400;

/* Per-program id namespace: childBase leaves plenty of headroom so phase ids
 * (0-4), leaf ids (up to ~300 per phase) and the placeholder id can never
 * collide, no matter how the random split skews toward one phase. */
function childBase(programId) { return programId * 10000; }
function placeholderId(programId) { return childBase(programId) + 9999; }

function updateFooter() {
	DHX.ui.setText("loaded_count", loadedTaskCount.toLocaleString());
	DHX.ui.setText("total_count", TOTAL_TASKS.toLocaleString());
}
function updateExpandedMetric() {
	DHX.ui.setText("expanded_count", String(loadedPrograms.size));
}
function setStatus(text) {
	gantt.message({ id: "fetch_status_message", text: text, expire: -1 });
}

gantt.attachEvent("onTaskOpened", function (id) {
	let task = gantt.getTask(id);
	if (!task.is_program || loadedPrograms.has(id)) return; // only top-level, only once

	let program = PROGRAMS_BY_ID[id];
	loadedPrograms.add(id); // mark immediately so a fast collapse/expand can't double-fetch

	setStatus("Fetching " + program.childCount + " tasks for \"" + program.name + "\" ...");
	let t0 = performance.now();

	setTimeout(function () {
		let childData = generateChildren(program);

		gantt.batchUpdate(function () {
			gantt.parse(childData);
			gantt.deleteTask(placeholderId(id));
		});

		let elapsed = ((performance.now() - t0) / 1000).toFixed(1);
		loadedTaskCount += program.childCount;

		updateFooter();
		updateExpandedMetric();
		setStatus("Fetched " + program.childCount + " tasks in " + elapsed + "s");
		gantt.render(); // refresh the grid so the scope badge flips to "loaded"
	}, FETCH_DELAY_MS);
});

function collapseAllPrograms() {
	gantt.batchUpdate(function(){
		PROGRAMS.forEach(function (p) {
			gantt.close(p.id);
		});
	})
	gantt.message.hide("fetch_status_message");
}

/* ---------------------------------------------------------------------------
 *  INIT: only the 40 program rows + one skeleton placeholder each
 * ------------------------------------------------------------------------ */
let initialData = { tasks: [], links: [] };
PROGRAMS.forEach(function (p) {
	initialData.tasks.push({
		id: p.id, text: p.name, type: "project", parent: 0, open: false,
		is_program: true, total_children: p.childCount
	});
	initialData.tasks.push({
		id: placeholderId(p.id), text: "Loading from server...", parent: p.id,
		start_date: formatDate(p.start), duration: p.spanDays,
		is_placeholder: true
	});
});

gantt.config.start_date = gantt.date.add(ANCHOR, -7, "day");
gantt.config.end_date   = gantt.date.add(ANCHOR, 250 + 140 + 14, "day");

gantt.init("gantt_here");
gantt.parse(initialData);

gantt.ext.zoom.setLevel("week");
DHX.zoom.syncUI();

updateFooter();
updateExpandedMetric();
<!DOCTYPE html>
<html lang="en">
<head>
	<meta http-equiv="Content-type" content="text/html; charset=utf-8">
	<title>On-demand branch loading: 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 { flex: 1 1 auto; min-height: 0; width: 100%; }

		.gantt_task_line { border: none; border-radius: 7px; box-shadow: 0 1px 2px rgba(16,24,40,.18); }
		.gantt_task_line .gantt_task_progress { border-radius: 7px 0 0 7px; }
		.gantt_task_line.gantt_project { border-radius: 5px; }

		.gantt_task_cell.weekend, .gantt_scale_cell.weekend { background-color: rgba(15,23,42,.04); }

		.scope_badge {
			display: inline-block;
			padding: 2px 9px;
			border-radius: 999px;
			font: 600 11px var(--dhx-font);
			background: var(--dhx-info-bg);
			color: var(--dhx-info-ink);
			white-space: nowrap;
		}
		.scope_badge--loaded {
			background: var(--dhx-ok-bg);
			color: var(--dhx-ok-ink);
		}

		.gantt_task_line.skeleton_bar {
			border: none;
			box-shadow: none;
			background: linear-gradient(90deg, #e3e6eb 25%, #eef0f3 37%, #e3e6eb 63%);
			background-size: 400% 100%;
			animation: dhx_shimmer 1.3s ease-in-out infinite;
		}
		.gantt_task_line.skeleton_bar .gantt_task_progress,
		.gantt_task_line.skeleton_bar .gantt_task_content { display: none; }
		@keyframes dhx_shimmer {
			0%   { background-position: 100% 0; }
			100% { background-position: 0 0; }
		}
		.gantt_row.skeleton_row .gantt_tree_content {
			color: var(--dhx-muted);
			font-style: italic;
			animation: dhx_pulse 1.3s ease-in-out infinite;
		}
		@keyframes dhx_pulse {
			0%, 100% { opacity: .5; }
			50%      { opacity: 1; }
		}

		.hint { font: 500 12px var(--dhx-font); color: var(--dhx-muted); }
		.hint b { color: var(--dhx-ink-2); font-weight: 600; }
	</style>
</head>
<body>

<div class="dhx_header" style="justify-content:center; gap:14px">
	<span class="dhx_metric_badge" id="programs_metric">Programs loaded: <strong id="expanded_count">0</strong> / 40</span>
	<div class="dhx_sep"></div>
	<div class="dhx_seg" id="zoom_seg">
		<button class="dhx_btn" data-zoom="day"     onclick="setZoom('day')">Day</button>
		<button class="dhx_btn active" data-zoom="week"    onclick="setZoom('week')">Week</button>
		<button class="dhx_btn" data-zoom="month"   onclick="setZoom('month')">Month</button>
		<button class="dhx_btn" data-zoom="quarter" onclick="setZoom('quarter')">Quarter</button>
	</div>
	<div class="dhx_sep"></div>
	<button class="dhx_btn dhx_btn--surface" onclick="collapseAllPrograms()">Collapse all</button>
</div>

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

<div class="dhx_footer">
	<span class="hint">Loaded <b id="loaded_count">0</b> of ~<b id="total_count">0</b> tasks - the rest stays on the server until you need it. Open a <b>program</b> row to trigger a fetch; a shimmering bar marks work still in flight.</span>
</div>


</body>
</html>