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

Minute scale

Plan a single production day in minutes, switching between a full day view and fifteen minute detail across two shifts.

gantt.plugins({ auto_scheduling: true });

gantt.config.date_format = "%Y-%m-%d %H:%i";
gantt.config.date_grid = "%H:%i";
gantt.config.work_time   = true;
gantt.config.row_height   = 36;
gantt.config.bar_height   = 24;
gantt.config.scale_height = 52;
gantt.config.duration_unit = "minute";
gantt.config.min_duration = 60 * 1000; // (1 minute)
gantt.config.show_tasks_outside_timescale = true;
gantt.config.auto_scheduling = {
	enabled: true,
	apply_constraints: false,
	gap_behavior: "compress"
};

/* Durations remain integer minute values in task data. The formatter converts
 * them to decimal hours for display and parses values entered with hour or
 * minute units in the grid and lightbox. */
const durationFormatter = gantt.ext.formatters.durationFormatter({
	enter: "minute",
	store: "minute",
	format: "auto",
	short: false
});

const durationEditor = {
	type: "duration",
	map_to: "duration",
	formatter: durationFormatter
};

/* Anchor to the next Monday so the demo never looks stale. Set visible
 * timeline to one working day: 05:30 (before shift start) to 14:30 (after
 * shift end). */
function mondayOnOrAfter(date) {
	date = gantt.date.day_start(new Date(date));
	while (date.getDay() !== 1) date = gantt.date.add(date, 1, "day");
	return date;
}
let ANCHOR = mondayOnOrAfter(new Date());

gantt.config.start_date = gantt.date.add(ANCHOR, 5 * 60 + 30, "minute");   // 05:30, a gutter before the shift
gantt.config.end_date   = gantt.date.add(ANCHOR, 14 * 60 + 30, "minute");  // 14:30, a gutter after it

/* Scale presets: full day (1-hour) and detail (15-minute). Swapped by
 * the toolbar toggle. */
let SCALES_FULL_DAY = [
	{ unit: "hour", step: 1, format: "%H:%i" },
	{ unit: "minute", step: 15, format: "%i" }
];

let SCALES_DETAIL = [
	{ unit: "hour", step: 1, format: "%H:%i" },
	{ unit: "minute", step: 5, format: "%i" }
];

gantt.config.scales = SCALES_FULL_DAY;
gantt.config.min_column_width = 34;

/* Work time: two shifts with a lunch break in between. setWorkTime
 * accepts an hours array with HH:MM-HH:MM strings and respects minute
 * precision. */
gantt.setWorkTime({ hours: ["6:00-10:00", "10:30-14:00"] });

/* Non-working cells (break time, before 06:00, after 14:00) are shaded
 * grey. Note: scale_cell_class receives (date), timeline_cell_class
 * receives (item, date). */
gantt.templates.scale_cell_class    = function (date)       {
	return gantt.isWorkTime({ date }) ? "" : "non_working";
};
gantt.templates.timeline_cell_class = function (task, date) {
	return gantt.isWorkTime({ date, task }) ? "" : "non_working";
};

/* Grid: duration values are displayed as decimal hours and edited with the
 * same formatter. Values such as "0.5 h", "45 min", and "1.25 hours" are
 * converted back into integer minutes. */
gantt.config.columns = [
	{ name: "text",       label: "Task", tree: true, width: 240, resize: true },
	{ name: "start_date", label: "Start", align: "center", width: 100, resize: true, template: function (task) {
			return task.type === gantt.config.types.project ? "" : gantt.templates.date_grid(task.start_date, task);
		}
	},
	{ name: "duration",   label: "Duration", align: "center", width: 120, resize: true,
		editor: durationEditor, template: function (task) {
			return task.type === gantt.config.types.project ? "" : durationFormatter.format(task.duration || 0);
		}
	},
	{ name: "add", width: 40, resize: true }
];

gantt.locale.labels.section_time = "Time period and duration";
gantt.config.lightbox.sections = [
	{ name: "description", height: 50, map_to: "text", type: "textarea", focus: true },
	{
		name: "time",
		height: 72,
		map_to: "auto",
		type: "duration",
		time_format: ["%d", "%m", "%Y", "%H:%i"],
		formatter: durationFormatter
	}
];

/* Dataset: two production lines and maintenance, anchored to ANCHOR.
 * Each line runs batch operations sequentially (finish-to-start links).
 * Realistic task names and durations (15-90 minutes), displayed as
 * 0.25-1.5 decimal hours. */
/* every leaf is seeded at 06:00 shift start; auto-scheduling packs each
 * line into a sequential chain around the 10:00-10:30 break */
const dateToStr = gantt.date.date_to_str("%Y-%m-%d %H:%i");
let SEED = dateToStr(gantt.date.add(ANCHOR, 6 * 60, "minute"));

let data = {
	tasks: [
		/* ---- Shift A - Line 1 (Dairy flavors) ---- */
		{ id: 1, text: "Shift A - Line 1", type: "project", open: true, progress: 0 },

		{ id: 11, text: "Line startup & safety check",     parent: 1, start_date: SEED, duration: 30,  progress: 1 },
		{ id: 12, text: "Batch run - Vanilla (1000 L)",     parent: 1, start_date: SEED, duration: 60,  progress: 0.8 },
		{ id: 13, text: "Quality sample - Vanilla",         parent: 1, start_date: SEED, duration: 15,  progress: 1 },
		{ id: 14, text: "Changeover to Strawberry",         parent: 1, start_date: SEED, duration: 30,  progress: 0.6 },
		{ id: 15, text: "Batch run - Strawberry (1200 L)",  parent: 1, start_date: SEED, duration: 75,  progress: 0.4 },
		{ id: 16, text: "Quality sample - Strawberry",      parent: 1, start_date: SEED, duration: 15,  progress: 0.2 },
		{ id: 17, text: "CIP flush & drain - Line 1",       parent: 1, start_date: SEED, duration: 30,  progress: 0 },
		{ id: 18, text: "Line restart after break",         parent: 1, start_date: SEED, duration: 15,  progress: 0 },
		{ id: 19, text: "Batch run - Pistachio (800 L)",    parent: 1, start_date: SEED, duration: 60,  progress: 0 },
		{ id: 20, text: "Quality sample - Pistachio",       parent: 1, start_date: SEED, duration: 15,  progress: 0 },
		{ id: 21, text: "Changeover to Hazelnut",           parent: 1, start_date: SEED, duration: 30,  progress: 0 },
		{ id: 22, text: "Batch run - Hazelnut (950 L)",     parent: 1, start_date: SEED, duration: 60,  progress: 0 },

		/* ---- Shift A - Line 2 (Specialty blends) ---- */
		{ id: 2, text: "Shift A - Line 2", type: "project", open: true, progress: 0 },

		{ id: 31, text: "Line startup & health check",      parent: 2, start_date: SEED, duration: 25,  progress: 0.9 },
		{ id: 32, text: "Batch run - Mango-Passion (900 L)", parent: 2, start_date: SEED, duration: 60,  progress: 0.7 },
		{ id: 33, text: "Quality sample - Tropical blend",  parent: 2, start_date: SEED, duration: 20,  progress: 0.8 },
		{ id: 34, text: "Changeover to Coconut-Lime",       parent: 2, start_date: SEED, duration: 35,  progress: 0.5 },
		{ id: 35, text: "Batch run - Coconut-Lime (850 L)", parent: 2, start_date: SEED, duration: 70,  progress: 0.3 },
		{ id: 36, text: "Quality sample - Coconut-Lime",    parent: 2, start_date: SEED, duration: 20,  progress: 0.1 },
		{ id: 37, text: "CIP rinse & pressure wash",        parent: 2, start_date: SEED, duration: 35,  progress: 0 },
		{ id: 38, text: "Line standby after break",         parent: 2, start_date: SEED, duration: 20,  progress: 0 },
		{ id: 39, text: "Batch run - Berry fusion (750 L)", parent: 2, start_date: SEED, duration: 55,  progress: 0 },
		{ id: 40, text: "Quality sample - Berry fusion",    parent: 2, start_date: SEED, duration: 20,  progress: 0 },

		/* ---- Maintenance window ---- */
		{ id: 3, text: "Maintenance & QA", type: "project", open: true, progress: 0 },

		{ id: 51, text: "Pump inspection & vibration check", parent: 3, start_date: SEED, duration: 45,  progress: 0.5 },
		{ id: 52, text: "Sensor calibration & validation",   parent: 3, start_date: SEED, duration: 40,  progress: 0.3 },
		{ id: 53, text: "Spare parts inventory",             parent: 3, start_date: SEED, duration: 30,  progress: 0 },
		{ id: 54, text: "Daily safety briefing",             parent: 3, start_date: SEED, duration: 20,  progress: 0 }
	],
	links: [
		/* Line 1 dependencies (finish-to-start) */
		{ id: 1,  source: 11, target: 12, type: "0" },
		{ id: 2,  source: 12, target: 13, type: "0" },
		{ id: 3,  source: 13, target: 14, type: "0" },
		{ id: 4,  source: 14, target: 15, type: "0" },
		{ id: 5,  source: 15, target: 16, type: "0" },
		{ id: 6,  source: 16, target: 17, type: "0" },
		{ id: 7,  source: 17, target: 18, type: "0" },
		{ id: 8,  source: 18, target: 19, type: "0" },
		{ id: 9,  source: 19, target: 20, type: "0" },
		{ id: 10, source: 20, target: 21, type: "0" },
		{ id: 11, source: 21, target: 22, type: "0" },

		/* Line 2 dependencies (finish-to-start) */
		{ id: 12, source: 31, target: 32, type: "0" },
		{ id: 13, source: 32, target: 33, type: "0" },
		{ id: 14, source: 33, target: 34, type: "0" },
		{ id: 15, source: 34, target: 35, type: "0" },
		{ id: 16, source: 35, target: 36, type: "0" },
		{ id: 17, source: 36, target: 37, type: "0" },
		{ id: 18, source: 37, target: 38, type: "0" },
		{ id: 19, source: 38, target: 39, type: "0" },
		{ id: 20, source: 39, target: 40, type: "0" },

		/* Maintenance dependencies */
		{ id: 21, source: 51, target: 52, type: "0" },
		{ id: 22, source: 52, target: 53, type: "0" },
		{ id: 23, source: 53, target: 54, type: "0" }
	]
};

/* Zoom toggle: swap scales and re-render. */
function applyZoom(preset) {
	if (preset === "detail") {
		gantt.config.scales = SCALES_DETAIL;
		gantt.config.min_column_width = 20;
	} else {
		gantt.config.scales = SCALES_FULL_DAY;
		gantt.config.min_column_width = 34;
	}
	DHX.ui.setActiveByAttribute("#zoom_seg .dhx_btn", "data-zoom", preset, "active");
	gantt.render();
}

gantt.init("gantt_here");
gantt.parse(data);
<!DOCTYPE html>
<html lang="en">
<head>
	<meta http-equiv="Content-type" content="text/html; charset=utf-8">
	<title>Minute scheduling and decimal durations: 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.non_working, .gantt_scale_cell.non_working { 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); }
	</style>
</head>
<body>

<div class="dhx_header" style="justify-content:center; gap:14px">
	<span class="toolbar_label">Minute scheduling & decimal durations</span>
	<div class="dhx_seg" id="zoom_seg">
		<button class="dhx_btn active" data-zoom="day" onclick="applyZoom('day')">Full day</button>
		<button class="dhx_btn"        data-zoom="detail" onclick="applyZoom('detail')">5-min detail</button>
	</div>
</div>

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

<div class="dhx_footer">
	<span class="hint">Durations are stored in <b>minutes</b> but can be entered as <b>0.5 h</b>, <b>45 min</b>, or <b>1.25 hours</b>. The work calendar uses two shifts (06:00-10:00, 10:30-14:00).</span>
</div>



</body>
</html>