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

Task grouping

Re-bucket a flat task list under virtual group rows by owner, priority, or delivery phase, without changing the underlying data.

gantt.plugins({ grouping: true });

gantt.config.date_format         = "%Y-%m-%d";
gantt.config.open_tree_initially = true;
gantt.config.order_branch        = "marker";
gantt.config.row_height          = 36;
gantt.config.bar_height          = 20;
gantt.config.scale_height        = 50;
gantt.config.scales = [
	{ unit: "month", step: 1, format: "%F %Y" },
	{ unit: "week",  step: 1, format: function (d) { return gantt.date.date_to_str("%d %M")(d); } }
];

/* Lookup lists shared by the grid templates and the group buckets. */
let OWNERS = [
	{ key: "team1", label: "Engineering & Platform" },
	{ key: "team2", label: "Product, Design & Launch" },

	{ key: "ana",  label: "Ana K.", owner: "team2" },
	{ key: "ben",  label: "Ben R.", owner: "team2" },
	{ key: "cleo", label: "Cleo M.", owner: "team2" },
	{ key: "dev",  label: "Dev S.", owner: "team1" },
	{ key: "finn", label: "Finn O.", owner: "team1" }
];
let PRIORITIES = [
	{ key: "high",   label: "High",   cls: "high" },
	{ key: "normal", label: "Normal", cls: "normal" },
	{ key: "low",    label: "Low",    cls: "low" }
];
let PHASES = [
	{ key: "discovery", label: "Discovery" },
	{ key: "build",     label: "Build" },
	{ key: "launch",    label: "Launch" }
];

function labelOf(list, key) {
	for (let i = 0; i < list.length; i++) {
		if (list[i].key === key) {
			return list[i].label;
		}
	}
	return "";
}

gantt.config.lightbox.sections = [
	{ name: "description", height: 38, map_to: "text", type: "textarea", focus: true },
	{ name: "owner", height: 22, map_to: "owner", type: "select", options: OWNERS },
	{ name: "priority", height: 22, map_to: "priority", type: "select", options: PRIORITIES },
	{ name: "time", type: "duration", map_to: "auto" }
];
gantt.locale.labels.section_owner = "Owner";
gantt.locale.labels.section_priority = "Priority";

/* ---------------------------------------------------------------------------
 *  GRID: text column doubles as the group label/count renderer
 * ------------------------------------------------------------------------ */
gantt.config.columns = [
	{ name: "text", label: "Task", tree: true, width: 270, resize: true, editor: {type: "text", map_to: "text"}, template: function (task) {
		if (task.$virtual) {
			const rows = gantt.getChildren(task.id).length;
			return "<span class='group_label'>" + task.text + "</span><span class='group_count'>" + rows + "</span>";
		}
		return task.text;
	}},
	{ name: "owner", label: "Owner", align: "center", width: 80, resize: true, editor: {type: "select", map_to: "owner", options: OWNERS }, template: function (task) {
		if (task.$virtual || !task.owner) return "";
		return "<span class='owner_pill' title='" + labelOf(OWNERS, task.owner) + "'>" +
			labelOf(OWNERS, task.owner).split(" ")[0] + "</span>";
	}},
	{ name: "priority", label: "Priority", align: "center", width: 86, editor: {type: "select", map_to: "priority", options: PRIORITIES }, resize: true, template: function (task) {
		if (task.$virtual || !task.priority) return "";
		const priority = PRIORITIES.filter(function (x) { return x.key === task.priority; })[0];
		return priority ? "<span class='pr " + priority.cls + "'><i></i>" + priority.label + "</span>" : "";
	}},
	{ name: "duration", label: "Days", align: "center", width: 54, resize: true, editor: {type: "number", map_to: "duration", min:0 }, template: function (task) {
		return task.$virtual ? "" : task.duration;
	}},
	{ name: "add", width: 40, resize: true }
];
gantt.config.grid_width = 530;

// Repaint the changes in the group mode after changing the task property
gantt.ext.inlineEditors.attachEvent("onSave", function (state) {
	if (state.columnName === gantt.getState().group_mode){
		gantt.render()
	};
})

/* Virtual group rows get a tinted band in both grid and timeline. */
gantt.templates.grid_row_class = 
gantt.templates.task_row_class = function (start, end, task) { return task.$virtual ? "group_row" : ""; };
gantt.templates.task_class     = function (start, end, task) { return task.$virtual ? "group_bar" : ""; };

/* ---------------------------------------------------------------------------
 *  DATA: a single flat-ish tree carrying owner / priority / phase fields
 * ------------------------------------------------------------------------ */
let demoData = {
	tasks: [
		{ id: 1,  text: "Mobile app v2.0", type: "project", open: true },

		{ id: 11, text: "User research",        parent: 1, start_date: "2026-04-06", duration: 6,  progress: 1,   owner: "ana",  priority: "high",   phase: "discovery" },
		{ id: 12, text: "Competitive analysis", parent: 1, start_date: "2026-04-06", duration: 4,  progress: 1,   owner: "cleo", priority: "low",    phase: "discovery" },
		{ id: 13, text: "Wireframes",           parent: 1, start_date: "2026-04-14", duration: 5,  progress: 0.8, owner: "ben",  priority: "normal", phase: "discovery" },
		{ id: 14, text: "Accessibility audit",  parent: 1, start_date: "2026-04-15", duration: 4,  progress: 0.7, owner: "ana",  priority: "normal", phase: "discovery" },
		{ id: 15, text: "Analytics plan",       parent: 1, start_date: "2026-04-17", duration: 3,  progress: 0.5, owner: "cleo", priority: "normal", phase: "discovery" },

		{ id: 21, text: "Design system",        parent: 1, start_date: "2026-04-21", duration: 8,  progress: 0.6, owner: "ben",  priority: "high",   phase: "build" },
		{ id: 22, text: "API endpoints",        parent: 1, start_date: "2026-04-21", duration: 12, progress: 0.4, owner: "dev",  priority: "high",   phase: "build" },
		{ id: 23, text: "Auth & onboarding",    parent: 1, start_date: "2026-05-05", duration: 7,  progress: 0.3, owner: "dev",  priority: "normal", phase: "build" },
		{ id: 24, text: "Offline sync",         parent: 1, start_date: "2026-05-12", duration: 9,  progress: 0.1, owner: "ana",  priority: "normal", phase: "build" },
		{ id: 25, text: "Push notifications",   parent: 1, start_date: "2026-05-18", duration: 5,  progress: 0,   owner: "cleo", priority: "low",    phase: "build" },
		{ id: 26, text: "Payments module",      parent: 1, start_date: "2026-05-19", duration: 8,  progress: 0,   owner: "dev",  priority: "high",   phase: "build" },
		{ id: 27, text: "Tablet layout",        parent: 1, start_date: "2026-05-21", duration: 6,  progress: 0,   owner: "ben",  priority: "normal", phase: "build" },
		{ id: 28, text: "Crash reporting",      parent: 1, start_date: "2026-05-25", duration: 4,  progress: 0,   owner: "dev",  priority: "high",   phase: "build" },

		{ id: 31, text: "Beta testing",         parent: 1, start_date: "2026-05-25", duration: 8,  progress: 0,   owner: "ana",  priority: "high",   phase: "launch" },
		{ id: 32, text: "App store assets",     parent: 1, start_date: "2026-06-01", duration: 4,  progress: 0,   owner: "cleo", priority: "normal", phase: "launch" },
		{ id: 33, text: "Marketing site",       parent: 1, start_date: "2026-06-01", duration: 6,  progress: 0,   owner: "ben",  priority: "low",    phase: "launch" },
		{ id: 34, text: "Support playbook",     parent: 1, start_date: "2026-06-04", duration: 5,  progress: 0,   owner: "ana",  priority: "normal", phase: "launch" },
		{ id: 35, text: "Submit to stores",     parent: 1, start_date: "2026-06-10", duration: 3,  progress: 0,   owner: "dev",  priority: "high",   phase: "launch" },
		{ id: 36, text: "Launch review",        parent: 1, start_date: "2026-06-15", duration: 2,  progress: 0,   owner: "cleo", priority: "normal", phase: "launch" },

		{ id: 41, text: "Localization strings", parent: 1, start_date: "2026-04-20", duration: 6,  progress: 0.5, owner: "finn", priority: "normal", phase: "build" },
		{ id: 42, text: "Third-party SDK audit",parent: 1, start_date: "2026-04-27", duration: 4,  progress: 0.4, owner: "finn", priority: "low",    phase: "build" },
		{ id: 43, text: "Performance profiling",parent: 1, start_date: "2026-05-11", duration: 5,  progress: 0,   owner: "finn", priority: "high",   phase: "build" },
		{ id: 44, text: "Release notes",        parent: 1, start_date: "2026-06-08", duration: 3,  progress: 0,   owner: "finn", priority: "normal", phase: "launch" }
	],
	links: [
		{ id: 1,  source: 11, target: 13, type: "0" },
		{ id: 2,  source: 12, target: 13, type: "0" },
		{ id: 3,  source: 13, target: 21, type: "0" },
		{ id: 4,  source: 14, target: 21, type: "0" },
		{ id: 5,  source: 22, target: 23, type: "0" },
		{ id: 6,  source: 22, target: 24, type: "0" },
		{ id: 7,  source: 22, target: 26, type: "0" },
		{ id: 8,  source: 21, target: 27, type: "0" },
		{ id: 9,  source: 21, target: 32, type: "0" },
		{ id: 10, source: 23, target: 31, type: "0" },
		{ id: 11, source: 24, target: 31, type: "0" },
		{ id: 12, source: 31, target: 34, type: "0" },
		{ id: 13, source: 31, target: 35, type: "0" },
		{ id: 14, source: 32, target: 35, type: "0" },
		{ id: 15, source: 35, target: 36, type: "0" },
		{ id: 16, source: 22, target: 42, type: "0" },
		{ id: 17, source: 41, target: 43, type: "0" },
		{ id: 18, source: 35, target: 44, type: "0" }
	]
};

/* ---------------------------------------------------------------------------
 *  GROUPING
 * ------------------------------------------------------------------------ */
function setGroup(mode) {
	DHX.ui.setActiveByAttribute("#group_seg .dhx_btn", "data-group", mode, "active");

	if (mode === "tree") {
		gantt.groupBy(false);
	} else if (mode === "owner") {
		gantt.groupBy({ groups: OWNERS, relation_property: "owner", group_id: "key", group_text: "label", default_group_label: "Unassigned" });
	} else if (mode === "priority") {
		gantt.groupBy({ groups: PRIORITIES, relation_property: "priority", group_id: "key", group_text: "label", default_group_label: "No priority" });
	} else if (mode === "phase") {
		gantt.groupBy({ groups: PHASES, relation_property: "phase", group_id: "key", group_text: "label", default_group_label: "Unphased" });
	}

	let labels = { tree: "none", owner: "Owner", priority: "Priority", phase: "Phase" };
}

/* ---------------------------------------------------------------------------
 *  INIT
 * ------------------------------------------------------------------------ */
gantt.init("gantt_here");
gantt.parse(demoData);
setGroup("owner");   // open already grouped so the headline feature is visible at a glance
<!DOCTYPE html>
<html lang="en">
<head>
	<meta http-equiv="Content-type" content="text/html; charset=utf-8">
	<title>Grouping: 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_grid_data .group_row,
		.gantt_grid_data .group_row.odd { background: #eafaf1; }
		:root[data-gantt-theme="dark"] .gantt_grid_data .group_row,
		:root[data-gantt-theme="dark"] .gantt_grid_data .group_row.odd { background: #17332a; }
		.gantt_task_row.group_row,
		.gantt_task_row.group_row.odd { background: rgba(32,181,109,.06); }

		.group_label { font-weight: 700; color: var(--dhx-ink); }
		.group_count {
			display: inline-block; margin-left: 8px; padding: 0 8px;
			border-radius: 10px; background: #d3f3e1; color: #0e7a49;
			font: 700 11px var(--dhx-font); font-variant-numeric: tabular-nums;
		}
		:root[data-gantt-theme="dark"] .group_count { background: #1d4433; color: #7be0ab; }

		.gantt_task_line.group_bar {
			background: #20B56D; border: none; border-radius: 4px;
		}
		.gantt_task_line.group_bar .gantt_task_progress { background: rgba(0,0,0,.15); }

		.pr { display: inline-flex; align-items: center; gap: 6px; }
		.pr i { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
		.pr.high   i { background: #e5484d; }
		.pr.normal i { background: #f5a623; }
		.pr.low    i { background: #3aa757; }

		.owner_pill {
			display: inline-block; min-width: 22px; height: 22px; line-height: 22px;
			padding: 0 7px; border-radius: 11px; background: var(--dhx-accent-soft);
			color: var(--dhx-accent); font: 600 11px var(--dhx-font); text-align: center;
		}
	</style>
</head>
<body>

<!-- Header -->
<div class="dhx_header" style="justify-content:center">
	<div class="dhx_seg" id="group_seg">
		<button class="dhx_btn active" data-group="tree"     onclick="setGroup('tree')">Tree</button>
		<button class="dhx_btn"        data-group="owner"    onclick="setGroup('owner')">Owner</button>
		<button class="dhx_btn"        data-group="priority" onclick="setGroup('priority')">Priority</button>
		<button class="dhx_btn"        data-group="phase"    onclick="setGroup('phase')">Phase</button>
	</div>
</div>

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


</body>
</html>