---
phase: 07-mode-switcher-week-mode
plan: 02
type: execute
wave: 2
depends_on: [07-01]
files_modified:
- assets/src/renderers/week.ts
- assets/src/heatmap.ts
- assets/test/week.test.ts
autonomous: false
requirements: [VIZ-02, TEST-01]
must_haves:
truths:
- "Switching to week mode renders 7 horizontal cells colored by aggregated metric"
- "Day labels (Mon-Sun or Sun-Sat) respect user's start-of-week preference"
- "Tooltip on week cell shows full weekday name + aggregated value"
- "Weekdays with zero tracked time render as empty cells (not lowest green)"
- "Switching back to year mode restores the full calendar heatmap"
artifacts:
- path: "assets/src/renderers/week.ts"
provides: "WeekModeRenderer implementing ModeRenderer"
exports: ["WeekModeRenderer"]
min_lines: 60
- path: "assets/test/week.test.ts"
provides: "Tests for week renderer aggregation and rendering"
min_lines: 50
key_links:
- from: "assets/src/heatmap.ts"
to: "assets/src/renderers/week.ts"
via: "import + registerRenderer"
pattern: "registerRenderer.*WeekModeRenderer"
- from: "assets/src/renderers/week.ts"
to: "assets/src/shared/color-scale.ts"
via: "buildColorScale for week cell fill"
pattern: "buildColorScale"
- from: "assets/src/renderers/week.ts"
to: "assets/src/shared/tooltip.ts"
via: "createTooltip/showTooltip/hideTooltip"
pattern: "createTooltip"
---
Implement the WeekModeRenderer that aggregates DayEntry data by weekday and renders a 7-cell horizontal heatmap, then wire it into the orchestrator.
Purpose: Delivers the first new visualization mode -- users can see which weekdays are busiest at a glance.
Output: Working week-mode renderer with tooltip, registered and dispatch-ready, with tests.
@/home/toph/code/toph/kimai-heatmap/.claude/get-shit-done/workflows/execute-plan.md
@/home/toph/code/toph/kimai-heatmap/.claude/get-shit-done/templates/summary.md
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/07-mode-switcher-week-mode/07-CONTEXT.md
@.planning/phases/07-mode-switcher-week-mode/07-RESEARCH.md
@.planning/phases/07-mode-switcher-week-mode/07-UI-SPEC.md
@.planning/phases/07-mode-switcher-week-mode/07-01-SUMMARY.md
From assets/src/renderers/types.ts:
```typescript
export interface RenderContext {
container: HTMLElement;
data: HeatmapData;
state: HeatmapState;
config: HeatmapConfig;
onCellClick?: (dateStr: string) => void;
emptyMessage?: string;
}
export interface ModeRenderer {
readonly mode: string;
render(ctx: RenderContext): void;
destroy?(): void;
}
```
From assets/src/types.ts:
```typescript
export interface DayEntry {
date: string; // "YYYY-MM-DD"
hours: number;
count: number;
}
export type DisplayMetric = 'hours' | 'count';
```
From assets/src/state.ts:
```typescript
export interface HeatmapState {
mode: HeatmapMode;
metric: DisplayMetric;
filters: FilterState;
weekStart: string;
data: HeatmapData | null;
}
```
From assets/src/shared/color-scale.ts:
```typescript
export function buildColorScale(days: DayEntry[], metric?: DisplayMetric): ScaleQuantize;
```
From assets/src/shared/tooltip.ts:
```typescript
export function createTooltip(): HTMLDivElement;
export function showTooltip(tip: HTMLDivElement, html: string, anchorRect: DOMRect, cellSize: number): void;
export function hideTooltip(tip: HTMLDivElement): void;
```
From assets/src/shared/date-utils.ts:
```typescript
export function getDayLabels(weekStart: string): string[];
// DAY_LABELS_MONDAY = ['Mon', '', 'Wed', '', 'Fri', '', '']
// DAY_LABELS_SUNDAY = ['Sun', '', 'Tue', '', 'Thu', '', 'Sat']
```
From assets/src/renderers/registry.ts:
```typescript
export function registerRenderer(renderer: ModeRenderer): void;
```
Existing registration pattern in heatmap.ts:
```typescript
import { YearModeRenderer } from './renderers/year';
registerRenderer(new YearModeRenderer());
```
Task 1: Implement WeekModeRenderer and register in orchestrator
assets/src/renderers/week.ts,
assets/src/heatmap.ts,
assets/test/week.test.ts
assets/src/renderers/year.ts,
assets/src/renderers/types.ts,
assets/src/renderers/registry.ts,
assets/src/shared/color-scale.ts,
assets/src/shared/tooltip.ts,
assets/src/shared/date-utils.ts,
assets/src/heatmap.ts,
assets/src/types.ts,
assets/src/state.ts,
.planning/phases/07-mode-switcher-week-mode/07-UI-SPEC.md
- WeekModeRenderer.mode equals 'week'
- render() with 3 DayEntries (Mon, Mon, Wed) aggregates: Monday totalHours = sum of both Mon entries, Wednesday totalHours = Wed entry hours
- render() produces an SVG with exactly 7 rect elements
- Rect for a weekday with data has a fill color from buildColorScale (not empty fill)
- Rect for a weekday with no data has class 'heatmap-empty'
- Labels respect weekStart: Monday-start first label is 'Mon', Sunday-start first label is 'Sun'
- Tooltip on hover shows full weekday name + aggregated value (e.g. "Monday: 8.5h (4 entries)")
- Tooltip for count metric shows "Monday: 4 entries (8.5h)" (count first)
- Empty weekday tooltip shows "Tuesday: No tracked time"
- destroy() removes tooltip element
**1. Create `assets/src/renderers/week.ts`:**
Define a local `WeekdayAggregate` interface:
```typescript
interface WeekdayAggregate {
dayIndex: number; // 0-6, relative to weekStart
label: string; // Full name: "Monday", "Tuesday", etc.
shortLabel: string; // Short: "Mon", "Tue", etc.
totalHours: number;
totalCount: number;
dayCount: number; // number of distinct days with entries
}
```
Weekday name arrays (for tooltip -- full names per D-07):
```typescript
const WEEKDAY_NAMES_MONDAY = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
const WEEKDAY_NAMES_SUNDAY = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const WEEKDAY_SHORT_MONDAY = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
const WEEKDAY_SHORT_SUNDAY = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
```
`aggregateByWeekday(days: DayEntry[], weekStart: string): WeekdayAggregate[]`:
- Create 7 buckets, indexed 0-6
- For each day entry: parse date, get JS day (`new Date(d.date + 'T00:00:00').getDay()`), compute index using `weekStart === 'sunday' ? jsDay : (jsDay + 6) % 7`
- Accumulate totalHours, totalCount, dayCount into bucket
- Set label from WEEKDAY_NAMES array by index, shortLabel from WEEKDAY_SHORT array
- Return all 7 buckets
`export class WeekModeRenderer implements ModeRenderer`:
- `readonly mode = 'week'`
- Private `tooltip: HTMLDivElement | null = null`
`render(ctx: RenderContext)`:
- Clear container: `ctx.container.innerHTML = ''`
- Handle empty data (same pattern as YearModeRenderer -- show emptyMessage or "No tracking data available")
- Destroy previous tooltip, create new one via `createTooltip()`
- Call `aggregateByWeekday(ctx.data.days, ctx.state.weekStart)`
- Build color scale: only pass DayEntry items that have data to `buildColorScale()` so scale domain reflects actual values. Create synthetic DayEntry array from aggregates for scale: `aggregates.filter(a => a.dayCount > 0).map(a => ({ date: '', hours: a.totalHours, count: a.totalCount }))`
- Then call `buildColorScale(syntheticDays, ctx.state.metric)`
- SVG layout constants (per UI-SPEC): cellWidth=60, cellHeight=40, cellGap=4, labelWidth=50
- SVG dimensions: width = `labelWidth + 7 * (cellWidth + cellGap)`, height = cellHeight
- Create SVG via d3 `select(ctx.container).append('svg')` with computed dimensions, class `heatmap-svg`
- Render 7 labels as `` elements: class `heatmap-label`, x=0, y = vertically centered in cell (`cellHeight / 2 + 3`), text = shortLabel for each aggregate. Show ALL 7 labels (not the sparse pattern from year-mode)
- Render 7 rects: x = `labelWidth + i * (cellWidth + cellGap)`, y = 0, width = cellWidth, height = cellHeight
- Class: `heatmap-week-cell` always, add `heatmap-empty` if `dayCount === 0`
- Fill: if `dayCount === 0`, return `''` (CSS handles empty fill). Otherwise, use `colorScale(ctx.state.metric === 'hours' ? agg.totalHours : agg.totalCount)`
- rx="2" ry="2" for rounded corners
- Tooltip on mouseenter: if `dayCount > 0`:
- hours metric: `"{label}
{totalHours.toFixed(1)}h ({totalCount} entries)"`
- count metric: `"{label}
{totalCount} entries ({totalHours.toFixed(1)}h)"`
- If `dayCount === 0`: `"{label}
No tracked time"`
- Use `showTooltip(tooltip, html, rect.getBoundingClientRect(), cellWidth)`
- On mouseleave: `hideTooltip(tooltip)`
- No click handler on week cells (per discretion -- aggregation has no single-date target)
`destroy()`:
- Remove tooltip if exists, set to null
Export the class.
**2. Modify `assets/src/heatmap.ts`:**
- Add import: `import { WeekModeRenderer } from './renderers/week';`
- Add registration below existing: `registerRenderer(new WeekModeRenderer());`
- That's the only change needed in heatmap.ts for this task (controls wiring done in Plan 01)
**3. Create `assets/test/week.test.ts`:**
Test the WeekModeRenderer using the pattern from existing test files. Import the renderer, create a container div, build test DayEntry arrays with known dates and values. Verify:
- SVG contains exactly 7 rect elements
- Aggregation sums correctly (two entries on same weekday -> combined hours)
- Empty weekdays get `heatmap-empty` class
- Data weekdays get a fill color (not empty string)
- Labels appear in correct weekStart order
- destroy() removes tooltip
- Test with weekStart='sunday' to verify label order changes
cd /home/toph/code/toph/kimai-heatmap && npx vitest run assets/test/week.test.ts && npm test
- assets/src/renderers/week.ts contains `export class WeekModeRenderer implements ModeRenderer`
- assets/src/renderers/week.ts contains `readonly mode = 'week'`
- assets/src/renderers/week.ts contains `aggregateByWeekday`
- assets/src/renderers/week.ts contains `buildColorScale`
- assets/src/renderers/week.ts contains `createTooltip`
- assets/src/renderers/week.ts contains `showTooltip` and `hideTooltip`
- assets/src/renderers/week.ts contains `No tracked time`
- assets/src/renderers/week.ts contains `heatmap-week-cell`
- assets/src/renderers/week.ts contains `heatmap-empty`
- assets/src/renderers/week.ts contains `(jsDay + 6) % 7` (Monday-start weekday index)
- assets/src/heatmap.ts contains `import { WeekModeRenderer } from './renderers/week'`
- assets/src/heatmap.ts contains `registerRenderer(new WeekModeRenderer())`
- assets/test/week.test.ts exists and `npx vitest run assets/test/week.test.ts` exits 0
- `npm test` exits 0 (all tests pass including new week tests)
- `npm run build:dev` exits 0 (bundle builds)
WeekModeRenderer aggregates DayEntry data by weekday, renders 7 horizontal SVG cells with color scale and tooltips, respects weekStart preference, and is registered for dispatch. All tests pass.
Task 2: Visual verification of mode switcher and week mode
none
Human visual verification of the complete Phase 7 feature set in a running Kimai instance. No code changes -- verify that Plan 01 (controls) and Plan 02 Task 1 (week renderer) work correctly together end-to-end.
cd /home/toph/code/toph/kimai-heatmap && npm test && npm run build:dev
Human confirms: mode switcher toggles year/week views, metric toggle switches hours/count coloring, week-mode shows 7 cells with correct tooltips, stats row hidden in week mode, no console errors.
Mode switcher (Year/Week) and metric toggle (Hours/Count) controls in the widget header. Week-mode renderer showing 7 weekday cells with aggregated data. Stats row hidden in week mode, visible in year mode.
1. Start local Kimai dev environment, navigate to dashboard
2. Verify the heatmap widget shows two controls in the header: "Year | Week" and "Hours | Count"
3. Click "Week" -- should show 7 horizontal cells with weekday labels (Mon-Sun), stats row should disappear
4. Hover over a colored cell -- tooltip shows full weekday name + aggregated hours and entry count
5. Hover over an empty cell -- tooltip shows "No tracked time"
6. Click "Count" -- cell colors should change (re-colored by entry count instead of hours)
7. Click "Year" -- should return to full calendar heatmap, stats row reappears
8. Click "Count" in year mode -- year cells re-color by count
9. Change project filter -- both modes should reflect filtered data
10. Check browser console -- no JavaScript errors
Type "approved" or describe issues
## Trust Boundaries
No new trust boundaries. Week renderer consumes the same authenticated data as year renderer. No new endpoints, inputs, or data flows.
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-07-02 | T (Tampering) | weekday aggregation | accept | Client-side aggregation of already-authenticated data; no security impact |
- `npx vitest run assets/test/week.test.ts` passes
- `npm test` passes (full suite)
- `npm run build:dev` succeeds
- Human verifies visual output in running Kimai
- Week-mode renders 7 horizontal cells colored by aggregated hours or count
- Day labels respect start-of-week preference
- Tooltips show weekday name + value in correct format for hours/count metrics
- Empty weekdays render as empty cells (not lowest green)
- Mode switching between year and week works without errors
- Metric toggle affects both modes