kimai-plugin-heatmap/.planning/research/FEATURES.md

12 KiB

Feature Landscape

Domain: Time-tracking heatmap dashboard widget (v1.1 milestone -- modes, filtering, toggles) Researched: 2026-04-08

Table Stakes

Features that users of a multi-mode heatmap widget would expect once modes are advertised. Missing any of these makes the feature set feel unfinished.

Feature Why Expected Complexity Notes
Mode switcher UI Users need a way to switch views. Without it, modes are invisible. Low Use Tabler's nav-segmented component -- it exists specifically for toggling views within the same context. Maps to data-bs-toggle="tab". Max 4-5 segments.
Year-view heatmap (existing) Already shipped in v1.0. Must remain default mode. Done Existing renderHeatmap() in heatmap.ts
Day-of-week aggregation (week mode) Standard "punchcard" analysis -- which weekdays are busiest. Every time-tracking analytics tool offers this. Medium Aggregate existing DayEntry[] client-side by Date.getDay(). No new backend query needed -- sum/average existing daily data by weekday. 7-row or 7-column chart with color intensity.
Hours vs entry-count toggle The data already has both hours and count fields. Users expect to choose which metric colors the heatmap. Low Toggle the colorScale domain between hours and count. A small segmented control or pair of radio buttons. Applies to all modes.
Cascading entity pickers (customer -> project -> activity) Kimai users see TomSelect pickers everywhere else in the app. A plain <select> feels foreign. Once activity filtering is promised, the cascade is expected. Medium-High See Complexity Notes below for full analysis of integration approach.
Activity filtering Descoped from v1.0. Now the primary reason for the entity picker upgrade. Users who filter by project will expect activity filtering too. Medium Backend: add ?activity=N param to HeatmapController::data(), add AND t.activity = :activity clause to HeatmapService::getDailyAggregation(). Frontend: wire activity picker into fetch URL. Kimai API route get_activities already accepts ?project=N for cascading.
Persistent filter/mode state Switching modes or filters then refreshing should not reset everything. Low Use URL query params (shareable, bookmarkable) or localStorage. URL params preferred.

Differentiators

Features that elevate this from "yet another heatmap" to a genuinely useful time-analysis tool. Not expected, but valued.

Feature Value Proposition Complexity Notes
Time-of-day heatmap (day mode) Shows when during the day work happens. Reveals morning-person vs night-owl patterns. Uncommon in time-tracking tools. High Requires new backend query: GROUP BY HOUR(t.begin) for hour-level aggregation. New data shape: HourEntry { hour: number, hours: number, count: number }. New d3 layout: 24-row/column grid. Needs new API response format or a mode query param.
Combined day/hour matrix Full punchcard: 7 rows (days) x 24 cols (hours). The GitHub punchcard view. Shows exactly when work happens across the week. High Backend: GROUP BY DAYOFWEEK(t.begin), HOUR(t.begin). New data shape: PunchcardEntry { day: number, hour: number, hours: number, count: number }. d3 layout: 7x24 grid with circle sizes or color intensity. Most complex visualization but highest insight value.
Customer-level filtering Filter heatmap by customer (one level above project). Useful for seeing client-specific patterns. Low The cascade handles this naturally: customer picker drives project picker via get_projects?customer=N. Backend HeatmapService needs ?customer=N param that joins through t.project.customer.
Color scale legend Small gradient bar showing what colors mean (0h to Xh). Low Standard d3 pattern. Horizontal gradient with tick labels. Applies to all modes.
Animated transitions between modes Smooth morphing when switching views (cells rearrange, fade, resize). Medium d3 transition() API. Key: use consistent key functions in .join() so d3 matches elements across renders. Polish, not critical.

Anti-Features

Things to explicitly NOT build in v1.1.

Anti-Feature Why Avoid What to Do Instead
Configurable date range selector Adds significant UI complexity (date pickers, presets). The fixed 1-year window works for personal use. Defer to v1.2+. If needed later, a simple "6mo / 1yr / 2yr" segmented control is sufficient.
Multi-user comparison This is a personal tracking tool, not a team dashboard. Out of scope per PROJECT.md.
Export/share heatmap as image No audience for a personal tool. SVG-to-PNG is fragile. Out of scope per PROJECT.md.
Custom color theme picker Kimai theme integration is sufficient. Adding a color picker adds options without insight. Keep hardcoded green scale (matches GitHub convention).
Real-time / auto-refresh Refresh-on-load is fine. WebSocket or polling adds complexity for no gain. Out of scope per PROJECT.md.
Full Kimai form plugin integration Kimai's KimaiFormPlugin lifecycle (activateForm/destroyForm) is designed for Symfony form pages, not standalone widgets. Coupling to it means depending on Kimai's internal JS architecture. Use TomSelect directly. Replicate only the cascading pattern. Style with Kimai's existing TomSelect CSS (already loaded globally). See Complexity Notes.
Drag-to-select date ranges Complex interaction, ambiguous purpose (filter? zoom? export?). Click-to-navigate to timesheet (existing) is sufficient.
Drill-down charts within widget Clicking a day to show a pie chart of projects adds major complexity. Click-through to Kimai's existing timesheet/report views instead.

Feature Dependencies

Customer picker -> Project picker -> Activity picker (cascade chain)
Activity picker -> Activity filter param in API (backend support)
Mode switcher UI -> All visualization modes (renders)
Hours/count toggle -> Color scale recalculation (applies to all modes)
Day mode (time-of-day) -> New backend aggregation query (HOUR grouping)
Combined matrix -> New backend aggregation query (DAY + HOUR grouping)
Week mode (day-of-week) -> NO new backend query (aggregate DayEntry[] client-side)

MVP Recommendation

Phase structure for v1.1:

Phase 1 -- Mode Switcher + Week Mode + Toggle:

  1. Mode switcher UI -- Tabler nav-segmented, initially Year (existing) + Week modes
  2. Week-mode (day-of-week) -- Client-side aggregation of existing daily data, no backend changes, proves the multi-mode rendering architecture
  3. Hours/count toggle -- Small segmented control, applies to all modes, low effort
  4. Rendering refactor -- Extract shared concerns (SVG container, color scale, tooltip) into base; modes provide layout strategy

Phase 2 -- Entity Pickers + Activity Filtering: 5. TomSelect entity pickers -- Replace plain <select> with TomSelect instances, wire cascading via Kimai's API routes (get_projects?customer=N, get_activities?project=N) 6. Activity filtering -- Backend ?activity=N param + frontend wiring 7. Customer filtering -- Backend ?customer=N param (join through project.customer)

Phase 3 -- Advanced Modes (defer or stretch): 8. Day mode (time-of-day) -- Needs new backend query + new data shape 9. Combined day/hour matrix -- Most complex layout, depends on hour-level backend data 10. Color scale legend -- Nice polish, do alongside advanced modes

Rationale: Phase 1 proves the multi-mode rendering architecture with zero backend changes. Phase 2 improves filtering UX with Kimai-native patterns and enables activity filtering. Phase 3 adds the data-intensive visualizations requiring new queries. Each phase delivers user-visible value independently.

Complexity Notes

TomSelect Integration (the hardest part that looks easy)

Kimai's TomSelect integration runs through a plugin lifecycle: KimaiFormPlugin -> KimaiFormTomselectPlugin -> KimaiFormSelect. The cascading logic in _activateApiSelects() (line 386-447 of KimaiFormSelect.js) listens for change events on selects with data-related-select attributes, fetches from data-api-url with form field interpolation (%fieldname% patterns), and updates the target select via _updateOptions() which dispatches data-reloaded events.

Why the plugin cannot use Kimai's system directly:

  • Requires Kimai's KimaiContainer DI (for getPlugin('api'))
  • Requires KimaiFormPlugin base class registration
  • URL interpolation (%fieldname%) assumes Symfony form field naming conventions
  • Lifecycle hooks (activateForm/destroyForm) expect to be called by Kimai's page init

Two integration approaches:

(a) Direct TomSelect instantiation (recommended):

  • Import TomSelect standalone (already loaded in Kimai's global JS)
  • Create <select> elements styled with Kimai's TomSelect CSS classes
  • Implement simplified cascading: on customer change, fetch('/api/projects?customer=N'), rebuild project options, trigger activity reload
  • Use same option grouping pattern (parentTitle for optgroups) as Kimai API responses
  • ~100-150 lines of cascading logic, much simpler than Kimai's generic system

(b) Symfony form fields in Twig template:

  • Render actual CustomerType/ProjectType/ActivityType form fields in the widget's Twig template
  • Let Kimai's existing JS auto-enhance them with TomSelect
  • Cleaner integration but requires investigation: do Kimai's form plugins activate inside widget Twig blocks?
  • Risk: widget Twig templates may not trigger Kimai's form initialization JS

Rendering Architecture Refactor

Current renderHeatmap() is tightly coupled to the year-view grid layout. Adding modes requires a strategy pattern:

  • Shared infrastructure: SVG container creation, color scale, tooltip, empty state, resize handler
  • Mode-specific: generateCells() / layout function, axis labels, dimensions
  • Extract into: createRenderer(mode) that returns the appropriate layout strategy
  • Each mode is a self-contained module: yearMode.ts, weekMode.ts, dayMode.ts, combinedMode.ts

Backend Aggregation Queries

Mode SQL Aggregation New Backend Code?
Year (existing) GROUP BY DATE(t.date) No
Week (day-of-week) Client-side aggregation of daily data No
Day (time-of-day) GROUP BY HOUR(t.begin) Yes -- new method in HeatmapService
Combined GROUP BY DAYOFWEEK(t.begin), HOUR(t.begin) Yes -- new method in HeatmapService

Note: day and combined modes need t.begin (timesheet start datetime), not t.date. Verify t.begin is available and contains time component in Kimai's Timesheet entity.

Sources