kimai-plugin-heatmap/.planning/milestones/v1.0-phases/phase-3/03-RESEARCH.md
Christopher Mühl 244c7c66fc
chore: archive v1.0 milestone
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 23:25:26 +02:00

3.2 KiB
Raw Permalink Blame History

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.initialized event (or immediately if kimai_context.javascriptRequest is true)
  • Chart.js widgets (DailyWorkingTimeChart, YearChart) use global Chart object 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/ via bin/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)

Bundle d3 modules with esbuild into a single IIFE file, then either:

  1. Serve from Resources/public/heatmap.js via asset symlink + <script> tag
  2. 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 instance
  • title — from widget.getTitle()
  • data — from widget.getData()
  • options — from widget.getOptions()

card.html.twig embed blocks

  • box_title — card header text
  • box_body — main content area
  • box_footer — optional footer
  • box_tools — header action buttons
  • box_header — full header override

Dashboard initialization events

  • kimai.initialized — main app ready
  • dashboard.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-success as 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.