Resource load & folded axis
A field-service quarter with a load strip under the chart: one row per crew, one bar per column, red where a crew is booked past its capacity - the specialist is one person with two sign-offs in the same week. Capacity is per resource, so a two-van crew takes two jobs at once. Fold the weekends out and the quarter fits in the width a month used to take. (requires @svgrid/enterprise)
A live, editable Svelte 5 data grid example from the SvGrid gallery (Gantt). See the SvGrid documentation for the full API.
About this example
Resource load and a folded timeline on the Svelte 5 data grid's Gantt view. A resource field says who is on each task and the histogram under the chart sums them per column, one row per crew, turning red where a crew is booked past its capacity - so a double-booked specialist is visible without reading a date. Capacity is per resource, so a two-van crew takes two jobs at once while one person does not. Folding weekends out of the axis leaves a hatched gap where each one was and fits a quarter in the width a month used to take, with working days keeping their real size.
A field-service quarter, where the question is not "when" but "who, and are they double-booked":
- **Resource histogram.**
resourceFieldsays who is on each task andresourceHistogramsums them into a strip under the chart: one row per crew, one bar per column, counting the jobs that touch it. Anything past that crew's capacity turns red, so a clash is visible without reading a single date. - **Capacity per resource.** The two-van crew can take two jobs at once; the specialist cannot. Same strip, different ceiling.
- **A folded axis.**
collapseWeekendstakes the Saturdays and Sundays out of the timeline and leaves a hatched gap where each one was. The quarter then fits in the width a month used to take, and the working days keep their real size - nothing is squashed.
Drag a job onto a week that is already full and watch the strip go red under it.
Imports, features and API used
Imports: @svgrid/grid, @svgrid/enterprise
Columns: name (Job), site (Site), crew (Crew), start (Start), end (Finish)
Frequently asked questions
What does a bar in the load strip count?
How many of that resource's tasks overlap the column. On a day or week axis that is concurrency: three bars over Tuesday reads 3. Only leaves are counted, so a phase does not book its owner twice for its children's work.
How is a resource's capacity set?
Name a field on each resource with the histogram's capacityField and give it a number; anything above it is drawn red. Without one every resource takes one task at a time.
What does folding weekends out actually change?
Only the axis. Each run of non-working days shrinks to a narrow hatched marker instead of two empty columns, while working days keep their normal width, so a long plan fits on screen. A task that does run over a folded weekend still draws across it.
Related documentation
Related articles
- Lazy-Loading Master-Detail Content in SvGrid - Fetch detail-panel data only when a row is expanded, cache the results, and cancel abandoned requests - keeping a large grid fast without paying for panels no one views.
- Empty, Loading, and Error States for a Svelte Data Grid - How to handle the three non-data states in a Svelte grid - empty (with two subtypes), skeleton loading that does not flash, and recoverable errors that keep existing rows visible.
Source code (477-gantt-resources.svelte)
<script lang="ts">
/**
* 477. Resource load & a folded axis (Enterprise Gantt Pro)
* ---------------------------------------------------------
* A field-service quarter, where the question is not "when" but
* "who, and are they double-booked":
*
* - **Resource histogram.** `resourceField` says who is on each task and
* `resourceHistogram` sums them into a strip under the chart: one row per
* crew, one bar per column, counting the jobs that touch it. Anything
* past that crew's capacity turns red, so a clash is visible without
* reading a single date.
* - **Capacity per resource.** The two-van crew can take two jobs at once;
* the specialist cannot. Same strip, different ceiling.
* - **A folded axis.** `collapseWeekends` takes the Saturdays and Sundays
* out of the timeline and leaves a hatched gap where each one was. The
* quarter then fits in the width a month used to take, and the working
* days keep their real size - nothing is squashed.
*
* Drag a job onto a week that is already full and watch the strip go red
* under it.
*/
import { SvGrid, type ColumnDef } from '@svgrid/grid'
import { enableGanttView, type GanttProConfig } from '@svgrid/enterprise'
enableGanttView()
type Job = {
id: string
name: string
site: string
crew: string
start: string
end: string
progress: number
parentId: string | null
color?: string
}
const pad = (n: number) => String(n).padStart(2, '0')
const iso = (d: Date) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
const base = new Date()
base.setHours(0, 0, 0, 0)
base.setDate(base.getDate() - ((base.getDay() + 6) % 7))
const at = (d: number) => {
const x = new Date(base)
x.setDate(x.getDate() + d)
return iso(x)
}
const CREW = { north: '#2563eb', south: '#0891b2', spec: '#b45309' } as const
// Two regional crews and one specialist. The specialist is deliberately
// booked twice over in week three - that is the red bar.
let rows = $state<Job[]>([
{ id: 'r1', name: 'North region', site: '', crew: '', start: at(0), end: at(24), progress: 0, parentId: null, color: CREW.north },
{ id: 'n1', name: 'Millbrook swap-out', site: 'Millbrook', crew: 'North crew', start: at(0), end: at(4), progress: 100, parentId: 'r1', color: CREW.north },
{ id: 'n2', name: 'Fenwick upgrade', site: 'Fenwick', crew: 'North crew', start: at(7), end: at(13), progress: 60, parentId: 'r1', color: CREW.north },
{ id: 'n3', name: 'Ashfield service', site: 'Ashfield', crew: 'North crew', start: at(10), end: at(17), progress: 20, parentId: 'r1', color: CREW.north },
{ id: 'n4', name: 'Millbrook follow-up', site: 'Millbrook', crew: 'North crew', start: at(21), end: at(24), progress: 0, parentId: 'r1', color: CREW.north },
{ id: 'r2', name: 'South region', site: '', crew: '', start: at(3), end: at(31), progress: 0, parentId: null, color: CREW.south },
{ id: 's1', name: 'Halston install', site: 'Halston', crew: 'South crew', start: at(3), end: at(10), progress: 80, parentId: 'r2', color: CREW.south },
{ id: 's2', name: 'Bracken retrofit', site: 'Bracken', crew: 'South crew', start: at(14), end: at(24), progress: 10, parentId: 'r2', color: CREW.south },
{ id: 's3', name: 'Halston handover', site: 'Halston', crew: 'South crew', start: at(28), end: at(31), progress: 0, parentId: 'r2', color: CREW.south },
{ id: 'r3', name: 'Commissioning', site: '', crew: '', start: at(4), end: at(30), progress: 0, parentId: null, color: CREW.spec },
{ id: 'c1', name: 'Millbrook sign-off', site: 'Millbrook', crew: 'Specialist', start: at(4), end: at(5), progress: 100, parentId: 'r3', color: CREW.spec },
// These two overlap, and the specialist is one person.
{ id: 'c2', name: 'Fenwick sign-off', site: 'Fenwick', crew: 'Specialist', start: at(14), end: at(17), progress: 0, parentId: 'r3', color: CREW.spec },
{ id: 'c3', name: 'Halston sign-off', site: 'Halston', crew: 'Specialist', start: at(15), end: at(18), progress: 0, parentId: 'r3', color: CREW.spec },
{ id: 'c4', name: 'Bracken sign-off', site: 'Bracken', crew: 'Specialist', start: at(28), end: at(30), progress: 0, parentId: 'r3', color: CREW.spec },
])
const columns: ColumnDef<any, Job>[] = [
{ field: 'name', header: 'Job', width: 190 },
{ field: 'site', header: 'Site', width: 100 },
{ field: 'crew', header: 'Crew', width: 110 },
]
// The table view shows the dates the bars are drawn from.
const tableColumns: ColumnDef<any, Job>[] = [
...columns,
{ field: 'start', header: 'Start', width: 110 },
{ field: 'end', header: 'Finish', width: 110 },
]
let folded = $state(true)
function writeSpan(row: Job, start: Date, end: Date) {
row.start = iso(start)
const last = new Date(end.getTime() - 86400000)
row.end = iso(last < start ? start : last)
}
const cfg: GanttProConfig<any, Job> = $derived({
startField: 'start',
endField: 'end',
titleField: 'name',
progressField: 'progress',
parentField: 'parentId',
colorField: 'color',
tableColumns: ['name', 'crew', '__duration'],
zoom: 'week',
weekStartsOn: 1,
// --- the resource layer -------------------------------------------------
resourceField: 'crew',
resources: [
// The north crew runs two vans, so two jobs at once is fine.
{ id: 'North crew', title: 'North crew', capacity: 2 } as never,
{ id: 'South crew', title: 'South crew', capacity: 1 } as never,
{ id: 'Specialist', title: 'Specialist', capacity: 1 } as never,
],
resourceHistogram: { capacityField: 'capacity', height: 96 },
// --- the folded axis ----------------------------------------------------
collapseWeekends: folded,
collapsedGapPx: 10,
editable: true,
tooltip: true,
onTaskMove: (e) => {
writeSpan(e.row, e.start, e.end)
for (const s of e.subtree ?? []) writeSpan(s.row, s.start, s.end)
},
onTaskResize: (e) => writeSpan(e.row, e.start, e.end),
})
// The same rows as a plain table: the Gantt is one view of the grid.
let view = $state<'gantt' | 'table'>('gantt')
</script>
<section class="rl">
<header class="rl-head">
<div class="rl-title">
<strong>Field service, this quarter</strong>
<span class="rl-sub">
The strip under the chart counts each crew's jobs per column. Red is past
what that crew can take - the specialist is one person with two sign-offs
in the same week.
</span>
</div>
<label class="rl-toggle">
<input type="checkbox" bind:checked={folded} />
Fold weekends out
</label>
<div class="rl-seg" role="tablist" aria-label="View">
<button class="rl-seg-btn" role="tab" aria-selected={view === 'gantt'} class:rl-on={view === 'gantt'} onclick={() => (view = 'gantt')}>Gantt</button>
<button class="rl-seg-btn" role="tab" aria-selected={view === 'table'} class:rl-on={view === 'table'} onclick={() => (view = 'table')}>Table</button>
</div>
</header>
<div class="rl-body">
{#if view === 'gantt'}
<SvGrid
columnResize
data={rows}
columns={columns}
getRowId={(r) => r.id}
containerHeight="100%"
gantt={cfg}
/>
{:else}
<SvGrid columnResize data={rows} columns={tableColumns} getRowId={(r) => r.id} containerHeight="100%" sortable fitColumns />
{/if}
</div>
</section>
<style>
.rl {
display: flex;
flex: 1 1 auto;
flex-direction: column;
min-height: 0;
border: 1px solid var(--sg-border, #e5e7eb);
border-radius: 12px;
overflow: hidden;
background: var(--sg-bg, #fff);
}
.rl-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 14px;
padding: 10px 14px;
border-bottom: 1px solid var(--sg-border, #e5e7eb);
}
.rl-title { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.rl-seg { display: inline-flex; flex: none; border: 1px solid var(--sg-border, #e5e7eb); border-radius: 8px; overflow: hidden; }
.rl-seg-btn { padding: 5px 12px; border: 0; background: transparent; color: inherit; font: inherit; font-size: 0.8rem; cursor: pointer; }
.rl-seg-btn:hover { background: color-mix(in srgb, var(--sg-fg, #1f2937) 6%, transparent); }
.rl-on { background: var(--sg-accent, #4f46e5); color: #fff; }
.rl-on:hover { background: var(--sg-accent, #4f46e5); }
.rl-sub { font-size: 0.78rem; color: var(--sg-muted, #6b7280); }
.rl-toggle {
display: inline-flex;
flex: none;
align-items: center;
gap: 6px;
padding-top: 3px;
font-size: 0.78rem;
color: var(--sg-fg, #374151);
white-space: nowrap;
cursor: pointer;
}
.rl-body { flex: 1 1 auto; min-height: 0; padding: 8px; }
@media (max-width: 767px) {
.rl { min-width: 860px; flex-shrink: 0; }
}
</style>More Gantt examples
- Project plan - A 14-week release as a Gantt: phases nest their tasks and draw a rolled-up summary bar, finish-to-start links draw as arrows, and a violated one turns red. Collapse a phase and its summary keeps the span. Toggle to the Table - same grid rows, just a view. The renderer ships in @svgrid/enterprise.
- Plan editing - The same plan with editing on: drag a bar or its edges, drag the diamond to set percent, drag a dot onto another bar to draw a link. A phase moves its whole subtree in one callback, successors slide forward over weekends, a cycle is refused, and Ctrl+Z replays it backwards. A log panel lists every write-back the view asks for.
- Critical path & baselines - A fit-out schedule with the planning layer on: the chain with no slack ringed in red with its arrows, a Slack column for everything else, baseline ghosts that redden where the plan has drifted, and two pinned tasks where a cascade stops at the constraint instead of running past it.
- One plan, four views: Grid, Gantt, Scheduler, Kanban - One <SvGrid>, one array, four renderers behind a switch: the plain table with inline editing, the Gantt with phases and links, a resource timeline with a lane per owner, and a Kanban of status lanes. Every view writes back through a callback to the same rows - change a status in the grid and the card moves lane, drag a bar in the Gantt and the timeline follows - and a line under the switcher names the last write.
- Product roadmap - a year in quarters - The Gantt as a roadmap: three teams, their initiatives across four quarters, and the releases that cut across them as milestones. A `task` snippet draws the owner and a status chip inside each bar, colour is by status, a custom tooltip carries the effort and a summary, the header chips filter by status, and a month / quarter / year zoom ladder drills in with Ctrl+wheel.