feat: About page graph typography, grid & animation improvements (closes #676)#677
Conversation
📝 WalkthroughWalkthroughEnhanced the About page chart with visual improvements and animations. Added left-to-right line animation via custom Chart.js plugin, AnimatedNumber component for stats, dark mode detection using MutationObserver, gradient-filled areas, bold grid lines, and updated axis styling. Maintained existing data fetching logic. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
src/app/about/page.jsx (1)
258-265: Consider sourcing all stats from the API for consistency.
years,projects, andcontributorscome from the API, butrepos,mentors, andcommunity membersare hardcoded. This creates a maintenance burden and potential for data drift.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/about/page.jsx` around lines 258 - 265, The hardcoded metrics in the JSX map should be sourced from the API-owned stats object instead of literals; update the code that builds/receives the stats object so it includes repos, mentors, and communityMembers (or similar keys), then replace the hardcoded values in the array (currently using 203, '88', '7500+') with stats.repos, stats.mentors, and stats.communityMembers in the mapped array; also add safe fallbacks (e.g., 0 or '—') where stats may be undefined and update any state/prop initialization or type definitions that construct stats to include these new fields so rendering remains stable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/app/about/page.jsx`:
- Around line 64-79: The effect that re-triggers chart animation (useEffect)
starts a requestAnimationFrame loop via animate but does not cancel it on
unmount or when graphInView changes; move the drawProgress ref declaration
(drawProgress) next to this useEffect for locality and add a cleanup that
cancels the scheduled animationFrame (store the id from requestAnimationFrame
and call cancelAnimationFrame in the returned cleanup). Ensure
chartRef.update('none') and setChartKey usages remain unchanged but the animate
loop is properly torn down to avoid callbacks running after unmount or
dependency changes.
- Around line 29-41: The useEffect animation using requestAnimationFrame
(function step) can continue after unmount and call setDisplay; update the
effect to store the request id (e.g., let rafId) and an "aborted" or "mounted"
flag (ref/variable) and in the cleanup return function call
cancelAnimationFrame(rafId) and set the flag so step exits early (avoid calling
setDisplay when aborted/unmounted); reference the existing useEffect, step,
requestAnimationFrame, setDisplay, inView, target, and duration to locate and
fix the logic.
- Around line 52-55: The initial graphData fallback has mismatched arrays
(graphData.labels length 9 vs graphData.data length 10); update the graphData
object so labels and data are the same length by either adding the missing year
to graphData.labels (e.g., include the extra year that corresponds to the extra
value) or removing the extra numeric entry from graphData.data so both arrays
align; edit the graphData constant in page.jsx (the graphData -> labels and data
fields) to ensure consistent lengths.
- Around line 113-114: Replace the hardcoded chart constants so the chart uses
the fetched API response: stop using rawData and the hardcoded labels and
instead read graphData.labels and graphData.data (or graphData?.labels /
graphData?.data) when rendering the chart; update the variables referenced in
the chart component (rawData, labels) to use graphData values with a safe
fallback (e.g., empty arrays) so chart rendering won’t break if graphData is
undefined; ensure the component that originally declared rawData/labels now maps
to graphData from your fetch (refer to the graphData variable and wherever
rawData/labels are passed into the Chart).
---
Nitpick comments:
In `@src/app/about/page.jsx`:
- Around line 258-265: The hardcoded metrics in the JSX map should be sourced
from the API-owned stats object instead of literals; update the code that
builds/receives the stats object so it includes repos, mentors, and
communityMembers (or similar keys), then replace the hardcoded values in the
array (currently using 203, '88', '7500+') with stats.repos, stats.mentors, and
stats.communityMembers in the mapped array; also add safe fallbacks (e.g., 0 or
'—') where stats may be undefined and update any state/prop initialization or
type definitions that construct stats to include these new fields so rendering
remains stable.
| useEffect(() => { | ||
| if (!inView) return; | ||
| const numeric = parseInt(String(target).replace(/\D/g, ''), 10) || 0; | ||
| let start = null; | ||
| const step = (ts) => { | ||
| if (!start) start = ts; | ||
| const progress = Math.min((ts - start) / duration, 1); | ||
| const ease = 1 - Math.pow(1 - progress, 3); | ||
| setDisplay(Math.floor(ease * numeric)); | ||
| if (progress < 1) requestAnimationFrame(step); | ||
| }; | ||
| requestAnimationFrame(step); | ||
| }, [inView, target, duration]); |
There was a problem hiding this comment.
Missing cleanup for requestAnimationFrame can cause state update on unmounted component.
If the component unmounts during the animation, setDisplay will still be called, potentially causing a React warning. Add a cleanup mechanism using a ref or abort flag.
🛡️ Proposed fix
useEffect(() => {
if (!inView) return;
const numeric = parseInt(String(target).replace(/\D/g, ''), 10) || 0;
let start = null;
+ let rafId;
+ let cancelled = false;
const step = (ts) => {
+ if (cancelled) return;
if (!start) start = ts;
const progress = Math.min((ts - start) / duration, 1);
const ease = 1 - Math.pow(1 - progress, 3);
setDisplay(Math.floor(ease * numeric));
- if (progress < 1) requestAnimationFrame(step);
+ if (progress < 1) rafId = requestAnimationFrame(step);
};
- requestAnimationFrame(step);
+ rafId = requestAnimationFrame(step);
+ return () => {
+ cancelled = true;
+ cancelAnimationFrame(rafId);
+ };
}, [inView, target, duration]);📝 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.
| useEffect(() => { | |
| if (!inView) return; | |
| const numeric = parseInt(String(target).replace(/\D/g, ''), 10) || 0; | |
| let start = null; | |
| const step = (ts) => { | |
| if (!start) start = ts; | |
| const progress = Math.min((ts - start) / duration, 1); | |
| const ease = 1 - Math.pow(1 - progress, 3); | |
| setDisplay(Math.floor(ease * numeric)); | |
| if (progress < 1) requestAnimationFrame(step); | |
| }; | |
| requestAnimationFrame(step); | |
| }, [inView, target, duration]); | |
| useEffect(() => { | |
| if (!inView) return; | |
| const numeric = parseInt(String(target).replace(/\D/g, ''), 10) || 0; | |
| let start = null; | |
| let rafId; | |
| let cancelled = false; | |
| const step = (ts) => { | |
| if (cancelled) return; | |
| if (!start) start = ts; | |
| const progress = Math.min((ts - start) / duration, 1); | |
| const ease = 1 - Math.pow(1 - progress, 3); | |
| setDisplay(Math.floor(ease * numeric)); | |
| if (progress < 1) rafId = requestAnimationFrame(step); | |
| }; | |
| rafId = requestAnimationFrame(step); | |
| return () => { | |
| cancelled = true; | |
| cancelAnimationFrame(rafId); | |
| }; | |
| }, [inView, target, duration]); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/about/page.jsx` around lines 29 - 41, The useEffect animation using
requestAnimationFrame (function step) can continue after unmount and call
setDisplay; update the effect to store the request id (e.g., let rafId) and an
"aborted" or "mounted" flag (ref/variable) and in the cleanup return function
call cancelAnimationFrame(rafId) and set the flag so step exits early (avoid
calling setDisplay when aborted/unmounted); reference the existing useEffect,
step, requestAnimationFrame, setDisplay, inView, target, and duration to locate
and fix the logic.
| graphData: { | ||
| labels: ['2016', '2017', '2018', '2019', '2020', '2021', '2022', '2023', '2024'], | ||
| data: [4, 8, 12, 9, 9, 11, 8, 6, 18, 22], | ||
| }, |
There was a problem hiding this comment.
Array length mismatch in initial state: 9 labels vs 10 data points.
The initial graphData has 9 labels (2016–2024) but 10 data values. While this fallback state appears unused since the chart uses hardcoded rawData/labels at lines 113-114, keeping it consistent avoids confusion.
🔧 Suggested fix
graphData: {
- labels: ['2016', '2017', '2018', '2019', '2020', '2021', '2022', '2023', '2024'],
+ labels: ['2016', '2017', '2018', '2019', '2020', '2021', '2022', '2023', '2024', '2025'],
data: [4, 8, 12, 9, 9, 11, 8, 6, 18, 22],
},📝 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.
| graphData: { | |
| labels: ['2016', '2017', '2018', '2019', '2020', '2021', '2022', '2023', '2024'], | |
| data: [4, 8, 12, 9, 9, 11, 8, 6, 18, 22], | |
| }, | |
| graphData: { | |
| labels: ['2016', '2017', '2018', '2019', '2020', '2021', '2022', '2023', '2024', '2025'], | |
| data: [4, 8, 12, 9, 9, 11, 8, 6, 18, 22], | |
| }, |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/about/page.jsx` around lines 52 - 55, The initial graphData fallback
has mismatched arrays (graphData.labels length 9 vs graphData.data length 10);
update the graphData object so labels and data are the same length by either
adding the missing year to graphData.labels (e.g., include the extra year that
corresponds to the extra value) or removing the extra numeric entry from
graphData.data so both arrays align; edit the graphData constant in page.jsx
(the graphData -> labels and data fields) to ensure consistent lengths.
| // re-trigger chart animation each time it enters view | ||
| useEffect(() => { | ||
| if (!graphInView) return; | ||
| setChartKey((k) => k + 1); | ||
| drawProgress.current = 0; | ||
| const start = performance.now(); | ||
| const duration = 4000; | ||
| const easeInOutQuart = (t) => t < 0.5 ? 8 * t * t * t * t : 1 - Math.pow(-2 * t + 2, 4) / 2; | ||
| const animate = (now) => { | ||
| const t = Math.min((now - start) / duration, 1); | ||
| drawProgress.current = easeInOutQuart(t); | ||
| chartRef.current?.update('none'); | ||
| if (t < 1) requestAnimationFrame(animate); | ||
| }; | ||
| requestAnimationFrame(animate); | ||
| }, [graphInView]); |
There was a problem hiding this comment.
Missing cleanup for chart animation requestAnimationFrame.
Similar to AnimatedNumber, if the component unmounts during animation, the callback will continue executing. Also, drawProgress is referenced here but declared later at line 144, which is valid JavaScript but reduces readability.
🛡️ Proposed fix
useEffect(() => {
if (!graphInView) return;
setChartKey((k) => k + 1);
drawProgress.current = 0;
const start = performance.now();
const duration = 4000;
const easeInOutQuart = (t) => t < 0.5 ? 8 * t * t * t * t : 1 - Math.pow(-2 * t + 2, 4) / 2;
+ let rafId;
+ let cancelled = false;
const animate = (now) => {
+ if (cancelled) return;
const t = Math.min((now - start) / duration, 1);
drawProgress.current = easeInOutQuart(t);
chartRef.current?.update('none');
- if (t < 1) requestAnimationFrame(animate);
+ if (t < 1) rafId = requestAnimationFrame(animate);
};
- requestAnimationFrame(animate);
+ rafId = requestAnimationFrame(animate);
+ return () => {
+ cancelled = true;
+ cancelAnimationFrame(rafId);
+ };
}, [graphInView]);Consider moving the drawProgress ref declaration (line 144) closer to this effect for better code locality.
📝 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.
| // re-trigger chart animation each time it enters view | |
| useEffect(() => { | |
| if (!graphInView) return; | |
| setChartKey((k) => k + 1); | |
| drawProgress.current = 0; | |
| const start = performance.now(); | |
| const duration = 4000; | |
| const easeInOutQuart = (t) => t < 0.5 ? 8 * t * t * t * t : 1 - Math.pow(-2 * t + 2, 4) / 2; | |
| const animate = (now) => { | |
| const t = Math.min((now - start) / duration, 1); | |
| drawProgress.current = easeInOutQuart(t); | |
| chartRef.current?.update('none'); | |
| if (t < 1) requestAnimationFrame(animate); | |
| }; | |
| requestAnimationFrame(animate); | |
| }, [graphInView]); | |
| // re-trigger chart animation each time it enters view | |
| useEffect(() => { | |
| if (!graphInView) return; | |
| setChartKey((k) => k + 1); | |
| drawProgress.current = 0; | |
| const start = performance.now(); | |
| const duration = 4000; | |
| const easeInOutQuart = (t) => t < 0.5 ? 8 * t * t * t * t : 1 - Math.pow(-2 * t + 2, 4) / 2; | |
| let rafId; | |
| let cancelled = false; | |
| const animate = (now) => { | |
| if (cancelled) return; | |
| const t = Math.min((now - start) / duration, 1); | |
| drawProgress.current = easeInOutQuart(t); | |
| chartRef.current?.update('none'); | |
| if (t < 1) rafId = requestAnimationFrame(animate); | |
| }; | |
| rafId = requestAnimationFrame(animate); | |
| return () => { | |
| cancelled = true; | |
| cancelAnimationFrame(rafId); | |
| }; | |
| }, [graphInView]); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/about/page.jsx` around lines 64 - 79, The effect that re-triggers
chart animation (useEffect) starts a requestAnimationFrame loop via animate but
does not cancel it on unmount or when graphInView changes; move the drawProgress
ref declaration (drawProgress) next to this useEffect for locality and add a
cleanup that cancels the scheduled animationFrame (store the id from
requestAnimationFrame and call cancelAnimationFrame in the returned cleanup).
Ensure chartRef.update('none') and setChartKey usages remain unchanged but the
animate loop is properly torn down to avoid callbacks running after unmount or
dependency changes.
| const rawData = [4, 8, 12, 9, 9, 11, 8, 6, 18, 22]; | ||
| const labels = ['2016', '2017', '2018', '2019', '2020', '2021', '2022', '2023', '2024', '2025']; |
There was a problem hiding this comment.
Fetched graphData from API is unused; chart uses hardcoded data instead.
The API response contains dynamic graphData with labels and data (see lines 95-96), but the chart uses hardcoded rawData and labels constants. This means the chart won't reflect actual project counts from the API.
🔧 Suggested fix to use API data
- const rawData = [4, 8, 12, 9, 9, 11, 8, 6, 18, 22];
- const labels = ['2016', '2017', '2018', '2019', '2020', '2021', '2022', '2023', '2024', '2025'];
+ const { labels, data: rawData } = stats.graphData;
const data = {
labels,
datasets: [
{
label: 'Completed Projects',
data: rawData,📝 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.
| const rawData = [4, 8, 12, 9, 9, 11, 8, 6, 18, 22]; | |
| const labels = ['2016', '2017', '2018', '2019', '2020', '2021', '2022', '2023', '2024', '2025']; | |
| const { labels, data: rawData } = stats.graphData; | |
| const data = { | |
| labels, | |
| datasets: [ | |
| { | |
| label: 'Completed Projects', | |
| data: rawData, |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/about/page.jsx` around lines 113 - 114, Replace the hardcoded chart
constants so the chart uses the fetched API response: stop using rawData and the
hardcoded labels and instead read graphData.labels and graphData.data (or
graphData?.labels / graphData?.data) when rendering the chart; update the
variables referenced in the chart component (rawData, labels) to use graphData
values with a safe fallback (e.g., empty arrays) so chart rendering won’t break
if graphData is undefined; ensure the component that originally declared
rawData/labels now maps to graphData from your fetch (refer to the graphData
variable and wherever rawData/labels are passed into the Chart).
Addressed Issues
Fixes #676
This PR improves the visual clarity, readability, and animation behavior of the line chart displayed on the About page. The changes focus on improving typography, grid visibility, and implementing a smoother chart animation that aligns better with the timeline-style data visualization.
Recordings:
Aossie.web.mp4
Changes Implemented
1. Axis Tick Typography Improvement
Previously, the axis tick labels (years on the x-axis and project counts on the y-axis) were rendered using font.weight: 'bold' in the Chart.js configuration. This caused the tick labels to appear visually heavy and reduced readability.
This PR updates the font weight to normal, making the tick labels lighter and more consistent with the rest of the UI typography.
2. Grid Line Visibility Enhancement
The grid lines were previously faint and difficult to notice, especially on larger screens. To improve visual structure and readability of the chart:
This change improves the ability to visually follow data points across the chart.
3. Left-to-Right Line Drawing Animation
The default Chart.js animation draws the line from bottom to top, which does not align well with a timeline-style dataset.
To improve the storytelling of the graph, a custom animation plugin was implemented that:
The animation is triggered when the chart enters the viewport using useInView, ensuring the animation only runs when visible to the user.
4. Fixed Axis Labels Hidden During Animation
Initially, the clipping logic used beforeDraw, which unintentionally clipped axis tick labels along with the dataset.
This issue was resolved by moving the clipping logic to beforeDatasetsDraw, ensuring that:
5. Improved "Year" Label Visibility
The "Year" label below the chart had poor visibility due to low contrast:
AI Usage Disclosure:
We encourage contributors to use AI tools responsibly when creating Pull Requests. While AI can be a valuable aid, it is essential to ensure that your contributions meet the task requirements, build successfully, include relevant tests, and pass all linters. Submissions that do not meet these standards may be closed without warning to maintain the quality and integrity of the project. Please take the time to understand the changes you are proposing and their impact. AI slop is strongly discouraged and may lead to banning and blocking. Do not spam our repos with AI slop.
Check one of the checkboxes below:
I have used the following AI models and tools: TODO
Checklist
Summary by CodeRabbit
Release Notes