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:
- Mode switcher UI -- Tabler
nav-segmented, initially Year (existing) + Week modes - Week-mode (day-of-week) -- Client-side aggregation of existing daily data, no backend changes, proves the multi-mode rendering architecture
- Hours/count toggle -- Small segmented control, applies to all modes, low effort
- 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
KimaiContainerDI (forgetPlugin('api')) - Requires
KimaiFormPluginbase 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 (
parentTitlefor 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/ActivityTypeform 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
- Tabler segmented control: https://docs.tabler.io/ui/components/segmented-control
- GitHub PunchCard visualization: https://github.com/vnau/punchcard
- Segmented control UX best practices: https://mobbin.com/glossary/segmented-control
- Apple HIG segmented controls: https://developer.apple.com/design/human-interface-guidelines/segmented-controls
- d3 heatmap patterns: https://d3-graph-gallery.com/heatmap.html
- Kimai form select cascading: local
dev/kimai/assets/js/forms/KimaiFormSelect.js(lines 386-447) - Kimai API routes:
get_projectsatsrc/API/ProjectController.php:56,get_activitiesatsrc/API/ActivityController.php:54 - Kimai SelectWithApiDataExtension: local
dev/kimai/src/Form/Extension/SelectWithApiDataExtension.php - TomSelect: https://tom-select.js.org/