Compare commits
2 Commits
433477e2c6
...
cee20f6bb3
| Author | SHA1 | Date | |
|---|---|---|---|
|
cee20f6bb3
|
|||
|
eda87a8231
|
@@ -1,69 +0,0 @@
|
|||||||
# Formatter Replacement Progress
|
|
||||||
|
|
||||||
## Components Completed ✅
|
|
||||||
1. DebtBreakdown.svelte - Replaced formatCurrency function
|
|
||||||
2. EnhancedBalance.svelte - Replaced formatCurrency function (with Math.abs wrapper)
|
|
||||||
|
|
||||||
## Remaining Files to Update
|
|
||||||
|
|
||||||
### Components (3 files)
|
|
||||||
- [ ] PaymentModal.svelte - Has formatCurrency function
|
|
||||||
- [ ] SplitMethodSelector.svelte - Has inline .toFixed() calls
|
|
||||||
- [ ] BarChart.svelte - Has inline .toFixed() calls
|
|
||||||
- [ ] IngredientsPage.svelte - Has .toFixed() for recipe calculations
|
|
||||||
|
|
||||||
### Cospend Pages (7 files)
|
|
||||||
- [ ] routes/cospend/+page.svelte - Has formatCurrency function
|
|
||||||
- [ ] routes/cospend/payments/+page.svelte - Has formatCurrency function
|
|
||||||
- [ ] routes/cospend/payments/view/[id]/+page.svelte - Has formatCurrency and .toFixed()
|
|
||||||
- [ ] routes/cospend/payments/add/+page.svelte - Has .toFixed() and .toLocaleString()
|
|
||||||
- [ ] routes/cospend/payments/edit/[id]/+page.svelte - Has multiple .toFixed() calls
|
|
||||||
- [ ] routes/cospend/recurring/+page.svelte - Has formatCurrency function
|
|
||||||
- [ ] routes/cospend/recurring/edit/[id]/+page.svelte - Has .toFixed() and .toLocaleString()
|
|
||||||
- [ ] routes/cospend/settle/+page.svelte - Has formatCurrency function
|
|
||||||
|
|
||||||
## Replacement Strategy
|
|
||||||
|
|
||||||
### Pattern 1: Identical formatCurrency functions
|
|
||||||
```typescript
|
|
||||||
// OLD
|
|
||||||
function formatCurrency(amount) {
|
|
||||||
return new Intl.NumberFormat('de-CH', {
|
|
||||||
style: 'currency',
|
|
||||||
currency: 'CHF'
|
|
||||||
}).format(amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
// NEW
|
|
||||||
import { formatCurrency } from '$lib/utils/formatters';
|
|
||||||
// Usage: formatCurrency(amount, 'CHF', 'de-CH')
|
|
||||||
```
|
|
||||||
|
|
||||||
### Pattern 2: .toFixed() for currency display
|
|
||||||
```typescript
|
|
||||||
// OLD
|
|
||||||
{payment.amount.toFixed(2)}
|
|
||||||
|
|
||||||
// NEW
|
|
||||||
import { formatNumber } from '$lib/utils/formatters';
|
|
||||||
{formatNumber(payment.amount, 2, 'de-CH')}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Pattern 3: .toLocaleString() for dates
|
|
||||||
```typescript
|
|
||||||
// OLD
|
|
||||||
nextDate.toLocaleString('de-CH', { weekday: 'long', ... })
|
|
||||||
|
|
||||||
// NEW
|
|
||||||
import { formatDateTime } from '$lib/utils/formatters';
|
|
||||||
formatDateTime(nextDate, 'de-CH', { weekday: 'long', ... })
|
|
||||||
```
|
|
||||||
|
|
||||||
### Pattern 4: Exchange rate display (4 decimals)
|
|
||||||
```typescript
|
|
||||||
// OLD
|
|
||||||
{exchangeRate.toFixed(4)}
|
|
||||||
|
|
||||||
// NEW
|
|
||||||
{formatNumber(exchangeRate, 4, 'de-CH')}
|
|
||||||
```
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Script to replace inline formatCurrency functions with shared formatter utilities
|
|
||||||
"""
|
|
||||||
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
files_to_update = [
|
|
||||||
"/home/alex/.local/src/homepage/src/routes/cospend/+page.svelte",
|
|
||||||
"/home/alex/.local/src/homepage/src/routes/cospend/payments/+page.svelte",
|
|
||||||
"/home/alex/.local/src/homepage/src/routes/cospend/payments/view/[id]/+page.svelte",
|
|
||||||
"/home/alex/.local/src/homepage/src/routes/cospend/recurring/+page.svelte",
|
|
||||||
"/home/alex/.local/src/homepage/src/routes/cospend/settle/+page.svelte",
|
|
||||||
]
|
|
||||||
|
|
||||||
def process_file(filepath):
|
|
||||||
print(f"Processing: {filepath}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(filepath, 'r', encoding='utf-8') as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
original_content = content
|
|
||||||
|
|
||||||
# Check if already has the import
|
|
||||||
has_formatter_import = 'from \'$lib/utils/formatters\'' in content or 'from "$lib/utils/formatters"' in content
|
|
||||||
|
|
||||||
# Find the <script> tag
|
|
||||||
script_match = re.search(r'(<script[^>]*>)', content)
|
|
||||||
if not script_match:
|
|
||||||
print(f" ⚠️ No <script> tag found")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Add import if not present
|
|
||||||
if not has_formatter_import:
|
|
||||||
script_tag = script_match.group(1)
|
|
||||||
# Find where to insert (after <script> tag)
|
|
||||||
script_end = script_match.end()
|
|
||||||
|
|
||||||
# Get existing imports to find the right place
|
|
||||||
imports_section_match = re.search(r'<script[^>]*>(.*?)(?:\n\n|\n export|\n let)', content, re.DOTALL)
|
|
||||||
if imports_section_match:
|
|
||||||
imports_end = imports_section_match.end() - len(imports_section_match.group(0).split('\n')[-1])
|
|
||||||
insert_pos = imports_end
|
|
||||||
else:
|
|
||||||
insert_pos = script_end
|
|
||||||
|
|
||||||
new_import = "\n import { formatCurrency } from '$lib/utils/formatters';"
|
|
||||||
content = content[:insert_pos] + new_import + content[insert_pos:]
|
|
||||||
print(f" ✓ Added import")
|
|
||||||
|
|
||||||
# Remove the formatCurrency function definition
|
|
||||||
# Pattern for the function with different variations
|
|
||||||
patterns = [
|
|
||||||
r'\n function formatCurrency\(amount\) \{\n return new Intl\.NumberFormat\(\'de-CH\',\s*\{\n\s*style:\s*\'currency\',\n\s*currency:\s*\'CHF\'\n\s*\}\)\.format\(amount\);\n \}',
|
|
||||||
r'\n function formatCurrency\(amount,\s*currency\s*=\s*\'CHF\'\) \{\n return new Intl\.NumberFormat\(\'de-CH\',\s*\{\n\s*style:\s*\'currency\',\n\s*currency:\s*currency\n\s*\}\)\.format\(amount\);\n \}',
|
|
||||||
]
|
|
||||||
|
|
||||||
for pattern in patterns:
|
|
||||||
if re.search(pattern, content):
|
|
||||||
content = re.sub(pattern, '', content)
|
|
||||||
print(f" ✓ Removed formatCurrency function")
|
|
||||||
break
|
|
||||||
|
|
||||||
# Check if content changed
|
|
||||||
if content != original_content:
|
|
||||||
with open(filepath, 'w', encoding='utf-8') as f:
|
|
||||||
f.write(content)
|
|
||||||
print(f" ✅ Updated successfully")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print(f" ⚠️ No changes needed")
|
|
||||||
return False
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ❌ Error: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def main():
|
|
||||||
print("=" * 60)
|
|
||||||
print("Replacing formatCurrency functions with shared utilities")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
success_count = 0
|
|
||||||
for filepath in files_to_update:
|
|
||||||
if process_file(filepath):
|
|
||||||
success_count += 1
|
|
||||||
print()
|
|
||||||
|
|
||||||
print("=" * 60)
|
|
||||||
print(f"Summary: {success_count}/{len(files_to_update)} files updated")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,205 +0,0 @@
|
|||||||
import { dbConnect } from '../src/utils/db';
|
|
||||||
import { Exercise } from '../src/models/Exercise';
|
|
||||||
|
|
||||||
// ExerciseDB API configuration
|
|
||||||
const RAPIDAPI_KEY = process.env.RAPIDAPI_KEY || 'your-rapidapi-key-here';
|
|
||||||
const RAPIDAPI_HOST = 'exercisedb.p.rapidapi.com';
|
|
||||||
const BASE_URL = 'https://exercisedb.p.rapidapi.com';
|
|
||||||
|
|
||||||
interface ExerciseDBExercise {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
gifUrl: string;
|
|
||||||
bodyPart: string;
|
|
||||||
equipment: string;
|
|
||||||
target: string;
|
|
||||||
secondaryMuscles: string[];
|
|
||||||
instructions: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchFromExerciseDB(endpoint: string): Promise<any> {
|
|
||||||
const response = await fetch(`${BASE_URL}${endpoint}`, {
|
|
||||||
headers: {
|
|
||||||
'X-RapidAPI-Key': RAPIDAPI_KEY,
|
|
||||||
'X-RapidAPI-Host': RAPIDAPI_HOST
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to fetch from ExerciseDB: ${response.status} ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function scrapeAllExercises(): Promise<void> {
|
|
||||||
console.log('🚀 Starting ExerciseDB scraping...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await dbConnect();
|
|
||||||
console.log('✅ Connected to database');
|
|
||||||
|
|
||||||
// Fetch all exercises
|
|
||||||
console.log('📡 Fetching exercises from ExerciseDB...');
|
|
||||||
const exercises: ExerciseDBExercise[] = await fetchFromExerciseDB('/exercises?limit=2000');
|
|
||||||
|
|
||||||
console.log(`📊 Found ${exercises.length} exercises`);
|
|
||||||
|
|
||||||
let imported = 0;
|
|
||||||
let skipped = 0;
|
|
||||||
let errors = 0;
|
|
||||||
|
|
||||||
for (const exercise of exercises) {
|
|
||||||
try {
|
|
||||||
// Check if exercise already exists
|
|
||||||
const existingExercise = await Exercise.findOne({ exerciseId: exercise.id });
|
|
||||||
|
|
||||||
if (existingExercise) {
|
|
||||||
skipped++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine difficulty based on equipment and complexity
|
|
||||||
let difficulty: 'beginner' | 'intermediate' | 'advanced' = 'intermediate';
|
|
||||||
|
|
||||||
if (exercise.equipment === 'body weight') {
|
|
||||||
difficulty = 'beginner';
|
|
||||||
} else if (exercise.equipment.includes('barbell') || exercise.equipment.includes('olympic')) {
|
|
||||||
difficulty = 'advanced';
|
|
||||||
} else if (exercise.equipment.includes('dumbbell') || exercise.equipment.includes('cable')) {
|
|
||||||
difficulty = 'intermediate';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new exercise
|
|
||||||
const newExercise = new Exercise({
|
|
||||||
exerciseId: exercise.id,
|
|
||||||
name: exercise.name,
|
|
||||||
gifUrl: exercise.gifUrl,
|
|
||||||
bodyPart: exercise.bodyPart.toLowerCase(),
|
|
||||||
equipment: exercise.equipment.toLowerCase(),
|
|
||||||
target: exercise.target.toLowerCase(),
|
|
||||||
secondaryMuscles: exercise.secondaryMuscles.map(m => m.toLowerCase()),
|
|
||||||
instructions: exercise.instructions,
|
|
||||||
difficulty,
|
|
||||||
isActive: true
|
|
||||||
});
|
|
||||||
|
|
||||||
await newExercise.save();
|
|
||||||
imported++;
|
|
||||||
|
|
||||||
if (imported % 100 === 0) {
|
|
||||||
console.log(`⏳ Imported ${imported} exercises...`);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`❌ Error importing exercise ${exercise.name}:`, error);
|
|
||||||
errors++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('✅ Scraping completed!');
|
|
||||||
console.log(`📈 Summary: ${imported} imported, ${skipped} skipped, ${errors} errors`);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('💥 Scraping failed:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateExistingExercises(): Promise<void> {
|
|
||||||
console.log('🔄 Updating existing exercises...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await dbConnect();
|
|
||||||
|
|
||||||
const exercises: ExerciseDBExercise[] = await fetchFromExerciseDB('/exercises?limit=2000');
|
|
||||||
|
|
||||||
let updated = 0;
|
|
||||||
|
|
||||||
for (const exercise of exercises) {
|
|
||||||
try {
|
|
||||||
const existingExercise = await Exercise.findOne({ exerciseId: exercise.id });
|
|
||||||
|
|
||||||
if (existingExercise) {
|
|
||||||
// Update with new data from API
|
|
||||||
existingExercise.name = exercise.name;
|
|
||||||
existingExercise.gifUrl = exercise.gifUrl;
|
|
||||||
existingExercise.bodyPart = exercise.bodyPart.toLowerCase();
|
|
||||||
existingExercise.equipment = exercise.equipment.toLowerCase();
|
|
||||||
existingExercise.target = exercise.target.toLowerCase();
|
|
||||||
existingExercise.secondaryMuscles = exercise.secondaryMuscles.map(m => m.toLowerCase());
|
|
||||||
existingExercise.instructions = exercise.instructions;
|
|
||||||
|
|
||||||
await existingExercise.save();
|
|
||||||
updated++;
|
|
||||||
|
|
||||||
if (updated % 100 === 0) {
|
|
||||||
console.log(`⏳ Updated ${updated} exercises...`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`❌ Error updating exercise ${exercise.name}:`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`✅ Updated ${updated} exercises`);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('💥 Update failed:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getExerciseStats(): Promise<void> {
|
|
||||||
try {
|
|
||||||
await dbConnect();
|
|
||||||
|
|
||||||
const totalExercises = await Exercise.countDocuments();
|
|
||||||
const activeExercises = await Exercise.countDocuments({ isActive: true });
|
|
||||||
|
|
||||||
const bodyParts = await Exercise.distinct('bodyPart');
|
|
||||||
const equipment = await Exercise.distinct('equipment');
|
|
||||||
const targets = await Exercise.distinct('target');
|
|
||||||
|
|
||||||
console.log('📊 Exercise Database Stats:');
|
|
||||||
console.log(` Total exercises: ${totalExercises}`);
|
|
||||||
console.log(` Active exercises: ${activeExercises}`);
|
|
||||||
console.log(` Body parts: ${bodyParts.length} (${bodyParts.join(', ')})`);
|
|
||||||
console.log(` Equipment types: ${equipment.length}`);
|
|
||||||
console.log(` Target muscles: ${targets.length}`);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('💥 Stats failed:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CLI interface
|
|
||||||
const command = process.argv[2];
|
|
||||||
|
|
||||||
switch (command) {
|
|
||||||
case 'scrape':
|
|
||||||
scrapeAllExercises()
|
|
||||||
.then(() => process.exit(0))
|
|
||||||
.catch(() => process.exit(1));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'update':
|
|
||||||
updateExistingExercises()
|
|
||||||
.then(() => process.exit(0))
|
|
||||||
.catch(() => process.exit(1));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'stats':
|
|
||||||
getExerciseStats()
|
|
||||||
.then(() => process.exit(0))
|
|
||||||
.catch(() => process.exit(1));
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
console.log('Usage: tsx scripts/scrape-exercises.ts [command]');
|
|
||||||
console.log('Commands:');
|
|
||||||
console.log(' scrape - Import all exercises from ExerciseDB');
|
|
||||||
console.log(' update - Update existing exercises with latest data');
|
|
||||||
console.log(' stats - Show database statistics');
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Script to update formatCurrency calls to include CHF and de-CH parameters
|
|
||||||
"""
|
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
files_to_update = [
|
|
||||||
"/home/alex/.local/src/homepage/src/routes/cospend/+page.svelte",
|
|
||||||
"/home/alex/.local/src/homepage/src/routes/cospend/payments/+page.svelte",
|
|
||||||
"/home/alex/.local/src/homepage/src/routes/cospend/payments/view/[id]/+page.svelte",
|
|
||||||
"/home/alex/.local/src/homepage/src/routes/cospend/recurring/+page.svelte",
|
|
||||||
"/home/alex/.local/src/homepage/src/routes/cospend/settle/+page.svelte",
|
|
||||||
]
|
|
||||||
|
|
||||||
def process_file(filepath):
|
|
||||||
print(f"Processing: {filepath}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(filepath, 'r', encoding='utf-8') as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
original_content = content
|
|
||||||
changes = 0
|
|
||||||
|
|
||||||
# Pattern 1: formatCurrency(amount) -> formatCurrency(amount, 'CHF', 'de-CH')
|
|
||||||
# But skip if already has parameters
|
|
||||||
def replace_single_param(match):
|
|
||||||
amount = match.group(1)
|
|
||||||
# Check if amount already contains currency parameter (contains comma followed by quote)
|
|
||||||
if ", '" in amount or ', "' in amount:
|
|
||||||
return match.group(0) # Already has parameters, skip
|
|
||||||
return f"formatCurrency({amount}, 'CHF', 'de-CH')"
|
|
||||||
|
|
||||||
content, count1 = re.subn(
|
|
||||||
r'formatCurrency\(([^)]+)\)',
|
|
||||||
replace_single_param,
|
|
||||||
content
|
|
||||||
)
|
|
||||||
changes += count1
|
|
||||||
|
|
||||||
if changes > 0:
|
|
||||||
with open(filepath, 'w', encoding='utf-8') as f:
|
|
||||||
f.write(content)
|
|
||||||
print(f" ✅ Updated {changes} formatCurrency calls")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print(f" ⚠️ No changes needed")
|
|
||||||
return False
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ❌ Error: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
return False
|
|
||||||
|
|
||||||
def main():
|
|
||||||
print("=" * 60)
|
|
||||||
print("Updating formatCurrency calls with CHF and de-CH params")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
success_count = 0
|
|
||||||
for filepath in files_to_update:
|
|
||||||
if process_file(filepath):
|
|
||||||
success_count += 1
|
|
||||||
print()
|
|
||||||
|
|
||||||
print("=" * 60)
|
|
||||||
print(f"Summary: {success_count}/{len(files_to_update)} files updated")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -120,13 +120,20 @@ export const GET: RequestHandler = async ({ locals }) => {
|
|||||||
upper: totalKcal + combinedMargin,
|
upper: totalKcal + combinedMargin,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Fetch extra measurements beyond the display limit to fill the SMA lookback window
|
||||||
|
const DISPLAY_LIMIT = 30;
|
||||||
|
const SMA_LOOKBACK = 6; // w - 1 where w = 7 max
|
||||||
const weightMeasurements = await BodyMeasurement.find(
|
const weightMeasurements = await BodyMeasurement.find(
|
||||||
{ createdBy: user.nickname, weight: { $ne: null } },
|
{ createdBy: user.nickname, weight: { $ne: null } },
|
||||||
{ date: 1, weight: 1, _id: 0 }
|
{ date: 1, weight: 1, _id: 0 }
|
||||||
)
|
)
|
||||||
.sort({ date: 1 })
|
.sort({ date: -1 })
|
||||||
.limit(30)
|
.limit(DISPLAY_LIMIT + SMA_LOOKBACK)
|
||||||
.lean();
|
.lean();
|
||||||
|
weightMeasurements.reverse(); // back to chronological order
|
||||||
|
|
||||||
|
// Split into lookback-only (not displayed) and display portions
|
||||||
|
const displayStart = Math.max(0, weightMeasurements.length - DISPLAY_LIMIT);
|
||||||
|
|
||||||
// Build chart-ready workouts-per-week with filled gaps
|
// Build chart-ready workouts-per-week with filled gaps
|
||||||
const weekMap = new Map<string, number>();
|
const weekMap = new Map<string, number>();
|
||||||
@@ -161,39 +168,38 @@ export const GET: RequestHandler = async ({ locals }) => {
|
|||||||
upper: (number | null)[];
|
upper: (number | null)[];
|
||||||
lower: (number | null)[];
|
lower: (number | null)[];
|
||||||
} = { labels: [], dates: [], data: [], sma: [], upper: [], lower: [] };
|
} = { labels: [], dates: [], data: [], sma: [], upper: [], lower: [] };
|
||||||
const weights: number[] = [];
|
const allWeights: number[] = weightMeasurements.map(m => m.weight!);
|
||||||
for (const m of weightMeasurements) {
|
for (let idx = displayStart; idx < weightMeasurements.length; idx++) {
|
||||||
const d = new Date(m.date);
|
const d = new Date(weightMeasurements[idx].date);
|
||||||
weightChart.labels.push(
|
weightChart.labels.push(
|
||||||
d.toLocaleDateString('en', { month: 'short', day: 'numeric' })
|
d.toLocaleDateString('en', { month: 'short', day: 'numeric' })
|
||||||
);
|
);
|
||||||
weightChart.dates.push(d.toISOString());
|
weightChart.dates.push(d.toISOString());
|
||||||
weightChart.data.push(m.weight!);
|
weightChart.data.push(allWeights[idx]);
|
||||||
weights.push(m.weight!);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adaptive window: 7 if enough data, otherwise half the data (min 2)
|
// Adaptive window: 7 if enough data, otherwise half the data (min 2)
|
||||||
const w = Math.min(7, Math.max(2, Math.floor(weights.length / 2)));
|
const w = Math.min(7, Math.max(2, Math.floor(allWeights.length / 2)));
|
||||||
for (let i = 0; i < weights.length; i++) {
|
for (let idx = displayStart; idx < allWeights.length; idx++) {
|
||||||
if (i < w - 1) {
|
// Use full window when available, otherwise use all points so far
|
||||||
weightChart.sma.push(null);
|
const k = Math.min(w, idx + 1);
|
||||||
weightChart.upper.push(null);
|
|
||||||
weightChart.lower.push(null);
|
|
||||||
} else {
|
|
||||||
let sum = 0;
|
let sum = 0;
|
||||||
for (let j = i - w + 1; j <= i; j++) sum += weights[j];
|
for (let j = idx - k + 1; j <= idx; j++) sum += allWeights[j];
|
||||||
const mean = sum / w;
|
const mean = sum / k;
|
||||||
|
|
||||||
let variance = 0;
|
let variance = 0;
|
||||||
for (let j = i - w + 1; j <= i; j++) variance += (weights[j] - mean) ** 2;
|
for (let j = idx - k + 1; j <= idx; j++) variance += (allWeights[j] - mean) ** 2;
|
||||||
const std = Math.sqrt(variance / w);
|
// Bessel's correction (k-1) for unbiased sample variance;
|
||||||
|
// scale by sqrt(w/k) so the band widens when k < w
|
||||||
|
const std = k > 1
|
||||||
|
? Math.sqrt(variance / (k - 1)) * Math.sqrt(w / k)
|
||||||
|
: Math.sqrt(variance) * Math.sqrt(w);
|
||||||
|
|
||||||
const round = (v: number) => Math.round(v * 100) / 100;
|
const round = (v: number) => Math.round(v * 100) / 100;
|
||||||
weightChart.sma.push(round(mean));
|
weightChart.sma.push(round(mean));
|
||||||
weightChart.upper.push(round(mean + std));
|
weightChart.upper.push(round(mean + std));
|
||||||
weightChart.lower.push(round(mean - std));
|
weightChart.lower.push(round(mean - std));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return json({
|
return json({
|
||||||
totalWorkouts,
|
totalWorkouts,
|
||||||
|
|||||||
Reference in New Issue
Block a user