3.2 KiB
Phase 3 Research: Core Heatmap Rendering
Researched: 2026-04-08
Kimai Widget Asset Serving
How existing widgets load JS
- Kimai widgets use inline
<script>tags in their Twig templates - JS fires on
kimai.initializedevent (or immediately ifkimai_context.javascriptRequestis true) - Chart.js widgets (DailyWorkingTimeChart, YearChart) use global
Chartobject loaded via Encore - No widget uses
<script type="module">— all use plain<script type="text/javascript">
Plugin asset path
- Plugin
Resources/public/→public/bundles/kimaiheatmap/viabin/console assets:install --symlink - Accessible in Twig:
asset('heatmap.js', 'KimaiHeatmap')— but lowercase bundle name in path - Alternative: inline the bundled JS directly in the template (simpler, avoids asset path issues)
Recommended approach for d3 heatmap
Bundle d3 modules with esbuild into a single IIFE file, then either:
- Serve from
Resources/public/heatmap.jsvia asset symlink +<script>tag - Or inline the compiled JS in the template
Option 1 is cleaner. The esbuild output should be an IIFE (not ESM) since Kimai templates use plain script tags.
kimai_context.javascriptRequest
Boolean flag set by Kimai. When the dashboard reloads via AJAX (e.g., GridStack widget repositioning), this is true. The pattern:
{% if kimai_context.javascriptRequest %}
renderHeatmap();
{% else %}
document.addEventListener('kimai.initialized', renderHeatmap);
{% endif %}
Widget template variables
When render_widget(widget) is called, the template receives:
widget— the widget instancetitle— fromwidget.getTitle()data— fromwidget.getData()options— fromwidget.getOptions()
card.html.twig embed blocks
box_title— card header textbox_body— main content areabox_footer— optional footerbox_tools— header action buttonsbox_header— full header override
Dashboard initialization events
kimai.initialized— main app readydashboard.initialized— GridStack layout ready (only in grid.html.twig)
d3 Calendar Heatmap Pattern
Grid layout
- 53 columns (weeks) × 7 rows (days, Mon-Sun)
- Cell size: ~12-14px square with 2px gap
- Total width: ~800px, fits full-width widget
Color scale
Use d3.scaleSequential with interpolator mapped to CSS variables:
- 0 hours:
var(--heatmap-empty)(light gray / dark theme equivalent) - Low: light green
- High: dark green
- Use Kimai's
--bs-successas base, generate lighter/darker variants
Tooltip
HTML div positioned absolutely near the hovered cell. Show date, hours, entry count.
Month labels
Detect when week crosses month boundary. Place label at first week of each month.
Day-of-week labels
Mon, Wed, Fri on Y-axis (standard GitHub heatmap pattern — skip Tue/Thu/Sat/Sun for space).
Testing Strategy
Vitest + jsdom
- d3-selection works with jsdom's DOM
- Test SVG output: correct number of
<rect>elements, color attributes, tooltip content - Snapshot test for overall structure
- Unit test color scale mapping
Test data
Generate mock API response matching { days: [{date, hours, count}], range: {begin, end} } format.