Skip to main content
Back to examples
Popular features/Progress line
Loading live demo…from dhtmlxcode.com
Popular features

Progress line

Bend a progress line at the status date so it points left to tasks running late and right to those ahead of plan, with hover dots showing the exact slip.

gantt.plugins({ marker: true });

gantt.config.date_format = "%Y-%m-%d";
gantt.config.work_time = true;
gantt.config.row_height   = 34;
gantt.config.bar_height   = 22;
gantt.config.scale_height = 52;
gantt.config.min_column_width = 26;
gantt.config.drag_links = false;

function mondayOnOrAfter(date) {
	date = gantt.date.day_start(new Date(date));
	while (date.getDay() !== 1) date = gantt.date.add(date, 1, "day");
	return date;
}

/* the plan started four weeks before the current week, and the initial
 * status date sits on its 24th day: mid-project, where a progress line is
 * actually interesting and both late and early tasks exist by design */
let A0  = gantt.date.add(mondayOnOrAfter(new Date()), -28, "day");

const dateToStr = gantt.date.date_to_str("%Y-%m-%d");
let fmt = date => dateToStr(date);
let day = function (n) { return gantt.date.add(A0, n, "day"); };

let STATUS = day(24);

gantt.config.start_date = day(-2);
gantt.config.end_date   = day(50);

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

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

gantt.config.columns = [
	{ name: "text", label: "Task", tree: true, width: 230, resize: true },
	{ name: "progress", label: "Done", align: "center", width: 60, resize: true,
		template: function (task) { return Math.round((task.progress || 0) * 100) + "%"; } },
	{ name: "duration", label: "Days", align: "center", width: 52, resize: true }
];

/* --- data: a data-center migration caught mid-flight ----------------------- */
function T(id, parent, text, off, dur, progress, type) {
	let task = { id: id, parent: parent, text: text, progress: progress || 0 };
	if (type) task.type = type;
	task.start_date = fmt(day(off));
	if (type !== "milestone") task.duration = dur;
	return task;
}

let DATA = {
	tasks: [
		{ id: 1, text: "Assessment and design", type: "project", open: true },
		T(11, 1, "Inventory audit",          0,  5, 1),
		T(12, 1, "Vendor contracts",         2,  6, 1),
		T(13, 1, "Capacity planning",        5,  4, 1),
		T(14, 1, "Network design",           9,  5, 1),
		T(15, 1, "Migration runbook",       14,  4, 0.85),

		{ id: 2, text: "Migration waves", type: "project", open: true },
		T(21, 2, "Wave 1: low-risk services", 14, 6, 1),
		T(22, 2, "Wave 2: internal apps",     18, 7, 0.45),
		T(23, 2, "Wave 3: customer APIs",     22, 7, 0.4),
		T(24, 2, "Database replication",      16, 12, 0.7),
		T(25, 2, "Storage sync",              20, 8, 0.2),
		T(26, 2, "User communications",       21, 4, 0.9),
		T(27, 2, "Legacy decommission prep",  25, 5, 0),

		{ id: 3, text: "Cutover and validation", type: "project", open: true },
		T(31, 3, "Failover drill",        31, 3, 0),
		T(32, 3, "Performance validation",34, 4, 0),
		T(33, 3, "Security audit",        34, 3, 0),
		T(34, 3, "DNS cutover",           39, 1, 0),
		T(35, 3, "Hypercare",             40, 5, 0),
		T(36, 3, "Migration sign-off",    46, 0, 0, "milestone")
	],
	links: [
		{ id: 1, source: 11, target: 13, type: "0" },
		{ id: 2, source: 13, target: 14, type: "0" },
		{ id: 3, source: 14, target: 15, type: "0" },
		{ id: 4, source: 15, target: 21, type: "0" },
		{ id: 5, source: 21, target: 22, type: "0" },
		{ id: 6, source: 22, target: 23, type: "0" },
		{ id: 7, source: 24, target: 25, type: "0" },
		{ id: 8, source: 23, target: 31, type: "0" },
		{ id: 9, source: 31, target: 32, type: "0" },
		{ id: 10, source: 31, target: 33, type: "0" },
		{ id: 11, source: 32, target: 34, type: "0" },
		{ id: 12, source: 33, target: 34, type: "0" },
		{ id: 13, source: 34, target: 35, type: "0" },
		{ id: 14, source: 35, target: 36, type: "0" }
	]
};

/* --- the line itself -------------------------------------------------------- */
let SVG_NS = "http://www.w3.org/2000/svg";
let showLine = true;

function isBendable(task) {
	if (task.type === "project" || task.type === "milestone") return false;
	return task.start_date < STATUS && (task.progress || 0) < 1;
}

function getTaskStatusInfo(task) {
	if (!isBendable(task)) {
		return null;
	}

	let span = task.end_date - task.start_date;
	let progressDate = new Date(task.start_date.getTime() + span * (task.progress || 0));
	let slipDays = Math.round((progressDate - STATUS) / 86400000);

	return {
		progressDate: progressDate,
		slipDays: slipDays,
		label: task.text + ": " + Math.abs(slipDays) +
			(Math.abs(slipDays) === 1 ? " day " : " days ") +
			(slipDays < 0 ? "behind" : "ahead")
	};
}

function createStatusLineSegment(task) {
	if (!showLine) {
		return false;
	}

	let sizes = gantt.getTaskPosition(task, task.start_date, task.end_date);
	let rowHeight = sizes.rowHeight || gantt.config.row_height;
	let middleY = rowHeight / 2;
	let statusX = gantt.posFromDate(STATUS);
	let bendX = statusX;
	let statusInfo = getTaskStatusInfo(task);

	if (statusInfo && statusInfo.slipDays !== 0) {
		bendX = gantt.posFromDate(statusInfo.progressDate);
	}

	let svg = document.createElementNS(SVG_NS, "svg");
	svg.setAttribute("class", "status_line_segment");
	svg.setAttribute("width", "100%");
	svg.setAttribute("height", rowHeight);
	svg.style.top = sizes.top + "px";

	let path = document.createElementNS(SVG_NS, "path");
	path.setAttribute(
		"d",
		"M" + statusX + ",0 " +
		"L" + bendX + "," + middleY + " " +
		"L" + statusX + "," + rowHeight
	);
	svg.appendChild(path);

	if (statusInfo && statusInfo.slipDays !== 0) {
		let circle = document.createElementNS(SVG_NS, "circle");
		circle.setAttribute("cx", bendX);
		circle.setAttribute("cy", middleY);
		circle.setAttribute("r", "4");
		circle.setAttribute("fill", statusInfo.slipDays < 0 ? "#e5484d" : "#22a06b");
		circle.setAttribute("stroke", "#fff");
		circle.setAttribute("stroke-width", "1.5");

		let title = document.createElementNS(SVG_NS, "title");
		title.textContent = statusInfo.label;
		circle.appendChild(title);
		svg.appendChild(circle);
	}

	return svg;
}

function updateStatusSummary() {
	if (!showLine) {
		updateChips(0, 0, 0);
		return;
	}

	let behind = 0;
	let ahead = 0;
	let onTrack = 0;
	let rows = gantt.getVisibleTaskCount();

	for (let i = 0; i < rows; i++) {
		let task = gantt.getTaskByIndex(i);
		let statusInfo = task && getTaskStatusInfo(task);

		if (!statusInfo) {
			continue;
		}

		if (statusInfo.slipDays <= -1) {
			behind++;
		} else if (statusInfo.slipDays >= 1) {
			ahead++;
		} else {
			onTrack++;
		}
	}

	updateChips(behind, ahead, onTrack);
}

function updateChips(behind, ahead, onTrack) {
	DHX.ui.setText("chip_behind", behind + " behind");
	DHX.ui.setText("chip_ahead",  ahead + " ahead");
	DHX.ui.setText("chip_ok",     onTrack + " on track");
}

/* --- controls ---------------------------------------------------------------- */
let statusMarkerId = null;
let labelFmt = gantt.date.date_to_str("%D, %j %M");

function syncStatusUI() {
	DHX.ui.setText("status_label", labelFmt(STATUS));
	let marker = gantt.getMarker(statusMarkerId);
	if (marker) {
		marker.start_date = STATUS;
		marker.text = "Report date";
		gantt.updateMarker(statusMarkerId);
	}
}

function shiftStatus(days) {
	let next = gantt.date.add(STATUS, days, "day");
	if (next <= gantt.config.start_date || next >= gantt.config.end_date) return;
	STATUS = next;
	syncStatusUI();
	gantt.render();   // repaint updates the task layer and the counters
}

function toggleLine() {
	showLine = !showLine;
	DHX.toggle.setWrap("line_toggle", showLine);
	gantt.render();
}

/* --- init --------------------------------------------------------------------- */
gantt.attachEvent("onGanttRender", updateStatusSummary);

statusMarkerId = gantt.addMarker({ start_date: STATUS, text: "Report date", css: "status_marker" });

gantt.init("gantt_here");
gantt.addTaskLayer({
	renderer: {
		render: createStatusLineSegment
	},
	topmost: true
});
gantt.parse(DATA);
syncStatusUI();
gantt.showDate(gantt.config.start_date);
<!DOCTYPE html>
<html lang="en">
<head>
	<meta http-equiv="Content-type" content="text/html; charset=utf-8">
	<title>Progress line: who is behind on the status date | 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); }

		.hint { font: 500 12px var(--dhx-font); color: var(--dhx-muted); }
		.hint b { color: var(--dhx-ink-2); font-weight: 600; }
		.toolbar_label { font: 600 13px var(--dhx-font); color: var(--dhx-ink-2); }

		.dhx_stepper .dhx_btn--icon:first-child { border-radius: 8px 0 0 8px; }
		.dhx_stepper .dhx_btn--icon:last-child  { border-radius: 0 8px 8px 0; margin-left: -1px; }

		#status_label { font: 600 13px var(--dhx-font); color: var(--dhx-ink); min-width: 92px; text-align: center; }

		.gantt_marker.status_marker { background: transparent; border-left: 1px dashed #e5484d; opacity: 1; width: 0; }
		.gantt_marker.status_marker .gantt_marker_content { color: #c62a30; background: transparent; font-weight: 600; }

		.status_line_segment {
			position: absolute;
			left: 0;
			z-index: 10;
			overflow: visible;
			pointer-events: none;
		}
		.status_line_segment path {
			fill: none;
			stroke: #e5484d;
			stroke-width: 2;
			stroke-linejoin: round;
			stroke-linecap: butt;
		}
		.status_line_segment circle {
			pointer-events: auto;
		}

		.chip { display: inline-flex; align-items: center; gap: 6px; font: 600 12px var(--dhx-font);
			padding: 2px 10px; border-radius: 10px; }
		.chip::before { content: ""; width: 8px; height: 8px; border-radius: 50%; }
		.chip--behind { color: var(--dhx-danger-ink); background: var(--dhx-danger-bg); }
		.chip--behind::before { background: #e5484d; }
		.chip--ahead { color: var(--dhx-ok-ink); background: var(--dhx-ok-bg); }
		.chip--ahead::before { background: #22a06b; }
		.chip--ok { color: var(--dhx-neutral-ink); background: var(--dhx-neutral-bg); }
		.chip--ok::before { background: #98a2ae; }
	</style>
</head>
<body>

<div class="dhx_header" style="justify-content:center; gap:8px">
	<span class="toolbar_label">Report date</span>

	<div class="dhx_stepper">
		<button class="dhx_btn dhx_btn--icon" title="Previous week" onclick="shiftStatus(-7)">
			<svg class="dhx_ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>
		</button>
		<button class="dhx_btn dhx_btn--icon" title="Next week" onclick="shiftStatus(7)">
			<svg class="dhx_ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg>
		</button>
	</div>
	<span id="status_label"></span>

	<label class="dhx_toggle active" id="line_toggle" onclick="toggleLine()" style="cursor:pointer">
		<span class="dhx_toggle__track on"></span>Progress line
	</label>
</div>

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

<div class="dhx_footer" style="gap:10px">
	<span class="hint" style="margin-right:6px">The red line bends <b>left</b> to every task running late and
	<b>right</b> to the ones ahead of plan. Hover a dot for the exact slip; move the status date to replay the project week by week.</span>
	<span class="chip chip--behind" id="chip_behind">0 behind</span>
	<span class="chip chip--ahead"  id="chip_ahead">0 ahead</span>
	<span class="chip chip--ok"     id="chip_ok">0 on track</span>
</div>


</body>
</html>