85 lines
3.2 KiB
Markdown
85 lines
3.2 KiB
Markdown
# 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)
|
||
|
||
### Recommended approach for d3 heatmap
|
||
**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:
|
||
```twig
|
||
{% 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.
|