Switch activity graph to canvas#1366
Conversation
There was a problem hiding this comment.
Pull request overview
This PR replaces the signed-in Home “daily activity” grid (previously rendered as many <Link> elements) with a single <canvas> that draws the graph, reducing DOM node count and shifting styling to CSS custom properties.
Changes:
- Reimplemented
ActivityGraph.svelteto render a canvas-based heatmap and handle hover/click interactions in JS. - Moved activity “intensity” colors from per-cell CSS classes to
:rootCSS variables for canvas color sampling.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| app/javascript/pages/Home/signedIn/ActivityGraph.svelte | Replaces per-day link grid with canvas drawing + pointer/keyboard handlers. |
| app/assets/tailwind/main.css | Defines --activity-cell-* variables used by the canvas renderer. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Greptile SummaryThis PR replaces the activity graph's 365 individual
Confidence Score: 3/5The canvas rendering and CSS color refactor work correctly, but keyboard navigation is broken and there are two smaller correctness bugs in hit-detection and width calculation. The keyboard handler silently does nothing for keyboard-only users because hoveredDate can only be set via mouse movement, which is a real behavioral regression from the previous Link-per-day approach. The 1px hit-detection error and the negative-width edge case are lower-stakes but present defects in the changed code. app/javascript/pages/Home/signedIn/ActivityGraph.svelte needs attention, particularly the onKeydown handler and dateFromPointer logic. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Component Mount] --> B[effect drawGraph]
B --> C[Read graphWidth and graphHeight]
C --> D[Set canvas dimensions x devicePixelRatio]
D --> E[activityColors via getComputedStyle]
E --> F[Iterate dates array]
F --> G[intensityLevel per date]
G --> H[Set fillStyle from colors array]
H --> I[roundRect and fill]
I --> F
J[User moves pointer] --> K[onPointerMove]
K --> L[dateFromPointer modulo hit test]
L --> M{In cell?}
M -- Yes --> N[hoveredDate = date]
M -- No --> O[hoveredDate = null]
P[User presses Enter or Space] --> Q{hoveredDate set?}
Q -- Yes --> R[router.visit date param]
Q -- No --> S[Nothing happens - keyboard regression]
Prompt To Fix All With AIFix the following 3 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 3
app/javascript/pages/Home/signedIn/ActivityGraph.svelte:121-126
**Keyboard navigation broken for keyboard-only users**
The `onKeydown` handler only navigates if `hoveredDate` is non-null, but `hoveredDate` is exclusively set by `onPointerMove` (mouse/touch input). A keyboard-only user who tabs to the canvas will be unable to activate any date — pressing Enter or Space does nothing. The original `<Link>`-per-day DOM approach gave keyboard users individual focusable, navigable elements for free. This canvas approach needs a separate mechanism (e.g., tracking a `focusedIndex` state that arrow keys move through) to restore that capability.
### Issue 2 of 3
app/javascript/pages/Home/signedIn/ActivityGraph.svelte:103-104
Off-by-one in hit detection: `<= cellSize` includes the first pixel of the gap (pixel index `cellSize` is the start of the gap, not the end of the cell). With `cellSize = 12` and `cellGap = 4`, the stride is 16, so pixels 0–11 are in the cell and pixel 12 starts the gap. The check should be `< cellSize`.
```suggestion
const inCellX = x % (cellSize + cellGap) < cellSize;
const inCellY = y % (cellSize + cellGap) < cellSize;
```
### Issue 3 of 3
app/javascript/pages/Home/signedIn/ActivityGraph.svelte:19
When `columns` is 0 (possible if `dates.length === 0`), the formula evaluates to `0 * 12 + (0 - 1) * 4 = -4`, which is a negative canvas width. Setting `canvas.width` to a negative number is invalid and will silently produce a broken canvas.
```suggestion
const graphWidth = $derived(columns > 0 ? columns * cellSize + (columns - 1) * cellGap : 0);
```
Reviews (1): Last reviewed commit: "Switch to CSS vars" | Re-trigger Greptile |
Summary of the problem
The activity graph currently uses 365 (!!) DOM nodes.
Describe your changes
Switch to a canvas, which reduces the node count to 1.
Screenshots / Media
N/A. Looks the same!