-
Notifications
You must be signed in to change notification settings - Fork 411
feat: implement statistics visualization system #302
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
Features: - Daily/Weekly/Monthly statistics views - Activity heatmap - Interruption tracking and analysis - Historical data visualization - Statistics drawer integration Files: - New components: Stats-day, Stats-week, Stats-month, Stats-heatmap - New utilities: StatisticsAnalyzer, StatisticsStore - Updated Timer with interruption dialog - Documentation: STATISTICS_FEATURE.md, STATISTICS_QUICKSTART.md
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR implements a comprehensive statistics visualization system for the Pomotroid application, adding data tracking and analysis capabilities to help users understand their productivity patterns. The implementation introduces session recording, multi-dimensional analytics (day/week/month/history), activity heatmaps, and interruption tracking.
Key changes:
- Data persistence layer with JSON file storage and UUID-based session tracking
- Analytics engine providing daily, weekly, monthly, and historical statistics
- Five new Vue components for statistics visualization (day, week, month, history views, plus heatmap)
- Timer integration with automatic session recording and interruption dialog
- Achievement system with 8 milestone badges
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 17 comments.
Show a summary per file
| File | Description |
|---|---|
src/renderer/utils/StatisticsStore.js |
New data persistence layer managing pomodoro session records with CRUD operations and JSON storage |
src/renderer/utils/StatisticsAnalyzer.js |
New analytics service providing multi-dimensional statistical analysis (day/week/month/history/heatmap) |
src/renderer/store/modules/Statistics.js |
New Vuex module managing statistics state and actions for session lifecycle |
src/renderer/components/timer/Timer.vue |
Integrated session tracking: creates records on start, marks completion, triggers interrupt dialog on reset |
src/renderer/components/statistics/Stats-day.vue |
Daily statistics view showing completed sessions, timeline, and metrics |
src/renderer/components/statistics/Stats-week.vue |
Weekly statistics with bar chart, daily breakdown, and efficiency insights |
src/renderer/components/statistics/Stats-month.vue |
Monthly calendar heatmap view with streak tracking and activity days |
src/renderer/components/statistics/Stats-history.vue |
Historical overview with cumulative stats, heatmap, and achievement system |
src/renderer/components/statistics/Stats-heatmap.vue |
24×7 hour/day heatmap showing work intensity distribution |
src/renderer/components/statistics/Stats-interruptions.vue |
Interruption analysis showing ranked disruption reasons |
src/renderer/components/InterruptDialog.vue |
New modal dialog for recording interruption reasons with predefined options |
src/renderer/components/drawer/Drawer-statistics.vue |
Statistics drawer component with tabbed navigation between views |
src/renderer/components/drawer/Drawer.vue |
Registered new statistics drawer component |
src/renderer/components/drawer/Drawer-menu.vue |
Added statistics menu icon/entry point |
src/renderer/App.vue |
Added InterruptDialog component to app root |
src/index.ejs |
Added Content Security Policy meta tag (includes unsafe-eval and unsafe-inline) |
docs/STATISTICS_FEATURE.md |
Comprehensive feature documentation with technical details and usage instructions |
docs/STATISTICS_QUICKSTART.md |
Quick start guide with testing procedures and debugging tips |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| while (true) { | ||
| const daySessions = this.store.getSessionsByDate(checkDate) | ||
| const hasCompletedWork = daySessions.some(s => s.type === 'work' && s.completed) | ||
|
|
||
| if (hasCompletedWork) { | ||
| streak++ | ||
| checkDate.setDate(checkDate.getDate() - 1) | ||
| } else { | ||
| // 如果是今天没有记录,继续往前查 | ||
| if (checkDate.toDateString() === today.toDateString()) { | ||
| checkDate.setDate(checkDate.getDate() - 1) | ||
| continue | ||
| } | ||
| break | ||
| } | ||
| } | ||
|
|
||
| return streak | ||
| } |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The calculateCurrentStreak() method contains an infinite while (true) loop that could potentially hang the application if there's an edge case not covered by the break conditions. Consider adding a safety counter or maximum iteration limit to prevent infinite loops.
| <html> | ||
| <head> | ||
| <meta charset="utf-8" /> | ||
| <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:;" /> |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Content Security Policy (CSP) includes 'unsafe-eval' and 'unsafe-inline' for scripts, which significantly weakens the security benefits of CSP. These directives allow inline scripts and eval(), making the application vulnerable to XSS attacks. Consider refactoring to remove inline scripts and avoid eval() to strengthen security, or document why these exceptions are necessary.
| export default { | ||
| state, | ||
| getters, | ||
| mutations, | ||
| actions | ||
| } |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Vuex module doesn't specify namespaced: true, which means all getters, mutations, and actions are registered in the global namespace. This can lead to naming conflicts if other modules use the same names (e.g., currentSession, currentView). Consider adding namespaced: true to the module export or ensuring unique naming across all modules.
| saveData() { | ||
| try { | ||
| fs.writeFileSync(this.path, JSON.stringify(this.data, null, 2)) | ||
| } catch (error) { | ||
| logger.error('Failed to save statistics data:', error) | ||
| } | ||
| } |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The saveData() method is called synchronously after every data modification (create, complete, delete, clear, import). If multiple operations happen in quick succession, this could lead to file system race conditions or data loss, especially on slower storage devices. Consider implementing a debounced save mechanism or using a queue to batch write operations.
| const electron = require('electron') | ||
| const fs = require('fs') | ||
| const path = require('path') |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The require import style is inconsistent with the ES6 import syntax used at the top of the file. Consider using import electron from 'electron', import fs from 'fs', and import path from 'path' for consistency.
| const electron = require('electron') | |
| const fs = require('fs') | |
| const path = require('path') | |
| import electron from 'electron' | |
| import fs from 'fs' | |
| import path from 'path' |
| } | ||
| .BarChart-bar { | ||
| background: linear-gradient(180deg, var(--color-accent) 0%, var(--color-accent-dark, var(--color-accent)) 100%); |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The CSS custom property fallback var(--color-accent-dark, var(--color-accent)) will use --color-accent if --color-accent-dark is not defined. However, this component depends on --color-accent-dark existing. If it doesn't exist in the theme, the gradient will be monochrome. Consider defining a proper fallback color or documenting the theme requirements.
| background: linear-gradient(180deg, var(--color-accent) 0%, var(--color-accent-dark, var(--color-accent)) 100%); | |
| background: linear-gradient(180deg, var(--color-accent) 0%, var(--color-accent-dark, #005fa3) 100%); |
| importFromJSON(jsonString) { | ||
| try { | ||
| const importedData = JSON.parse(jsonString) | ||
| if (importedData.sessions && Array.isArray(importedData.sessions)) { | ||
| this.data = importedData | ||
| this.saveData() | ||
| logger.info('Data imported successfully') | ||
| } | ||
| } catch (error) { | ||
| logger.error('Failed to import data:', error) | ||
| throw new Error('Invalid JSON format') | ||
| } | ||
| } |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing input validation for importFromJSON. If the imported data contains sessions without required fields (e.g., missing id, type, or startTime), this could lead to runtime errors when the data is later used. Consider validating the structure and required fields of imported sessions before accepting the data.
| }) | ||
| EventBus.$on('call-timer-reset', () => { | ||
| this.resetTimer() |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The resetTimer function is called with skipInterrupt = false by default at line 371, which will trigger the interrupt dialog every time the timer is reset, even when called via the 'call-timer-reset' event. This may not be the intended behavior for all reset scenarios. Consider reviewing if all reset paths should trigger the interrupt dialog.
| this.resetTimer() | |
| this.resetTimer(true) |
| background-color: rgba(var(--color-accent-rgb, 76, 175, 80), 0.3); | ||
| } | ||
| &--level-2 { | ||
| background-color: rgba(var(--color-accent-rgb, 76, 175, 80), 0.5); | ||
| } | ||
| &--level-3 { | ||
| background-color: rgba(var(--color-accent-rgb, 76, 175, 80), 0.75); | ||
| } | ||
| &--level-4 { | ||
| background-color: var(--color-accent); |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The heatmap uses rgba(var(--color-accent-rgb, 76, 175, 80), 0.3) syntax which expects the CSS variable --color-accent-rgb to be defined as RGB values without the rgb() wrapper. If this variable is not defined in the theme system, the fallback 76, 175, 80 will be used. Consider documenting this requirement or checking if --color-accent-rgb is consistently defined across all themes.
| // 创建最近7天的测试数据 | ||
| const store = require('@/utils/StatisticsStore').getStatisticsStore() | ||
| const today = new Date() | ||
|
|
||
| for (let day = 0; day < 7; day++) { | ||
| const date = new Date(today) | ||
| date.setDate(today.getDate() - day) | ||
|
|
||
| // 每天随机创建3-8个番茄钟 | ||
| const count = Math.floor(Math.random() * 6) + 3 | ||
|
|
||
| for (let i = 0; i < count; i++) { | ||
| const hour = Math.floor(Math.random() * 12) + 8 // 8-20点 | ||
| const session = { | ||
| id: Math.random().toString(36).substr(2, 9), | ||
| type: 'work', | ||
| duration: 25, | ||
| startTime: new Date(date.setHours(hour, 0, 0, 0)).toISOString(), | ||
| endTime: new Date(date.setHours(hour, 25, 0, 0)).toISOString(), | ||
| completed: Math.random() > 0.2, // 80%完成率 | ||
| interrupted: Math.random() < 0.2, | ||
| interruptReason: Math.random() < 0.2 ? ['紧急事项', '会议', '电话'][Math.floor(Math.random() * 3)] : null, | ||
| taskName: null, | ||
| taskId: null | ||
| } | ||
| store.data.sessions.push(session) | ||
| } | ||
| } | ||
|
|
||
| store.saveData() | ||
| console.log('测试数据已生成!刷新统计页面查看。') |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The quick start guide provides example code that directly manipulates the store's internal data structure (store.data.sessions.push(session)), bypassing the public API methods like createSession(). This violates encapsulation and could lead to data inconsistencies. The example should use the public API: store.createSession(sessionData) followed by store.completeSession(session.id, completed, interruptReason).
Features:
Files: