The question a heatmap has to answer honestly
"Is anyone overloaded next week?" sounds like it should be simple to answer from a task list — count the tasks per person, done. But a task with an 8-hour estimate that's 75% complete doesn't have 8 hours of work left, and a task spanning three weeks doesn't belong entirely in the week it happens to be due. Get either of those wrong and the heatmap either cries wolf on people who are actually fine, or misses someone who's genuinely buried.
Step one: remaining work, not total work
WKFGo starts from what's actually left, not what was originally estimated:
remaining := task.EstimatedTime * (1 - task.Progress/100)
A task estimated at 20 hours and marked 75% done contributes 5 remaining hours to the heatmap, not 20. This single line is the difference between a heatmap that reflects reality and one that just re-displays your estimation table — progress tracking only matters here if it actually changes the number.
Step two: split across every assignee
If a task has two assignees, neither one carries the whole remaining estimate — it's divided evenly:
share := remaining / float64(len(visible))
This is a simplification (it assumes equal contribution, which isn't always true), but it's an honest one: the alternative — showing the full remaining estimate under every assignee — would double-count the same hours and make total team capacity look far worse than it is the moment more than one person shares a task.
Step three: spread across the weeks it actually spans
A task due in three weeks doesn't dump its whole remaining estimate into that one week — it's spread evenly across the span from when the work can start (now, or a future start date) to when it's due:
spanLen := endIdx - startIdx + 1
perWeek := share / float64(spanLen)
This is the part a naive "group by due date" heatmap gets wrong — it would show three quiet weeks followed by one alarming spike, when the honest picture is a steady load spread across all three. Overdue tasks are the one deliberate exception: their entire remaining share lands fully in the current week, because "overdue" already means the spreading window has collapsed to now.
Step four: turn hours into a signal, not just a number
Raw allocated hours only mean something next to a capacity baseline:
cell.Utilization = int(math.Round(cell.Allocated / capacity * 100))
Default weekly capacity is 40 hours per person, overridable per deployment via WORKLOAD_WEEKLY_CAPACITY — because not every team runs a 40-hour week, and hard-coding that assumption would silently mislabel every part-time contributor or four-day-week team as overloaded.
What gets excluded, and why that matters
The query behind this only pulls tasks that are still open — progress < 100 AND status <> 'done' — and only counts assignments not already marked done on the user-task level. A task someone finished their part of but that's still open for a co-assignee doesn't count against the person who's already done. And a task with no time estimate doesn't silently contribute zero to someone's load — it's tracked separately as an Unestimated count per person, specifically so "this person looks fine" doesn't quietly mean "this person has twelve tasks with no estimate that the math can't see."
Sort order is part of the design, not an afterthought
The heatmap sorts people by current week utilization, most-loaded first:
sort.SliceStable(hm.Users, func(i, j int) bool {
return hm.Users[i].Weeks[0].Utilization > hm.Users[j].Weeks[0].Utilization
})
The person you need to act on this week is the first row you see — the tool is built around "who needs attention right now," not an alphabetical roster you have to scan.
Common mistakes
Grouping remaining work entirely under a task's due date. This produces the false spike-then-quiet pattern that makes long tasks look deceptively harmless until the week they're due, when the work was actually happening the whole time.
Counting full task estimates regardless of progress. A heatmap that ignores progress is just an estimates table with extra steps — it won't reflect a team actually chipping away at its backlog.
Assuming a fixed 40-hour week for every team. Contractors, part-time staff, and four-day-week teams all need a different capacity baseline, or "overloaded" starts meaning nothing.
Silently treating unestimated tasks as zero load. That's not "no load," it's "we don't know" — collapsing the two into the same number hides exactly the tasks most likely to blow up unexpectedly.
FAQ
How far into the future does the heatmap look?
Configurable, clamped between 1 and 26 weeks — short enough to stay accurate (estimates further out are guesses anyway), long enough to see a hiring or reassignment decision coming.
Does a task with three assignees split three ways even if one person does most of the actual work?
Yes — the even split is a known simplification; it's the same tradeoff most lightweight capacity tools make rather than requiring per-assignee percentage estimates nobody wants to maintain.
What happens to a task's load the week after it's completed?
Nothing — the underlying query only pulls open tasks, so a completed task simply disappears from every future heatmap computation; there's no separate "clear the load" step needed.
Summary
A workload number is only useful if the arithmetic behind it matches how work actually happens — remaining, not total; split across who's actually on it; spread across when it's actually happening, not just when it's due. Skip any one of those and the heatmap still produces a number, it's just a number nobody should trust.