├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── habitTracker.js ├── index.html └── styles.css /.gitignore: -------------------------------------------------------------------------------- 1 | # System Files 2 | .DS_Store 3 | Thumbs.db 4 | 5 | # IDE Files 6 | .idea/ 7 | .vscode/ 8 | *.sublime-project 9 | *.sublime-workspace 10 | 11 | # Logs 12 | *.log 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Habit Tracker Wheel 2 | 3 | We love your input! We want to make contributing to Habit Tracker Wheel as easy and transparent as possible, whether it's: 4 | 5 | - Reporting a bug 6 | - Discussing the current state of the code 7 | - Submitting a fix 8 | - Proposing new features 9 | - Becoming a maintainer 10 | 11 | ## We Develop with Github 12 | We use Github to host code, to track issues and feature requests, as well as accept pull requests. 13 | 14 | ## Pull Requests 15 | 1. Fork the repo and create your branch from `main`. 16 | 2. If you've added code that should be tested, add tests. 17 | 3. If you've changed APIs, update the documentation. 18 | 4. Ensure the test suite passes. 19 | 5. Make sure your code lints. 20 | 6. Issue that pull request! 21 | 22 | ## Any contributions you make will be under the MIT Software License 23 | In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern. 24 | 25 | ## Report bugs using Github's [issue tracker](https://github.com/yourusername/habit-tracker-wheel/issues) 26 | We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/yourusername/habit-tracker-wheel/issues/new). 27 | 28 | ## Write bug reports with detail, background, and sample code 29 | 30 | **Great Bug Reports** tend to have: 31 | 32 | - A quick summary and/or background 33 | - Steps to reproduce 34 | - Be specific! 35 | - Give sample code if you can. 36 | - What you expected would happen 37 | - What actually happens 38 | - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) 39 | 40 | ## License 41 | By contributing, you agree that your contributions will be licensed under its MIT License. 42 | 43 | ## References 44 | This document was adapted from the open-source contribution guidelines for [Facebook's Draft](https://github.com/facebook/draft-js/blob/a9316a723f9e918afde44dea68b5f9f39b7d9b00/CONTRIBUTING.md). 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Anshul Mittal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Habit Tracker Wheel 🎯 2 | 3 | A visually appealing habit tracking application featuring a unique wheel-based interface to help you build and maintain daily, weekly, and monthly habits. 4 | 5 |  6 | 7 | ## Features ✨ 8 | 9 | - **Visual Habit Wheel**: Track daily habits with an intuitive circular interface 10 | - **Multiple Habit Types**: Support for daily, weekly, and monthly habits 11 | - **Progress Tracking**: Visual progress bars and completion percentages 12 | - **Data Management**: 13 | - Local storage for persistent data 14 | - Import/Export functionality via CSV 15 | - **Responsive Design**: Works on both desktop and mobile devices 16 | - **Monthly Navigation**: Easy switching between different months 17 | - **Color-coded Habits**: Visual distinction between different habits 18 | 19 | ## Getting Started 🌟 20 | 21 | 1. Clone the repository: 22 | ``` 23 | git clone https://github.com/anshulmittal712/habit-tracker.git 24 | ``` 25 | 26 | 27 | 2. Open `index.html` in your browser 28 | 29 | That's it! No build process or dependencies required. 30 | 31 | ## Usage 📝 32 | 33 | 1. **Select a Month**: Use the month picker at the top to select your tracking period 34 | 2. **Add Habits**: 35 | - Enter habit names in the respective sections (Daily/Weekly/Monthly) 36 | - Click the '+' button to add them 37 | 3. **Track Progress**: 38 | - Daily habits: Click segments in the wheel 39 | - Weekly habits: Click week boxes (W1-W5) 40 | - Monthly habits: Use the checkbox 41 | 4. **View Progress**: Check the summary section for completion rates 42 | 5. **Data Management**: 43 | - Export: Click 'Export CSV' to save your data 44 | - Import: Click 'Import CSV' to restore previous data 45 | 46 | ## Contributing 🤝 47 | 48 | Contributions are welcome! Please feel free to submit a Pull Request. 49 | 50 | 1. Fork the repository 51 | 2. Create your feature branch (`git checkout -b feature/AmazingFeature`) 52 | 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) 53 | 4. Push to the branch (`git push origin feature/AmazingFeature`) 54 | 5. Open a Pull Request 55 | 56 | ## License 📄 57 | 58 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 59 | 60 | ## Acknowledgments 🙏 61 | 62 | - Built with vanilla JavaScript, HTML, and CSS 63 | - Uses SVG for the wheel visualization 64 | -------------------------------------------------------------------------------- /habitTracker.js: -------------------------------------------------------------------------------- 1 | class HabitTracker { 2 | constructor() { 3 | this.state = { 4 | month: '', 5 | dailyHabits: [], 6 | weeklyHabits: [], 7 | monthlyHabits: [], 8 | dailyProgress: {}, // Format: {habitIndex: [day1, day2, ...]} 9 | weeklyProgress: {}, // Format: {habitIndex: [week1, week2, ...]} 10 | monthlyProgress: {} // Format: {habitIndex: boolean} 11 | }; 12 | 13 | this.daysInMonth = 31; // Default value 14 | this.habitColors = [ 15 | '#007bff', // blue 16 | '#28a745', // green 17 | '#dc3545', // red 18 | '#ffc107', // yellow 19 | '#17a2b8', // cyan 20 | '#6f42c1', // purple 21 | '#fd7e14', // orange 22 | ]; 23 | 24 | this.init(); 25 | this.loadFromLocalStorage(); 26 | } 27 | 28 | init() { 29 | // Initialize month input 30 | const monthInput = document.getElementById('monthInput'); 31 | monthInput.addEventListener('change', (e) => { 32 | this.state.month = e.target.value; 33 | // Update days in month when month changes 34 | const [year, month] = e.target.value.split('-'); 35 | this.daysInMonth = new Date(year, month, 0).getDate(); 36 | this.drawWheel(); 37 | this.updateSummary(); 38 | this.saveToLocalStorage(); 39 | }); 40 | 41 | // Initialize habit input handlers 42 | this.initializeHabitInputs(); 43 | 44 | // Initialize wheel 45 | this.drawWheel(); 46 | 47 | // Initialize import/export 48 | this.initializeImportExport(); 49 | 50 | // Initialize summary 51 | this.updateSummary(); 52 | } 53 | 54 | initializeHabitInputs() { 55 | // Add habit button handlers 56 | document.querySelectorAll('.add-habit').forEach(button => { 57 | button.addEventListener('click', (e) => { 58 | const container = e.target.closest('.habit-input').parentElement; 59 | const input = container.querySelector('input'); 60 | // Determine habit type based on parent container 61 | const habitType = container.closest('.daily-habits') ? 'daily' : 62 | container.closest('.weekly-habits') ? 'weekly' : 'monthly'; 63 | 64 | if (input.value.trim()) { 65 | this.addHabit(habitType, input.value.trim()); 66 | input.value = ''; 67 | } 68 | }); 69 | }); 70 | } 71 | 72 | addHabit(type, name) { 73 | const habitList = `${type}Habits`; 74 | this.state[habitList].push(name); 75 | this.renderHabits(); 76 | this.saveToLocalStorage(); 77 | } 78 | 79 | drawWheel() { 80 | const svg = document.getElementById('habitWheel'); 81 | svg.innerHTML = ''; 82 | 83 | // Calculate dimensions 84 | const centerX = 300; 85 | const centerY = 250; 86 | const radius = 160; 87 | const startAngleOffset = 0; // Start from top (-90 degrees) 88 | const endAngleOffset = 270; // End at bottom-left (180 degrees) 89 | 90 | // Create a group for each habit 91 | this.state.dailyHabits.forEach((habit, habitIndex) => { 92 | const habitGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g'); 93 | habitGroup.setAttribute('class', 'habit-ring'); 94 | habitGroup.setAttribute('data-habit', habitIndex); 95 | 96 | // Calculate radius for this habit ring 97 | const habitRadius = radius - (habitIndex * 30); 98 | 99 | // Get habit color 100 | const habitColor = this.habitColors[habitIndex % this.habitColors.length]; 101 | 102 | // Draw segments for each day 103 | for (let day = 1; day <= this.daysInMonth; day++) { 104 | const angleRange = endAngleOffset - startAngleOffset; 105 | const startAngle = startAngleOffset + ((day - 1) * (angleRange / this.daysInMonth)); 106 | const endAngle = startAngleOffset + (day * (angleRange / this.daysInMonth)); 107 | 108 | // Calculate path coordinates 109 | const path = this.describeArc(centerX, centerY, habitRadius, startAngle, endAngle); 110 | 111 | // Create segment 112 | const segment = document.createElementNS('http://www.w3.org/2000/svg', 'path'); 113 | segment.setAttribute('d', path); 114 | segment.setAttribute('class', 'wheel-segment'); 115 | segment.setAttribute('data-day', day); 116 | 117 | segment.style.fill = this.state.dailyProgress[habitIndex]?.includes(day) 118 | ? habitColor 119 | : '#f0f0f0'; 120 | segment.style.stroke = habitColor; 121 | 122 | if (this.state.dailyProgress[habitIndex]?.includes(day)) { 123 | segment.classList.add('completed'); 124 | } 125 | 126 | segment.addEventListener('click', () => this.toggleDailyHabit(habitIndex, day)); 127 | habitGroup.appendChild(segment); 128 | } 129 | 130 | // Add habit label in the second quadrant (top-left) 131 | const labelX = 100; 132 | const labelY = 100 + (habitIndex * 30); 133 | 134 | // Add colored line connecting label to habit ring 135 | const connectingLine = document.createElementNS('http://www.w3.org/2000/svg', 'line'); 136 | connectingLine.setAttribute('x1', labelX); 137 | connectingLine.setAttribute('y1', labelY+5); 138 | connectingLine.setAttribute('x2', centerX); 139 | connectingLine.setAttribute('y2', labelY+5); 140 | connectingLine.setAttribute('stroke', habitColor); 141 | connectingLine.setAttribute('stroke-width', '1.5'); 142 | connectingLine.setAttribute('class', 'connecting-line'); 143 | habitGroup.appendChild(connectingLine); 144 | 145 | const habitLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text'); 146 | habitLabel.setAttribute('x', labelX); 147 | habitLabel.setAttribute('y', labelY); 148 | habitLabel.setAttribute('class', 'habit-wheel-label'); 149 | habitLabel.setAttribute('text-anchor', 'start'); 150 | habitLabel.style.fill = habitColor; 151 | habitLabel.textContent = habit; 152 | 153 | habitGroup.appendChild(habitLabel); 154 | 155 | svg.appendChild(habitGroup); 156 | }); 157 | 158 | // Add date numbers around the wheel 159 | for (let day = 1; day <= this.daysInMonth; day++) { 160 | const angleRange = endAngleOffset - startAngleOffset; 161 | const angle = startAngleOffset + ((day - 1) * (angleRange / this.daysInMonth)); 162 | const dateRadius = radius + 15; 163 | const position = this.polarToCartesian(centerX, centerY, dateRadius, angle); 164 | 165 | const text = document.createElementNS('http://www.w3.org/2000/svg', 'text'); 166 | text.setAttribute('x', position.x); 167 | text.setAttribute('y', position.y); 168 | text.setAttribute('text-anchor', 'middle'); 169 | text.setAttribute('alignment-baseline', 'middle'); 170 | text.setAttribute('class', 'date-label'); 171 | text.textContent = day; 172 | 173 | svg.appendChild(text); 174 | } 175 | } 176 | 177 | describeArc(x, y, radius, startAngle, endAngle) { 178 | const start = this.polarToCartesian(x, y, radius, endAngle); 179 | const end = this.polarToCartesian(x, y, radius, startAngle); 180 | const largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1"; 181 | 182 | return [ 183 | "M", start.x, start.y, 184 | "A", radius, radius, 0, largeArcFlag, 0, end.x, end.y, 185 | "L", x, y, 186 | "Z" 187 | ].join(" "); 188 | } 189 | 190 | polarToCartesian(centerX, centerY, radius, angleInDegrees) { 191 | const angleInRadians = (angleInDegrees - 90) * Math.PI / 180.0; 192 | return { 193 | x: centerX + (radius * Math.cos(angleInRadians)), 194 | y: centerY + (radius * Math.sin(angleInRadians)) 195 | }; 196 | } 197 | 198 | saveToLocalStorage() { 199 | localStorage.setItem('habitTrackerState', JSON.stringify(this.state)); 200 | } 201 | 202 | loadFromLocalStorage() { 203 | const saved = localStorage.getItem('habitTrackerState'); 204 | if (saved) { 205 | this.state = JSON.parse(saved); 206 | this.renderHabits(); 207 | if (this.state.month) { 208 | document.getElementById('monthInput').value = this.state.month; 209 | } 210 | } 211 | } 212 | 213 | exportToCsv() { 214 | // Implementation for CSV export 215 | const csv = this.convertStateToCSV(); 216 | const blob = new Blob([csv], { type: 'text/csv' }); 217 | const url = window.URL.createObjectURL(blob); 218 | const a = document.createElement('a'); 219 | a.setAttribute('hidden', ''); 220 | a.setAttribute('href', url); 221 | a.setAttribute('download', `habit-tracker-${this.state.month || 'export'}.csv`); 222 | document.body.appendChild(a); 223 | a.click(); 224 | document.body.removeChild(a); 225 | } 226 | 227 | convertStateToCSV() { 228 | // Implementation for state to CSV conversion 229 | // This is a basic implementation - you might want to enhance it 230 | return JSON.stringify(this.state); 231 | } 232 | 233 | importFromCsv(file) { 234 | const reader = new FileReader(); 235 | reader.onload = (e) => { 236 | try { 237 | this.state = JSON.parse(e.target.result); 238 | this.saveToLocalStorage(); 239 | this.renderHabits(); 240 | if (this.state.month) { 241 | document.getElementById('monthInput').value = this.state.month; 242 | } 243 | } catch (error) { 244 | console.error('Error importing file:', error); 245 | alert('Invalid file format'); 246 | } 247 | }; 248 | reader.readAsText(file); 249 | } 250 | 251 | initializeImportExport() { 252 | document.getElementById('exportBtn').addEventListener('click', () => this.exportToCsv()); 253 | 254 | const importBtn = document.getElementById('importBtn'); 255 | const importInput = document.getElementById('importInput'); 256 | 257 | importBtn.addEventListener('click', () => importInput.click()); 258 | importInput.addEventListener('change', (e) => { 259 | if (e.target.files.length > 0) { 260 | this.importFromCsv(e.target.files[0]); 261 | } 262 | }); 263 | } 264 | 265 | renderHabits() { 266 | // Render daily habits 267 | this.renderHabitList('daily'); 268 | 269 | // Render weekly habits 270 | this.renderHabitList('weekly'); 271 | 272 | // Render monthly habits 273 | this.renderHabitList('monthly'); 274 | 275 | // Update summary 276 | this.updateSummary(); 277 | } 278 | 279 | renderHabitList(type) { 280 | // Skip rendering for daily habits as they're handled in the summary 281 | if (type === 'daily') { 282 | this.drawWheel(); 283 | return; 284 | } 285 | 286 | const container = document.getElementById(`${type}HabitsContainer`); 287 | const habitList = this.state[`${type}Habits`]; 288 | const progress = this.state[`${type}Progress`]; 289 | 290 | // Clear existing habits (except the input) 291 | const inputDiv = container.querySelector('.habit-input'); 292 | container.innerHTML = ''; 293 | container.appendChild(inputDiv); 294 | 295 | // Add each habit 296 | habitList.forEach((habit, index) => { 297 | const habitDiv = document.createElement('div'); 298 | habitDiv.className = 'habit-item'; 299 | 300 | // Add delete button 301 | const deleteBtn = document.createElement('button'); 302 | deleteBtn.className = 'delete-habit'; 303 | deleteBtn.innerHTML = '×'; 304 | deleteBtn.addEventListener('click', () => this.deleteHabit(type, index)); 305 | 306 | if (type === 'weekly') { 307 | // Create a span for the habit name and a container for week boxes 308 | habitDiv.innerHTML = ` 309 | 312 |