Documentation
Build the canonical dense timeline, low-level rail primitives, and split-pane Gantt views with the shipped solverforge-ui APIs.
Scheduling Views
solverforge-ui ships one canonical dense scheduling surface and one
complementary chart-first surface:
- Timeline for compact, lane-by-lane operator workflows with overview clustering, inline expand/collapse, exact interval geometry, and packed detailed lanes
- Gantt for dense, timeline-first planning and diagnostics
Timeline API
The shipped dense timeline surface is built around:
SF.rail.createTimeline(config)→{el, setModel, setViewport, expandCluster, destroy}
Use this as the default scheduling surface when you need sticky time headers,
sticky lane labels, synchronized scrolling, overview clustering, or packed
detailed inspection. Since solverforge-ui 0.6.4, detailed timeline blocks keep
their exact interval width instead of receiving timeline-specific minimum-width
inflation.
Timeline Model Contract
SF.rail.createTimeline(...) expects a normalized integer-minute model. All
time coordinates are absolute minutes:
model.axis.startMinuteandmodel.axis.endMinutemodel.axis.initialViewport.startMinuteandendMinutemodel.axis.days[].startMinuteandendMinutemodel.axis.ticks[].minutemodel.lanes[].items[].startMinuteandendMinute- overlay
startMinuteandendMinutewhen an overlay is time-positioned
Use application code to convert dates, shifts, and route windows into these
integer-minute coordinates before calling createTimeline(...).
Timeline Example
var timeline = SF.rail.createTimeline({
title: "Scheduling timeline",
labelWidth: 280,
zoomPresets: ["1w", "2w", "4w", "reset"],
model: {
axis: {
startMinute: 0,
endMinute: 28 * 1440,
days: buildDays(28),
ticks: buildSixHourTicks(28),
initialViewport: { startMinute: 0, endMinute: 14 * 1440 },
},
lanes: [
{
id: "ward-east",
label: "By location · Ward East",
mode: "overview",
items: [
{
id: "east-rush",
clusterId: "east-rush",
startMinute: 360,
endMinute: 1080,
label: "Monday intake surge",
tone: "blue",
summary: {
primaryLabel: "Monday intake surge",
secondaryLabel: "ER intake · trauma hold · overflow beds",
count: 24,
openCount: 3,
toneSegments: [
{ tone: "blue", count: 15 },
{ tone: "amber", count: 6 },
{ tone: "rose", count: 3 },
],
},
detailItems: [
{
id: "east-1",
startMinute: 360,
endMinute: 840,
label: "ER intake",
tone: "blue",
},
{
id: "east-2",
startMinute: 420,
endMinute: 960,
label: "Trauma hold",
tone: "amber",
},
{
id: "east-3",
startMinute: 480,
endMinute: 1080,
label: "Overflow beds",
tone: "rose",
},
],
},
],
},
{
id: "employee-ada",
label: "By employee · Ada",
mode: "detailed",
items: [
{
id: "ada-1",
startMinute: 2 * 1440 + 360,
endMinute: 2 * 1440 + 840,
label: "Primary shift",
tone: "amber",
},
{
id: "ada-2",
startMinute: 2 * 1440 + 660,
endMinute: 2 * 1440 + 1020,
label: "Handoff overlap",
tone: "amber",
},
],
},
],
},
});
container.appendChild(timeline.el);
timeline.expandCluster("ward-east", "east-rush");
Overview summaries are additive per field. If a summary item overrides aggregate
count beyond the concrete items the library can inspect, provide
summary.openCount and summary.toneSegments explicitly if you want those
aggregate signals rendered.
Detailed Geometry and Viewport Rules
solverforge-ui 0.6.4 tightened the timeline contract for solved schedules:
zoomPresetsdefaults to["1w", "2w", "4w", "reset"]; pass[]to omit zoom controls for fixed-horizon app surfaces- detailed blocks render exact interval geometry with no timeline-specific minimum width inflation
- adjacent detailed intervals, such as
[60, 120]and[120, 180], stay disjoint on one track - true detailed interval overlaps are packed onto separate track rows
- the body viewport owns vertical scrolling for dense solved schedules, while horizontal scroll and drag-pan stay synchronized with the sticky header
- timelines created or updated before DOM attachment resynchronize layout after mount so label compaction and content width use real viewport dimensions
Low-level Rail Primitives
The original rail helpers remain shipped for custom primitive compositions:
SF.rail.createHeader(config)→HTMLElementSF.rail.createCard(config)→{el, rail, addBlock, clearBlocks, setSolving, setUnassigned}SF.rail.addBlock(rail, config)→HTMLElementSF.rail.addChangeover(rail, config)→HTMLElementSF.rail.createHeatmap(config)→HTMLElement | nullSF.rail.createUnassignedRail(tasks, onTaskClick)→HTMLElement
Use these when you need bespoke resource-card layouts rather than the canonical dense timeline surface.
Gantt API
SF.gantt.create(config) returns:
el,mount,setTasks,refresh,changeViewMode,highlightTask,getChart,destroy
Gantt Example
var gantt = SF.gantt.create({
gridTitle: "Tasks",
chartTitle: "Schedule",
viewMode: "Quarter Day",
splitSizes: [40, 60],
columns: [
{ key: "name", label: "Task", sortable: true },
{ key: "start", label: "Start", sortable: true },
{ key: "end", label: "End", sortable: true },
],
onTaskClick: function (task) {
console.log("clicked", task.id);
},
onDateChange: function (task, start, end) {
console.log("moved", task.id, start, end);
},
});
gantt.mount("my-container");
gantt.setTasks([
{
id: "task-1",
name: "Design review",
start: "2026-03-15 09:00",
end: "2026-03-15 10:30",
priority: 1,
projectIndex: 0,
pinned: true,
custom_class: "project-color-0 priority-1",
dependencies: "",
},
]);
gantt.changeViewMode("Day");
gantt.highlightTask("task-1");
View modes include Quarter Day, Half Day, Day, Week, and Month.
Sortable headers are opt-in per column.
Choosing Timeline, Rail, or Gantt
Use timeline when operators need overview clustering, inline detail inspection, exact interval geometry, synchronized time navigation, and dense lane scanning.
Use the low-level rail primitives when you are composing custom resource cards, gauges, and changeover-aware lanes by hand.
Use Gantt when analysts need sortable task grids, dependency arrows, and high-density timeline review.