Explore thousands of people and assignments, then jump to the last row. Compare progressive row rendering off and on, with simple labels or interactive row headers.
const dp = new DayPilot.Scheduler("dp", {
startDate: "2026-10-01",
days: 31,
scale: "Day",
cellWidth: 56,
timeHeaders: [
{groupBy: "Month", format: "MMMM yyyy"},
{groupBy: "Day", format: "ddd d"}
],
heightSpec: "Fixed",
height: 500,
rowHeaderWidth: 300,
rowHeaderWidthAutoFit: false,
rowMinHeight: 36,
rowMarginTop: 6,
rowMarginBottom: 6,
eventHeight: 28,
treeEnabled: true,
progressiveRowRendering: true,
// Only a few assignments are visible at once. Fill them immediately
// on a jump so the row-header comparison has a complete viewport.
scrollDelayEvents: 0,
timeRangeSelectedHandling: "Disabled",
eventMoveHandling: "Disabled",
eventResizeHandling: "Disabled",
onBeforeRowHeaderRender: args => {
if (app.elements.content.value === "html") {
args.row.html = app.personLabel(args.row.data);
}
},
onBeforeRowHeaderDomAdd: args => {
if (app.elements.content.value === "dom") {
args.target = "Text";
args.element = app.createPerson(args.row.data);
}
}
});
dp.init();
const app = {
elements: {
root: document.getElementById("rowheaderdemo"),
enabled: document.getElementById("enabled"),
count: document.getElementById("rowCount"),
layout: document.getElementById("layout"),
content: document.getElementById("contentMode"),
renderTime: document.getElementById("renderTime"),
dataset: document.getElementById("dataset"),
hint: document.getElementById("contentHint")
},
// A row header can be removed when it leaves the viewport. Persistent
// application state belongs outside the header's DOM element.
markedRows: new Set(),
data: null,
dataKey: null,
pending: 0,
names: ["Avery Morgan", "Jordan Lee", "Sam Rivera", "Riley Chen", "Alex Parker", "Casey Blake", "Taylor Reed", "Morgan Ellis", "Jamie Quinn", "Drew Patel", "Skyler Evans", "Cameron Brooks"],
roles: ["Design", "Engineering", "Quality assurance", "Product", "Operations"],
teamNames: ["Product studio", "Platform team", "Customer experience", "Research team", "Operations team"],
init() {
this.addEventHandlers();
this.applySettings();
},
addEventHandlers() {
for (const control of [this.elements.enabled, this.elements.count, this.elements.layout, this.elements.content]) {
control.addEventListener("change", () => this.applySettings());
}
this.elements.root.querySelectorAll("[data-jump]").forEach(button => {
button.addEventListener("click", () => {
if (!this.data || dp.disposed()) {
return;
}
const index = Math.min(this.data.rows.length - 1, Math.floor(this.data.rows.length * Number(button.dataset.jump)));
const row = dp.rows.find(this.data.rows[index].id);
row.parent()?.expand();
dp.scrollToResource(row);
});
});
},
async applySettings() {
const request = ++this.pending;
// Large eager DOM trees can block a browser for minutes. Keep
// the public comparison responsive; the larger presets use
// progressive rendering throughout.
const large = Number(this.elements.count.value) > 10000;
this.elements.enabled.disabled = large;
if (large) {
this.elements.enabled.checked = true;
}
this.elements.enabled.parentElement.title = large
? "Choose 10,000 rows or fewer to compare rendering off and on."
: "Render row headers near the viewport. Clear to render all visible rows in advance.";
this.elements.renderTime.textContent = "Loading " + Number(this.elements.count.value).toLocaleString("en-US") + " rows…";
this.elements.root.setAttribute("aria-busy", "true");
// Let the browser display loading feedback before synchronous work.
await new Promise(resolve => requestAnimationFrame(() => setTimeout(resolve, 0)));
if (request !== this.pending || !this.elements.root.isConnected || dp.disposed()) {
return;
}
const dataKey = this.elements.count.value + ":" + this.elements.layout.value;
const sameData = dataKey === this.dataKey;
const scrollY = sameData ? dp.getScrollY() : 0;
const scrollX = sameData ? dp.getScrollX() : 0;
const dataStart = performance.now();
if (!sameData) {
this.data = this.generateData(Number(this.elements.count.value), this.elements.layout.value === "teams");
this.dataKey = dataKey;
}
const dataMs = performance.now() - dataStart;
const started = performance.now();
dp.update({
resources: this.data.resources,
events: this.data.events,
progressiveRowRendering: this.elements.enabled.checked
});
dp.setScroll(scrollX, scrollY);
const renderMs = performance.now() - started;
this.elements.renderTime.textContent = "Rendering: " + renderMs.toFixed(0) + " ms";
this.elements.renderTime.title = "Time spent updating the Scheduler and restoring the scroll position; browser painting follows. " + (sameData ? "Existing dataset reused." : "Data generation: " + dataMs.toFixed(0) + " ms.");
this.elements.dataset.textContent = this.data.rows.length.toLocaleString("en-US") + " rows · " + this.data.events.length.toLocaleString("en-US") + " assignments";
const teams = this.elements.layout.value === "teams";
this.elements.hint.textContent = {
text: "All rows and assignments are loaded. " + (teams ? "Expand a team or jump through the list to explore." : "Jump through the list to explore."),
html: "People have initials, roles and unique numbers." + (teams ? " Expand or collapse teams using the arrows." : ""),
dom: "Bookmark a person or team, then scroll away and back. Your bookmarks stay selected."
}[this.elements.content.value];
if (large) {
this.elements.hint.textContent += " Progressive rendering stays on above 10,000 rows; choose a smaller preset for the off/on comparison.";
}
this.elements.root.setAttribute("aria-busy", "false");
},
generateData(count, teams) {
const resources = [];
const rows = [];
const events = [];
const dates = Array.from({length: 32}, (_, i) => dp.startDate.addDays(i).toString());
for (let i = 0; i < count; i++) {
const team = teams && i % 10 === 0;
const name = team ? this.teamNames[Math.floor(i / 10) % this.teamNames.length] : this.names[i % this.names.length];
const number = String(i + 1).padStart(6, "0");
const resource = {
id: "R" + i,
name: name + " · " + number,
tags: {team, name, number, role: team ? "9 people" : this.roles[i % this.roles.length], tone: i % 4}
};
if (team) {
resource.children = [];
resource.expanded = true;
}
if (teams && !team) {
resources[resources.length - 1].children.push(resource);
}
else {
resources.push(resource);
}
rows.push(resource);
if (team) {
continue;
}
for (let j = 0; j < 2; j++) {
const day = (i * 3 % 6) + j * 10;
events.push({
id: i + "-" + j,
resource: resource.id,
start: dates[day],
end: dates[day + 3 + i % 3],
text: ["Website refresh", "Client onboarding", "Product launch", "Design review", "Platform update"][i % 5]
});
}
}
return {resources, rows, events};
},
personLabel(resource) {
const tags = resource.tags;
const initials = tags.name.split(" ").map(word => word[0]).slice(0, 2).join("").toUpperCase();
return `<span class="person ${tags.team ? "team" : ""}">
<span class="avatar tone-${tags.tone}">${initials}</span>
<span><span class="person-name">${tags.name}</span><span class="person-detail">${tags.role} · ${tags.number}</span></span>
</span>`;
},
createPerson(resource) {
const container = document.createElement("div");
container.className = "header-content";
container.innerHTML = this.personLabel(resource);
const button = document.createElement("button");
button.type = "button";
button.className = "mark-row";
button.dataset.resource = resource.id;
button.setAttribute("aria-label", "Bookmark " + resource.name);
button.setAttribute("aria-pressed", String(this.markedRows.has(resource.id)));
button.title = "Bookmark this row";
button.innerHTML = '<svg width="14" height="18" viewBox="0 0 16 20" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path d="M3 2h10v16l-5-3-5 3z"/></svg>';
button.addEventListener("click", event => {
event.stopPropagation();
if (this.markedRows.has(resource.id)) {
this.markedRows.delete(resource.id);
}
else {
this.markedRows.add(resource.id);
}
button.setAttribute("aria-pressed", String(this.markedRows.has(resource.id)));
});
container.appendChild(button);
return container;
}
};
app.init();