feat: Implement Project Analytics Dashboard (#41)#149
feat: Implement Project Analytics Dashboard (#41)#149sameezy667 wants to merge 1 commit intoStabilityNexus:mainfrom
Conversation
- Add comprehensive analytics service for metrics calculation - Create 3 chart components (Pie, Line, Bar) using native Canvas API - Build two-view analytics dashboard (Platform + Project details) - Implement time-series tracking with localStorage persistence - Add JSON and CSV export functionality - Maintain 100% client-side operation (no backend required) - Include complete documentation and user guides Closes StabilityNexus#41
WalkthroughIntroduces a comprehensive client-side Analytics feature for the Bene platform, featuring data collection and metrics calculation via AnalyticsService, three Canvas-based chart components (PieChart, LineChart, BarChart), a dual-view Analytics dashboard with platform and project-level insights, localStorage-based time-series persistence, and JSON/CSV export capabilities. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Analytics Dashboard
participant AnalyticsService
participant localStorage
participant Chart Components
User->>Analytics Dashboard: Navigate to Analytics
activate Analytics Dashboard
Analytics Dashboard->>AnalyticsService: calculatePlatformAnalytics(projects)
activate AnalyticsService
AnalyticsService->>AnalyticsService: Process all projects<br/>compute metrics
AnalyticsService-->>Analytics Dashboard: PlatformAnalytics result
deactivate AnalyticsService
Analytics Dashboard->>Chart Components: Render Platform Overview
activate Chart Components
Chart Components-->>Analytics Dashboard: Charts rendered
deactivate Chart Components
User->>Analytics Dashboard: Click on project
Analytics Dashboard->>AnalyticsService: calculateProjectMetrics(project)
activate AnalyticsService
AnalyticsService->>localStorage: Store snapshot
AnalyticsService-->>Analytics Dashboard: ProjectMetrics + TimeSeriesData
deactivate AnalyticsService
Analytics Dashboard->>Chart Components: Render Project Details<br/>(PieChart, LineChart)
activate Chart Components
Chart Components-->>Analytics Dashboard: Charts rendered
deactivate Chart Components
User->>Analytics Dashboard: Click Export JSON
Analytics Dashboard->>AnalyticsService: exportAsJSON(data)
activate AnalyticsService
AnalyticsService->>AnalyticsService: Generate Blob
AnalyticsService-->>User: Download file
deactivate AnalyticsService
deactivate Analytics Dashboard
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes
Poem
Pre-merge checks and finishing touches✅ Passed checks (5 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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 (8)
ANALYTICS_FEATURE.md (1)
131-146: Consider adding language identifiers to code blocks.For better syntax highlighting and clarity, add language identifiers to the fenced code blocks (e.g.,
```bashor```plaintext).Apply this pattern:
-``` +```plaintext Platform Overview → "📥 Export Data" button</blockquote></details> <details> <summary>src/lib/components/charts/PieChart.svelte (1)</summary><blockquote> `74-78`: **Consider theme-aware text colors.** The percentage labels use hardcoded white text (`#ffffff`), which may have poor contrast on light themes. While the documentation claims "theme-aware" charts, the canvas text color doesn't adapt. You could detect the theme and adjust text color accordingly, or use a semi-transparent approach that works on both themes. However, if the pie slices always use relatively dark colors, white text may be acceptable. </blockquote></details> <details> <summary>src/routes/Analytics.svelte (4)</summary><blockquote> `17-17`: **Use explicit typing instead of `any[]`.** `timeSeriesData` is typed as `any[]`, losing type safety. Since `TimeSeriesData` is already imported from the analytics service, use it here. ```diff - let timeSeriesData: any[] = []; + let timeSeriesData: import("$lib/analytics/analytics-service").TimeSeriesData[] = [];Or add
TimeSeriesDatato the existing import on line 4.
26-34: Consider throttling snapshot storage to avoid near-duplicate data points.Every time
selectedProjectchanges, a snapshot is stored immediately. If a user navigates back and forth between projects or re-selects the same project, the history accumulates many data points with nearly identical timestamps and values, degrading time-series data quality.Consider adding a minimum interval (e.g., 5 minutes) or checking if the last snapshot's metrics differ meaningfully before storing:
$: if (selectedProject) { selectedProjectMetrics = AnalyticsService.calculateProjectMetrics(selectedProject, currentHeight); timeSeriesData = AnalyticsService.generateTimeSeriesData(selectedProject.project_id); // Store current snapshot - if (selectedProjectMetrics) { + // Only store if enough time has passed since last snapshot + const lastSnapshot = timeSeriesData[timeSeriesData.length - 1]; + const MIN_INTERVAL = 5 * 60 * 1000; // 5 minutes + if (selectedProjectMetrics && (!lastSnapshot || Date.now() - lastSnapshot.timestamp > MIN_INTERVAL)) { AnalyticsService.storeAnalyticsSnapshot(selectedProject.project_id, selectedProjectMetrics); } }
184-205: Metrics are recalculated on every render.
calculateProjectMetricsis called inline for each project in the{#each}block. This computation runs on every component re-render (e.g., when exporting data). Additionally,calculatePlatformAnalyticsalready iterates all projects and computes their metrics internally.Consider caching the per-project metrics in a reactive variable to avoid redundant calculations:
$: projectMetricsMap = new Map( projects.map(p => [p.project_id, AnalyticsService.calculateProjectMetrics(p, currentHeight)]) );Then reference
projectMetricsMap.get(project.project_id)in the template.
1-94: Unused import:onMount.The
onMountimport from Svelte (line 2) is not used anywhere in the component. Consider removing it to keep imports clean.<script lang="ts"> - import { onMount } from "svelte"; import { type Project } from "$lib/common/project";src/lib/analytics/analytics-service.ts (2)
218-218: Handle potentiallocalStoragequota error.
localStorage.setItemcan throw aQuotaExceededErrorif the storage limit is reached. While the 1000-snapshot limit helps, accumulated data across many projects could still exceed quotas on some browsers.- localStorage.setItem(key, JSON.stringify(existing)); + try { + localStorage.setItem(key, JSON.stringify(existing)); + } catch (error) { + console.error("Failed to store analytics snapshot (quota exceeded?):", error); + // Optionally: clear older data or notify user + }
284-291: CSV export doesn't handle newlines in values.The escaping logic handles commas and quotes, but strings containing newlines would break CSV row structure. While unlikely with current data, this could cause issues if project names/content include line breaks.
const value = row[header]; // Escape commas and quotes - if (typeof value === 'string' && (value.includes(',') || value.includes('"'))) { + if (typeof value === 'string' && (value.includes(',') || value.includes('"') || value.includes('\n'))) { return `"${value.replace(/"/g, '""')}"`; } return value;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (12)
ANALYTICS_FEATURE.md(1 hunks)GIT_COMMIT_MESSAGE.md(1 hunks)IMPLEMENTATION_SUMMARY.md(1 hunks)ISSUE_41_COMPLETION.md(1 hunks)QUICK_START_ANALYTICS.md(1 hunks)README.md(1 hunks)src/lib/analytics/analytics-service.ts(1 hunks)src/lib/components/charts/BarChart.svelte(1 hunks)src/lib/components/charts/LineChart.svelte(1 hunks)src/lib/components/charts/PieChart.svelte(1 hunks)src/routes/Analytics.svelte(1 hunks)src/routes/App.svelte(4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/lib/analytics/analytics-service.ts (2)
src/lib/common/project.ts (1)
Project(60-85)src/lib/common/store.ts (1)
projects(14-17)
🪛 LanguageTool
ANALYTICS_FEATURE.md
[grammar] ~247-~247: Use a hyphen to join words.
Context: ...ytics features: 1. Maintain client-side only approach 2. Use localStorage for pe...
(QB_NEW_EN_HYPHEN)
🪛 markdownlint-cli2 (0.18.1)
QUICK_START_ANALYTICS.md
91-91: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
108-108: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
ANALYTICS_FEATURE.md
131-131: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
137-137: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
143-143: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
186-186: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
260-260: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
🔇 Additional comments (11)
ISSUE_41_COMPLETION.md (1)
1-259: LGTM! Comprehensive completion documentation.The completion report is well-structured, provides clear visual representations, and thoroughly documents the delivered features. The statistics and metadata provide good context for the implementation scope.
README.md (1)
146-191: LGTM! Clear and comprehensive analytics documentation.The Analytics Feature section is well-integrated into the README, provides a clear overview of capabilities, and properly links to detailed documentation files. The emphasis on client-side operation aligns with the platform's decentralized philosophy.
src/routes/App.svelte (2)
254-258: LGTM! Analytics navigation properly integrated.The Analytics tab has been correctly added to both desktop and mobile navigation menus, maintaining consistency with existing navigation patterns.
Also applies to: 349-353
372-374: LGTM! Props correctly passed to Analytics component.The data transformation from
projectsStore.data(Map) to an array is correct, and the nullish coalescing forcurrent_heightproperly handles the type conversion fromnumber | nulltonumber | undefined.QUICK_START_ANALYTICS.md (1)
1-179: LGTM! Comprehensive and user-friendly guide.The quick start guide is well-structured with clear instructions for developers, users, and project owners. The troubleshooting section addresses common issues effectively. Code examples are accurate and actionable.
src/lib/components/charts/PieChart.svelte (1)
33-83: LGTM! Solid pie chart implementation with good edge case handling.The chart correctly handles empty data, zero totals, and division-by-zero scenarios. The math for angle calculations is accurate, and the 5% threshold for labels prevents visual clutter.
src/lib/components/charts/LineChart.svelte (1)
26-140: LGTM! Well-implemented line chart with good scaling and labeling.The chart properly calculates min/max values, scales data appropriately, and uses smart label frequency to avoid crowding. Grid lines and axis labels enhance readability.
src/lib/components/charts/BarChart.svelte (1)
26-142: Good bar chart implementation with useful features.The chart includes nice touches like gradient fills, value labels above bars, and smart label rotation for long text. The overall structure and rendering logic are solid.
GIT_COMMIT_MESSAGE.md (1)
1-114: Comprehensive commit documentation.The commit message provides a thorough summary of the analytics feature implementation, covering all key aspects including changes, features, technical details, and testing status. This level of documentation aids future maintainability.
IMPLEMENTATION_SUMMARY.md (1)
1-228: Well-structured implementation documentation.The implementation summary provides clear traceability to Issue #41 requirements, documents the architecture decisions, and includes a useful testing checklist. Good reference material for future maintenance.
src/lib/analytics/analytics-service.ts (1)
91-142: Well-structured analytics service.The
AnalyticsServiceprovides a clean, stateless API for calculating metrics, managing persistence, and exporting data. The use of static methods is appropriate given the utility nature of the service, and the TypeScript interfaces provide good type safety.
| const averageProjectSuccess = projects.length > 0 | ||
| ? (successfulProjects / (successfulProjects + failedProjects)) * 100 | ||
| : 0; |
There was a problem hiding this comment.
Potential NaN from division by zero.
If all projects are currently active (none have ended), both successfulProjects and failedProjects will be 0, resulting in 0 / 0 = NaN.
const averageProjectSuccess = projects.length > 0
- ? (successfulProjects / (successfulProjects + failedProjects)) * 100
+ ? (successfulProjects + failedProjects > 0
+ ? (successfulProjects / (successfulProjects + failedProjects)) * 100
+ : 0)
: 0;📝 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 averageProjectSuccess = projects.length > 0 | |
| ? (successfulProjects / (successfulProjects + failedProjects)) * 100 | |
| : 0; | |
| const averageProjectSuccess = projects.length > 0 | |
| ? (successfulProjects + failedProjects > 0 | |
| ? (successfulProjects / (successfulProjects + failedProjects)) * 100 | |
| : 0) | |
| : 0; |
🤖 Prompt for AI Agents
In src/lib/analytics/analytics-service.ts around lines 173 to 175, the
calculation can produce NaN when successfulProjects + failedProjects equals 0;
change the logic to compute a denominator variable (e.g., const totalEnded =
successfulProjects + failedProjects) and only perform the division when
totalEnded > 0, otherwise set averageProjectSuccess to 0 (or another safe
fallback); ensure the result uses the computed denominator and preserves the
percentage multiplication.
| data.forEach((item, index) => { | ||
| const x = padding.left + (chartWidth / data.length) * index + barSpacing / 2; | ||
| const barHeight = (item.value / maxValue) * chartHeight; | ||
| const y = padding.top + chartHeight - barHeight; | ||
|
|
||
| const color = item.color || defaultColor; | ||
|
|
||
| // Draw bar with gradient | ||
| const gradient = ctx!.createLinearGradient(x, y, x, y + barHeight); | ||
| gradient.addColorStop(0, color); | ||
| gradient.addColorStop(1, color + "aa"); | ||
|
|
||
| ctx!.fillStyle = gradient; | ||
| ctx!.fillRect(x, y, barWidth, barHeight); | ||
|
|
||
| // Draw border | ||
| ctx!.strokeStyle = color; | ||
| ctx!.lineWidth = 2; | ||
| ctx!.strokeRect(x, y, barWidth, barHeight); | ||
|
|
||
| // Draw value on top of bar | ||
| ctx!.fillStyle = "#333"; | ||
| ctx!.font = "bold 12px sans-serif"; | ||
| ctx!.textAlign = "center"; | ||
| ctx!.textBaseline = "bottom"; | ||
| ctx!.fillText(item.value.toFixed(0), x + barWidth / 2, y - 5); | ||
| }); |
There was a problem hiding this comment.
Potential issues with color gradient and zero values.
Two concerns:
-
Line 75:
color + "aa"assumes the color is in hex format. If a color is provided inrgb(),rgba(), orhsl()format, the concatenation will produce an invalid color string. -
Line 67: If all data values are 0,
maxValuewill be 0, causingitem.value / maxValueto be0/0 = NaN, resulting inNaNfor bar heights.
Consider these fixes:
function drawChart() {
if (!ctx || !data || data.length === 0) return;
// ...
const maxValue = Math.max(...data.map(d => d.value));
+ if (maxValue === 0) {
+ // Draw empty chart with message or just return
+ ctx.fillStyle = "#888";
+ ctx.font = "14px sans-serif";
+ ctx.textAlign = "center";
+ ctx.fillText("No data to display", width / 2, height / 2);
+ return;
+ }
// ...
// Draw bars
data.forEach((item, index) => {
// ...
const color = item.color || defaultColor;
// Draw bar with gradient
const gradient = ctx!.createLinearGradient(x, y, x, y + barHeight);
gradient.addColorStop(0, color);
- gradient.addColorStop(1, color + "aa");
+ // Only add alpha if color is hex format
+ const colorWithAlpha = color.startsWith('#') ? color + "aa" : color;
+ gradient.addColorStop(1, colorWithAlpha);Committable suggestion skipped: line range outside the PR's diff.
| data.forEach((point, index) => { | ||
| const x = padding.left + (chartWidth / (data.length - 1)) * index; | ||
| const y = padding.top + chartHeight - ((point.value - minValue) / valueRange) * chartHeight; | ||
|
|
||
| if (index === 0) { | ||
| ctx!.moveTo(x, y); | ||
| } else { | ||
| ctx!.lineTo(x, y); | ||
| } | ||
| }); |
There was a problem hiding this comment.
Potential division by zero with single data point.
Lines 71 and 85 use (data.length - 1) as a divisor for calculating X coordinates. If the data array has only one element, this results in division by zero, producing NaN coordinates.
Add a guard or special handling for single data points:
function drawChart() {
if (!ctx || !data || data.length === 0) return;
+
+ // Handle single data point
+ if (data.length === 1) {
+ const point = data[0];
+ const x = padding.left + chartWidth / 2;
+ const y = padding.top + chartHeight / 2;
+
+ ctx.beginPath();
+ ctx.arc(x, y, 4, 0, Math.PI * 2);
+ ctx.fillStyle = color;
+ ctx.fill();
+ ctx.strokeStyle = "#fff";
+ ctx.lineWidth = 2;
+ ctx.stroke();
+
+ // Draw label
+ ctx.fillStyle = "#888";
+ ctx.font = "11px sans-serif";
+ ctx.textAlign = "center";
+ ctx.fillText(point.label, x, padding.top + chartHeight + 10);
+ return;
+ }
// Clear canvas
ctx.clearRect(0, 0, width, height);Also applies to: 84-95
🤖 Prompt for AI Agents
In src/lib/components/charts/LineChart.svelte around lines 70 to 95, the code
divides by (data.length - 1) when computing X coordinates which yields
division-by-zero for a single data point; compute a safe stepX first (e.g. stepX
= data.length > 1 ? chartWidth / (data.length - 1) : 0) and then compute x using
either padding.left + stepX * index for multi-point datasets or center the
single point (e.g. padding.left + chartWidth / 2) when stepX is 0; apply the
same safe stepX logic to the other block referenced (lines ~84–95) so no code
path divides by zero.
| projects as projectsStore, | ||
| } from "$lib/common/store"; | ||
| import { fetchProjects } from "$lib/ergo/fetch"; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
rg -n "fetchProjects" --type=svelte -A2 -B2Repository: StabilityNexus/BenefactionPlatform-Ergo
Length of output: 113
🏁 Script executed:
rg -n "fetchProjects" src/routes/App.svelteRepository: StabilityNexus/BenefactionPlatform-Ergo
Length of output: 138
🏁 Script executed:
rg -n "fetchProjects" --max-count=20Repository: StabilityNexus/BenefactionPlatform-Ergo
Length of output: 1166
Remove unused fetchProjects import.
The fetchProjects import on line 16 is not used anywhere in this file and should be removed.
🤖 Prompt for AI Agents
In src/routes/App.svelte around lines 14 to 16, the import fetchProjects is
unused; remove the fetchProjects import from the import list (delete the "import
{ fetchProjects } from \"$lib/ergo/fetch\";" or remove fetchProjects from the
existing import statement) and ensure trailing commas/line breaks remain
consistent so the remaining imports compile correctly.
Closes #41
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.