Skip to content

refactor(dashboard): true visual graduation markers on decay curve#61

Closed
Gradata wants to merge 1 commit intomainfrom
refactor/decay-curve-clean-markers
Closed

refactor(dashboard): true visual graduation markers on decay curve#61
Gradata wants to merge 1 commit intomainfrom
refactor/decay-curve-clean-markers

Conversation

@Gradata
Copy link
Copy Markdown
Owner

@Gradata Gradata commented Apr 15, 2026

Summary

  • Move the data-graduation-marker test hook onto the actual <ReferenceLine> via a custom SVG label renderer (<g role='img' aria-label=... ><title>...</title></g>), and drop the redundant hidden-span fallback.
  • Add native SVG <title> tooltip on hover showing the graduated rule's description (no extra JS/tooltip library).
  • Global ResponsiveContainer mock in tests/setup.ts so jsdom actually renders Recharts SVG (required for on-chart test hooks).

Changes

  • cloud/dashboard/src/components/brain/CorrectionDecayCurve.tsx — custom label on ReferenceLine, invisible hit-target <rect> for hover, hidden-span block removed. buildDecayCurve untouched. 12-marker cap + "+N more" caption preserved (kept — still useful when markers cluster).
  • cloud/dashboard/tests/CorrectionDecayCurve.test.tsx — 2 new tests: x-position correctness for a 5-day-old graduation in a 7d range, and aria-label/SVG <title> accessibility.
  • cloud/dashboard/tests/setup.ts — mock ResponsiveContainer with fixed 800x400.
  • cloud/dashboard/tests/dashboard-page.test.tsx — switch one matcher to getAllByText (marker <title> now legitimately surfaces the same description).

Test plan

  • npm run test:run — 137/137 pass
  • Verify on https://gradata-dashboard.pages.dev that dashed markers align with graduation dates and hover shows the rule description
  • Screen reader check (NVDA/VoiceOver): marker announces "Rule graduated: "

…n fallback

- Move data-graduation-marker hook onto an SVG <g> rendered via the
  ReferenceLine label prop. Markers now live on the actual chart,
  not a sibling hidden div.
- Add role='img' + aria-label + SVG <title> for native hover tooltip
  showing the graduated rule's description (no extra JS needed).
- Add invisible hit-target <rect> so hovering near the dashed line
  triggers the SVG title tooltip.
- Drop the separate <div aria-hidden><span data-graduation-marker>
  fallback block now that the hook lives on the real marker.
- Mock Recharts ResponsiveContainer in tests/setup.ts so jsdom
  actually renders chart SVG (fixed-800x400). Required for
  in-chart test hooks going forward.
- Add test: marker renders at correct x position for a lesson
  graduated 5 days ago in the 7d range (left third of plot area).
- Add test: marker carries aria-label + SVG <title> with rule
  description.
- Fix dashboard-page.test.tsx false collision on 'Never use em
  dashes' (now also appears in the marker <title>, so use
  getAllByText).

All 137 dashboard tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Oliver Le <oliver@gradata.ai>
Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gradata has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 15, 2026

📝 Walkthrough

Summary

  • Refactored decay curve graduation markers from hidden <div> fallback to native SVG <g> elements rendered via custom ReferenceLine label prop
  • Added accessibility features: aria-label for screen readers and native SVG <title> elements for hover tooltips showing graduated rule descriptions
  • Added invisible hit-target <rect> to enable hover interactions on the markers
  • Added test utilities: ResponsiveContainer mock in setup.ts with fixed 800×400 dimensions to ensure Recharts SVG renders properly in jsdom
  • Added two new test cases: verify marker x-position correctness for lessons within date ranges and validate aria-label/SVG title accessibility semantics
  • Updated dashboard-page test: changed single getByText() to getAllByText() to account for lesson descriptions appearing in decay curve SVG titles
  • Maintained compatibility: preserved 12-marker cap and "+N more" caption; buildDecayCurve function untouched
  • Test status: all 137 unit tests passing; manual verification of visual alignment and screen-reader behavior pending

Walkthrough

This PR refactors the graduation marker rendering in the CorrectionDecayCurve component by replacing hidden span elements with accessible SVG labels attached to ReferenceLine elements. The new labels include native tooltips, ARIA attributes, hit targets, and test data attributes. Related tests and test infrastructure are updated accordingly.

Changes

Cohort / File(s) Summary
Graduation Marker SVG Labels
cloud/dashboard/src/components/brain/CorrectionDecayCurve.tsx
Replaces hidden <div> with <span> fallback with SVG <g> labels attached to ReferenceLine elements. New labels include <title> for native tooltips, role="img", aria-label derived from lesson data, transparent <rect> hit target, and test hook attributes (data-graduation-marker, data-lesson-id, data-graduated-at, data-x).
Graduation Marker Tests
cloud/dashboard/tests/CorrectionDecayCurve.test.tsx
Added two new test cases: (1) verifies marker data-x positioning accuracy for lessons within a time range; (2) verifies accessibility semantics by asserting aria-label and <title> contain expected lesson description text.
Dashboard Page Tests
cloud/dashboard/tests/dashboard-page.test.tsx
Updates assertion for demo lesson text matching to use getAllByText(...) instead of getByText(...), accounting for text appearing multiple times due to graduation marker SVG <title> elements.
Test Infrastructure
cloud/dashboard/tests/setup.ts
Adds Vitest module mock for recharts ResponsiveContainer with fixed dimensions (800×400) and explicit width/height props to ensure chart SVG rendering in jsdom test environment.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

refactor

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: moving graduation markers from hidden fallback elements to true visual SVG elements on the decay curve chart.
Description check ✅ Passed The description is well-structured and directly related to the changeset, covering the summary, specific changes, and test plan for the graduation markers refactor.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/decay-curve-clean-markers

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying gradata-dashboard with  Cloudflare Pages  Cloudflare Pages

Latest commit: a4ab152
Status: ✅  Deploy successful!
Preview URL: https://97c0e6a3.gradata-dashboard.pages.dev
Branch Preview URL: https://refactor-decay-curve-clean-m.gradata-dashboard.pages.dev

View logs

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cloud/dashboard/tests/setup.ts`:
- Around line 13-21: The ResponsiveContainer mock currently only accepts
children and drops any other props; update the mock signature for
ResponsiveContainer to accept and forward all props (e.g., ({ children, ...props
})) and spread those props into both the outer div and the call to
actual.ResponsiveContainer (React.createElement(actual.ResponsiveContainer, {
width: 800, height: 400, ...props }, children)) so additional props are
preserved while still keeping the fixed size used in the test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: af7aba85-a9dc-4fb6-9235-11100320f498

📥 Commits

Reviewing files that changed from the base of the PR and between 5fd7215 and a4ab152.

📒 Files selected for processing (4)
  • cloud/dashboard/src/components/brain/CorrectionDecayCurve.tsx
  • cloud/dashboard/tests/CorrectionDecayCurve.test.tsx
  • cloud/dashboard/tests/dashboard-page.test.tsx
  • cloud/dashboard/tests/setup.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.13)
  • GitHub Check: test (3.11)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (3)
cloud/dashboard/src/components/brain/CorrectionDecayCurve.tsx (1)

108-150: SVG marker label implementation looks solid.

Attaching the hook and accessibility metadata directly to the rendered ReferenceLine label is a good improvement, and the native <title> + hit-target approach matches the intended behavior.

cloud/dashboard/tests/CorrectionDecayCurve.test.tsx (1)

45-74: Great coverage additions for marker geometry and accessibility text.

These tests validate the new marker contract (data-x, aria-label, and SVG <title>) and directly protect the refactor.

cloud/dashboard/tests/dashboard-page.test.tsx (1)

81-84: Good matcher adjustment for multi-surface text rendering.

Switching to getAllByText here correctly handles duplicated lesson text from chart SVG tooltip/title content.

Comment on lines +13 to +21
ResponsiveContainer: ({ children }: { children: React.ReactNode }) =>
React.createElement(
'div',
{ style: { width: 800, height: 400 } },
React.createElement(
actual.ResponsiveContainer,
{ width: 800, height: 400 },
children,
),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether any ResponsiveContainer call sites rely on props beyond width/height.
rg -nP --type=tsx -C2 '<ResponsiveContainer\b'

Repository: Gradata/gradata

Length of output: 86


🏁 Script executed:

# First, verify the file exists and check its structure
ls -la cloud/dashboard/tests/setup.ts

Repository: Gradata/gradata

Length of output: 137


🏁 Script executed:

# Search for ResponsiveContainer usages without the tsx type flag
rg -nP -C2 '<ResponsiveContainer\b' --max-count=20

Repository: Gradata/gradata

Length of output: 785


🏁 Script executed:

# Also examine the actual code at the specified lines in setup.ts
head -30 cloud/dashboard/tests/setup.ts | cat -n

Repository: Gradata/gradata

Length of output: 1100


🏁 Script executed:

# Search more broadly for all ResponsiveContainer usages to see what props are passed
rg 'ResponsiveContainer' --max-count=30 -A3

Repository: Gradata/gradata

Length of output: 2613


🏁 Script executed:

# Check the recharts type definitions or documentation to understand what props ResponsiveContainer accepts
# First, check if there's a node_modules or package.json
ls cloud/dashboard/package.json 2>/dev/null || ls package.json 2>/dev/null

Repository: Gradata/gradata

Length of output: 87


🏁 Script executed:

# Look for recharts imports to understand the library version
rg 'from ["\']recharts' --max-count=10

Repository: Gradata/gradata

Length of output: 127


🏁 Script executed:

# Check recharts version in package.json
cat cloud/dashboard/package.json | grep -A2 -B2 recharts

Repository: Gradata/gradata

Length of output: 192


🏁 Script executed:

# Search for any other ResponsiveContainer prop patterns or attributes
rg 'ResponsiveContainer' -B1 -A5 cloud/dashboard/src

Repository: Gradata/gradata

Length of output: 2473


Consider forwarding extra props through the ResponsiveContainer mock for better completeness.

The mock currently accepts only children and drops all other props passed to ResponsiveContainer. While the single visible usage only passes width and height (which the mock overrides anyway), forwarding additional props makes the mock more robust and future-proof in case other props are needed.

The proposed fix is sound:

♻️ Proposed fix
-    ResponsiveContainer: ({ children }: { children: React.ReactNode }) =>
+    ResponsiveContainer: (
+      { children, ...props }: React.ComponentProps<typeof actual.ResponsiveContainer>,
+    ) =>
       React.createElement(
         'div',
         { style: { width: 800, height: 400 } },
         React.createElement(
           actual.ResponsiveContainer,
-          { width: 800, height: 400 },
+          { ...props, width: 800, height: 400 },
           children,
         ),
       ),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ResponsiveContainer: ({ children }: { children: React.ReactNode }) =>
React.createElement(
'div',
{ style: { width: 800, height: 400 } },
React.createElement(
actual.ResponsiveContainer,
{ width: 800, height: 400 },
children,
),
ResponsiveContainer: (
{ children, ...props }: React.ComponentProps<typeof actual.ResponsiveContainer>,
) =>
React.createElement(
'div',
{ style: { width: 800, height: 400 } },
React.createElement(
actual.ResponsiveContainer,
{ ...props, width: 800, height: 400 },
children,
),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cloud/dashboard/tests/setup.ts` around lines 13 - 21, The ResponsiveContainer
mock currently only accepts children and drops any other props; update the mock
signature for ResponsiveContainer to accept and forward all props (e.g., ({
children, ...props })) and spread those props into both the outer div and the
call to actual.ResponsiveContainer
(React.createElement(actual.ResponsiveContainer, { width: 800, height: 400,
...props }, children)) so additional props are preserved while still keeping the
fixed size used in the test.

@Gradata Gradata closed this Apr 15, 2026
@Gradata Gradata deleted the refactor/decay-curve-clean-markers branch April 15, 2026 07:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant