Popular features/Work and material resources
Loading live demo…from dhtmlxcode.com
Popular features
Work and material resources
Track people and shared equipment on one capacity panel, where a day cell turns red once demand passes the limit.
gantt.plugins({ tooltip: true });
gantt.message({
text: "Week 2 needs 3 GPU nodes but the pool holds 2. Double-click a task to reassign, or drag its bar into free space.",
expire: -1
});
gantt.config.date_format = "%Y-%m-%d";
gantt.config.resources = true; // parse the "resources" collection + assignments
gantt.config.resource_store = "resource";
gantt.config.resource_property = "resources";
gantt.config.work_time = true;
gantt.config.order_branch = true;
gantt.config.open_tree_initially = true;
gantt.config.row_height = 34;
gantt.config.bar_height = 22;
gantt.config.scale_height = 50;
gantt.config.min_column_width = 44;
gantt.config.grid_width = 420;
gantt.config.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" }
];
let WORK_DAY = 8; // hours/day capacity for a person
/* People get a color; equipment renders as neutral pills. */
let PEOPLE_COLORS = { 10: "#6366f1", 11: "#0ea5e9", 12: "#10b981" };
let RESOURCES = [
{ id: 1, text: "People", parent: null, category: "people" },
{ id: 2, text: "Shared infrastructure", parent: null, category: "equipment" },
{ id: 10, text: "Ana Kim (ML)", parent: 1, type: "work", unit: "h/day", capacity: WORK_DAY, category: "people" },
{ id: 11, text: "Ben Ruiz (Backend)", parent: 1, type: "work", unit: "h/day", capacity: WORK_DAY, category: "people" },
{ id: 12, text: "Cleo Mara (QA)", parent: 1, type: "work", unit: "h/day", capacity: WORK_DAY, category: "people" },
{ id: 20, text: "CI build agents", parent: 2, type: "material", unit: "agents", capacity: 4, category: "equipment" },
{ id: 21, text: "GPU eval nodes", parent: 2, type: "material", unit: "nodes", capacity: 2, category: "equipment" },
{ id: 22, text: "Device lab", parent: 2, type: "material", unit: "devices", capacity: 6, category: "equipment" },
{ id: 23, text: "Staging seats", parent: 2, type: "material", unit: "seats", capacity: 3, category: "equipment" }
];
function getResourceStore() {
return gantt.getDatastore(gantt.config.resource_store);
}
/* Bar color by the task's first person. */
let barStyles = document.createElement("style");
barStyles.textContent = Object.keys(PEOPLE_COLORS).map(function (id) {
return ".gantt_task_line.owner_" + id + " { background: " + PEOPLE_COLORS[id] + "; }";
}).join("\n");
document.head.appendChild(barStyles);
function isAssignable(task) {
return task.type !== gantt.config.types.project && task.type !== gantt.config.types.milestone;
}
function firstPerson(task) {
let list = task.resources || [];
for (let i = 0; i < list.length; i++) {
let resource = getResourceStore().getItem(list[i].resource_id);
if (resource && resource.type === "work") return list[i].resource_id;
}
return null;
}
/* ---------------------------------------------------------------------------
* CAPACITY MATH: one rule for both kinds: concurrent demand vs ceiling
* ------------------------------------------------------------------------ */
function ceiling(resource) {
return resource.type === "work" ? WORK_DAY : resource.capacity;
}
function cellLoad(resource, tasks) {
let load = 0;
tasks.forEach(function (task) {
gantt.getResourceAssignments(resource.id, task.id).forEach(function (assignment) { load += Number(assignment.value); });
});
return load;
}
function peakLoad(resource) {
let byDay = {};
gantt.getResourceAssignments(resource.id).forEach(function (assignment) {
let task = gantt.getTask(assignment.task_id);
for (let date = task.start_date; date < task.end_date; date = gantt.date.add(date, 1, "day")) {
if (gantt.isWorkTime({ date: date, task })) byDay[+date] = (byDay[+date] || 0) + Number(assignment.value);
}
});
let peak = 0;
for (let k in byDay) peak = Math.max(peak, byDay[k]);
return peak;
}
/* ---------------------------------------------------------------------------
* TEMPLATES
* ------------------------------------------------------------------------ */
gantt.templates.timeline_cell_class = function (task, date) {
return gantt.isWorkTime({ date, task }) ? "" : "week_end";
};
gantt.templates.task_class = function (start, end, task) {
if (!isAssignable(task)) return "";
let person = firstPerson(task);
return person ? "owner_" + person : "no_owner";
};
gantt.templates.tooltip_text = function (start, end, task) {
if (!isAssignable(task)) return "<b>" + task.text + "</b>";
let rows = (task.resources || []).filter(function (assignment) {
let resource = getResourceStore().getItem(assignment.resource_id);
return resource && resource.type; // skip not-yet-picked rows (default to a group folder id)
}).map(function (assignment) {
let resource = getResourceStore().getItem(assignment.resource_id);
return resource.text + ": " + assignment.value + " " + resource.unit; // work resources already carry unit "h/day"
}).join("<br>");
return "<b>" + task.text + "</b><br>" + (rows || "No resources");
};
/* Per-day allocation dot in the resource panel. */
gantt.templates.resource_cell_class = function (start, end, resource, tasks) {
if (getResourceStore().hasChild(resource.id)) return "";
let kind = resource.type === "work" ? "res_work" : "res_material";
return "resource_marker " + kind + " " + (cellLoad(resource, tasks) > ceiling(resource) ? "workday_over" : "workday_ok");
};
gantt.templates.resource_cell_value = function (start, end, resource, tasks) {
if (getResourceStore().hasChild(resource.id)) return "";
let load = cellLoad(resource, tasks);
if (!load) return "";
if (resource.type === "work") return "<div>" + load + "</div>"; // hours in a circle
return "<div>" + load + "<span class='cap'>/" + resource.capacity + "</span></div>"; // used / pool in a pill
};
/* ---------------------------------------------------------------------------
* MAIN GRID
* ------------------------------------------------------------------------ */
gantt.config.columns = [
{ name: "text", label: "Task", tree: true, width: 210, resize: true },
{ name: "start_date", label: "Start", align: "center", width: 100, resize: true },
{ name: "resources", label: "Resources", align: "left", width: 156, resize: true, template: function (task) {
if (!isAssignable(task)) return "";
let list = (task.resources || []).filter(function (assignment) {
let resource = getResourceStore().getItem(assignment.resource_id);
return resource && resource.type; // skip not-yet-picked rows (default to a group folder id)
});
if (!list.length) return "<span class='muted'>None</span>";
return list.map(function (assignment) {
let resource = getResourceStore().getItem(assignment.resource_id);
if (resource.type === "work") {
return "<span class='chip' style='background:" + PEOPLE_COLORS[resource.id] + "' title='" + resource.text + ", " + assignment.value + "h/day'>" + resource.text.charAt(0) + "</span>";
}
return "<span class='pill' title='" + resource.text + "'>" + assignment.value + " " + resource.unit + "</span>";
}).join(" ");
}},
{ name: "duration", label: "Days", align: "center", width: 50, resize: true }
];
/* ---------------------------------------------------------------------------
* LIGHTBOX: double-click a task to add, edit or remove its assignments
* ------------------------------------------------------------------------ */
let assignResourceEditor = { type: "select", map_to: "resource_id", options: gantt.serverList("resourceOptions") };
let assignValueEditor = { type: "number", map_to: "value", min: 0, max: 20 };
let resourceLightboxConfig = {
columns: [
{ name: "resource", label: "Resource", align: "center", width: 160, editor: assignResourceEditor, template: function (assignment) {
let resource = getResourceStore().getItem(assignment.resource_id);
// New rows default to the first resource-tree node, a group folder (no .type):
// treat that the same as "nothing picked yet" rather than showing the folder name.
return (resource && resource.type) ? resource.text : "<span class='muted'>Choose a resource...</span>";
}},
{ name: "value", label: "Amount", align: "center", width: 90, editor: assignValueEditor, template: function (assignment) {
let resource = getResourceStore().getItem(assignment.resource_id);
if (!assignment.value || !resource || !resource.type) return "";
return resource.type === "work" ? assignment.value + " h/day" : assignment.value + " " + resource.unit;
}},
{ name: "delete", label: "", align: "center", width: 40, template: function (assignment) {
return "<div data-assignment-id='" + assignment.id + "' data-assignment-delete='" + assignment.id + "' class='dhx_gantt_icon dhx_gantt_icon_delete'></div>";
}}
]
};
gantt.locale.labels.section_resources = "Resources";
gantt.config.lightbox = {
sections: [
{ name: "description", height: 38, map_to: "text", type: "textarea", focus: true },
{ name: "resource_selector", label: "Resources", type: "resource_selector", config: resourceLightboxConfig },
{ name: "time", type: "duration", map_to: "auto" }
]
};
/* ---------------------------------------------------------------------------
* RESOURCE PANEL (bottom grid + per-day timeline)
* ------------------------------------------------------------------------ */
let resourceConfig = {
scale_height: 30,
row_height: 34,
scales: [{ unit: "day", step: 1, format: "%d" }],
columns: [
{ name: "name", label: "Resource", tree: true, width: 210, resize: true, template: function (resource) {
if (getResourceStore().hasChild(resource.id)) return resource.text;
return "<span class='res_kind res_kind--" + resource.type + "'></span>" + resource.text;
}},
{ name: "capacity", label: "Capacity", align: "center", width: 96, template: function (resource) {
if (getResourceStore().hasChild(resource.id)) return "";
return resource.type === "work" ? WORK_DAY + " h/day" : resource.capacity + " " + resource.unit;
}},
{ name: "peak", label: "Peak", align: "center", width: 76, template: function (resource) {
if (getResourceStore().hasChild(resource.id)) return "";
let person = peakLoad(resource), over = person > ceiling(resource);
return "<span class='res_peak" + (over ? " res_peak--over" : "") + "'>" + person + (resource.type === "work" ? "h" : "") + "</span>";
}}
]
};
let resourceTemplates = {
grid_row_class: function (start, end, resource) { return getResourceStore().hasChild(resource.id) ? "group_row" : ""; },
task_row_class: function (start, end, resource) { return getResourceStore().hasChild(resource.id) ? "group_row" : ""; }
};
gantt.config.layout = {
css: "gantt_container",
rows: [
{ gravity: 2, cols: [
{ view: "grid", group: "grids", scrollY: "scrollVer" },
{ resizer: true, width: 1 },
{ view: "timeline", scrollX: "scrollHor", scrollY: "scrollVer" },
{ view: "scrollbar", id: "scrollVer", group: "vertical" }
]},
{ resizer: true, width: 1, next: "resources" },
{ gravity: 1, id: "resources", config: resourceConfig, templates: resourceTemplates, cols: [
{ view: "resourceGrid", group: "grids", scrollY: "resourceVScroll" },
{ resizer: true, width: 1 },
{ view: "resourceTimeline", scrollX: "scrollHor", scrollY: "resourceVScroll" },
{ view: "scrollbar", id: "resourceVScroll", group: "vertical" }
]},
{ view: "scrollbar", id: "scrollHor" }
]
};
/* ---------------------------------------------------------------------------
* INIT + DATA
* Week 2: ranking + safety + regression evals overlap -> 3 GPU nodes needed,
* pool is 2 -> red. Everyone stays under 8h/day AND Ana is free in week 3,
* so the manager can drag one eval there to level the load back to green.
* ------------------------------------------------------------------------ */
gantt.init("gantt_here");
/* The resources plugin auto-fills "resourceOptions" from the WHOLE resource tree
* (group folders included) as soon as it parses: rebuild it to leaf items only
* so the assignment editor's dropdown doesn't list "People" / "Shared infrastructure"
* as pickable resources. */
getResourceStore().attachEvent("onParse", function () {
gantt.updateCollection("resourceOptions", RESOURCES.filter(function (resource) { return !!resource.type; }).map(function (resource) {
return { key: resource.id, label: resource.text };
}));
});
gantt.parse({
tasks: [
{ id: 100, text: "AI Search 4.0", type: "project", open: true },
{ id: 11, text: "Data pipeline", parent: 100, start_date: "2026-07-06", duration: 4, progress: 0.7, resources: [{ resource_id: 11, value: 6 }, { resource_id: 20, value: 2 }] },
{ id: 12, text: "Model training run", parent: 100, start_date: "2026-07-06", duration: 4, progress: 0.6, resources: [{ resource_id: 10, value: 6 }, { resource_id: 21, value: 1 }] },
{ id: 13, text: "Eval harness", parent: 100, start_date: "2026-07-07", duration: 3, progress: 0.5, resources: [{ resource_id: 12, value: 5 }, { resource_id: 20, value: 1 }] },
{ id: 14, text: "Data validation checks", parent: 100, start_date: "2026-07-06", duration: 1, progress: 0.8, resources: [{ resource_id: 12, value: 3 }, { resource_id: 22, value: 2 }] },
{ id: 21, text: "Eval: ranking", parent: 100, start_date: "2026-07-13", duration: 3, progress: 0.2, resources: [{ resource_id: 10, value: 5 }, { resource_id: 21, value: 1 }] },
{ id: 22, text: "Eval: safety", parent: 100, start_date: "2026-07-13", duration: 3, progress: 0.2, resources: [{ resource_id: 11, value: 5 }, { resource_id: 21, value: 1 }] },
{ id: 23, text: "Regression eval", parent: 100, start_date: "2026-07-13", duration: 3, progress: 0.1, resources: [{ resource_id: 12, value: 6 }, { resource_id: 21, value: 1 }] },
{ id: 24, text: "Perf benchmark", parent: 100, start_date: "2026-07-13", duration: 3, progress: 0.1, resources: [{ resource_id: 11, value: 3 }, { resource_id: 20, value: 2 }] },
{ id: 25, text: "Latency profiling", parent: 100, start_date: "2026-07-16", duration: 2, progress: 0, resources: [{ resource_id: 10, value: 4 }, { resource_id: 20, value: 1 }] },
{ id: 26, text: "Eval report review", parent: 100, start_date: "2026-07-13", duration: 2, progress: 0, resources: [{ resource_id: 12, value: 4 }, { resource_id: 23, value: 1 }] },
{ id: 31, text: "Device QA sweep", parent: 100, start_date: "2026-07-20", duration: 4, progress: 0, resources: [{ resource_id: 12, value: 6 }, { resource_id: 22, value: 4 }] },
{ id: 32, text: "Beta on staging", parent: 100, start_date: "2026-07-20", duration: 3, progress: 0, resources: [{ resource_id: 11, value: 5 }, { resource_id: 23, value: 3 }] },
{ id: 33, text: "Bug bash fixes", parent: 100, start_date: "2026-07-23", duration: 2, progress: 0, resources: [{ resource_id: 11, value: 3 }, { resource_id: 20, value: 2 }] },
{ id: 34, text: "Localization QA", parent: 100, start_date: "2026-07-22", duration: 3, progress: 0, resources: [{ resource_id: 10, value: 4 }, { resource_id: 22, value: 2 }] },
{ id: 36, text: "Release notes prep", parent: 100, start_date: "2026-07-24", duration: 2, progress: 0, resources: [{ resource_id: 12, value: 4 }, { resource_id: 20, value: 1 }] },
{ id: 37, text: "Post-launch monitoring setup", parent: 100, start_date: "2026-07-27", duration: 2, progress: 0, resources: [{ resource_id: 11, value: 4 }, { resource_id: 20, value: 1 }] },
{ id: 35, text: "Ship AI Search 4.0", parent: 100, start_date: "2026-07-27", type: "milestone" }
],
links: [
{ id: 1, source: 11, target: 13, type: "0" },
{ id: 2, source: 12, target: 21, type: "0" },
{ id: 3, source: 12, target: 22, type: "0" },
{ id: 4, source: 23, target: 31, type: "0" },
{ id: 5, source: 31, target: 35, type: "0" },
{ id: 6, source: 32, target: 35, type: "0" },
{ id: 7, source: 33, target: 37, type: "0" }
],
resources: RESOURCES
});
/* ---------------------------------------------------------------------------
* LIVE CAPACITY STATUS: recompute the bottleneck as the manager reschedules
* ------------------------------------------------------------------------ */
function currentBottleneck() {
let worst = null;
getResourceStore().eachItem(function (resource) {
if (getResourceStore().hasChild(resource.id)) return;
let over = peakLoad(resource) - ceiling(resource);
if (over > 0 && (!worst || over > worst.over)) worst = { resource: resource, over: over, peak: peakLoad(resource) };
});
return worst;
}
function updateStatus() {
let worst = currentBottleneck(), el = document.getElementById("footer_status");
if (worst) {
el.className = "status status--over";
el.innerHTML = "Over capacity: <b>" + worst.resource.text + "</b>, " + worst.peak + " needed, only " + worst.resource.capacity + " in the pool";
} else {
el.className = "status status--ok";
el.innerHTML = "✓ All resources within capacity";
}
}
/* Keep every assignment aligned with the task while its dates are changing. */
function syncAssignmentDates(task) {
if (!task || !isAssignable(task)) return;
var start = new Date(task.start_date);
var end = new Date(task.end_date);
var duration = gantt.calculateDuration(start, end);
var assignmentStore = gantt.getDatastore(gantt.config.resource_assignment_store);
var assignments = gantt.getTaskAssignments(task.id);
assignments.forEach(function (assignment) {
assignment.start_date = new Date(start);
assignment.end_date = new Date(end);
assignment.delay = 0;
assignment.duration = duration;
assignmentStore.updateItem(assignment.id, assignment);
});
/* Store changes are copied back to the task's resource property. */
gantt.updateTaskAssignments(task.id);
}
/* Refresh the assignment data and the complete lower resource panel. */
var resourceRefreshFrame = null;
function refreshResourcePanel() {
if (resourceRefreshFrame != null) return;
resourceRefreshFrame = requestAnimationFrame(function () {
resourceRefreshFrame = null;
var assignmentStore = gantt.getDatastore(gantt.config.resource_assignment_store);
var resourceStore = gantt.getDatastore(gantt.config.resource_store);
assignmentStore.refresh();
resourceStore.refresh();
["resourceGrid", "resourceTimeline"].forEach(function (viewName) {
var view = gantt.getLayoutView(viewName);
if (view && typeof view.render === "function") {
view.render();
}
});
updateStatus();
});
}
gantt.attachEvent("onTaskDrag", function (id, mode, task) {
syncAssignmentDates(task);
refreshResourcePanel();
return true;
});
gantt.attachEvent("onAfterTaskDrag", function (id) {
if (gantt.isTaskExists(id)) {
syncAssignmentDates(gantt.getTask(id));
}
refreshResourcePanel();
});
gantt.attachEvent("onAfterTaskUpdate", refreshResourcePanel);
updateStatus();
function toggleFocus() {
let on = document.body.classList.toggle("focus_over");
DHX.toggle.setById("focus_switch", on);
}<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>Work & material resources: 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: 6px; box-shadow: 0 1px 2px rgba(16,24,40,.16); }
.gantt_task_line .gantt_task_progress { background: rgba(0,0,0,.18); border-radius: 6px 0 0 6px; }
.gantt_task_cell.week_end { background: rgba(15,23,42,.04); }
.gantt_task_line.no_owner { background: #94a3b8; }
.chip { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; border-radius: 50%; margin: 0 1px; color: #fff; font: 700 10px var(--dhx-font); vertical-align: middle; }
.pill { display: inline-flex; align-items: center; height: 18px; padding: 0 7px; margin: 0 1px; border-radius: 9px; background: #e8edf3; color: #475569; font: 600 10px var(--dhx-font); vertical-align: middle; }
.muted { color: var(--dhx-muted); }
.resource_marker { text-align: center; }
.resource_marker div {
height: 26px; min-width: 26px; margin: auto; padding: 0 2px;
display: inline-flex; align-items: center; justify-content: center;
border-radius: 50%; color: #fff; font: 700 11px var(--dhx-font);
}
.resource_marker.res_material div { border-radius: 8px; padding: 0 7px; }
.resource_marker .cap { font-weight: 600; opacity: .78; }
.resource_marker.workday_ok div { background: #3aa757; }
.resource_marker.workday_over div { background: #e5484d; }
body.focus_over .resource_marker.workday_ok div { opacity: .22; }
.gantt_grid_data .group_row, .group_row { font-weight: 600; }
.gantt_grid_data .group_row { background: #f1f5fb; }
:root[data-gantt-theme="dark"] .pill { background: #333c48; color: #c3ccd8; }
:root[data-gantt-theme="dark"] .gantt_grid_data .group_row { background: #232a34; }
:root[data-gantt-theme="dark"] .resource_marker.workday_ok div { background: #1d3326; color: #69d391; }
.res_peak { font: 700 11px var(--dhx-font); color: var(--dhx-ink-2); }
.res_peak.res_peak--over { color: #e5484d; }
.res_kind { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; vertical-align: middle; }
.res_kind--work { background: #6366f1; }
.res_kind--material { background: #0ea5e9; }
.legend { display: inline-flex; gap: 14px; }
.legend span { display: inline-flex; align-items: center; gap: 6px; font: 500 12px var(--dhx-font); color: var(--dhx-ink-2); }
.legend i { width: 12px; height: 12px; border-radius: 4px; }
.hint { font: 500 12px var(--dhx-font); color: var(--dhx-muted); }
.hint b { color: var(--dhx-ink-2); font-weight: 600; }
.status { font: 600 12px var(--dhx-font); }
.status--over, .status--over b { color: #e5484d; }
.status--ok { color: #3aa757; }
</style>
</head>
<body>
<div class="dhx_header">
<div class="dhx_brand dhx_brand--static">
<span class="dhx_brand__btn">
<span class="dhx_brand__logo" style="background:linear-gradient(140deg,#0ea5e9,#6366f1)">RP</span>
<span class="dhx_brand__titles">
<span class="dhx_brand__title-row"><span class="dhx_brand__title">Resource Planner</span></span>
<span class="dhx_brand__meta"><span class="dhx_brand__subtitle">People and shared equipment in one capacity view</span></span>
</span>
</span>
</div>
<div class="dhx_header__center">
<label class="dhx_toggle" onclick="toggleFocus()">
<span id="focus_label">Focus bottlenecks</span><span class="dhx_toggle__track" id="focus_switch"></span>
</label>
</div>
<div class="legend">
<span><i style="background:#6366f1"></i> People: hours/day</span>
<span><i style="background:#0ea5e9"></i> Equipment: shared pool</span>
<span><i style="background:#e5484d"></i> Over capacity</span>
</div>
</div>
<div id="gantt_here"></div>
<div class="dhx_footer">
<span class="hint">Double-click a task to add, change or remove its people and equipment. The lower panel sums each day: <b>people</b> in hours vs 8h/day, <b>equipment</b> as <b>used/pool</b> units.</span>
<div class="dhx_footer__right"><span id="footer_status"></span></div>
</div>
</body>
</html>